#!/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