#!/usr/bin/env bash
# Attempts every escalation and host mutation the sandbox must not permit, from
# inside a live sandbox, and fails unless each one is refused. Resource-exhaustion
# proofs are NOT here — those belong in the danger lab.
set -uo pipefail

HOST="${1:-debian1}"
SSH=(ssh -F "$HOME/.ssh/config" -o BatchMode=yes "$HOST")
ID="denialproof"
FAILURES=0

run_in_sandbox() {
  local script=$1 extra_flags=${2-}
  "${SSH[@]}" ".local/share/overdeck-sandbox/bin/sandbox-run --id $ID${extra_flags:+ }$extra_flags -- /bin/bash -c $(printf '%q' "$script")" 2>&1
}

expect_mount_set() {
  local name=$1 flags=$2 expected=$3
  local actual expected_sorted actual_sorted diff_output cmd rc
  cmd="$(cat <<'SCRIPT'
awk '
{
  mp=$5
  perm=($6 == "ro" || $6 ~ /^ro,/) ? "ro" : "rw"
  if (mp == "/" || mp == "/etc/hosts" || mp == "/etc/hostname" || mp == "/etc/resolv.conf" || mp == "/run/.containerenv" || mp == "/proc" || mp ~ /^\/proc\// || mp == "/sys" || mp ~ /^\/sys\// || mp == "/dev" || mp ~ /^\/dev\//) next
  print mp " " perm
}' /proc/self/mountinfo | sort
SCRIPT
)"
  actual="$(run_in_sandbox "$cmd" "$flags")"
  rc=$?
  if [ "$rc" -ne 0 ]; then
    printf 'PROVE-FAIL   %-28s sandbox-run rc=%s\n%s\n' "$name" "$rc" "$actual"
    FAILURES=$((FAILURES + 1))
    return
  fi
  expected_sorted="$(printf '%s\n' "$expected" | sort)"
  actual_sorted="$(printf '%s\n' "$actual" | sort)"
  if diff_output="$(diff -u <(printf '%s\n' "$expected_sorted") <(printf '%s\n' "$actual_sorted") )"; then
    printf 'PROVEN       %-28s %s\n' "$name" "mount set matched"
  else
    printf 'PROVE-FAIL   %-28s mount set mismatch:\n%s\n' "$name" "$diff_output"
    FAILURES=$((FAILURES + 1))
  fi
}

# A container that never started exits non-zero for EVERY escalation attempt, so a
# broken launcher would otherwise print a full page of REFUSED and read as a security
# pass — the worst direction for a gate to fail. These are the launcher's own refusals
# (sandbox-run preflight, or the egress entrypoint aborting), never the sandbox denying
# the thing under test.
launch_failed() {
  printf '%s' "$1" | grep -qE '^(sandbox-run|sandbox-egress-init):'
}

expect_refused() {
  local name=$1 script=$2
  local out rc
  out="$(run_in_sandbox "$script")"
  rc=$?
  if [ "$rc" -eq 0 ]; then
    printf 'DENIED-FAIL  %-28s exited 0 — NOT refused\n%s\n' "$name" "$out"
    FAILURES=$((FAILURES + 1))
    return
  fi
  if launch_failed "$out"; then
    printf 'SETUP-FAIL   %-28s the sandbox never launched, so this proves nothing: %s\n' \
      "$name" "$(printf '%s' "$out" | tail -1)"
    FAILURES=$((FAILURES + 1))
    return
  fi
  printf 'REFUSED      %-28s rc=%s  %s\n' "$name" "$rc" "$(printf '%s' "$out" | tail -1)"
}

expect_output() {
  local name=$1 script=$2 want=$3
  local out
  out="$(run_in_sandbox "$script")"
  if printf '%s' "$out" | grep -qF "$want"; then
    printf 'PROVEN       %-28s %s\n' "$name" "$(printf '%s' "$out" | tail -1)"
  else
    printf 'PROVE-FAIL   %-28s wanted %q, got:\n%s\n' "$name" "$want" "$out"
    FAILURES=$((FAILURES + 1))
  fi
}

echo "=== agent sandbox denial proofs on $HOST ==="

