fix(quick-261009-p0m): Review-Befunde WR-01/02/04/05, IN-01/02/03/04 (Abbild-Bau, Semgrep im Container, Zeitbudget, ZAP-Haken)

- WR-01: prisma generate als eigener Schritt mit Pruefung im api-Abbild, Bau scheitert ohne Client; Startprobe als .gitea/scripts/image-start-check.sh versioniert
- WR-02: Semgrep im offiziellen Container, per Digest angepinnt (kein pipx/PyPI mehr)
- WR-04: Zeitbudget 1500 s, Limits je Werkzeug, Ergebniszeile sofort nach jedem Werkzeug
- WR-05: Authorization-Ersetzung der ZAP-Pruefung nur fuer das Ziel
- IN-01: yarn/npm per Platzhalter entfernt und Abwesenheit geprueft (api und web)
- IN-02: Kommentar zu doppelten Kopfzeilen in next.config.ts richtiggestellt
- IN-03: Berichtsordner Modus 700, Zugangsdaten nur als base64 (keine curl-Konfiguration)
- IN-04: flacher Klon wird bei gitleaks als unvollstaendig gemeldet

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-09 20:59:12 +02:00
parent b085889657
commit bf632d90a5
7 changed files with 413 additions and 148 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# image-start-check.sh -- Startprobe fuer das Server-Abbild (quick-261009-p0m).
#
# Beweist, dass das fertig gebaute api-Abbild (ohne Entwicklungswerkzeuge) gegen eine
# FRISCHE, leere Datenbank alle Migrationen anwendet und die API startet. Das faengt
# genau den Fehler ab, den ein Bau allein nicht zeigt: ein Abbild, dem der Prisma-Client
# oder ein Laufzeitpaket fehlt, wird gebaut und veroeffentlicht, stirbt aber beim Start.
#
# Aufruf (aus dem Repository-Wurzelverzeichnis, Docker noetig):
# bash .gitea/scripts/image-start-check.sh # lokales Abbild tessera-ctl-api:latest
# bash .gitea/scripts/image-start-check.sh localhost:3002/schalli/tessera-ctl/api:beta
#
# Es startet ein kurzlebiges Postgres 16 und das Abbild in einem eigenen Docker-Netz, mit
# reinen Testwerten (keine echten Zugangsdaten, kein Zugriff auf Tessera oder alpha).
# Alles Angelegte wird beim Beenden wieder entfernt. Bei Erfolg endet die Ausgabe mit
# "frische-datenbank ok" und Exit 0, sonst mit einer Fehlermeldung, dem Ende des
# API-Protokolls und Exit 1. Wird nicht automatisch von der Pipeline aufgerufen; es
# gehoert in die Freigabe-Checkliste (docs/anleitung-betrieb.md, docs/anleitung-entwicklung.md).
set -u
IMAGE="${1:-tessera-ctl-api:latest}"
ID="start-check-$$"
NET="$ID-net"
DB="$ID-db"
API="$ID-api"
LOG="$(mktemp)"
cleanup() {
docker rm -f "$API" "$DB" >/dev/null 2>&1
docker network rm "$NET" >/dev/null 2>&1
rm -f "$LOG"
}
trap cleanup EXIT INT TERM HUP PIPE
fail() {
echo "FEHLER: $*" >&2
echo "--- Protokoll der API (letzte 40 Zeilen) ---" >&2
docker logs "$API" 2>&1 | tail -40 >&2
exit 1
}
docker network create "$NET" >/dev/null || { echo "FEHLER: Netz nicht anlegbar" >&2; exit 1; }
docker run -d --name "$DB" --network "$NET" \
-e POSTGRES_USER=tessera -e POSTGRES_PASSWORD=tessera_test -e POSTGRES_DB=tessera \
postgres:16-alpine >/dev/null || { echo "FEHLER: Datenbank startet nicht" >&2; exit 1; }
i=0
until docker exec "$DB" pg_isready -U tessera -d tessera >/dev/null 2>&1; do
i=$((i + 1))
[ "$i" -gt 60 ] && { echo "FEHLER: Datenbank wird nicht bereit" >&2; exit 1; }
sleep 1
done
# pg_isready meldet schon waehrend der Einrichtung "bereit"; kurz warten, dann
# eine echte Abfrage (die Einrichtung startet den Server einmal neu).
sleep 3
i=0
until docker exec "$DB" psql -U tessera -d tessera -tAc 'select 1' >/dev/null 2>&1; do
i=$((i + 1))
[ "$i" -gt 30 ] && { echo "FEHLER: Datenbank antwortet nicht" >&2; exit 1; }
sleep 1
done
KEY="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
docker run -d --name "$API" --network "$NET" \
-e "DATABASE_URL=postgresql://tessera:tessera_test@$DB:5432/tessera" \
-e "TESSERA_ENCRYPTION_KEY=$KEY" \
-e "JWT_SECRET=fresh-db-test-jwt-secret" \
-e TESSERA_ADMIN_USER=admin -e TESSERA_ADMIN_EMAIL=admin@tessera.local \
-e TESSERA_ADMIN_PASSWORD=fresh-db-test-pw -e TESSERA_FORCE_CHANGE=false \
"$IMAGE" >/dev/null || { echo "FEHLER: API startet nicht" >&2; exit 1; }
i=0
while :; do
docker logs "$API" >"$LOG" 2>&1
grep -q "Tessera API running" "$LOG" && break
[ "$(docker inspect -f '{{.State.Running}}' "$API" 2>/dev/null)" = "true" ] || fail "Container ist beendet, bevor die Startzeile kam"
i=$((i + 1))
[ "$i" -gt 120 ] && fail "keine Startzeile nach 120 Sekunden"
sleep 1
done
grep -qi "successfully applied" "$LOG" || fail "Prisma meldet nicht, dass die Migrationen angewendet wurden"
N="$(docker exec "$DB" psql -U tessera -d tessera -tAc "select count(*) from _prisma_migrations where finished_at is not null")"
[ "${N:-0}" -ge 1 ] || fail "keine abgeschlossene Migration in der Datenbank"
echo "migrationen=$N"
echo "frische-datenbank ok"
+233 -127
View File
@@ -11,7 +11,8 @@
# gitleaks gesamte Git-Historie auf Zugangsdaten (--redact, Ausnahmen in .gitleaks.toml) # gitleaks gesamte Git-Historie auf Zugangsdaten (--redact, Ausnahmen in .gitleaks.toml)
# pnpm audit bekannte Schwachstellen der Produktions-Abhaengigkeiten # pnpm audit bekannte Schwachstellen der Produktions-Abhaengigkeiten
# osv-scanner pnpm-lock.yaml und apps/desktop/src-tauri/Cargo.lock # osv-scanner pnpm-lock.yaml und apps/desktop/src-tauri/Cargo.lock
# semgrep statische Code-Analyse (Ausnahmen in .semgrepignore) # 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 fs Abhaengigkeiten, Dockerfile-Einstellungen, Zugangsdaten im Quellstand
# trivy image die frisch gebauten Abbilder api und web (nur main und Tags v*) # trivy image die frisch gebauten Abbilder api und web (nur main und Tags v*)
# #
@@ -44,6 +45,15 @@
# Werkzeug-Versionen anheben: neue Version UND die SHA256-Summe aus der offiziellen # Werkzeug-Versionen anheben: neue Version UND die SHA256-Summe aus der offiziellen
# Pruefsummendatei der Veroeffentlichung unten eintragen (docs/ci-cd-setup.md). # 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 # Dieses Skript kennt kein Secret und gibt keines aus: Zeilen und Zusammenfassung
# enthalten nur Zaehlwerte und Statuswoerter. # enthalten nur Zaehlwerte und Statuswoerter.
@@ -62,9 +72,27 @@ OSV_VERSION=2.6.0
OSV_URL="https://github.com/google/osv-scanner/releases/download/v2.6.0/osv-scanner_linux_amd64" OSV_URL="https://github.com/google/osv-scanner/releases/download/v2.6.0/osv-scanner_linux_amd64"
OSV_SHA256=ca69b3d3cd08f889a49dc0a383122f71cc528b83803671df5fd874d97485b108 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_VERSION=1.180.0
SEMGREP_IMAGE=semgrep/semgrep@sha256:529ee8a277ec8adc5b534d7c74eea0a47e9de21d62852b6ba7ac6ba9566845c3
PNPM_VERSION=9.15.0 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}" REGISTRY="${REGISTRY:-localhost:3002/schalli/tessera-ctl}"
REF="${GITHUB_REF:-}" REF="${GITHUB_REF:-}"
MODE="${1:-}" MODE="${1:-}"
@@ -98,9 +126,6 @@ esac
GITLEAKS_BIN="$TOOLDIR/gitleaks-$GITLEAKS_VERSION/gitleaks" GITLEAKS_BIN="$TOOLDIR/gitleaks-$GITLEAKS_VERSION/gitleaks"
TRIVY_BIN="$TOOLDIR/trivy-$TRIVY_VERSION/trivy" TRIVY_BIN="$TOOLDIR/trivy-$TRIVY_VERSION/trivy"
OSV_BIN="$TOOLDIR/osv-scanner-$OSV_VERSION/osv-scanner" OSV_BIN="$TOOLDIR/osv-scanner-$OSV_VERSION/osv-scanner"
PIPX_HOME_DIR="$TOOLDIR/pipx"
PIPX_BIN_PATH="$TOOLDIR/pipx-bin"
SEMGREP_BIN="$PIPX_BIN_PATH/semgrep"
COREPACK_HOME_DIR="$TOOLDIR/corepack" COREPACK_HOME_DIR="$TOOLDIR/corepack"
TRIVY_CACHE="$TOOLDIR/trivy-cache" TRIVY_CACHE="$TOOLDIR/trivy-cache"
@@ -125,7 +150,8 @@ print_plan() {
echo "werkzeug gitleaks $GITLEAKS_VERSION" echo "werkzeug gitleaks $GITLEAKS_VERSION"
echo "werkzeug trivy $TRIVY_VERSION" echo "werkzeug trivy $TRIVY_VERSION"
echo "werkzeug osv-scanner $OSV_VERSION" echo "werkzeug osv-scanner $OSV_VERSION"
echo "werkzeug semgrep $SEMGREP_VERSION" echo "werkzeug semgrep $SEMGREP_VERSION (Abbild $SEMGREP_IMAGE)"
echo "zeitbudget ${BUDGET_SECONDS}s"
echo "werkzeug pnpm $PNPM_VERSION" echo "werkzeug pnpm $PNPM_VERSION"
echo "werkzeugordner $TOOLDIR" echo "werkzeugordner $TOOLDIR"
if [ -n "$IMAGES" ]; then if [ -n "$IMAGES" ]; then
@@ -153,17 +179,27 @@ esac
# --- Hilfsfunktionen --------------------------------------------------------- # --- Hilfsfunktionen ---------------------------------------------------------
WORK="" WORK=""
SEMGREP_CT=""
cleanup() { cleanup() {
[ -n "$SEMGREP_CT" ] && docker rm -f "$SEMGREP_CT" >/dev/null 2>&1
[ -n "$WORK" ] && rm -rf "$WORK" [ -n "$WORK" ] && rm -rf "$WORK"
return 0 return 0
} }
trap cleanup EXIT trap cleanup EXIT
trap 'exit 0' INT TERM trap 'exit 0' INT TERM
# Laufzeitgrenze je Werkzeug, falls `timeout` vorhanden ist. # 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() { tmo() {
_secs="$1" _secs="$1"
shift 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 if command -v timeout >/dev/null 2>&1; then
timeout "$_secs" "$@" timeout "$_secs" "$@"
else else
@@ -181,6 +217,22 @@ setstatus() {
return 0 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() { sha_ok() {
# $1 Datei, $2 erwartete SHA256 # $1 Datei, $2 erwartete SHA256
[ -f "$1" ] || return 1 [ -f "$1" ] || return 1
@@ -195,7 +247,7 @@ fetch_verified() {
return 0 return 0
fi fi
rm -f "$4" "$4.part" rm -f "$4" "$4.part"
if ! curl -fsSL --retry 2 --connect-timeout 20 --max-time 600 -o "$4.part" "$2" 2>/dev/null; then if ! curl -fsSL --retry 2 --connect-timeout 20 --max-time "$T_DOWNLOAD" -o "$4.part" "$2" 2>/dev/null; then
rm -f "$4.part" rm -f "$4.part"
FETCH_REASON=download FETCH_REASON=download
return 1 return 1
@@ -252,18 +304,17 @@ install_osv() {
} }
install_semgrep() { install_semgrep() {
if [ -x "$SEMGREP_BIN" ] && "$SEMGREP_BIN" --version 2>/dev/null | grep -q "^$SEMGREP_VERSION"; then if ! command -v docker >/dev/null 2>&1; then
echo "Werkzeug semgrep $SEMGREP_VERSION bereit (vorhanden)" skipline semgrep kein-docker
return 0 return 0
fi fi
if ! command -v pipx >/dev/null 2>&1; then if docker image inspect "$SEMGREP_IMAGE" >/dev/null 2>&1; then
skipline semgrep kein-pipx echo "Werkzeug semgrep $SEMGREP_VERSION bereit (Abbild vorhanden, Digest angepinnt)"
return 0 return 0
fi fi
mkdir -p "$PIPX_HOME_DIR" "$PIPX_BIN_PATH" 2>/dev/null if tmo "$T_SEMGREP_PULL" docker pull -q "$SEMGREP_IMAGE" >/dev/null 2>&1 &&
if PIPX_HOME="$PIPX_HOME_DIR" PIPX_BIN_DIR="$PIPX_BIN_PATH" tmo 600 pipx install --force "semgrep==$SEMGREP_VERSION" >/dev/null 2>&1 && docker image inspect "$SEMGREP_IMAGE" >/dev/null 2>&1; then
"$SEMGREP_BIN" --version 2>/dev/null | grep -q "^$SEMGREP_VERSION"; then echo "Werkzeug semgrep $SEMGREP_VERSION bereit (Abbild geladen, Digest angepinnt)"
echo "Werkzeug semgrep $SEMGREP_VERSION bereit"
else else
skipline semgrep install skipline semgrep install
fi fi
@@ -271,7 +322,11 @@ install_semgrep() {
# pnpm: eine pnpm 9.15.x im PATH, sonst corepack mit angepinnter Version. # pnpm: eine pnpm 9.15.x im PATH, sonst corepack mit angepinnter Version.
PNPM_MODE="" PNPM_MODE=""
PNPM_CHECKED=no
detect_pnpm() { detect_pnpm() {
[ "$PNPM_CHECKED" = yes ] && [ -n "$PNPM_MODE" ] && return 0
[ "$PNPM_CHECKED" = yes ] && return 1
PNPM_CHECKED=yes
PNPM_MODE="" PNPM_MODE=""
if command -v pnpm >/dev/null 2>&1; then if command -v pnpm >/dev/null 2>&1; then
_v="$(cd "$TOOLDIR" 2>/dev/null && pnpm --version 2>/dev/null)" _v="$(cd "$TOOLDIR" 2>/dev/null && pnpm --version 2>/dev/null)"
@@ -283,7 +338,7 @@ detect_pnpm() {
esac esac
fi fi
if command -v corepack >/dev/null 2>&1; then if command -v corepack >/dev/null 2>&1; then
_v="$(COREPACK_HOME="$COREPACK_HOME_DIR" COREPACK_ENABLE_DOWNLOAD_PROMPT=0 tmo 120 corepack "pnpm@$PNPM_VERSION" --version 2>/dev/null)" _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 if [ "$_v" = "$PNPM_VERSION" ]; then
PNPM_MODE=corepack PNPM_MODE=corepack
return 0 return 0
@@ -295,9 +350,9 @@ detect_pnpm() {
# pnpm im aktuellen Ordner ausfuehren (nach detect_pnpm). # pnpm im aktuellen Ordner ausfuehren (nach detect_pnpm).
run_pnpm() { run_pnpm() {
if [ "$PNPM_MODE" = path ]; then if [ "$PNPM_MODE" = path ]; then
tmo 300 pnpm "$@" tmo "$T_AUDIT" pnpm "$@"
else else
COREPACK_HOME="$COREPACK_HOME_DIR" COREPACK_ENABLE_DOWNLOAD_PROMPT=0 tmo 300 corepack "pnpm@$PNPM_VERSION" "$@" COREPACK_HOME="$COREPACK_HOME_DIR" COREPACK_ENABLE_DOWNLOAD_PROMPT=0 tmo "$T_AUDIT" corepack "pnpm@$PNPM_VERSION" "$@"
fi fi
} }
@@ -322,60 +377,98 @@ do_install() {
} }
# --- Pruefungen -------------------------------------------------------------- # --- 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() { scan_gitleaks() {
if [ ! -x "$GITLEAKS_BIN" ]; then if [ ! -x "$GITLEAKS_BIN" ]; then
setstatus gitleaks nicht-installiert setstatus gitleaks nicht-installiert
report_tool gitleaks
return 0 return 0
fi fi
setstatus gitleaks fehler # 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" && cd "$ROOT" &&
tmo 900 "$GITLEAKS_BIN" git --config "$ROOT/.gitleaks.toml" --redact --no-banner \ 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 --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 return 0
} }
scan_pnpm_audit() { scan_pnpm_audit() {
if ! detect_pnpm; then if ! detect_pnpm; then
setstatus pnpm-audit-prod kein-pnpm setstatus pnpm-audit-prod kein-pnpm
report_tool pnpm-audit-prod
return 0 return 0
fi fi
setstatus pnpm-audit-prod fehler setstatus pnpm-audit-prod fehler
( (
cd "$WORK/src" && run_pnpm audit --prod --json >"$REPORT/pnpm-audit-prod.json" 2>/dev/null 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 return 0
} }
scan_osv() { scan_osv() {
if [ ! -x "$OSV_BIN" ]; then if [ ! -x "$OSV_BIN" ]; then
setstatus osv-scanner nicht-installiert setstatus osv-scanner nicht-installiert
report_tool osv-scanner
return 0 return 0
fi fi
setstatus osv-scanner fehler setstatus osv-scanner fehler
( (
cd "$WORK/src" && cd "$WORK/src" &&
tmo 600 "$OSV_BIN" scan source --lockfile pnpm-lock.yaml \ tmo "$T_OSV" "$OSV_BIN" scan source --lockfile pnpm-lock.yaml \
--lockfile apps/desktop/src-tauri/Cargo.lock \ --lockfile apps/desktop/src-tauri/Cargo.lock \
--format json --output "$REPORT/osv-scanner.json" >/dev/null 2>&1 --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 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() { scan_semgrep() {
if [ ! -x "$SEMGREP_BIN" ]; then 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 setstatus semgrep nicht-installiert
report_tool semgrep
return 0 return 0
fi fi
setstatus semgrep fehler setstatus semgrep fehler
( SEMGREP_CT="tessera-semgrep-$$"
cd "$WORK/src" && if docker create --name "$SEMGREP_CT" -w /src \
SEMGREP_SEND_METRICS=off SEMGREP_ENABLE_VERSION_CHECK=0 \ -e SEMGREP_SEND_METRICS=off -e SEMGREP_ENABLE_VERSION_CHECK=0 \
tmo 1500 "$SEMGREP_BIN" scan --config p/default --config p/typescript \ "$SEMGREP_IMAGE" semgrep scan --config p/default --config p/typescript \
--config p/nodejs --config p/secrets --config p/dockerfile \ --config p/nodejs --config p/secrets --config p/dockerfile \
--metrics=off --quiet --timeout 20 --json --output "$REPORT/semgrep.json" . >/dev/null 2>&1 --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 return 0
} }
@@ -393,6 +486,7 @@ EOF
scan_trivy_fs() { scan_trivy_fs() {
if [ ! -x "$TRIVY_BIN" ]; then if [ ! -x "$TRIVY_BIN" ]; then
setstatus trivy-fs nicht-installiert setstatus trivy-fs nicht-installiert
report_tool trivy-fs
return 0 return 0
fi fi
setstatus trivy-fs fehler setstatus trivy-fs fehler
@@ -400,46 +494,58 @@ scan_trivy_fs() {
mkdir -p "$TRIVY_CACHE" 2>/dev/null mkdir -p "$TRIVY_CACHE" 2>/dev/null
( (
cd "$WORK/src" && cd "$WORK/src" &&
tmo 900 "$TRIVY_BIN" fs --cache-dir "$TRIVY_CACHE" --quiet --no-progress --skip-version-check \ 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" \ --scanners vuln,misconfig,secret --secret-config "$WORK/trivy-secret.yaml" \
--exit-code 0 --format json --output "$REPORT/trivy-fs.json" . >/dev/null 2>&1 --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 return 0
} }
scan_trivy_images() { scan_trivy_images() {
[ -n "$IMAGES" ] || return 0 if [ -z "$IMAGES" ]; then
emit "SECURITY-SUMMARY trivy-image skipped reason=kein-abbild-fuer-diesen-ref"
return 0
fi
for _img in $IMAGES; do for _img in $IMAGES; do
_name="$(image_name "$_img")" _name="$(image_name "$_img")"
_tool="trivy-image-$_name"
if [ ! -x "$TRIVY_BIN" ]; then if [ ! -x "$TRIVY_BIN" ]; then
setstatus "trivy-image-$_name" nicht-installiert setstatus "$_tool" nicht-installiert
report_tool "$_tool"
continue continue
fi fi
if ! command -v docker >/dev/null 2>&1 || ! docker image inspect "$_img" >/dev/null 2>&1; then if ! command -v docker >/dev/null 2>&1 || ! docker image inspect "$_img" >/dev/null 2>&1; then
setstatus "trivy-image-$_name" abbild-fehlt setstatus "$_tool" abbild-fehlt
report_tool "$_tool"
continue continue
fi fi
setstatus "trivy-image-$_name" fehler setstatus "$_tool" fehler
mkdir -p "$TRIVY_CACHE" 2>/dev/null mkdir -p "$TRIVY_CACHE" 2>/dev/null
tmo 900 "$TRIVY_BIN" image --image-src docker --cache-dir "$TRIVY_CACHE" --quiet --no-progress \ 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 \ --skip-version-check --scanners vuln,secret --exit-code 0 --format json \
--output "$REPORT/trivy-image-$_name.json" "$_img" >/dev/null 2>&1 --output "$REPORT/$_tool.json" "$_img" >/dev/null 2>&1
note_rc $? "$_tool" "$REPORT/$_tool.json"
report_tool "$_tool"
done done
return 0 return 0
} }
# Zusammenfassung: nur Zaehlwerte, geschrieben ins Protokoll und nach summary.txt. # Zeile ins Protokoll UND (sobald der Berichtsordner steht) nach summary.txt.
summarize() { emit() {
_imgnames="" echo "$1"
for _img in $IMAGES; do [ -n "$WORK" ] && [ -d "$REPORT" ] && echo "$1" >>"$REPORT/summary.txt"
_imgnames="$_imgnames $(image_name "$_img")" return 0
done }
if command -v python3 >/dev/null 2>&1; then
python3 -I -c ' # 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 import json, os, sys
rep, st, names = sys.argv[1], sys.argv[2], sys.argv[3].split() rep, st, tool = sys.argv[1], sys.argv[2], sys.argv[3]
out = []
def load(name): def load(name):
@@ -450,13 +556,16 @@ def load(name):
return None return None
def skip(tool): def status():
try: try:
with open(os.path.join(st, tool), encoding="utf-8") as f: with open(os.path.join(st, tool), encoding="utf-8") as f:
reason = f.read().split()[0] return f.read().split()[0]
except Exception: except Exception:
reason = "kein-bericht" return "kein-bericht"
out.append("SECURITY-SUMMARY %s skipped reason=%s" % (tool, reason))
def skip():
return "SECURITY-SUMMARY %s skipped reason=%s" % (tool, status())
def sev_counts(results, key="Vulnerabilities"): def sev_counts(results, key="Vulnerabilities"):
@@ -469,87 +578,80 @@ def sev_counts(results, key="Vulnerabilities"):
return c return c
d = load("gitleaks.json") line = None
if isinstance(d, list): if tool == "gitleaks":
out.append("SECURITY-SUMMARY gitleaks findings=%d" % len(d)) d = load("gitleaks.json")
else: if isinstance(d, list):
skip("gitleaks") 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
}
d = load("pnpm-audit-prod.json") report_tool() {
m = d.get("metadata", {}).get("vulnerabilities") if isinstance(d, dict) else None if [ -n "$WORK" ] && [ -f "$WORK/summ.py" ] && command -v python3 >/dev/null 2>&1; then
if isinstance(m, dict): _line="$(python3 -I "$WORK/summ.py" "$REPORT" "$WORK/status" "$1" 2>/dev/null)"
out.append( [ -n "$_line" ] || _line="SECURITY-SUMMARY $1 skipped reason=kein-bericht"
"SECURITY-SUMMARY pnpm-audit-prod critical=%d high=%d moderate=%d low=%d" emit "$_line"
% (m.get("critical", 0), m.get("high", 0), m.get("moderate", 0), m.get("low", 0))
)
else:
skip("pnpm-audit-prod")
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")))
out.append("SECURITY-SUMMARY osv-scanner packages=%d" % len(pk))
else:
skip("osv-scanner")
d = load("semgrep.json")
if isinstance(d, dict) and isinstance(d.get("results"), list):
out.append(
"SECURITY-SUMMARY semgrep findings=%d errors=%d"
% (len(d["results"]), len(d.get("errors") or []))
)
else:
skip("semgrep")
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)
out.append(
"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)
)
else:
skip("trivy-fs")
if names:
for n in names:
tool = "trivy-image-" + n
d = load(tool + ".json")
if isinstance(d, dict):
c = sev_counts(d.get("Results"))
out.append(
"SECURITY-SUMMARY %s critical=%d high=%d medium=%d low=%d"
% (tool, c["CRITICAL"], c["HIGH"], c["MEDIUM"], c["LOW"])
)
else:
skip(tool)
else:
out.append("SECURITY-SUMMARY trivy-image skipped reason=kein-abbild-fuer-diesen-ref")
out.append("SECURITY-SUMMARY fertig (nur Bericht, Exit 0)")
text = "\n".join(out) + "\n"
try:
with open(os.path.join(rep, "summary.txt"), "w", encoding="utf-8") as f:
f.write(text)
except Exception:
pass
sys.stdout.write(text)
' "$REPORT" "$WORK/status" "$_imgnames" 2>/dev/null
else else
skipline summary kein-python emit "SECURITY-SUMMARY $1 skipped reason=kein-python"
echo "SECURITY-SUMMARY fertig (nur Bericht, Exit 0)"
fi fi
return 0 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() { do_run() {
if ! mkdir -p "$REPORT" 2>/dev/null; then if ! mkdir -p "$REPORT" 2>/dev/null; then
skipline berichtsordner nicht-beschreibbar skipline berichtsordner nicht-beschreibbar
@@ -567,6 +669,7 @@ do_run() {
return 0 return 0
fi fi
mkdir -p "$WORK/src" "$WORK/status" mkdir -p "$WORK/src" "$WORK/status"
write_summary_helper
# git darf in einem Ordner arbeiten, der einem anderen Benutzer gehoert (CI-Container # git darf in einem Ordner arbeiten, der einem anderen Benutzer gehoert (CI-Container
# als root, lokaler Klon als Entwickler). # als root, lokaler Klon als Entwickler).
@@ -586,14 +689,17 @@ do_run() {
scan_semgrep scan_semgrep
scan_trivy_fs scan_trivy_fs
else else
for _t in pnpm-audit-prod osv-scanner semgrep trivy-fs; do setstatus "$_t" export; done for _t in pnpm-audit-prod osv-scanner semgrep trivy-fs; do
setstatus "$_t" export
report_tool "$_t"
done
fi fi
scan_trivy_images scan_trivy_images
# Layer-Zwischenspeicher von Trivy waechst stark; die Datenbank bleibt. # Layer-Zwischenspeicher von Trivy waechst stark; die Datenbank bleibt.
rm -rf "$TRIVY_CACHE/fanal" 2>/dev/null rm -rf "$TRIVY_CACHE/fanal" 2>/dev/null
summarize finish_line
echo "Berichte: $REPORT (in der Pipeline als Artefakt sicherheitsberichte)" echo "Berichte: $REPORT (in der Pipeline als Artefakt sicherheitsberichte)"
return 0 return 0
} }
+53 -13
View File
@@ -28,7 +28,11 @@
# Entwicklungsrechner aus eine Anmeldung (401) verlangt. Der # Entwicklungsrechner aus eine Anmeldung (401) verlangt. Der
# Kopf geht ueber eine temporaere Umgebungsdatei (Modus 0600, # Kopf geht ueber eine temporaere Umgebungsdatei (Modus 0600,
# wird beim Beenden entfernt) in den Container, nie auf eine # wird beim Beenden entfernt) in den Container, nie auf eine
# Befehlszeile und in keine Ausgabe. # Befehlszeile und in keine Ausgabe. Der Haken schickt ihn NUR an
# das Ziel (ZAP_TARGET), nie an andere Adressen. Sonderzeichen im
# Passwort (" oder \) sind erlaubt.
#
# Berichtsordner: Modus 700 (nur der ZAP-Benutzer mit Kennung 1000 schreibt, niemand sonst liest).
# #
# Hinweis: Wird die Anmeldedatei benutzt, enthaelt der Bericht die gesendeten Kopfzeilen # Hinweis: Wird die Anmeldedatei benutzt, enthaelt der Bericht die gesendeten Kopfzeilen
# und damit die Zugangsdaten. Dann den Berichtsordner nicht weitergeben. # und damit die Zugangsdaten. Dann den Berichtsordner nicht weitergeben.
@@ -64,10 +68,18 @@ if [ "${1:-}" = "--print-plan" ]; then
fi fi
TMP_FILES="" TMP_FILES=""
RUN_LOG=""
# Entfernt die temporaeren Dateien mit dem Kopf (Zugangsdaten).
cleanup() { cleanup() {
for f in $TMP_FILES; do rm -f "$f"; done for f in $TMP_FILES; do rm -f "$f"; done
TMP_FILES=""
} }
trap cleanup EXIT HUP INT TERM PIPE # Beim Beenden (auch bei Abbruch) zusaetzlich das Zwischenprotokoll entfernen.
cleanup_all() {
cleanup
[ -n "$RUN_LOG" ] && rm -f "$RUN_LOG"
}
trap cleanup_all EXIT HUP INT TERM PIPE
echo "ZAP-Grundpruefung gegen $TARGET (nur passiv, keine Formulare, keine Angriffe)" echo "ZAP-Grundpruefung gegen $TARGET (nur passiv, keine Formulare, keine Angriffe)"
@@ -84,19 +96,22 @@ case "$CODE" in
echo "benutzer:passwort, und rufen Sie das Skript mit ZAP_BASIC_AUTH_FILE=Pfad auf." >&2 echo "benutzer:passwort, und rufen Sie das Skript mit ZAP_BASIC_AUTH_FILE=Pfad auf." >&2
exit 4 exit 4
fi fi
CRED=$(head -n 1 "$ZAP_BASIC_AUTH_FILE" | tr -d '\r\n') # Die Zugangsdaten gehen nur als base64-Text weiter (enthaelt keine Anfuehrungszeichen,
CURL_CFG=$(mktemp) || exit 4 # Rueckstriche oder Leerzeichen), also nie durch einen Parser, der Sonderzeichen deutet:
# weder curl-Konfiguration noch Befehlszeile. Passwoerter mit " oder \ sind unkritisch.
B64=$(head -n 1 "$ZAP_BASIC_AUTH_FILE" | tr -d '\r\n' | base64 | tr -d '\n')
HDR_FILE=$(mktemp) || exit 4
ENV_FILE=$(mktemp) || exit 4 ENV_FILE=$(mktemp) || exit 4
TMP_FILES="$CURL_CFG $ENV_FILE" TMP_FILES="$HDR_FILE $ENV_FILE"
chmod 600 "$CURL_CFG" "$ENV_FILE" chmod 600 "$HDR_FILE" "$ENV_FILE"
printf 'user = "%s"\n' "$CRED" > "$CURL_CFG" printf 'Authorization: Basic %s\n' "$B64" > "$HDR_FILE"
CODE2=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 -K "$CURL_CFG" "$TARGET/login" 2>/dev/null) || CODE2=000 printf 'ZAP_BASIC_AUTH=Basic %s\n' "$B64" > "$ENV_FILE"
B64=""
CODE2=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 -H "@$HDR_FILE" "$TARGET/login" 2>/dev/null) || CODE2=000
if [ "$CODE2" != "200" ]; then if [ "$CODE2" != "200" ]; then
echo "Mit den hinterlegten Zugangsdaten antwortet das Ziel mit Status $CODE2, Abbruch." >&2 echo "Mit den hinterlegten Zugangsdaten antwortet das Ziel mit Status $CODE2, Abbruch." >&2
exit 4 exit 4
fi fi
printf 'ZAP_BASIC_AUTH=Basic %s\n' "$(printf '%s' "$CRED" | base64 | tr -d '\n')" > "$ENV_FILE"
CRED=""
;; ;;
*) *)
echo "Das Ziel antwortet auf /login nicht mit Status 200 (Status $CODE), Abbruch." >&2 echo "Das Ziel antwortet auf /login nicht mit Status 200 (Status $CODE), Abbruch." >&2
@@ -106,9 +121,24 @@ case "$CODE" in
esac esac
# --- Berichtsordner: der Container laeuft als Benutzer zap (Kennung 1000) --------- # --- Berichtsordner: der Container laeuft als Benutzer zap (Kennung 1000) ---------
# Nur dieser eine Benutzer darf schreiben (Modus 700), kein anderer Benutzer des Rechners
# darf lesen -- der Bericht kann die Zugangsdaten enthalten. Hat Ihr Konto die Kennung
# 1000 (der Normalfall), genuegt chmod; sonst uebergibt ein kurzer Container-Aufruf den
# Ordner an die Kennung 1000 und danach wieder an Sie zurueck.
mkdir -p "$REPORT_DIR" || exit 3 mkdir -p "$REPORT_DIR" || exit 3
REPORT_DIR=$(cd "$REPORT_DIR" && pwd) REPORT_DIR=$(cd "$REPORT_DIR" && pwd)
chmod 777 "$REPORT_DIR" chmod 700 "$REPORT_DIR"
ME_UID=$(id -u)
ME_GID=$(id -g)
HANDOVER=no
if [ "$ME_UID" != "1000" ]; then
if docker run --rm --user 0:0 -v "$REPORT_DIR:/zap/wrk:rw" "$IMAGE" chown 1000:1000 /zap/wrk >/dev/null 2>&1; then
HANDOVER=yes
else
echo "Der Berichtsordner konnte nicht an den ZAP-Benutzer (Kennung 1000) uebergeben werden." >&2
exit 3
fi
fi
set -- docker run --rm set -- docker run --rm
[ -n "${ZAP_DOCKER_NETWORK:-}" ] && set -- "$@" --network "$ZAP_DOCKER_NETWORK" [ -n "${ZAP_DOCKER_NETWORK:-}" ] && set -- "$@" --network "$ZAP_DOCKER_NETWORK"
@@ -119,10 +149,20 @@ set -- "$@" -v "$REPORT_DIR:/zap/wrk:rw" -v "$HOOK_FILE:$HOOK_IN_CONTAINER:ro" "
# Exit-Codes von ZAP (0 ok, 1 Fehler, 2 Warnung, 3 Fehlstart) sind hier nicht # Exit-Codes von ZAP (0 ok, 1 Fehler, 2 Warnung, 3 Fehlstart) sind hier nicht
# entscheidend: es zaehlt, ob der Bericht entstanden ist. # entscheidend: es zaehlt, ob der Bericht entstanden ist.
"$@" > "$REPORT_DIR/zap-lauf.log" 2>&1 # Das Protokoll geht zuerst in eine eigene Datei: der Berichtsordner gehoert waehrend des
# Laufs eventuell der Kennung 1000, nicht Ihrem Konto.
RUN_LOG=$(mktemp) || exit 3
"$@" > "$RUN_LOG" 2>&1
# Die Umgebungsdatei mit dem Kopf wird nur fuer den Lauf gebraucht: sofort entfernen. # Die Umgebungsdatei mit dem Kopf wird nur fuer den Lauf gebraucht: sofort entfernen.
cleanup cleanup
TMP_FILES="" if [ "$HANDOVER" = yes ]; then
# Ordner samt Inhalt zurueck an das eigene Konto, damit Sie den Bericht lesen koennen.
docker run --rm --user 0:0 -v "$REPORT_DIR:/zap/wrk:rw" "$IMAGE" chown -R "$ME_UID:$ME_GID" /zap/wrk >/dev/null 2>&1 ||
echo "Hinweis: Der Berichtsordner gehoert noch der Kennung 1000; mit sudo chown -R $ME_UID:$ME_GID uebernehmen." >&2
fi
cat "$RUN_LOG" > "$REPORT_DIR/zap-lauf.log" 2>/dev/null
rm -f "$RUN_LOG"
RUN_LOG=""
echo "ZAP-Lauf beendet (Protokoll: $REPORT_DIR/zap-lauf.log)" echo "ZAP-Lauf beendet (Protokoll: $REPORT_DIR/zap-lauf.log)"
if [ ! -s "$REPORT_DIR/zap.json" ]; then if [ ! -s "$REPORT_DIR/zap.json" ]; then
+9 -1
View File
@@ -8,10 +8,13 @@ Aufgaben:
die Pruefung rein passiv: es werden nur Seiten abgerufen (GET), nie Daten die Pruefung rein passiv: es werden nur Seiten abgerufen (GET), nie Daten
an die Anwendung geschickt (kein POST). an die Anwendung geschickt (kein POST).
2. Nur wenn die Umgebungsvariable ZAP_BASIC_AUTH gesetzt ist (Anmeldedatei-Weg des 2. Nur wenn die Umgebungsvariable ZAP_BASIC_AUTH gesetzt ist (Anmeldedatei-Weg des
Skripts), wird ein Ersetzungsregel fuer den Kopf Authorization eingetragen. Skripts), wird eine Ersetzungsregel fuer den Kopf Authorization eingetragen --
beschraenkt auf Adressen des Ziels (Schema, Host, Port). Andere Hosts (Weiterleitungen,
eingebundene Fremdseiten, Zusatzdienste von ZAP) bekommen den Kopf nie.
Der Wert steht nie in einem Aufruf oder einer Ausgabe. Der Wert steht nie in einem Aufruf oder einer Ausgabe.
""" """
import os import os
import re
def zap_started(zap, target): def zap_started(zap, target):
@@ -21,6 +24,10 @@ def zap_started(zap, target):
auth = os.environ.get("ZAP_BASIC_AUTH") auth = os.environ.get("ZAP_BASIC_AUTH")
if auth: if auth:
# Die Regel gilt nur fuer URLs, die mit der Ziel-Adresse beginnen (Java-Regex, die
# ganze URL muss passen). Danach darf nur ein Pfad, eine Abfrage oder ein Anker
# folgen -- so passt auch http://host:30001 nicht auf das Ziel http://host:3000.
url_regex = re.escape(target.rstrip("/")) + r"(?:[/?#].*)?"
zap.replacer.add_rule( zap.replacer.add_rule(
description="basicauth", description="basicauth",
enabled=True, enabled=True,
@@ -29,4 +36,5 @@ def zap_started(zap, target):
matchregex=False, matchregex=False,
replacement=auth, replacement=auth,
initiators="", initiators="",
url=url_regex,
) )
+15 -2
View File
@@ -37,6 +37,11 @@ COPY apps/api/package.json ./apps/api/
COPY packages/shared/package.json ./packages/shared/ COPY packages/shared/package.json ./packages/shared/
COPY apps/api/prisma ./apps/api/prisma/ COPY apps/api/prisma ./apps/api/prisma/
RUN pnpm install --frozen-lockfile --prod --filter=@tessera/api... RUN pnpm install --frozen-lockfile --prod --filter=@tessera/api...
# Der Prisma-Client entsteht im postinstall von apps/api, der Fehler verschluckt
# (`prisma generate || true`). Deshalb hier noch einmal ausdruecklich erzeugen und pruefen:
# fehlt der Client, scheitert der Bau -- statt eines Abbilds, das erst beim Start stirbt.
RUN apps/api/node_modules/.bin/prisma generate --schema apps/api/prisma/schema.prisma \
&& find node_modules/.pnpm -path '*/.prisma/client/index.js' | grep -q .
# quick-261009-p0m: Laufzeitstufe direkt vom Node-Abbild (ohne das vorbereitete pnpm # quick-261009-p0m: Laufzeitstufe direkt vom Node-Abbild (ohne das vorbereitete pnpm
# der Stufe base). Die Paketverwaltungen des Node-Abbilds (npm, npx, corepack, yarn) # der Stufe base). Die Paketverwaltungen des Node-Abbilds (npm, npx, corepack, yarn)
@@ -49,8 +54,16 @@ ARG APP_CHANNEL
ARG APP_COMMIT ARG APP_COMMIT
ARG APP_BUILD_TIME ARG APP_BUILD_TIME
ENV APP_VERSION=$APP_VERSION APP_CHANNEL=$APP_CHANNEL APP_COMMIT=$APP_COMMIT APP_BUILD_TIME=$APP_BUILD_TIME ENV APP_VERSION=$APP_VERSION APP_CHANNEL=$APP_CHANNEL APP_COMMIT=$APP_COMMIT APP_BUILD_TIME=$APP_BUILD_TIME
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn-v1.22.22 \ # Die Ordner-/Dateinamen werden mit Platzhaltern gesucht (yarn-v1.22.22 wechselt mit dem
/usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg # Node-Abbild); die letzte Zeile laesst den Bau scheitern, falls eines der Werkzeuge
# bleibt -- sonst verpufft die Absicherung still bei einem neuen Basisabbild.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn* \
/usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn* \
&& for c in npm npx corepack yarn yarnpkg; do \
if command -v "$c" >/dev/null 2>&1; then echo "FEHLER: $c ist noch im Abbild" >&2; exit 1; fi; \
done \
&& [ -z "$(ls -d /opt/yarn* /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack 2>/dev/null)" ] \
&& node --version
RUN addgroup --system --gid 1001 nestjs && \ RUN addgroup --system --gid 1001 nestjs && \
adduser --system --uid 1001 nestjs && \ adduser --system --uid 1001 nestjs && \
mkdir -p /app/user-files && \ mkdir -p /app/user-files && \
+10 -2
View File
@@ -44,8 +44,16 @@ ARG APP_BUILD_TIME
ENV APP_VERSION=$APP_VERSION APP_CHANNEL=$APP_CHANNEL APP_COMMIT=$APP_COMMIT APP_BUILD_TIME=$APP_BUILD_TIME ENV APP_VERSION=$APP_VERSION APP_CHANNEL=$APP_CHANNEL APP_COMMIT=$APP_COMMIT APP_BUILD_TIME=$APP_BUILD_TIME
# quick-261009-p0m: Paketverwaltungen des Node-Abbilds (npm, npx, corepack, yarn) entfernen; # quick-261009-p0m: Paketverwaltungen des Node-Abbilds (npm, npx, corepack, yarn) entfernen;
# der Web-Server braucht nur `node`. # der Web-Server braucht nur `node`.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn-v1.22.22 \ # Die Ordner-/Dateinamen werden mit Platzhaltern gesucht (yarn-v1.22.22 wechselt mit dem
/usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn /usr/local/bin/yarnpkg # Node-Abbild); die letzte Zeile laesst den Bau scheitern, falls eines der Werkzeuge
# bleibt -- sonst verpufft die Absicherung still bei einem neuen Basisabbild.
RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack /opt/yarn* \
/usr/local/bin/npm /usr/local/bin/npx /usr/local/bin/corepack /usr/local/bin/yarn* \
&& for c in npm npx corepack yarn yarnpkg; do \
if command -v "$c" >/dev/null 2>&1; then echo "FEHLER: $c ist noch im Abbild" >&2; exit 1; fi; \
done \
&& [ -z "$(ls -d /opt/yarn* /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/corepack 2>/dev/null)" ] \
&& node --version
RUN addgroup --system --gid 1001 nodejs && \ RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nextjs adduser --system --uid 1001 nextjs
COPY --from=builder /app/apps/web/public ./apps/web/public COPY --from=builder /app/apps/web/public ./apps/web/public
+7 -3
View File
@@ -29,9 +29,13 @@ function readChangelog(): string {
} }
// Schutz-Kopfzeilen der Anwendung (quick-261009-p0m, aus dem ersten ZAP-Lauf). Sie gelten fuer // Schutz-Kopfzeilen der Anwendung (quick-261009-p0m, aus dem ersten ZAP-Lauf). Sie gelten fuer
// alle Pfade und stehen hier, damit sie auch ohne vorgeschalteten Proxy gesendet werden // alle Pfade und stehen hier, damit sie auch ohne vorgeschalteten Proxy gesendet werden.
// (doppelt gesichert; setzt der Proxy dieselben Kopfzeilen, gewinnt keine Seite etwas oder // Setzt der Proxy dieselben Kopfzeilen mit gleichem Wert, aendert sich nichts. Setzt er sie mit
// verliert etwas). Absichtlich KEINE vollstaendige Content-Security-Policy -- sie braucht eine // ANDEREM Wert (etwa eine eigene Content-Security-Policy oder X-Frame-Options: DENY), gelten
// beide nebeneinander und der Browser nimmt die strengere -- das kann Tessera-Seiten sperren,
// die sich selbst einrahmen. Ob der Proxy welche sendet, ist mit dem ersten ZAP-Lauf gegen die
// oeffentliche Adresse zu pruefen (docs/sicherheitsprotokoll.md).
// Absichtlich KEINE vollstaendige Content-Security-Policy -- sie braucht eine
// eigene Pruefung der Inline-Skripte von Next.js und bleibt im Sicherheitsprotokoll "offen". // eigene Pruefung der Inline-Skripte von Next.js und bleibt im Sicherheitsprotokoll "offen".
// `frame-ancestors 'self'` ist nur die eine Richtlinie gegen das Einrahmen durch fremde Seiten // `frame-ancestors 'self'` ist nur die eine Richtlinie gegen das Einrahmen durch fremde Seiten
// (Gegenstueck zu X-Frame-Options fuer neuere Browser). Eingebettete FREMDE Seiten (XFrame-Kachel, // (Gegenstueck zu X-Frame-Options fuer neuere Browser). Eingebettete FREMDE Seiten (XFrame-Kachel,