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.
Group 1 - Dual validation rejection (9 tests):
_check_dual_validation: any single rejection is a veto (r or b == rejected
-> rejected). Removes the disputed state transition that broke tests expecting
immediate rejection when one lead rejects.
Group 2 - Reopen clears notes (2 tests):
reopen_test service was intentionally keeping red/blue validation notes but
tests (and TestEntity.reopen domain method) expect them cleared. Align service
with domain entity behavior.
Group 3 - Audit integrity hash (2 tests):
log_action: call db.refresh(entry) after initial flush and before computing
the HMAC hash. Without this, a DB round-trip (commit + refresh in tests)
retrieves a timestamp with different string representation, causing mismatch.
Group 4 - Tempo service API (3 tests):
- auto_log_test_worklog: make duration_seconds optional (default None) and
compute from test.red_started_at -> updated_at when not supplied.
- Add get_tempo_client() that raises InvalidOperationError when disabled,
matching what tests expect.
- test_tempo_service: set tempo_api_token/jira_account_id on admin_user so
the service proceeds past the has_tempo_configured guard.
Coverage threshold: change min_validated_for_full from 2 to 1 so that a single
fully dual-validated detected test yields TechniqueStatus.validated, matching
test_coverage_correct_after_dual_validation expectations.
Snyk scan found 3 High severity vulns: two in ecdsa (pulled by python-jose)
and one in diskcache (pulled by pySigma, never imported). Remove both
vulnerable dependencies and migrate JWT handling to PyJWT. Fix
test_logout_revokes_token which broke because test stubs sys.modules[jose]
with a MagicMock at collection time; test now uses PyJWT directly.