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
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.
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.
- 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
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
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
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.
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)
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.
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)
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.
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
- 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
- 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"
- 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
- 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
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.
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)
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)
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
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
- 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
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
- 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
- audit_service: set timestamp=datetime.now(utc) explicitly so DB never stores NULL
- AuditLogPage: formatDate handles null/undefined timestamps (was showing Jan 1 1970)
- nginx.conf: add CSP script-src hash for inline script (sha256-31OgE8E9...)
- system.py: MITRE sync now runs in BackgroundTasks — returns immediately, no more 120s timeout
- mitre_sync_job.py: add _run_data_sources_sync job (every 6h) that checks sync_frequency
and auto-syncs overdue enabled data sources
- SystemPage: MITRE sync result shows "started" vs "complete" message
- test-templates.ts: add updateTemplate() API function
- SystemPage: template name cell is now clickable — opens TemplateDetailModal with
full edit form (name, description, procedure, detection, platform, severity, tool)
and Save / Activate / Deactivate / Close buttons
- Add must_change_password field to User model with migration b023
- Add POST /auth/change-password endpoint with password policy validation
- Add require_password_changed dependency to block requests until password is changed
- Add ChangePasswordModal with live password policy checklist (forced on first login)
- Show password policy in CreateUserModal and EditUserModal
- Fix backend permissions: tests, campaigns, templates, reports, evidence, worklogs
- red_tech/blue_tech: execute only, cannot create tests/campaigns/templates
- red_lead/blue_lead: create/edit tests/campaigns/templates, generate reports, no system access
- viewer: read-only everywhere, can generate reports
- Fix frontend role checks across TestDetailPage, TestDetailHeader, TeamTabs, TestsPage, CampaignsPage, CampaignDetailPage, Sidebar
Pause/Resume timer:
- Add paused_at, red_paused_seconds, blue_paused_seconds fields to Test model
- Add pause_timer/resume_timer workflow functions with accumulated pause tracking
- Auto-resume on phase submit; subtract paused time from worklog duration
- Add POST /tests/{id}/pause-timer and resume-timer endpoints
- Update LiveTimer component with pause/resume button and paused visual state
- Wire pause/resume mutations through TestDetailPage and TestDetailHeader
Professional Reporting Engine - Fase 2:
- Add ReportEngine service with Jinja2 HTML rendering, WeasyPrint PDF, and docxtpl DOCX
- Add corporate CSS stylesheet with cover page, data tables, stats grid, findings
- Create purple_campaign, coverage_report, and executive_summary HTML templates
- Add report_generation_service collecting domain data for each report type
- Add professional_reports router: GET /reports/generate/purple-campaign/{id}, coverage-summary, executive-summary
- Add analytics router with flat JSON endpoints for PowerBI: /coverage, /tests, /trends, /operators
- Add advanced_metrics router: /coverage-by-tactic, /never-tested, /avg-validation-time, /detection-rate-trend
- Add weasyprint and docxtpl to requirements.txt
- Add REPORT_TEMPLATES_DIR, REPORT_OUTPUT_DIR, COMPANY_NAME, COMPANY_LOGO_PATH to config
Full Jira/Tempo pipeline: link Aegis entities to Jira issues, auto-sync
status hourly, log time internally with integrity hashing, and optionally
push worklogs to Tempo.
- 1.1 JiraLink model + Worklog model: Alembic migration b020 with indexes,
enums (jiralinkentitytype, jirasyncdirection), and integrity_hash column
- 1.2 Jira service: atlassian-python-api wrapper with lazy singleton client,
search/create/sync operations, feature-flagged via JIRA_ENABLED
- 1.3 Jira router: CRUD endpoints for /jira/links, /jira/search,
/jira/create-issue with audit logging and entity-to-issue auto-creation
- 1.4 Tempo service: worklog push via tempo-api-python-client, auto-log from
test completions when TEMPO_ENABLED, graceful fallback on failure
- 1.5 Worklog service + router: immutable internal time records with SHA-256
integrity hash, CRUD at /worklogs, /worklogs/{id}/verify endpoint
- 1.6 Frontend: JiraLinkPanel component (search, link, sync, unlink) and
WorklogTimeline component (timeline view, manual log form) integrated into
TestDetailPage sidebar, CampaignDetailPage grid, TechniqueDetailPage
- 1.7 Jira sync job: APScheduler hourly job syncs all links from Jira,
registered in background scheduler alongside existing jobs
Critical (1-3):
- Replace hardcoded admin credentials with secure auto-generation (seed.py)
- Enforce SECRET_KEY configuration, fail in production if missing (config.py)
- Add Zip Slip and Zip Bomb protection to all ZIP import services
High/Medium (4-9):
- Add 50MB file size limit and extension whitelist to evidence uploads
- Configure CORS origins via environment variable instead of hardcoded
- Migrate JWT storage from localStorage to HttpOnly cookies (frontend+backend)
- Add rate limiting (5/min) on login endpoint via slowapi
- Replace generic dict payloads with Pydantic schemas (mass assignment)
Medium (10-17):
- Check is_active on login to prevent disabled users from authenticating
- Sanitize exception messages in API responses (system, data_sources)
- Escape LIKE wildcards in all ilike search filters across 8 routers
- Run Docker container as non-root user (appuser)
- Make MINIO_SECURE configurable via environment variable
- Add password complexity policy (12+ chars, upper/lower/digit/special)
- Implement JWT token revocation via in-memory blacklist + reduce TTL to 15min
- Replace xml.etree with defusedxml to prevent Billion Laughs attacks
Low (18-20):
- Add security headers to Nginx (CSP, X-Frame-Options, HSTS-ready, etc.)
- Disable Swagger UI/ReDoc/OpenAPI in production
- Restrict /health endpoint to internal networks via Nginx ACL
Also: rewrite install.sh as interactive wizard for guided deployment,
fix test-from-template validation error (technique_id UUID vs MITRE ID)
- Fix hardcoded localhost:8000 URLs in frontend to use relative /api/v1
path (works with Nginx proxy in prod and VITE_API_URL in dev)
- Create production entrypoint (entrypoint.prod.sh) that runs migrations,
seeds, and starts uvicorn with 4 workers (no --reload)
- Create comprehensive install.sh script for production deployment that
generates secure .env, builds containers, waits for health, and
optionally triggers initial MITRE sync
- Update docker-compose.prod.yml to use production entrypoint
- Update Dockerfile to make both entrypoints executable
- Remove init.ps1 (production will always be Linux)
- Update README with production deployment instructions
- Make D3FEND defense cards clickable with expandable details and external link
- Fix D3FEND URLs to use PascalCase technique names matching the ontology
- Remove duplicate Import Atomic Red Team from System page (use Data Sources)
- Add bulk Activate All / Deactivate All buttons with confirmation modal
- Fix template admin list to show both active and inactive templates
- Add PATCH /test-templates/bulk-activate backend endpoint
- Auto-seed data sources on container startup via entrypoint.sh
- Fix SigmaHQ, CALDERA, GTFOBins import issues
- Register D3FEND sync handler in data sources router
- Add CIS Controls v8 compliance framework import
- Expand Test Catalog source filters (CALDERA, LOLBAS, GTFOBins)
- Campaign Generate from Threat Actor now opens actor selector modal
- Add coverage snapshot creation button to Comparison page
- Update README with accurate data source and feature documentation
T-113: Rewrite models.ts with v2 types - TestState now includes red_executing/blue_evaluating, add TeamSide, ValidationStatus, TestTemplate, TestTemplateSummary, TestTimelineEntry types, RED_EDITABLE_STATES/BLUE_EDITABLE_STATES constants, and dual validation fields on Test interface. Remove old validated_by/validated_at references from TestDetailPage and techniques API.
T-114: Rewrite tests.ts API client with 16 functions covering full Red/Blue workflow (createTestFromTemplate, updateTestRed/Blue, startExecution, submitRed/Blue, validateAsRedLead/BlueLead, reopenTest, getTestTimeline). Rewrite evidence.ts with team parameter on upload/list and new deleteEvidence. Create test-templates.ts with getTemplates, getTemplateById, getTemplatesByTechnique, createTemplate, importAtomicTests.