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>
- tempo: remove unsupported `workType` kwarg from create_worklog call;
tempoapiclient v4 does not accept it → was causing every Tempo sync to fail
- tests: set created_at=datetime.utcnow() explicitly on test creation (both
create_test and create_test_from_template) since the DB column has no
server default, causing 'Created —' in the UI
- jira: remove duplicate Proof of Concept section from ticket description body;
PoC already lives in customfield_10309, no need to repeat it in description
- ui: add TestPhaseTimeline component (read-only) showing RT execution time,
blue queue time, blue evaluation time and lead validation timestamps derived
from test phase timestamps; placed above WorklogTimeline in test detail page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jira — PoC custom field:
- Add customfield_10309 (Proof of Concept) to issue fields when creating
test tickets so the attack procedure appears in the dedicated Jira field
Tempo — blue team exclusion:
- Remove blue_team_evaluation from _TEMPO_ACTIVITY_TYPES; blue team time
is tracked internally (worklogs) for SLA but never sent to Tempo since
blue team has no Jira access
Evidence — uploaded_at NULL fix:
- Set uploaded_at=datetime.utcnow() explicitly in upload_evidence router;
the DB column has no server default so it was saving as NULL
Evidence — presigned URL browser access:
- Add MINIO_PUBLIC_ENDPOINT setting (config.py, docker-compose.prod.yml)
- storage.py uses a dedicated _public_client for presigned URL generation
so browsers receive URLs with the publicly accessible hostname instead of
the internal Docker service name (minio:9000)
- Expose MinIO port 9000 in docker-compose.prod.yml
Evidence — Jira attachment:
- After upload to MinIO, call jira.add_attachment() to attach the file to
the linked Jira ticket (non-fatal; errors are logged and swallowed)
Settings — hide Jira/Tempo from blue team:
- ProfileSection checks user role; blue_lead and blue_tech do not see the
Jira Integration or Tempo Integration personal settings sections
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The field is the general parent (e.g. OFS-20795) under which campaigns
are created directly. 'Campaign Parent Ticket' was misleading.
Standalone Tests Parent Ticket remains separate (e.g. OFS-20798).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Jira status → In Progress on Start Execution
- push_test_event calls set_issue_status("In Progress") when
new_state == "red_executing" (non-fatal, separate try/except)
2. Jira assignee set on Start Execution
- assign_issue() called with actor.jira_account_id when operator
clicks Start (non-fatal)
3. Standalone tests parent ticket (OFS-20798)
- New jira.parent_ticket_standalone config key
- get_jira_parent_ticket_standalone() falls back to parent_ticket
- auto_create_test_issue uses standalone parent for non-campaign tests
- Exposed in /system/jira-config GET+PATCH and SettingsPage UI
4. Tests table: Created + Updated columns
- Add Created column (created_at), fix Updated to show updated_at
- Both use UTC-aware date parsing (append Z if no tz suffix)
- updated_at added to Test TypeScript interface
5. Sortable columns in tests table
- All 7 columns sortable: Name, Technique, State, Current Team,
Platform, Created, Updated
- Click to sort asc, click again to reverse; ChevronUp/Down indicator
- Default sort: Created desc (newest first)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend returns naive UTC datetimes without 'Z' suffix. JavaScript
new Date("2026-05-27T09:29:18") parses as local time (UTC+2 in Spain),
making the timer start at 02:00:06 instead of 00:00:00.
Fix: append 'Z' to any timestamp string that lacks timezone info before
passing it to new Date(), so the browser always interprets it as UTC.
Applied to both startedAt and pausedAt in LiveTimer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously blue_started_at was set when the RED team submitted evidence
(= queue open time), so Tempo was getting total queue wait time instead
of actual work time.
Changes:
- DB: add blue_work_started_at column (migration b045), set when a blue
tech explicitly picks up the test (mirrors red_started_at for red team)
- Workflow: new start_blue_work() function + POST /tests/{id}/start-blue-work
endpoint (blue_tech / blue_lead roles). Cannot be called twice.
- submit_blue_evidence: uses blue_work_started_at (when available) as the
phase start for the Tempo worklog, falls back to blue_started_at
- reopen_test: clears blue_work_started_at alongside other timing fields
- Tempo: both red_team_execution and blue_team_evaluation now synced;
correct work_date and description per activity type
- Frontend: "Start Evaluation" button shown in blue_evaluating state when
blue_work_started_at is null; live timer shows from pick-up time
What each timestamp tracks:
blue_started_at = queue entry (SLA / internal tracking)
blue_work_started_at = pick-up by blue tech (Tempo start)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each user can now store their own personal Tempo API token in their
profile settings. Time is logged using each user's own credentials.
Backend:
- Migration b044: adds tempo_api_token column to users table
- User model: adds tempo_api_token column
- UserPreferencesUpdate: adds tempo_api_token field (write-only)
- UserOut: adds tempo_api_token (excluded) + tempo_token_set bool;
@model_validator derives both jira_token_set and tempo_token_set
- users router: handles tempo_api_token same as jira_api_token
(empty string clears it, never returned in responses)
- tempo_service: refactored to per-user token; has_tempo_configured(),
get_user_tempo_client(user) use user.tempo_api_token; global
TEMPO_ENABLED still acts as kill-switch
- system router: /system/tempo-test now uses current user's personal
token (any role); removed global TEMPO_API_TOKEN dependency
Frontend:
- settings.ts: UserPreferencesUpdate.tempo_api_token, UserMeOut.tempo_token_set
- SettingsPage ProfileSection: Tempo Integration section with password
field, show/hide toggle, configured badge, and Test Tempo button —
mirrors the Jira token UX exactly
- JiraConfigSection: removed stale global Tempo test block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jira URL fix:
- JiraLinkPanel now fetches the configured Jira base URL via getJiraConfig()
instead of hardcoding https://jira.atlassian.com; falls back to the old
value if config is not yet loaded
Description fix:
- _build_test_description: renamed 'h3. Procedure' -> 'h3. Proof of Concept'
so the procedure/tool block maps to the correct Jira field label
Tempo debug:
- New POST /system/tempo-test endpoint: checks TEMPO_ENABLED, token,
user jira_account_id, and makes a real API call; always returns HTTP 200
with status field (Cloudflare-safe)
- docker-compose.prod.yml: added TEMPO_ENABLED, TEMPO_API_TOKEN,
TEMPO_DEFAULT_WORK_TYPE env vars (default off, ready to enable)
- SettingsPage: added 'Test Tempo Connection' button in Jira admin tab
with clear feedback showing what's missing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- jira-test now returns {status: "ok"|"error", message: ...} with
HTTP 200 so Cloudflare never intercepts the response
- jira_service strips trailing slash from URL before creating Jira
client (avoids double-slash in REST paths)
- Frontend reads data.status field instead of HTTP status code
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Using setQueryData with the PATCH response means jira_token_set is
reflected in the UI instantly — no extra GET round-trip that could
leave the badge stale.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace render-body setState with useEffect so field initialisation
is idiomatic React and never races with user input. Also clarifies
placeholder text: empty token field = keep current, not clear it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users can now set a separate Atlassian email for Jira authentication
in Settings → Profile → Jira Integration. Falls back to the Aegis
account email when not set, so existing setups are unaffected.
- Migration b043: adds jira_email column to users table
- User model/schema: expose jira_email read/write
- jira_service: _effective_jira_email() uses jira_email ?? email
- Frontend: replaces read-only email display with editable input
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- backend: add parent_ticket field to JiraConfigOut/JiraConfigUpdate/_JIRA_KEYS
- backend: add get_jira_parent_ticket() helper in jira_service; use it in auto_create_test_issue() to set issue parent
- frontend/api: add jira_token_set to UserMeOut, jira_api_token to UserPreferencesUpdate, and full JiraConfigOut/Update types with getJiraConfig/updateJiraConfig/testJiraConnection functions
- frontend: expand ProfileSection with Jira API token password field (show/hide), token status badge, and account-id field
- frontend: add JiraConfigSection component (admin): enabled toggle, URL, project key, parent ticket, save + test connection
- frontend: add Jira tab (admin-only) with Link2 icon in SettingsPage sidebar