Skip to content

Instantly share code, notes, and snippets.

@steig
Last active May 20, 2026 14:47
Show Gist options
  • Select an option

  • Save steig/ca5d31670d1ae3c9e444875272baa3f6 to your computer and use it in GitHub Desktop.

Select an option

Save steig/ca5d31670d1ae3c9e444875272baa3f6 to your computer and use it in GitHub Desktop.
Audit-only supply-chain hardening check for dev machines (npm/pnpm/pip/uv/SSH/VS Code/brew)
#!/usr/bin/env bash
# dev-supply-chain-audit.sh — developer machine supply-chain hardening audit
#
# Audit-only. Reports findings against current attack vectors and prints the
# command needed to remediate each one. Makes no changes.
#
# Usage:
# curl -fsSL <raw-url> | bash
# bash dev-supply-chain-audit.sh # full audit, human output
# bash dev-supply-chain-audit.sh --json # one JSON object per finding
# bash dev-supply-chain-audit.sh --quiet # only show fails
# bash dev-supply-chain-audit.sh --only NPM-001 # run a single rule
# bash dev-supply-chain-audit.sh --skip GH-001 # skip rule(s) (comma-sep)
# bash dev-supply-chain-audit.sh --list # list all rule IDs
# bash dev-supply-chain-audit.sh --version
#
# Exit codes: 0 = clean, 1 = warnings only, 2 = any high/critical failures.
#
# Verify before you `curl | bash`:
# curl -fsSL <raw-url> -o /tmp/audit.sh
# shasum -a 256 /tmp/audit.sh # compare to the SHA256 published with the gist
# bash /tmp/audit.sh
#
# License: do whatever. No warranty.
set -u
VERSION="0.2.0"
# ---------- argv ------------------------------------------------------------
JSON_MODE=0
QUIET=0
ONLY=""
SKIP=""
LIST=0
while [[ $# -gt 0 ]]; do
case "$1" in
--json) JSON_MODE=1; shift ;;
--quiet|-q) QUIET=1; shift ;;
--only) ONLY="$2"; shift 2 ;;
--skip) SKIP="$2"; shift 2 ;;
--list) LIST=1; shift ;;
--version) echo "dev-supply-chain-audit.sh $VERSION"; exit 0 ;;
--help|-h)
sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'
exit 0 ;;
*) echo "unknown arg: $1" >&2; exit 64 ;;
esac
done
# ---------- output helpers --------------------------------------------------
if [[ -t 1 && $JSON_MODE -eq 0 ]]; then
C_RED=$'\033[31m'; C_YEL=$'\033[33m'; C_GRN=$'\033[32m'; C_MAG=$'\033[35m'
C_CYA=$'\033[36m'; C_BLD=$'\033[1m'; C_DIM=$'\033[2m'; C_RST=$'\033[0m'
else
C_RED=; C_YEL=; C_GRN=; C_MAG=; C_CYA=; C_BLD=; C_DIM=; C_RST=
fi
CRIT_COUNT=0
HIGH_COUNT=0
MED_COUNT=0
LOW_COUNT=0
PASS_COUNT=0
INFO_COUNT=0
# ---------- rule registry --------------------------------------------------
# Each rule has an ID like CAT-NNN. Map ID -> default-severity / title.
declare -A RULE_TITLE=()
declare -A RULE_SEV=() # default severity if it fails
declare -A RULE_GROUP=()
register_rule() {
local id="$1" sev="$2" group="$3" title="$4"
RULE_TITLE[$id]="$title"
RULE_SEV[$id]="$sev"
RULE_GROUP[$id]="$group"
}
rule_enabled() {
local id="$1"
if [[ -n "$ONLY" ]]; then
[[ ",$ONLY," == *",$id,"* ]] && return 0 || return 1
fi
if [[ -n "$SKIP" ]]; then
[[ ",$SKIP," == *",$id,"* ]] && return 1
fi
return 0
}
# Emit a finding. Args: id status[pass|fail|warn|info] severity message [fix]
emit() {
local id="$1" status="$2" sev="$3" msg="$4" fix="${5:-}"
case "$status" in
pass) PASS_COUNT=$((PASS_COUNT+1)) ;;
info) INFO_COUNT=$((INFO_COUNT+1)) ;;
warn) MED_COUNT=$((MED_COUNT+1)) ;;
fail)
case "$sev" in
critical) CRIT_COUNT=$((CRIT_COUNT+1)) ;;
high) HIGH_COUNT=$((HIGH_COUNT+1)) ;;
medium) MED_COUNT=$((MED_COUNT+1)) ;;
low) LOW_COUNT=$((LOW_COUNT+1)) ;;
esac
;;
esac
if [[ $JSON_MODE -eq 1 ]]; then
# JSON-escape strings using printf+sed (no jq dep)
j_esc() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g; s/\n/\\n/g'; }
printf '{"rule":"%s","status":"%s","severity":"%s","group":"%s","message":"%s","fix":"%s"}\n' \
"$id" "$status" "$sev" "${RULE_GROUP[$id]:-}" \
"$(j_esc "$msg")" "$(j_esc "$fix")"
return
fi
if [[ $QUIET -eq 1 && "$status" != fail ]]; then return; fi
local glyph color
case "$status" in
pass) glyph='✓'; color=$C_GRN ;;
info) glyph='·'; color=$C_DIM ;;
warn) glyph='!'; color=$C_YEL ;;
fail)
case "$sev" in
critical) glyph='✗'; color=$C_MAG ;;
high) glyph='✗'; color=$C_RED ;;
medium) glyph='!'; color=$C_YEL ;;
low) glyph='·'; color=$C_DIM ;;
esac
;;
esac
printf ' %s%s%s [%s] %s\n' "$color" "$glyph" "$C_RST" "$id" "$msg"
[[ -n "$fix" ]] && printf ' %sfix:%s %s\n' "$C_DIM" "$C_RST" "$fix"
}
section() {
[[ $JSON_MODE -eq 1 ]] && return
[[ $QUIET -eq 1 ]] && return
printf '\n%s━━━ %s ━━━%s\n' "$C_CYA$C_BLD" "$1" "$C_RST"
}
# ---------- platform / portable stat ---------------------------------------
OS="$(uname -s)"
case "$OS" in
Darwin) PLATFORM=macos ;;
Linux) PLATFORM=linux ;;
*) PLATFORM=unknown ;;
esac
if stat --version >/dev/null 2>&1; then
stat_mode() { stat -c '%a' "$1" 2>/dev/null; }
else
stat_mode() { stat -f '%Lp' "$1" 2>/dev/null; }
fi
have() { command -v "$1" >/dev/null 2>&1; }
# ============================================================================
# Rule registrations (so --list works without running checks)
# ============================================================================
register_rule NPM-001 high "npm/pnpm" "ignore-scripts blocks postinstall execution"
register_rule NPM-002 high "npm/pnpm" "npm minimum-release-age quarantine (npm 11.6+)"
register_rule NPM-003 high "npm/pnpm" "pnpm minimum-release-age quarantine"
register_rule NPM-004 medium "npm/pnpm" "no plaintext _authToken in ~/.npmrc"
register_rule NPM-005 high "npm/pnpm" "npm audit clean (current directory)"
register_rule NPM-006 high "npm/pnpm" "pnpm audit clean (current directory)"
register_rule PIP-001 medium "python" "pip require-virtualenv"
register_rule PIP-002 low "python" "uv config present"
register_rule PIP-003 high "python" "pip-audit clean (current directory)"
register_rule FS-001 critical "secrets" "no world-readable secrets in ~/.config / ~/.aws / etc"
register_rule FS-002 high "secrets" ".env files in cwd are gitignored"
register_rule FS-003 high "secrets" "SSH private key perms are 600/400"
register_rule SSH-001 medium "ssh" "no weak (<3072-bit) RSA keys"
register_rule SSH-002 medium "ssh" "no global ForwardAgent=yes"
register_rule VSC-001 high "editor" "VS Code / Cursor extension auto-update disabled"
register_rule VSC-002 medium "editor" "VS Code / Cursor extensions from recognized publishers"
register_rule BREW-001 medium "homebrew" "no greedy casks"
register_rule BREW-002 low "homebrew" "no taps from unknown orgs"
register_rule BROWSER-001 medium "browser" "browser extensions inventoried"
register_rule MCP-001 high "claude" "MCP servers in ~/.claude.json reviewed"
register_rule GH-001 medium "github" "gh CLI token scopes are minimal"
register_rule OS-001 high "os" "FileVault enabled (macOS)"
register_rule OS-002 high "os" "Gatekeeper enabled (macOS)"
register_rule OS-003 medium "os" "SIP enabled (macOS)"
register_rule OS-004 medium "os" "Firewall enabled"
register_rule CI-001 high "ci" ".github/workflows actions pinned to SHA"
register_rule CI-002 critical "ci" "no pull_request_target + checkout(ref:head.sha)"
register_rule HIST-001 medium "shell" "no obvious secrets in shell history"
register_rule GIT-001 low "git" "pre-commit / gitleaks installed in repo"
register_rule DOCKER-001 medium "docker" "no insecure-registries in docker daemon config"
# ---------- --list shortcut -------------------------------------------------
if [[ $LIST -eq 1 ]]; then
printf '%-15s %-10s %-10s %s\n' "ID" "SEVERITY" "GROUP" "TITLE"
for id in "${!RULE_TITLE[@]}"; do
printf '%-15s %-10s %-10s %s\n' "$id" "${RULE_SEV[$id]}" "${RULE_GROUP[$id]}" "${RULE_TITLE[$id]}"
done | sort
exit 0
fi
# ---------- banner ----------------------------------------------------------
if [[ $JSON_MODE -eq 0 && $QUIET -eq 0 ]]; then
printf '%s%sDeveloper Supply-Chain Audit v%s%s\n' "$C_BLD" "$C_CYA" "$VERSION" "$C_RST"
printf 'platform=%s date=%s host=%s pwd=%s\n' \
"$PLATFORM" "$(date +%Y-%m-%d)" "$(hostname -s 2>/dev/null || hostname)" "$(pwd)"
fi
# ============================================================================
# NPM / PNPM
# ============================================================================
section "npm / pnpm"
NPMRC="$HOME/.npmrc"
PNPM_RC="$HOME/.config/pnpm/rc"
NPMRC_CONTENT=""; PNPM_RC_CONTENT=""
[[ -f "$NPMRC" ]] && NPMRC_CONTENT="$(cat "$NPMRC")"
[[ -f "$PNPM_RC" ]] && PNPM_RC_CONTENT="$(cat "$PNPM_RC")"
if rule_enabled NPM-001; then
if echo "$NPMRC_CONTENT" | grep -qE '^\s*ignore-scripts\s*=\s*true'; then
emit NPM-001 pass high "ignore-scripts=true in ~/.npmrc"
else
emit NPM-001 fail high "ignore-scripts not enabled — postinstall hooks will run" \
"echo 'ignore-scripts=true' >> ~/.npmrc"
fi
fi
if rule_enabled NPM-002; then
age="$(echo "$NPMRC_CONTENT" | grep -oE 'minimum-release-age\s*=\s*[0-9]+' | grep -oE '[0-9]+' | head -1)"
if [[ -n "${age:-}" && "$age" -ge 1440 ]]; then
emit NPM-002 pass high "npm minimum-release-age=${age} min ($(( age / 60 ))h)"
else
emit NPM-002 fail high "npm has no release-age quarantine (need 1440+ min)" \
"echo 'minimum-release-age=4320' >> ~/.npmrc # 3 days, requires npm 11.6+"
fi
fi
if rule_enabled NPM-003; then
age="$(echo "$PNPM_RC_CONTENT" | grep -oE 'minimum-release-age\s*=\s*[0-9]+' | grep -oE '[0-9]+' | head -1)"
if have pnpm; then
if [[ -n "${age:-}" && "$age" -ge 1440 ]]; then
emit NPM-003 pass high "pnpm minimum-release-age=${age} min ($(( age / 60 ))h)"
else
emit NPM-003 fail high "pnpm has no release-age quarantine" \
"mkdir -p ~/.config/pnpm && echo 'minimum-release-age=4320' >> ~/.config/pnpm/rc"
fi
else
emit NPM-003 info low "pnpm not installed — skipping"
fi
fi
if rule_enabled NPM-004; then
if echo "$NPMRC_CONTENT" | grep -qE '_authToken\s*=\s*[A-Za-z0-9_-]{20,}\s*$'; then
emit NPM-004 fail medium "~/.npmrc contains a plaintext npm token" \
"Move token out of file; reference as \${NPM_TOKEN} and export from a 600-perm secret file"
else
emit NPM-004 pass medium "no plaintext npm token in ~/.npmrc"
fi
fi
# CVE checks via npm/pnpm audit on the current directory
if rule_enabled NPM-005; then
if [[ -f "$PWD/package.json" && -f "$PWD/package-lock.json" ]] && have npm; then
if out="$(npm audit --json 2>/dev/null)"; then
if have jq; then
crit=$(echo "$out" | jq -r '.metadata.vulnerabilities.critical // 0')
high=$(echo "$out" | jq -r '.metadata.vulnerabilities.high // 0')
mod=$(echo "$out" | jq -r '.metadata.vulnerabilities.moderate // 0')
low=$(echo "$out" | jq -r '.metadata.vulnerabilities.low // 0')
else
crit=$(echo "$out" | grep -oE '"critical":[0-9]+' | head -1 | grep -oE '[0-9]+')
high=$(echo "$out" | grep -oE '"high":[0-9]+' | head -1 | grep -oE '[0-9]+')
mod=$(echo "$out" | grep -oE '"moderate":[0-9]+' | head -1 | grep -oE '[0-9]+')
low=$(echo "$out" | grep -oE '"low":[0-9]+' | head -1 | grep -oE '[0-9]+')
fi
total=$(( ${crit:-0} + ${high:-0} + ${mod:-0} + ${low:-0} ))
if [[ $total -eq 0 ]]; then
emit NPM-005 pass high "npm audit: 0 vulnerabilities in $(basename "$PWD")"
else
sev=medium
[[ ${high:-0} -gt 0 ]] && sev=high
[[ ${crit:-0} -gt 0 ]] && sev=critical
emit NPM-005 fail "$sev" \
"npm audit: ${crit:-0} critical / ${high:-0} high / ${mod:-0} mod / ${low:-0} low" \
"npm audit fix # auto-fix safe ones; review breaking changes manually"
fi
else
emit NPM-005 info low "npm audit failed to run (offline or registry blocked)"
fi
else
emit NPM-005 info low "no package-lock.json in cwd — skipping npm CVE scan"
fi
fi
if rule_enabled NPM-006; then
if [[ -f "$PWD/pnpm-lock.yaml" ]] && have pnpm; then
if out="$(pnpm audit --json 2>/dev/null)"; then
if have jq; then
crit=$(echo "$out" | jq -r '.metadata.vulnerabilities.critical // 0' 2>/dev/null || echo 0)
high=$(echo "$out" | jq -r '.metadata.vulnerabilities.high // 0' 2>/dev/null || echo 0)
mod=$(echo "$out" | jq -r '.metadata.vulnerabilities.moderate // 0' 2>/dev/null || echo 0)
low=$(echo "$out" | jq -r '.metadata.vulnerabilities.low // 0' 2>/dev/null || echo 0)
else
crit=$(echo "$out" | grep -oE '"critical":[0-9]+' | head -1 | grep -oE '[0-9]+')
high=$(echo "$out" | grep -oE '"high":[0-9]+' | head -1 | grep -oE '[0-9]+')
mod=$(echo "$out" | grep -oE '"moderate":[0-9]+' | head -1 | grep -oE '[0-9]+')
low=$(echo "$out" | grep -oE '"low":[0-9]+' | head -1 | grep -oE '[0-9]+')
fi
total=$(( ${crit:-0} + ${high:-0} + ${mod:-0} + ${low:-0} ))
if [[ $total -eq 0 ]]; then
emit NPM-006 pass high "pnpm audit: 0 vulnerabilities"
else
sev=medium
[[ ${high:-0} -gt 0 ]] && sev=high
[[ ${crit:-0} -gt 0 ]] && sev=critical
emit NPM-006 fail "$sev" \
"pnpm audit: ${crit:-0} crit / ${high:-0} high / ${mod:-0} mod / ${low:-0} low" \
"pnpm update --interactive # bump vulnerable deps"
fi
fi
else
emit NPM-006 info low "no pnpm-lock.yaml in cwd — skipping pnpm CVE scan"
fi
fi
# ============================================================================
# PIP / UV / PYTHON
# ============================================================================
section "Python (pip / uv)"
PIP_CONF="$HOME/.config/pip/pip.conf"
[[ "$PLATFORM" == macos ]] && PIP_CONF_MAC="$HOME/Library/Application Support/pip/pip.conf"
UV_TOML="$HOME/.config/uv/uv.toml"
if rule_enabled PIP-001; then
pip_conf_text=""
[[ -f "$PIP_CONF" ]] && pip_conf_text+="$(cat "$PIP_CONF")"
[[ "${PIP_CONF_MAC:-}" && -f "$PIP_CONF_MAC" ]] && pip_conf_text+="$(cat "$PIP_CONF_MAC")"
if echo "$pip_conf_text" | grep -qE '^\s*require-virtualenv\s*=\s*(true|yes|1)'; then
emit PIP-001 pass medium "pip require-virtualenv enabled"
else
emit PIP-001 fail medium "pip will install into system Python without a venv" \
"mkdir -p ~/.config/pip && printf '[global]\nrequire-virtualenv = true\n' >> ~/.config/pip/pip.conf"
fi
fi
if rule_enabled PIP-002; then
if [[ -f "$UV_TOML" ]]; then
emit PIP-002 pass low "uv.toml present"
elif have uv; then
emit PIP-002 fail low "uv installed but no ~/.config/uv/uv.toml" \
"mkdir -p ~/.config/uv && printf '[pip]\nindex-url = \"https://pypi.org/simple\"\n' >> ~/.config/uv/uv.toml"
else
emit PIP-002 info low "uv not installed"
fi
fi
if rule_enabled PIP-003; then
scan_target=""
[[ -f "$PWD/requirements.txt" ]] && scan_target="-r $PWD/requirements.txt"
[[ -f "$PWD/pyproject.toml" ]] && scan_target="$scan_target"
[[ -f "$PWD/uv.lock" ]] && scan_target="$scan_target"
if [[ -n "$scan_target" ]]; then
if have pip-audit; then
if pip-audit $scan_target -f json >/tmp/pip-audit.$$ 2>/dev/null; then
if have jq; then
vuln=$(jq -r '.dependencies // [] | map(.vulns | length) | add // 0' /tmp/pip-audit.$$ 2>/dev/null)
else
vuln=$(grep -c '"id":' /tmp/pip-audit.$$ 2>/dev/null || echo 0)
fi
if [[ "${vuln:-0}" -eq 0 ]]; then
emit PIP-003 pass high "pip-audit: 0 vulnerabilities"
else
emit PIP-003 fail high "pip-audit: $vuln vulnerable dependency findings" \
"pip-audit --fix # or upgrade pinned versions manually"
fi
else
emit PIP-003 info low "pip-audit run failed"
fi
rm -f /tmp/pip-audit.$$
else
emit PIP-003 info low "pip-audit not installed (recommend: pipx install pip-audit)"
fi
else
emit PIP-003 info low "no Python project in cwd — skipping"
fi
fi
# ============================================================================
# SECRETS / FILESYSTEM
# ============================================================================
section "Secrets & filesystem"
if rule_enabled FS-001; then
declare -a SECRET_GLOBS=(
"$HOME/.config/gcloud/*.json"
"$HOME/.config/gcloud/*.db"
"$HOME/.config/gws/*"
"$HOME/.config/*/credentials*"
"$HOME/.config/*/client_secret*"
"$HOME/.config/*/token*"
"$HOME/.config/*/*.pem"
"$HOME/.config/*/*.key"
"$HOME/.aws/credentials"
"$HOME/.aws/config"
"$HOME/.docker/config.json"
"$HOME/.netrc"
"$HOME/.pgpass"
"$HOME/.kube/config"
)
declare -A seen=()
found=0
for pat in "${SECRET_GLOBS[@]}"; do
for f in $pat; do
[[ -f "$f" ]] || continue
[[ -n "${seen[$f]:-}" ]] && continue
seen[$f]=1
mode="$(stat_mode "$f")"
[[ -z "$mode" ]] && continue
go_perm="${mode:${#mode}-2:2}"
if [[ "$go_perm" != "00" ]]; then
emit FS-001 fail critical "${f/#$HOME/~} mode=$mode (should be 600/400)" \
"chmod 600 \"$f\""
found=$((found+1))
fi
done
done
[[ $found -eq 0 ]] && emit FS-001 pass critical "no group/world-readable secret files in standard locations"
fi
if rule_enabled FS-002; then
if [[ -d "$PWD/.git" ]]; then
leaked=()
findings=0
for env in "$PWD"/.env "$PWD"/.env.local "$PWD"/.env.*; do
[[ -f "$env" ]] || continue
relpath="${env#$PWD/}"
# Skip templates: .env.example, .env.sample, .env.template (intentionally committed)
case "$relpath" in
.env.example|.env.sample|.env.template|.env.dist|.env.defaults) continue ;;
esac
if ! git -C "$PWD" check-ignore -q "$env" 2>/dev/null; then
if ! git -C "$PWD" ls-files --error-unmatch "$relpath" >/dev/null 2>&1; then
leaked+=("$relpath")
findings=$((findings+1))
else
emit FS-002 fail critical "$relpath is COMMITTED to git" \
"git rm --cached \"$relpath\" && echo \"$relpath\" >> .gitignore && git commit"
findings=$((findings+1))
fi
fi
done
if [[ ${#leaked[@]} -gt 0 ]]; then
for f in "${leaked[@]}"; do
emit FS-002 fail high "$f is untracked but not gitignored (one wrong 'git add .' away from leak)" \
"echo '$f' >> .gitignore"
done
fi
[[ $findings -eq 0 ]] && emit FS-002 pass high ".env files in cwd are gitignored, templates, or absent"
else
emit FS-002 info low "cwd is not a git repo — skipping"
fi
fi
if rule_enabled FS-003; then
ssh_bad=0
if [[ -d "$HOME/.ssh" ]]; then
for f in "$HOME"/.ssh/id_* "$HOME"/.ssh/*.pem; do
[[ -f "$f" ]] || continue
[[ "$f" == *.pub ]] && continue
mode="$(stat_mode "$f")"
[[ -z "$mode" ]] && continue
if [[ "$mode" != "600" && "$mode" != "400" ]]; then
emit FS-003 fail high "~/.ssh/${f##*/} mode=$mode (should be 600)" \
"chmod 600 \"$f\""
ssh_bad=$((ssh_bad+1))
fi
done
fi
[[ $ssh_bad -eq 0 ]] && emit FS-003 pass high "all SSH private keys have safe perms"
fi
# ============================================================================
# SSH
# ============================================================================
section "SSH"
if rule_enabled SSH-001; then
if [[ -f "$HOME/.ssh/id_rsa.pub" ]] && have ssh-keygen; then
bits="$(ssh-keygen -lf "$HOME/.ssh/id_rsa.pub" 2>/dev/null | awk '{print $1}')"
if [[ -n "$bits" && "$bits" -lt 3072 ]]; then
emit SSH-001 fail medium "id_rsa is ${bits}-bit RSA (weak)" \
"ssh-keygen -t ed25519 -C \"\$(whoami)@\$(hostname)\" # migrate and delete id_rsa"
else
emit SSH-001 pass medium "id_rsa is ${bits}-bit RSA (acceptable)"
fi
else
emit SSH-001 pass medium "no legacy id_rsa"
fi
fi
if rule_enabled SSH-002; then
if grep -rEq '^\s*ForwardAgent\s+yes' "$HOME/.ssh/config" "$HOME/.ssh/config.d" 2>/dev/null; then
# accept it if scoped per-host, fail only if unconditional `Host *`
if grep -B1 -E '^\s*ForwardAgent\s+yes' "$HOME/.ssh/config" 2>/dev/null | grep -qE '^\s*Host\s+\*\s*$'; then
emit SSH-002 fail medium "ForwardAgent yes is set on Host * (any compromised host can sign with your keys)" \
"Scope ForwardAgent to specific hosts only, or remove it"
else
emit SSH-002 pass medium "ForwardAgent is scoped per-host"
fi
else
emit SSH-002 pass medium "ForwardAgent not enabled"
fi
fi
# ============================================================================
# VS CODE / CURSOR
# ============================================================================
section "VS Code / Cursor"
declare -a CODE_BINS=()
have code && CODE_BINS+=("code")
have cursor && CODE_BINS+=("cursor")
if [[ ${#CODE_BINS[@]} -eq 0 ]]; then
emit VSC-001 info low "neither VS Code nor Cursor found"
else
trusted_regex='^(ms-|github\.|anthropic\.|openai\.|hashicorp\.|golang\.|rust-lang\.|redhat\.|esbenp\.|dbaeumer\.|charliermarsh\.|bradlc\.|eamodio\.|editorconfig\.|bbenoist\.|enkia\.|dracula-theme\.|bierner\.|davidanson\.|yzhang\.|christian-kohler\.|alefragnani\.|gruntfuggly\.|shopify\.|mtxr\.|mechatroner\.|tomoki1207\.|jebbs\.)'
for bin in "${CODE_BINS[@]}"; do
case "$PLATFORM" in
macos)
cap_name="$(echo "$bin" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')"
SETTINGS="$HOME/Library/Application Support/$cap_name/User/settings.json" ;;
linux)
cap_name="$(echo "$bin" | awk '{print toupper(substr($0,1,1)) substr($0,2)}')"
SETTINGS="$HOME/.config/$cap_name/User/settings.json" ;;
esac
if rule_enabled VSC-001; then
if [[ -f "$SETTINGS" ]] && grep -qE '"extensions\.autoUpdate"\s*:\s*false' "$SETTINGS"; then
emit VSC-001 pass high "$bin: auto-update disabled"
else
emit VSC-001 fail high "$bin: extensions auto-update silently (no review on publisher takeover)" \
"Add \"extensions.autoUpdate\": false and \"extensions.autoCheckUpdates\": false to $SETTINGS"
fi
fi
if rule_enabled VSC-002; then
if exts="$("$bin" --list-extensions 2>/dev/null)"; then
flagged=()
while IFS= read -r ext; do
[[ -z "$ext" ]] && continue
if ! echo "$ext" | grep -qE "$trusted_regex"; then
flagged+=("$ext")
fi
done <<< "$exts"
if [[ ${#flagged[@]} -eq 0 ]]; then
emit VSC-002 pass medium "$bin: all extensions from recognized publishers"
else
for ext in "${flagged[@]}"; do
emit VSC-002 fail medium "$bin: unverified publisher: $ext" \
"Audit at https://marketplace.visualstudio.com/items?itemName=$ext or remove: $bin --uninstall-extension $ext"
done
fi
fi
fi
done
fi
# ============================================================================
# BROWSER EXTENSIONS
# ============================================================================
section "Browser extensions"
if rule_enabled BROWSER-001; then
declare -a BROWSER_PATHS=()
if [[ "$PLATFORM" == macos ]]; then
BROWSER_PATHS=(
"$HOME/Library/Application Support/Google/Chrome/Default/Extensions"
"$HOME/Library/Application Support/Google/Chrome/Profile 1/Extensions"
"$HOME/Library/Application Support/BraveSoftware/Brave-Browser/Default/Extensions"
"$HOME/Library/Application Support/Arc/User Data/Default/Extensions"
"$HOME/Library/Application Support/Zen/Profiles"
"$HOME/Library/Application Support/Firefox/Profiles"
"$HOME/Library/Application Support/Microsoft Edge/Default/Extensions"
)
else
BROWSER_PATHS=(
"$HOME/.config/google-chrome/Default/Extensions"
"$HOME/.config/BraveSoftware/Brave-Browser/Default/Extensions"
"$HOME/.config/microsoft-edge/Default/Extensions"
"$HOME/.mozilla/firefox"
)
fi
any_found=0
declare -A reported_ext=()
for dir in "${BROWSER_PATHS[@]}"; do
[[ -d "$dir" ]] || continue
any_found=1
browser="$(echo "$dir" | awk -F/ '{for(i=1;i<=NF;i++) if($i~/Chrome|Brave|Arc|Zen|Firefox|Edge/) {print $i; exit}}')"
risky=0
total=0
while IFS= read -r -d '' manifest; do
total=$((total+1))
if grep -qE '"(<all_urls>|nativeMessaging|webRequest|webRequestBlocking|cookies|debugger|tabs|management)"' "$manifest" 2>/dev/null; then
ext_id="$(echo "$manifest" | awk -F/ '{print $(NF-2)}')"
# dedupe by (browser, ext_id) — multiple profiles install the same extension
key="$browser:$ext_id"
[[ -n "${reported_ext[$key]:-}" ]] && continue
reported_ext[$key]=1
# Strip i18n placeholders like __MSG_appName__ and clean quotes
name="$(python3 -c "import json,sys
try:
d=json.load(open(sys.argv[1]))
n=d.get('name','?')
if isinstance(n,str) and not n.startswith('__MSG'):
print(n)
else:
print(d.get('short_name', sys.argv[1].split('/')[-3]))
except Exception:
print('?')
" "$manifest" 2>/dev/null || echo "?")"
emit BROWSER-001 fail medium "$browser ext '$name' ($ext_id) requests high-risk perms" \
"Review at chrome://extensions or about:addons; remove if not essential"
risky=$((risky+1))
fi
done < <(find "$dir" -name manifest.json -not -path '*node_modules*' -print0 2>/dev/null)
if [[ $total -gt 0 && $risky -eq 0 ]]; then
emit BROWSER-001 pass medium "$browser: $total extensions, none with high-risk perms"
elif [[ $total -eq 0 ]]; then
emit BROWSER-001 info low "$browser: no extensions"
fi
done
[[ $any_found -eq 0 ]] && emit BROWSER-001 info low "no supported browsers found"
fi
# ============================================================================
# CLAUDE / MCP
# ============================================================================
section "Claude Code / MCP servers"
if rule_enabled MCP-001; then
CLAUDE_JSON="$HOME/.claude.json"
if [[ -f "$CLAUDE_JSON" ]]; then
if have jq; then
servers="$(jq -r '.mcpServers // {} | to_entries[] | "\(.key)\t\(.value.type // "stdio")\t\(.value.url // .value.command // "?")"' "$CLAUDE_JSON" 2>/dev/null)"
else
servers="$(grep -oE '"[a-zA-Z0-9_-]+":\s*\{[^}]+\}' "$CLAUDE_JSON" | head -20)"
fi
if [[ -z "$servers" ]]; then
emit MCP-001 pass high "no MCP servers configured"
else
while IFS=$'\t' read -r name typ target; do
[[ -z "$name" ]] && continue
# Allowlist of known-safe MCP sources
if echo "$target" | grep -qE '^(https://(api\.anthropic\.com|dash\.brain-ai\.dev|.*\.githubusercontent\.com)|^/nix/store/|^npx|^node\s)'; then
emit MCP-001 pass high "MCP $name [$typ] → $target (recognized)"
else
emit MCP-001 fail high "MCP $name [$typ] → $target (unrecognized — verify you added it)" \
"Inspect ~/.claude.json; remove via: jq 'del(.mcpServers.\"$name\")' ~/.claude.json"
fi
done <<< "$servers"
fi
else
emit MCP-001 info low "~/.claude.json not present"
fi
fi
# ============================================================================
# GITHUB CLI
# ============================================================================
section "GitHub CLI"
if rule_enabled GH-001; then
if have gh; then
if gh auth status >/tmp/ghstat.$$ 2>&1; then
scopes=$(grep -i 'scopes:' /tmp/ghstat.$$ | head -1 | sed 's/.*scopes://; s/ //g')
if echo "$scopes" | grep -qE "delete_repo|admin:org|admin:enterprise|workflow"; then
emit GH-001 fail medium "gh token has dangerous scopes: $scopes" \
"Reissue with minimal scopes: gh auth refresh --scopes repo,read:org"
else
emit GH-001 pass medium "gh token scopes: $scopes"
fi
else
emit GH-001 info low "gh not logged in"
fi
rm -f /tmp/ghstat.$$
else
emit GH-001 info low "gh CLI not installed"
fi
fi
# ============================================================================
# OS BASELINE
# ============================================================================
section "OS baseline"
if [[ "$PLATFORM" == macos ]]; then
if rule_enabled OS-001; then
if have fdesetup; then
if fdesetup status 2>/dev/null | grep -q "FileVault is On"; then
emit OS-001 pass high "FileVault is on"
else
emit OS-001 fail high "FileVault is OFF — disk unencrypted at rest" \
"Enable in System Settings → Privacy & Security → FileVault"
fi
fi
fi
if rule_enabled OS-002; then
if have spctl; then
st="$(spctl --status 2>/dev/null)"
if echo "$st" | grep -q "assessments enabled"; then
emit OS-002 pass high "Gatekeeper enabled"
else
emit OS-002 fail high "Gatekeeper disabled — unsigned binaries can run silently" \
"sudo spctl --master-enable"
fi
fi
fi
if rule_enabled OS-003; then
if have csrutil; then
if csrutil status 2>/dev/null | grep -qi enabled; then
emit OS-003 pass medium "SIP enabled"
else
emit OS-003 fail medium "SIP disabled — system file protections off" \
"Reboot to recovery; csrutil enable; reboot"
fi
fi
fi
if rule_enabled OS-004; then
fw=/usr/libexec/ApplicationFirewall/socketfilterfw
if [[ -x $fw ]]; then
if $fw --getglobalstate 2>/dev/null | grep -q enabled; then
emit OS-004 pass medium "Application Firewall enabled"
else
emit OS-004 fail medium "Application Firewall is OFF" \
"sudo $fw --setglobalstate on"
fi
fi
fi
elif [[ "$PLATFORM" == linux ]]; then
if rule_enabled OS-004; then
if have ufw; then
if ufw status 2>/dev/null | grep -qi "Status: active"; then
emit OS-004 pass medium "ufw active"
else
emit OS-004 fail medium "ufw is inactive" "sudo ufw enable"
fi
elif have firewall-cmd; then
if firewall-cmd --state 2>/dev/null | grep -q running; then
emit OS-004 pass medium "firewalld running"
else
emit OS-004 fail medium "firewalld is not running" "sudo systemctl enable --now firewalld"
fi
else
emit OS-004 info low "no recognized firewall tool"
fi
fi
fi
# ============================================================================
# CI / GITHUB ACTIONS
# ============================================================================
section "CI workflows"
if [[ -d "$PWD/.github/workflows" ]]; then
if rule_enabled CI-001; then
declare -A seen_action=()
unpinned=0
while IFS= read -r line; do
action="${line#*uses:}"
action="${action# }"
action="${action%%[[:space:]]*}"
# skip local actions and ones already pinned to a 40-char SHA
[[ -z "$action" ]] && continue
[[ "$action" == "./"* ]] && continue
if echo "$action" | grep -qE '@[0-9a-f]{40}\b'; then continue; fi
[[ -n "${seen_action[$action]:-}" ]] && continue
seen_action[$action]=1
unpinned=$((unpinned+1))
emit CI-001 fail high "unpinned action: $action" \
"Replace tag with full SHA: gh api repos/$(echo "$action" | cut -d@ -f1)/commits/$(echo "$action" | cut -d@ -f2) -q .sha"
done < <(grep -rhE '^\s*-?\s*uses:' "$PWD/.github/workflows" 2>/dev/null)
[[ $unpinned -eq 0 ]] && emit CI-001 pass high "all action references are SHA-pinned (or local)"
fi
if rule_enabled CI-002; then
danger=0
for wf in "$PWD"/.github/workflows/*.{yml,yaml}; do
[[ -f "$wf" ]] || continue
if grep -qE 'pull_request_target' "$wf" && grep -qE 'checkout.*ref.*head\.sha' "$wf"; then
emit CI-002 fail critical "$wf has pull_request_target + checkout PR head SHA (RCE via PR)" \
"Drop pull_request_target unless you understand the threat model; never check out PR head with secrets"
danger=$((danger+1))
fi
done
[[ $danger -eq 0 ]] && emit CI-002 pass critical "no dangerous pull_request_target + checkout patterns"
fi
else
emit CI-001 info low "no .github/workflows in cwd"
fi
# ============================================================================
# SHELL HISTORY SECRETS
# ============================================================================
section "Shell history"
if rule_enabled HIST-001; then
declare -a HIST_FILES=("$HOME/.zsh_history" "$HOME/.bash_history" "$HOME/.local/share/fish/fish_history")
patterns='AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|gho_[A-Za-z0-9]{30,}|ghs_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{32,}|xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}'
hits=0
for hf in "${HIST_FILES[@]}"; do
[[ -f "$hf" ]] || continue
if grep -cE "$patterns" "$hf" >/tmp/histhits.$$ 2>/dev/null; then
count="$(cat /tmp/histhits.$$ 2>/dev/null)"
if [[ "${count:-0}" -gt 0 ]]; then
emit HIST-001 fail medium "$count probable-secret hit(s) in ${hf/#$HOME/~}" \
"Rotate any matching credential; trim history: grep -vE '<pattern>' $hf > $hf.clean && mv $hf.clean $hf"
hits=$((hits+count))
fi
fi
done
rm -f /tmp/histhits.$$
[[ $hits -eq 0 ]] && emit HIST-001 pass medium "no obvious secret patterns in shell history"
fi
# ============================================================================
# GIT / GITLEAKS
# ============================================================================
section "Git / gitleaks"
if rule_enabled GIT-001; then
if [[ -d "$PWD/.git" ]]; then
if [[ -f "$PWD/.pre-commit-config.yaml" ]] && grep -q gitleaks "$PWD/.pre-commit-config.yaml"; then
if [[ -f "$PWD/.git/hooks/pre-commit" ]]; then
emit GIT-001 pass low "gitleaks pre-commit configured AND installed"
else
emit GIT-001 fail low "gitleaks configured but hook not installed" \
"pre-commit install"
fi
else
emit GIT-001 info low "no gitleaks pre-commit hook in this repo"
fi
fi
fi
# ============================================================================
# DOCKER
# ============================================================================
section "Docker"
if rule_enabled DOCKER-001; then
for daemon in /etc/docker/daemon.json "$HOME/.docker/daemon.json"; do
[[ -f "$daemon" ]] || continue
if grep -qE '"insecure-registries"' "$daemon"; then
registries="$(grep -A5 'insecure-registries' "$daemon" | tr -d ' \t' | grep -oE '"[^"]+\.[^"]+"' | head -5)"
emit DOCKER-001 fail medium "$daemon has insecure-registries: $registries" \
"Remove insecure-registries; use TLS or local-network-only allowlists"
else
emit DOCKER-001 pass medium "$daemon has no insecure-registries"
fi
done
fi
# ============================================================================
# SUMMARY
# ============================================================================
if [[ $JSON_MODE -eq 0 ]]; then
printf '\n%s%sSummary%s\n' "$C_BLD" "$C_CYA" "$C_RST"
printf ' %s✗ critical: %d%s %s✗ high: %d%s %s! medium: %d%s %s· low: %d%s %s✓ pass: %d%s %s· info: %d%s\n' \
"$C_MAG" "$CRIT_COUNT" "$C_RST" \
"$C_RED" "$HIGH_COUNT" "$C_RST" \
"$C_YEL" "$MED_COUNT" "$C_RST" \
"$C_DIM" "$LOW_COUNT" "$C_RST" \
"$C_GRN" "$PASS_COUNT" "$C_RST" \
"$C_DIM" "$INFO_COUNT" "$C_RST"
cat <<'EOF'
Reading findings:
✗ critical immediate exfil risk (world-readable keys, RCE-shaped CI)
✗ high attacker can pivot here within hours of disclosure
! medium needs fixing but not an active fire
· low informational / hardening suggestion
This script makes no changes. Paste the printed `fix:` commands selectively.
Filters:
bash audit.sh --quiet # only show fails
bash audit.sh --only NPM-005 # run one rule
bash audit.sh --skip GH-001 # skip rule(s)
bash audit.sh --json | jq . # one finding per line
bash audit.sh --list # all rule IDs + severities
EOF
fi
# Exit code: 2 if any critical/high, 1 if any medium, 0 otherwise
if [[ $CRIT_COUNT -gt 0 || $HIGH_COUNT -gt 0 ]]; then exit 2
elif [[ $MED_COUNT -gt 0 ]]; then exit 1
else exit 0
fi
0dc0d2823d2065fb88ff0044571b44071bde5b24a27dc0476a2dc16bfa38d366 dev-supply-chain-audit.sh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment