my-homelab-configs/scripts/security-scan

213 lines
5.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
HOMELAB_STATE_DIR="${HOMELAB_STATE_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/homelab}"
SECURITY_REPORT_DIR="${SECURITY_REPORT_DIR:-${HOMELAB_STATE_DIR}/security-reports}"
SECURITY_TARGETS_FILE="${SECURITY_TARGETS_FILE:-${REPO_ROOT}/security/targets.txt}"
TRIVY_IMAGE="${TRIVY_IMAGE:-aquasec/trivy:latest}"
ZAP_IMAGE="${ZAP_IMAGE:-ghcr.io/zaproxy/zaproxy:stable}"
KUBE_BENCH_IMAGE="${KUBE_BENCH_IMAGE:-aquasec/kube-bench:latest}"
NUCLEI_IMAGE="${NUCLEI_IMAGE:-projectdiscovery/nuclei:latest}"
usage() {
cat <<'EOF'
Usage: scripts/security-scan {all|prepare|trivy|zap|kube-bench|nuclei}
Environment:
SECURITY_REPORT_DIR Report output directory.
SECURITY_TARGETS_FILE Target allowlist for web scans.
SECURITY_ZAP_TARGET Single URL override for ZAP.
SECURITY_NUCLEI_TARGET Single URL override for nuclei.
EOF
}
timestamp() {
date -u +%Y%m%dT%H%M%SZ
}
ensure_report_dir() {
mkdir -p "${SECURITY_REPORT_DIR}"
}
require_docker() {
if ! command -v docker >/dev/null 2>&1; then
echo "docker is required for containerized security scans." >&2
exit 1
fi
}
prepare_security_scans() {
local image
local -a images=(
"${TRIVY_IMAGE}"
"${ZAP_IMAGE}"
"${KUBE_BENCH_IMAGE}"
"${NUCLEI_IMAGE}"
)
ensure_report_dir
require_docker
if ! docker info >/dev/null 2>&1; then
echo "docker is installed but not reachable by this user." >&2
exit 1
fi
if [[ ! -s "${SECURITY_TARGETS_FILE}" ]]; then
echo "Missing target file: ${SECURITY_TARGETS_FILE}" >&2
exit 1
fi
if [[ ! -w "${SECURITY_REPORT_DIR}" ]]; then
echo "Report directory is not writable: ${SECURITY_REPORT_DIR}" >&2
exit 1
fi
echo "Preparing security scan images..."
for image in "${images[@]}"; do
echo "--> docker pull ${image}"
docker pull "${image}"
done
echo "Security scan prerequisites are ready."
echo "Report directory: ${SECURITY_REPORT_DIR}"
}
run_trivy() {
local stamp
stamp="$(timestamp)"
ensure_report_dir
echo "Running Trivy filesystem and IaC/config scans..."
if command -v trivy >/dev/null 2>&1; then
trivy fs --scanners vuln,secret,misconfig --format table --output "${SECURITY_REPORT_DIR}/trivy-fs-${stamp}.txt" "${REPO_ROOT}"
trivy config --format table --output "${SECURITY_REPORT_DIR}/trivy-config-${stamp}.txt" "${REPO_ROOT}"
else
require_docker
docker run --rm \
-v "${REPO_ROOT}:/repo:ro" \
-v "${SECURITY_REPORT_DIR}:/reports" \
"${TRIVY_IMAGE}" fs --scanners vuln,secret,misconfig --format table --output "/reports/trivy-fs-${stamp}.txt" /repo
docker run --rm \
-v "${REPO_ROOT}:/repo:ro" \
-v "${SECURITY_REPORT_DIR}:/reports" \
"${TRIVY_IMAGE}" config --format table --output "/reports/trivy-config-${stamp}.txt" /repo
fi
echo "Trivy reports written to ${SECURITY_REPORT_DIR}"
}
scan_targets() {
local target_override="$1"
if [[ -n "${target_override}" ]]; then
printf '%s\n' "${target_override}"
return 0
fi
if [[ ! -s "${SECURITY_TARGETS_FILE}" ]]; then
echo "Missing target file: ${SECURITY_TARGETS_FILE}" >&2
exit 1
fi
sed -e 's/#.*//' -e '/^[[:space:]]*$/d' "${SECURITY_TARGETS_FILE}"
}
safe_target_name() {
printf '%s' "$1" | sed -E 's#^https?://##; s#[^A-Za-z0-9._-]+#_#g; s#_+$##'
}
run_zap() {
local stamp
local target
local safe_name
stamp="$(timestamp)"
ensure_report_dir
require_docker
echo "Running OWASP ZAP baseline passive scans..."
while IFS= read -r target; do
[[ -n "${target}" ]] || continue
safe_name="$(safe_target_name "${target}")"
echo "--> ZAP baseline ${target}"
docker run --rm \
-v "${SECURITY_REPORT_DIR}:/zap/wrk:rw" \
"${ZAP_IMAGE}" zap-baseline.py \
-t "${target}" \
-I \
-r "zap-${safe_name}-${stamp}.html" \
-J "zap-${safe_name}-${stamp}.json"
done < <(scan_targets "${SECURITY_ZAP_TARGET:-}")
echo "ZAP reports written to ${SECURITY_REPORT_DIR}"
}
run_nuclei() {
local stamp
local target
local safe_name
stamp="$(timestamp)"
ensure_report_dir
require_docker
echo "Running nuclei low-noise HTTP exposure scans..."
while IFS= read -r target; do
[[ -n "${target}" ]] || continue
safe_name="$(safe_target_name "${target}")"
echo "--> nuclei ${target}"
docker run --rm \
-v "${SECURITY_REPORT_DIR}:/reports:rw" \
"${NUCLEI_IMAGE}" \
-u "${target}" \
-severity low,medium,high,critical \
-rate-limit "${SECURITY_NUCLEI_RATE_LIMIT:-5}" \
-retries 1 \
-timeout 5 \
-o "/reports/nuclei-${safe_name}-${stamp}.txt"
done < <(scan_targets "${SECURITY_NUCLEI_TARGET:-}")
echo "nuclei reports written to ${SECURITY_REPORT_DIR}"
}
run_kube_bench() {
local stamp
stamp="$(timestamp)"
ensure_report_dir
require_docker
echo "Running kube-bench CIS checks against this kubeadm host..."
docker run --rm --pid=host \
-v /etc:/etc:ro \
-v /var:/var:ro \
-v /usr/bin:/usr/local/mount-from-host/bin:ro \
"${KUBE_BENCH_IMAGE}" run --targets master,node | tee "${SECURITY_REPORT_DIR}/kube-bench-${stamp}.txt"
}
case "${1:-all}" in
all)
run_trivy
run_zap
;;
prepare)
prepare_security_scans
;;
trivy)
run_trivy
;;
zap)
run_zap
;;
nuclei)
run_nuclei
;;
kube-bench)
run_kube_bench
;;
-h | --help | help)
usage
;;
*)
usage >&2
exit 1
;;
esac