From 4f1c973cb5a2ad50eeb7c125533b5c1175a6e6cd Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Mon, 29 Jun 2026 14:25:47 -0600 Subject: [PATCH] Add defensive security scan commands --- docs/jeannie.1.md | 50 ++++++++++++ jeannie | 42 +++++++++- scripts/security-scan | 174 ++++++++++++++++++++++++++++++++++++++++++ security/README.md | 37 +++++++++ security/targets.txt | 18 +++++ 5 files changed, 320 insertions(+), 1 deletion(-) create mode 100755 scripts/security-scan create mode 100644 security/README.md create mode 100644 security/targets.txt diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index 23a54d1..bcfcaa1 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -127,6 +127,24 @@ structure. : Check local AI helper prerequisites, knowledge index, and Ollama availability when the backstage helper is enabled. +### Defensive Security + +`security-scan` +: Run the default defensive scan set: Trivy repo/IaC scans plus OWASP ZAP +baseline passive scans for targets in `security/targets.txt`. + +`security-zap` +: Run only OWASP ZAP baseline passive scans against public targets. + +`security-k8s` +: Run kube-bench CIS checks against the Debian kubeadm host. + +`security-trivy` +: Run only Trivy filesystem and IaC/config scans. + +`security-nuclei` +: Run low-rate nuclei HTTP exposure scans against public targets. + ### Maintenance `state-backup` @@ -184,6 +202,20 @@ checks already show short output by default. : DNS name used by `status` and `doctor-preapply` for the Pi-hole resolver smoke test. Defaults to `cloudflare.com`. +`SECURITY_REPORT_DIR` +: Output directory for defensive security reports. Defaults to +`$HOMELAB_STATE_DIR/security-reports`. + +`SECURITY_TARGETS_FILE` +: Public target allowlist for ZAP and nuclei scans. Defaults to +`security/targets.txt`. + +`SECURITY_ZAP_TARGET` +: Optional single URL override for `security-zap`. + +`SECURITY_NUCLEI_TARGET` +: Optional single URL override for `security-nuclei`. + `LAB_AUTO_APPROVE=false` : Disable automatic OpenTofu approval for apply paths that support confirmation. @@ -249,6 +281,12 @@ script name. `$HOMELAB_STATE_DIR/tofu-state-backups` : Local OpenTofu and generated state backups. +`$HOMELAB_STATE_DIR/security-reports` +: Defensive scan reports. + +`security/targets.txt` +: Public target allowlist for defensive web scans. + ## EXAMPLES Start a three-worker homelab: @@ -269,6 +307,18 @@ Run focused edge diagnostics: ./jeannie doctor-edge ``` +Run the default defensive scan set: + +```sh +./jeannie security-scan +``` + +Run a ZAP baseline scan against one URL: + +```sh +SECURITY_ZAP_TARGET=https://lab2025.duckdns.org/ ./jeannie security-zap +``` + Run the blocking pre-apply doctor directly: ```sh diff --git a/jeannie b/jeannie index 02fe028..10b81a6 100755 --- a/jeannie +++ b/jeannie @@ -5760,6 +5760,31 @@ PY echo "AI check passed." } +security_scan() { + require_debian_server "security-scan" + "${REPO_ROOT}/scripts/security-scan" all +} + +security_zap() { + require_debian_server "security-zap" + "${REPO_ROOT}/scripts/security-scan" zap +} + +security_k8s() { + require_debian_server "security-k8s" + "${REPO_ROOT}/scripts/security-scan" kube-bench +} + +security_trivy() { + require_debian_server "security-trivy" + "${REPO_ROOT}/scripts/security-scan" trivy +} + +security_nuclei() { + require_debian_server "security-nuclei" + "${REPO_ROOT}/scripts/security-scan" nuclei +} + case "${1:-}" in up) up @@ -5861,6 +5886,21 @@ case "${1:-}" in ai-check) ai_check ;; + security-scan) + security_scan + ;; + security-zap) + security_zap + ;; + security-k8s) + security_k8s + ;; + security-trivy) + security_trivy + ;; + security-nuclei) + security_nuclei + ;; openwrt) openwrt ;; @@ -5872,7 +5912,7 @@ case "${1:-}" in echo "Log: ${JEANNIE_LOG_FILE}" ;; *) - echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-gitea-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|openwrt|nuke}" + echo "Usage: $0 {up|plan [all|provisioning|cluster|platform|apps|edge]|rebuild-cluster|stop-cluster|start-cluster|status|apps|website-translation-model|website-ollama-listen|ollama-setup|deploy-gitea|rpi-services|bootstrap-gitea-repo|backup-gitea|drill-gitea-restore|install-gitea-runner|move-prometheus-stack-workers|doctor-versions|doctor-edge|doctor-gitea|doctor-rpi|doctor-cluster|preflight|doctor-preapply|inventory-check|state-backup|fix-debian-docker-root|secrets-init|secrets-check|tailnet-policy-check|ai-index|ai-check|security-scan|security-zap|security-k8s|security-trivy|security-nuclei|openwrt|nuke}" exit 1 ;; esac diff --git a/scripts/security-scan b/scripts/security-scan new file mode 100755 index 0000000..2cac648 --- /dev/null +++ b/scripts/security-scan @@ -0,0 +1,174 @@ +#!/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 diff --git a/security/README.md b/security/README.md new file mode 100644 index 0000000..e995250 --- /dev/null +++ b/security/README.md @@ -0,0 +1,37 @@ +# Defensive Security Scans + +This directory contains defensive scan configuration for the homelab and public +website. These checks are for assets owned by this lab only. + +Use Jeannie from the Debian homelab host: + +```bash +./jeannie security-scan +./jeannie security-zap +./jeannie security-k8s +``` + +The default `security-scan` runs: + +- Trivy filesystem and IaC/config scans against this repo. +- OWASP ZAP baseline passive scans against `security/targets.txt`. + +The focused commands are: + +- `security-zap`: OWASP ZAP baseline scan only. +- `security-k8s`: kube-bench CIS checks against the kubeadm host. +- `security-trivy`: Trivy repo and IaC/config scans only. +- `security-nuclei`: low-rate nuclei HTTP exposure scans only. + +Reports are written to: + +```text +${HOMELAB_STATE_DIR}/security-reports +``` + +The ZAP scan uses baseline/passive mode. Do not run active attack scans against +public endpoints unless you intentionally schedule a maintenance window and +understand the traffic it will generate. + +`security/targets.txt` is the public target allowlist. Keep it limited to +services you own. diff --git a/security/targets.txt b/security/targets.txt new file mode 100644 index 0000000..f785bfe --- /dev/null +++ b/security/targets.txt @@ -0,0 +1,18 @@ +# Public targets for defensive baseline scans. +# Keep this list scoped to services you own. +https://lab2025.duckdns.org/ +https://lab2025.duckdns.org/git/ +https://demos.lab2025.duckdns.org/ +https://heimdall.lab2025.duckdns.org/ +https://argocd.lab2025.duckdns.org/ +https://grafana.lab2025.duckdns.org/ +https://prometheus.lab2025.duckdns.org/ +https://alertmanager.lab2025.duckdns.org/ +https://n8n.lab2025.duckdns.org/ +https://prowlarr.lab2025.duckdns.org/ +https://sonarr.lab2025.duckdns.org/ +https://radarr.lab2025.duckdns.org/ +https://qbittorrent.lab2025.duckdns.org/ +https://kapowarr.lab2025.duckdns.org/ +https://suwayomi.lab2025.duckdns.org/ +https://maintainerr.lab2025.duckdns.org/