"""Abstract C2 adapter interface and shared dataclasses.""" from __future__ import annotations import base64 import binascii from abc import ABC, abstractmethod from dataclasses import dataclass @dataclass class C2Health: ok: bool error: str | None = None @dataclass class C2Callback: display_id: int active: bool host: str user: str domain: str last_checkin: str # ISO-8601 string @dataclass class C2TaskStatus: display_id: int status: str completed: bool @dataclass class C2TaskPage: items: list[dict] # raw task dicts from Mythic total: int page: int page_size: int def decode_response_text(raw: str) -> str: """Decode a base64-encoded Mythic response_text field. On binascii.Error (binary payload) returns " " + hex string so execution_result never silently corrupts. """ try: return base64.b64decode(raw).decode("utf-8") except binascii.Error: return " " + raw.encode().hex() except UnicodeDecodeError: raw_bytes = base64.b64decode(raw) return " " + raw_bytes.hex() class C2Adapter(ABC): """Thin interface over a C2 backend (Mythic or custom).""" @abstractmethod def test_connection(self) -> C2Health: """Verify that the C2 is reachable and the token is valid.""" ... @abstractmethod def list_callbacks(self) -> list[C2Callback]: """Return active callbacks visible to this API token.""" ... @abstractmethod def create_task( self, callback_display_id: int, command: str, params: str | None = None, ) -> int: """Issue a task and return its Mythic display_id.""" ... @abstractmethod def get_task(self, task_display_id: int) -> C2TaskStatus: """Return current status of a task.""" ... @abstractmethod def get_task_output(self, task_display_id: int) -> str: """Return decoded, concatenated output for a completed task.""" ... @abstractmethod def list_callback_tasks( self, callback_display_id: int, page: int = 1, page_size: int = 25, ) -> C2TaskPage: """Return a paginated history of tasks for a callback.""" ...