Files
threadnet-operating/monitoring/cve/scan-loop.sh
T

101 lines
4.4 KiB
Bash
Raw Normal View History

#!/bin/sh
# Trivy-Scan-Schleife (gitops#47, seit #0106 mit abgeleiteter Zielmenge).
#
# Die Ziele standen bis 2026-08-21 in cve/images.txt und wurden von Hand
# nachgezogen. Gemessen an dem Tag: 27 von 51 laufenden Images, also 52 % -
# nicht gescannt wurden unter anderem der Web-Client, den jeder Nutzer laedt,
# beide Traefik-Schichten und die Registry selbst. Gleichzeitig lief ein Scan
# gegen threadnet-web:v0.3.0, ein Image, das nirgends mehr laeuft.
#
# Seitdem schreibt der cve-exporter die Soll-Menge nach /targets/targets.txt
# (Quellen: Cluster-Metriken, cAdvisor des Betriebs-Hosts, Registry). Diese
# Datei wird NICHT von Hand gepflegt - siehe ADR-0026.
#
# Kein Abbruch bei Einzelfehlern: Ein nicht mehr pullbares Image darf den Rest
# nicht verhindern; der Exporter alarmiert ueber trivy_last_scan_timestamp,
# wenn ein Report veraltet.
set -u
# Ueber die Umgebung ueberschreibbar - der Exporter fuehrt RESULTS_DIR
# bereits so, und test_scan_loop.sh laedt diese Datei damit.
RESULTS="${RESULTS_DIR:-/results}"
TARGETS="${TARGETS_FILE:-/targets/targets.txt}"
INTERVAL="${SCAN_INTERVAL_SECONDS:-86400}"
# ⚠️ Kurze Wartezeit, wenn die Zielliste (noch) fehlt: Beim ersten Start nach
# einem Ausrollen schreibt der Exporter sie erst beim naechsten Scrape. Mit dem
# vollen 24h-Intervall stuende der Scanner deswegen einen Tag still.
WARTEZEIT="${WARTE_INTERVAL_SECONDS:-60}"
# Trivy ist im Test durch eine Attrappe ersetzbar; hier der echte Aufruf.
scanne() { trivy image --scanners vuln --format json --output "$2" "$1"; }
runde() {
# ⚠️ Ohne brauchbare Zielliste wird NICHTS getan. Der Exporter schreibt sie
# nur, wenn mindestens eine Quelle geliefert hat - faellt die Herleitung
# ganz aus, bleibt die alte Datei stehen und diese Schleife arbeitet
# weiter mit dem letzten bekannten Stand, statt ins Leere zu laufen.
if [ ! -s "$TARGETS" ]; then
echo "warte: $TARGETS fehlt oder ist leer - keine Runde"
return 1
fi
anzahl=0
erwartet=$(mktemp)
while IFS= read -r img; do
case "$img" in ""|\#*) continue;; esac
anzahl=$((anzahl + 1))
safe=$(echo "$img" | tr '/:@' '___')
echo "$safe" >> "$erwartet"
echo "scan: $img"
scanne "$img" "$RESULTS/$safe.json.tmp" \
&& mv "$RESULTS/$safe.json.tmp" "$RESULTS/$safe.json" \
|| echo "FEHLER bei $img (Report bleibt auf altem Stand)"
done < "$TARGETS"
# Berichte ohne Ziel entfernen (#0106, Kriterium 3).
#
# ⚠️ WARUM DAS NOETIG IST: Der Exporter liest JEDE *.json in /results und
# nimmt das Ziel aus Trivys ArtifactName. Ein Bericht, dessen Image aus der
# Soll-Menge gefallen ist, meldet sonst weiter - genau der Zustand vom
# 2026-08-20, als der Security-Raum HIGH-CVEs zu threadnet-web:v0.3.0 meldete,
# das nirgends mehr lief. Rauschen, das wie Signal aussieht.
#
# ⚠️ WARUM ES HIER STEHT UND NICHT WEITER OBEN: Die Wache am Schleifenanfang
# hat bereits sichergestellt, dass eine nicht-leere Zielliste vorliegt. Ohne
# sie wird die Runde ausgesetzt - und damit auch nichts geloescht. Sonst
# raeumte ein Neustart waehrend eines Prometheus-Ausfalls den gesamten
# Bestand ab, und der Verlust faellt erst beim naechsten Ernstfall auf.
entfernt=0
for bericht in "$RESULTS"/*.json; do
[ -e "$bericht" ] || continue
name=$(basename "$bericht" .json)
# Host-rootfs-Scans (Ausbaustufe, Dateiname traegt __host__) stammen nicht
# aus dieser Zielliste und werden deshalb nicht danach beurteilt.
case "$name" in *__host__*) continue;; esac
if ! grep -qxF "$name" "$erwartet"; then
echo "verwaist, entfernt: $name"
rm -f "$bericht"
entfernt=$((entfernt + 1))
fi
done
rm -f "$erwartet"
echo "runde fertig: ${anzahl} Ziele, ${entfernt} verwaiste Berichte entfernt"
}
# ⚠️ Ladewache: test_scan_loop.sh laedt diese Datei, um die ECHTE runde() zu
# pruefen statt eine nachgebaute. Ohne die Wache liefe beim Laden die
# Endlosschleife an, und der Test haette einen Nachbau geprueft - also den
# Nachbau, nicht die Sache.
[ "${SCAN_LOOP_NUR_LADEN:-0}" = "1" ] && return 0
while true; do
# Ausdruecklich als if, nicht als `runde || sleep A && sleep B`: Das gruppiert
# links-assoziativ zu `(runde || sleep A) && sleep B` und schliefe bei
# fehlender Zielliste erst die Wartezeit UND danach das volle Intervall -
# also doch einen Tag, genau das, was die Wartezeit verhindern soll.
if runde; then
sleep "$INTERVAL"
else
sleep "$WARTEZEIT"
fi
done