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>
The OFS Jira project does not have the default Jira priority scheme
(Highest/High/Medium/Low/Lowest), causing a 'priority selected is invalid'
error on every ticket creation. Removing the priority field lets Jira use
the project default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Jira tickets now follow the correct hierarchy:
OFS-9107 (system parent)
├── Standalone test ticket (unchanged — was already working)
└── Campaign ticket (NEW — created on campaign creation)
├── Test 1 ticket (NEW — created per test)
└── Test 2 ticket (NEW — created per test)
Changes:
- jira_service: add auto_create_campaign_issue() — creates campaign
ticket as child of OFS-9107; stores JiraLink(entity_type=campaign)
- jira_service: add get_campaign_jira_key() / get_test_jira_key()
helpers to look up existing Jira links by entity
- jira_service: auto_create_test_issue() gains parent_ticket_override
param — when set, uses it as parent instead of OFS-9107
- campaigns router/create_campaign: triggers auto_create_campaign_issue
after commit
- campaigns router/from-threat-actor: triggers campaign ticket then
iterates campaign_tests and creates each test ticket under it
- campaigns router/add_test_to_campaign: if campaign has a Jira ticket
and the test has none yet, creates test ticket under campaign ticket
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- jira-test: when myself() returns empty displayName/emailAddress/name,
fall back to the configured Atlassian auth email so 'Connected as:' is
never empty
- jira-test: 401 error message now includes which email was used, making
misconfigured Jira email easier to diagnose
- jira-test: missing jira_url now returns HTTP 200 {status: error} instead
of HTTP 400, consistent with Cloudflare-safe pattern
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>
FastAPI uses __pydantic_validator__.validate_python() which bypasses
model_validate() overrides. Switch to @model_validator(mode='after')
which the Pydantic Rust core always calls, so jira_token_set is now
correctly derived from the excluded jira_api_token field.
Also add a 10s timeout to the jira-test endpoint and better error
messages (the Atlassian library's "Expecting value" JSON error was
ambiguous).
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
- Add jira_api_token field to User model + migration b042
- Per-user Jira client: user's corporate email + personal Atlassian token
- Admin-configurable Jira URL/project via system_configs (GET/PATCH /system/jira-config + POST /system/jira-test)
- Auto-create Jira ticket when a test is created (non-fatal)
- Push lifecycle comments on every state transition: draft→red_executing→blue_evaluating→in_review→validated/rejected→draft
- Rich ticket descriptions with technique, MITRE ID, priority from severity, labels
- UserOut.jira_token_set (bool) instead of exposing raw token
- PATCH /users/me/preferences now accepts jira_api_token
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates 14 comprehensive wiki pages covering architecture, roles,
test lifecycle, API reference, security, deployment, and QA guide.
Run from a machine with access to internal Gitea (192.168.1.107:3000).
- Accept 409 for playbook creation (unique per technique+type is correct behavior)
- Space logins 13s apart to avoid 5/min rate limit on login endpoint
- Reuse admin session from initial login to avoid duplicate login call
fix(qa): pass technique_id and test_id context between test suites
fix(qa): playbook creation requires technique_id field
fix(qa): lesson creation requires what_happened and root_cause fields
fix(qa): campaign complete test now activates with test before completing
fix(qa): rate limit test notes loopback exemption instead of failing
- fix(auth): enforce API key scopes in require_role/require_any_role;
attach _api_key_scopes to user on API key auth; add require_scope()
dependency — scopes were stored but never enforced (CWE-285)
- fix(sso): read SECURE_COOKIES env var for SSO cookie instead of
hardcoded secure=False — SAML sessions now respect HTTPS config (CWE-614)
- fix(webhooks): SSRF prevention — validate webhook URLs against private
and reserved CIDRs at creation/update time (CWE-918)
- fix(knowledge): restrict playbook/lesson create, update and restore
to admin/red_lead/blue_lead roles — was open to any authenticated user (CWE-284)
- fix(alerts): restrict alert acknowledge/resolve/dismiss to admin/lead
roles — any user could silence security alerts (CWE-284)
- security: delete get_admin_creds.py, check_auth.py, deploy.py scripts
containing hardcoded root SSH credentials and production DB access;
add scripts/.gitignore to prevent reintroduction (CWE-798)
- Add dispatch_webhook_targeted() to webhook_service for rule-specific delivery
- evaluate_all_rules() now dispatches in-app notifications (admins/leads) and
webhooks after each alert fires (targeted + global alert.fired broadcast)
- APScheduler: _run_alert_evaluation() job registered hourly alongside existing jobs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PostureSnapshot model, Alembic migration (b039exec), schemas, service
aggregating all phases (coverage/risk/operations/knowledge/MTTD), and
router at /api/v1/dashboard with executive view, KPIs, coverage-by-tactic,
posture-history, posture-snapshot, and activity-feed endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Playbooks: versioned Markdown runbooks per technique × type (attack/detect/investigate/respond/hunt)
- PlaybookVersion: immutable snapshots on every update; restore to any previous version
- LessonLearned: post-mortem records linked to tests/campaigns/attack-paths or manual
- Alembic migration b037know (raw SQL, idempotent, no PostgreSQL enums)
- Router /api/v1/knowledge: 14 endpoints for playbooks + lessons + stats
- Pydantic validators for playbook_type, severity, entity_type (422 on invalid)
- Knowledge stats endpoint: totals + breakdown by severity and playbook type
- Soft-delete on both resources; include_inactive filter for admin recovery
- QA script: 70+ tests across CRUD, versioning, filtering, auth, soft-delete, regression
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /ownership/queue with an invalid reason or priority was silently
passing Pydantic and crashing at the DB layer (PostgreSQL enum type
mismatch → 500). Added @field_validator for both fields, matching the
existing validators in QueueItemPatch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLAlchemy fires before_create for ALL known enum types when any table
is created via op.create_table, causing DuplicateObject even with
create_type=False. Rewrite both CREATE TABLE statements as raw SQL via
conn.execute(sa.text(...)) and use CREATE TABLE IF NOT EXISTS / CREATE
INDEX IF NOT EXISTS for full idempotency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace _enum_exists() helper (which had connection context issues in
Alembic) with PostgreSQL DO $$ BEGIN ... EXCEPTION WHEN duplicate_object
THEN NULL; END $$ blocks, which are truly idempotent regardless of
transaction state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous fix changed _now() to return naive UTC, but the code still
called .replace(tzinfo=utc) on most_recent (from DB) before subtracting.
This caused "can't subtract offset-naive and offset-aware datetimes".
Now we strip tzinfo if present, keeping everything naive UTC consistently.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace datetime.now(timezone.utc) with datetime.utcnow() in _now() across
all three Phase 8 files to match DB DateTime column type (naive UTC)
- Guard POST /assets/{id}/techniques/{tid} against duplicate mappings:
if mapping already exists, update coverage_type/confidence_level instead
of inserting a duplicate row
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Phase 6.1: WebhookConfig model, CRUD router (/api/v1/webhooks, admin-only),
dispatch_webhook() with HMAC signing; integrated into test validation,
campaign completion, and MITRE sync job
- Phase 7.1: SMTP email service with send_test_validated_email,
send_campaign_completed_email, send_new_mitre_techniques_email;
notify_role_with_email() added to notification_service
- Phase 7.2: notification_preferences and jira_account_id on User model;
PATCH /users/me/preferences endpoint; Alembic migrations b031phase6 and b032phase7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- nginx.conf: add new CSP script-src hash (sha256-Yvj83pg...) alongside previous one
- SystemPage: remove pencil icon from template name button, keep cyan underline style
- SystemPage: switch from selectedTemplate state to selectedTemplateId + useQuery
for getTemplateById() — ensures full template data (description, attack_procedure,
expected_detection, tool_suggested etc.) loads before modal opens
- DB backfill already applied via SQL: UPDATE audit_logs SET timestamp = NOW()
WHERE timestamp IS NULL (358 rows fixed)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>