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: 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>
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>
TestCatalogPage: 'Use Template' button had no role check — any user
(including viewer/blue_tech/red_tech) could see and click it, which
would fail at the backend (POST /tests/from-template requires
red_lead|blue_lead). Added canUseTemplate check; button hidden for
viewer, blue_tech, red_tech.
TechniqueDetailPage: 'Run This Test' / 'Re-run' buttons in the
Available Templates section also had no role check. Added canRunTemplate
(same criteria: admin|red_lead|blue_lead). The 'View test' button for
active tests remains visible to everyone (read-only navigation).
Principle: if a user cannot perform the action, the button does not
appear — no permission error messages, just absence of the control.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. /executive-dashboard: add 'viewer' to ProtectedRoute roles — sidebar
showed the link to viewers but the route redirected them to /dashboard.
2. /comparison: same fix — viewer was in sidebar roles but not in route.
3. /techniques/review-queue: add ProtectedRoute (leads+admin) — the page
had no route-level protection, any authenticated user could access it.
4. TechniqueDetailPage review banner: hide from users who can't act on it.
Previously shown to everyone with a 'Leads only' badge; now only shown
to canReview users (admin/red_lead/blue_lead). Non-leads don't need to
see alerts about changes they cannot acknowledge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the Digital Operational Resilience Act as a compliance framework
using the same pattern as CIS Controls v8 (hardcoded curated mappings,
no official STIX bundle exists for DORA).
22 controls across 5 chapters mapped to MITRE ATT&CK techniques:
Ch. II — ICT Risk Management (Art. 5–15): governance, identification,
protection, detection, response, backup, threat intel
Ch. III — Incident Management (Art. 17–19): classification, reporting
Ch. IV — Resilience Testing (Art. 24–27): general testing + TLPT
(Art. 26 explicitly based on TIBER-EU/ATT&CK threat-led testing)
Ch. V — Third-Party Risk (Art. 28, 30, 42): supply chain, trusted rels.
Ch. VI — Information Sharing (Art. 45)
Technique mappings derived from ENISA DORA guidelines and TIBER-EU framework.
Import is triggered via POST /api/v1/compliance/import/dora (admin only).
Frontend: new 'DORA' button in the Compliance page import section.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The useState hook was placed after the isLoading/error early returns,
violating the Rules of Hooks. First render hit the early return without
calling the hook; second render (after data loaded) called it, producing
'more hooks than previous render' — React error #310 and a white screen.
Moved const [showTemplateModal] to the state block at the top of the
component, alongside the other useState declarations.
Previously a JS rendering error produced a blank white screen with no
feedback. PageErrorBoundary now catches the error, shows the error
message + stack trace, and offers a reload button. This will surface
the exact crash message for the inaccessible test page.
Adds a 'Save as Template' button in the Details sidebar (visible to
red_lead, blue_lead and admin only). Opens a modal pre-filled from
the test's own fields:
test.name → template name
test.description → description
test.platform → platform
test.procedure_text → attack_procedure
test.tool_used → tool_suggested
test.technique_mitre_id → mitre_technique_id
User can also set severity and write expected_detection (Blue Team
guidance — not stored on tests). Calls POST /test-templates with
source='custom' on submit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/techniques (technique browser with filters) was an orphaned route —
only reachable via 'Back to techniques' button or direct URL.
Now exposed in the sidebar as part of a new ATT&CK group:
ATT&CK ▾
Techniques → /techniques
Coverage Matrix → /matrix
Review Queue → /techniques/review-queue (leads+admin only)
Child role filtering added to SidebarLink.
Review Queue badge moved to the ATT&CK group header.
CSS grid was stretching both cells in a row to the same height as the
tallest card, making the unexpanded card appear 'open' with blank space.
items-start makes each card only as tall as its own content.
def.id was undefined for D3FEND items, causing expandedId===undefined
to match ALL items simultaneously and opening every card at once.
d3fend_id (e.g. 'D3-DSDP') is always present and unique per defense.
Techniques don't have their own Jira tickets — tickets exist on tests
and campaigns. The previous JiraLinkPanel entityType='technique' always
returned empty.
Backend: add entity_ids (list) filter to GET /jira/links so multiple
test IDs can be fetched in a single request.
Frontend API: listJiraLinks() accepts entity_ids[] and serialises them
as repeated query params (required by FastAPI List[UUID] parsing).
TechniqueDetailPage: replace JiraLinkPanel with TechniqueJiraSection —
a dedicated read-only component that:
- Takes technique.tests (already loaded)
- Batch-fetches all test Jira links in one request
- Shows test name + ticket key + status + priority + open-in-Jira link
- Hides itself when no tickets exist (avoids empty panel)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WorklogTimeline: add readOnly prop — hides 'Log Time' button and form.
TestPhaseTimeline: remove 'Sync to Tempo' button from TempoSyncBadge;
only displays the green 'Tempo' badge when already synced. Cleans up
unused imports (useState, useMutation, useQueryClient, syncTestToTempo).
CampaignDetailPage: JiraLinkPanel and WorklogTimeline both now rendered
with readOnly=true; JiraLinkPanel receives campaign name as label.
Jira tickets and time worklogs are created automatically by the system
(campaign activation, test workflow) — no manual editing from detail pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Campaign test deletion: removing a test from a campaign now also
deletes the underlying Test record and recalculates technique status.
2. Review Queue triggers: review_required=True is now also set when
- Sigma/Elastic detection rules are imported for a technique
- A test is validated (coverage status changes)
3. Test detail — Technique link: 'Technique' entry added at the top of
the Details sidebar showing MITRE ID + name as a clickable link to
/techniques/{mitre_id}.
4. Jira panel — read-only on test page: added readOnly + label props to
JiraLinkPanel. TestDetailPage now passes readOnly=true and the test
name as label, hiding Link Issue / Sync / Unlink controls (automatic
Jira creation only — no manual management).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each template card in 'Available Test Templates' now shows contextual
status derived from technique.tests (already loaded):
- Active test (draft/executing/evaluating/in_review):
blue 'Executing / In Review' badge + 'View test →' button
(prevents blind duplicate creation)
- Validated / detected (fresh):
green 'Detected' badge + dimmed 'Re-run' button
- Validated / not_detected or partial:
red/yellow result badge + full 'Run This Test' button (re-run encouraged)
- Validated but stale (review_required=true):
result badge + '⚠ Coverage may be stale' line
- No tests: normal 'Run This Test' button
No extra API calls — status is derived from the technique detail
already in-memory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: add not_in_any_campaign filter to list_tests (subquery on
CampaignTest) and expose it as a query param on GET /tests.
Frontend: the 'Existing Test' tab now requests only
state=draft & not_in_any_campaign=true
so tests already linked to any campaign or not in draft state
are never shown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- From template: name is pre-filled as '[Campaign] {template.name}'
(user can edit before confirming).
- Existing test: renamed via PATCH /tests/{id} to prepend '[Campaign] '
before being linked to the campaign, consistent with the APT-generated
campaign flow.
Idempotent — skips rename if the name already starts with '[Campaign]'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The modal now has two tabs:
- 'From Template' (default): searchable/filterable template catalog
→ select template → customise name/platform/procedure/tool
→ 'Create & Add to Campaign' (two-step: POST /tests/from-template
then POST /campaigns/{id}/tests)
- 'Existing Test': previous behaviour — add an already-created test
Both tabs share an added-count footer badge.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /techniques/review-queue page: lists all techniques flagged for
review after a MITRE ATT&CK sync, grouped by tactic. Leads and admins
can mark each one reviewed inline without leaving the page.
- Sidebar: 'Review Queue' link (admin/red_lead/blue_lead only) with an
amber badge showing the live pending count.
- TechniqueDetailPage: amber banner when review_required=true explaining
what happened and who can act; 'Mark as Reviewed' button now amber
coloured for visual distinction. 'Leads only' chip shown for blue_tech.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The tick/cross buttons navigated to /tests/:id/validate and /tests/:id/reject
which are non-existent routes (catch-all redirected to dashboard).
Removed both buttons; the View (FileText) icon is the correct entry point
to the test detail page where the full workflow lives.
(Citation: ...) patterns are stripped from body text, replaced with
Unicode superscript numbers (¹²³), and shown in a compact "Sources"
section below — collapsed when there are more than 3, expanded otherwise.
Deduplication ensures the same citation reference appears only once.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New shared MarkdownText component (react-markdown + remark-gfm)
that renders links, bold, italic, lists, code, blockquotes.
External links open in a new tab with rel=noopener.
- Applied to: technique description, threat actor description,
test description, campaign description, detection rule descriptions,
D3FEND defense descriptions, red/blue summaries and validation notes.
- procedure_text (code/commands) stays in <pre> — not processed as MD.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar: add `end` prop to child NavLinks so "All Tests" (/tests) is
only highlighted when exactly on /tests, not on /tests/validated.
- Backend: recalculate technique status_global for all affected techniques
when tests are deleted via delete_campaign(delete_tests=True), preventing
stale coverage metrics on the dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /tests/validated page with its own route and sidebar link, showing
only validated tests with Attack and Detection result badges.
- Removed the duplicate "My Pending Tasks" sidebar entry (same as All Tests).
- All Tests table no longer shows validated tests; clicking the Validated
counter card navigates to the new page instead.
- Validated option removed from the state filter dropdown in All Tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Validated tests no longer appear in the active tests table; they are
shown in a dedicated collapsible "Validated Tests" section at the bottom
(with its own sortable table and count badge).
- Added "Waiting" column to the main table showing elapsed time since
last update for blue_evaluating tests, sortable so Blue Team can
prioritise the oldest pending evaluations.
- Sorting by Waiting pushes blue_evaluating rows to the top and orders
them oldest-first by default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The form captured name/description/platform/procedure/tool edits but
never sent them — the created test always used the raw template values.
- TestTemplateInstantiate schema: add optional override fields
(name, description, platform, procedure_text, tool_used)
- create_test_from_template service: accept *_override kwargs;
use override value when provided, fall back to template value
- Router: pass all override fields from payload to service
- Frontend API createTestFromTemplate: accept overrides object, spread into body
- TestFromTemplateForm: pass all form state values as overrides
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- technique_query_service.get_technique_detail() now queries DetectionRule
by mitre_technique_id == mitre_id (same field the heatmap uses)
- Rules sorted: critical → high → medium → low → informational, then alphabetically
- Returns: id, title, description, source, source_url, rule_format,
severity, platforms, false_positive_rate
Frontend:
- New DetectionRulesSection component with expandable rows per rule
- Color-coded severity dots and badges (red/orange/yellow/blue/gray)
- Source badges (sigma=purple, elastic=blue, splunk=orange, custom=cyan)
- Shows format, false positive rate, platforms, source link on expand
- Empty state when no rules exist
Fixes: T1189 showed green in heatmap but no rules on detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Auto-trigger POST /risk/compute on first load if no profiles exist
- Add "Refresh scores" button next to Critical Gaps header (spins while computing)
- Add computeRiskScores() to frontend/src/api/risk.ts
- After compute, invalidate risk-profiles query so table updates immediately
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Create frontend/src/api/risk.ts with getRiskProfiles() API function
- Executive Dashboard fetches risk profiles and builds a techniqueId→profile map
- Critical Gaps sorted by risk_score DESC (highest risk shown first)
- Ties resolved: not_covered before not_evaluated; unscored techniques last
- Table now shows Risk Score (0-100, color-coded) and Risk Level badge per row
- Column renamed to "Critical Gaps — Top 10 by Risk Priority"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: add DELETE /campaigns/{id}?delete_tests=bool endpoint
- Backend: add delete_campaign() service — handles draft-only restriction,
optional test deletion, nullifies child campaign FKs
- Backend: remove early Jira ticket creation from POST /campaigns,
POST /campaigns/{id}/tests, and POST /campaigns/from-threat-actor
- Backend: activate endpoint now creates campaign Jira ticket if missing,
then creates test tickets (all deferred from creation to activation)
- Frontend: add deleteCampaign() API function to campaigns.ts
- Frontend: two-step confirmation dialog on CampaignDetailPage —
first confirms deletion, then asks whether to also delete associated tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove WorklogTimeline (manual time log) from test detail page
- TestPhaseTimeline now accepts testId, fetches its own worklogs,
and shows Tempo sync status on the Red Team Execution row:
• green badge if already synced (with worklog ID tooltip)
• 'Sync to Tempo' button (blue) if not yet synced
- Add POST /tests/{id}/sync-tempo backend endpoint for manual sync:
finds unsynced red_team_execution worklogs and pushes them to Tempo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a View button (eye icon) on each evidence card for previewable file
types. Opens a full-screen modal:
- Images (png/jpg/gif/webp/svg/…): rendered directly via <img> tag
- JSON: fetched authenticated, pretty-printed in green mono
- Text/log/md/csv/xml/yaml/…: fetched authenticated, shown in <pre>
Non-previewable files only show the Download button as before.
Modal closes on Escape or backdrop click.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>