From 5aeb9533626a53fa3bec55f3ba23a252a32587de Mon Sep 17 00:00:00 2001 From: juvdiaz Date: Mon, 29 Jun 2026 14:58:35 -0600 Subject: [PATCH] Add web security quick check --- docs/jeannie.1.md | 7 +++ jeannie | 10 +++- scripts/security-scan | 107 +++++++++++++++++++++++++++++++++++++++++- security/README.md | 3 ++ 4 files changed, 125 insertions(+), 2 deletions(-) diff --git a/docs/jeannie.1.md b/docs/jeannie.1.md index c55103f..90f19b0 100644 --- a/docs/jeannie.1.md +++ b/docs/jeannie.1.md @@ -152,6 +152,10 @@ on first. This command is report-only. `security-nuclei` : Run low-rate nuclei HTTP exposure scans against public targets. +`security-web` +: Run quick HTTPS, status, and defensive-header checks against owned web +targets. + ### Maintenance `state-backup` @@ -223,6 +227,9 @@ smoke test. Defaults to `cloudflare.com`. `SECURITY_NUCLEI_TARGET` : Optional single URL override for `security-nuclei`. +`SECURITY_WEB_TARGET` +: Optional single URL override for `security-web`. + `lynis` : `security-host` requires Lynis to be installed on the Debian host. Install it explicitly with `sudo apt install lynis`. diff --git a/jeannie b/jeannie index c74832c..7ecca1a 100755 --- a/jeannie +++ b/jeannie @@ -5795,6 +5795,11 @@ security_nuclei() { "${REPO_ROOT}/scripts/security-scan" nuclei } +security_web() { + require_debian_server "security-web" + "${REPO_ROOT}/scripts/security-scan" web +} + case "${1:-}" in up) up @@ -5917,6 +5922,9 @@ case "${1:-}" in security-nuclei) security_nuclei ;; + security-web) + security_web + ;; openwrt) openwrt ;; @@ -5928,7 +5936,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|security-scan|security-prepare|security-zap|security-k8s|security-host|security-trivy|security-nuclei|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-prepare|security-zap|security-k8s|security-host|security-trivy|security-nuclei|security-web|openwrt|nuke}" exit 1 ;; esac diff --git a/scripts/security-scan b/scripts/security-scan index feeb257..06f807f 100755 --- a/scripts/security-scan +++ b/scripts/security-scan @@ -23,13 +23,14 @@ truthy() { usage() { cat <<'EOF' -Usage: scripts/security-scan {all|prepare|trivy|zap|kube-bench|nuclei|host} +Usage: scripts/security-scan {all|prepare|trivy|zap|kube-bench|nuclei|host|web} 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. + SECURITY_WEB_TARGET Single URL override for web header checks. LAB_BACKSTAGE_BRAIN_ENABLED=true enables LLM-assisted Lynis triage. EOF } @@ -180,6 +181,107 @@ run_nuclei() { echo "nuclei reports written to ${SECURITY_REPORT_DIR}" } +header_value() { + local headers_file="$1" + local header_name="$2" + + awk -v name="${header_name}" ' + BEGIN { IGNORECASE = 1 } + index($0, name ":") == 1 { + sub(/^[^:]+:[[:space:]]*/, "") + sub(/\r$/, "") + print + exit + } + ' "${headers_file}" +} + +run_security_web() { + local stamp + local target + local safe_name + local headers_file + local body_file + local report_file + local status + local failures=0 + local missing=0 + local -a required_headers=( + "strict-transport-security" + "x-content-type-options" + "x-frame-options" + "referrer-policy" + ) + + stamp="$(timestamp)" + ensure_report_dir + report_file="${SECURITY_REPORT_DIR}/security-web-${stamp}.txt" + + { + echo "Security web quick check" + echo "Generated: ${stamp}" + echo + } >"${report_file}" + + while IFS= read -r target; do + [[ -n "${target}" ]] || continue + safe_name="$(safe_target_name "${target}")" + headers_file="${SECURITY_REPORT_DIR}/headers-${safe_name}-${stamp}.txt" + body_file="${SECURITY_REPORT_DIR}/headers-${safe_name}-${stamp}.body" + + echo "== ${target}" | tee -a "${report_file}" + if [[ "${target}" != https://* ]]; then + echo "fail: target is not HTTPS" | tee -a "${report_file}" + failures=$((failures + 1)) + fi + + status="$( + curl -k -sS -L \ + --connect-timeout "${SECURITY_WEB_CONNECT_TIMEOUT:-8}" \ + --max-time "${SECURITY_WEB_MAX_TIME:-20}" \ + -D "${headers_file}" \ + -o "${body_file}" \ + -w '%{http_code}' \ + "${target}" 2>>"${report_file}" || true + )" + + if [[ ! "${status}" =~ ^2|3[0-9][0-9]$ ]]; then + echo "fail: HTTP status ${status:-curl failed}" | tee -a "${report_file}" + failures=$((failures + 1)) + else + echo "ok: HTTP status ${status}" | tee -a "${report_file}" + fi + + missing=0 + for header_name in "${required_headers[@]}"; do + if [[ -n "$(header_value "${headers_file}" "${header_name}")" ]]; then + echo "ok: ${header_name}" | tee -a "${report_file}" + else + echo "warn: missing ${header_name}" | tee -a "${report_file}" + missing=$((missing + 1)) + fi + done + + if [[ -n "$(header_value "${headers_file}" "content-security-policy")" ]]; then + echo "ok: content-security-policy" | tee -a "${report_file}" + else + echo "info: content-security-policy missing or intentionally deferred" | tee -a "${report_file}" + fi + + if ((missing > 0)); then + failures=$((failures + 1)) + fi + echo | tee -a "${report_file}" >/dev/null + rm -f "${body_file}" + done < <(scan_targets "${SECURITY_WEB_TARGET:-}") + + echo "Security web report: ${report_file}" + if ((failures > 0)); then + echo "security-web found ${failures} target(s) needing review." >&2 + return 1 + fi +} + run_kube_bench() { local stamp stamp="$(timestamp)" @@ -395,6 +497,9 @@ case "${1:-all}" in nuclei) run_nuclei ;; + web) + run_security_web + ;; kube-bench) run_kube_bench ;; diff --git a/security/README.md b/security/README.md index 4dab248..faa149a 100644 --- a/security/README.md +++ b/security/README.md @@ -11,6 +11,7 @@ Use Jeannie from the Debian homelab host: ./jeannie security-zap ./jeannie security-k8s ./jeannie security-host +./jeannie security-web ``` The default `security-scan` runs: @@ -28,6 +29,8 @@ The focused commands are: report-only and prints one highest-risk finding to focus on first. - `security-trivy`: Trivy repo and IaC/config scans only. - `security-nuclei`: low-rate nuclei HTTP exposure scans only. +- `security-web`: quick HTTPS, status, and defensive-header checks for owned + web targets. Reports are written to: