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
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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user