"""Deterministic in-memory C2 adapter — used when MIMIC_C2_ADAPTER=fake. Intended for integration tests and local development without a live Mythic instance. Task state is per-instance so parallel tests don't interfere with each other. """ from __future__ import annotations from backend.app.services.c2.adapter import ( C2Adapter, C2Callback, C2Error, C2Health, C2HistoricalTask, C2TaskPage, C2TaskStatus, ) # Frozen base timestamp — all fake history tasks share this prefix for determinism. _BASE_TS = "2026-06-10T00:00:00Z" # Deterministic history for list_callback_tasks: # callback 1 → 12 tasks, callback 2 → 0 tasks, callback 3 → 5 tasks. # Commands cycle through a fixed set; even-indexed tasks are completed. _HISTORY_COMMANDS = ["whoami", "hostname", "id", "ipconfig", "net user", "pwd"] _FAKE_HISTORY: dict[int, list[C2HistoricalTask]] = { 1: [ C2HistoricalTask( display_id=100 + i, command=_HISTORY_COMMANDS[i % len(_HISTORY_COMMANDS)], params=None, status="completed" if i % 2 == 0 else "submitted", completed=i % 2 == 0, timestamp=_BASE_TS if i % 2 == 0 else None, ) for i in range(12) ], 2: [], 3: [ C2HistoricalTask( display_id=200 + i, command=_HISTORY_COMMANDS[i % len(_HISTORY_COMMANDS)], params=None, status="completed" if i % 2 == 0 else "submitted", completed=i % 2 == 0, timestamp=_BASE_TS if i % 2 == 0 else None, ) for i in range(5) ], } # Three fixed callbacks the test suite can pin against. _FAKE_CALLBACKS = [ C2Callback( display_id=1, active=True, host="WORKSTATION-01", user="jdoe", domain="LAB", last_checkin="2026-06-10T00:00:00Z", ), C2Callback( display_id=2, active=True, host="SERVER-DC01", user="svc_backup", domain="LAB", last_checkin="2026-06-10T00:01:00Z", ), C2Callback( display_id=3, active=True, host="LAPTOP-RT", user="admin", domain="LAB", last_checkin="2026-06-10T00:02:00Z", ), ] class FakeAdapter(C2Adapter): """In-memory adapter with deterministic behaviour. Each instance starts with an empty task store and display_ids from 1000. get_task() state progression per task (keyed by display_id): - First call after create_task → submitted, completed=False - Second and subsequent calls → completed=True, status="completed" """ def __init__(self) -> None: self._tasks: dict[int, dict] = {} self._next_task_id = 1000 # Tracks how many times get_task has been called per display_id. self._get_task_calls: dict[int, int] = {} def test_connection(self) -> C2Health: return C2Health(ok=True) def list_callbacks(self) -> list[C2Callback]: return list(_FAKE_CALLBACKS) def create_task( self, callback_display_id: int, command: str, params: str | None = None, ) -> int: tid = self._next_task_id self._next_task_id += 1 self._tasks[tid] = { "display_id": tid, "callback_display_id": callback_display_id, "command": command, "params": params, "status": "submitted", "completed": False, "output": None, } return tid def get_task(self, task_display_id: int) -> C2TaskStatus: """Deterministic state progression: first call → submitted, second+ → completed. Tracks call count regardless of whether the task was created by this instance, so the endpoint poll-on-read flow works across separate adapter instantiations. """ call_count = self._get_task_calls.get(task_display_id, 0) + 1 self._get_task_calls[task_display_id] = call_count task = self._tasks.get(task_display_id) if call_count >= 2: completed = True status = "completed" if task is not None: task["status"] = "completed" task["completed"] = True else: completed = False status = task["status"] if task is not None else "submitted" return C2TaskStatus( display_id=task_display_id, status=status, completed=completed, command=task["command"] if task is not None else None, ) def get_task_output(self, task_display_id: int) -> str: """Returns deterministic output once task is completed; raises C2Error before that.""" # Check call count — completed if get_task was called at least twice. if self._get_task_calls.get(task_display_id, 0) < 2: # Also allow tasks in _tasks that were explicitly set to completed. task = self._tasks.get(task_display_id) if task is None or not task.get("completed", False): raise C2Error("task not completed") task = self._tasks.get(task_display_id) command = task["command"] if task is not None else "unknown" return f"output for task {task_display_id}: {command}\n" def list_callback_tasks( self, callback_display_id: int, page: int = 1, page_size: int = 25, ) -> C2TaskPage: all_items = _FAKE_HISTORY.get(callback_display_id, []) start = (page - 1) * page_size return C2TaskPage( items=all_items[start : start + page_size], total=len(all_items), page=page, page_size=page_size, )