Commit Graph

321 Commits

Author SHA1 Message Date
kitos 675870b469 fix(campaigns): add missing crud_activate and log_action imports; add style comments to previous import fixes
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 15:41:59 +02:00
kitos 92f4bdcdce fix(compliance): add missing import_nist_800_53_mappings to router imports
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 14:29:52 +02:00
kitos 3ec51524d6 fix(imports): add missing TestTemplate and DetectionRule imports in lolbas and sigma import services
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 14:17:15 +02:00
kitos 7ded48bdb7 fix(routers+imports): fix missing DetectionRule import and correct -> list return type annotations that actually return paginated dicts
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 13:22:51 +02:00
kitos 6ca37f743f fix(caldera): add missing TestTemplate import in caldera_import_service
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 13:14:15 +02:00
kitos cea518b33c fix(install): reset postgres volume on reconfigure to avoid auth failures
Aegis CI / lint-and-test (push) Has been cancelled
When the wizard reconfigures and generates a new DB_PASSWORD, the existing
Postgres volume retains the old password (Docker only initializes credentials
on a fresh empty volume). The backend then fails to connect because .env
has the new password but Postgres still uses the old one.

Fix: run 'docker compose down -v' before 'up --build' whenever the wizard
reconfigures (SKIP_CONFIG=false), so Postgres always initializes with the
current .env credentials. Also add a pre-confirmation warning when existing
volumes are detected.
2026-06-11 12:03:33 +02:00
kitos 22293804ab fix(install): auto-generate MINIO_ACCESS_KEY instead of leaving default minioadmin
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 11:51:12 +02:00
kitos 64cc438bcc fix(main): restore missing settings import lost in merge conflict resolution
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-11 11:23:51 +02:00
kitos 8fea0c1ada feat(refactor): PEP8, type annotations, docstrings and PyJWT security fix 2026-06-11 11:09:41 +02:00
kitos 1f19bd8432 fix(security): replace python-jose with PyJWT to eliminate ecdsa CVEs
Snyk scan found 3 High severity vulns: two in ecdsa (pulled by python-jose)
and one in diskcache (pulled by pySigma, never imported). Remove both
vulnerable dependencies and migrate JWT handling to PyJWT. Fix
test_logout_revokes_token which broke because test stubs sys.modules[jose]
with a MagicMock at collection time; test now uses PyJWT directly.
2026-06-11 11:06:56 +02:00
kitos d2a46feba8 refactor(docs+comments): add Google-style docstrings and inline comments across backend
Task D — Google-style docstrings (Args/Returns) on every public function,
method, and class across all 158 Python files in the backend. Zero ruff D
violations (pydocstyle Google convention).

Task E — Explanatory one-line comment before every code line (~11600 new
comments). ruff check passes clean after isort re-sort.
2026-06-11 11:06:55 +02:00
kitos 9ff0f04ba3 refactor(types): add comprehensive type annotations across backend Python codebase
Enable ANN rules in ruff.toml (flake8-annotations) and resolve all 221 violations:

ANN201/ANN202 — return types on 168 public/private functions:
- All 28 FastAPI routers: endpoints annotated with dict/list/specific schema/
  StreamingResponse/FileResponse/JSONResponse as appropriate
- main.py: lifespan→AsyncGenerator[None,None], exception handlers→JSONResponse
- database.py: get_db→Generator[Session,None,None], proxy methods→correct types
- middleware/request_context.py: dispatch→Response with Callable call_next type

ANN001/ANN002/ANN003 — 32 missing argument types:
- seed_demo.py: all db parameters typed as Session
- domain/unit_of_work.py: __aexit__ exc_type/exc_val/exc_tb typed with TracebackType
- services: audit_service user_id→UUID|None, heatmap_service query/model/builder,
  notification_service test→Test, tempo_service test→Test/user→User,
  test_workflow_service test_id→UUID, campaign_crud **fields→object,
  test_crud **fields→object (4 sites)

ANN401 — 16 Any usages resolved:
- Domain entities (campaign/technique/threat_actor/test_entity): replaced Any with
  actual ORM types via TYPE_CHECKING guards to avoid circular imports
- detection_rule_service: test_id/detection_rule_id/evaluator_id→UUID
- score_cache: kept Any with # noqa: ANN401 (genuinely generic cache)
- jira_service/tempo_service: kept Any with # noqa: ANN401 (lazy optional deps)
- d3fend_import_service: _to_str(v: Any) kept with # noqa: ANN401

