monitoring: CVE-Pipeline v1 (gitops#47) - Scanner, Exporter, Regeln, Routing, Dashboard
- cve-scan: Trivy-Loop ueber die 29 real deployten Images (Cluster-Inventur 2026-08-01 + Prod-Web-Image); 24h-Intervall, Fehler einzelner Images blockieren nicht - cve-exporter: Stdlib-Exporter mit first_seen-State (Zeitstrahl), Schema trivy_vuln_info/_count/_first_seen/_last_scan gemaess Pflichtfeldern - 3 Alertregeln (CRITICAL sofort, HIGH mit 24h-Daempfung, Scan-Frische) - promtool SUCCESS 9 rules; alle mit room=security - matrix-alerts: Label-basiertes Raum-Routing (MATRIX_ROOM_<NAME>), Edits landen im richtigen Raum via State - Grafana-Dashboard cve-overview: Severity-Stats, CVE-Tabelle mit NVD-Link/Fix-Version/first-seen, Zeitstrahl, Verlauf UNGETESTET bis zum Deploy auf CFGMON (compose up -d). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
This commit is contained in:
co-authored by
Claude Fable 5
parent
f45e01219b
commit
b6007c50fd
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
# Trivy-JSON -> Prometheus-Metriken (gitops#47). Stdlib-only, Machart wie die
|
||||
# uebrigen Monitoring-Helfer. Liest die Reports des cve-scan-Sidecars und
|
||||
# serviert /metrics; merkt sich je (CVE, Target) den Erstfund (first_seen),
|
||||
# damit der geforderte Zeitstrahl (erstmals gesehen / geschlossen) abbildbar
|
||||
# ist - "geschlossen" = Serie verschwindet, resolved kommt via Alertmanager.
|
||||
#
|
||||
# Schema (Pflichtfelder-Vorgabe sorb: CVE-ID, Mitigation, Zeitstrahl, Ort, Typ):
|
||||
# trivy_vuln_info{cve,severity,target,target_type,host,pkg,installed,fixed_version} 1
|
||||
# (nur HIGH/CRITICAL als Einzelserien - Kardinalitaet)
|
||||
# trivy_vuln_count{target,target_type,host,severity} <n> (alle Severities)
|
||||
# trivy_vuln_first_seen_timestamp{cve,target} <unix>
|
||||
# trivy_last_scan_timestamp{target,target_type,host} <unix>
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
RESULTS = os.environ.get("RESULTS_DIR", "/results")
|
||||
STATE_FILE = os.environ.get("STATE_FILE", "/state/first-seen.json")
|
||||
# Ort: Container-Images laufen (bis auf Weiteres) alle auf dem MATRIX-Host;
|
||||
# Host-rootfs-Scans (Ausbaustufe) bringen ihren Hostnamen im Dateinamen mit.
|
||||
DEFAULT_HOST = os.environ.get("DEFAULT_HOST", "matrix")
|
||||
|
||||
try:
|
||||
with open(STATE_FILE) as f:
|
||||
first_seen = json.load(f) # "cve|target" -> unix-ts
|
||||
except Exception:
|
||||
first_seen = {}
|
||||
|
||||
|
||||
def esc(v):
|
||||
return str(v).replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ")
|
||||
|
||||
|
||||
def collect():
|
||||
lines = []
|
||||
now = int(time.time())
|
||||
seen_keys = set()
|
||||
for fn in sorted(os.listdir(RESULTS)):
|
||||
if not fn.endswith(".json"):
|
||||
continue
|
||||
path = os.path.join(RESULTS, fn)
|
||||
try:
|
||||
rep = json.load(open(path))
|
||||
except Exception:
|
||||
continue
|
||||
target = rep.get("ArtifactName", fn[:-5])
|
||||
ttype = "host" if rep.get("ArtifactType") in ("filesystem", "rootfs") else "image"
|
||||
host = fn.split("__host__")[1].split(".json")[0] if "__host__" in fn else DEFAULT_HOST
|
||||
base = f'target="{esc(target)}",target_type="{ttype}",host="{esc(host)}"'
|
||||
lines.append(f'trivy_last_scan_timestamp{{{base}}} {int(os.path.getmtime(path))}')
|
||||
counts = {}
|
||||
for res in rep.get("Results") or []:
|
||||
for v in res.get("Vulnerabilities") or []:
|
||||
sev = v.get("Severity", "UNKNOWN")
|
||||
counts[sev] = counts.get(sev, 0) + 1
|
||||
if sev in ("HIGH", "CRITICAL"):
|
||||
cve = v.get("VulnerabilityID", "?")
|
||||
key = f"{cve}|{target}"
|
||||
if key not in first_seen:
|
||||
first_seen[key] = now
|
||||
seen_keys.add(key)
|
||||
lines.append(
|
||||
'trivy_vuln_info{cve="%s",severity="%s",%s,pkg="%s",installed="%s",fixed_version="%s"} 1'
|
||||
% (esc(cve), sev, base, esc(v.get("PkgName", "?")),
|
||||
esc(v.get("InstalledVersion", "?")), esc(v.get("FixedVersion", ""))))
|
||||
lines.append(f'trivy_vuln_first_seen_timestamp{{cve="{esc(cve)}",target="{esc(target)}"}} {first_seen[key]}')
|
||||
for sev, n in sorted(counts.items()):
|
||||
lines.append(f'trivy_vuln_count{{{base},severity="{sev}"}} {n}')
|
||||
# State kompakt halten: verschwundene Findings raus (= "geschlossen")
|
||||
for k in list(first_seen):
|
||||
if k not in seen_keys:
|
||||
del first_seen[k]
|
||||
try:
|
||||
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
|
||||
with open(STATE_FILE, "w") as f:
|
||||
json.dump(first_seen, f)
|
||||
except Exception as e:
|
||||
print(f"state save failed: {e}", flush=True)
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
if self.path != "/metrics":
|
||||
self.send_response(404); self.end_headers(); return
|
||||
body = collect().encode()
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "text/plain; version=0.0.4")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
pass
|
||||
|
||||
|
||||
ThreadingHTTPServer(("0.0.0.0", 9101), Handler).serve_forever()
|
||||
@@ -0,0 +1,29 @@
|
||||
clamav/clamav:1.5.3
|
||||
coturn/coturn:latest
|
||||
docker.io/grafana/alloy:v1.7.5
|
||||
docker.io/library/haproxy:3.2-alpine
|
||||
docker.io/library/postgres:17.9-bookworm
|
||||
docker.io/livekit/livekit-server:v1.10.0
|
||||
docker.io/postgres:17-alpine
|
||||
docker.io/prometheuscommunity/postgres-exporter:v0.18.1
|
||||
ghcr.io/element-hq/ess-helm/matrix-tools:0.7.3
|
||||
ghcr.io/element-hq/matrix-authentication-service:1.15.0
|
||||
ghcr.io/fluxcd/helm-controller:v1.5.3
|
||||
ghcr.io/fluxcd/kustomize-controller:v1.8.3
|
||||
ghcr.io/fluxcd/notification-controller:v1.8.3
|
||||
ghcr.io/fluxcd/source-controller:v1.8.2
|
||||
ghcr.io/goauthentik/server:2026.2.3
|
||||
gnuxie/draupnir:v3.1.0
|
||||
nginx:1.26-alpine
|
||||
oci.element.io/element-admin:0.1.11
|
||||
oci.element.io/lk-jwt-service:0.4.2
|
||||
oci.element.io/synapse:v1.151.0-ess.1
|
||||
quay.io/jetstack/cert-manager-cainjector:v1.14.0
|
||||
quay.io/jetstack/cert-manager-controller:v1.14.0
|
||||
quay.io/jetstack/cert-manager-webhook:v1.14.0
|
||||
quay.io/prometheus-operator/prometheus-config-reloader:v0.81.0
|
||||
registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.15.0
|
||||
rohana.axion1337.de/sorb/axion-backup:v2
|
||||
rohana.axion1337.de/sorb/axion-secret-rotation:v1
|
||||
rohana.axion1337.de/sorb/clamav-http-scanner:v1.0.0
|
||||
rohana.axion1337.de/sorb/threadnet-web:v0.3.0
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Trivy-Scan-Schleife (gitops#47): scannt alle 24h die Liste der real deployten
|
||||
# Images (cve/images.txt, aus dem Cluster inventarisiert) und legt die
|
||||
# JSON-Reports fuer den cve-exporter ab. 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
|
||||
RESULTS=/results
|
||||
IMAGES=/config/images.txt
|
||||
INTERVAL="${SCAN_INTERVAL_SECONDS:-86400}"
|
||||
|
||||
while true; do
|
||||
while IFS= read -r img; do
|
||||
case "$img" in ""|\#*) continue;; esac
|
||||
safe=$(echo "$img" | tr '/:@' '___')
|
||||
echo "scan: $img"
|
||||
trivy image --scanners vuln --format json --output "$RESULTS/$safe.json.tmp" "$img" \
|
||||
&& mv "$RESULTS/$safe.json.tmp" "$RESULTS/$safe.json" \
|
||||
|| echo "FEHLER bei $img (Report bleibt auf altem Stand)"
|
||||
done < "$IMAGES"
|
||||
echo "runde fertig, schlafe ${INTERVAL}s"
|
||||
sleep "$INTERVAL"
|
||||
done
|
||||
Reference in New Issue
Block a user