#!/usr/bin/env bash
# Purpose: audit a running agent container from the host - what it has mounted, what is in
#          its environment, which networks it is attached to, whether a container socket is
#          exposed, and which privilege settings are in force - and name each finding as
#          one of the faults from this part's challenge.
# Platform: spark, strix, nvidia (Docker Engine or Podman); mac through Docker Desktop
# Minimum memory: 8 GB for the stack being audited; this script needs almost nothing
# Assumes: run on the HOST, not inside the container, with docker (or podman, through
#          `alias docker=podman`) and python3 on PATH, and the container running. It only
#          reads: `docker inspect` changes nothing.
#
# Usage:
#   ./audit-sandbox.sh agent-sandbox-agent-run-1
#   ENGINE=podman ./audit-sandbox.sh my-agent
#   LABBOOK=~/agent-lab/labbook.md ./audit-sandbox.sh my-agent

set -euo pipefail

CONTAINER="${1:-}"
ENGINE="${ENGINE:-docker}"
LABBOOK="${LABBOOK:-$PWD/labbook.md}"
PROJECT_DIR="${PROJECT_DIR:-}"

if [ -z "$CONTAINER" ]; then
    echo "usage: $0 <container-name-or-id>" >&2
    echo "  list running containers with: $ENGINE ps" >&2
    exit 2
fi

for required in "$ENGINE" python3; do
    if ! command -v "$required" >/dev/null 2>&1; then
        echo "$required is required and was not found on PATH." >&2
        exit 1
    fi
done

if ! "$ENGINE" inspect "$CONTAINER" >/dev/null 2>&1; then
    echo "No such container: $CONTAINER" >&2
    exit 1
fi

INSPECT_JSON="$("$ENGINE" inspect "$CONTAINER")"

# A container's own inspect output does not report whether the networks it is attached to
# are internal; that lives on the network objects. Collect them all and let the report
# match them by name. If this fails, the report says the flag was not available rather
# than assuming either answer.
NETWORKS_JSON="[]"
if network_names="$("$ENGINE" network ls --format '{{.Name}}' 2>/dev/null)"; then
    # shellcheck disable=SC2086
    NETWORKS_JSON="$("$ENGINE" network inspect $network_names 2>/dev/null || echo '[]')"
fi

CONTAINER="$CONTAINER" PROJECT_DIR="$PROJECT_DIR" LABBOOK="$LABBOOK" \
INSPECT_JSON="$INSPECT_JSON" NETWORKS_JSON="$NETWORKS_JSON" python3 - <<'PY'
"""Read one container's inspect output and name every fault it finds."""
import datetime
import json
import os
import sys

data = json.loads(os.environ["INSPECT_JSON"])
if not data:
    print("inspect returned nothing")
    raise SystemExit(1)

container = data[0]
name = container.get("Name", os.environ["CONTAINER"]).lstrip("/")
config = container.get("Config") or {}
host_config = container.get("HostConfig") or {}
network_settings = container.get("NetworkSettings") or {}
mounts = container.get("Mounts") or []
project_dir = os.environ.get("PROJECT_DIR") or ""

findings = []


def fault(code, detail):
    findings.append({"fault": code, "detail": detail})


print(f"Sandbox audit: {name}")
print("=" * (16 + len(name)))
print()

# ---------------------------------------------------------------- 1. what it can see
print("Mounts")
socket_names = ("docker.sock", "podman.sock", "containerd.sock")
sensitive_roots = ("/etc", "/var", "/usr", "/root", "/boot", "/proc", "/sys")

if not mounts:
    print("  (none)")
for mount in mounts:
    source = mount.get("Source", "?")
    destination = mount.get("Destination", "?")
    mode = "rw" if mount.get("RW", True) else "ro"
    print(f"  {mode}  {source} -> {destination}")

    if any(token in source for token in socket_names):
        fault(
            "container-socket-mounted",
            f"{source} is a container socket. A process that can reach it can start a "
            "container that mounts the host root, so this container is not a boundary.",
        )
        continue

    trimmed = source.rstrip("/")
    if source == "/":
        fault("host-root-mounted", "the host root filesystem is mounted into the container")
        continue

    looks_like_home = (
        trimmed.startswith("/home/") or trimmed.startswith("/Users/")
    ) and trimmed.count("/") == 2
    if looks_like_home:
        fault(
            "home-directory-mounted",
            f"{source} is a home directory. Read-only does not help: keys, tokens and "
            "every private repository under it are readable by the agent.",
        )
        continue

    system_mount = False
    for root in sensitive_roots:
        if trimmed == root or trimmed.startswith(root + "/"):
            fault(
                "system-directory-mounted",
                f"{source} is a system directory and has no business inside an agent sandbox",
            )
            system_mount = True
            break
    if system_mount:
        continue

    if mode == "rw" and project_dir and os.path.realpath(source) != os.path.realpath(project_dir):
        fault(
            "extra-writable-mount",
            f"{source} is writable and is not the project directory you named",
        )
print()

