2026-04-21 17:48:45 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Test harness for docker/scripts/bash-guard-hook.sh.
|
|
|
|
|
#
|
|
|
|
|
# Feeds each command to the hook (via the same JSON stdin contract Claude
|
|
|
|
|
# Code uses) and asserts the expected exit code.
|
|
|
|
|
#
|
|
|
|
|
# Run:
|
|
|
|
|
# bash docker/scripts/tests/bash-guard-tests.sh
|
|
|
|
|
#
|
|
|
|
|
# Exit 0 on full pass, 1 on any failure.
|
|
|
|
|
|
|
|
|
|
set -u
|
|
|
|
|
|
|
|
|
|
HOOK="$(cd "$(dirname "$0")/.." && pwd)/bash-guard-hook.sh"
|
|
|
|
|
if [[ ! -x "$HOOK" ]]; then
|
|
|
|
|
# chmod may not have been applied in the dev checkout — run via bash.
|
|
|
|
|
HOOK="bash $HOOK"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
PASS=0
|
|
|
|
|
FAIL=0
|
|
|
|
|
FAILS=()
|
|
|
|
|
|
|
|
|
|
# run_case <label> <expected_exit> <command>
|
|
|
|
|
run_case() {
|
|
|
|
|
local label="$1"
|
|
|
|
|
local expected="$2"
|
|
|
|
|
local cmd="$3"
|
|
|
|
|
local json
|
|
|
|
|
# shellcheck disable=SC2016
|
|
|
|
|
json=$(python3 -c 'import json, sys; print(json.dumps({"tool_name":"Bash","tool_input":{"command":sys.argv[1]}}))' "$cmd")
|
|
|
|
|
local actual
|
|
|
|
|
echo "$json" | $HOOK >/dev/null 2>&1
|
|
|
|
|
actual=$?
|
|
|
|
|
if [[ "$actual" == "$expected" ]]; then
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
FAILS+=("[$label] expected $expected, got $actual | cmd: $cmd")
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# ---------- DENY cases (exit 2) ----------
|
|
|
|
|
# Git network/auth ops already covered by original hook.
|
|
|
|
|
run_case "deny git fetch" 2 "git fetch origin"
|
|
|
|
|
run_case "deny compound git push" 2 "cd /workspace && git push origin main"
|
|
|
|
|
run_case "deny git clone" 2 "git clone https://github.com/foo/bar"
|
|
|
|
|
|
|
|
|
|
# Credential file references.
|
|
|
|
|
run_case "deny cat .git/config" 2 "cat .git/config"
|
|
|
|
|
run_case "deny cat netrc" 2 "cat ~/.netrc"
|
|
|
|
|
run_case "deny ls .ssh" 2 "ls ~/.ssh/"
|
|
|
|
|
run_case "deny grep token gitconf" 2 "grep token .git/config"
|
|
|
|
|
|
|
|
|
|
# /proc env/cmdline exfil.
|
|
|
|
|
run_case "deny /proc/self/environ" 2 "cat /proc/self/environ"
|
|
|
|
|
run_case "deny /proc/1/environ" 2 "cat /proc/1/environ"
|
|
|
|
|
run_case "deny redirect /proc env" 2 'read -r v < /proc/self/environ && echo "$v"'
|
|
|
|
|
|
|
|
|
|
# env / printenv / set / declare / compgen / export dumps.
|
|
|
|
|
run_case "deny bare env" 2 "env"
|
|
|
|
|
run_case "deny bare printenv" 2 "printenv"
|
|
|
|
|
run_case "deny bare set" 2 "set"
|
|
|
|
|
run_case "deny set piped" 2 "set | grep TOKEN"
|
|
|
|
|
run_case "deny declare -x" 2 "declare -x"
|
|
|
|
|
run_case "deny export -p" 2 "export -p"
|
|
|
|
|
run_case "deny compgen -v" 2 "compgen -v"
|
|
|
|
|
run_case "deny compgen -e" 2 "compgen -e"
|
|
|
|
|
run_case "deny typeset -p" 2 "typeset -p"
|
|
|
|
|
|
|
|
|
|
# Sourcing credential-bearing files.
|
|
|
|
|
run_case "deny source .env" 2 "source .env"
|
|
|
|
|
run_case "deny dot-source /etc/env" 2 ". /etc/environment"
|
|
|
|
|
run_case "deny source /proc env" 2 "source /proc/self/environ"
|
|
|
|
|
|
|
|
|
|
# Encoding tools on credential files.
|
|
|
|
|
run_case "deny base64 .env" 2 "base64 .env"
|
|
|
|
|
run_case "deny xxd netrc" 2 "xxd ~/.netrc"
|
|
|
|
|
run_case "deny strings .git/config" 2 "strings .git/config"
|
|
|
|
|
run_case "deny od -c gitconfig" 2 "od -c .git/config"
|
|
|
|
|
|
|
|
|
|
# Interpreter one-liners against cred paths.
|
|
|
|
|
run_case "deny python open .env" 2 "python3 -c 'print(open(\".env\").read())'"
|
|
|
|
|
run_case "deny perl read netrc" 2 "perl -e 'open(F,\".netrc\"); print <F>'"
|
|
|
|
|
run_case "deny node fs netrc" 2 "node -e 'console.log(require(\"fs\").readFileSync(\".netrc\",\"utf8\"))'"
|
|
|
|
|
|
|
|
|
|
# GitHub HTTP.
|
|
|
|
|
run_case "deny curl github" 2 "curl https://github.com/foo"
|
|
|
|
|
run_case "deny wget api.github" 2 "wget https://api.github.com/repos/foo"
|
|
|
|
|
|
|
|
|
|
# rm on system paths.
|
|
|
|
|
run_case "deny rm -rf /app" 2 "rm -rf /app/roboco"
|
|
|
|
|
run_case "deny rm -rf /etc" 2 "rm -rf /etc"
|
|
|
|
|
|
2026-06-29 05:38:21 +02:00
|
|
|
# ---------- /app venv protection (exit 2) ----------
|
|
|
|
|
# /app/.venv is the image-baked MCP-gateway venv. Retargeting uv onto it
|
|
|
|
|
# rebuilds + bricks every gateway tool (be-dev-1 root cause, 2026-06-29).
|
|
|
|
|
# The package-mutation block (uv sync/pip install + /app target) and the
|
|
|
|
|
# uv-run block (uv run --active, or uv run with an explicit /app target).
|
|
|
|
|
run_case "deny uv sync --project /app" 2 "uv sync --project /app"
|
|
|
|
|
run_case "deny uv pip install /app venv" 2 "uv pip install --python /app/.venv/bin/python foo"
|
|
|
|
|
run_case "deny cd /app && uv sync" 2 "cd /app && uv sync"
|
2026-06-30 09:58:36 +02:00
|
|
|
# uv run --active: not the contract (bare `uv run` uses the workspace .venv).
|
|
|
|
|
# VIRTUAL_ENV is no longer image-baked, so --active has no active env and
|
|
|
|
|
# errors; the guard still denies it with a clear remediation message.
|
2026-06-29 05:38:21 +02:00
|
|
|
run_case "deny uv run --active" 2 "uv run --active pytest"
|
|
|
|
|
run_case "deny uv run --active ruff" 2 "uv run --active ruff check ."
|
|
|
|
|
run_case "deny env venv /app uv run active" 2 "VIRTUAL_ENV=/app/.venv uv run --active pytest"
|
|
|
|
|
# uv run with an explicit /app target (same brick, literal /app in the command).
|
|
|
|
|
run_case "deny uv run --project /app" 2 "uv run --project /app pytest"
|
|
|
|
|
run_case "deny uv run --directory /app" 2 "uv run --directory /app pytest"
|
|
|
|
|
run_case "deny cd /app && uv run" 2 "cd /app && uv run pytest"
|
|
|
|
|
run_case "deny UV_PROJECT_ENV=/app uv run" 2 "UV_PROJECT_ENVIRONMENT=/app/.venv uv run pytest"
|
|
|
|
|
|
2026-04-21 17:48:45 +02:00
|
|
|
# ---------- ALLOW cases (exit 0) — must NOT be denied ----------
|
|
|
|
|
run_case "allow set -e" 0 "set -e"
|
|
|
|
|
run_case "allow set -euo pipefail" 0 "set -euo pipefail"
|
|
|
|
|
run_case "allow set -o pipefail" 0 "set -o pipefail"
|
|
|
|
|
run_case "allow env VAR=val cmd" 0 "env FOO=bar uv run pytest"
|
|
|
|
|
run_case "allow env -i cmd" 0 "env -i HOME=/tmp ls /tmp"
|
|
|
|
|
run_case "allow ls" 0 "ls -la /workspace"
|
|
|
|
|
run_case "allow uv run ruff" 0 "uv run ruff check ."
|
2026-06-29 05:38:21 +02:00
|
|
|
run_case "allow uv run pytest" 0 "uv run pytest -q"
|
|
|
|
|
run_case "allow uv run --no-sync" 0 "uv run --no-sync pytest -q"
|
|
|
|
|
run_case "allow uv run --with dep" 0 "uv run --with httpx python -c 'pass'"
|
|
|
|
|
run_case "allow uv run in workspace" 0 "cd /data/workspaces/proj && uv run pytest"
|
2026-04-21 17:48:45 +02:00
|
|
|
run_case "allow pnpm typecheck" 0 "pnpm typecheck"
|
|
|
|
|
run_case "allow rm in workspace" 0 "rm -rf /workspace/tmp"
|
|
|
|
|
run_case "allow declare -a arr" 0 "declare -a arr=(a b c)"
|
|
|
|
|
run_case "allow cat README" 0 "cat README.md"
|
|
|
|
|
run_case "allow curl non-github" 0 "curl https://example.com/info"
|
|
|
|
|
|
2026-06-19 09:15:01 +02:00
|
|
|
# ---------- grok variant: camelCase input + ROBOCO_GUARD_SKIP_GIT ----------
|
|
|
|
|
# The grok CLI sends `toolInput` (camelCase) and the grok hook runs with
|
|
|
|
|
# ROBOCO_GUARD_SKIP_GIT=1 (git is handled by graceful native --deny). Exfil
|
|
|
|
|
# categories must STILL deny; git must now pass through to --deny.
|
|
|
|
|
run_case_grok() {
|
|
|
|
|
local label="$1" expected="$2" cmd="$3" json actual
|
|
|
|
|
# shellcheck disable=SC2016
|
|
|
|
|
json=$(python3 -c 'import json, sys; print(json.dumps({"toolName":"run_terminal_command","toolInput":{"command":sys.argv[1]}}))' "$cmd")
|
|
|
|
|
echo "$json" | ROBOCO_GUARD_SKIP_GIT=1 $HOOK >/dev/null 2>&1
|
|
|
|
|
actual=$?
|
|
|
|
|
if [[ "$actual" == "$expected" ]]; then
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
FAILS+=("[$label] expected $expected, got $actual | cmd: $cmd")
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
run_case_grok "grok skip-git: git push allowed" 0 "git push origin main"
|
|
|
|
|
run_case_grok "grok camelCase: netrc denied" 2 "cat ~/.netrc"
|
|
|
|
|
run_case_grok "grok camelCase: env denied" 2 "env"
|
|
|
|
|
run_case_grok "grok camelCase: identity forgery" 2 "export ROBOCO_AGENT_ID=other"
|
|
|
|
|
run_case_grok "grok camelCase: allow ls" 0 "ls -la /workspace"
|
|
|
|
|
|
2026-04-21 17:48:45 +02:00
|
|
|
# ---------- Report ----------
|
|
|
|
|
echo
|
|
|
|
|
echo "===== bash-guard-hook tests ====="
|
|
|
|
|
echo " passed: $PASS"
|
|
|
|
|
echo " failed: $FAIL"
|
|
|
|
|
if (( FAIL > 0 )); then
|
|
|
|
|
echo " failures:"
|
|
|
|
|
for f in "${FAILS[@]}"; do
|
|
|
|
|
echo " - $f"
|
|
|
|
|
done
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
exit 0
|