mirror of
https://github.com/jmagar/unraid-mcp
synced 2026-04-21 13:37:53 +00:00
- bin/bump-version.sh: one-command version bump across all four files; supports explicit version or major/minor/patch keywords; uses CLAUDE_PLUGIN_ROOT when set (hook context), dirname fallback otherwise - tests/test_bump_version.bats: 9 bats tests covering all bump modes, all-files-in-sync, output format, and dirname fallback - scripts/ renamed to bin/ - Bump 1.3.5 → 1.3.6
145 lines
6.1 KiB
Bash
Executable file
145 lines
6.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# check-docker-security.sh — Verify Dockerfile follows plugin security conventions
|
|
# Run standalone: bash scripts/check-docker-security.sh [path/to/Dockerfile]
|
|
# Run in pre-commit: add as a hook (see .pre-commit-config.yaml example in plugin-setup-guide)
|
|
#
|
|
# Checks:
|
|
# 1. Multi-stage build (separate builder + runtime stages)
|
|
# 2. Non-root user (USER 1000:1000 or ${PUID}:${PGID})
|
|
# 3. No sensitive ENV directives baked into the image
|
|
# 4. HEALTHCHECK present
|
|
set -euo pipefail
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
WARN=0
|
|
|
|
pass() { echo " ✓ PASS: $1"; PASS=$((PASS + 1)); }
|
|
fail() { echo " ✗ FAIL: $1 — $2"; FAIL=$((FAIL + 1)); }
|
|
warn() { echo " ⚠ WARN: $1 — $2"; WARN=$((WARN + 1)); }
|
|
|
|
# Find Dockerfile
|
|
DOCKERFILE="${1:-Dockerfile}"
|
|
if [[ ! -f "$DOCKERFILE" ]]; then
|
|
echo "Error: $DOCKERFILE not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "=== Docker Security Check: $DOCKERFILE ==="
|
|
|
|
# ── 1. Multi-stage build ─────────────────────────────────────────────────────
|
|
FROM_COUNT=$(grep -cE '^FROM\s' "$DOCKERFILE" || true)
|
|
if [[ "$FROM_COUNT" -ge 2 ]]; then
|
|
pass "Multi-stage build ($FROM_COUNT stages)"
|
|
else
|
|
fail "Multi-stage build" "Found $FROM_COUNT FROM directive(s) — need at least 2 (builder + runtime)"
|
|
fi
|
|
|
|
# Check for named stages
|
|
if grep -qE '^FROM\s.+\sAS\s+builder' "$DOCKERFILE"; then
|
|
pass "Named builder stage"
|
|
else
|
|
warn "Named builder stage" "No 'FROM ... AS builder' found — recommend naming stages"
|
|
fi
|
|
|
|
if grep -qE '^FROM\s.+\sAS\s+runtime' "$DOCKERFILE"; then
|
|
pass "Named runtime stage"
|
|
else
|
|
warn "Named runtime stage" "No 'FROM ... AS runtime' found — recommend naming stages"
|
|
fi
|
|
|
|
# ── 2. Non-root user ─────────────────────────────────────────────────────────
|
|
# Check for USER directive
|
|
if grep -qE '^USER\s' "$DOCKERFILE"; then
|
|
USER_LINE=$(grep -E '^USER\s' "$DOCKERFILE" | tail -1)
|
|
USER_VALUE=$(echo "$USER_LINE" | sed 's/^USER\s*//')
|
|
|
|
# Check for 1000:1000 or variable-based UID:GID
|
|
if echo "$USER_VALUE" | grep -qE '^\$?\{?PUID|1000:1000|1000$'; then
|
|
pass "Non-root user ($USER_VALUE)"
|
|
else
|
|
warn "Non-root user" "USER is '$USER_VALUE' — expected 1000:1000 or \${PUID}:\${PGID}"
|
|
fi
|
|
else
|
|
# Check if docker-compose.yaml handles it via user: directive
|
|
if [[ -f "docker-compose.yaml" ]] && grep -qE '^\s+user:' docker-compose.yaml; then
|
|
warn "Non-root user" "No USER in Dockerfile but docker-compose.yaml sets user: — acceptable if always run via compose"
|
|
else
|
|
fail "Non-root user" "No USER directive found — container runs as root"
|
|
fi
|
|
fi
|
|
|
|
# Check there's no USER root after the runtime stage
|
|
RUNTIME_START=$(grep -nE '^FROM\s.+\sAS\s+runtime' "$DOCKERFILE" | head -1 | cut -d: -f1 || true)
|
|
if [[ -n "$RUNTIME_START" ]]; then
|
|
if tail -n +"$RUNTIME_START" "$DOCKERFILE" | grep -qE '^USER\s+root'; then
|
|
fail "No root in runtime" "USER root found after runtime stage — never run as root in production"
|
|
else
|
|
pass "No root in runtime stage"
|
|
fi
|
|
fi
|
|
|
|
# ── 3. No sensitive ENV baked in ──────────────────────────────────────────────
|
|
SENSITIVE_PATTERNS='(API_KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|PRIVATE_KEY|AUTH)'
|
|
BAKED_ENVS=$(grep -nE "^ENV\s+.*${SENSITIVE_PATTERNS}" "$DOCKERFILE" || true)
|
|
if [[ -n "$BAKED_ENVS" ]]; then
|
|
fail "No baked secrets" "Sensitive ENV directives found in Dockerfile:"
|
|
echo "$BAKED_ENVS" | while IFS= read -r line; do
|
|
echo " $line"
|
|
done
|
|
else
|
|
pass "No baked secrets in ENV directives"
|
|
fi
|
|
|
|
# Check for ARG with defaults that look like secrets
|
|
BAKED_ARGS=$(grep -nE "^ARG\s+.*${SENSITIVE_PATTERNS}.*=" "$DOCKERFILE" || true)
|
|
if [[ -n "$BAKED_ARGS" ]]; then
|
|
warn "No baked ARG secrets" "ARG with sensitive defaults found (may leak via docker history):"
|
|
echo "$BAKED_ARGS" | while IFS= read -r line; do
|
|
echo " $line"
|
|
done
|
|
else
|
|
pass "No baked secrets in ARG defaults"
|
|
fi
|
|
|
|
# ── 4. HEALTHCHECK ────────────────────────────────────────────────────────────
|
|
if grep -qE '^HEALTHCHECK\s' "$DOCKERFILE"; then
|
|
pass "HEALTHCHECK directive present"
|
|
if grep -qE '/health' "$DOCKERFILE"; then
|
|
pass "HEALTHCHECK uses /health endpoint"
|
|
else
|
|
warn "HEALTHCHECK endpoint" "HEALTHCHECK doesn't reference /health — ensure it matches your health endpoint"
|
|
fi
|
|
else
|
|
warn "HEALTHCHECK" "No HEALTHCHECK in Dockerfile — relying on docker-compose healthcheck only"
|
|
fi
|
|
|
|
# ── 5. Dependency layer caching ───────────────────────────────────────────────
|
|
# Check that manifest files are copied before source (for layer caching)
|
|
COPY_LINES=$(grep -nE '^COPY\s' "$DOCKERFILE" || true)
|
|
FIRST_MANIFEST_COPY=""
|
|
FIRST_SOURCE_COPY=""
|
|
|
|
while IFS= read -r line; do
|
|
linenum=$(echo "$line" | cut -d: -f1)
|
|
content=$(echo "$line" | cut -d: -f2-)
|
|
if echo "$content" | grep -qE '(pyproject\.toml|package.*\.json|Cargo\.(toml|lock)|go\.(mod|sum)|uv\.lock)'; then
|
|
[[ -z "$FIRST_MANIFEST_COPY" ]] && FIRST_MANIFEST_COPY="$linenum"
|
|
elif echo "$content" | grep -qE '\.\s+\.|src/|lib/'; then
|
|
[[ -z "$FIRST_SOURCE_COPY" ]] && FIRST_SOURCE_COPY="$linenum"
|
|
fi
|
|
done <<< "$COPY_LINES"
|
|
|
|
if [[ -n "$FIRST_MANIFEST_COPY" && -n "$FIRST_SOURCE_COPY" ]]; then
|
|
if [[ "$FIRST_MANIFEST_COPY" -lt "$FIRST_SOURCE_COPY" ]]; then
|
|
pass "Dependency manifest copied before source (layer caching)"
|
|
else
|
|
warn "Layer caching" "Source copied before dependency manifest — swap order for better Docker layer caching"
|
|
fi
|
|
fi
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
echo
|
|
echo "Results: $PASS passed, $FAIL failed, $WARN warnings"
|
|
[[ "$FAIL" -eq 0 ]] && echo "DOCKER SECURITY CHECK PASSED" && exit 0
|
|
echo "DOCKER SECURITY CHECK FAILED" && exit 1
|