diff --git a/backend/app/routers/campaigns.py b/backend/app/routers/campaigns.py index a5b62a4..d74dca0 100644 --- a/backend/app/routers/campaigns.py +++ b/backend/app/routers/campaigns.py @@ -25,7 +25,7 @@ from sqlalchemy.orm import Session from app.database import get_db # Import get_current_user, require_any_role from app.dependencies.auth -from app.dependencies.auth import get_current_user, require_any_role +from app.dependencies.auth import get_current_user, require_any_role, require_any_role_strict # Import UnitOfWork from app.domain.unit_of_work from app.domain.unit_of_work import UnitOfWork @@ -319,7 +319,10 @@ def create_campaign( # Entry: db db: Session = Depends(get_db), # Entry: current_user - current_user: User = Depends(require_any_role("red_lead", "blue_lead", "manager")), + # Strict variant — admin must NOT get a free pass here. Admin + # administers the site, not campaign content; the only admin path to + # an active campaign is the emergency /activate override below. + current_user: User = Depends(require_any_role_strict("red_lead", "blue_lead", "manager")), ) -> dict: """Create a new campaign. @@ -987,7 +990,9 @@ def generate_campaign_from_actor( payload: GenerateFromActorPayload = GenerateFromActorPayload(), db: Session = Depends(get_db), # Entry: current_user - current_user: User = Depends(require_any_role("red_lead", "blue_lead")), + # Strict variant — admin must NOT get a free pass here, same as the + # plain create_campaign endpoint above. + current_user: User = Depends(require_any_role_strict("red_lead", "blue_lead")), ) -> dict: """Auto-generate a campaign from a threat actor's uncovered techniques. diff --git a/backend/app/routers/tests.py b/backend/app/routers/tests.py index 9e193df..5aa82e0 100644 --- a/backend/app/routers/tests.py +++ b/backend/app/routers/tests.py @@ -94,6 +94,11 @@ from app.services.test_crud_service import ( create_test_from_template as crud_create_from_template, ) +# Import from app.services.test_crud_service +from app.services.test_crud_service import ( + delete_test as crud_delete_test, +) + # Import from app.services.test_crud_service from app.services.test_crud_service import ( get_test_detail as crud_get_test_detail, @@ -543,6 +548,38 @@ def update_test( return test +@router.delete("/{test_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_test( + test_id: uuid.UUID, + db: Session = Depends(get_db), + # manager-only, and admin does NOT get a free pass — this is a + # queue-cleanup action for the manager role specifically, not a site + # administration task. + current_user: User = Depends(require_any_role_strict("manager")), +) -> None: + """Delete a standalone test that hasn't started yet. + + Only tests still in ``draft`` state and not linked to any campaign can + be deleted this way. + + Args: + test_id (uuid.UUID): Primary key of the test to delete. + db (Session): SQLAlchemy database session. + current_user (User): Authenticated manager performing the deletion. + """ + with UnitOfWork(db) as uow: + crud_delete_test(db, test_id) + log_action( + db, + user_id=current_user.id, + action="delete_test", + entity_type="test", + entity_id=test_id, + details={}, + ) + uow.commit() + + # --------------------------------------------------------------------------- # PATCH /tests/{id}/classification — admin data classification # --------------------------------------------------------------------------- diff --git a/backend/app/services/test_crud_service.py b/backend/app/services/test_crud_service.py index ba3f231..fcab67a 100644 --- a/backend/app/services/test_crud_service.py +++ b/backend/app/services/test_crud_service.py @@ -438,6 +438,28 @@ def get_test_or_raise(db: Session, test_id: uuid.UUID) -> Test: return test +def delete_test(db: Session, test_id: uuid.UUID) -> None: + """Delete a standalone, not-yet-started test from the queue. + + Only a test still in ``draft`` (execution hasn't started) and not + linked to any campaign may be deleted this way — a test already being + worked on, or one that's part of a campaign's plan, must be handled + through the normal workflow/campaign-modification paths instead. + Raises EntityNotFoundError, BusinessRuleViolation. Does not commit; + caller commits. + """ + test = get_test_or_raise(db, test_id) + + if test.state != TestState.draft: + raise BusinessRuleViolation("Only tests that haven't started yet can be deleted") + + in_campaign = db.query(CampaignTest).filter(CampaignTest.test_id == test.id).first() + if in_campaign: + raise BusinessRuleViolation("Cannot delete a test that belongs to a campaign") + + db.delete(test) + + # Define function get_test_with_technique def get_test_with_technique(db: Session, test_id: uuid.UUID) -> Test: """Fetch a test with technique joined. Raises EntityNotFoundError if not found. diff --git a/backend/tests/test_admin_excluded_from_test_workflow.py b/backend/tests/test_admin_excluded_from_test_workflow.py index 8c68972..f254478 100644 --- a/backend/tests/test_admin_excluded_from_test_workflow.py +++ b/backend/tests/test_admin_excluded_from_test_workflow.py @@ -39,6 +39,8 @@ DUMMY_ID = str(uuid.uuid4()) ("post", "/api/v1/tests", {"technique_id": DUMMY_ID, "name": "x"}), ("post", "/api/v1/tests/from-template", {"template_id": DUMMY_ID, "technique_id": DUMMY_ID}), ("post", "/api/v1/tests/import-rt", {"name": "x", "techniques": []}), + ("post", "/api/v1/campaigns", {"name": "x"}), + ("post", f"/api/v1/campaigns/from-threat-actor/{DUMMY_ID}", None), ], ) def test_admin_forbidden(client, auth_headers, method, path, payload): diff --git a/backend/tests/test_manager_delete_test.py b/backend/tests/test_manager_delete_test.py new file mode 100644 index 0000000..93a64a5 --- /dev/null +++ b/backend/tests/test_manager_delete_test.py @@ -0,0 +1,73 @@ +"""A manager (and only a manager) can delete a standalone test from the +queue, but only while it hasn't started yet (draft) and isn't linked to +any campaign — a test already in progress, or one that's part of a +campaign's plan, must go through the normal workflow instead. +""" + +import pytest + + +@pytest.fixture +def technique(api, auth_headers): + resp = api( + "post", "/api/v1/techniques", auth_headers, + json={"mitre_id": "T1059.600", "name": "Command Line Delete Test"}, + ) + assert resp.status_code == 201, resp.text + return resp.json()["id"] + + +@pytest.fixture +def draft_test(api, red_lead_headers, technique): + resp = api( + "post", "/api/v1/tests", red_lead_headers, + json={"technique_id": technique, "name": "Standalone draft test"}, + ) + assert resp.status_code == 201, resp.text + return resp.json()["id"] + + +def test_manager_can_delete_draft_standalone_test(api, manager_headers, draft_test): + resp = api("delete", f"/api/v1/tests/{draft_test}", manager_headers) + assert resp.status_code == 204, resp.text + + reloaded = api("get", f"/api/v1/tests/{draft_test}", manager_headers) + assert reloaded.status_code == 404 + + +def test_red_lead_cannot_delete_test(api, red_lead_headers, draft_test): + resp = api("delete", f"/api/v1/tests/{draft_test}", red_lead_headers) + assert resp.status_code == 403 + + +def test_admin_cannot_delete_test(api, auth_headers, draft_test): + resp = api("delete", f"/api/v1/tests/{draft_test}", auth_headers) + assert resp.status_code == 403 + + +def test_manager_cannot_delete_started_test(api, manager_headers, red_tech_headers, draft_test): + started = api("post", f"/api/v1/tests/{draft_test}/start-execution", red_tech_headers) + assert started.status_code == 200, started.text + + resp = api("delete", f"/api/v1/tests/{draft_test}", manager_headers) + assert resp.status_code == 400 + + +def test_manager_cannot_delete_test_linked_to_a_campaign( + api, db, manager_headers, red_lead_headers, draft_test, +): + campaign = api( + "post", "/api/v1/campaigns", red_lead_headers, + json={"name": "Holds the test"}, + ) + assert campaign.status_code == 201, campaign.text + campaign_id = campaign.json()["id"] + + add = api( + "post", f"/api/v1/campaigns/{campaign_id}/tests", red_lead_headers, + json={"test_id": draft_test}, + ) + assert add.status_code in (200, 201), add.text + + resp = api("delete", f"/api/v1/tests/{draft_test}", manager_headers) + assert resp.status_code == 400 diff --git a/frontend/src/api/tests.ts b/frontend/src/api/tests.ts index e4a6d34..bbf37d2 100644 --- a/frontend/src/api/tests.ts +++ b/frontend/src/api/tests.ts @@ -372,6 +372,11 @@ export async function resumeTest(testId: string): Promise { return data; } +/** Delete a standalone test that hasn't started yet. Manager only. */ +export async function deleteTest(testId: string): Promise { + await client.delete(`/tests/${testId}`); +} + // ── Timeline ─────────────────────────────────────────────────────── /** Get the audit-log timeline for a test. */ diff --git a/frontend/src/components/AddTestToCampaignModal.tsx b/frontend/src/components/AddTestToCampaignModal.tsx index cf3219d..24c3e54 100644 --- a/frontend/src/components/AddTestToCampaignModal.tsx +++ b/frontend/src/components/AddTestToCampaignModal.tsx @@ -22,9 +22,9 @@ import type { Test, TestState, TestTemplateSummary } from "../types/models"; const stateBadge: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", - red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30", + red_executing: "bg-red-900/50 text-red-400 border-red-500/30", red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30", - blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30", + blue_evaluating: "bg-blue-900/50 text-blue-400 border-blue-500/30", blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30", in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30", validated: "bg-green-900/50 text-green-400 border-green-500/30", diff --git a/frontend/src/components/CampaignTimeline.tsx b/frontend/src/components/CampaignTimeline.tsx index 7f66401..185d71e 100644 --- a/frontend/src/components/CampaignTimeline.tsx +++ b/frontend/src/components/CampaignTimeline.tsx @@ -21,9 +21,9 @@ const PHASES = [ const stateColors: Record = { draft: { bg: "bg-gray-800", text: "text-gray-400", border: "border-gray-600" }, - red_executing: { bg: "bg-orange-900/50", text: "text-orange-400", border: "border-orange-500/50" }, + red_executing: { bg: "bg-red-900/50", text: "text-red-400", border: "border-red-500/50" }, red_review: { bg: "bg-amber-900/50", text: "text-amber-400", border: "border-amber-500/50" }, - blue_evaluating: { bg: "bg-indigo-900/50", text: "text-indigo-400", border: "border-indigo-500/50" }, + blue_evaluating: { bg: "bg-blue-900/50", text: "text-blue-400", border: "border-blue-500/50" }, blue_review: { bg: "bg-purple-900/50", text: "text-purple-400", border: "border-purple-500/50" }, in_review: { bg: "bg-blue-900/50", text: "text-blue-400", border: "border-blue-500/50" }, validated: { bg: "bg-green-900/50", text: "text-green-400", border: "border-green-500/50" }, diff --git a/frontend/src/components/CampaignTimingPanel.tsx b/frontend/src/components/CampaignTimingPanel.tsx index 799a939..002f7c0 100644 --- a/frontend/src/components/CampaignTimingPanel.tsx +++ b/frontend/src/components/CampaignTimingPanel.tsx @@ -83,9 +83,9 @@ export default function CampaignTimingPanel({ campaignId }: Props) { { label: "Red Execution", value: data.red_execution_secs, - icon: , - color: "text-orange-400", - bg: "bg-orange-500/10 border-orange-500/20", + icon: , + color: "text-red-400", + bg: "bg-red-500/10 border-red-500/20", }, { label: "Blue Queue", @@ -97,9 +97,9 @@ export default function CampaignTimingPanel({ campaignId }: Props) { { label: "Blue Evaluation", value: data.blue_evaluation_secs, - icon: , - color: "text-indigo-400", - bg: "bg-indigo-500/10 border-indigo-500/20", + icon: , + color: "text-blue-400", + bg: "bg-blue-500/10 border-blue-500/20", }, { label: "Total", @@ -127,14 +127,14 @@ export default function CampaignTimingPanel({ campaignId }: Props) { {data.total_secs > 0 && (
- + - +
- Red Execution + Red Execution Blue Queue - Blue Evaluation + Blue Evaluation
)} @@ -160,9 +160,9 @@ export default function CampaignTimingPanel({ campaignId }: Props) { {/* Mini stacked bar */}
- + - +
{b.red_execution_secs > 0 && 🗡 {fmtDuration(b.red_execution_secs)}} diff --git a/frontend/src/components/TestPhaseTimeline.tsx b/frontend/src/components/TestPhaseTimeline.tsx index 195f396..ce43a28 100644 --- a/frontend/src/components/TestPhaseTimeline.tsx +++ b/frontend/src/components/TestPhaseTimeline.tsx @@ -264,8 +264,8 @@ export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelinePro
{hasRedExec && ( } + dotClass="border-red-500/60" + icon={} label={red_round_number > 1 ? `Red Team Execution (Round ${red_round_number})` : "Red Team Execution"} startTs={red_started_at} duration={redExecSecs} @@ -299,8 +299,8 @@ export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelinePro {hasBlueEval && ( } + dotClass="border-blue-500/60" + icon={} label={blue_round_number > 1 ? `Blue Evaluation (Round ${blue_round_number})` : "Blue Evaluation"} startTs={blue_work_started_at} duration={blueEvalSecs} @@ -310,8 +310,8 @@ export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelinePro {hasRedValidation && ( } + dotClass="border-red-400/60" + icon={} label="Red Lead Validation" badge={} startTs={red_validated_at} @@ -322,8 +322,8 @@ export default function TestPhaseTimeline({ test, testId }: TestPhaseTimelinePro {hasBlueValidation && ( } + dotClass="border-blue-400/60" + icon={} label="Blue Lead Validation" badge={} startTs={blue_validated_at} diff --git a/frontend/src/components/WorklogTimeline.tsx b/frontend/src/components/WorklogTimeline.tsx index 35e51dc..29b167d 100644 --- a/frontend/src/components/WorklogTimeline.tsx +++ b/frontend/src/components/WorklogTimeline.tsx @@ -12,11 +12,11 @@ interface WorklogTimelineProps { } const activityColors: Record = { - red_team: { bg: "bg-orange-900/30", text: "text-orange-400", icon: "border-orange-500/40" }, - blue_validation: { bg: "bg-indigo-900/30", text: "text-indigo-400", icon: "border-indigo-500/40" }, + red_team: { bg: "bg-red-900/30", text: "text-red-400", icon: "border-red-500/40" }, + blue_validation: { bg: "bg-blue-900/30", text: "text-blue-400", icon: "border-blue-500/40" }, purple_review: { bg: "bg-purple-900/30", text: "text-purple-400", icon: "border-purple-500/40" }, reporting: { bg: "bg-cyan-900/30", text: "text-cyan-400", icon: "border-cyan-500/40" }, - execution: { bg: "bg-orange-900/30", text: "text-orange-400", icon: "border-orange-500/40" }, + execution: { bg: "bg-red-900/30", text: "text-red-400", icon: "border-red-500/40" }, }; const defaultActivity = { bg: "bg-gray-800/50", text: "text-gray-400", icon: "border-gray-600" }; diff --git a/frontend/src/components/test-detail/AssigneeControl.tsx b/frontend/src/components/test-detail/AssigneeControl.tsx index 284d3c5..a9f2e98 100644 --- a/frontend/src/components/test-detail/AssigneeControl.tsx +++ b/frontend/src/components/test-detail/AssigneeControl.tsx @@ -24,8 +24,8 @@ interface Props { } const SIDE_STYLE = { - red: "border-orange-500/40 bg-orange-900/20 text-orange-400 hover:bg-orange-900/40", - blue: "border-indigo-500/40 bg-indigo-900/20 text-indigo-400 hover:bg-indigo-900/40", + red: "border-red-500/40 bg-red-900/20 text-red-400 hover:bg-red-900/40", + blue: "border-blue-500/40 bg-blue-900/20 text-blue-400 hover:bg-blue-900/40", }; const ELIGIBLE_ROLES: Record<"operator" | "reviewer", Record<"red" | "blue", string[]>> = { diff --git a/frontend/src/components/test-detail/LiveTimer.tsx b/frontend/src/components/test-detail/LiveTimer.tsx index 06000ce..ff9fd0d 100644 --- a/frontend/src/components/test-detail/LiveTimer.tsx +++ b/frontend/src/components/test-detail/LiveTimer.tsx @@ -66,14 +66,14 @@ export default function LiveTimer({ const colors = isPaused ? "border-yellow-500/40 bg-yellow-900/30 text-yellow-300" : variant === "red" - ? "border-orange-500/40 bg-orange-900/30 text-orange-300" - : "border-indigo-500/40 bg-indigo-900/30 text-indigo-300"; + ? "border-red-500/40 bg-red-900/30 text-red-300" + : "border-blue-500/40 bg-blue-900/30 text-blue-300"; const dotColor = isPaused ? "bg-yellow-400" : variant === "red" - ? "bg-orange-400" - : "bg-indigo-400"; + ? "bg-red-400" + : "bg-blue-400"; return (
diff --git a/frontend/src/components/test-detail/ResolveDisputeModal.tsx b/frontend/src/components/test-detail/ResolveDisputeModal.tsx index c279b90..13ccbe3 100644 --- a/frontend/src/components/test-detail/ResolveDisputeModal.tsx +++ b/frontend/src/components/test-detail/ResolveDisputeModal.tsx @@ -53,7 +53,7 @@ export default function ResolveDisputeModal({ onClick={() => setTargetTeam("red")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg border p-3 text-sm font-medium transition-colors ${ targetTeam === "red" - ? "border-orange-500 bg-orange-500/10 text-orange-400" + ? "border-red-500 bg-red-500/10 text-red-400" : "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600" }`} > @@ -64,7 +64,7 @@ export default function ResolveDisputeModal({ onClick={() => setTargetTeam("blue")} className={`flex flex-1 items-center justify-center gap-2 rounded-lg border p-3 text-sm font-medium transition-colors ${ targetTeam === "blue" - ? "border-indigo-500 bg-indigo-500/10 text-indigo-400" + ? "border-blue-500 bg-blue-500/10 text-blue-400" : "border-gray-700 bg-gray-800 text-gray-400 hover:border-gray-600" }`} > diff --git a/frontend/src/components/test-detail/ReviewModal.tsx b/frontend/src/components/test-detail/ReviewModal.tsx index 1093e62..f589118 100644 --- a/frontend/src/components/test-detail/ReviewModal.tsx +++ b/frontend/src/components/test-detail/ReviewModal.tsx @@ -41,7 +41,7 @@ export default function ReviewModal({ const [systemGaps, setSystemGaps] = useState(""); const title = isRed ? "Review Red Team Submission" : "Review Blue Team Submission"; - const accent = isRed ? "orange" : "indigo"; + const accent = isRed ? "red" : "blue"; const evidences = isRed ? test.red_evidences || [] : test.blue_evidences || []; const requiresNotes = decision === "reopen" && notes.trim().length === 0; diff --git a/frontend/src/components/test-detail/TeamTabs.tsx b/frontend/src/components/test-detail/TeamTabs.tsx index 1ac7c10..9eb4cf2 100644 --- a/frontend/src/components/test-detail/TeamTabs.tsx +++ b/frontend/src/components/test-detail/TeamTabs.tsx @@ -43,8 +43,8 @@ const TABS: { key: TabKey; label: string; icon: React.ReactNode }[] = [ ]; const TAB_COLORS: Record = { - red: "border-orange-500 text-orange-400", - blue: "border-indigo-500 text-indigo-400", + red: "border-red-500 text-red-400", + blue: "border-blue-500 text-blue-400", summary: "border-cyan-500 text-cyan-400", timeline: "border-gray-500 text-gray-400", }; @@ -209,7 +209,7 @@ export default function TeamTabs({
{/* Locked hint for red_tech in draft state */} {redLockedHint && ( -
+
{redLockedHint}
@@ -407,7 +407,7 @@ export default function TeamTabs({
{/* Locked hint for blue_tech before Start Evaluation */} {blueLockedHint && ( -
+
{blueLockedHint}
@@ -422,7 +422,7 @@ export default function TeamTabs({ value={blueDraft.detect_procedure} onChange={(e) => onBlueFieldChange("detect_procedure", e.target.value)} rows={6} - className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 font-mono" + className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 font-mono" placeholder="Describe how you detected the attack..." /> ) : ( @@ -533,7 +533,7 @@ export default function TeamTabs({ 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" + className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" /> ) : (

@@ -552,7 +552,7 @@ export default function TeamTabs({ 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" + className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" /> ) : (

@@ -574,7 +574,7 @@ export default function TeamTabs({ value={blueDraft.blue_summary} onChange={(e) => onBlueFieldChange("blue_summary", e.target.value)} rows={4} - className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 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 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" placeholder="Summarize the Blue Team analysis..." /> ) : ( @@ -602,7 +602,7 @@ export default function TeamTabs({ {/* Detection Rule Checklist */}

- + Detection Rule Evaluation

{/* Red Side */} -
-

+
+

Red Team (Attack)

@@ -804,8 +804,8 @@ export default function TeamTabs({
{/* Blue Side */} -
-

+
+

Blue Team (Detection)

diff --git a/frontend/src/components/test-detail/TestDetailHeader.tsx b/frontend/src/components/test-detail/TestDetailHeader.tsx index 2867110..49fdd0a 100644 --- a/frontend/src/components/test-detail/TestDetailHeader.tsx +++ b/frontend/src/components/test-detail/TestDetailHeader.tsx @@ -52,9 +52,9 @@ const STATE_INDEX: Record = { const STATE_BADGE: Record = { draft: "bg-gray-800/50 text-gray-400 border-gray-600/30", - red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30", + red_executing: "bg-red-900/50 text-red-400 border-red-500/30", red_review: "bg-amber-900/50 text-amber-400 border-amber-500/30", - blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30", + blue_evaluating: "bg-blue-900/50 text-blue-400 border-blue-500/30", blue_review: "bg-purple-900/50 text-purple-400 border-purple-500/30", in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30", validated: "bg-green-900/50 text-green-400 border-green-500/30", @@ -203,7 +203,7 @@ export default function TestDetailHeader({ key="start" onClick={onStartExecution} disabled={isTransitioning} - 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 transition-colors disabled:opacity-50" + className="flex items-center gap-1.5 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500 transition-colors disabled:opacity-50" > {isTransitioning ? : } Start Execution @@ -223,7 +223,7 @@ export default function TestDetailHeader({ onClick={onSubmitRed} disabled={isTransitioning || !hasRedEvidence} title={!hasRedEvidence ? "Upload at least one Red Team evidence file before submitting" : undefined} - 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 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" + className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" > {isTransitioning ? : } Submit to Blue Team @@ -282,7 +282,7 @@ export default function TestDetailHeader({ key="start-blue-work" onClick={onStartBlueWork} disabled={isTransitioning} - 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 transition-colors disabled:opacity-50" + className="flex items-center gap-1.5 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 transition-colors disabled:opacity-50" > {isTransitioning ? : } Start Evaluation @@ -320,7 +320,7 @@ export default function TestDetailHeader({