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_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
const redLockedHint =
test.state === "draft" && role === "red_tech"
@@ -238,11 +243,12 @@ export default function TeamTabs({
<div className="grid grid-cols-2 gap-4">
<div>
<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>
{canEditRed ? (
<input
type="datetime-local"
required
value={redDraft.execution_start_time}
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"
@@ -255,11 +261,12 @@ export default function TeamTabs({
</div>
<div>
<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>
{canEditRed ? (
<input
type="datetime-local"
required
value={redDraft.execution_end_time}
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"
@@ -391,13 +398,14 @@ export default function TeamTabs({
)}
</div>
{/* Containment Result */}
{/* Containment Result — only when detected/partially_detected */}
{isDetected && (
<div>
<label className="mb-2 block text-sm font-medium text-gray-300">
Containment Result
</label>
{canEditBlue ? (
<div className={`flex flex-wrap gap-2 ${blueDraft.detection_result === "not_detected" ? "opacity-40 pointer-events-none" : ""}`}>
<div className="flex flex-wrap gap-2">
{CONTAINMENT_RESULTS.map((opt) => (
<button
key={opt.value}
@@ -411,9 +419,6 @@ export default function TeamTabs({
{opt.label}
</button>
))}
{blueDraft.detection_result === "not_detected" && (
<span className="self-center text-xs text-gray-500">N/A attack not detected</span>
)}
</div>
) : (
<p className="text-sm">
@@ -429,25 +434,25 @@ export default function TeamTabs({
>
{test.containment_result.replace(/_/g, " ")}
</span>
) : test.detection_result === "not_detected" ? (
<span className="text-gray-500">N/A</span>
) : (
<span className="text-gray-500">Not evaluated yet</span>
)}
</p>
)}
</div>
)}
{/* Detection & Containment times — only relevant when detected */}
{(canEditBlue || test.detection_result === "detected" || test.detection_result === "partially_detected") && (
{/* Detection & Containment times — only when detected/partially_detected */}
{isDetected && (
<div className="grid grid-cols-2 gap-4">
<div>
<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>
{canEditBlue ? (
<input
type="datetime-local"
required
value={blueDraft.detection_time}
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"
@@ -460,11 +465,12 @@ export default function TeamTabs({
</div>
<div>
<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>
{canEditBlue ? (
<input
type="datetime-local"
required
value={blueDraft.containment_time}
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"
+11 -2
View File
@@ -471,7 +471,11 @@ export default function TestDetailPage() {
<div className="flex justify-end gap-3">
{canSaveRed && (
<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}
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 && (
<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}
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"
>