diff --git a/backend/app/services/test_workflow_service.py b/backend/app/services/test_workflow_service.py index 83f8465..cad8a08 100644 --- a/backend/app/services/test_workflow_service.py +++ b/backend/app/services/test_workflow_service.py @@ -405,12 +405,14 @@ def reopen_red_review(db: Session, test: Test, user: User, notes: str) -> Test: test.red_review_at = datetime.utcnow() test.red_review_notes = notes.strip() test.red_round_number = (test.red_round_number or 1) + 1 - # New sheet — the operator fills these in fresh for this round; the - # values that were just archived stay visible via round_history. - test.procedure_text = None - test.tool_used = None + # Only the per-round verdict/timing fields get a blank slate — the + # operator must explicitly answer "was THIS attempt successful" and + # "when did THIS attempt run" rather than silently inheriting round 1's + # answer. The old values aren't lost: they're in round_history and + # shown on the archived Phase Timeline card. Free-text fields + # (procedure, tool, summary) are NOT cleared — the operator edits them + # in place for the new round instead of retyping from scratch. test.attack_success = None - test.red_summary = None test.execution_start_time = None test.execution_end_time = None db.flush() @@ -640,13 +642,14 @@ def reopen_blue_review(db: Session, test: Test, user: User, notes: str) -> Test: test.blue_review_at = datetime.utcnow() test.blue_review_notes = notes.strip() test.blue_round_number = (test.blue_round_number or 1) + 1 - # New sheet for this round; the archived values above remain visible - # via round_history. + # Same split as the red side: verdict/timing fields get a blank slate + # for this round (old values live on in round_history + the archived + # Phase Timeline card), but blue_summary is free text the operator + # edits in place rather than retyping. test.detection_result = None test.containment_result = None test.detection_time = None test.containment_time = None - test.blue_summary = None db.flush() log_action( diff --git a/backend/tests/test_workflow.py b/backend/tests/test_workflow.py index 488eef3..558031f 100644 --- a/backend/tests/test_workflow.py +++ b/backend/tests/test_workflow.py @@ -740,9 +740,11 @@ class TestReviewDecisions: @patch("app.services.test_workflow_service.log_action") def test_reopen_red_review_archives_round_and_resets_fields(self, mock_log): - """Reopening must not erase what Red already submitted — it archives - the round (visible via round_history) and only then blanks the live - fields for a fresh attempt, bumping the round counter.""" + """Reopening must not erase what Red already submitted. The round is + archived (visible via round_history) and only the per-round verdict + fields (attack_success, execution start/end) are blanked for a fresh + answer — free-text fields (procedure, tool, summary) stay in place + for the operator to edit, not retype from scratch.""" from app.models.test_round_history import TestRoundHistory test = _make_test( @@ -766,10 +768,12 @@ class TestReviewDecisions: assert archived[0].red_summary == "Got a shell." assert archived[0].review_notes == "incomplete evidence" - assert test.procedure_text is None - assert test.tool_used is None + assert test.procedure_text == "ran mimikatz" + assert test.tool_used == "mimikatz" + assert test.red_summary == "Got a shell." assert test.attack_success is None - assert test.red_summary is None + assert test.execution_start_time is None + assert test.execution_end_time is None assert test.red_round_number == 2 @patch("app.services.test_workflow_service.log_action") @@ -797,7 +801,8 @@ class TestReviewDecisions: assert archived[0].review_notes == "missing containment steps" assert test.detection_result is None - assert test.blue_summary is None + assert test.containment_result is None + assert test.blue_summary == "Caught it via EDR." assert test.blue_round_number == 2 @patch("app.services.test_workflow_service.log_action")