feat(tests): archive round history on reopen, stop pausing from setting Jira On Hold
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

- Reopening a test for rework (red or blue) now archives the round that's
  ending into a new test_round_history table before resetting the live
  fields — procedure/results/detection data is preserved instead of
  overwritten, and Phase Timeline renders every past round alongside the
  current one, so Blue Team keeps visibility into earlier Red attempts.
- Each archived round also posts a permanent Jira comment (push_round_archived)
  since Jira's custom fields only ever show the latest value once the next
  round resubmits — the comment thread is what preserves full history there.
- push_pause_event no longer transitions the Jira issue to "On Hold" — a
  timer pause is not a real Hold, and flipping Jira status for it was
  misleading. Only the explicit Hold action (push_hold_event) does that now.
This commit is contained in:
kitos
2026-07-13 12:08:45 +02:00
parent 0bb34f6834
commit 8fe38dc661
12 changed files with 526 additions and 26 deletions
+51 -4
View File
@@ -32,6 +32,7 @@ from app.models.campaign import Campaign, CampaignTest
from app.models.enums import TestState, TeamSide
from app.models.evidence import Evidence
from app.models.test import Test
from app.models.test_round_history import TestRoundHistory
from app.models.user import User
from app.services.audit_service import log_action
from app.services.notification_service import (
@@ -383,18 +384,41 @@ def reopen_red_review(db: Session, test: Test, user: User, notes: str) -> Test:
if not notes or not notes.strip():
raise InvalidOperationError("A comment is required when reopening a test for rework")
# Archive this round's data before resetting the live fields — reopening
# is for a fresh attempt, not for erasing what Blue Team already saw
# about the first one.
archived_round = TestRoundHistory(
test_id=test.id, team="red", round_number=test.red_round_number or 1,
started_at=test.red_started_at, ended_at=datetime.utcnow(),
paused_seconds=test.red_paused_seconds or 0,
procedure_text=test.procedure_text, tool_used=test.tool_used,
attack_success=test.attack_success, red_summary=test.red_summary,
execution_start_time=test.execution_start_time, execution_end_time=test.execution_end_time,
review_notes=notes.strip(), reviewed_by=user.id,
)
db.add(archived_round)
entity = TestEntity.from_orm(test)
entity.reopen_red_review()
entity.apply_to(test)
test.red_review_by = user.id
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
test.attack_success = None
test.red_summary = None
test.execution_start_time = None
test.execution_end_time = None
db.flush()
log_action(
db, user_id=user.id, action="reopen_red_review",
entity_type="test", entity_id=test.id,
details={"notes": notes, "test_name": test.name},
details={"notes": notes, "test_name": test.name, "round_number": archived_round.round_number},
)
if test.red_tech_assignee:
@@ -409,11 +433,12 @@ def reopen_red_review(db: Session, test: Test, user: User, notes: str) -> Test:
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
try:
from app.services.jira_service import push_test_event
from app.services.jira_service import push_test_event, push_round_archived
original_operator = (
db.query(User).filter(User.id == test.red_tech_assignee).first()
if test.red_tech_assignee else None
)
push_round_archived(db, test, user, round_data=archived_round)
push_test_event(
db, test, user, "red_executing",
extra={"notes": notes.strip()}, assignee=original_operator,
@@ -594,6 +619,19 @@ def reopen_blue_review(db: Session, test: Test, user: User, notes: str) -> Test:
if not notes or not notes.strip():
raise InvalidOperationError("A comment is required when reopening a test for rework")
# Archive this round's data before resetting the live fields — same
# reasoning as the red side: don't lose what was already evaluated.
archived_round = TestRoundHistory(
test_id=test.id, team="blue", round_number=test.blue_round_number or 1,
started_at=test.blue_work_started_at, ended_at=datetime.utcnow(),
paused_seconds=test.blue_paused_seconds or 0,
detection_result=test.detection_result, containment_result=test.containment_result,
detection_time=test.detection_time, containment_time=test.containment_time,
blue_summary=test.blue_summary,
review_notes=notes.strip(), reviewed_by=user.id,
)
db.add(archived_round)
entity = TestEntity.from_orm(test)
entity.reopen_blue_review()
entity.apply_to(test)
@@ -601,12 +639,20 @@ def reopen_blue_review(db: Session, test: Test, user: User, notes: str) -> Test:
test.blue_review_by = user.id
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.
test.detection_result = None
test.containment_result = None
test.detection_time = None
test.containment_time = None
test.blue_summary = None
db.flush()
log_action(
db, user_id=user.id, action="reopen_blue_review",
entity_type="test", entity_id=test.id,
details={"notes": notes, "test_name": test.name},
details={"notes": notes, "test_name": test.name, "round_number": archived_round.round_number},
)
if test.blue_tech_assignee:
@@ -621,7 +667,8 @@ def reopen_blue_review(db: Session, test: Test, user: User, notes: str) -> Test:
logger.warning("Notification failed for test %s: %s", test.id, e, exc_info=True)
try:
from app.services.jira_service import push_test_event
from app.services.jira_service import push_test_event, push_round_archived
push_round_archived(db, test, user, round_data=archived_round)
push_test_event(db, test, user, "blue_evaluating", extra={"notes": notes.strip()})
except Exception as e:
logger.warning("Jira push failed for test %s: %s", test.id, e, exc_info=True)