Add web security quick check

This commit is contained in:
juvdiaz 2026-06-29 14:58:35 -06:00
parent e1c33ff42e
commit 5aeb953362
4 changed files with 125 additions and 2 deletions

View File

@ -152,6 +152,10 @@ on first. This command is report-only.
`security-nuclei` `security-nuclei`
: Run low-rate nuclei HTTP exposure scans against public targets. : 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 ### Maintenance
`state-backup` `state-backup`
@ -223,6 +227,9 @@ smoke test. Defaults to `cloudflare.com`.
`SECURITY_NUCLEI_TARGET` `SECURITY_NUCLEI_TARGET`
: Optional single URL override for `security-nuclei`. : Optional single URL override for `security-nuclei`.
`SECURITY_WEB_TARGET`
: Optional single URL override for `security-web`.
`lynis` `lynis`
: `security-host` requires Lynis to be installed on the Debian host. Install it : `security-host` requires Lynis to be installed on the Debian host. Install it
explicitly with `sudo apt install lynis`. explicitly with `sudo apt install lynis`.

10
jeannie
View File

@ -5795,6 +5795,11 @@ security_nuclei() {
"${REPO_ROOT}/scripts/security-scan" nuclei "${REPO_ROOT}/scripts/security-scan" nuclei
} }
security_web() {
require_debian_server "security-web"
"${REPO_ROOT}/scripts/security-scan" web
}
case "${1:-}" in case "${1:-}" in
up) up)
up up
@ -5917,6 +5922,9 @@ case "${1:-}" in
security-nuclei) security-nuclei)
security_nuclei security_nuclei
;; ;;
security-web)
security_web
;;
openwrt) openwrt)
openwrt openwrt
;; ;;
@ -5928,7 +5936,7 @@ case "${1:-}" in
echo "Log: ${JEANNIE_LOG_FILE}" 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 exit 1
;; ;;
esac esac

View File

@ -23,13 +23,14 @@ truthy() {
usage() { usage() {
cat <<'EOF' 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: Environment:
SECURITY_REPORT_DIR Report output directory. SECURITY_REPORT_DIR Report output directory.
SECURITY_TARGETS_FILE Target allowlist for web scans. SECURITY_TARGETS_FILE Target allowlist for web scans.
SECURITY_ZAP_TARGET Single URL override for ZAP. SECURITY_ZAP_TARGET Single URL override for ZAP.
SECURITY_NUCLEI_TARGET Single URL override for nuclei. 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. LAB_BACKSTAGE_BRAIN_ENABLED=true enables LLM-assisted Lynis triage.
EOF EOF
} }
@ -180,6 +181,107 @@ run_nuclei() {
echo "nuclei reports written to ${SECURITY_REPORT_DIR}" 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() { run_kube_bench() {
local stamp local stamp
stamp="$(timestamp)" stamp="$(timestamp)"
@ -395,6 +497,9 @@ case "${1:-all}" in
nuclei) nuclei)
run_nuclei run_nuclei
;; ;;
web)
run_security_web
;;
kube-bench) kube-bench)
run_kube_bench run_kube_bench
;; ;;

View File

@ -11,6 +11,7 @@ Use Jeannie from the Debian homelab host:
./jeannie security-zap ./jeannie security-zap
./jeannie security-k8s ./jeannie security-k8s
./jeannie security-host ./jeannie security-host
./jeannie security-web
``` ```
The default `security-scan` runs: 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. report-only and prints one highest-risk finding to focus on first.
- `security-trivy`: Trivy repo and IaC/config scans only. - `security-trivy`: Trivy repo and IaC/config scans only.
- `security-nuclei`: low-rate nuclei HTTP exposure 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: Reports are written to: