Focus Lynis host audit on highest risk finding
This commit is contained in:
parent
eaf74b50eb
commit
e1c33ff42e
|
|
@ -143,8 +143,8 @@ baseline passive scans for targets in `security/targets.txt`.
|
||||||
: Run kube-bench CIS checks against the Debian kubeadm host.
|
: Run kube-bench CIS checks against the Debian kubeadm host.
|
||||||
|
|
||||||
`security-host`
|
`security-host`
|
||||||
: Run a Lynis host audit on Debian and print only warnings and suggestions.
|
: Run a Lynis host audit on Debian and print one highest-risk finding to focus
|
||||||
This command is report-only.
|
on first. This command is report-only.
|
||||||
|
|
||||||
`security-trivy`
|
`security-trivy`
|
||||||
: Run only Trivy filesystem and IaC/config scans.
|
: Run only Trivy filesystem and IaC/config scans.
|
||||||
|
|
@ -227,6 +227,11 @@ smoke test. Defaults to `cloudflare.com`.
|
||||||
: `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`.
|
||||||
|
|
||||||
|
`LAB_BACKSTAGE_BRAIN_ENABLED=true`
|
||||||
|
: Allows `security-host` to ask the local Ollama/RAG helper to choose the single
|
||||||
|
highest-risk Lynis finding. If the helper is disabled or unavailable, Jeannie
|
||||||
|
falls back to the first Lynis warning, then the first suggestion.
|
||||||
|
|
||||||
`LAB_AUTO_APPROVE=false`
|
`LAB_AUTO_APPROVE=false`
|
||||||
: Disable automatic OpenTofu approval for apply paths that support confirmation.
|
: Disable automatic OpenTofu approval for apply paths that support confirmation.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,17 @@ ZAP_IMAGE="${ZAP_IMAGE:-ghcr.io/zaproxy/zaproxy:stable}"
|
||||||
KUBE_BENCH_IMAGE="${KUBE_BENCH_IMAGE:-aquasec/kube-bench:latest}"
|
KUBE_BENCH_IMAGE="${KUBE_BENCH_IMAGE:-aquasec/kube-bench:latest}"
|
||||||
NUCLEI_IMAGE="${NUCLEI_IMAGE:-projectdiscovery/nuclei:latest}"
|
NUCLEI_IMAGE="${NUCLEI_IMAGE:-projectdiscovery/nuclei:latest}"
|
||||||
|
|
||||||
|
truthy() {
|
||||||
|
case "${1,,}" in
|
||||||
|
1 | true | yes | y | on | enabled)
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
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}
|
||||||
|
|
@ -19,6 +30,7 @@ Environment:
|
||||||
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.
|
||||||
|
LAB_BACKSTAGE_BRAIN_ENABLED=true enables LLM-assisted Lynis triage.
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,34 +194,153 @@ run_kube_bench() {
|
||||||
"${KUBE_BENCH_IMAGE}" run --targets master,node | tee "${SECURITY_REPORT_DIR}/kube-bench-${stamp}.txt"
|
"${KUBE_BENCH_IMAGE}" run --targets master,node | tee "${SECURITY_REPORT_DIR}/kube-bench-${stamp}.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
print_lynis_recommendations() {
|
lynis_findings() {
|
||||||
local report_file="$1"
|
local report_file="$1"
|
||||||
local warnings
|
|
||||||
local suggestions
|
sed -n 's/^warning\[\]=/warning|/p; s/^suggestion\[\]=/suggestion|/p' "${report_file}" 2>/dev/null |
|
||||||
|
awk -F'|' 'NF >= 2 { source = $1; finding = $2; test = $NF; print source "|" finding (NF > 2 ? " [" test "]" : "") }'
|
||||||
|
}
|
||||||
|
|
||||||
|
print_lynis_fallback_focus() {
|
||||||
|
local findings_file="$1"
|
||||||
|
local selected
|
||||||
|
|
||||||
|
selected="$(awk -F'|' '$1 == "warning" { print; exit }' "${findings_file}")"
|
||||||
|
if [[ -z "${selected}" ]]; then
|
||||||
|
selected="$(awk -F'|' '$1 == "suggestion" { print; exit }' "${findings_file}")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${selected}" ]]; then
|
||||||
|
printf '\n== Highest-Risk Lynis Finding ==\n'
|
||||||
|
printf 'No Lynis warnings or suggestions were found.\n'
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '\n== Highest-Risk Lynis Finding ==\n'
|
||||||
|
printf 'Source: %s\n' "$(cut -d'|' -f1 <<<"${selected}")"
|
||||||
|
printf 'Finding: %s\n' "$(cut -d'|' -f2- <<<"${selected}")"
|
||||||
|
printf 'Why this one: LLM triage was unavailable, so Jeannie selected the first Lynis warning, falling back to the first suggestion.\n'
|
||||||
|
printf 'Mode: report-only. Add an explicit narrow Jeannie hardening command before changing host state.\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
print_lynis_llm_focus() {
|
||||||
|
local findings_file="$1"
|
||||||
|
local endpoint="${LAB_AI_GATEWAY_URL:-${LAB_OLLAMA_URL:-http://127.0.0.1:11434}}"
|
||||||
|
local model="${LAB_AI_GATEWAY_MODEL:-qwen2.5:0.5b}"
|
||||||
|
local timeout="${LAB_AI_GATEWAY_TIMEOUT_SECONDS:-20}"
|
||||||
|
local index_dir="${LAB_AI_KNOWLEDGE_INDEX_DIR:-/data/homelab-ai/index}"
|
||||||
|
local context=""
|
||||||
|
|
||||||
|
if ! truthy "${LAB_BACKSTAGE_BRAIN_ENABLED:-false}"; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -s "${index_dir}/index.json" && -x "${REPO_ROOT}/scripts/query-homelab-ai-index" ]]; then
|
||||||
|
context="$("${REPO_ROOT}/scripts/query-homelab-ai-index" --index-dir "${index_dir}" --context-only --limit 4 "Lynis host hardening Debian Docker kubeadm SSH homelab" 2>/dev/null || true)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
LYNIS_FINDINGS="$(head -120 "${findings_file}")" \
|
||||||
|
LYNIS_CONTEXT="${context}" \
|
||||||
|
python3 - "${endpoint}" "${model}" "${timeout}" <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
endpoint, model, timeout = sys.argv[1:4]
|
||||||
|
findings = os.environ.get("LYNIS_FINDINGS", "").strip()
|
||||||
|
context = os.environ.get("LYNIS_CONTEXT", "").strip()
|
||||||
|
if not findings:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
timeout_seconds = int(timeout)
|
||||||
|
except ValueError:
|
||||||
|
timeout_seconds = 20
|
||||||
|
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(f"{endpoint.rstrip('/')}/api/tags", timeout=3) as response:
|
||||||
|
tags = json.loads(response.read().decode("utf-8"))
|
||||||
|
available = {item.get("name", "") for item in tags.get("models", [])}
|
||||||
|
if model not in available and f"{model}:latest" not in available:
|
||||||
|
raise SystemExit(1)
|
||||||
|
except (OSError, TimeoutError, urllib.error.URLError, json.JSONDecodeError):
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
prompt = f"""You are Jeannie, a report-only homelab security triage helper.
|
||||||
|
The host is a Debian personal homelab server running Docker, kubeadm, Gitea,
|
||||||
|
Tailscale, local registry, and public edge routing. Do not recommend broad
|
||||||
|
automatic fixes. Pick exactly ONE Lynis finding to focus on first.
|
||||||
|
|
||||||
|
Rank by risk and operational value. Prefer findings that reduce real exposure
|
||||||
|
without likely breaking SSH, Docker networking, Kubernetes, or remote access.
|
||||||
|
|
||||||
|
Return exactly this format:
|
||||||
|
Finding: <one selected finding>
|
||||||
|
Why highest risk: <one sentence>
|
||||||
|
Next safe action: <one concrete manual validation or narrow remediation>
|
||||||
|
Automation stance: report-only; add an explicit Jeannie command only after review
|
||||||
|
|
||||||
|
Homelab context:
|
||||||
|
{context}
|
||||||
|
|
||||||
|
Lynis findings:
|
||||||
|
{findings}
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"stream": False,
|
||||||
|
"options": {
|
||||||
|
"temperature": 0.1,
|
||||||
|
"num_predict": 220,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
request = urllib.request.Request(
|
||||||
|
f"{endpoint.rstrip('/')}/api/generate",
|
||||||
|
data=json.dumps(payload).encode("utf-8"),
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
method="POST",
|
||||||
|
)
|
||||||
|
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
|
||||||
|
result = json.loads(response.read().decode("utf-8"))
|
||||||
|
except (OSError, TimeoutError, urllib.error.URLError, json.JSONDecodeError):
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
answer = (result.get("response") or "").strip()
|
||||||
|
if not answer:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
print("\n== Highest-Risk Lynis Finding ==")
|
||||||
|
print(answer)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
print_lynis_focus() {
|
||||||
|
local report_file="$1"
|
||||||
|
local findings_file
|
||||||
local hardening_index
|
local hardening_index
|
||||||
|
|
||||||
hardening_index="$(awk -F= '$1 == "hardening_index" { print $2; exit }' "${report_file}" 2>/dev/null || true)"
|
hardening_index="$(awk -F= '$1 == "hardening_index" { print $2; exit }' "${report_file}" 2>/dev/null || true)"
|
||||||
warnings="$(sed -n 's/^warning\[\]=//p' "${report_file}" 2>/dev/null | awk -F'|' '{ print $1 (NF > 1 ? " [" $NF "]" : "") }')"
|
|
||||||
suggestions="$(sed -n 's/^suggestion\[\]=//p' "${report_file}" 2>/dev/null | awk -F'|' '{ print $1 (NF > 1 ? " [" $NF "]" : "") }')"
|
|
||||||
|
|
||||||
printf '\n== Lynis Host Recommendations ==\n'
|
printf '\n== Lynis Host Audit ==\n'
|
||||||
if [[ -n "${hardening_index}" ]]; then
|
if [[ -n "${hardening_index}" ]]; then
|
||||||
printf 'Hardening index: %s\n' "${hardening_index}"
|
printf 'Hardening index: %s\n' "${hardening_index}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '\n-- Warnings\n'
|
findings_file="$(mktemp)"
|
||||||
if [[ -n "${warnings}" ]]; then
|
lynis_findings "${report_file}" >"${findings_file}"
|
||||||
printf '%s\n' "${warnings}" | sed 's/^/- /'
|
if ! print_lynis_llm_focus "${findings_file}"; then
|
||||||
else
|
print_lynis_fallback_focus "${findings_file}"
|
||||||
printf 'none\n'
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '\n-- Suggestions\n'
|
|
||||||
if [[ -n "${suggestions}" ]]; then
|
|
||||||
printf '%s\n' "${suggestions}" | sed 's/^/- /'
|
|
||||||
else
|
|
||||||
printf 'none\n'
|
|
||||||
fi
|
fi
|
||||||
|
rm -f "${findings_file}"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_lynis_host() {
|
run_lynis_host() {
|
||||||
|
|
@ -242,7 +373,7 @@ EOF
|
||||||
--report-file "${report_file}" \
|
--report-file "${report_file}" \
|
||||||
--log-file "${log_file}" >/dev/null
|
--log-file "${log_file}" >/dev/null
|
||||||
|
|
||||||
print_lynis_recommendations "${report_file}"
|
print_lynis_focus "${report_file}"
|
||||||
printf '\nFull Lynis report: %s\n' "${report_file}"
|
printf '\nFull Lynis report: %s\n' "${report_file}"
|
||||||
printf 'Full Lynis log: %s\n' "${log_file}"
|
printf 'Full Lynis log: %s\n' "${log_file}"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ The focused commands are:
|
||||||
- `security-zap`: OWASP ZAP baseline scan only.
|
- `security-zap`: OWASP ZAP baseline scan only.
|
||||||
- `security-k8s`: kube-bench CIS checks against the kubeadm host.
|
- `security-k8s`: kube-bench CIS checks against the kubeadm host.
|
||||||
- `security-host`: Lynis host audit summary for Debian. This command is
|
- `security-host`: Lynis host audit summary for Debian. This command is
|
||||||
report-only and prints warnings/suggestions.
|
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.
|
||||||
|
|
||||||
|
|
@ -44,5 +44,8 @@ services you own.
|
||||||
|
|
||||||
`security-host` intentionally does not self-fix Lynis findings. Some hardening
|
`security-host` intentionally does not self-fix Lynis findings. Some hardening
|
||||||
recommendations can break SSH access, Docker networking, kubeadm, or public edge
|
recommendations can break SSH access, Docker networking, kubeadm, or public edge
|
||||||
routing. Use Lynis output to choose one narrow remediation at a time, then add an
|
routing. When `LAB_BACKSTAGE_BRAIN_ENABLED=true`, Jeannie asks the local
|
||||||
explicit Jeannie command for that specific change.
|
Ollama/RAG helper to select exactly one finding; otherwise it falls back to the
|
||||||
|
first Lynis warning, then the first suggestion. The full Lynis report is still
|
||||||
|
saved so you can review the complete output before adding any explicit hardening
|
||||||
|
command.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue