#!/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|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 } 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 ;; trivy) run_trivy ;; zap) run_zap ;; nuclei) run_nuclei ;; kube-bench) run_kube_bench ;; -h | --help | help) usage ;; *) usage >&2 exit 1 ;; esac