- All webhook emails now render as branded HTML (dark header, inline base64
Aegis logo, card layout, CTA-button links) instead of plain text.
- Wired the 7 remaining notification-preference keys that had no trigger:
stale coverage alerts, campaign-activated assignment emails, generic
test-state-change steps (execution started / blue evaluating / in review),
all-team-validation broadcasts on every lead vote, webhook delivery
failures (3rd consecutive failure), new user registration, and background
job errors (APScheduler global error listener).
- New notify_roles_by_email() helper for role-scoped, preference-gated,
actor-excludable broadcasts.
- Fixed apscheduler.events stubbing gaps in several test files' sys.modules
fakes that broke full-suite collection after adding the APScheduler
error-listener import.
Only the per-round verdict/timing fields (attack_success, execution
start/end for red; detection_result, containment_result, detection/
containment time for blue) get cleared on reopen — the operator must
answer those fresh for each round, and the prior round's answer stays
visible via round_history + the archived Phase Timeline card. Free-text
fields (procedure_text, tool_used, red_summary, blue_summary) are no
longer cleared: the operator edits them in place for the new round
instead of retyping everything from scratch.
- 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.
Admin administers the site — it no longer has any override on
validate-red/blue, review-red/blue, resolve-dispute, request-discussion,
reopen, hold, resume, assign-operators, or classification-update.
Introduces require_any_role_strict(), a role dependency without the
global admin bypass, so these specific endpoints truly exclude admin
instead of only removing it from the (redundant) role tuple.
Managers gain the ability to assign red_tech/blue_tech operators
(POST /tests/{id}/assign, GET /users/operators) alongside leads, since
that's coordination, not resolving a ticket.
Also enlarges and repositions the operator-assignment controls next
to the Start Execution button, and fixes a literal '\u2014' rendering
as text instead of an em dash in the test detail technique line.
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.
- 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
- Add test_test_entity.py with 46 pure unit tests covering the full domain entity
- Fix _FakeSettings in 11 test files (REPORT_TEMPLATES_DIR, JIRA, TEMPO)
- Fix stale db.commit assertions to db.flush after UoW refactor
- Add missing mock fields for TestEntity.from_orm compatibility
- Make database.py skip pool args for SQLite in test environment
- Disable slowapi rate limiter in test client fixture
- Inject test engine into app.database to fix threading errors
- Update role assertions to match current require_any_role policy
- Mark 6 legacy V1 endpoint tests as xfail (replaced by V2 workflow)
Foundational changes required before any new feature work can begin.
- 0.1 Redis infrastructure: add redis:7-alpine to docker-compose dev and prod,
REDIS_URL config, singleton client in app/infrastructure/redis_client.py
- 0.2 Token blacklist on Redis SEC-001: replace in-memory dict with Redis SETEX
keyed by jti, auto-expiring TTL derived from token exp
- 0.3 Database indexes SR-006: Alembic migration b019 with 5 composite indexes
for scoring, MTTD/MTTR, remediation, and notification queries
- 0.4 Domain exceptions TD-003: app/domain/exceptions.py with typed errors,
error_handler middleware mapping them to HTTP, services decoupled from FastAPI
- 0.5 Fix silenced exceptions TD-007: replace 4 bare except-pass blocks in
test_workflow_service with logger.warning with exc_info
- 0.6 CI pipeline TD-009: GitHub Actions workflow with Postgres and Redis
service containers, ruff lint, pytest; ruff.toml for baseline config