#!/usr/bin/env python3 """Capture bounded, metadata-only G7 deployment topology evidence. ``linux_compose`` pins two running Docker Compose containers (API and Caddy) to their exact project/service labels, container IDs, image IDs, init PIDs, start timestamps, and restart counts. ``windows_host`` instead pins the Windows API and cloudflared processes to their PIDs, creation times, executable hashes, command-line hashes, working directories, and one Git SHA. Every sample validates its mode-specific pins both before and after collection so a restart or executable/source replacement cannot be reported as one continuous run. Only infrastructure counters are retained. Socket endpoints, request data, audio, transcripts, command output, and Cloudflare-internal queue data are never written to evidence. """ from __future__ import annotations import argparse import hashlib import json import math import ntpath import os import re import shutil import subprocess import time from dataclasses import asdict, dataclass from datetime import UTC, datetime from decimal import Decimal, InvalidOperation from pathlib import Path, PurePosixPath, PureWindowsPath from typing import Any, Protocol, Sequence SCHEMA_VERSION = "vignette.g7-topology-evidence.v1" LINUX_COMPOSE_MODE = "linux_compose" WINDOWS_HOST_MODE = "windows_host" PINNED_PSUTIL_VERSION = "6.1.1" DEFAULT_INTERVAL_SECONDS = 1.0 DEFAULT_SAMPLES = 10 MAX_SAMPLES = 7_200 MAX_INTERVAL_SECONDS = 60.0 MAX_CAPTURE_WINDOW_SECONDS = 7_200.0 _SHA256_PATTERN = re.compile(r"^sha256:[0-9a-f]{64}$") _RAW_SHA256_PATTERN = re.compile(r"^[0-9a-f]{64}$") _GIT_SHA_PATTERN = re.compile(r"^[0-9a-f]{40,64}$") _VERSION_PATTERN = re.compile(r"^[0-9]+(?:\.[0-9]+){2}(?:[A-Za-z0-9.+-]*)?$") _WINDOWS_PINNED_SCRIPTS = { "runner": "scripts/run-g7-external-proof-window.py", "collector": "scripts/capture-g7-topology-evidence.py", "checker": "scripts/check-g7-external-proof.py", } _BYTE_VALUE_PATTERN = re.compile(r"^([0-9]+(?:\.[0-9]+)?)\s*([A-Za-z]+)$") _RETRANSMIT_PATTERN = re.compile(r"(?:^|\s)retrans:(\d+)(?:/(\d+))?(?:\s|$)") _BYTE_MULTIPLIERS = { "B": Decimal(1), "KB": Decimal(1_000), "MB": Decimal(1_000_000), "GB": Decimal(1_000_000_000), "TB": Decimal(1_000_000_000_000), "KIB": Decimal(1_024), "MIB": Decimal(1_048_576), "GIB": Decimal(1_073_741_824), "TIB": Decimal(1_099_511_627_776), } class EvidenceFailure(RuntimeError): """A required evidence source or pinned runtime contract failed.""" class CommandRunner(Protocol): def available(self, command: str) -> bool: ... def run(self, args: Sequence[str]) -> str: ... class FileSource(Protocol): def read_text(self, path: str) -> str: ... def list_names(self, path: str) -> list[str]: ... class WindowsSource(Protocol): def dependency_version(self, name: str) -> str: ... def read_identity( self, *, pid: int, role: str, git_sha: str, ) -> "WindowsProcessIdentity": ... def read_metrics(self, *, pid: int, role: str) -> dict[str, int | float]: ... def read_tcp_metrics( self, *, api_pid: int, cloudflared_pid: int, api_listen_port: int, ) -> dict[str, object]: ... class SubprocessCommandRunner: def available(self, command: str) -> bool: return shutil.which(command) is not None def run(self, args: Sequence[str]) -> str: try: completed = subprocess.run( list(args), check=False, capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30, shell=False, ) except (OSError, subprocess.TimeoutExpired) as exc: raise EvidenceFailure(f"command_unavailable:{args[0]}") from exc if completed.returncode != 0: raise EvidenceFailure(f"command_failed:{args[0]}") return completed.stdout class LocalFileSource: def read_text(self, path: str) -> str: try: return Path(path).read_text(encoding="utf-8") except (OSError, UnicodeError) as exc: raise EvidenceFailure("required_linux_source_unreadable") from exc def list_names(self, path: str) -> list[str]: try: return os.listdir(path) except OSError as exc: raise EvidenceFailure("required_linux_source_unreadable") from exc def _sha256_file(path: str, *, role: str) -> str: digest = hashlib.sha256() try: with open(path, "rb") as handle: while chunk := handle.read(1024 * 1024): digest.update(chunk) except OSError as exc: raise EvidenceFailure(f"process_executable_unreadable:{role}") from exc return digest.hexdigest() class LocalWindowsSource: """Read process and TCP metadata through psutil without retaining endpoints.""" def __init__(self) -> None: if os.name != "nt": raise EvidenceFailure("windows_host_required") try: import psutil except ImportError as exc: raise EvidenceFailure("command_unavailable:psutil") from exc self._psutil = psutil self._processes: dict[int, Any] = {} def dependency_version(self, name: str) -> str: if name != "psutil": raise EvidenceFailure(f"runtime_dependency_unknown:{name}") version = getattr(self._psutil, "__version__", None) if not isinstance(version, str) or not version: raise EvidenceFailure("psutil_version_unreadable") return version def _process(self, pid: int, *, role: str): cached = self._processes.get(pid) if cached is not None: return cached try: process = self._psutil.Process(pid) except (self._psutil.NoSuchProcess, self._psutil.AccessDenied) as exc: raise EvidenceFailure(f"process_unreadable:{role}") from exc self._processes[pid] = process return process def read_identity( self, *, pid: int, role: str, git_sha: str, ) -> "WindowsProcessIdentity": process = self._process(pid, role=role) try: with process.oneshot(): executable_path = process.exe() executable_name = process.name() cwd = process.cwd() started_epoch = process.create_time() command_line = process.cmdline() except ( self._psutil.NoSuchProcess, self._psutil.AccessDenied, self._psutil.ZombieProcess, OSError, ) as exc: raise EvidenceFailure(f"process_identity_unreadable:{role}") from exc if not executable_path or not executable_name or not cwd or not command_line: raise EvidenceFailure(f"process_identity_incomplete:{role}") command_line_sha256 = hashlib.sha256( "\0".join(command_line).encode("utf-8", errors="strict") ).hexdigest() return WindowsProcessIdentity( role=role, pid=pid, started_at=datetime.fromtimestamp(started_epoch, UTC) .isoformat() .replace("+00:00", "Z"), executable_name=executable_name, executable_sha256=_sha256_file(executable_path, role=role), command_line_sha256=command_line_sha256, git_sha=git_sha, cwd=ntpath.normpath(cwd), ) def read_metrics(self, *, pid: int, role: str) -> dict[str, int | float]: process = self._process(pid, role=role) try: with process.oneshot(): memory = process.memory_info() cpu = process.cpu_times() cpu_percent = process.cpu_percent(interval=None) handles = process.num_handles() threads = process.num_threads() except ( self._psutil.NoSuchProcess, self._psutil.AccessDenied, self._psutil.ZombieProcess, OSError, ) as exc: raise EvidenceFailure(f"process_metrics_unreadable:{role}") from exc peak_rss = getattr(memory, "peak_wset", None) if peak_rss is None: raise EvidenceFailure(f"process_peak_rss_unavailable:{role}") return { "rss_bytes": int(memory.rss), "peak_rss_bytes": int(peak_rss), "cpu_time_seconds": float(cpu.user + cpu.system), "cpu_percent": float(cpu_percent), "handles": int(handles), "threads": int(threads), } def read_tcp_metrics( self, *, api_pid: int, cloudflared_pid: int, api_listen_port: int, ) -> dict[str, object]: try: connections = self._psutil.net_connections(kind="tcp") except (self._psutil.AccessDenied, OSError) as exc: raise EvidenceFailure("windows_tcp_unreadable") from exc roles = {"api": api_pid, "cloudflared": cloudflared_pid} processes: dict[str, dict[str, int]] = { role: {"connections": 0, "established": 0, "listeners": 0} for role in roles } owned_listener_count = 0 conflicting_listener_count = 0 for connection in connections: pid = connection.pid status = str(connection.status).upper() for role, role_pid in roles.items(): if pid == role_pid: processes[role]["connections"] += 1 if status == "ESTABLISHED": processes[role]["established"] += 1 if status == "LISTEN": processes[role]["listeners"] += 1 local = connection.laddr local_port = getattr(local, "port", None) if status == "LISTEN" and local_port == api_listen_port: if pid == api_pid: owned_listener_count += 1 else: conflicting_listener_count += 1 return { "processes": processes, "host_tcp": {"connections": len(connections)}, "api_listener": { "port": api_listen_port, "owned_listener_count": owned_listener_count, "conflicting_listener_count": conflicting_listener_count, }, } @dataclass(frozen=True, slots=True) class CaptureConfig: compose_project: str public_host: str api_container: str api_service: str api_image_digest: str caddy_container: str caddy_service: str caddy_image_digest: str samples: int = DEFAULT_SAMPLES interval_seconds: float = DEFAULT_INTERVAL_SECONDS proc_root: str = "/proc" cgroup_root: str = "/sys/fs/cgroup" @dataclass(frozen=True, slots=True) class WindowsHostConfig: public_host: str repo_root: str git_sha: str git_tree_sha: str runner_script_sha256: str collector_script_sha256: str checker_script_sha256: str psutil_version: str api_pid: int api_executable_name: str api_executable_sha256: str api_cwd: str api_listen_port: int cloudflared_pid: int cloudflared_executable_name: str cloudflared_executable_sha256: str cloudflared_cwd: str samples: int = DEFAULT_SAMPLES interval_seconds: float = DEFAULT_INTERVAL_SECONDS @dataclass(frozen=True, slots=True) class ContainerIdentity: role: str compose_project: str compose_service: str container_id: str container_name: str image_digest: str init_pid: int started_at: str restart_count: int cgroup_path: str @dataclass(frozen=True, slots=True) class WindowsProcessIdentity: role: str pid: int started_at: str executable_name: str executable_sha256: str command_line_sha256: str git_sha: str cwd: str def utc_now() -> str: return datetime.now(UTC).isoformat().replace("+00:00", "Z") def _clean_identifier(value: object, *, code: str) -> str: if not isinstance(value, str): raise EvidenceFailure(code) cleaned = value.strip() if not cleaned or len(cleaned) > 256 or any(ord(char) < 32 for char in cleaned): raise EvidenceFailure(code) return cleaned def _parse_nonnegative_int(value: object, *, code: str) -> int: if isinstance(value, bool): raise EvidenceFailure(code) try: parsed = int(value) except (TypeError, ValueError) as exc: raise EvidenceFailure(code) from exc if parsed < 0: raise EvidenceFailure(code) return parsed def validate_config(config: CaptureConfig) -> None: for value, code in ( (config.compose_project, "compose_project_invalid"), (config.public_host, "public_host_invalid"), (config.api_container, "api_container_invalid"), (config.api_service, "api_service_invalid"), (config.caddy_container, "caddy_container_invalid"), (config.caddy_service, "caddy_service_invalid"), ): _clean_identifier(value, code=code) if config.api_service == config.caddy_service: raise EvidenceFailure("compose_services_not_distinct") for digest, code in ( (config.api_image_digest, "api_image_digest_invalid"), (config.caddy_image_digest, "caddy_image_digest_invalid"), ): if not _SHA256_PATTERN.fullmatch(digest.lower()): raise EvidenceFailure(code) if not 1 <= config.samples <= MAX_SAMPLES: raise EvidenceFailure("sample_count_out_of_bounds") if not math.isfinite(config.interval_seconds) or not ( 0.05 <= config.interval_seconds <= MAX_INTERVAL_SECONDS ): raise EvidenceFailure("sample_interval_out_of_bounds") if (config.samples - 1) * config.interval_seconds > MAX_CAPTURE_WINDOW_SECONDS: raise EvidenceFailure("capture_window_out_of_bounds") for root, code in ( (config.proc_root, "proc_root_invalid"), (config.cgroup_root, "cgroup_root_invalid"), ): pure = PurePosixPath(root) if not pure.is_absolute() or ".." in pure.parts: raise EvidenceFailure(code) def _windows_path(value: object, *, code: str) -> PureWindowsPath: cleaned = _clean_identifier(value, code=code) path = PureWindowsPath(cleaned) if not path.is_absolute() or ".." in path.parts: raise EvidenceFailure(code) return path def validate_windows_config(config: WindowsHostConfig) -> None: _clean_identifier(config.public_host, code="public_host_invalid") repo_root = _windows_path(config.repo_root, code="repo_root_invalid") api_cwd = _windows_path(config.api_cwd, code="api_cwd_invalid") cloudflared_cwd = _windows_path( config.cloudflared_cwd, code="cloudflared_cwd_invalid" ) for cwd, code in ( (api_cwd, "api_cwd_outside_repo"), (cloudflared_cwd, "cloudflared_cwd_outside_repo"), ): if not cwd.is_relative_to(repo_root): raise EvidenceFailure(code) for value, code in ( (config.git_sha, "git_sha_invalid"), (config.git_tree_sha, "git_tree_sha_invalid"), ): if not _GIT_SHA_PATTERN.fullmatch(value.lower()): raise EvidenceFailure(code) for digest, code in ( (config.runner_script_sha256, "runner_script_sha256_invalid"), (config.collector_script_sha256, "collector_script_sha256_invalid"), (config.checker_script_sha256, "checker_script_sha256_invalid"), ): if not _RAW_SHA256_PATTERN.fullmatch(digest.lower()): raise EvidenceFailure(code) if ( not _VERSION_PATTERN.fullmatch(config.psutil_version) or config.psutil_version != PINNED_PSUTIL_VERSION ): raise EvidenceFailure("psutil_version_invalid") for pid, code in ( (config.api_pid, "api_pid_invalid"), (config.cloudflared_pid, "cloudflared_pid_invalid"), ): if isinstance(pid, bool) or not isinstance(pid, int) or pid <= 0: raise EvidenceFailure(code) if config.api_pid == config.cloudflared_pid: raise EvidenceFailure("process_targets_not_distinct") for name, code in ( (config.api_executable_name, "api_executable_name_invalid"), ( config.cloudflared_executable_name, "cloudflared_executable_name_invalid", ), ): cleaned = _clean_identifier(name, code=code) if PureWindowsPath(cleaned).name != cleaned: raise EvidenceFailure(code) for digest, code in ( (config.api_executable_sha256, "api_executable_sha256_invalid"), ( config.cloudflared_executable_sha256, "cloudflared_executable_sha256_invalid", ), ): if not _RAW_SHA256_PATTERN.fullmatch(digest.lower()): raise EvidenceFailure(code) if not 1 <= config.api_listen_port <= 65_535: raise EvidenceFailure("api_listen_port_invalid") if not 1 <= config.samples <= MAX_SAMPLES: raise EvidenceFailure("sample_count_out_of_bounds") if not math.isfinite(config.interval_seconds) or not ( 0.05 <= config.interval_seconds <= MAX_INTERVAL_SECONDS ): raise EvidenceFailure("sample_interval_out_of_bounds") if (config.samples - 1) * config.interval_seconds > MAX_CAPTURE_WINDOW_SECONDS: raise EvidenceFailure("capture_window_out_of_bounds") def _normalized_windows_path(value: str) -> str: return ntpath.normcase(ntpath.normpath(value)) def _read_git_sha( config: WindowsHostConfig, runner: CommandRunner, ) -> str: value = runner.run( ["git", "-C", config.repo_root, "rev-parse", "--verify", "HEAD"] ).strip() if not _GIT_SHA_PATTERN.fullmatch(value.lower()): raise EvidenceFailure("git_sha_unreadable") if value.lower() != config.git_sha.lower(): raise EvidenceFailure("git_sha_drift") return value.lower() def _sha256_evidence_script(repo_root: str, relative_path: str, *, role: str) -> str: path = Path(repo_root, *PurePosixPath(relative_path).parts) digest = hashlib.sha256() try: with path.open("rb") as handle: while chunk := handle.read(1024 * 1024): digest.update(chunk) except OSError as exc: raise EvidenceFailure(f"evidence_script_unreadable:{role}") from exc return digest.hexdigest() def _expected_script_sha256(config: WindowsHostConfig, role: str) -> str: if role == "runner": return config.runner_script_sha256.lower() if role == "collector": return config.collector_script_sha256.lower() if role == "checker": return config.checker_script_sha256.lower() raise EvidenceFailure(f"evidence_script_role_invalid:{role}") def read_windows_source_provenance( config: WindowsHostConfig, runner: CommandRunner, source: WindowsSource, ) -> dict[str, object]: """Observe and enforce one detached, tracked-clean source/toolchain pin.""" git_sha = _read_git_sha(config, runner) branch = runner.run( ["git", "-C", config.repo_root, "rev-parse", "--abbrev-ref", "HEAD"] ).strip() if branch != "HEAD": raise EvidenceFailure("git_head_not_detached") tracked_status = runner.run( [ "git", "-C", config.repo_root, "status", "--porcelain=v1", "--untracked-files=no", ] ) if tracked_status.strip(): raise EvidenceFailure("git_tracked_worktree_dirty") git_tree_sha = runner.run( ["git", "-C", config.repo_root, "rev-parse", "--verify", "HEAD^{tree}"] ).strip().lower() if not _GIT_SHA_PATTERN.fullmatch(git_tree_sha): raise EvidenceFailure("git_tree_sha_unreadable") if git_tree_sha != config.git_tree_sha.lower(): raise EvidenceFailure("git_tree_sha_drift") scripts: dict[str, str] = {} for role, relative_path in _WINDOWS_PINNED_SCRIPTS.items(): digest = _sha256_evidence_script(config.repo_root, relative_path, role=role) if digest != _expected_script_sha256(config, role): raise EvidenceFailure(f"evidence_script_sha256_drift:{role}") scripts[role] = digest psutil_version = source.dependency_version("psutil") if psutil_version != config.psutil_version: raise EvidenceFailure("psutil_version_drift") return { "detached_head": True, "tracked_clean": True, "git_sha": git_sha, "git_tree_sha": git_tree_sha, "script_sha256": scripts, "runtime_dependencies": {"psutil": psutil_version}, } def validate_windows_source_provenance( config: WindowsHostConfig, runner: CommandRunner, source: WindowsSource, baseline: dict[str, object], ) -> None: current = read_windows_source_provenance(config, runner, source) if current != baseline: raise EvidenceFailure("windows_source_provenance_drift") def _expected_windows_target( config: WindowsHostConfig, role: str, ) -> tuple[int, str, str, str]: if role == "api": return ( config.api_pid, config.api_executable_name, config.api_executable_sha256, config.api_cwd, ) if role == "cloudflared": return ( config.cloudflared_pid, config.cloudflared_executable_name, config.cloudflared_executable_sha256, config.cloudflared_cwd, ) raise EvidenceFailure("windows_process_role_invalid") def _validate_windows_target( config: WindowsHostConfig, target: WindowsProcessIdentity, ) -> None: pid, executable_name, executable_sha256, cwd = _expected_windows_target( config, target.role ) if target.pid != pid: raise EvidenceFailure(f"process_pid_mismatch:{target.role}") if target.executable_name.casefold() != executable_name.casefold(): raise EvidenceFailure(f"process_executable_name_mismatch:{target.role}") if target.executable_sha256.lower() != executable_sha256.lower(): raise EvidenceFailure(f"process_executable_sha256_mismatch:{target.role}") if not _RAW_SHA256_PATTERN.fullmatch(target.command_line_sha256.lower()): raise EvidenceFailure(f"process_command_line_sha256_invalid:{target.role}") if target.git_sha.lower() != config.git_sha.lower(): raise EvidenceFailure(f"process_git_sha_mismatch:{target.role}") if _normalized_windows_path(target.cwd) != _normalized_windows_path(cwd): raise EvidenceFailure(f"process_cwd_mismatch:{target.role}") try: started_at = datetime.fromisoformat(target.started_at.replace("Z", "+00:00")) except ValueError as exc: raise EvidenceFailure(f"process_started_at_invalid:{target.role}") from exc if started_at.tzinfo is None: raise EvidenceFailure(f"process_started_at_invalid:{target.role}") if started_at.astimezone(UTC) > datetime.now(UTC): raise EvidenceFailure(f"process_started_at_future:{target.role}") def pin_windows_targets( config: WindowsHostConfig, runner: CommandRunner, source: WindowsSource, ) -> dict[str, WindowsProcessIdentity]: git_sha = _read_git_sha(config, runner) targets: dict[str, WindowsProcessIdentity] = {} for role in ("api", "cloudflared"): pid, _, _, _ = _expected_windows_target(config, role) target = source.read_identity(pid=pid, role=role, git_sha=git_sha) _validate_windows_target(config, target) targets[role] = target if targets["api"].pid == targets["cloudflared"].pid: raise EvidenceFailure("process_targets_not_distinct") return targets def validate_windows_pins( config: WindowsHostConfig, runner: CommandRunner, source: WindowsSource, targets: dict[str, WindowsProcessIdentity], ) -> None: git_sha = _read_git_sha(config, runner) for role in ("api", "cloudflared"): baseline = targets[role] current = source.read_identity(pid=baseline.pid, role=role, git_sha=git_sha) _validate_windows_target(config, current) for field, code in ( ("started_at", "process_start_drift"), ("executable_sha256", "process_executable_drift"), ("command_line_sha256", "process_command_line_drift"), ("cwd", "process_cwd_drift"), ("git_sha", "process_git_sha_drift"), ): baseline_value = getattr(baseline, field) current_value = getattr(current, field) if field == "cwd": matches = _normalized_windows_path( baseline_value ) == _normalized_windows_path(current_value) else: matches = baseline_value.casefold() == current_value.casefold() if not matches: raise EvidenceFailure(f"{code}:{role}") def parse_container_inspect(stdout: str) -> list[dict[str, object]]: try: payload = json.loads(stdout) except json.JSONDecodeError as exc: raise EvidenceFailure("docker_inspect_invalid_json") from exc if not isinstance(payload, list) or not payload: raise EvidenceFailure("docker_inspect_empty") if not all(isinstance(item, dict) for item in payload): raise EvidenceFailure("docker_inspect_invalid_shape") return payload def _identity_from_inspect( item: dict[str, object], *, role: str, expected_project: str, expected_service: str, expected_digest: str, cgroup_path: str, ) -> ContainerIdentity: config = item.get("Config") state = item.get("State") if not isinstance(config, dict) or not isinstance(state, dict): raise EvidenceFailure(f"docker_inspect_fields_missing:{role}") labels = config.get("Labels") if not isinstance(labels, dict): raise EvidenceFailure(f"compose_labels_missing:{role}") project = labels.get("com.docker.compose.project") service = labels.get("com.docker.compose.service") if project != expected_project: raise EvidenceFailure(f"compose_project_mismatch:{role}") if service != expected_service: raise EvidenceFailure(f"compose_service_mismatch:{role}") container_id = _clean_identifier( item.get("Id"), code=f"container_id_missing:{role}" ).lower() if not re.fullmatch(r"[0-9a-f]{64}", container_id): raise EvidenceFailure(f"container_id_invalid:{role}") image_digest = _clean_identifier( item.get("Image"), code=f"image_digest_missing:{role}" ).lower() if image_digest != expected_digest.lower(): raise EvidenceFailure(f"image_digest_mismatch:{role}") if state.get("Running") is not True: raise EvidenceFailure(f"container_not_running:{role}") init_pid = _parse_nonnegative_int(state.get("Pid"), code=f"init_pid_missing:{role}") if init_pid <= 0: raise EvidenceFailure(f"init_pid_invalid:{role}") started_at = _clean_identifier( state.get("StartedAt"), code=f"started_at_missing:{role}" ) restart_count = _parse_nonnegative_int( item.get("RestartCount"), code=f"restart_count_missing:{role}" ) container_name = _clean_identifier( item.get("Name"), code=f"container_name_missing:{role}" ).lstrip("/") if not container_name: raise EvidenceFailure(f"container_name_missing:{role}") return ContainerIdentity( role=role, compose_project=expected_project, compose_service=expected_service, container_id=container_id, container_name=container_name, image_digest=image_digest, init_pid=init_pid, started_at=started_at, restart_count=restart_count, cgroup_path=cgroup_path, ) def _posix_join(root: str, *parts: str) -> str: result = PurePosixPath(root) for part in parts: result /= part return str(result) def read_cgroup_path(source: FileSource, *, proc_root: str, pid: int, role: str) -> str: path = _posix_join(proc_root, str(pid), "cgroup") try: content = source.read_text(path) except Exception as exc: if isinstance(exc, EvidenceFailure): raise raise EvidenceFailure(f"proc_cgroup_missing:{role}") from exc unified_paths = [] for line in content.splitlines(): parts = line.split(":", 2) if len(parts) == 3 and parts[0] == "0" and parts[1] == "": unified_paths.append(parts[2].strip()) if len(unified_paths) != 1: raise EvidenceFailure(f"cgroup_v2_path_missing:{role}") cgroup_path = PurePosixPath(unified_paths[0]) if not cgroup_path.is_absolute() or ".." in cgroup_path.parts: raise EvidenceFailure(f"cgroup_v2_path_invalid:{role}") return str(cgroup_path) def _inspect( runner: CommandRunner, selectors: Sequence[str], ) -> list[dict[str, object]]: stdout = runner.run(["docker", "inspect", "--type", "container", *selectors]) items = parse_container_inspect(stdout) if len(items) != len(selectors): raise EvidenceFailure("docker_inspect_target_count_mismatch") return items def pin_targets( config: CaptureConfig, runner: CommandRunner, source: FileSource, ) -> dict[str, ContainerIdentity]: items = _inspect(runner, [config.api_container, config.caddy_container]) by_service: dict[str, dict[str, object]] = {} for item in items: item_config = item.get("Config") labels = item_config.get("Labels") if isinstance(item_config, dict) else None service = ( labels.get("com.docker.compose.service") if isinstance(labels, dict) else None ) if isinstance(service, str): if service in by_service: raise EvidenceFailure("docker_inspect_duplicate_service") by_service[service] = item targets: dict[str, ContainerIdentity] = {} for role, service, digest in ( ("api", config.api_service, config.api_image_digest), ("caddy", config.caddy_service, config.caddy_image_digest), ): item = by_service.get(service) if item is None: raise EvidenceFailure(f"compose_service_missing:{role}") state = item.get("State") if not isinstance(state, dict): raise EvidenceFailure(f"docker_inspect_fields_missing:{role}") pid = _parse_nonnegative_int(state.get("Pid"), code=f"init_pid_missing:{role}") cgroup_path = read_cgroup_path( source, proc_root=config.proc_root, pid=pid, role=role, ) targets[role] = _identity_from_inspect( item, role=role, expected_project=config.compose_project, expected_service=service, expected_digest=digest, cgroup_path=cgroup_path, ) if targets["api"].container_id == targets["caddy"].container_id: raise EvidenceFailure("container_targets_not_distinct") return targets def validate_pins( runner: CommandRunner, source: FileSource, config: CaptureConfig, targets: dict[str, ContainerIdentity], ) -> None: ordered = [targets["api"], targets["caddy"]] items = _inspect(runner, [target.container_id for target in ordered]) by_id = { str(item.get("Id") or "").lower(): item for item in items if isinstance(item, dict) } for baseline in ordered: item = by_id.get(baseline.container_id) if item is None: raise EvidenceFailure(f"container_id_drift:{baseline.role}") current_image_digest = str(item.get("Image") or "").strip().lower() if current_image_digest != baseline.image_digest: raise EvidenceFailure(f"image_digest_drift:{baseline.role}") current = _identity_from_inspect( item, role=baseline.role, expected_project=baseline.compose_project, expected_service=baseline.compose_service, expected_digest=baseline.image_digest, cgroup_path=baseline.cgroup_path, ) if current.container_name != baseline.container_name: raise EvidenceFailure(f"container_name_drift:{baseline.role}") if current.init_pid != baseline.init_pid: raise EvidenceFailure(f"init_pid_drift:{baseline.role}") if ( current.started_at != baseline.started_at or current.restart_count != baseline.restart_count ): raise EvidenceFailure(f"container_restart_drift:{baseline.role}") current_cgroup_path = read_cgroup_path( source, proc_root=config.proc_root, pid=current.init_pid, role=current.role, ) if current_cgroup_path != baseline.cgroup_path: raise EvidenceFailure(f"cgroup_path_drift:{baseline.role}") def _read_source(source: FileSource, path: str, *, code: str) -> str: try: value = source.read_text(path) except Exception as exc: if isinstance(exc, EvidenceFailure): raise raise EvidenceFailure(code) from exc if not value.strip(): raise EvidenceFailure(code) return value def parse_cpu_stat(content: str, *, role: str) -> dict[str, int]: parsed: dict[str, int] = {} for line in content.splitlines(): parts = line.split() if len(parts) != 2: raise EvidenceFailure(f"cgroup_cpu_stat_invalid:{role}") key, raw_value = parts if key in parsed: raise EvidenceFailure(f"cgroup_cpu_stat_duplicate:{role}") parsed[key] = _parse_nonnegative_int( raw_value, code=f"cgroup_cpu_stat_invalid:{role}" ) required = {"usage_usec", "user_usec", "system_usec"} if not required.issubset(parsed): raise EvidenceFailure(f"cgroup_cpu_stat_fields_missing:{role}") return dict(sorted(parsed.items())) def read_cgroup_metrics( source: FileSource, *, cgroup_root: str, target: ContainerIdentity, ) -> dict[str, object]: base = _posix_join(cgroup_root, target.cgroup_path.lstrip("/")) def read_integer(filename: str) -> int: content = _read_source( source, _posix_join(base, filename), code=f"cgroup_source_missing:{target.role}:{filename}", ) return _parse_nonnegative_int( content.strip(), code=f"cgroup_source_invalid:{target.role}:{filename}" ) cpu_stat = parse_cpu_stat( _read_source( source, _posix_join(base, "cpu.stat"), code=f"cgroup_source_missing:{target.role}:cpu.stat", ), role=target.role, ) return { "memory_current_bytes": read_integer("memory.current"), "memory_peak_bytes": read_integer("memory.peak"), "cpu_stat": cpu_stat, "pids_current": read_integer("pids.current"), } def parse_proc_status(content: str, *, role: str) -> dict[str, int]: values: dict[str, int] = {} for line in content.splitlines(): if ":" not in line: continue key, raw = line.split(":", 1) if key in {"VmRSS", "VmHWM"}: parts = raw.split() if len(parts) != 2 or parts[1] != "kB": raise EvidenceFailure(f"proc_status_field_invalid:{role}:{key}") values[f"{key}_bytes"] = ( _parse_nonnegative_int( parts[0], code=f"proc_status_field_invalid:{role}:{key}" ) * 1_024 ) elif key == "Threads": values["Threads"] = _parse_nonnegative_int( raw.strip(), code=f"proc_status_field_invalid:{role}:Threads" ) required = {"VmRSS_bytes", "VmHWM_bytes", "Threads"} if not required.issubset(values): raise EvidenceFailure(f"proc_status_fields_missing:{role}") return { "vm_rss_bytes": values["VmRSS_bytes"], "vm_hwm_bytes": values["VmHWM_bytes"], "threads": values["Threads"], } def read_proc_metrics( source: FileSource, *, proc_root: str, target: ContainerIdentity, ) -> dict[str, int]: status_path = _posix_join(proc_root, str(target.init_pid), "status") metrics = parse_proc_status( _read_source( source, status_path, code=f"proc_status_missing:{target.role}", ), role=target.role, ) try: fd_names = source.list_names(_posix_join(proc_root, str(target.init_pid), "fd")) except Exception as exc: if isinstance(exc, EvidenceFailure): raise raise EvidenceFailure(f"proc_fd_missing:{target.role}") from exc metrics["fd_count"] = len(fd_names) return metrics def parse_human_bytes(value: str, *, code: str) -> int: match = _BYTE_VALUE_PATTERN.fullmatch(value.strip()) if match is None: raise EvidenceFailure(code) try: number = Decimal(match.group(1)) except InvalidOperation as exc: raise EvidenceFailure(code) from exc multiplier = _BYTE_MULTIPLIERS.get(match.group(2).upper()) if multiplier is None: raise EvidenceFailure(code) return int(number * multiplier) def _parse_percent(value: object, *, code: str) -> float: if not isinstance(value, str) or not value.strip().endswith("%"): raise EvidenceFailure(code) try: result = float(value.strip()[:-1]) except ValueError as exc: raise EvidenceFailure(code) from exc if not math.isfinite(result) or result < 0: raise EvidenceFailure(code) return result def _parse_pair(value: object, *, code: str) -> tuple[int, int]: if not isinstance(value, str): raise EvidenceFailure(code) parts = value.split("/") if len(parts) != 2: raise EvidenceFailure(code) return ( parse_human_bytes(parts[0], code=code), parse_human_bytes(parts[1], code=code), ) def parse_docker_stats( stdout: str, targets: dict[str, ContainerIdentity], ) -> dict[str, dict[str, object]]: records: list[dict[str, object]] = [] for line in stdout.splitlines(): if not line.strip(): continue try: record = json.loads(line) except json.JSONDecodeError as exc: raise EvidenceFailure("docker_stats_invalid_json") from exc if not isinstance(record, dict): raise EvidenceFailure("docker_stats_invalid_shape") records.append(record) if len(records) != len(targets): raise EvidenceFailure("docker_stats_target_count_mismatch") result: dict[str, dict[str, object]] = {} for role, target in targets.items(): matches = [] for record in records: stats_id = str(record.get("ID") or record.get("Container") or "").lower() if stats_id and target.container_id.startswith(stats_id): matches.append(record) if len(matches) != 1: raise EvidenceFailure(f"docker_stats_target_missing:{role}") record = matches[0] memory_usage, memory_limit = _parse_pair( record.get("MemUsage"), code=f"docker_stats_mem_usage_invalid:{role}" ) network_rx, network_tx = _parse_pair( record.get("NetIO"), code=f"docker_stats_net_io_invalid:{role}" ) block_read, block_write = _parse_pair( record.get("BlockIO"), code=f"docker_stats_block_io_invalid:{role}" ) result[role] = { "cpu_percent": _parse_percent( record.get("CPUPerc"), code=f"docker_stats_cpu_invalid:{role}" ), "memory_usage_bytes": memory_usage, "memory_limit_bytes": memory_limit, "memory_percent": _parse_percent( record.get("MemPerc"), code=f"docker_stats_mem_percent_invalid:{role}" ), "network_rx_bytes": network_rx, "network_tx_bytes": network_tx, "block_read_bytes": block_read, "block_write_bytes": block_write, "pids": _parse_nonnegative_int( record.get("PIDs"), code=f"docker_stats_pids_invalid:{role}" ), } return result def parse_ss_tcp(stdout: str) -> dict[str, int]: lines = stdout.splitlines() if not lines: raise EvidenceFailure("ss_output_empty") header_index = -1 recv_index = -1 send_index = -1 for index, line in enumerate(lines): tokens = line.split() if "Recv-Q" in tokens and "Send-Q" in tokens: header_index = index recv_index = tokens.index("Recv-Q") send_index = tokens.index("Send-Q") break if header_index < 0: raise EvidenceFailure("ss_queue_fields_missing") connections = 0 recv_q_total = 0 send_q_total = 0 retransmit_current_total = 0 retransmit_cumulative_total = 0 retransmit_fields_observed = 0 for line in lines[header_index + 1 :]: if not line.strip(): continue if not line[0].isspace(): tokens = line.split() if len(tokens) <= max(recv_index, send_index): raise EvidenceFailure("ss_connection_fields_missing") recv_q_total += _parse_nonnegative_int( tokens[recv_index], code="ss_recv_q_invalid" ) send_q_total += _parse_nonnegative_int( tokens[send_index], code="ss_send_q_invalid" ) connections += 1 for match in _RETRANSMIT_PATTERN.finditer(line): current = int(match.group(1)) cumulative = int(match.group(2) or match.group(1)) retransmit_current_total += current retransmit_cumulative_total += cumulative retransmit_fields_observed += 1 return { "connections": connections, "recv_q_bytes_total": recv_q_total, "send_q_bytes_total": send_q_total, "retransmit_current_total": retransmit_current_total, "retransmit_cumulative_total": retransmit_cumulative_total, "retransmit_fields_observed": retransmit_fields_observed, } def collect_sample( runner: CommandRunner, source: FileSource, config: CaptureConfig, targets: dict[str, ContainerIdentity], *, sequence: int, ) -> dict[str, object]: validate_pins(runner, source, config, targets) container_metrics: dict[str, dict[str, object]] = {} for role, target in targets.items(): container_metrics[role] = { "cgroup_v2": read_cgroup_metrics( source, cgroup_root=config.cgroup_root, target=target, ), "init_process": read_proc_metrics( source, proc_root=config.proc_root, target=target, ), } stats_stdout = runner.run( [ "docker", "stats", "--no-stream", "--format", "{{json .}}", targets["api"].container_id, targets["caddy"].container_id, ] ) stats = parse_docker_stats(stats_stdout, targets) for role in targets: container_metrics[role]["docker_stats"] = stats[role] host_tcp = parse_ss_tcp(runner.run(["ss", "-tinm"])) validate_pins(runner, source, config, targets) return { "sequence": sequence, "observed_at_utc": utc_now(), "containers": container_metrics, "host_tcp": host_tcp, } def _summary(samples: list[dict[str, object]]) -> dict[str, object]: containers: dict[str, dict[str, int | float]] = {} for role in ("api", "caddy"): role_samples = [sample["containers"][role] for sample in samples] # type: ignore[index] containers[role] = { "cgroup_memory_current_bytes_max": max( item["cgroup_v2"]["memory_current_bytes"] for item in role_samples # type: ignore[index] ), "cgroup_memory_peak_bytes_max": max( item["cgroup_v2"]["memory_peak_bytes"] for item in role_samples # type: ignore[index] ), "cgroup_cpu_usage_usec_max": max( item["cgroup_v2"]["cpu_stat"]["usage_usec"] for item in role_samples # type: ignore[index] ), "cgroup_pids_current_max": max( item["cgroup_v2"]["pids_current"] for item in role_samples # type: ignore[index] ), "proc_vm_rss_bytes_max": max( item["init_process"]["vm_rss_bytes"] for item in role_samples # type: ignore[index] ), "proc_vm_hwm_bytes_max": max( item["init_process"]["vm_hwm_bytes"] for item in role_samples # type: ignore[index] ), "proc_threads_max": max( item["init_process"]["threads"] for item in role_samples # type: ignore[index] ), "proc_fd_count_max": max( item["init_process"]["fd_count"] for item in role_samples # type: ignore[index] ), "docker_cpu_percent_max": max( item["docker_stats"]["cpu_percent"] for item in role_samples # type: ignore[index] ), "docker_memory_usage_bytes_max": max( item["docker_stats"]["memory_usage_bytes"] for item in role_samples # type: ignore[index] ), "docker_pids_max": max( item["docker_stats"]["pids"] for item in role_samples # type: ignore[index] ), } tcp_samples = [sample["host_tcp"] for sample in samples] host_tcp = { f"{field}_max": max(item[field] for item in tcp_samples) # type: ignore[index] for field in ( "connections", "recv_q_bytes_total", "send_q_bytes_total", "retransmit_current_total", "retransmit_cumulative_total", ) } return {"containers": containers, "host_tcp": host_tcp} def _scope() -> dict[str, object]: return { "metadata_only": True, "raw_command_output_retained": False, "socket_endpoints_retained": False, "request_payloads_retained": False, "audio_retained": False, "transcripts_retained": False, "host_tcp_scope": "all_tcp_sockets_visible_to_host_sampler", "cloudflare_edge": { "measured": False, "internal_queue_measured": False, "evidence_boundary": "separate_external_artifact_required", }, } def base_evidence(config: CaptureConfig) -> dict[str, object]: return { "schema_version": SCHEMA_VERSION, "topology_mode": LINUX_COMPOSE_MODE, "status": "running", "started_at_utc": utc_now(), "ended_at_utc": None, "scope": _scope(), "requested": { "compose_project": config.compose_project, "public_host": config.public_host, "samples": config.samples, "interval_seconds": config.interval_seconds, "roles": { "api": { "compose_service": config.api_service, "expected_image_digest": config.api_image_digest.lower(), }, "caddy": { "compose_service": config.caddy_service, "expected_image_digest": config.caddy_image_digest.lower(), }, }, }, "targets": {}, "samples_completed": 0, "samples": [], "summary": {}, "failure_type": None, } def capture_evidence( config: CaptureConfig, runner: CommandRunner, source: FileSource, *, sleep=time.sleep, ) -> dict[str, object]: validate_config(config) for command in ("docker", "ss"): if not runner.available(command): raise EvidenceFailure(f"command_unavailable:{command}") targets = pin_targets(config, runner, source) evidence = base_evidence(config) evidence["targets"] = {role: asdict(target) for role, target in targets.items()} samples: list[dict[str, object]] = [] for index in range(config.samples): if index: sleep(config.interval_seconds) samples.append( collect_sample( runner, source, config, targets, sequence=index + 1, ) ) evidence["samples"] = samples evidence["samples_completed"] = len(samples) evidence["summary"] = _summary(samples) evidence["status"] = "passed" evidence["ended_at_utc"] = utc_now() return evidence def _validate_windows_process_metrics( metrics: dict[str, int | float], *, role: str, ) -> None: for field in ("rss_bytes", "peak_rss_bytes", "handles", "threads"): value = metrics.get(field) if isinstance(value, bool) or not isinstance(value, int) or value <= 0: raise EvidenceFailure(f"process_metric_invalid:{role}:{field}") for field, positive in (("cpu_time_seconds", True), ("cpu_percent", False)): value = metrics.get(field) if isinstance(value, bool) or not isinstance(value, (int, float)): raise EvidenceFailure(f"process_metric_invalid:{role}:{field}") numeric = float(value) if not math.isfinite(numeric) or numeric < 0 or (positive and numeric == 0): raise EvidenceFailure(f"process_metric_invalid:{role}:{field}") def _validate_windows_tcp_metrics( metrics: dict[str, object], *, config: WindowsHostConfig, ) -> None: processes = metrics.get("processes") host_tcp = metrics.get("host_tcp") listener = metrics.get("api_listener") if not isinstance(processes, dict) or set(processes) != {"api", "cloudflared"}: raise EvidenceFailure("windows_tcp_processes_invalid") for role in ("api", "cloudflared"): values = processes.get(role) if not isinstance(values, dict): raise EvidenceFailure(f"windows_tcp_process_invalid:{role}") for field in ("connections", "established", "listeners"): value = values.get(field) if isinstance(value, bool) or not isinstance(value, int) or value < 0: raise EvidenceFailure(f"windows_tcp_metric_invalid:{role}:{field}") if not isinstance(host_tcp, dict): raise EvidenceFailure("windows_host_tcp_invalid") host_connections = host_tcp.get("connections") if ( isinstance(host_connections, bool) or not isinstance(host_connections, int) or host_connections <= 0 ): raise EvidenceFailure("windows_host_tcp_invalid") if not isinstance(listener, dict): raise EvidenceFailure("api_listener_invalid") if listener.get("port") != config.api_listen_port: raise EvidenceFailure("api_listener_port_mismatch") owned = listener.get("owned_listener_count") conflicts = listener.get("conflicting_listener_count") if isinstance(owned, bool) or not isinstance(owned, int) or owned <= 0: raise EvidenceFailure("api_listener_not_owned") if isinstance(conflicts, bool) or not isinstance(conflicts, int) or conflicts != 0: raise EvidenceFailure("api_listener_owner_conflict") api = processes["api"] cloudflared = processes["cloudflared"] if api.get("listeners", 0) <= 0: raise EvidenceFailure("api_process_listener_missing") if cloudflared.get("established", 0) <= 0: raise EvidenceFailure("cloudflared_tunnel_connection_missing") def collect_windows_sample( config: WindowsHostConfig, runner: CommandRunner, source: WindowsSource, targets: dict[str, WindowsProcessIdentity], source_provenance: dict[str, object], *, sequence: int, ) -> dict[str, object]: validate_windows_source_provenance(config, runner, source, source_provenance) validate_windows_pins(config, runner, source, targets) process_metrics: dict[str, dict[str, int | float]] = {} for role, target in targets.items(): metrics = source.read_metrics(pid=target.pid, role=role) _validate_windows_process_metrics(metrics, role=role) process_metrics[role] = metrics tcp_metrics = source.read_tcp_metrics( api_pid=targets["api"].pid, cloudflared_pid=targets["cloudflared"].pid, api_listen_port=config.api_listen_port, ) _validate_windows_tcp_metrics(tcp_metrics, config=config) validate_windows_pins(config, runner, source, targets) validate_windows_source_provenance(config, runner, source, source_provenance) return { "sequence": sequence, "observed_at_utc": utc_now(), "processes": process_metrics, "process_tcp": tcp_metrics["processes"], "host_tcp": tcp_metrics["host_tcp"], "api_listener": tcp_metrics["api_listener"], } def _validate_windows_cpu_monotonic( previous: dict[str, object], current: dict[str, object], ) -> None: for role in ("api", "cloudflared"): previous_value = previous["processes"][role]["cpu_time_seconds"] # type: ignore[index] current_value = current["processes"][role]["cpu_time_seconds"] # type: ignore[index] if current_value < previous_value: # type: ignore[operator] raise EvidenceFailure(f"process_cpu_time_regressed:{role}") def _windows_summary(samples: list[dict[str, object]]) -> dict[str, object]: processes: dict[str, dict[str, int | float]] = {} for role in ("api", "cloudflared"): process_samples = [sample["processes"][role] for sample in samples] # type: ignore[index] tcp_samples = [sample["process_tcp"][role] for sample in samples] # type: ignore[index] processes[role] = { f"{field}_max": max(item[field] for item in process_samples) # type: ignore[index] for field in ( "rss_bytes", "peak_rss_bytes", "cpu_time_seconds", "cpu_percent", "handles", "threads", ) } processes[role].update( { f"tcp_{field}_max": max(item[field] for item in tcp_samples) # type: ignore[index] for field in ("connections", "established", "listeners") } ) listener_samples = [sample["api_listener"] for sample in samples] host_samples = [sample["host_tcp"] for sample in samples] return { "processes": processes, "api_listener": { "port": listener_samples[0]["port"], # type: ignore[index] "owned_listener_count_min": min( item["owned_listener_count"] for item in listener_samples # type: ignore[index] ), "conflicting_listener_count_max": max( item["conflicting_listener_count"] for item in listener_samples # type: ignore[index] ), }, "host_tcp": { "connections_max": max(item["connections"] for item in host_samples) # type: ignore[index] }, } def base_windows_evidence(config: WindowsHostConfig) -> dict[str, object]: scope = _scope() scope["topology_boundary"] = "windows_host_api_and_cloudflared_processes" scope["configured_listener_port_retained"] = True return { "schema_version": SCHEMA_VERSION, "topology_mode": WINDOWS_HOST_MODE, "status": "running", "started_at_utc": utc_now(), "ended_at_utc": None, "scope": scope, "requested": { "public_host": config.public_host, "repo_root": config.repo_root, "git_sha": config.git_sha.lower(), "source_pin": { "git_tree_sha": config.git_tree_sha.lower(), "script_sha256": { "runner": config.runner_script_sha256.lower(), "collector": config.collector_script_sha256.lower(), "checker": config.checker_script_sha256.lower(), }, "runtime_dependencies": {"psutil": config.psutil_version}, }, "samples": config.samples, "interval_seconds": config.interval_seconds, "api_listen_port": config.api_listen_port, "roles": { "api": { "pid": config.api_pid, "expected_executable_name": config.api_executable_name, "expected_executable_sha256": ( config.api_executable_sha256.lower() ), "expected_cwd": config.api_cwd, }, "cloudflared": { "pid": config.cloudflared_pid, "expected_executable_name": config.cloudflared_executable_name, "expected_executable_sha256": ( config.cloudflared_executable_sha256.lower() ), "expected_cwd": config.cloudflared_cwd, }, }, }, "source_provenance": {}, "targets": {}, "samples_completed": 0, "samples": [], "summary": {}, "failure_type": None, } def capture_windows_evidence( config: WindowsHostConfig, runner: CommandRunner, source: WindowsSource, *, sleep=time.sleep, ) -> dict[str, object]: validate_windows_config(config) if not runner.available("git"): raise EvidenceFailure("command_unavailable:git") source_provenance = read_windows_source_provenance(config, runner, source) targets = pin_windows_targets(config, runner, source) evidence = base_windows_evidence(config) evidence["source_provenance"] = source_provenance evidence["targets"] = {role: asdict(target) for role, target in targets.items()} samples: list[dict[str, object]] = [] for index in range(config.samples): if index: sleep(config.interval_seconds) sample = collect_windows_sample( config, runner, source, targets, source_provenance, sequence=index + 1, ) if samples: _validate_windows_cpu_monotonic(samples[-1], sample) samples.append(sample) evidence["samples"] = samples evidence["samples_completed"] = len(samples) evidence["summary"] = _windows_summary(samples) evidence["status"] = "passed" evidence["ended_at_utc"] = utc_now() return evidence def parser() -> argparse.ArgumentParser: result = argparse.ArgumentParser( description="Bounded metadata-only G7 deployment topology sampler" ) result.add_argument( "--topology-mode", choices=("linux-compose", "windows-host"), default="linux-compose", ) result.add_argument("--compose-project") result.add_argument( "--public-host", required=True, help="public WSS/API host bound to this exact deployment", ) result.add_argument("--api-container") result.add_argument("--api-service", default="api") result.add_argument("--api-image-digest") result.add_argument("--caddy-container") result.add_argument("--caddy-service", default="caddy") result.add_argument("--caddy-image-digest") result.add_argument("--repo-root") result.add_argument("--git-sha") result.add_argument("--git-tree-sha") result.add_argument("--runner-script-sha256") result.add_argument("--collector-script-sha256") result.add_argument("--checker-script-sha256") result.add_argument("--psutil-version") result.add_argument("--api-pid", type=int) result.add_argument("--api-executable-name") result.add_argument("--api-executable-sha256") result.add_argument("--api-cwd") result.add_argument("--api-listen-port", type=int) result.add_argument("--cloudflared-pid", type=int) result.add_argument("--cloudflared-executable-name") result.add_argument("--cloudflared-executable-sha256") result.add_argument("--cloudflared-cwd") result.add_argument("--samples", type=int, default=DEFAULT_SAMPLES) result.add_argument( "--interval-seconds", type=float, default=DEFAULT_INTERVAL_SECONDS ) result.add_argument("--proc-root", default="/proc") result.add_argument("--cgroup-root", default="/sys/fs/cgroup") result.add_argument("--evidence-output", type=Path) return result def _required_cli_text(args: argparse.Namespace, name: str) -> str: value = getattr(args, name) if not isinstance(value, str) or not value.strip(): raise EvidenceFailure(f"required_argument_missing:{name.replace('_', '-')}") return value.strip() def _required_cli_int(args: argparse.Namespace, name: str) -> int: value = getattr(args, name) if isinstance(value, bool) or not isinstance(value, int): raise EvidenceFailure(f"required_argument_missing:{name.replace('_', '-')}") return value def _compose_config_from_args(args: argparse.Namespace) -> CaptureConfig: return CaptureConfig( compose_project=_required_cli_text(args, "compose_project"), public_host=args.public_host, api_container=_required_cli_text(args, "api_container"), api_service=args.api_service, api_image_digest=_required_cli_text(args, "api_image_digest"), caddy_container=_required_cli_text(args, "caddy_container"), caddy_service=args.caddy_service, caddy_image_digest=_required_cli_text(args, "caddy_image_digest"), samples=args.samples, interval_seconds=args.interval_seconds, proc_root=args.proc_root, cgroup_root=args.cgroup_root, ) def _windows_config_from_args(args: argparse.Namespace) -> WindowsHostConfig: return WindowsHostConfig( public_host=args.public_host, repo_root=_required_cli_text(args, "repo_root"), git_sha=_required_cli_text(args, "git_sha"), git_tree_sha=_required_cli_text(args, "git_tree_sha"), runner_script_sha256=_required_cli_text(args, "runner_script_sha256"), collector_script_sha256=_required_cli_text( args, "collector_script_sha256" ), checker_script_sha256=_required_cli_text(args, "checker_script_sha256"), psutil_version=_required_cli_text(args, "psutil_version"), api_pid=_required_cli_int(args, "api_pid"), api_executable_name=_required_cli_text(args, "api_executable_name"), api_executable_sha256=_required_cli_text(args, "api_executable_sha256"), api_cwd=_required_cli_text(args, "api_cwd"), api_listen_port=_required_cli_int(args, "api_listen_port"), cloudflared_pid=_required_cli_int(args, "cloudflared_pid"), cloudflared_executable_name=_required_cli_text( args, "cloudflared_executable_name" ), cloudflared_executable_sha256=_required_cli_text( args, "cloudflared_executable_sha256" ), cloudflared_cwd=_required_cli_text(args, "cloudflared_cwd"), samples=args.samples, interval_seconds=args.interval_seconds, ) def _minimal_failure_evidence( *, topology_mode: str, public_host: str, ) -> dict[str, object]: observed_at = utc_now() return { "schema_version": SCHEMA_VERSION, "topology_mode": topology_mode, "status": "failed", "started_at_utc": observed_at, "ended_at_utc": observed_at, "scope": _scope(), "requested": {"public_host": public_host}, "targets": {}, "samples_completed": 0, "samples": [], "summary": {}, "failure_type": None, } def _write_evidence(evidence: dict[str, object], output: Path | None) -> None: payload = json.dumps(evidence, ensure_ascii=False, indent=2, sort_keys=True) + "\n" if output is not None: output.parent.mkdir(parents=True, exist_ok=True) output.write_text(payload, encoding="utf-8") print(payload, end="") def main() -> int: args = parser().parse_args() topology_mode = ( WINDOWS_HOST_MODE if args.topology_mode == "windows-host" else LINUX_COMPOSE_MODE ) config: CaptureConfig | WindowsHostConfig | None = None try: if topology_mode == LINUX_COMPOSE_MODE: config = _compose_config_from_args(args) evidence = capture_evidence( config, SubprocessCommandRunner(), LocalFileSource() ) else: config = _windows_config_from_args(args) evidence = capture_windows_evidence( config, SubprocessCommandRunner(), LocalWindowsSource(), ) _write_evidence(evidence, args.evidence_output) return 0 except EvidenceFailure as exc: if isinstance(config, CaptureConfig): evidence = base_evidence(config) elif isinstance(config, WindowsHostConfig): evidence = base_windows_evidence(config) else: evidence = _minimal_failure_evidence( topology_mode=topology_mode, public_host=args.public_host, ) evidence["status"] = "failed" evidence["failure_type"] = str(exc) evidence["ended_at_utc"] = utc_now() _write_evidence(evidence, args.evidence_output) return 1 if __name__ == "__main__": raise SystemExit(main())