"""Create a small, local-only support bundle without copying private evidence.""" from __future__ import annotations import datetime as dt import json import math import os import platform import re import signal import stat import subprocess import sys from collections.abc import Callable from pathlib import Path from typing import Any from . import __version__ from .client import request from .jsonio import JsonInputError, write_new_json Query = Callable[..., dict[str, Any]] MAX_DETECTIONS = 100 TOKEN = re.compile(r"[A-Za-z0-9_.:-]{1,128}\Z") SUPPORT_BUNDLE_SCHEMA = "lumi-eggcracker.support-bundle.v1" MAX_VALIDATION_BYTES = 1024 * 1024 MAX_JSON_DEPTH = 32 MAX_AGGREGATE_COUNT = 1_000_000_000 MAX_STATE_KEYS = 4096 def _token(value: object) -> str | None: return value if isinstance(value, str) and TOKEN.fullmatch(value) else None def _systemd_version() -> str | None: try: result = subprocess.run( ["/usr/bin/systemd-run", "--version"], capture_output=True, text=True, check=False, timeout=5, ) except (OSError, subprocess.TimeoutExpired): return None line = (result.stdout or "").splitlines() return line[0][:80] if result.returncode == 0 and line else None def _host() -> dict[str, Any]: controllers: list[str] = [] try: controllers = sorted( value for value in Path("/sys/fs/cgroup/cgroup.controllers") .read_text(encoding="ascii") .split() if value in {"cpu", "memory", "pids"} ) except (OSError, UnicodeDecodeError): pass return { "platform": platform.system(), "kernel": platform.release(), "machine": platform.machine(), "python": ".".join(str(value) for value in sys.version_info[:3]), "cgroup_v2": Path("/sys/fs/cgroup/cgroup.controllers").is_file(), "controllers": controllers, "pidfd_open": hasattr(os, "pidfd_open"), "pidfd_send_signal": hasattr(signal, "pidfd_send_signal"), "systemd": _systemd_version(), } def _doctor(value: dict[str, Any]) -> dict[str, Any]: discovery = value.get("discovery") if isinstance(value.get("discovery"), dict) else {} network = value.get("network") if isinstance(value.get("network"), dict) else {} primitives = network.get("primitives") if isinstance(network.get("primitives"), dict) else {} return { "result": _token(value.get("result")), "backend": _token(value.get("backend")), "version": _token(value.get("version")), "workload_uid": value.get("workload_uid"), "autonomous_discovery": value.get("autonomous_discovery"), "cgroup_v2": value.get("cgroup_v2"), "pidfd": value.get("pidfd"), "execution_boundary": value.get("execution_boundary"), "installation": value.get("installation"), "incidents": value.get("incidents"), "network": { "mode": _token(network.get("mode")), "cleanup_healthy": network.get("cleanup_healthy"), "primitives_supported": primitives.get("supported"), }, "discovery": { "healthy": discovery.get("healthy"), "consecutive_failures": discovery.get("consecutive_failures"), "last_scan_duration_ms": discovery.get("last_scan_duration_ms"), "last_scan_completed": discovery.get("last_scan_completed"), "receipt_persistence_healthy": discovery.get("receipt_persistence_healthy"), }, } def _detection(value: dict[str, Any]) -> dict[str, Any]: detector = value.get("detector") if isinstance(value.get("detector"), dict) else {} trigger_value = value.get("trigger") trigger = trigger_value.get("kind") if isinstance(trigger_value, dict) else trigger_value result = { "event_id": _token(value.get("event_id")), "result": _token(value.get("result")), "trigger": _token(trigger), "version": _token(value.get("version")), "profile": _token(detector.get("profile")), } boundary = value.get("boundary") if isinstance(boundary, dict): policy_sha256 = boundary.get("policy_sha256") result["boundary"] = { "address_family": _token(boundary.get("address_family")), "mode": _token(boundary.get("mode")), "policy_sha256": ( policy_sha256 if isinstance(policy_sha256, str) and re.fullmatch(r"[0-9a-f]{64}", policy_sha256) else None ), "violation": _token(boundary.get("violation")), } return result def _workload_health(value: dict[str, Any]) -> dict[str, Any]: runs = value.get("runs") if isinstance(value.get("runs"), list) else [] states: dict[str, int] = {} for item in runs: if not isinstance(item, dict): continue state = _token(item.get("state")) if state is not None: states[state] = states.get(state, 0) + 1 return {"run_count": len(runs), "states": states} def _incident_health(value: dict[str, Any]) -> dict[str, Any]: incidents = value.get("incidents") if isinstance(value.get("incidents"), list) else [] active = 0 states: dict[str, int] = {} for item in incidents: if not isinstance(item, dict): continue state = _token(item.get("state")) if state is not None: states[state] = states.get(state, 0) + 1 if state in {"ACTIVE", "ACKNOWLEDGED"}: active += 1 return {"count": len(incidents), "active": active, "states": states} def collect(query: Query = request) -> dict[str, Any]: """Collect only bounded fields from public read-only supervisor queries.""" doctor = query("doctor") detections_raw = query("detections") list_raw = query("list") incidents_raw = query("incidents") detections = detections_raw.get("detections") if not isinstance(detections, list): detections = [] return { "schema_version": "lumi-eggcracker.support-bundle.v1", "generated_utc": dt.datetime.now(dt.UTC).isoformat().replace("+00:00", "Z"), "product": "Lumi Eggcracker", "version": _token(doctor.get("version", __version__)) or __version__, "host": _host(), "health": _doctor(doctor), "workloads": _workload_health(list_raw), "incidents": _incident_health(incidents_raw), "receipts": [_detection(item) for item in detections[:MAX_DETECTIONS] if isinstance(item, dict)], "privacy": { "network": "aggregate-boundary-events-only", "raw_receipts": False, "argv": False, "paths": False, "pids": False, "model_data": False, }, } def write_bundle(destination: Path, query: Query = request) -> dict[str, Any]: if destination.exists() or destination.is_symlink() or not destination.parent.is_dir(): raise JsonInputError("support bundle output must be a new file under an existing directory") value = collect(query) write_new_json(destination, value) return value def main(destination: Path) -> int: try: write_bundle(destination) except (JsonInputError, OSError) as error: print(f"eggcracker support-bundle: {error}", file=sys.stderr) return 4 print(json.dumps({"result": "WRITTEN", "path": str(destination)}, sort_keys=True)) return 0 class _InvalidSupportBundle(ValueError): """Internal marker for a support bundle outside the bounded v1 contract.""" def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: result: dict[str, Any] = {} for key, value in pairs: if key in result: raise _InvalidSupportBundle result[key] = value return result def _reject_json_constant(_value: str) -> None: raise _InvalidSupportBundle def _finite_json_float(value: str) -> float: result = float(value) if not math.isfinite(result): raise _InvalidSupportBundle return result def _check_json_depth(value: str) -> None: depth = 0 quoted = False escaped = False for character in value: if quoted: if escaped: escaped = False elif character == "\\": escaped = True elif character == '"': quoted = False continue if character == '"': quoted = True elif character in "[{": depth += 1 if depth > MAX_JSON_DEPTH: raise _InvalidSupportBundle elif character in "]}": depth -= 1 if depth < 0: raise _InvalidSupportBundle def _read_bounded_regular_file(path: Path) -> bytes: descriptor: int | None = None try: before = path.lstat() if not stat.S_ISREG(before.st_mode) or before.st_size > MAX_VALIDATION_BYTES: raise _InvalidSupportBundle flags = os.O_RDONLY | getattr(os, "O_BINARY", 0) flags |= ( getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NONBLOCK", 0) ) descriptor = os.open(path, flags) opened = os.fstat(descriptor) if ( not stat.S_ISREG(opened.st_mode) or opened.st_size > MAX_VALIDATION_BYTES or opened.st_dev != before.st_dev or opened.st_ino != before.st_ino ): raise _InvalidSupportBundle with os.fdopen(descriptor, "rb") as handle: descriptor = None data = bytearray() while len(data) <= MAX_VALIDATION_BYTES: block = handle.read(min(64 * 1024, MAX_VALIDATION_BYTES + 1 - len(data))) if not block: break data.extend(block) if len(data) > MAX_VALIDATION_BYTES: raise _InvalidSupportBundle return bytes(data) except (OSError, TypeError, ValueError): raise _InvalidSupportBundle from None finally: if descriptor is not None: os.close(descriptor) def _require_object(value: object, keys: set[str]) -> dict[str, Any]: if not isinstance(value, dict) or set(value) != keys: raise _InvalidSupportBundle return value def _require_text(value: object, *, maximum: int = 256) -> None: if not isinstance(value, str) or len(value) > maximum: raise _InvalidSupportBundle try: if len(value.encode("utf-8", errors="strict")) > maximum * 4: raise _InvalidSupportBundle except UnicodeEncodeError: raise _InvalidSupportBundle from None def _require_token(value: object, *, nullable: bool = False) -> None: if nullable and value is None: return if _token(value) is None: raise _InvalidSupportBundle def _require_bool(value: object, *, nullable: bool = False) -> None: if nullable and value is None: return if type(value) is not bool: raise _InvalidSupportBundle def _require_count(value: object, *, nullable: bool = False, maximum: int = MAX_AGGREGATE_COUNT) -> None: if nullable and value is None: return if type(value) is not int or value < 0 or value > maximum: raise _InvalidSupportBundle def _require_finite_number(value: object, *, nullable: bool = False) -> None: if nullable and value is None: return if type(value) is int: if value < 0 or value > MAX_AGGREGATE_COUNT: raise _InvalidSupportBundle return if type(value) is not float or not math.isfinite(value) or value < 0: raise _InvalidSupportBundle def _validate_state_counts(value: object, total: int) -> int: if not isinstance(value, dict) or len(value) > MAX_STATE_KEYS: raise _InvalidSupportBundle subtotal = 0 for state, count in value.items(): _require_token(state) _require_count(count, maximum=total) if count == 0: raise _InvalidSupportBundle subtotal += count if subtotal > total: raise _InvalidSupportBundle return len(value) def _validate_health(value: object) -> None: health = _require_object( value, { "result", "backend", "version", "workload_uid", "autonomous_discovery", "cgroup_v2", "pidfd", "execution_boundary", "installation", "incidents", "network", "discovery", }, ) for name in ("result", "backend", "version"): _require_token(health[name], nullable=True) _require_count(health["workload_uid"], nullable=True, maximum=2**32 - 1) for name in ("autonomous_discovery", "cgroup_v2", "pidfd"): _require_bool(health[name], nullable=True) execution_boundary = health["execution_boundary"] if execution_boundary is not None: execution = _require_object( execution_boundary, {"linux", "architecture", "fcntl", "libc", "user_notification", "supported"}, ) for fact in execution.values(): _require_bool(fact) installation = health["installation"] if installation is not None: if not isinstance(installation, dict) or set(installation) not in ( {"state", "journal", "files_match"}, {"state", "journal", "files_match", "manifest_version"}, ): raise _InvalidSupportBundle _require_token(installation["state"]) _require_bool(installation["journal"]) _require_bool(installation["files_match"]) if "manifest_version" in installation: _require_token(installation["manifest_version"]) incidents = health["incidents"] if incidents is not None: incident_health = _require_object( incidents, {"healthy", "count", "active", "lockdown"}, ) _require_bool(incident_health["healthy"]) _require_bool(incident_health["lockdown"]) _require_count(incident_health["count"]) _require_count(incident_health["active"], maximum=incident_health["count"]) network = _require_object( health["network"], {"mode", "cleanup_healthy", "primitives_supported"}, ) _require_token(network["mode"], nullable=True) _require_bool(network["cleanup_healthy"], nullable=True) _require_bool(network["primitives_supported"], nullable=True) discovery = _require_object( health["discovery"], { "healthy", "consecutive_failures", "last_scan_duration_ms", "last_scan_completed", "receipt_persistence_healthy", }, ) _require_bool(discovery["healthy"], nullable=True) _require_count(discovery["consecutive_failures"], nullable=True) _require_finite_number(discovery["last_scan_duration_ms"], nullable=True) _require_bool(discovery["last_scan_completed"], nullable=True) _require_bool(discovery["receipt_persistence_healthy"], nullable=True) def _validate_support_bundle(value: object) -> dict[str, Any]: bundle = _require_object( value, { "schema_version", "generated_utc", "product", "version", "host", "health", "workloads", "incidents", "receipts", "privacy", }, ) if bundle["schema_version"] != SUPPORT_BUNDLE_SCHEMA or bundle["product"] != "Lumi Eggcracker": raise _InvalidSupportBundle generated = bundle["generated_utc"] if ( not isinstance(generated, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z", generated) ): raise _InvalidSupportBundle try: dt.datetime.fromisoformat(generated) except ValueError: raise _InvalidSupportBundle from None _require_token(bundle["version"]) host = _require_object( bundle["host"], { "platform", "kernel", "machine", "python", "cgroup_v2", "controllers", "pidfd_open", "pidfd_send_signal", "systemd", }, ) for name in ("platform", "kernel", "machine", "python"): _require_text(host[name]) for name in ("cgroup_v2", "pidfd_open", "pidfd_send_signal"): _require_bool(host[name]) controllers = host["controllers"] if ( not isinstance(controllers, list) or len(controllers) > 3 or any(controller not in {"cpu", "memory", "pids"} for controller in controllers) or controllers != sorted(set(controllers)) ): raise _InvalidSupportBundle if host["systemd"] is not None: _require_text(host["systemd"], maximum=80) _validate_health(bundle["health"]) workloads = _require_object(bundle["workloads"], {"run_count", "states"}) _require_count(workloads["run_count"]) workload_state_count = _validate_state_counts(workloads["states"], workloads["run_count"]) incidents = _require_object(bundle["incidents"], {"count", "active", "states"}) _require_count(incidents["count"]) _require_count(incidents["active"], maximum=incidents["count"]) incident_state_count = _validate_state_counts(incidents["states"], incidents["count"]) active_from_states = sum( count for state, count in incidents["states"].items() if state in {"ACTIVE", "ACKNOWLEDGED"} ) if incidents["active"] != active_from_states: raise _InvalidSupportBundle receipts = bundle["receipts"] if not isinstance(receipts, list) or len(receipts) > MAX_DETECTIONS: raise _InvalidSupportBundle for receipt in receipts: if not isinstance(receipt, dict) or set(receipt) not in ( {"event_id", "result", "trigger", "version", "profile"}, {"event_id", "result", "trigger", "version", "profile", "boundary"}, ): raise _InvalidSupportBundle for name in ("event_id", "result", "trigger", "version", "profile"): _require_token(receipt[name], nullable=True) if "boundary" in receipt: boundary = _require_object( receipt["boundary"], {"address_family", "mode", "policy_sha256", "violation"}, ) for name in ("address_family", "mode", "violation"): _require_token(boundary[name], nullable=True) digest = boundary["policy_sha256"] if digest is not None and ( not isinstance(digest, str) or not re.fullmatch(r"[0-9a-f]{64}", digest) ): raise _InvalidSupportBundle privacy = _require_object( bundle["privacy"], {"network", "raw_receipts", "argv", "paths", "pids", "model_data"}, ) if ( type(privacy["network"]) is not str or privacy["network"] != "aggregate-boundary-events-only" or any( type(privacy[name]) is not bool or privacy[name] is not False for name in ("raw_receipts", "argv", "paths", "pids", "model_data") ) ): raise _InvalidSupportBundle return { "result": "STRUCTURE_VALID", "schema_version": SUPPORT_BUNDLE_SCHEMA, "receipts": { "included": len(receipts), "maximum_included": MAX_DETECTIONS, "total_represented": False, }, "workloads": { "run_count": workloads["run_count"], "reported_state_count": workload_state_count, }, "incidents": { "count": incidents["count"], "active_count": incidents["active"], "reported_state_count": incident_state_count, }, "limitations": ( "Structural validation only; not authentication, privacy assurance, completeness, " "containment, or incident truth." ), } def validate_bundle(path: Path) -> dict[str, Any]: """Validate a bounded v1 support bundle and return only aggregate counts.""" try: content = _read_bounded_regular_file(Path(path)) text = content.decode("utf-8", errors="strict") _check_json_depth(text) value = json.loads( text, object_pairs_hook=_unique_json_object, parse_constant=_reject_json_constant, parse_float=_finite_json_float, ) return _validate_support_bundle(value) except ( _InvalidSupportBundle, UnicodeDecodeError, json.JSONDecodeError, RecursionError, TypeError, ValueError, ): raise JsonInputError("support bundle is invalid or cannot be read") from None def validate_main(arguments: list[str]) -> int: if len(arguments) != 1: print( "eggcracker validate-support-bundle: expected exactly one FILE argument", file=sys.stderr, ) return 4 try: summary = validate_bundle(Path(arguments[0])) except JsonInputError: print( "eggcracker validate-support-bundle: support bundle is invalid or cannot be read", file=sys.stderr, ) return 4 print(json.dumps(summary, sort_keys=True, separators=(",", ":"))) return 0