fix(backend): complete c2 task→simulation mapping per spec + sanitize adapter errors (sprint 8 code-review)

mapping.py — full §0.11 contract:
1. execution_result: append '$ <command>\n<output>\n' block (previously
   wrote raw output without command header, making multi-task blobs
   unreadable in exports)
2. executed_at: set from task.completed_at when currently null (was
   completely missing — simulation.executed_at stayed null forever)
3. commands: append task.command deduplicated line-by-line (was
   completely missing — simulation.commands stayed empty)

mythic.py — sanitize transport errors:
Replace 'raise C2Error(str(exc))' (which leaks the Mythic URL via
requests exception repr) with 'raise C2Error(f"C2 transport error:
{type(exc).__name__}")'. Original exc stays chained for backend logs.

api/c2.py — remove redundant 'task.mapping_applied = True' in import
endpoint (apply_task_to_simulation() already sets it).

test_c2_mapping.py — full rewrite: 19 tests covering command blocks,
executed_at set/preserve, commands dedup, idempotency.

test_c2_adapter_mythic.py — add URL-leak sanitization assertion.

468 passed; ruff + mypy clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Knacky
2026-06-10 20:28:49 +02:00
parent 7d3d39639e
commit 38e282a126
5 changed files with 205 additions and 66 deletions

View File

@@ -158,7 +158,7 @@ class MythicAdapter(C2Adapter):
try:
data = self._post({"query": _CALLBACKS_QUERY})
except requests.RequestException as exc:
raise C2Error(str(exc)) from exc
raise C2Error(f"C2 transport error: {type(exc).__name__}") from exc
callbacks_raw = data.get("data", {}).get("callback", [])
return [
@@ -190,7 +190,7 @@ class MythicAdapter(C2Adapter):
},
})
except requests.RequestException as exc:
raise C2Error(str(exc)) from exc
raise C2Error(f"C2 transport error: {type(exc).__name__}") from exc
task_data = data.get("data", {}).get("createTask", {})
error_msg = task_data.get("error")
@@ -206,7 +206,7 @@ class MythicAdapter(C2Adapter):
"variables": {"display_id": task_display_id},
})
except requests.RequestException as exc:
raise C2Error(str(exc)) from exc
raise C2Error(f"C2 transport error: {type(exc).__name__}") from exc
rows = data.get("data", {}).get("task", [])
if not rows:
@@ -238,7 +238,7 @@ class MythicAdapter(C2Adapter):
"variables": {"display_id": task_display_id},
})
except requests.RequestException as exc:
raise C2Error(str(exc)) from exc
raise C2Error(f"C2 transport error: {type(exc).__name__}") from exc
rows = data.get("data", {}).get("response", [])
return "".join(
@@ -269,7 +269,7 @@ class MythicAdapter(C2Adapter):
"variables": {"callback_display_id": callback_display_id},
})
except requests.RequestException as exc:
raise C2Error(str(exc)) from exc
raise C2Error(f"C2 transport error: {type(exc).__name__}") from exc
rows = data.get("data", {}).get("task", [])
total: int = (