diff --git a/backend/app/services/export.py b/backend/app/services/export.py
index 368f72b..7e37999 100644
--- a/backend/app/services/export.py
+++ b/backend/app/services/export.py
@@ -22,19 +22,6 @@ def _export_filename(engagement: Engagement, ext: str) -> str:
return f"engagement-{engagement.id}-{slug}-{today}.{ext}"
-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)
@@ -43,10 +30,29 @@ def _creator(obj: object) -> str:
return getattr(cb, "username", "") or ""
+def _format_execution(sim: Simulation) -> str:
+ parts = [
+ sim.executed_at.isoformat() if sim.executed_at else "",
+ sim.commands or "",
+ sim.execution_result or "",
+ ]
+ return "\n".join(parts)
+
+
# ---------------------------------------------------------------------------
# Markdown
# ---------------------------------------------------------------------------
+_MD_HEADERS = [
+ "Scénario",
+ "Test",
+ "Source de log",
+ "Commentaires SOC",
+ "Exécution",
+ "Logs remontés au SIEM",
+ "Cyber incident",
+]
+
def render_engagement_markdown(
engagement: Engagement, simulations: list[Simulation]
@@ -65,9 +71,7 @@ def render_engagement_markdown(
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 by**: {_creator(engagement)}")
lines.append(
f"**Created at**: {engagement.created_at.isoformat() if engagement.created_at else 'N/A'}"
)
@@ -81,46 +85,29 @@ def render_engagement_markdown(
lines.append("## Simulations")
lines.append("")
+ header_row = "| " + " | ".join(_MD_HEADERS) + " |"
+ separator = "| " + " | ".join("---" for _ in _MD_HEADERS) + " |"
+ lines.append(header_row)
+ lines.append(separator)
+
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"
+ execution = _format_execution(sim).replace("\n", "
")
- 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("")
+ def _cell(value: str | None) -> str:
+ return (value or "").replace("|", "\\|").replace("\n", "
")
- 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("")
+ row = "| " + " | ".join([
+ _cell(sim.name),
+ _cell(sim.description),
+ _cell(sim.log_source),
+ _cell(sim.soc_comment),
+ _cell(execution),
+ _cell(sim.logs),
+ _cell(sim.incident_number),
+ ]) + " |"
+ lines.append(row)
+ lines.append("")
return "\n".join(lines)
@@ -129,22 +116,13 @@ def render_engagement_markdown(
# ---------------------------------------------------------------------------
_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",
+ "Scénario",
+ "Test",
+ "Source de log",
+ "Commentaires SOC",
+ "Exécution",
+ "Logs remontés au SIEM",
+ "Cyber incident",
]
# \t and \r included: Excel auto-trims leading whitespace, so a tab/CR prefix still
@@ -174,27 +152,15 @@ def render_engagement_csv(
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 [])
-
+ execution = _format_execution(sim)
writer.writerow([
- sim.id,
- _csv_safe(sim.name),
- sim.status.value,
- _csv_safe(tech_ids),
- _csv_safe(tactic_str),
+ _csv_safe(sim.name or ""),
_csv_safe(sim.description or ""),
- _csv_safe(sim.commands or ""),
- _csv_safe(sim.prerequisites or ""),
- sim.executed_at.isoformat() if sim.executed_at else "",
- _csv_safe(sim.execution_result or ""),
_csv_safe(sim.log_source or ""),
- _csv_safe(sim.logs or ""),
_csv_safe(sim.soc_comment or ""),
+ _csv_safe(execution),
+ _csv_safe(sim.logs or ""),
_csv_safe(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()
@@ -208,15 +174,22 @@ _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, td { border: 1px solid #ccc; padding: 4px 8px; text-align: left; vertical-align: top; white-space: pre-wrap; }
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; }
"""
+_HTML_HEADERS = [
+ "Scénario",
+ "Test",
+ "Source de log",
+ "Commentaires SOC",
+ "Exécution",
+ "Logs remontés au SIEM",
+ "Cyber incident",
+]
+
def _render_engagement_html(
engagement: Engagement, simulations: list[Simulation]
@@ -241,50 +214,22 @@ def _render_engagement_html(
if simulations:
parts.append("
| 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 '')} |