feat(tests): require evidence upload before phase transitions
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Backend: - submit_red_evidence: raises InvalidOperationError if no Red Team evidence file has been uploaded for the test - submit_blue_evidence: raises InvalidOperationError if no Blue Team evidence file has been uploaded Frontend: - 'Submit to Blue Team' button: disabled + '⚠ Upload evidence first' hint when test.red_evidences is empty - 'Submit for Review' button: same for test.blue_evidences - Native tooltip on disabled buttons explains the requirement - Buttons re-enable automatically after the first file is uploaded Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,8 @@ from sqlalchemy.orm import Session
|
||||
from app.config import settings
|
||||
from app.domain.exceptions import InvalidOperationError, InvalidTransitionError
|
||||
from app.domain.test_entity import TestEntity
|
||||
from app.models.enums import TestState
|
||||
from app.models.enums import TestState, TeamSide
|
||||
from app.models.evidence import Evidence
|
||||
from app.models.test import Test
|
||||
from app.models.user import User
|
||||
from app.services.audit_service import log_action
|
||||
@@ -146,9 +147,21 @@ def submit_red_evidence(db: Session, test: Test, user: User) -> Test:
|
||||
"""Move from ``red_executing`` → ``blue_evaluating``.
|
||||
|
||||
Called by **red_tech** once they have finished documenting the attack.
|
||||
Requires at least one Red Team evidence file to be uploaded.
|
||||
Stops the Red Team timer and creates an automatic worklog.
|
||||
Starts the Blue Team timer by recording ``blue_started_at``.
|
||||
"""
|
||||
# Evidence is mandatory before submitting
|
||||
red_evidence_count = (
|
||||
db.query(Evidence)
|
||||
.filter(Evidence.test_id == test.id, Evidence.team == TeamSide.red)
|
||||
.count()
|
||||
)
|
||||
if red_evidence_count == 0:
|
||||
raise InvalidOperationError(
|
||||
"Cannot submit to Blue Team: at least one Red Team evidence file must be uploaded first."
|
||||
)
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Auto-resume if paused
|
||||
@@ -224,10 +237,22 @@ def submit_blue_evidence(db: Session, test: Test, user: User) -> Test:
|
||||
"""Move from ``blue_evaluating`` → ``in_review``.
|
||||
|
||||
Called by **blue_tech** once they have finished documenting detection.
|
||||
Requires at least one Blue Team evidence file to be uploaded.
|
||||
Stops the Blue Team timer and creates an automatic worklog.
|
||||
Uses blue_work_started_at as the phase start for Tempo if available,
|
||||
otherwise falls back to blue_started_at (queue-entry timestamp).
|
||||
"""
|
||||
# Evidence is mandatory before submitting
|
||||
blue_evidence_count = (
|
||||
db.query(Evidence)
|
||||
.filter(Evidence.test_id == test.id, Evidence.team == TeamSide.blue)
|
||||
.count()
|
||||
)
|
||||
if blue_evidence_count == 0:
|
||||
raise InvalidOperationError(
|
||||
"Cannot submit for review: at least one Blue Team evidence file must be uploaded first."
|
||||
)
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# Auto-resume if paused
|
||||
|
||||
@@ -111,21 +111,27 @@ export default function TestDetailHeader({
|
||||
);
|
||||
}
|
||||
|
||||
// Red Team in red_executing -> Submit to Blue Team
|
||||
// Red Team in red_executing -> Submit to Blue Team (requires ≥1 red evidence)
|
||||
if (
|
||||
test.state === "red_executing" &&
|
||||
(role === "red_tech" || role === "red_lead" || role === "admin")
|
||||
) {
|
||||
const hasRedEvidence = (test.red_evidences?.length ?? 0) > 0;
|
||||
buttons.push(
|
||||
<button
|
||||
key="submit-red"
|
||||
onClick={onSubmitRed}
|
||||
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"
|
||||
>
|
||||
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Submit to Blue Team
|
||||
</button>,
|
||||
<div key="submit-red" className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{isTransitioning ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Submit to Blue Team
|
||||
</button>
|
||||
{!hasRedEvidence && (
|
||||
<span className="text-[10px] text-orange-400">⚠ Upload evidence first</span>
|
||||
)}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,16 +155,23 @@ export default function TestDetailHeader({
|
||||
</button>,
|
||||
);
|
||||
} else {
|
||||
// Submit for Review requires ≥1 blue evidence
|
||||
const hasBlueEvidence = (test.blue_evidences?.length ?? 0) > 0;
|
||||
buttons.push(
|
||||
<button
|
||||
key="submit-blue"
|
||||
onClick={onSubmitBlue}
|
||||
disabled={isTransitioning}
|
||||
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 ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Submit for Review
|
||||
</button>,
|
||||
<div key="submit-blue" className="flex flex-col items-end gap-1">
|
||||
<button
|
||||
onClick={onSubmitBlue}
|
||||
disabled={isTransitioning || !hasBlueEvidence}
|
||||
title={!hasBlueEvidence ? "Upload at least one Blue Team evidence file before submitting" : undefined}
|
||||
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 ? <Loader2 className="h-4 w-4 animate-spin" /> : <Send className="h-4 w-4" />}
|
||||
Submit for Review
|
||||
</button>
|
||||
{!hasBlueEvidence && (
|
||||
<span className="text-[10px] text-orange-400">⚠ Upload evidence first</span>
|
||||
)}
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user