# ------------------------------------------------------------ 2. what it was handed
print("Environment")
env_pairs = [entry.split("=", 1) for entry in (config.get("Env") or []) if "=" in entry]
secretish = ("TOKEN", "SECRET", "PASSWORD", "PASSWD", "API_KEY", "APIKEY", "CREDENTIAL")
allowed = {"OPENAI_API_KEY", "AGENT_GATEWAY_KEY", "ANTHROPIC_AUTH_TOKEN"}
env_findings = 0
for key, value in env_pairs:
    upper = key.upper()
    if any(token in upper for token in secretish):
        shown = "set" if value else "empty"
        marker = "  " if key in allowed else "! "
        print(f"  {marker}{key} = <{shown}>")
        if key not in allowed:
            env_findings += 1
            fault(
                "secret-in-environment",
                f"{key} was passed into the container. An agent inherits its environment, "
                "and anything in it is available to every command the agent runs.",
            )
if env_findings == 0:
    print("  no unexpected credential-shaped variables")
print(f"  ({len(env_pairs)} variable(s) in total)")
print()

# ------------------------------------------------------------- 3. where it can reach
print("Network")
network_mode = host_config.get("NetworkMode", "default")
print(f"  mode: {network_mode}")
if network_mode == "host":
    fault(
        "host-network-mode",
        "the container shares the host network namespace, so every service bound to "
        "the host loopback address is reachable from inside it",
    )
try:
    all_networks = json.loads(os.environ.get("NETWORKS_JSON") or "[]")
except json.JSONDecodeError:
    all_networks = []
internal_by_name = {
    net.get("Name"): net.get("Internal")
    for net in all_networks
    if isinstance(net, dict)
}

networks = network_settings.get("Networks") or {}
if not networks and network_mode not in ("none", "host"):
    print("  (no attached networks reported)")
for net_name, net in networks.items():
    internal = net.get("Internal")
    if internal is None:
        internal = internal_by_name.get(net_name)
    if internal is None:
        label = "internal flag not available; check it with `network inspect` by hand"
    else:
        label = "internal" if internal else "externally connected"
    print(f"  {net_name}: {label}")
    if internal is False:
        fault(
            "network-policy-not-applied",
            f"network {net_name} is not internal, so the agent has a route off this machine",
        )
if network_mode == "none":
    print("  no network at all; an agent here cannot reach a model either")
print()

# --------------------------------------------------------------- 4. what it may do
print("Privileges")
user = config.get("User") or "(image default, probably root)"
print(f"  user: {user}")
if not config.get("User") or config.get("User").split(":")[0] in ("0", "root"):
    fault("runs-as-root", "the container process runs as root inside the container")

if host_config.get("Privileged"):
    fault("privileged-container", "the container is privileged, which removes almost every control")
print(f"  privileged: {bool(host_config.get('Privileged'))}")

readonly_rootfs = bool(host_config.get("ReadonlyRootfs"))
print(f"  read-only root filesystem: {readonly_rootfs}")
if not readonly_rootfs:
    fault(
        "writable-root-filesystem",
        "the root filesystem is writable, so the agent can install tools and leave "
        "things behind outside the project",
    )

cap_add = host_config.get("CapAdd") or []
if cap_add:
    print(f"  added capabilities: {', '.join(cap_add)}")
    fault("capabilities-added", f"capabilities added: {', '.join(cap_add)}")

security_opt = host_config.get("SecurityOpt") or []
print(f"  security options: {', '.join(security_opt) if security_opt else '(none)'}")
if not any("no-new-privileges" in str(option) for option in security_opt):
    fault(
        "new-privileges-allowed",
        "no-new-privileges is not set, so setuid binaries inside the image can still "
        "raise privileges",
    )

memory = host_config.get("Memory") or 0
pids = host_config.get("PidsLimit") or 0
print(f"  memory limit: {'unlimited' if not memory else str(memory) + ' bytes'}")
print(f"  pids limit: {'unlimited' if not pids else pids}")
if not memory:
    fault(
        "no-resource-limits",
        "no memory limit: a runaway agent can make the whole machine unusable. This is "
        "not a security boundary, only a usability one.",
    )
print()

# ------------------------------------------------------------------------- verdict
print("Findings")
if not findings:
    print("  none. Every check this script performs is satisfied.")
else:
    seen = set()
    for item in findings:
        key = (item["fault"], item["detail"])
        if key in seen:
            continue
        seen.add(key)
        print(f"  [{item['fault']}] {item['detail']}")
print()
print(f"{len(findings)} finding(s).")

record = {
    "lab": "part-25-agent-that-escaped",
    "recorded": datetime.datetime.now(datetime.timezone.utc)
    .replace(microsecond=0)
    .isoformat(),
    "container": name,
    "findings": [item["fault"] for item in findings],
    "clean": not findings,
}
try:
    with open(os.environ["LABBOOK"], "a", encoding="utf-8") as handle:
        handle.write(json.dumps(record) + "\n")
    print("recorded in " + os.environ["LABBOOK"])
except OSError as error:
    print("could not write the lab notebook: " + str(error))

sys.exit(1 if findings else 0)
PY
