vignette/scripts/capture-g7-topology-evidence.py
2026-08-09 18:22:03 +09:00

1797 lines
64 KiB
Python

#!/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())