#!/bin/sh # security-scan.sh -- Sicherheitspruefungen als reiner Bericht (quick-261009-p0m). # # Zweck: Die Pipeline (Job `security`) und jeder Entwickler-Rechner pruefen mit # denselben, fest angepinnten Werkzeugen den eingecheckten Stand von Tessera. # NUR BERICHT -- das Skript bricht nie ab und endet in jedem Modus mit Exit 0. # Gefundene Probleme erscheinen als Zeilen `SECURITY-SUMMARY ...` im Protokoll # und in {Berichte}/summary.txt; die Rohberichte (JSON) liegen im Berichtsordner. # # Pruefungen: # gitleaks gesamte Git-Historie auf Zugangsdaten (--redact, Ausnahmen in .gitleaks.toml) # pnpm audit bekannte Schwachstellen der Produktions-Abhaengigkeiten # osv-scanner pnpm-lock.yaml und apps/desktop/src-tauri/Cargo.lock # semgrep statische Code-Analyse (Ausnahmen in .semgrepignore); laeuft im # offiziellen Semgrep-Abbild, das per Digest angepinnt ist # trivy fs Abhaengigkeiten, Dockerfile-Einstellungen, Zugangsdaten im Quellstand # trivy image die frisch gebauten Abbilder api und web (nur main und Tags v*) # # Quellstand: Die Quellpruefungen lesen einen `git archive HEAD`-Export in einem # Arbeitsordner, gitleaks liest nur die Git-Historie. Damit sieht jeder Lauf genau # den eingecheckten Stand; nicht eingecheckte oder private Dateien eines # Arbeitsordners erreichen nie ein Werkzeug, einen Bericht oder ein Artefakt. # # Modi: # install Werkzeuge laden, Pruefsumme (SHA256) gegen das Literal im Skript # vergleichen, auspacken; Fehler ueberspringen nur das Werkzeug # run alle Pruefungen ausfuehren, Zusammenfassung schreiben # all install, dann run (so ruft die Pipeline es auf) # --print-plan Werkzeuge, Abbilder und Berichtsordner anzeigen (ohne Netz) # # Umgebungsvariablen: # GITHUB_REF Anlass (refs/heads/main -> Abbilder :beta, refs/tags/v* -> :live, # sonst keine Abbilder); setzt der Runner selbst # SCAN_IMAGES Abbilder (durch Leerzeichen getrennt) statt des Plans aus GITHUB_REF # REGISTRY Abbild-Praefix (Standard localhost:3002/schalli/tessera-ctl) # SECURITY_REPORT_DIR Berichtsordner (Standard security-reports im aktuellen Ordner) # SECURITY_TOOL_DIR Werkzeugordner (Standard: /opt/hostedtoolcache/tessera-security, # wenn beschreibbar, sonst ~/.cache/tessera-security) # # Lokale Aufrufe: # sh .gitea/scripts/security-scan.sh --print-plan # GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh all # SCAN_IMAGES="tessera-ctl-api:latest" sh .gitea/scripts/security-scan.sh run # # Werkzeug-Versionen anheben: neue Version UND die SHA256-Summe aus der offiziellen # Pruefsummendatei der Veroeffentlichung unten eintragen (docs/ci-cd-setup.md). # # Zeitplan: Der Job der Pipeline hat 30 Minuten (timeout-minutes). Das Skript haelt sich # selbst an ein Gesamtbudget von 25 Minuten (BUDGET_SECONDS = 1500) und gibt die Zeile # `SECURITY-SUMMARY ...` JE WERKZEUG aus, sobald es fertig ist -- bricht der Runner den # Job doch einmal ab, stehen die bis dahin fertigen Ergebnisse im Protokoll. Jedes # Werkzeug hat ein eigenes Zeitlimit (T_*); die Limits ergeben zusammen hoechstens 1500 s, # und `tmo` kuerzt jedes Limit auf die noch verbleibende Gesamtzeit. Ein Werkzeug, das # sein Limit reisst, wird als `skipped reason=zeitgrenze` gemeldet, eines, fuer das das # Gesamtbudget nicht mehr reicht, als `skipped reason=gesamtbudget`. # # Dieses Skript kennt kein Secret und gibt keines aus: Zeilen und Zusammenfassung # enthalten nur Zaehlwerte und Statuswoerter. # Bewusst kein `set -e`: Jedes Werkzeug laeuft geschuetzt, jeder Fehler ist erwartet. set -u GITLEAKS_VERSION=8.30.1 GITLEAKS_URL="https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz" GITLEAKS_SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb TRIVY_VERSION=0.75.0 TRIVY_URL="https://github.com/aquasecurity/trivy/releases/download/v0.75.0/trivy_0.75.0_Linux-64bit.tar.gz" TRIVY_SHA256=c6e65abddb348e25f10549df887045629cf28cc72453cd1c63acb717316b3f3f OSV_VERSION=2.6.0 OSV_URL="https://github.com/google/osv-scanner/releases/download/v2.6.0/osv-scanner_linux_amd64" OSV_SHA256=ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108 # Semgrep laeuft als Container, angepinnt per Digest (Index des Abbilds semgrep/semgrep:1.180.0, # 2026-10-09 geholt). Docker prueft beim Laden, dass der Inhalt zum Digest passt; kein # Paketindex, keine frei aufgeloesten Abhaengigkeiten. SEMGREP_VERSION=1.180.0 SEMGREP_IMAGE=semgrep/semgrep@sha256:529ee8a277ec8adc5b534d7c74eea0a47e9de21d62852b6ba7ac6ba9566845c3 PNPM_VERSION=9.15.0 # Zeitplan in Sekunden (siehe Kopf). Summe Installation: 3 x 60 + 150 + 45 = 375; # Summe Pruefungen: 180 + 90 + 120 + 300 + 180 + 2 x 120 = 1110; zusammen 1485 <= 1500. BUDGET_SECONDS=1500 T_DOWNLOAD=60 T_SEMGREP_PULL=150 T_COREPACK=45 T_GITLEAKS=180 T_AUDIT=90 T_OSV=120 T_SEMGREP=300 T_TRIVY_FS=180 T_TRIVY_IMAGE=120 START_EPOCH=$(date +%s) REGISTRY="${REGISTRY:-localhost:3002/schalli/tessera-ctl}" REF="${GITHUB_REF:-}" MODE="${1:-}" # Aufrufordner merken, dann in die Wurzel des Repositorys wechseln (Skript liegt # in .gitea/scripts/). START_DIR="$(pwd)" ROOT="$(cd "$(dirname "$0")/../.." 2>/dev/null && pwd)" [ -n "$ROOT" ] || ROOT="$START_DIR" # Berichtsordner absolut machen (relativ zum Aufrufordner). REPORT="${SECURITY_REPORT_DIR:-security-reports}" case "$REPORT" in /*) ;; *) REPORT="$START_DIR/$REPORT" ;; esac # Werkzeugordner. if [ -n "${SECURITY_TOOL_DIR:-}" ]; then TOOLDIR="$SECURITY_TOOL_DIR" elif [ -d /opt/hostedtoolcache ] && [ -w /opt/hostedtoolcache ]; then TOOLDIR=/opt/hostedtoolcache/tessera-security else TOOLDIR="${XDG_CACHE_HOME:-${HOME:-/tmp}/.cache}/tessera-security" fi case "$TOOLDIR" in /*) ;; *) TOOLDIR="$START_DIR/$TOOLDIR" ;; esac GITLEAKS_BIN="$TOOLDIR/gitleaks-$GITLEAKS_VERSION/gitleaks" TRIVY_BIN="$TOOLDIR/trivy-$TRIVY_VERSION/trivy" OSV_BIN="$TOOLDIR/osv-scanner-$OSV_VERSION/osv-scanner" COREPACK_HOME_DIR="$TOOLDIR/corepack" TRIVY_CACHE="$TOOLDIR/trivy-cache" # --- Abbilder je Anlass ------------------------------------------------------ if [ -n "${SCAN_IMAGES:-}" ]; then IMAGES="$SCAN_IMAGES" else case "$REF" in refs/heads/main) IMAGES="$REGISTRY/api:beta $REGISTRY/web:beta" ;; refs/tags/v*) IMAGES="$REGISTRY/api:live $REGISTRY/web:live" ;; *) IMAGES="" ;; esac fi # Name fuer Berichtsdatei und Zusammenfassung: letztes Pfadstueck vor dem Doppelpunkt. image_name() { _n="${1##*/}" printf '%s' "${_n%%:*}" } print_plan() { echo "werkzeug gitleaks $GITLEAKS_VERSION" echo "werkzeug trivy $TRIVY_VERSION" echo "werkzeug osv-scanner $OSV_VERSION" echo "werkzeug semgrep $SEMGREP_VERSION (Abbild $SEMGREP_IMAGE)" echo "zeitbudget ${BUDGET_SECONDS}s" echo "werkzeug pnpm $PNPM_VERSION" echo "werkzeugordner $TOOLDIR" if [ -n "$IMAGES" ]; then for _i in $IMAGES; do echo "scan-image $_i" done else echo "scan-image keine (nur main und Tags v*)" fi echo "berichte $REPORT" } if [ "$MODE" = "--print-plan" ]; then print_plan exit 0 fi case "$MODE" in install | run | all) ;; *) echo "Aufruf: sh .gitea/scripts/security-scan.sh install|run|all|--print-plan (nur Bericht, Exit 0)" exit 0 ;; esac # --- Hilfsfunktionen --------------------------------------------------------- WORK="" SEMGREP_CT="" cleanup() { [ -n "$SEMGREP_CT" ] && docker rm -f "$SEMGREP_CT" >/dev/null 2>&1 [ -n "$WORK" ] && rm -rf "$WORK" return 0 } trap cleanup EXIT trap 'exit 0' INT TERM # Laufzeitgrenze je Werkzeug, falls `timeout` vorhanden ist. Das Limit wird auf die noch # verbleibende Gesamtzeit gekuerzt; ist das Gesamtbudget aufgebraucht, laeuft nichts mehr # (Rueckgabe 97). Rueckgabe 124 = Limit gerissen (von `timeout`). BUDGET_RC=97 tmo() { _secs="$1" shift _left=$((START_EPOCH + BUDGET_SECONDS - $(date +%s))) if [ "$_left" -le 5 ]; then return "$BUDGET_RC" fi [ "$_secs" -gt "$_left" ] && _secs="$_left" if command -v timeout >/dev/null 2>&1; then timeout "$_secs" "$@" else "$@" fi } skipline() { echo "SECURITY-SUMMARY $1 skipped reason=$2" } # Statuswort je Werkzeug (fuer die Zusammenfassung, falls kein Bericht entsteht). setstatus() { [ -n "$WORK" ] && [ -d "$WORK/status" ] && printf '%s\n' "$2" >"$WORK/status/$1" return 0 } # Rueckgabewert eines Werkzeugs auswerten: Zeitgrenze oder Gesamtbudget -> Statuswort, # ein evtl. halbfertiger Bericht wird verworfen. $1 Rueckgabewert, $2 Werkzeug, $3 Bericht. note_rc() { case "$1" in 124) setstatus "$2" zeitgrenze rm -f "$3" ;; "$BUDGET_RC") setstatus "$2" gesamtbudget rm -f "$3" ;; esac return 0 } sha_ok() { # $1 Datei, $2 erwartete SHA256 [ -f "$1" ] || return 1 printf '%s %s\n' "$2" "$1" | sha256sum -c - >/dev/null 2>&1 } # Archiv laden und pruefen: $1 Name, $2 URL, $3 SHA256, $4 Zieldatei. # Rueckgabe 0 = Datei liegt geprueft vor; sonst Statuswort in FETCH_REASON. FETCH_REASON="" fetch_verified() { if sha_ok "$4" "$3"; then return 0 fi rm -f "$4" "$4.part" if ! curl -fsSL --retry 2 --connect-timeout 20 --max-time "$T_DOWNLOAD" -o "$4.part" "$2" 2>/dev/null; then rm -f "$4.part" FETCH_REASON=download return 1 fi if ! sha_ok "$4.part" "$3"; then rm -f "$4.part" FETCH_REASON=checksum return 1 fi mv "$4.part" "$4" return 0 } # Binaerwerkzeug aus tar.gz: $1 Name, $2 Version, $3 URL, $4 SHA256, $5 Binaerdatei im Archiv. install_tar_tool() { _dir="$TOOLDIR/$1-$2" _dl="$TOOLDIR/dl" _archive="$_dl/${3##*/}" mkdir -p "$_dir" "$_dl" 2>/dev/null || { skipline "$1" ordner return 0 } if ! fetch_verified "$1" "$3" "$4" "$_archive"; then skipline "$1" "$FETCH_REASON" return 0 fi # Bei jedem Lauf frisch aus dem geprueften Archiv auspacken. rm -f "$_dir/$5" if ! tar -xzf "$_archive" -C "$_dir" "$5" 2>/dev/null || [ ! -x "$_dir/$5" ]; then skipline "$1" auspacken return 0 fi echo "Werkzeug $1 $2 bereit (Pruefsumme stimmt)" } install_osv() { _dir="$TOOLDIR/osv-scanner-$OSV_VERSION" _dl="$TOOLDIR/dl" _file="$_dl/osv-scanner_linux_amd64-$OSV_VERSION" mkdir -p "$_dir" "$_dl" 2>/dev/null || { skipline osv-scanner ordner return 0 } if ! fetch_verified osv-scanner "$OSV_URL" "$OSV_SHA256" "$_file"; then skipline osv-scanner "$FETCH_REASON" return 0 fi rm -f "$_dir/osv-scanner" if ! cp "$_file" "$_dir/osv-scanner" 2>/dev/null || ! chmod +x "$_dir/osv-scanner"; then skipline osv-scanner auspacken return 0 fi echo "Werkzeug osv-scanner $OSV_VERSION bereit (Pruefsumme stimmt)" } install_semgrep() { if ! command -v docker >/dev/null 2>&1; then skipline semgrep kein-docker return 0 fi if docker image inspect "$SEMGREP_IMAGE" >/dev/null 2>&1; then echo "Werkzeug semgrep $SEMGREP_VERSION bereit (Abbild vorhanden, Digest angepinnt)" return 0 fi if tmo "$T_SEMGREP_PULL" docker pull -q "$SEMGREP_IMAGE" >/dev/null 2>&1 && docker image inspect "$SEMGREP_IMAGE" >/dev/null 2>&1; then echo "Werkzeug semgrep $SEMGREP_VERSION bereit (Abbild geladen, Digest angepinnt)" else skipline semgrep install fi } # pnpm: eine pnpm 9.15.x im PATH, sonst corepack mit angepinnter Version. PNPM_MODE="" PNPM_CHECKED=no detect_pnpm() { [ "$PNPM_CHECKED" = yes ] && [ -n "$PNPM_MODE" ] && return 0 [ "$PNPM_CHECKED" = yes ] && return 1 PNPM_CHECKED=yes PNPM_MODE="" if command -v pnpm >/dev/null 2>&1; then _v="$(cd "$TOOLDIR" 2>/dev/null && pnpm --version 2>/dev/null)" case "$_v" in 9.15.*) PNPM_MODE=path return 0 ;; esac fi if command -v corepack >/dev/null 2>&1; then _v="$(COREPACK_HOME="$COREPACK_HOME_DIR" COREPACK_ENABLE_DOWNLOAD_PROMPT=0 tmo "$T_COREPACK" corepack "pnpm@$PNPM_VERSION" --version 2>/dev/null)" if [ "$_v" = "$PNPM_VERSION" ]; then PNPM_MODE=corepack return 0 fi fi return 1 } # pnpm im aktuellen Ordner ausfuehren (nach detect_pnpm). run_pnpm() { if [ "$PNPM_MODE" = path ]; then tmo "$T_AUDIT" pnpm "$@" else COREPACK_HOME="$COREPACK_HOME_DIR" COREPACK_ENABLE_DOWNLOAD_PROMPT=0 tmo "$T_AUDIT" corepack "pnpm@$PNPM_VERSION" "$@" fi } do_install() { case "$(uname -m 2>/dev/null)" in x86_64 | amd64) ;; *) for _t in gitleaks trivy osv-scanner; do skipline "$_t" plattform; done ;; esac if [ "$(uname -m 2>/dev/null)" = x86_64 ] || [ "$(uname -m 2>/dev/null)" = amd64 ]; then install_tar_tool gitleaks "$GITLEAKS_VERSION" "$GITLEAKS_URL" "$GITLEAKS_SHA256" gitleaks install_tar_tool trivy "$TRIVY_VERSION" "$TRIVY_URL" "$TRIVY_SHA256" trivy install_osv fi install_semgrep if detect_pnpm; then echo "Werkzeug pnpm $PNPM_VERSION bereit ($PNPM_MODE)" else skipline pnpm-audit-prod kein-pnpm fi } # --- Pruefungen -------------------------------------------------------------- # Jede Pruefung gibt ihre Zeile `SECURITY-SUMMARY ...` aus, sobald sie fertig ist # (report_tool), damit ein Abbruch des ganzen Jobs die fertigen Ergebnisse nicht verschluckt. scan_gitleaks() { if [ ! -x "$GITLEAKS_BIN" ]; then setstatus gitleaks nicht-installiert report_tool gitleaks return 0 fi # Ein flacher Klon enthaelt nur einen Teil der Historie: das Ergebnis waere "sauber", # ohne dass alle Commits geprueft wurden. Der Lauf wird trotzdem gemacht, aber als # unvollstaendig gemeldet. if [ "$(git -C "$ROOT" rev-parse --is-shallow-repository 2>/dev/null)" = true ]; then setstatus gitleaks flacher-klon else setstatus gitleaks fehler fi ( cd "$ROOT" && tmo "$T_GITLEAKS" "$GITLEAKS_BIN" git --config "$ROOT/.gitleaks.toml" --redact --no-banner \ --exit-code 0 --report-format json --report-path "$REPORT/gitleaks.json" . >/dev/null 2>&1 ) note_rc $? gitleaks "$REPORT/gitleaks.json" report_tool gitleaks return 0 } scan_pnpm_audit() { if ! detect_pnpm; then setstatus pnpm-audit-prod kein-pnpm report_tool pnpm-audit-prod return 0 fi setstatus pnpm-audit-prod fehler ( cd "$WORK/src" && run_pnpm audit --prod --json >"$REPORT/pnpm-audit-prod.json" 2>/dev/null ) note_rc $? pnpm-audit-prod "$REPORT/pnpm-audit-prod.json" report_tool pnpm-audit-prod return 0 } scan_osv() { if [ ! -x "$OSV_BIN" ]; then setstatus osv-scanner nicht-installiert report_tool osv-scanner return 0 fi setstatus osv-scanner fehler ( cd "$WORK/src" && tmo "$T_OSV" "$OSV_BIN" scan source --lockfile pnpm-lock.yaml \ --lockfile apps/desktop/src-tauri/Cargo.lock \ --format json --output "$REPORT/osv-scanner.json" >/dev/null 2>&1 ) note_rc $? osv-scanner "$REPORT/osv-scanner.json" report_tool osv-scanner return 0 } # Semgrep im angepinnten Container: der Quellstand wird mit `docker cp` hinein und der # Bericht heraus kopiert (kein Ordner-Mount, denn der Pfad der Pipeline liegt im # Job-Container, nicht auf dem Docker-Rechner). scan_semgrep() { if ! command -v docker >/dev/null 2>&1; then setstatus semgrep kein-docker report_tool semgrep return 0 fi if ! docker image inspect "$SEMGREP_IMAGE" >/dev/null 2>&1; then setstatus semgrep nicht-installiert report_tool semgrep return 0 fi setstatus semgrep fehler SEMGREP_CT="tessera-semgrep-$$" if docker create --name "$SEMGREP_CT" -w /src \ -e SEMGREP_SEND_METRICS=off -e SEMGREP_ENABLE_VERSION_CHECK=0 \ "$SEMGREP_IMAGE" semgrep scan --config p/default --config p/typescript \ --config p/nodejs --config p/secrets --config p/dockerfile \ --metrics=off --quiet --timeout 20 --json --output /tmp/semgrep.json . >/dev/null 2>&1 && docker cp "$WORK/src/." "$SEMGREP_CT:/src" >/dev/null 2>&1; then tmo "$T_SEMGREP" docker start -a "$SEMGREP_CT" >/dev/null 2>&1 _rc=$? if [ "$_rc" = 124 ] || [ "$_rc" = "$BUDGET_RC" ]; then note_rc "$_rc" semgrep "$REPORT/semgrep.json" else docker cp "$SEMGREP_CT:/tmp/semgrep.json" "$REPORT/semgrep.json" >/dev/null 2>&1 fi fi docker rm -f "$SEMGREP_CT" >/dev/null 2>&1 SEMGREP_CT="" report_tool semgrep return 0 } # Trivy: Zugangsdaten-Suche ueberspringt nur den Ordner mit den Testschluesseln # des Zertifikatsmanagers (Konfiguration entsteht im Arbeitsordner, nicht im Repository). write_trivy_secret_config() { cat >"$WORK/trivy-secret.yaml" <<'EOF' allow-rules: - id: tessera-zertifikatsmanager-testschluessel description: Testschluessel des Zertifikatsmanagers (absichtlich im Repository) path: ^apps/api/src/cert-manager/__fixtures__/ EOF } scan_trivy_fs() { if [ ! -x "$TRIVY_BIN" ]; then setstatus trivy-fs nicht-installiert report_tool trivy-fs return 0 fi setstatus trivy-fs fehler write_trivy_secret_config mkdir -p "$TRIVY_CACHE" 2>/dev/null ( cd "$WORK/src" && tmo "$T_TRIVY_FS" "$TRIVY_BIN" fs --cache-dir "$TRIVY_CACHE" --quiet --no-progress --skip-version-check \ --scanners vuln,misconfig,secret --secret-config "$WORK/trivy-secret.yaml" \ --exit-code 0 --format json --output "$REPORT/trivy-fs.json" . >/dev/null 2>&1 ) note_rc $? trivy-fs "$REPORT/trivy-fs.json" report_tool trivy-fs return 0 } scan_trivy_images() { if [ -z "$IMAGES" ]; then emit "SECURITY-SUMMARY trivy-image skipped reason=kein-abbild-fuer-diesen-ref" return 0 fi for _img in $IMAGES; do _name="$(image_name "$_img")" _tool="trivy-image-$_name" if [ ! -x "$TRIVY_BIN" ]; then setstatus "$_tool" nicht-installiert report_tool "$_tool" continue fi if ! command -v docker >/dev/null 2>&1 || ! docker image inspect "$_img" >/dev/null 2>&1; then setstatus "$_tool" abbild-fehlt report_tool "$_tool" continue fi setstatus "$_tool" fehler mkdir -p "$TRIVY_CACHE" 2>/dev/null tmo "$T_TRIVY_IMAGE" "$TRIVY_BIN" image --image-src docker --cache-dir "$TRIVY_CACHE" --quiet --no-progress \ --skip-version-check --scanners vuln,secret --exit-code 0 --format json \ --output "$REPORT/$_tool.json" "$_img" >/dev/null 2>&1 note_rc $? "$_tool" "$REPORT/$_tool.json" report_tool "$_tool" done return 0 } # Zeile ins Protokoll UND (sobald der Berichtsordner steht) nach summary.txt. emit() { echo "$1" [ -n "$WORK" ] && [ -d "$REPORT" ] && echo "$1" >>"$REPORT/summary.txt" return 0 } # Auswertung EINES Werkzeugs (nur Zaehlwerte): Hilfsprogramm im Arbeitsordner, damit es # nach jedem Werkzeug einzeln aufgerufen werden kann. write_summary_helper() { cat >"$WORK/summ.py" <<'EOF' import json, os, sys rep, st, tool = sys.argv[1], sys.argv[2], sys.argv[3] def load(name): try: with open(os.path.join(rep, name), encoding="utf-8") as f: return json.load(f) except Exception: return None def status(): try: with open(os.path.join(st, tool), encoding="utf-8") as f: return f.read().split()[0] except Exception: return "kein-bericht" def skip(): return "SECURITY-SUMMARY %s skipped reason=%s" % (tool, status()) def sev_counts(results, key="Vulnerabilities"): c = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0} for r in results or []: for v in r.get(key) or []: s = str(v.get("Severity", "")).upper() if s in c: c[s] += 1 return c line = None if tool == "gitleaks": d = load("gitleaks.json") if isinstance(d, list): line = "SECURITY-SUMMARY gitleaks findings=%d" % len(d) if status() == "flacher-klon": line += " unvollstaendig (flacher Klon: nur die sichtbaren Commits geprueft)" elif tool == "pnpm-audit-prod": d = load("pnpm-audit-prod.json") m = d.get("metadata", {}).get("vulnerabilities") if isinstance(d, dict) else None if isinstance(m, dict): line = ( "SECURITY-SUMMARY pnpm-audit-prod critical=%d high=%d moderate=%d low=%d" % (m.get("critical", 0), m.get("high", 0), m.get("moderate", 0), m.get("low", 0)) ) elif tool == "osv-scanner": d = load("osv-scanner.json") if isinstance(d, dict) and isinstance(d.get("results"), list): pk = set() for r in d["results"]: for p in r.get("packages") or []: if p.get("vulnerabilities"): x = p.get("package") or {} pk.add((x.get("ecosystem"), x.get("name"), x.get("version"))) line = "SECURITY-SUMMARY osv-scanner packages=%d" % len(pk) elif tool == "semgrep": d = load("semgrep.json") if isinstance(d, dict) and isinstance(d.get("results"), list): line = "SECURITY-SUMMARY semgrep findings=%d errors=%d" % ( len(d["results"]), len(d.get("errors") or []), ) elif tool == "trivy-fs": d = load("trivy-fs.json") if isinstance(d, dict): res = d.get("Results") or [] c = sev_counts(res) mis = sum(len(r.get("Misconfigurations") or []) for r in res) sec = sum(len(r.get("Secrets") or []) for r in res) line = ( "SECURITY-SUMMARY trivy-fs critical=%d high=%d medium=%d low=%d misconfig=%d secrets=%d" % (c["CRITICAL"], c["HIGH"], c["MEDIUM"], c["LOW"], mis, sec) ) elif tool.startswith("trivy-image-"): d = load(tool + ".json") if isinstance(d, dict): c = sev_counts(d.get("Results")) line = "SECURITY-SUMMARY %s critical=%d high=%d medium=%d low=%d" % ( tool, c["CRITICAL"], c["HIGH"], c["MEDIUM"], c["LOW"], ) print(line or skip()) EOF } report_tool() { if [ -n "$WORK" ] && [ -f "$WORK/summ.py" ] && command -v python3 >/dev/null 2>&1; then _line="$(python3 -I "$WORK/summ.py" "$REPORT" "$WORK/status" "$1" 2>/dev/null)" [ -n "$_line" ] || _line="SECURITY-SUMMARY $1 skipped reason=kein-bericht" emit "$_line" else emit "SECURITY-SUMMARY $1 skipped reason=kein-python" fi return 0 } finish_line() { emit "SECURITY-SUMMARY dauer=$(($(date +%s) - START_EPOCH))s von ${BUDGET_SECONDS}s" emit "SECURITY-SUMMARY fertig (nur Bericht, Exit 0)" } do_run() { if ! mkdir -p "$REPORT" 2>/dev/null; then skipline berichtsordner nicht-beschreibbar echo "SECURITY-SUMMARY fertig (nur Bericht, Exit 0)" return 0 fi # Alte Berichte eines frueheren Laufs entfernen, damit keine Zahlen veraltet wirken. rm -f "$REPORT"/gitleaks.json "$REPORT"/pnpm-audit-prod.json "$REPORT"/osv-scanner.json \ "$REPORT"/semgrep.json "$REPORT"/trivy-fs.json "$REPORT"/trivy-image-*.json "$REPORT"/summary.txt WORK="$(mktemp -d 2>/dev/null)" || WORK="" if [ -z "$WORK" ]; then skipline arbeitsordner nicht-anlegbar echo "SECURITY-SUMMARY fertig (nur Bericht, Exit 0)" return 0 fi mkdir -p "$WORK/src" "$WORK/status" write_summary_helper # git darf in einem Ordner arbeiten, der einem anderen Benutzer gehoert (CI-Container # als root, lokaler Klon als Entwickler). _n="${GIT_CONFIG_COUNT:-0}" eval "export GIT_CONFIG_KEY_$_n=safe.directory GIT_CONFIG_VALUE_$_n=\"\$ROOT\"" export GIT_CONFIG_COUNT=$((_n + 1)) EXPORT_OK=no if (cd "$ROOT" && git archive HEAD) 2>/dev/null | tar -x -C "$WORK/src" 2>/dev/null; then EXPORT_OK=yes fi scan_gitleaks if [ "$EXPORT_OK" = yes ]; then scan_pnpm_audit scan_osv scan_semgrep scan_trivy_fs else for _t in pnpm-audit-prod osv-scanner semgrep trivy-fs; do setstatus "$_t" export report_tool "$_t" done fi scan_trivy_images # Layer-Zwischenspeicher von Trivy waechst stark; die Datenbank bleibt. rm -rf "$TRIVY_CACHE/fanal" 2>/dev/null finish_line echo "Berichte: $REPORT (in der Pipeline als Artefakt sicherheitsberichte)" return 0 } case "$MODE" in install) do_install ;; run) do_run ;; all) do_install do_run ;; esac exit 0