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
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:
@@ -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,63 +398,61 @@ export default function TeamTabs({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Containment Result */}
|
||||
<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" : ""}`}>
|
||||
{CONTAINMENT_RESULTS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => onBlueFieldChange("containment_result", opt.value)}
|
||||
className={`rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
blueDraft.containment_result === opt.value
|
||||
? opt.color
|
||||
: "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
|
||||
}`}
|
||||
>
|
||||
{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">
|
||||
{test.containment_result ? (
|
||||
<span
|
||||
className={`inline-flex rounded-full border px-2.5 py-0.5 text-xs font-medium ${
|
||||
test.containment_result === "contained"
|
||||
? "border-green-500/30 bg-green-900/50 text-green-400"
|
||||
: test.containment_result === "not_contained"
|
||||
? "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.detection_result === "not_detected" ? (
|
||||
<span className="text-gray-500">N/A</span>
|
||||
) : (
|
||||
<span className="text-gray-500">Not evaluated yet</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* 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">
|
||||
{CONTAINMENT_RESULTS.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => onBlueFieldChange("containment_result", opt.value)}
|
||||
className={`rounded-lg border px-3 py-2 text-sm font-medium transition-colors ${
|
||||
blueDraft.containment_result === opt.value
|
||||
? opt.color
|
||||
: "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
{test.containment_result ? (
|
||||
<span
|
||||
className={`inline-flex rounded-full border px-2.5 py-0.5 text-xs font-medium ${
|
||||
test.containment_result === "contained"
|
||||
? "border-green-500/30 bg-green-900/50 text-green-400"
|
||||
: test.containment_result === "not_contained"
|
||||
? "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>
|
||||
) : (
|
||||
<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"
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user