- New module backend/app/services/export.py with render_engagement_markdown,
render_engagement_csv, render_engagement_pdf, _render_engagement_html helper,
and _export_filename slugifier (NFKD + fallback "unnamed").
- Extend engagements_bp with GET /api/engagements/<int:eid>/export?format=md|csv|pdf,
gated @role_required("admin","redteam"). Returns 400 on missing/unknown format,
404 on unknown engagement, correct Content-Type + Content-Disposition headers.
- Reuses _enrich_techniques and _enrich_tactics from serializers.py; resilient
to MITRE bundle not loaded (returns empty tactics, no crash).
- Adds weasyprint>=60.0 to backend/requirements.txt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- mitre.py: add _SLUG_TO_TA_ID reverse map; _build_matrix() now emits tactic_id
as TA-id (e.g. "TA0007") so frontend can send it back verbatim in PATCH tactic_ids
- test_mitre.py: update all matrix assertions to use TA-ids; add
test_get_matrix_tactic_id_is_ta_format regression guard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- test_engagement_lifecycle.py, test_simulations_techniques.py: replace hardcoded
absolute paths with Path(__file__).parent.parent / migrations/... (portable)
- simulation_workflow.py: remove dead branch in transition() — the IN_PROGRESS
hook was unreachable since _ALLOWED_TRANSITIONS only targets review_required/done
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Migration 0003: enforce techniques NOT NULL via batch_alter_table
- Migration 0003: remove unused _sims table proxy and dead column/table imports
- mitre.py: add _TACTIC_NAMES dict to fix 'Command And Control' → 'Command and Control'
Added bundle-loaded guard in _resolve_technique_ids() before attempting any
lookup; matches behavior of GET /api/mitre/matrix and GET /api/mitre/techniques.
Added corresponding test case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>