The /api/results/ endpoint returns a LIST: [{name: crowdstrike, adversaries: [...]}]
Previous code called data.get() on the list → AttributeError crash on every import.
Fix: detect list vs dict response, extract the crowdstrike vendor entry first,
then get its adversaries list. Keeps legacy dict fallback just in case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fallback names now use hyphens matching live API (carbanak-fin7, wizard-spider-sandworm)
- Add APT3 (R1) and Enterprise 2025/er7 (R7) to fallback - verified from live API
- Remove OilRig (R6) from fallback - CrowdStrike did not participate in Round 6
- Orange fallback banner only shows when NO rounds are available at all
- Soft gray note when rounds are loaded but API had transient error
- Check-new and import errors: detect 502/Cloudflare messages and show user-friendly text
instead of raw Cloudflare HTML error messages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add browser User-Agent and Referer headers to all evals.mitre.org requests
- fetch_rounds_with_status() returns api_reachable flag + rounds list
- Fallback to 5 known public CrowdStrike rounds (APT29/R2 through OilRig/R6)
when live API is blocked, so UI always shows something actionable
- Router returns {rounds, api_reachable, api_error} instead of plain array
- Frontend shows orange warning banner when using fallback data
- Remove 502 HTTPException - rounds are always returned (live or fallback)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each technique in the RT import JSON now requires at least one evidence
image (PNG/JPG/GIF/WebP/BMP, max 10 MB decoded) embedded as base64.
Backend:
- RTEvidenceEntry model: filename, data (base64), caption (optional)
- RTTechniqueEntry.evidence is now required
- Pre-validation raises 422 if any technique is missing evidence
- After test creation, images are decoded and stored in MinIO as
Evidence records (team=red) linked to the test
Frontend:
- RTEvidenceEntry type added to api/tests.ts
- parseJson() validates evidence presence and structure per technique
- Preview table shows base64 thumbnails (up to 3 + overflow count)
- Format reference updated: evidence fields moved to Required section
- Import result shows total evidence images attached
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build_threat_actor_layer was adding ALL techniques to the layer —
actor techniques with their real score and non-actor techniques with
score=0/enabled=False. This caused every tactic column to appear in
the matrix even when the actor has no techniques for that tactic.
Now only actor techniques are included. The frontend already filters
visible tactics to those with data, so empty tactic columns disappear
automatically.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: activate endpoint returns 409 with structured warning when
start_date is in the future; accepts force=true to bypass.
test_crud_service: always excludes tests from draft campaigns with future
start_date so they do not appear in the team queue prematurely.
Frontend: catches 409 on activate and shows amber confirmation modal
with Keep scheduled / Activate now anyway options.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
- 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
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.
'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.
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
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>
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>
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.
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>
MITRE ATT&CK STIX data never includes primary_motivation on intrusion-set
objects. Motivation is now derived with a 3-tier fallback:
1. Curated MITRE-ID override map (100+ known groups mapped by hand)
2. STIX primary_motivation field (if MITRE ever adds it)
3. Description keyword inference (financ/ransomware/espionage/
nation-state/destructive/hacktivist patterns)
Re-running MITRE sync will now backfill motivation for existing actors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. fix(search 500): func.cast(col, func.text()) is invalid SQLAlchemy —
replaced with cast(col, Text) for both aliases and target_sectors
JSONB columns. Generating correct CAST(col AS TEXT) SQL.
2. feat(motivation): extract primary_motivation and sophistication from
STIX intrusion-set objects during MITRE sync. Added _normalize_motivation()
to map STIX vocabulary → simplified frontend values (espionage / financial /
destruction / hacktivism). Both create and update paths now set these fields.
Run MITRE sync to backfill existing actors.
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>
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>
Extends the review queue triggers to cover test template imports:
- atomic_import_service: flags techniques when new Atomic Red Team
templates are imported
- caldera_import_service: same for Caldera templates
- lolbas_import_service: same for LOLBAS templates
- test_templates router (manual creation): flags the technique when
an admin/lead creates a custom template via the API
Pattern is identical to the Sigma/Elastic detection rule approach:
collect new mitre_ids during the loop, bulk-update after commit.
Manual creation does a single technique lookup and sets the flag
inside the existing UnitOfWork.
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>
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>
- 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>
When must_change_password is true the user must pick a genuinely new
password. Added a verify_password check against the existing hash before
accepting the new value, raising BusinessRuleViolation if they match.
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>
- 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>
FastAPI 0.136.1 + Pydantic 2.13.4 serialises responses via TypeAdapter which
calls the compiled Rust validator directly, bypassing any Python-level
`model_validate` classmethod override. The @model_validator(mode='before')
decorator IS invoked by the Rust pipeline, so the evidence red/blue split and
technique field population now run on every serialisation path.
Also eager-load technique in get_test_detail to avoid lazy-load surprises.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root causes found for Tempo worklogs never reaching Tempo:
1. Wrong API region: workspace is on api.eu.tempo.io/4 but code used api.tempo.io/4
→ Tempo returned "User is invalid" (400) for all POST /worklogs
2. Trailing space in jira_account_id stored in DB (now stripped with .strip())
3. tempo_synced field was never updated even on success (now set from Tempo response)
Fix: add tempo.base_url system_config key (admin-configurable without redeploy),
fall back to TEMPO_BASE_URL env-var, then global default. DB already updated with
https://api.eu.tempo.io/4 for this workspace.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accessing obj.evidences on a session-expired ORM object (mutation endpoints
do commit+refresh without joinload) triggers a lazy query that fails or
returns stale data. Use obj.__dict__.get('evidences') instead — SQLAlchemy
stores joinloaded relationships in __dict__; absent means not loaded.
Mutation endpoints (submit-red, submit-blue, etc.) return empty evidence
lists, which is fine: the frontend invalidates and refetches GET /tests/{id},
which uses joinedload and correctly populates red_evidences / blue_evidences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
tempo: tempoapiclient raises SystemExit (BaseException) on API errors like
'User is invalid' 400 responses; except Exception never catches it, killing
the uvicorn worker and causing a 500. Wrap create_worklog() to intercept
BaseException and re-raise as RuntimeError so callers can catch it safely.
evidence: TestOut schema was missing red_evidences / blue_evidences fields.
The ORM model has evidences loaded via joinedload but they were never
serialized into the API response. Add both fields to TestOut and override
model_validate to split Test.evidences by team, injecting the backend-proxy
download_url for each one (/api/v1/evidence/{id}/file).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>