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.
|
||||
|
||||
`security-host`
|
||||
: Run a Lynis host audit on Debian and print only warnings and suggestions.
|
||||
This command is report-only.
|
||||
: Run a Lynis host audit on Debian and print one highest-risk finding to focus
|
||||
on first. This command is report-only.
|
||||
|
||||
`security-trivy`
|
||||
: 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
|
||||
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`
|
||||
: 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}"
|
||||
NUCLEI_IMAGE="${NUCLEI_IMAGE:-projectdiscovery/nuclei:latest}"
|
||||
|
||||
truthy() {
|
||||
case "${1,,}" in
|
||||
1 | true | yes | y | on | enabled)
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
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_ZAP_TARGET Single URL override for ZAP.
|
||||
SECURITY_NUCLEI_TARGET Single URL override for nuclei.
|
||||
LAB_BACKSTAGE_BRAIN_ENABLED=true enables LLM-assisted Lynis triage.
|
||||
EOF
|
||||
}
|
||||
|
||||
|
|
@ -182,34 +194,153 @@ run_kube_bench() {
|
|||
"${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 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
|
||||
|
||||
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
|
||||
printf 'Hardening index: %s\n' "${hardening_index}"
|
||||
fi
|
||||
|
||||
printf '\n-- Warnings\n'
|
||||
if [[ -n "${warnings}" ]]; then
|
||||
printf '%s\n' "${warnings}" | sed 's/^/- /'
|
||||
else
|
||||
printf 'none\n'
|
||||
fi
|
||||
|
||||
printf '\n-- Suggestions\n'
|
||||
if [[ -n "${suggestions}" ]]; then
|
||||
printf '%s\n' "${suggestions}" | sed 's/^/- /'
|
||||
else
|
||||
printf 'none\n'
|
||||
findings_file="$(mktemp)"
|
||||
lynis_findings "${report_file}" >"${findings_file}"
|
||||
if ! print_lynis_llm_focus "${findings_file}"; then
|
||||
print_lynis_fallback_focus "${findings_file}"
|
||||
fi
|
||||
rm -f "${findings_file}"
|
||||
}
|
||||
|
||||
run_lynis_host() {
|
||||
|
|
@ -242,7 +373,7 @@ EOF
|
|||
--report-file "${report_file}" \
|
||||
--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 'Full Lynis log: %s\n' "${log_file}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ The focused commands are:
|
|||
- `security-zap`: OWASP ZAP baseline scan only.
|
||||
- `security-k8s`: kube-bench CIS checks against the kubeadm host.
|
||||
- `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-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
|
||||
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
|
||||
explicit Jeannie command for that specific change.
|
||||
routing. When `LAB_BACKSTAGE_BRAIN_ENABLED=true`, Jeannie asks the local
|
||||
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