# Preflight: one trivial command must run inside the sandbox. Without this a launcher
# that refuses every container still prints a page of REFUSED lines and a small
# "N denial proof(s) FAILED" summary, which reads as a mostly-passing security report.
preflight="$(run_in_sandbox 'echo sandbox-alive')" || true
if ! printf '%s' "$preflight" | grep -q 'sandbox-alive'; then
  printf 'SETUP-FAIL   the sandbox on %s does not launch — no denial proof below would mean anything\n%s\n' \
    "$HOST" "$preflight"
  echo
  echo "denial proofs ABORTED: fix the launcher first"
  exit 1
fi

expect_output "no capabilities"        'grep ^CapEff /proc/self/status'            'CapEff:	0000000000000000'
expect_output "no_new_privs set"       'grep ^NoNewPrivs /proc/self/status'        'NoNewPrivs:	1'
expect_output "host /etc not mounted"  'cat /etc/hostname'                         'sandbox-'
expect_output "swap disabled"          'cat /sys/fs/cgroup/memory.swap.max'        '0'

expect_mount_set "host mounts are the base set" "" \
$'/run/overdeck-egress/rules.nft ro\n/sandbox rw\n/sandbox-secrets/e2e_key ro\n/sandbox-secrets/e2e_known_hosts ro'

"${SSH[@]}" "mkdir -p ~/sandbox/denialproof-cred" \
  || { echo "SETUP-FAIL   could not create the credential mount source on $HOST"; FAILURES=$((FAILURES + 1)); }

expect_mount_set "credential mount adds one ro" \
  '--mount "$HOME/sandbox/denialproof-cred:/sandbox-secrets/cred:ro"' \
  $'/run/overdeck-egress/rules.nft ro\n/sandbox rw\n/sandbox-secrets/e2e_key ro\n/sandbox-secrets/e2e_known_hosts ro\n/sandbox-secrets/cred ro'

expect_output "syscalls need the caps the sandbox lacks" \
  'python3 -c "
import ctypes, errno, socket
libc = ctypes.CDLL(None, use_errno=True)
def perm(fn):
    try:
        rc = fn()
    except OSError as err:
        return err.errno == errno.EPERM
    if rc == 0: return False
    return ctypes.get_errno() == errno.EPERM
mount = perm(lambda: libc.mount(b\"/\", b\"/mnt\", b\"none\", 4096, None))
hostname = perm(lambda: libc.sethostname(b\"pwned\", 5))
def rawsock():
    socket.socket(socket.AF_PACKET, socket.SOCK_RAW).close()
    return 0
raw = perm(rawsock)
print(f\"mount=EPERM:{mount} sethostname=EPERM:{hostname} rawsocket=EPERM:{raw}\")
"' 'mount=EPERM:True sethostname=EPERM:True rawsocket=EPERM:True'

expect_refused "sudo"                  'sudo -n true'
expect_refused "deck-sudo"             'deck-sudo true'
expect_refused "pkexec"                'pkexec true'
expect_refused "su to root"            'su -c true root'
expect_refused "write host sshd_config" 'echo "ListenAddress 1.2.3.4" >> /etc/ssh/sshd_config.d/10-tailscale.conf'
expect_refused "write /etc/fstab"      'echo "x" >> /etc/fstab'
expect_refused "write host unit"       'echo "[Unit]" > /etc/systemd/system/evil.service'
expect_refused "systemctl host unit"   'systemctl restart ssh'
expect_refused "systemctl enable"      'systemctl enable evil.service'
expect_refused "iptables"              'iptables -L'
expect_refused "nft"                   'nft list ruleset'
expect_refused "ip link down"          'ip link set lo down'
expect_refused "ip addr add"           'ip addr add 10.9.9.9/32 dev lo'
expect_refused "mount"                 'mount -t tmpfs none /mnt'
expect_refused "mount host root"       'mkdir -p /mnt/h && mount --bind / /mnt/h'
expect_refused "modprobe"              'modprobe dummy'
expect_refused "tailscale"             'tailscale down'
expect_refused "podman socket"         'podman ps'
expect_refused "reach host ssh keys"   'cat /sandbox-secrets/../../home/user/.ssh/id_ed25519'
expect_refused "reboot"                'reboot'
expect_refused "sysctl write"          'sysctl -w kernel.panic=1'

echo
if [ "$FAILURES" -eq 0 ]; then
  echo "all denial proofs held"
else
  echo "$FAILURES denial proof(s) FAILED"
fi
exit "$FAILURES"