ANN204/ANN205/ANN206 — special/static/class methods:
- database.py proxy __call__/__getattr__: *args: object/**kwargs: object
- schemas/test.py model_validate: obj→object, **kwargs→object
- sa_technique_repository._int_type→type

All 439 unit tests pass. ruff check app/ → All checks passed!
2026-06-11 11:06:54 +02:00
kitos 8f98bdd273 refactor(pep8): enforce full PEP8 compliance across backend Python codebase
- ruff.toml: select E/W/F/I/N rules, line-length=120, drop legacy ignores
- Auto-fix: sort 82 import blocks (isort), remove 29 unused imports,
  strip 6 trailing-whitespace blank lines in docstrings
- main.py: move setup_logging and settings imports to top (E402)
- errors.py: noqa N818 on DDD exception names (96 call sites, safe)
- intel_service.py: noqa N817 for universal ET alias
- atomic/elastic/sigma import services: move _MAX_UNCOMPRESSED_SIZE and
  _MAX_ENTRIES to module level (N806)
- compliance_import_service.py: move SAMPLE_CONTROLS / CIS_CONTROLS to
  module level; wrap long description strings (N806 + E501)
- snapshot_service.py: move STATUS_ORDER dict to module level (N806)
- sigma_import_service.py: remove dead dedup_key expression (F841)
- threat_actor_import_service.py: remove dead stix_to_actor expression (F841)
- data_source.py, seed_demo.py, campaign_scheduler_service.py,
  lolbas_import_service.py: wrap lines exceeding 120 chars (E501)
- d3fend_import_service.py: per-file E501 ignore (data file with long strings)

All 439 unit tests pass. ruff check app/ → All checks passed!
2026-06-11 11:06:54 +02:00
kitos 98fddccd32 chore(gitignore): remove .cursor from tracking and add to gitignore
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-08 16:09:57 +02:00
kitos f6d33638fd chore(gitignore): exclude docs/confluence, CLAUDE.md, .claude/, qa scripts
Aegis CI / lint-and-test (push) Has been cancelled
2026-06-08 16:06:23 +02:00
kitos 0001b33594 refactor(ui): move SSO, Export/Import, System Info from SystemPage to Settings
SystemPage now only shows operational content: MITRE Sync, Intel Scan,
ATT&CK Evaluations, Scheduled Jobs, and Template Management.

Settings gets two new admin-only tabs:
- "SSO / Azure AD": full SAML 2.0 wizard (5-step setup for Azure AD)
- "System": System Status + Version Info + Configuration Export/Import
2026-06-08 15:01:22 +02:00
kitos a7725ba519 feat(sso): Azure AD / Entra ID SAML 2.0 integration
- sso_service: fix process_callback for Azure AD claim URIs (email, role)
  - Default role_attr to full Azure role claim URI
  - Fallback email resolution via Azure email claim URI + NameID
  - Username defaults to full email (prevents collision with local accounts)
  - User lookup also tries email field for existing local accounts
  - Logs warning when unknown role received from IdP

- frontend/api/sso.ts: new API module with getSsoStatus, getSsoConfig, updateSsoConfig

- LoginPage: redesigned for SSO-first flow
  - Shows Azure SSO button as primary when SSO enabled+configured
  - Local login collapsed under "Emergency admin access" section
  - Falls back to normal local login form when SSO is disabled

- SystemPage: new SsoConfigSection component (guided 5-step wizard)
  - Step 1: Copy SP Entity ID and ACS URL for IT team + metadata XML download
  - Step 2: Azure App Roles reference table (6 roles with exact values)
  - Step 3: Tenant ID field auto-fills idp_entity_id and idp_sso_url
  - Step 4: X.509 certificate paste field
  - Step 5: Attribute mapping pre-filled with Azure AD claim URIs
  - Enable/disable toggle + save
2026-06-08 13:48:36 +02:00
kitos 0c9f3051b4 fix(evaluations): fix duplicate substeps and improve eval test format by scenario grouping 2026-06-08 13:20:42 +02:00
kitos e2861a08bc feat(evaluations): enrich eval tests with attack path, criteria and data sources
- Capture Step.Description (HTML stripped), step name/number, substep ref,
  criteria, and data sources from MITRE ATT&CK Evaluations API
- _aggregate_by_technique() now accumulates ALL occurrences per technique
  (multiple substep refs, criteria, step contexts) instead of keeping only
  the best-scoring one
- New helper functions _build_procedure_text(), _build_description(),
  _build_red_summary() generate rich narratives from accumulated occurrences
- New re_enrich_evaluation_round() service function + POST endpoint
  /system/attck-evaluations/re-enrich to update already-imported tests
  without changing detection results or validation state
- Frontend: Re-enrich button per imported round + result banner in SystemPage
2026-06-08 11:42:08 +02:00
kitos 467afc334d fix(evaluations): optional chaining on evalPendingData to fix TS18048 2026-06-05 16:56:29 +02:00
kitos b630cd3210 feat(evaluations): bulk approve evaluation tests with 4-step confirmation modal
Backend:
- POST /system/attck-evaluations/bulk-approve: finds all [EVAL R*] tests in
  in_review state, approves blue side, transitions to validated, recalculates
  technique statuses, audit logs each test
- GET /system/attck-evaluations/pending-count: returns count of pending eval tests

Frontend:
- BulkApproveModal: 4 mandatory checkboxes before confirm button enables
  (lab env / not org detection / metrics impact / spot-check recommendation)
- Bulk Approve button in header badge showing pending count
- Green result banner showing approved tests + techniques recalculated
- Invalidates techniques, metrics and review-queue queries on success
2026-06-05 16:53:00 +02:00
kitos c0cecab797 fix(evaluations): results API returns list of vendors, not dict
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.
2026-06-05 16:42:27 +02:00
kitos 51d86e5436 fix(evaluations): correct fallback rounds + friendlier error messages
- 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
2026-06-05 16:24:06 +02:00
kitos 8515b8de17 fix(evaluations): bypass Cloudflare 403 with browser headers + hardcoded fallback rounds
- 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)
2026-06-05 16:10:27 +02:00
kitos b037500b7c feat(evaluations): ATT&CK Evaluations importer for CrowdStrike Falcon [FASE-6.1]
- Migration b048: evaluation_imports table (adversary, round, status, tests_created)
- EvaluationImport SQLAlchemy model
- attck_evaluations_service: fetch rounds from evals.mitre.org API, import per-technique
  detection results (Technique/Tactic/Telemetry -> detected/partially/not_detected)
- All imported tests land in in_review state with lab-environment disclaimer
- Idempotency guard prevents duplicate round imports
- 4 new endpoints: list rounds, import specific, import latest, check-new
- Weekly APScheduler cron (Mon 06:00) auto-checks and imports new rounds
- SystemPage UI: rounds table, import buttons, check-new, result feedback
- Disclaimer callout reminding admins these are lab results not org coverage
2026-06-05 15:57:03 +02:00
kitos 6f835c8501 feat(techniques): move legend to top with descriptions and review_required
Replaces the minimal bottom legend with a full coverage legend panel
placed above the filters. Each status shows a cell mock matching the
exact colors used in the matrix, a color-coded label, and a short
description of what it means. Includes review_required with its
orange alert-triangle badge. Removes the old minimal bottom legend.
2026-06-05 13:23:44 +02:00
kitos 46ade20d14 feat(rt-import): add Image to Base64 converter utility
New drag-and-drop section at the bottom of the Import RT page so operators
can convert screenshots to base64 without leaving the page. Includes
thumbnail preview, copy-base64 and copy-JSON-snippet buttons with
2s feedback, per-image delete and clear-all.
2026-06-05 13:08:55 +02:00
kitos 5f54396cb6 feat(rt-import): require base64 evidence images per technique
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
2026-06-05 12:57:22 +02:00
kitos f4289249b8 refactor(system): rename Threat Intel Scan to Security Feed Monitor
The previous name implied data from a dedicated threat intelligence team.
The feature actually monitors public RSS feeds and security blogs for
ATT&CK technique mentions, so Security Feed Monitor is more accurate.
Updated description and all references across SystemPage and ReviewQueuePage.
2026-06-05 10:23:59 +02:00
kitos 6ab61c8ace refactor(dashboard): replace security posture claims with programme-scoped language
Overall Security Score renamed to Overall Programme Score. Descriptions across
Executive Dashboard and Dashboard page now clarify scores reflect Red/Blue Team
exercise maturity and coverage breadth, not the organisation real-world security
state, to avoid overstating what ATT&CK simulation tests can guarantee.
2026-06-05 09:33:41 +02:00
kitos 725cf3406e fix(heatmap): hide empty tactics in threat-actor layer
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.
2026-06-04 17:23:28 +02:00
kitos 564eb406aa fix(campaigns): fix start_date modal — interceptor was losing structured detail
client.ts: when FastAPI detail is an object, extract .message for the error
string and preserve the full detail on enhancedError.detail so consumers
can inspect structured error payloads (e.g. 409 start_date_in_future).

CampaignDetailPage: use enhancedErr.status (not response.status) and
enhancedErr.detail (not response.data.detail) to detect 409 and show
the confirmation modal instead of the toast.
2026-06-04 16:22:17 +02:00
kitos bf3add9b09 fix(campaigns): correct Axios error parsing in activateMutation
FastAPI wraps error bodies as {detail: string | object}, not at the
top level. Was reading data.message instead of data.detail.message,
causing [object Object] in the toast for all non-409 errors.

Now correctly extracts:
- 409 with object detail -> start_date warning modal
- Other errors with string detail -> readable toast message
- Other errors with object detail -> detail.message in toast
2026-06-04 15:57:54 +02:00
kitos 840e1ac0bb feat(threat-actors): Generate Campaign button on actor detail page
Adds a Generate Campaign button (purple, visible to leads/admin) in the
threat actor header. Opens a modal with:
- Actor name shown as context
- Start date picker (required — validated: must be today or future)
- Warning message showing when tests will be queued
- Error display for API failures
- On success: redirects to the new campaign detail page

Start date is mandatory here (unlike the CampaignsPage flow where it
is optional) to enforce scheduling discipline when generating from actors.
2026-06-04 15:45:55 +02:00
kitos b5f924abe0 fix(ts): explicit useMutation generic types for activateMutation
useMutation<Campaign, unknown, boolean> to fix TS2322/TS1345/TS2345 errors
caused by inferred void variables type.
2026-06-04 15:26:12 +02:00
kitos 6b1f5d690a fix(campaigns): start_date modal + hide future-campaign tests from queue
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.
2026-06-04 14:05:58 +02:00
kitos 27c67a5f76 feat(campaigns): start_date for threat-actor-generated campaigns
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
2026-06-04 13:37:40 +02:00
kitos f605b52d89 fix(security): remediate CVE-2026-42043 — upgrade axios ^1.14.0
- 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)
2026-06-04 13:17:45 +02:00
kitos af864ed735 fix(security): upgrade axios to >=1.14.0 — CVE-2026-42043 (CVSS 10)
Bumps minimum Axios version from 1.13.5 to 1.14.0 to remediate
CVE-2026-42043 identified by VMT / Wiz (asset: AegisTest).
2026-06-04 10:16:41 +02:00
kitos 92e8ff7aff feat(campaigns): campaign start date — scheduled activation, Jira start_date
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
2026-06-03 16:57:06 +02:00
kitos 9fb84fa65c refactor(campaigns): move CampaignTimingPanel next to Progress panel
Progress and Timing now share a 2-column grid at the top of the detail page.
Removed CampaignTimingPanel from the bottom Jira section.
2026-06-03 16:42:45 +02:00
kitos cafd7db94b feat(compliance): add mapping confidence warnings for DORA, ISO 27001, ISO 42001
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.
2026-06-03 16:37:25 +02:00
kitos 80991b2f59 feat(compliance): executive descriptions and mapping rationale for all 5 frameworks
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.
2026-06-03 16:28:16 +02:00
kitos 200ef88d67 feat(compliance): add ISO/IEC 27001:2022 and ISO/IEC 42001:2023 frameworks
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.
2026-06-03 15:50:54 +02:00
kitos fd39658f5d feat(disputed): symmetric UX for both leads in disputed state
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.
2026-06-03 14:09:52 +02:00
kitos 3b552dbe4e fix(disputed): add admin role + contact info in discussion modal
- 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
2026-06-03 13:02:57 +02:00
kitos 2ecb950770 feat(disputed): Confirm My Validation button + discussion request modal
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
2026-06-03 12:48:08 +02:00
kitos c141e5bb67 fix(disputed): add disputed to TestState in test_entity.py
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.
2026-06-03 12:36:21 +02:00
kitos 6c343bd7a1 fix(ts): add disputed to all Record<TestState> maps to satisfy TypeScript 2026-06-03 12:23:53 +02:00
kitos 643e65fbe5 feat(tests): disputed state + fix timestamps on reopen
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
2026-06-03 12:21:47 +02:00