diff --git a/backend/app/services/test_workflow_service.py b/backend/app/services/test_workflow_service.py index 361d2fd..f26357e 100644 --- a/backend/app/services/test_workflow_service.py +++ b/backend/app/services/test_workflow_service.py @@ -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 diff --git a/frontend/src/components/test-detail/TestDetailHeader.tsx b/frontend/src/components/test-detail/TestDetailHeader.tsx index 42df425..a6151f7 100644 --- a/frontend/src/components/test-detail/TestDetailHeader.tsx +++ b/frontend/src/components/test-detail/TestDetailHeader.tsx @@ -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( - , +