feat(tests): containment fields hidden until detected, required datetime validation
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

This commit is contained in:
kitos
2026-06-25 09:40:28 +02:00
parent d7ad981b89
commit 006bdc60e1
2 changed files with 69 additions and 54 deletions
@@ -148,6 +148,11 @@ export default function TeamTabs({
((role === "blue_lead" || role === "admin") || ((role === "blue_lead" || role === "admin") ||
(role === "blue_tech" && !!test.blue_work_started_at)); (role === "blue_tech" && !!test.blue_work_started_at));
// Containment fields only visible when attack was detected (draft or saved value)
const isDetected = canEditBlue
? blueDraft.detection_result === "detected" || blueDraft.detection_result === "partially_detected"
: test.detection_result === "detected" || test.detection_result === "partially_detected";
// Hint messages shown to operators when editing is locked // Hint messages shown to operators when editing is locked
const redLockedHint = const redLockedHint =
test.state === "draft" && role === "red_tech" test.state === "draft" && role === "red_tech"
@@ -238,11 +243,12 @@ export default function TeamTabs({
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-gray-300"> <label className="mb-1.5 block text-sm font-medium text-gray-300">
Execution Start (UTC) Execution Start (UTC) <span className="text-red-400">*</span>
</label> </label>
{canEditRed ? ( {canEditRed ? (
<input <input
type="datetime-local" type="datetime-local"
required
value={redDraft.execution_start_time} value={redDraft.execution_start_time}
onChange={(e) => onRedFieldChange("execution_start_time", e.target.value)} onChange={(e) => onRedFieldChange("execution_start_time", e.target.value)}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500"
@@ -255,11 +261,12 @@ export default function TeamTabs({
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-gray-300"> <label className="mb-1.5 block text-sm font-medium text-gray-300">
Execution End (UTC) Execution End (UTC) <span className="text-red-400">*</span>
</label> </label>
{canEditRed ? ( {canEditRed ? (
<input <input
type="datetime-local" type="datetime-local"
required
value={redDraft.execution_end_time} value={redDraft.execution_end_time}
onChange={(e) => onRedFieldChange("execution_end_time", e.target.value)} onChange={(e) => onRedFieldChange("execution_end_time", e.target.value)}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500"
@@ -391,63 +398,61 @@ export default function TeamTabs({
)} )}
</div> </div>
{/* Containment Result */} {/* Containment Result — only when detected/partially_detected */}
<div> {isDetected && (
<label className="mb-2 block text-sm font-medium text-gray-300"> <div>
Containment Result <label className="mb-2 block text-sm font-medium text-gray-300">
</label> Containment Result
{canEditBlue ? ( </label>
<div className={`flex flex-wrap gap-2 ${blueDraft.detection_result === "not_detected" ? "opacity-40 pointer-events-none" : ""}`}> {canEditBlue ? (
{CONTAINMENT_RESULTS.map((opt) => ( <div className="flex flex-wrap gap-2">
<button {CONTAINMENT_RESULTS.map((opt) => (
key={opt.value} <button
onClick={() => onBlueFieldChange("containment_result", opt.value)} key={opt.value}
className={`rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${ onClick={() => onBlueFieldChange("containment_result", opt.value)}
blueDraft.containment_result === opt.value className={`rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
? opt.color blueDraft.containment_result === opt.value
: "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600" ? opt.color
}`} : "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
> }`}
{opt.label} >
</button> {opt.label}
))} </button>
{blueDraft.detection_result === "not_detected" && ( ))}
<span className="self-center text-xs text-gray-500">N/A attack not detected</span> </div>
)} ) : (
</div> <p className="text-sm">
) : ( {test.containment_result ? (
<p className="text-sm"> <span
{test.containment_result ? ( className={`inline-flex rounded-full border px-2.5 py-0.5 text-xs font-medium ${
<span test.containment_result === "contained"
className={`inline-flex rounded-full border px-2.5 py-0.5 text-xs font-medium ${ ? "border-green-500/30 bg-green-900/50 text-green-400"
test.containment_result === "contained" : test.containment_result === "not_contained"
? "border-green-500/30 bg-green-900/50 text-green-400" ? "border-red-500/30 bg-red-900/50 text-red-400"
: test.containment_result === "not_contained" : "border-yellow-500/30 bg-yellow-900/50 text-yellow-400"
? "border-red-500/30 bg-red-900/50 text-red-400" }`}
: "border-yellow-500/30 bg-yellow-900/50 text-yellow-400" >
}`} {test.containment_result.replace(/_/g, " ")}
> </span>
{test.containment_result.replace(/_/g, " ")} ) : (
</span> <span className="text-gray-500">Not evaluated yet</span>
) : test.detection_result === "not_detected" ? ( )}
<span className="text-gray-500">N/A</span> </p>
) : ( )}
<span className="text-gray-500">Not evaluated yet</span> </div>
)} )}
</p>
)}
</div>
{/* Detection & Containment times — only relevant when detected */} {/* Detection & Containment times — only when detected/partially_detected */}
{(canEditBlue || test.detection_result === "detected" || test.detection_result === "partially_detected") && ( {isDetected && (
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-gray-300"> <label className="mb-1.5 block text-sm font-medium text-gray-300">
Detection Time (UTC) Detection Time (UTC) <span className="text-red-400">*</span>
</label> </label>
{canEditBlue ? ( {canEditBlue ? (
<input <input
type="datetime-local" type="datetime-local"
required
value={blueDraft.detection_time} value={blueDraft.detection_time}
onChange={(e) => onBlueFieldChange("detection_time", e.target.value)} onChange={(e) => onBlueFieldChange("detection_time", e.target.value)}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
@@ -460,11 +465,12 @@ export default function TeamTabs({
</div> </div>
<div> <div>
<label className="mb-1.5 block text-sm font-medium text-gray-300"> <label className="mb-1.5 block text-sm font-medium text-gray-300">
Containment Time (UTC) Containment Time (UTC) <span className="text-red-400">*</span>
</label> </label>
{canEditBlue ? ( {canEditBlue ? (
<input <input
type="datetime-local" type="datetime-local"
required
value={blueDraft.containment_time} value={blueDraft.containment_time}
onChange={(e) => onBlueFieldChange("containment_time", e.target.value)} onChange={(e) => onBlueFieldChange("containment_time", e.target.value)}
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500" className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
+11 -2
View File
@@ -471,7 +471,11 @@ export default function TestDetailPage() {
<div className="flex justify-end gap-3"> <div className="flex justify-end gap-3">
{canSaveRed && ( {canSaveRed && (
<button <button
onClick={() => saveRedMutation.mutate()} onClick={() => {
if (!redDraft.execution_start_time) { showToast("Execution Start time is required", "error"); return; }
if (!redDraft.execution_end_time) { showToast("Execution End time is required", "error"); return; }
saveRedMutation.mutate();
}}
disabled={saveRedMutation.isPending} disabled={saveRedMutation.isPending}
className="flex items-center gap-1.5 rounded-lg bg-orange-600 px-4 py-2 text-sm font-medium text-white hover:bg-orange-500 disabled:opacity-50 transition-colors" className="flex items-center gap-1.5 rounded-lg bg-orange-600 px-4 py-2 text-sm font-medium text-white hover:bg-orange-500 disabled:opacity-50 transition-colors"
> >
@@ -481,7 +485,12 @@ export default function TestDetailPage() {
)} )}
{canSaveBlue && ( {canSaveBlue && (
<button <button
onClick={() => saveBlueMutation.mutate()} onClick={() => {
const isDetected = blueDraft.detection_result === "detected" || blueDraft.detection_result === "partially_detected";
if (isDetected && !blueDraft.detection_time) { showToast("Detection Time is required", "error"); return; }
if (isDetected && !blueDraft.containment_time) { showToast("Containment Time is required", "error"); return; }
saveBlueMutation.mutate();
}}
disabled={saveBlueMutation.isPending} disabled={saveBlueMutation.isPending}
className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50 transition-colors" className="flex items-center gap-1.5 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50 transition-colors"
> >