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.
The Coverage Matrix heatmap only ever showed the bare MITRE ID on each
cell (name only via hover tooltip), and colored cells from a continuous
score -> hex gradient with no discrete status concept — unlike the
Techniques page, which showed ID+name inline with a fixed 5-color status
palette. Unifying the two pages' visual style requires both pieces of
data to exist server-side.
- Adds "name" to all 4 layer builders (coverage/threat-actor/detection-
rules/campaign) — harmless extra field for Navigator exports, lets the
frontend show it inline.
- Adds "status" (the real TechniqueStatus value) to the coverage and
threat-actor layers specifically — the only two backed by an actual
TechniqueStatus concept; detection-rules/campaign scores don't
correspond to one and don't get this field.
GET /tests caps limit at 200, so any page relying on len(results) for a
"total" count (e.g. Validated Tests) silently under-reports once there
are more matches than the page size — the 200 shown was a page cap, not
the real total.
- Adds GET /tests/count, sharing its query-building with list_tests()
via a new _build_test_query() so the two can't drift apart.
- Adds technique_search (free-text on MITRE ID or name), attack_success,
detection_result, and validated_from/validated_to filters, so callers
aren't limited to state/technique_id/platform/creator.
POST /system/jira-test bundled 4 independent checks (enabled, url,
admin_email, admin_api_token) behind has_admin_jira_configured() but
always blamed "Admin Email and Admin API Token" regardless of which one
failed. Found via live QA: URL/email/token were all correctly saved, but
the "Enable Jira Integration" toggle was never switched on — the error
message pointed at the wrong fields, making it look like the already-set
credentials were missing.
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.
_REDACTED_KEYS listed jira.api_token/jira.password/tempo.api_token, but
routers/system.py actually writes admin credentials under
jira.admin_api_token and tempo.admin_token. The mismatch meant every
config export bundle included the live Jira and Tempo admin API tokens
in plaintext instead of redacting them. Found while researching the
Jira/Tempo integration for a service-account request; added a
regression test.
The "Export Configuration" admin feature (GET/POST /admin/export-config,
/admin/import-config) was already fully implemented — admin-gated,
redacts secrets, scoped to genuine config (settings/webhooks/SSO/scoring/
custom templates/users) rather than bulk data — but had zero test
coverage for an admin-sensitive data-migration endpoint.
Adds coverage for: admin-only gating on both endpoints, secret redaction
(SMTP password, SSO private key), custom-template-only scoping, safe user
import (forced password reset, no secret restoration from "[REDACTED]"
placeholders), and round-trip import idempotency. All pass against the
existing implementation — no bugs found.
- start_execution now blocks a test from starting before its campaign's
start_date, closing a gap where the field was persisted but never
enforced
- GET /tests gains a reviewer_id "my reviews" filter for the red_review/
blue_review lead-gate stage, distinct from pending_validation_side
(which only ever covered the later in_review stage and ignored
assignment entirely)
- get_coverage_by_tactic now returns all 14 MITRE tactics in canonical
kill-chain order instead of an alphabetical, partial list — the
regular Dashboard and Executive Dashboard both consume this endpoint
and previously disagreed on order/completeness
- test-template listing now includes existing_test_count per technique
so the catalog can warn before creating a likely-duplicate test
Mirrors the existing Atomic/Caldera/Elastic/Sigma/LOLBAS import pattern —
D3FEND mapping import was the one source silently skipping the
review_required flag, so leads never got prompted to review techniques
that only gained new D3FEND coverage.
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.
- /auth/refresh now allows a short grace window past expiry and checks
the blacklist, so an active session's silent refresh no longer fails
the instant its own token expires
- normal manager /approve flow now creates Jira tickets for the campaign
and its already-linked tests, matching the admin-only /activate path
- GenerateFromActorPayload now accepts start_date and threads it through
to the new campaign instead of silently discarding it
Now that only the manager sets start_date, and only at the moment of
approval (which immediately activates the campaign), a draft campaign
can never have a start_date. This made three things permanently
unreachable: the hourly _run_scheduled_campaign_activation cron job,
the activate_campaign 409/force future-date guard, and a query filter
hiding tests from scheduled-but-inactive draft campaigns. Removes all
three rather than leaving dead code behind. Also adds the missing
manager-cannot-directly-activate router test.
Moves the local _post cookie-clearing helper into a shared 'api' fixture
in conftest.py so later router tests in this plan (Tasks 10/11) don't
have to reinvent or forget the TestClient cookie-vs-Authorization-header
gotcha. Also adds a one-line comment at both require_any_role("manager")
call sites clarifying admin passthrough is automatic.
serialize_modification_request crashed toward a string 'None' instead of
JSON null for requests whose test_id was nulled by the SET NULL cascade.
Also corrects two comments that still described the debunked ordering
theory instead of the actual SET NULL fix, and fixes a test that
asserted post-cascade state without committing first (the ORM only
picks up out-of-band DB-side SET NULL on already-loaded objects after
expire_on_commit forces a re-read, matching real router behavior).
Approving a remove_test modification request deletes the underlying Test
row, which was cascading through test_id's ON DELETE CASCADE and wiping
out the request row itself before it could be read back. Changed to
ON DELETE SET NULL so the audit record (justification, reviewer,
decision) survives. Adds regression coverage plus double-approve/reject
idempotency tests.
- Single admin Jira account stored in system_configs (jira.admin_email, jira.admin_api_token)
- Admin Tempo token in system_configs (tempo.admin_token)
- get_admin_jira_client() + has_admin_jira_configured() replace per-user auth in all lifecycle hooks
- lookup_user_jira_account_id() auto-discovers each user's Atlassian accountId by email on login
- auto_log_test_worklog() now uses admin Tempo token and logs both red+blue team time
- Settings > Jira: admin credentials fields + test buttons moved to admin Jira tab
- Profile section simplified: jira_account_id shown read-only (auto-detected)