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
+58 -12
View File
@@ -1025,11 +1025,13 @@ def push_hold_event(
def push_pause_event(db: Session, test, actor, *, resuming: bool = False) -> None:
"""Post a timer pause/resume comment to Jira and toggle 'On Hold' / 'In Progress'.
"""Post a timer pause/resume comment to Jira — status is left untouched.
Distinct from :func:`push_hold_event` — a short timer pause (the
operator stepping away briefly) maps to "On Hold", while an explicit
hold (something external blocking progress) maps to "Blocked".
Distinct from :func:`push_hold_event` — a short timer pause (the timer
sitting idle between rework rounds, or the operator briefly stepping
away) is not the same thing as the test actually being blocked, so it
must not flip the Jira issue to "On Hold". Only a genuine Hold action
(:func:`push_hold_event`) does that. This just leaves a record.
Non-fatal — any Jira error is logged and swallowed.
"""
if not has_admin_jira_configured(db):
@@ -1052,16 +1054,8 @@ def push_pause_event(db: Session, test, actor, *, resuming: bool = False) -> Non
if resuming:
comment = f"h3. ▶ Timer Resumed\n\n*Resumed by:* {actor.username}\n*At:* {ts}"
try:
jira.set_issue_status(link.jira_issue_key, "In Progress")
except Exception as exc_t:
logger.warning("Could not transition %s off On Hold: %s", link.jira_issue_key, exc_t)
else:
comment = f"h3. ⏸ Timer Paused\n\n*Paused by:* {actor.username}\n*At:* {ts}"
try:
jira.set_issue_status(link.jira_issue_key, "On Hold")
except Exception as exc_t:
logger.warning("Could not transition %s to On Hold: %s", link.jira_issue_key, exc_t)
jira.issue_add_comment(link.jira_issue_key, comment)
link.last_synced_at = datetime.utcnow()
@@ -1071,6 +1065,58 @@ def push_pause_event(db: Session, test, actor, *, resuming: bool = False) -> Non
logger.warning("Failed to push pause event for test %s: %s", test.id, exc, exc_info=True)
def push_round_archived(db: Session, test, actor, *, round_data) -> None:
"""Post a permanent Jira comment summarizing a round right before it's
reset for rework.
``round_data`` custom fields (attack success, detection result, etc.)
only ever show Jira's *latest* value — each new round's submission
overwrites the field. This comment is what preserves every prior
round's results in Jira, since comments are never overwritten.
Non-fatal — any Jira error is logged and swallowed.
"""
if not has_admin_jira_configured(db):
return
link = (
db.query(JiraLink)
.filter(
JiraLink.entity_type == JiraLinkEntityType.test,
JiraLink.entity_id == test.id,
)
.first()
)
if not link:
return
try:
jira = get_admin_jira_client(db)
team = round_data.team
lines = [f"h3. \U0001F4CB Round {round_data.round_number} archived ({team} team)\n"]
if team == "red":
lines.append(f"*Procedure:* {round_data.procedure_text or '_none recorded_'}")
lines.append(f"*Tool used:* {round_data.tool_used or '-'}")
lines.append(f"*Attack success:* {round_data.attack_success.value if round_data.attack_success else '-'}")
lines.append(f"*Summary:* {round_data.red_summary or '-'}")
else:
lines.append(f"*Detection result:* {round_data.detection_result.value if round_data.detection_result else '-'}")
lines.append(f"*Containment result:* {round_data.containment_result.value if round_data.containment_result else '-'}")
lines.append(f"*Summary:* {round_data.blue_summary or '-'}")
lines.append(f"*Sent back by:* {actor.username}")
lines.append(f"*Reopen notes:* {round_data.review_notes or '-'}")
comment = "\n".join(lines)
jira.issue_add_comment(link.jira_issue_key, comment)
link.last_synced_at = datetime.utcnow()
db.flush()
logger.info(
"Posted round-archived comment to Jira %s (team=%s, round=%s)",
link.jira_issue_key, team, round_data.round_number,
)
except Exception as exc:
logger.warning("Failed to push round-archived comment for test %s: %s", test.id, exc, exc_info=True)
def push_bt_work_started(db: Session, test, actor) -> None:
"""Transition the Jira issue to "In Progress" when a blue tech picks up
a queued test to start evaluating.