flag_blue_review_gap saved system_gaps on the test but the Jira
in_review comment never mentioned it, so a flagged capability gap was
silently invisible in Jira. Now included in the comment and tagged
with a system-gap label for filtering.
Also surfaces TestTemplate.detect_suggested_procedure as a read-only
reference in the Test Catalog's create-from-template modal — it was
only ever shown in the admin template editor, not here, so an approved
suggestion appeared to vanish when viewed from the catalog.
Confirmed via issue_editmeta and a real production failure log: Time to
Detect / Time to Contain are Jira datetime fields despite the name, not
numeric duration fields. Pushing a computed hour count made Jira reject
the entire fields payload (validated atomically), silently sinking BT End
Date and Attack Detected on the same call even though only these two
fields were actually invalid. Now pushes the real detection_time/
containment_time timestamps. Removed the now-unused _compute_hours.
- BT Start Date had the same bug RT Start Date did: pushed on every round,
clobbering round 1's real pickup date. Now only pushed on round 1,
mirroring the "first round survives every reopen" rule.
- push_test_event's assignment branches (red_executing, red/blue reviewer,
blue_evaluating reassignment) only read the cached jira_account_id and
silently no-op'd if it was never populated — which is exactly what
happened to a blue_lead who hadn't logged in since Jira was configured,
making it look like Blue's reviewer reassignment was broken when Red's
wasn't purely because that particular red_lead happened to have logged
in already. Extracted the live-lookup fallback that only
push_assignee_update had into a shared _resolve_jira_account_id(), used
by every assignment call site now.
- Tests created without a campaign parent ticket now get a "standalone-test"
label, so they're identifiable in Jira without cross-referencing Aegis.
- The comment posted on every red/blue submission was a generic one-liner
with no data — only a reopened round got a full data dump (via
push_round_archived), so a round that was approved outright left no
record in Jira of what was actually done. Now every submission comment
includes the round number, procedure/tool/attack success (red) or
detection/containment result (blue), and summary.
- RT/BT Start/End Date are genuine Jira "datetime" custom fields (confirmed
via issue_editmeta), not plain dates. Pushing a bare "YYYY-MM-DD" string
made Jira default the time-of-day to midnight, so RT Start Date and RT
End Date ended up showing the same meaningless midnight timestamp instead
of the actual execution window. Now sends a full ISO datetime.
- The test detail page showed "RT: Unassigned" for the operator who WAS
correctly assigned, because GET /users/operators (the only source
AssigneeControl used to resolve an assignee ID into a username) is
restricted to leads/managers — a plain operator has no other way to
resolve their own ID. TestOut now resolves and includes the assignee's
username directly (red/blue tech + reviewer), so the badge no longer
depends on a permission-gated list the viewer might not have access to.
- execution_start_time/execution_end_time/detection_time/containment_time
were captured from a datetime-local input with no local->UTC conversion,
then stored/displayed as if already UTC — off by the browser's offset.
Frontend now converts properly in both directions; backend schemas
defensively normalize any tz-aware input to naive UTC as well.
- push_rt_submitted was pushing datetime.utcnow() (whenever the submit
button was clicked) into Jira's RT Start/End Date fields instead of the
operator-entered execution window. Now pushes the *first* round's
execution start (recovered from round_history across reopens) and the
*current* round's execution end. Removed push_rt_started, which only
ever pushed a meaningless click-timestamp.
- Reopening a test now adds a "reopened" Jira label (read-modify-write so
existing labels aren't clobbered), on top of the existing round-archived
comment and status push.
- 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.
- reopen_red_review starts the timer paused (paused_at set) instead of
immediately running, since the operator hasn't resumed working yet —
blue already did this via blue_work_started_at, red needed the same
behavior via the existing pause/resume mechanism.
- update_test_red/update_test_blue now lock edits to the actual assigned
operator for leads too, not just techs — a lead could previously edit
another operator's in-progress test. Mirrored in TeamTabs.tsx.
- push_test_event reassigns the Jira ticket to the original operator
(not the reopening lead) when a test returns to red_executing or
blue_evaluating for rework, instead of leaving it on the lead.
The red_lead reviewer stayed assigned in Jira through blue_evaluating
(and the blue_lead reviewer through in_review) since push_test_event
never touched the assignee on those transitions — only red_executing
and the review gates did. Both are hand-offs with no single owner yet,
so the assignee is cleared instead of left stale.
Also fixes push_bt_work_started never assigning the blue_tech who
actually picks up the queued test — it only moved the status to
In Progress before.
- Fix the red/blue draft form losing unsaved fields (e.g. execution_start_time) whenever an evidence upload refetches the test — the hydration effect now runs once per test, not on every refetch.
- Add color-scheme: dark so native date/time picker popups match the app theme instead of rendering white.
- Fix _STATE_TO_JIRA_STATUS: real Jira statuses are 'To Do' and 'Red Team test review', not 'To-Do' and 'RT Test Review' — confirmed live against the actual workflow transitions, which is why Submit to Blue Team never moved the ticket past 'In Progress'.
- Tempo worklog sync was silently blocked by TEMPO_ENABLED defaulting to False with no admin-facing toggle, even after configuring a Tempo token via Settings. Now bypassed once an admin or personal token is actually configured, mirroring Jira's DB-backed enabled flag.
- Hold now actually pauses the running phase timer (paused_at), and Resume accumulates the held duration into red/blue_paused_seconds — previously the timer kept counting through a hold.
lookup_user_jira_account_id() only ever searched by Aegis login email,
ignoring the jira_email override field that exists precisely for users
whose corporate Atlassian email differs from their Aegis login email
(the rest of jira_service.py already documents and uses this priority
order). Found via jesus-huertas, whose real Jira account uses a
different email than his Aegis account.
Admin can no longer be picked as a red_tech/blue_tech assignee — it
administers the site, it doesn't operate tests. Applied to the
eligible-assignee list on both the picker UI and the assign endpoint's
validation, and dropped from the GET /users/operators pool.
push_assignee_update() now falls back to a fresh Jira account-id lookup
when the assignee hasn't logged in yet (so jira_account_id is still
empty), instead of silently no-op'ing — assignment now syncs to Jira
immediately regardless of whether the assignee has ever logged into
Aegis before.
GET /system/jira-config was admin-only, so non-admin users hit a 403
and the frontend silently fell back to a hardcoded jira.atlassian.com
base URL when building /browse/{key} links. Opened it to any
authenticated user (still no secrets returned).
Also drops the redundant 'security-test' Jira label, and adds a
page-size selector (10/25/50/100) and a 'no existing test yet' filter
to the Test Catalog.
Adds GET /users/operators (leads+admin) and wires a lead-only assign
control into the test detail header, calling the existing but
previously unreachable POST /tests/{id}/assign endpoint. Manual
assignment now also pushes the Jira ticket assignee immediately
instead of waiting for the operator to start execution.
Also adds test.platform as a kebab-case label on Jira ticket creation.
Jira Cloud's REST API rejects a bare string for select-type custom
fields with 'Specify a valid id or name for <field>' — confirmed live
against the real Jira instance. This silently broke Severity, Data
Sensitivity, Attack success, Attack detected, and Attack contained on
every ticket create/update, masked until now by the non-fatal
exception handling around each call.
The auto-create-ticket call was sending customfield_11233 (a field that
doesn't exist in the project) instead of the real Data Sensitivity field
customfield_11814, which made Jira reject the WHOLE issue and silently
block ticket creation for every new test.
Also replaces Aegis's invented 4-tier data_classification scheme
(public_release/general_use/confidential/restricted) with the 6 values
Jira's Data Sensitivity field actually uses (public/general_use/
internal_use_only/trusted_people/customer_content/pii), since that is
the classification the organization actually applies. Includes a data
migration remapping existing rows and a defensive retry if Jira ever
rejects an unscreened custom field again.
atlassian-python-api takes limit, not maxResults. The TypeError was
silently swallowed at DEBUG level, so every non-admin user's Jira
account-id lookup failed even with a valid email and working admin
Jira connection.
Previously only red_executing (-> "In Progress") ever changed the Jira
issue's status; every other Aegis transition (red_review, blue_evaluating,
blue_review, in_review, validated, rejected, disputed) only posted a
comment, leaving the Jira board stuck on whatever status it started with.
Expands push_test_event() to a full TestState -> Jira-status table
covering the whole lifecycle (To-Do -> In Progress -> RT Test Review ->
Queued Blue Team -> In Progress -> Blue Team Test Review -> Validation ->
Done/Rejected/Dispute), matching the Purple Team Jira workflow. Adds two
new lifecycle hooks that the generic dispatch can't handle correctly:
- push_bt_work_started(): blue_evaluating covers both "queued, unclaimed"
and "actively being worked" — needs an explicit push when the blue tech
clicks Start Evaluation, or it never leaves "Queued Blue Team".
- disputed state push: a conflicting lead vote previously produced zero
Jira signal at all.
Also fills two real gaps: reopen_red_review and reopen_blue_review never
synced anything to Jira before. Their target status differs on purpose —
reopen_red_review pushes "In Progress" (Aegis resumes the same operator
immediately, no re-claim), while reopen_blue_review pushes "Queued Blue
Team" (Aegis clears blue_work_started_at, forcing a fresh "Start
Evaluation" click) — verified against the actual field-reset behavior in
test_entity.py rather than assumed symmetry between the two teams.
Maps Test.containment_result (contained/partially_contained/not_contained)
to Yes/Partial/No, mirroring the existing Attack Success/Attack Detected
field push in push_bt_submitted. Field ID was the last missing piece
blocking this from Block 4.