Backend:
- campaign_service.generate_campaign_from_threat_actor: accept optional
start_date kwarg and set it on the Campaign model
- campaigns router: new GenerateFromActorPayload schema, /from-threat-actor
endpoint now accepts optional body with start_date
Frontend:
- generateCampaignFromThreatActor API: accept optional options param
- Generate Campaign modal: date picker + warning message, same UX as the
manual create form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- package.json: bump axios constraint from ^1.13.5 to ^1.14.0
- Dockerfile build stage: npm ci -> npm install so the semver range
in package.json is honoured at build time (npm ci uses the lockfile
exactly, bypassing the updated constraint)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bumps minimum Axios version from 1.13.5 to 1.14.0 to remediate
CVE-2026-42043 identified by VMT / Wiz (asset: AegisTest).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DB: migration b047 adds start_date (DateTime nullable) + index to campaigns.
Backend:
- Campaign model: start_date field
- CampaignCreate/Update schemas: accept start_date (ISO string)
- CRUD service: persist + serialize start_date in both serializers
- Activation endpoint: blocks manual activation if start_date is in the future
(campaign will auto-activate via scheduler)
- Scheduler: new hourly job _run_scheduled_campaign_activation — finds draft
campaigns with start_date <= now, activates them, creates Jira tickets,
notifies red_tech team
- Jira: campaign + test tickets now include JIRA_START_DATE_FIELD (configurable,
default customfield_10015). Campaign uses start_date if set, else created_at.
Tests inherit campaign start_date.
- config.py: JIRA_START_DATE_FIELD setting
Frontend:
- Campaign type: start_date field on Campaign + CampaignSummary
- CampaignCreatePayload: start_date optional field
- Create form: date picker with min=today, warning message explaining behavior
- Campaign detail header: start_date badge showing days remaining or started date
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Progress and Timing now share a 2-column grid at the top of the detail page.
Removed CampaignTimingPanel from the bottom Jira section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Amber banner for DORA and ISO 27001:2022 — community-based mapping, no official CTID source.
Orange banner for ISO 42001:2023 — experimental, MITRE ATT&CK has no AI-specific techniques yet.
Each notice explains the mapping source, limitations, and what executives should consider
before using the data in formal audits or regulatory submissions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: expose description in control status response, add rich business-language
descriptions to all curated controls (ISO 27001, ISO 42001, CIS v8, DORA) explaining
requirements and ATT&CK mapping rationale. ISO 42001 includes infrastructure-mapping note.
Frontend: description field in type, info panel in ControlsTable expanded rows,
framework info banner with description and official standard link in CompliancePage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ISO 27001:2022: 37 Annex A controls across 4 themes (Organizational,
People, Physical, Technological) mapped to MITRE ATT&CK techniques.
ISO 42001:2023: 25 Annex A controls for AI Management Systems mapped to
relevant ATT&CK techniques covering AI supply chain, data pipeline
integrity, model serving security, and third-party AI risk.
Backend: import functions, _import_curated_framework() shared helper,
and POST /compliance/import/iso-27001 + iso-42001 endpoints.
Frontend: API client functions + import buttons in CompliancePage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lead who approved: Request Discussion button becomes Discussion Requested after sending.
Lead who rejected: new Change to Approved button to resolve conflict after offline discussion.
Both leads retain vote-change buttons. discussionSent state flag tracks send status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- request-discussion endpoint: add 'admin' to allowed roles
- Return rejector_email and rejector_role in the response
- Modal success state shows contact card with username, role, email link
so the approving lead can immediately reach out to the rejecting lead
Backend: POST /tests/{id}/request-discussion
- Only callable by the lead whose vote is 'approved' in a disputed test
- Sends notification to the rejecting lead: 'Lead X confirms their
approval and wants to discuss your rejection'
- Logs the action in audit trail
Frontend:
- 'Confirm My Validation' button (amber outline) alongside 'Change to Rejected'
- Opens a modal showing:
* Explanation: both leads must agree to finalise
* Other lead's rejection reason/notes
* What happens next (stays disputed, notification sent, either can change)
- 'Send Discussion Request' → calls the new endpoint → shows success state:
'Lead username has been notified...'
- Instruction to reach out via team channels to resolve offline
Flow summary for disputed tests:
Approving lead sees 2 options:
a) 'Confirm My Validation' → modal → send request → other lead notified
b) 'Change to Rejected' → validation modal → both agree to reject → rejected
test_entity.py has its own TestState enum separate from domain/enums.py.
Only domain/enums.py was updated, causing AttributeError when SQLAlchemy
tried to map 'disputed' from DB to the test_entity.TestState class.
Also adds disputed to VALID_TRANSITIONS so the entity can transition
into and out of the disputed state.
1. New 'disputed' state — one lead approved, the other rejected:
- Both approved → validated (unchanged)
- Both rejected → rejected (unchanged)
- One approves + one rejects → disputed (new)
- DB: ALTER TYPE teststate ADD VALUE 'disputed'
- Notification sent to the approving lead explaining the conflict
with the rejection notes
2. Disputed UI in TestDetailHeader:
- Amber banner showing conflict + rejection reason from notes
- 'Change Vote to Rejected' button for the lead who approved
- Validation indicators shown for disputed state too
3. Fix timestamps on reopen (rejected → draft):
- Keep red_started_at, blue_started_at etc. as historical record
- Only clear paused_at defensively
- Timestamps naturally update when test is re-executed
4. disputed badge (amber) added to all badge color maps
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend (reopen_test):
- Preserve red/blue validation NOTES — teams see exactly what to fix
without losing the rejection context. Previously both notes were cleared.
- Preserve all content fields: procedure_text, tool_used, red_summary,
attack_success, blue_summary, detection_result (already the case).
- Preserve evidences (separate table, unaffected — already the case).
- Still clear: validation statuses + who/when validated (fresh re-validation
required). Phase timing reset so the new execution starts clean.
Frontend:
- Button label: 'Reopen Test' → 'Continue Test' (more accurate intent)
- Dialog title: 'Reopen Test' → 'Continue Test'
- Dialog message: replaces alarming 'workflow will be restarted / clear all'
with accurate description of what is preserved vs reset
- Toast: explains what to do next
useMemo executes its factory immediately on first render. lastActivityDate
was defined with const after the useMemo call inside the component, causing
a temporal dead zone: 'Cannot access v before initialization'.
Fix: move the function to module scope (before the component), where it
is fully initialized before any hook runs.
TestsPage 'Updated' column: compute lastActivityDate() from the most
recent available timestamp — blue_validated_at > red_validated_at >
blue_work_started_at > blue_started_at > red_started_at > created_at.
Also fixes the sort-by-updated_at case.
ValidatedTestsPage 'Validated' column: use blue_validated_at (when Blue
Lead approved) falling back to red_validated_at. Fixes both the display
and the default sort-by-validated.
updated_at column does not exist in the tests table — it was always
undefined, so formatElapsed() always returned '—'.
Replace with blue_started_at (set when Red Team submits to Blue Team),
which correctly shows how long a test has been waiting for Blue Team
evaluation. Also fixed the waiting_time sort to use the same field.
- Global document paste listener captures image/* items from clipboard
- Auto-generates filename: screenshot-YYYY-MM-DDTHH-MM-SS.png
- Brief cyan pulse animation confirms the paste was detected
- Shows image preview before uploading (max-h 192px, object-contain)
- Drop zone hint now says 'Drag & drop, browse, or Ctrl+V to paste'
- Works with any source: OS screenshot (PrintScreen/Cmd+Shift+4),
browser Inspect screenshots, any image copied to clipboard
Root cause: avg times were ~2-3 minutes (< 1h). round(0.033, 1) = 0.0
which is falsy in JS, so the frontend showed N/A instead of the value.
Fix (backend): _safe_stats() and team metrics now convert to minutes
when avg < 1 hour, adding a 'unit' field ('min' or 'hrs').
Fix (frontend): use != null instead of truthy check for avg_completion_hours,
MTTD, MTTR — correctly shows 0.0 and uses the unit field to show 'min' or 'hrs'.
MTTD: was querying AuditLog for action names that don't match actual
logged actions. Now uses red_started_at → blue_started_at directly
(both stored on the Test record). Net of red_paused_seconds.
MTTR: was searching for remediation_status=completed (no data). Redefined
as total pipeline time: red_started_at → blue_validated_at net of all
paused time. Only counts fully validated tests.
Red avg time: was using red_validated_at - created_at (created_at NULL
for many tests). Now uses blue_started_at - red_started_at net paused.
Blue avg time: was using blue_validated_at - red_validated_at (wrong
phase boundary). Now uses blue_work_started_at (or blue_started_at
fallback) → blue_validated_at net of blue_paused_seconds.
- Convert horizontal bar chart to vertical bars (columns)
- Sort all 14 MITRE ATT&CK tactics in official order:
Reconnaissance → Resource Development → Initial Access → Execution →
Persistence → Privilege Escalation → Defense Evasion → Credential Access →
Discovery → Lateral Movement → Collection → C2 → Exfiltration → Impact
- Show ALL tactics (not a subset)
- Labels rotated -45° to fit all names
- Bars have rounded top corners; horizontal gridlines only
'Validation Throughput (tests/week)' was time-dependent — director wanted
an activity-based metric instead.
New metric: Pipeline Conversion Rate
formula: validated / (validated + rejected + in_review) × 100
unit: % (no time reference)
meaning: 'of all tests that have entered validation, X% succeeded'
trend: declining if in_review backlog > validated count,
improving if conversion ≥ 80%, stable otherwise
Backend: calculate_validation_throughput() rewritten — same API key
(tests_per_week) kept for compatibility, new conversion_rate field added.
Frontend: label → 'Pipeline Conversion', unit → '%', tooltip updated.
Replace single list with two-column layout:
- LEFT '⚠ Highest Exposure': top 5 actors by uncovered technique count,
red border, text explaining 'these attacks would go unnoticed today'
- RIGHT '✅ Strongest Detection': top 5 actors by coverage %, green border,
text explaining 'Blue Team would likely detect an intrusion from these'
Shows both the risks (where to focus testing) and the strengths
(what's already well protected) to give executives a balanced view.
New MetricTooltip component — a small ⓘ icon showing an executive-
friendly explanation panel on hover (CSS, no JS, instant).
DashboardPage: tooltips on all 6 coverage summary cards (Total
Techniques, Validated, Partial, In Progress, Not Covered, Not
Evaluated), Coverage Evolution chart, Test Pipeline funnel,
Team Activity and Validation Rate section headers.
ExecutiveDashboardPage: tooltips on all 4 sub-scores (Coverage,
Detection, Critical, Response), Score Trend, Top Threat Actors,
4 KPIs (MTTD, MTTR, Detection Efficacy, Validation Throughput),
Coverage by Tactic, Critical Gaps table, and all 6 team metrics
(Red/Blue Tests Done, Avg Time, Rejection).
Each tooltip explains what the metric measures, what a good/bad
value looks like, and what action to take — written for non-
technical executives.
Tempo rejects durations under 60 seconds ('Duration must be at least
one minute'). Now:
- Always send ≥ 60 s (1 minute minimum)
- Round UP to nearest whole minute (math.ceil)
- 2 s → 60 s, 3m20s (200s) → 240 s, 5m00s (300s) → 300 s
red_tech can only edit procedure/tool/summary when the test is in
red_executing state (after pressing Start Execution). In draft state they
see a read-only view and an orange hint 'Press Start Execution to begin
editing — the timer must be running first.'
blue_tech can only edit when blue_work_started_at is set (after pressing
Start Evaluation). Before that they see an indigo hint 'Press Start
Evaluation to begin editing — pick up the test first.'
red_lead, blue_lead and admin are unaffected — they retain full edit
access in all applicable states including draft.
Problem: 15-minute tokens with no refresh mechanism kicked users to login
even when actively using the app.
Fixes:
1. config.py: raise ACCESS_TOKEN_EXPIRE_MINUTES from 15 → 480 (8h).
Reasonable for an enterprise internal tool; still configurable via env.
2. POST /auth/refresh: new endpoint that reads the current aegis_token
cookie and issues a fresh token if the session is still valid. Returns
the new token in the cookie + body (same shape as /auth/login).
3. frontend/api/client.ts: response interceptor now attempts a silent
refresh on 401 before redirecting to login:
- Calls POST /auth/refresh once per failed request
- If refresh succeeds: retries the original request transparently
- If refresh fails: redirects to /login as before
- Deduplicates concurrent refresh attempts (refresh once, resolve all)
- Never attempts refresh on /auth/refresh or /auth/login themselves
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: Microsoft Teams Incoming Webhooks require MessageCard JSON
format. The service was sending generic Aegis JSON which Teams rejected
with a 400, incrementing failure_count on every dispatch.
Fix: _send_webhook() now auto-detects the target from the URL:
- webhook.office.com / teams.microsoft.com → Teams MessageCard
(colored card with event title + key/value facts table)
- hooks.slack.com → Slack attachments format
- everything else → current generic Aegis JSON
Also resets failure_count=0 in production so the webhook starts fresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- submit_red_evidence: raises InvalidOperationError if no Red Team
evidence file has been uploaded for the test
- submit_blue_evidence: raises InvalidOperationError if no Blue Team
evidence file has been uploaded
Frontend:
- 'Submit to Blue Team' button: disabled + '⚠ Upload evidence first'
hint when test.red_evidences is empty
- 'Submit for Review' button: same for test.blue_evidences
- Native tooltip on disabled buttons explains the requirement
- Buttons re-enable automatically after the first file is uploaded
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: GET /campaigns/{id}/timing-summary
Aggregates timing across all campaign tests:
- red_execution_secs: red_started_at → blue_started_at (minus paused)
- blue_queue_secs: blue_started_at → blue_work_started_at
- blue_evaluation_secs: blue_work_started_at → validated (minus paused)
- total_secs: sum of all three phases
Returns totals + per-test breakdown sorted by total time desc.
Frontend: new CampaignTimingPanel component replaces WorklogTimeline
- 4 summary cards: Red Execution / Blue Queue / Blue Evaluation / Total
- Stacked horizontal bar showing time distribution
- Per-test breakdown with individual mini-bars and phase durations
- Shows 'No tests started yet' when no timing data available
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
title= attribute tooltip is browser-native, tiny, and often invisible.
New StatusBadge component uses a Tailwind group-hover absolute panel
that appears immediately on hover with:
- Clear heading per status
- 'Meaning' and 'Action' lines
- Arrow pointing to the badge
- 200ms fade-in transition
Used in TechniquesPage (list table) and TechniqueDetailPage (header).
1. Status logic (v3): require ≥2 validated tests with 'detected' result
to reach 'validated' status. With only 1 validated+detected test the
technique stays 'partial' (single test is insufficient evidence).
Backfilled existing data: T1012 and T1059.001 downgraded to 'partial'.
2. Hover tooltips on status badges in TechniquesPage and TechniqueDetailPage:
- validated: ≥2 tests executed and detected
- partial: some tests done but incomplete coverage
- in_progress: tests exist but none validated yet
- not_covered: tests run but Blue Team didn't detect
- not_evaluated: no tests created yet
- review_required: recent update needs acknowledgment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously: alphabetical order (first 5 actors from list_actors query).
Now: ranked by uncovered technique count = technique_count × (1 - coverage_pct/100).
Tiebreak: higher technique_count first (broader attack surface).
Fetches 100 actors, sorts client-side, shows top 5 with:
- Rank badge (1-5) colored red/orange/yellow/gray
- 'N uncovered / M techniques' subtitle instead of target sectors
- Coverage bar + percentage
This ensures the actors with the largest coverage gap appear first.
Root cause: Cloudflare CDN was caching empty/error API responses from
/api/v1/metrics/* endpoints during the backend startup window (502 errors).
Subsequent requests were served from Cloudflare edge cache, never reaching
nginx or the backend, so the dashboard always showed empty metrics data.
NoCacheAPIMiddleware adds Cache-Control: no-store + Pragma: no-cache to
all /api/ responses so Cloudflare and browsers never cache them.
Root cause: after backend restart (502 errors on startup), metric queries
(pipeline, team, recent, validation) get cached in error state. When the
user stays on the dashboard, the component never remounts so queries don't
auto-retry.
Fixes:
1. refetchOnMount:'always' — queries ALWAYS refetch when component mounts,
even if cached with error/stale data. Prevents stuck empty state.
2. gcTime:0 — error state is not cached; next mount starts a fresh query.
3. retry:3 — more retries before giving up (covers slow startup windows).
4. Refresh button in header — manually invalidates and refetches all 4
metric queries with a single click. Spinner icon during refetch.
1. metrics_query_service: use NULLS LAST in get_recent_tests() so tests
with actual dates always appear before NULL-dated ones.
2. campaign_service: set created_at=datetime.utcnow() when creating tests
from campaigns (was missing, leaving 21 tests with NULL created_at).
Fixed existing NULL values directly in production DB.
3. DashboardPage: add isError handling to all V2 metric widgets
(pipeline, team activity, validation rate, recent tests).
- Add retry:2 to all secondary metric queries so transient failures
are retried before showing empty state.
- Show 'Could not load X — refresh' instead of empty/misleading
'No tests created yet' when a query actually fails.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Duplicate function definition: the old 2-param _entry_matches shadowed
the new 3-param version — Python uses the last definition, so the call
with 3 args threw TypeError. Removed the stale old definition.
2. NIST NVD deprecated their XML RSS feeds in 2023 — URL returns 404.
Replaced with SecurityWeek RSS which is active and covers CVEs/threats.
RT tests are created in 'in_review' state (not validated):
- red_validation_status = 'approved' (RT confirmed execution)
- blue_validation_status = null (pending Blue Lead review)
- detection_result is pre-filled from the import JSON
Blue Lead sees these in their normal validation queue and confirms
or rejects the detection result. Only after Blue Lead approval does
the technique coverage update to validated/not_covered/partial.
This gives Blue Lead oversight over RT findings rather than auto-
accepting external engagement results as ground truth.
Backend — POST /tests/import-rt (red_lead + admin):
Accepts engagement JSON with name/date/description/operator and
a list of techniques each with mitre_id, result, attack_success,
platform, notes. Creates one Test per technique directly in
'validated' state (red + blue validation = approved) bypassing
the normal workflow. Recalculates technique.status_global for
all affected techniques. Returns created/skipped summary.
Frontend — /tests/import-rt (new dedicated page):
- Format reference panel (collapsible) with field descriptions
- Download template JSON button (generates a filled example)
- Paste JSON textarea + file upload (.json)
- Live validation + preview table showing what will be imported
- Import button with spinner
- Success / warning / error result display
Accessible to admin and red_lead only.
Added to sidebar under Tests > Import RT Results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- intel_service: remove 50-technique limit (scan all techniques), improve
pattern matching with word boundaries (\bT1059\b), raise min name length
to 8 chars to reduce false positives, skip entries with empty titles
- technique_query_service: add intel_items to get_technique_detail() so
the technique page now shows recent threat intel articles (last 20)
- New GET /intel/items endpoint with optional technique_id filter
Frontend:
- New api/intel.ts with listIntelItems()
- ReviewQueuePage: complete redesign
* Expandable rows — click a technique to see its intel articles inline
* IntelPanel component fetches articles per technique on expand
* 'Create Template from Intel' button opens pre-filled modal:
name (from article title), source_url (article link), technique_id
User reads the article and fills the attack procedure
* Updated explanation text: lists all 3 reasons a technique can be flagged
(MITRE update / intel scan / new template or detection rule)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>