"""Engagement export renderers — Markdown, CSV, PDF.""" from __future__ import annotations import csv import io import re import unicodedata from datetime import date from typing import TYPE_CHECKING if TYPE_CHECKING: from backend.app.models.engagement import Engagement from backend.app.models.simulation import Simulation def _export_filename(engagement: Engagement, ext: str) -> str: name = engagement.name or "" normalized = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode() slug = re.sub(r"[^a-z0-9]+", "-", normalized.lower()).strip("-")[:60] or "unnamed" today = date.today().strftime("%Y%m%d") return f"engagement-{engagement.id}-{slug}-{today}.{ext}" def _technique_names(techniques: list[dict]) -> str: return " | ".join(t.get("name", t.get("id", "")) for t in (techniques or [])) def _technique_ids(techniques: list[dict]) -> str: return " | ".join(t.get("id", "") for t in (techniques or [])) def _tactic_names(tactic_ids: list[str]) -> str: from backend.app.serializers import _enrich_tactics enriched = _enrich_tactics(tactic_ids or []) return " | ".join(e.get("name", e.get("id", "")) for e in enriched) def _enrich_sim_techniques(techniques: list[dict]) -> list[dict]: from backend.app.serializers import _enrich_techniques return _enrich_techniques(techniques or []) def _creator(obj: object) -> str: """Return username string from an ORM object with a created_by relationship.""" cb = getattr(obj, "created_by", None) if cb is None: return "" return getattr(cb, "username", "") or "" # --------------------------------------------------------------------------- # Markdown # --------------------------------------------------------------------------- def render_engagement_markdown( engagement: Engagement, simulations: list[Simulation] ) -> str: lines: list[str] = [] lines.append(f"# {engagement.name}") lines.append("") if engagement.description: lines.append(engagement.description) lines.append("") lines.append(f"**Status**: {engagement.status.value}") lines.append( f"**Start date**: {engagement.start_date.isoformat() if engagement.start_date else 'N/A'}" ) lines.append( f"**End date**: {engagement.end_date.isoformat() if engagement.end_date else 'N/A'}" ) lines.append( f"**Created by**: {_creator(engagement)}" ) lines.append( f"**Created at**: {engagement.created_at.isoformat() if engagement.created_at else 'N/A'}" ) lines.append("") if not simulations: return "\n".join(lines) lines.append("---") lines.append("") lines.append("## Simulations") lines.append("") for sim in simulations: enriched_techniques = _enrich_sim_techniques(sim.techniques) tech_str = " | ".join( f"{t['id']} — {t['name']}" for t in enriched_techniques ) if enriched_techniques else "N/A" tactic_str = _tactic_names(sim.tactic_ids) or "N/A" lines.append(f"### {sim.name}") lines.append("") lines.append(f"**Status**: {sim.status.value}") lines.append(f"**Techniques**: {tech_str}") lines.append(f"**Tactics**: {tactic_str}") if sim.description: lines.append(f"**Description**: {sim.description}") lines.append( f"**Executed at**: {sim.executed_at.isoformat() if sim.executed_at else 'N/A'}" ) lines.append(f"**Execution result**: {sim.execution_result or 'N/A'}") lines.append("") lines.append("**Commands**:") if sim.commands: safe = sim.commands.replace("~~~", "\\~\\~\\~") lines.append("~~~bash") lines.append(safe) lines.append("~~~") else: lines.append("N/A") lines.append("") lines.append(f"**Prerequisites**: {sim.prerequisites or 'N/A'}") lines.append("") lines.append("#### SOC") lines.append(f"**Log source**: {sim.log_source or 'N/A'}") lines.append(f"**Logs**: {sim.logs or 'N/A'}") lines.append(f"**SOC comment**: {sim.soc_comment or 'N/A'}") lines.append(f"**Incident number**: {sim.incident_number or 'N/A'}") lines.append("") return "\n".join(lines) # --------------------------------------------------------------------------- # CSV # --------------------------------------------------------------------------- _CSV_HEADERS = [ "id", "name", "status", "techniques", "tactics", "description", "commands", "prerequisites", "executed_at", "execution_result", "log_source", "logs", "soc_comment", "incident_number", "created_at", "updated_at", ] def render_engagement_csv( engagement: Engagement, simulations: list[Simulation] ) -> str: buf = io.StringIO() writer = csv.writer(buf) writer.writerow(_CSV_HEADERS) for sim in simulations: enriched_techniques = _enrich_sim_techniques(sim.techniques) tech_ids = "|".join(t["id"] for t in enriched_techniques) tactic_str = "|".join(sim.tactic_ids or []) writer.writerow([ sim.id, sim.name, sim.status.value, tech_ids, tactic_str, sim.description or "", sim.commands or "", sim.prerequisites or "", sim.executed_at.isoformat() if sim.executed_at else "", sim.execution_result or "", sim.log_source or "", sim.logs or "", sim.soc_comment or "", sim.incident_number or "", sim.created_at.isoformat() if sim.created_at else "", sim.updated_at.isoformat() if sim.updated_at else "", ]) return buf.getvalue() # --------------------------------------------------------------------------- # HTML (internal, used by PDF renderer) # --------------------------------------------------------------------------- _CSS = """ body { font-family: sans-serif; font-size: 13px; color: #1a1a1a; margin: 40px; } h1 { font-size: 22px; border-bottom: 2px solid #333; padding-bottom: 6px; } h2 { font-size: 17px; margin-top: 32px; color: #333; } h3 { font-size: 14px; margin-top: 24px; background: #f0f0f0; padding: 4px 8px; } h3:nth-of-type(odd) { background: #e8e8e8; } table { border-collapse: collapse; width: 100%; margin-bottom: 12px; } th, td { border: 1px solid #ccc; padding: 4px 8px; text-align: left; vertical-align: top; } th { background: #e0e0e0; } pre { background: #f5f5f5; padding: 8px; font-size: 11px; white-space: pre-wrap; word-break: break-all; } .meta { color: #555; margin-bottom: 16px; } """ def _html_escape(text: str) -> str: return ( text.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """) ) def _render_engagement_html( engagement: Engagement, simulations: list[Simulation] ) -> str: h = _html_escape parts: list[str] = [] parts.append("
") parts.append(f"") parts.append(f"| Status | {h(sim.status.value)} |
|---|---|
| Techniques | {tech_str} |
| Tactics | {tactic_str} |
| Description | {h(sim.description or '')} |
| Executed at | {h(executed)} |
| Execution result | {h(sim.execution_result or '')} |
Commands:
{h(sim.commands)}")
if sim.prerequisites:
parts.append(
f"Prerequisites: {h(sim.prerequisites)}
" ) parts.append("| SOC | |
|---|---|
| Log source | {h(sim.log_source or '')} |
| Logs | {h(sim.logs or '')} |
| SOC comment | {h(sim.soc_comment or '')} |
| Incident number | {h(sim.incident_number or '')} |