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)
Tar Slip (CWE-22) — 3 import services:
threat_actor, lolbas, caldera: add path validation before extractall()
to prevent malicious zip members with ../ escaping the target directory.
(sigma, elastic, atomic already had this protection)
Path Traversal (CWE-23) — professional_reports.py:
Add _assert_safe_report_path() check on all 5 report endpoints to
verify the generated filepath stays within REPORT_OUTPUT_DIR.
Open Redirect (CWE-601) — sso.py:
Validate IdP redirect URL scheme (must be http/https) before
issuing RedirectResponse, blocking javascript: and data: redirects.
DOM XSS (CWE-79) — 4 frontend pages:
Create src/utils/url.ts with safeUrl() that rejects non-http/https
protocols; apply to actor.mitre_url, ref.url, intel.url.
Sanitize framework name to alphanumeric-only before DOM insertion.
Restrict evidence MIME types to an explicit safe allowlist (png/jpg/gif/webp).
Hardcoded credentials (CWE-798):
verify_gaps.py, create_wiki.py: replace literal passwords with
environment variable reads (AEGIS_ADMIN_PASSWORD, GITEA_PASSWORD).
- config.py: move REPORT_OUTPUT_DIR from /tmp (world-writable) to /app/reports
to prevent CWE-377 symlink attack vector (B108, only real security issue)
- main.py: log startup seed failures instead of silently swallowing them (B110)
- Add # nosec annotations to intentional try/except patterns that are by design:
Jira integration errors, email failures, DetachedInstanceError, storage errors,
and Jira session timeout (all B110/B112 false positives)
- Add # nosec B105 to false positives where bandit misidentifies config key
names and masking strings as hardcoded passwords
- Add .bandit config to skip B311 in seed_demo.py (random used for fake
demo data generation, not cryptographic purposes)
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.