ci(quick-261009-p0m): Sicherheitspruefung als reiner Bericht in der Pipeline
- Skript .gitea/scripts/security-scan.sh: gitleaks, pnpm audit, osv-scanner, Semgrep, Trivy (Quellstand und Abbilder), angepinnt und per SHA256 geprueft, Exit immer 0 - Job security nach publish (nur main und Tags v*, continue-on-error, Zeitlimit 30 Minuten, kein Secret) - Ausnahmelisten .gitleaks.toml und .semgrepignore fuer geprueft harmlose Treffer - docs/ci-cd-setup.md beschreibt den Job Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,4 @@ dist
|
|||||||
# (apps/web/next.config.ts liest sie zur Bauzeit, COPY im Web-Dockerfile).
|
# (apps/web/next.config.ts liest sie zur Bauzeit, COPY im Web-Dockerfile).
|
||||||
!CHANGELOG.md
|
!CHANGELOG.md
|
||||||
coverage
|
coverage
|
||||||
|
security-reports
|
||||||
|
|||||||
Executable
+610
@@ -0,0 +1,610 @@
|
|||||||
|
#!/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)
|
||||||
|
# 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).
|
||||||
|
#
|
||||||
|
# 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_VERSION=1.180.0
|
||||||
|
PNPM_VERSION=9.15.0
|
||||||
|
|
||||||
|
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"
|
||||||
|
PIPX_HOME_DIR="$TOOLDIR/pipx"
|
||||||
|
PIPX_BIN_PATH="$TOOLDIR/pipx-bin"
|
||||||
|
SEMGREP_BIN="$PIPX_BIN_PATH/semgrep"
|
||||||
|
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"
|
||||||
|
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=""
|
||||||
|
cleanup() {
|
||||||
|
[ -n "$WORK" ] && rm -rf "$WORK"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
trap 'exit 0' INT TERM
|
||||||
|
|
||||||
|
# Laufzeitgrenze je Werkzeug, falls `timeout` vorhanden ist.
|
||||||
|
tmo() {
|
||||||
|
_secs="$1"
|
||||||
|
shift
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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 600 -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 [ -x "$SEMGREP_BIN" ] && "$SEMGREP_BIN" --version 2>/dev/null | grep -q "^$SEMGREP_VERSION"; then
|
||||||
|
echo "Werkzeug semgrep $SEMGREP_VERSION bereit (vorhanden)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! command -v pipx >/dev/null 2>&1; then
|
||||||
|
skipline semgrep kein-pipx
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$PIPX_HOME_DIR" "$PIPX_BIN_PATH" 2>/dev/null
|
||||||
|
if PIPX_HOME="$PIPX_HOME_DIR" PIPX_BIN_DIR="$PIPX_BIN_PATH" tmo 600 pipx install --force "semgrep==$SEMGREP_VERSION" >/dev/null 2>&1 &&
|
||||||
|
"$SEMGREP_BIN" --version 2>/dev/null | grep -q "^$SEMGREP_VERSION"; then
|
||||||
|
echo "Werkzeug semgrep $SEMGREP_VERSION bereit"
|
||||||
|
else
|
||||||
|
skipline semgrep install
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# pnpm: eine pnpm 9.15.x im PATH, sonst corepack mit angepinnter Version.
|
||||||
|
PNPM_MODE=""
|
||||||
|
detect_pnpm() {
|
||||||
|
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 120 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 300 pnpm "$@"
|
||||||
|
else
|
||||||
|
COREPACK_HOME="$COREPACK_HOME_DIR" COREPACK_ENABLE_DOWNLOAD_PROMPT=0 tmo 300 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 --------------------------------------------------------------
|
||||||
|
scan_gitleaks() {
|
||||||
|
if [ ! -x "$GITLEAKS_BIN" ]; then
|
||||||
|
setstatus gitleaks nicht-installiert
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
setstatus gitleaks fehler
|
||||||
|
(
|
||||||
|
cd "$ROOT" &&
|
||||||
|
tmo 900 "$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
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
scan_pnpm_audit() {
|
||||||
|
if ! detect_pnpm; then
|
||||||
|
setstatus pnpm-audit-prod kein-pnpm
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
setstatus pnpm-audit-prod fehler
|
||||||
|
(
|
||||||
|
cd "$WORK/src" && run_pnpm audit --prod --json >"$REPORT/pnpm-audit-prod.json" 2>/dev/null
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
scan_osv() {
|
||||||
|
if [ ! -x "$OSV_BIN" ]; then
|
||||||
|
setstatus osv-scanner nicht-installiert
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
setstatus osv-scanner fehler
|
||||||
|
(
|
||||||
|
cd "$WORK/src" &&
|
||||||
|
tmo 600 "$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
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
scan_semgrep() {
|
||||||
|
if [ ! -x "$SEMGREP_BIN" ]; then
|
||||||
|
setstatus semgrep nicht-installiert
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
setstatus semgrep fehler
|
||||||
|
(
|
||||||
|
cd "$WORK/src" &&
|
||||||
|
SEMGREP_SEND_METRICS=off SEMGREP_ENABLE_VERSION_CHECK=0 \
|
||||||
|
tmo 1500 "$SEMGREP_BIN" scan --config p/default --config p/typescript \
|
||||||
|
--config p/nodejs --config p/secrets --config p/dockerfile \
|
||||||
|
--metrics=off --quiet --timeout 20 --json --output "$REPORT/semgrep.json" . >/dev/null 2>&1
|
||||||
|
)
|
||||||
|
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
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
setstatus trivy-fs fehler
|
||||||
|
write_trivy_secret_config
|
||||||
|
mkdir -p "$TRIVY_CACHE" 2>/dev/null
|
||||||
|
(
|
||||||
|
cd "$WORK/src" &&
|
||||||
|
tmo 900 "$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
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
scan_trivy_images() {
|
||||||
|
[ -n "$IMAGES" ] || return 0
|
||||||
|
for _img in $IMAGES; do
|
||||||
|
_name="$(image_name "$_img")"
|
||||||
|
if [ ! -x "$TRIVY_BIN" ]; then
|
||||||
|
setstatus "trivy-image-$_name" nicht-installiert
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! command -v docker >/dev/null 2>&1 || ! docker image inspect "$_img" >/dev/null 2>&1; then
|
||||||
|
setstatus "trivy-image-$_name" abbild-fehlt
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
setstatus "trivy-image-$_name" fehler
|
||||||
|
mkdir -p "$TRIVY_CACHE" 2>/dev/null
|
||||||
|
tmo 900 "$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/trivy-image-$_name.json" "$_img" >/dev/null 2>&1
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Zusammenfassung: nur Zaehlwerte, geschrieben ins Protokoll und nach summary.txt.
|
||||||
|
summarize() {
|
||||||
|
_imgnames=""
|
||||||
|
for _img in $IMAGES; do
|
||||||
|
_imgnames="$_imgnames $(image_name "$_img")"
|
||||||
|
done
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
python3 -I -c '
|
||||||
|
import json, os, sys
|
||||||
|
|
||||||
|
rep, st, names = sys.argv[1], sys.argv[2], sys.argv[3].split()
|
||||||
|
out = []
|
||||||
|
|
||||||
|
|
||||||
|
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 skip(tool):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(st, tool), encoding="utf-8") as f:
|
||||||
|
reason = f.read().split()[0]
|
||||||
|
except Exception:
|
||||||
|
reason = "kein-bericht"
|
||||||
|
out.append("SECURITY-SUMMARY %s skipped reason=%s" % (tool, reason))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
d = load("gitleaks.json")
|
||||||
|
if isinstance(d, list):
|
||||||
|
out.append("SECURITY-SUMMARY gitleaks findings=%d" % len(d))
|
||||||
|
else:
|
||||||
|
skip("gitleaks")
|
||||||
|
|
||||||
|
d = load("pnpm-audit-prod.json")
|
||||||
|
m = d.get("metadata", {}).get("vulnerabilities") if isinstance(d, dict) else None
|
||||||
|
if isinstance(m, dict):
|
||||||
|
out.append(
|
||||||
|
"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))
|
||||||
|
)
|
||||||
|
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
|
||||||
|
skipline summary kein-python
|
||||||
|
echo "SECURITY-SUMMARY fertig (nur Bericht, Exit 0)"
|
||||||
|
fi
|
||||||
|
return 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"
|
||||||
|
|
||||||
|
# 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; done
|
||||||
|
fi
|
||||||
|
scan_trivy_images
|
||||||
|
|
||||||
|
# Layer-Zwischenspeicher von Trivy waechst stark; die Datenbank bleibt.
|
||||||
|
rm -rf "$TRIVY_CACHE/fanal" 2>/dev/null
|
||||||
|
|
||||||
|
summarize
|
||||||
|
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
|
||||||
@@ -17,6 +17,9 @@
|
|||||||
# quick-260917-kgc: Die beiden tauri-build-Schritte signieren die Pakete mit dem
|
# quick-260917-kgc: Die beiden tauri-build-Schritte signieren die Pakete mit dem
|
||||||
# Updater-Schluessel (Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD, nur an diesen
|
# Updater-Schluessel (Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD, nur an diesen
|
||||||
# zwei Schritten); desktop-collect.sh traegt die .sig-Inhalte ins Manifest.
|
# zwei Schritten); desktop-collect.sh traegt die .sig-Inhalte ins Manifest.
|
||||||
|
# quick-261009-p0m: Job `security` (Sicherheitspruefung) laeuft nach `publish`, nur auf
|
||||||
|
# main und Tags v*, ist reiner Bericht (continue-on-error, nie in einem `needs`, kein
|
||||||
|
# Secret) und ruft .gitea/scripts/security-scan.sh auf.
|
||||||
name: Tessera CI/CD
|
name: Tessera CI/CD
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -258,3 +261,35 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: sh .gitea/scripts/publish-release.sh
|
run: sh .gitea/scripts/publish-release.sh
|
||||||
|
|
||||||
|
security:
|
||||||
|
name: Sicherheitspruefung (nur Bericht)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: publish
|
||||||
|
# Ausdruecklich noetig: Ein uebersprungener `needs`-Job blockiert in Gitea nicht,
|
||||||
|
# ohne diese Bedingung liefe der Job auch nach einem Push auf `live` (ohne Abbilder).
|
||||||
|
if: gitea.ref == 'refs/heads/main' || startsWith(gitea.ref, 'refs/tags/v')
|
||||||
|
# Reiner Bericht: Ergebnis darf nie etwas blockieren oder den Lauf rot faerben.
|
||||||
|
continue-on-error: true
|
||||||
|
# Ein haengender Scan darf den (einzigen) Runner nicht dauerhaft belegen.
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
# Volle Historie: gitleaks liest alle Commits.
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Kein Secret in diesem Job; das Skript bricht nie ab (siehe docs/ci-cd-setup.md).
|
||||||
|
- name: Sicherheitspruefungen (nur Bericht)
|
||||||
|
run: sh .gitea/scripts/security-scan.sh all || true
|
||||||
|
|
||||||
|
# actions/upload-artifact@v4 lehnt Gitea ab; v3 ist der laufende Stand.
|
||||||
|
# Best effort: Die Zeilen SECURITY-SUMMARY im Protokoll genuegen immer.
|
||||||
|
- name: Berichte als Artefakt ablegen
|
||||||
|
if: always()
|
||||||
|
continue-on-error: true
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: sicherheitsberichte
|
||||||
|
path: security-reports/
|
||||||
|
retention-days: 30
|
||||||
|
|||||||
@@ -46,3 +46,6 @@ desktop-dist/*
|
|||||||
|
|
||||||
# Privater Updater-Signierschluessel liegt ausserhalb des Repos (~/.tessera/desktop-updater/) -- nie einchecken
|
# Privater Updater-Signierschluessel liegt ausserhalb des Repos (~/.tessera/desktop-updater/) -- nie einchecken
|
||||||
*.key
|
*.key
|
||||||
|
|
||||||
|
# Berichte der Sicherheitspruefung (quick-261009-p0m, .gitea/scripts/security-scan.sh)
|
||||||
|
security-reports/
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# gitleaks-Einstellungen fuer Tessera (quick-261009-p0m).
|
||||||
|
# Die Standardregeln bleiben vollstaendig aktiv; unten stehen nur Ausnahmen fuer
|
||||||
|
# Treffer, die einzeln geprueft wurden und keine echten Zugangsdaten sind.
|
||||||
|
# Grundsatz: so eng wie moeglich -- ein Ordner nur dort, wo er allein Testschluessel
|
||||||
|
# enthaelt, alles andere als einzelne Datei mit Regel (und bei produktivem Code
|
||||||
|
# zusaetzlich mit der geprueften Zeile). Ein neuer Treffer wird nie vorsorglich
|
||||||
|
# freigegeben, sondern erst geprueft.
|
||||||
|
|
||||||
|
title = "Tessera gitleaks"
|
||||||
|
|
||||||
|
[extend]
|
||||||
|
useDefault = true
|
||||||
|
|
||||||
|
[[allowlists]]
|
||||||
|
description = "Testschluessel des Zertifikatsmanagers: Der Ordner existiert nur, um Schluesseldateien fuer Tests bereitzuhalten (zufaellig erzeugt, nirgends verwendet)."
|
||||||
|
paths = ['''^apps/api/src/cert-manager/__fixtures__/''']
|
||||||
|
|
||||||
|
[[allowlists]]
|
||||||
|
description = "Schluessel-Ausschnitte (-----BEGIN ...) in den Tests des Zertifikatsmanagers und in einer Recherche-Notiz: Testdaten bzw. Beschreibungstext, keine echten Schluessel."
|
||||||
|
targetRules = ["private-key"]
|
||||||
|
paths = [
|
||||||
|
'''^apps/api/src/cert-manager/cert-keys\.spec\.ts$''',
|
||||||
|
'''^apps/api/src/cert-manager/cert-output\.spec\.ts$''',
|
||||||
|
'''^apps/api/src/cert-manager/cert-templates\.spec\.ts$''',
|
||||||
|
'''^\.planning/quick/261009-ikt-cert-manager-umbau-mehrere-dateien-zip-f/261009-ikt-RESEARCH\.md$''',
|
||||||
|
]
|
||||||
|
|
||||||
|
[[allowlists]]
|
||||||
|
description = "Testwert 'Benutzer/Passwort' im Formular-Test des Proxmox-Moduls und Tabellenzeile im Pruefplan der Ausschreibungs-Benachrichtigungen: Beschriftungen, keine Zugangsdaten."
|
||||||
|
targetRules = ["generic-api-key"]
|
||||||
|
paths = [
|
||||||
|
'''^apps/web/src/app/\(portal\)/modules/proxmox/settings/components/ServerForm\.test\.tsx$''',
|
||||||
|
'''^\.planning/phases/12-tender-notifications/12-VALIDATION\.md$''',
|
||||||
|
]
|
||||||
|
|
||||||
|
[[allowlists]]
|
||||||
|
description = "Beschriftung der Anmeldemethode in der deutschen Oberflaechen-Textdatei (Zeile 'authMethodPassword': 'Benutzer/Passwort'): nur diese eine gepruefte Zeile."
|
||||||
|
condition = "AND"
|
||||||
|
targetRules = ["generic-api-key"]
|
||||||
|
paths = ['''^apps/web/src/messages/de\.json$''']
|
||||||
|
regexTarget = "line"
|
||||||
|
regexes = ['''"authMethodPassword"\s*:\s*"Benutzer/Passwort"''']
|
||||||
|
|
||||||
|
[[allowlists]]
|
||||||
|
description = "curl-Beispiel gegen den lokalen Wegwerf-Nextcloud-Testcontainer in einer Recherche-Notiz (Platzhalter statt Zugangsdaten)."
|
||||||
|
targetRules = ["curl-auth-user"]
|
||||||
|
paths = ['''^\.planning/quick/261008-mzu-modul-nextcloud-dateien-eigenstaendiger-/261008-mzu-RESEARCH\.md$''']
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Semgrep-Ausnahmen (quick-261009-p0m). Test- und Hilfsdateien sind keine Auslieferung;
|
||||||
|
# so bleibt der Bericht auf echten Quelltext konzentriert.
|
||||||
|
:include .gitignore
|
||||||
|
|
||||||
|
# Testschluessel und Testdaten
|
||||||
|
apps/api/src/cert-manager/__fixtures__/
|
||||||
|
apps/api/src/tenders/__fixtures__/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
*.spec.ts
|
||||||
|
*.test.ts
|
||||||
|
*.test.tsx
|
||||||
|
|
||||||
|
# Planungsnotizen und Pruefskripte
|
||||||
|
.planning/
|
||||||
+71
-3
@@ -119,7 +119,7 @@ hardcoden.
|
|||||||
|
|
||||||
Die CI/CD-Pipeline (`.gitea/workflows/ci.yml`) laeuft bei jedem Push auf die
|
Die CI/CD-Pipeline (`.gitea/workflows/ci.yml`) laeuft bei jedem Push auf die
|
||||||
Zweige `main` und `live` sowie bei jedem Tag `v*` (z. B. `v1.0.0`) und besteht
|
Zweige `main` und `live` sowie bei jedem Tag `v*` (z. B. `v1.0.0`) und besteht
|
||||||
aus vier aufeinander aufbauenden Jobs:
|
aus fuenf Jobs (vier bauen aufeinander auf, der fuenfte ist ein reiner Bericht):
|
||||||
|
|
||||||
1. **quality** -- Lint und TypeScript Type-Check (Lint ist derzeit ein Leerlauf,
|
1. **quality** -- Lint und TypeScript Type-Check (Lint ist derzeit ein Leerlauf,
|
||||||
siehe WINDOWS #35; der Type-Check ist echt)
|
siehe WINDOWS #35; der Type-Check ist echt)
|
||||||
@@ -128,9 +128,12 @@ aus vier aufeinander aufbauenden Jobs:
|
|||||||
und bei Tags `v*`, siehe unten)
|
und bei Tags `v*`, siehe unten)
|
||||||
4. **publish** -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry
|
4. **publish** -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry
|
||||||
veroeffentlichen
|
veroeffentlichen
|
||||||
|
5. **security** -- Sicherheitspruefung (nur Bericht): nach `publish`, nur auf `main`
|
||||||
|
und bei Tags `v*`; bricht nie ab, blockiert nie etwas, bekommt kein Secret
|
||||||
|
|
||||||
Ablauf: `quality` -> `test` -> `desktop` -> `publish` (jeder Job nur bei Erfolg
|
Ablauf: `quality` -> `test` -> `desktop` -> `publish` -> `security` (jeder Job nur
|
||||||
des vorherigen; `desktop` selbst laeuft nur, wenn die `if`-Bedingung zutrifft
|
bei Erfolg des vorherigen; `security` ist nur ein Bericht und steht in keinem
|
||||||
|
`needs` eines anderen Jobs; `desktop` selbst laeuft nur, wenn die `if`-Bedingung zutrifft
|
||||||
-- auf einem Push nach `live` ohne Tag entfaellt der Job, `publish` startet in
|
-- auf einem Push nach `live` ohne Tag entfaellt der Job, `publish` startet in
|
||||||
diesem Fall trotzdem, weil `needs: desktop` bei einem uebersprungenen Job nicht
|
diesem Fall trotzdem, weil `needs: desktop` bei einem uebersprungenen Job nicht
|
||||||
blockiert). Der Job `publish` besteht aus sechs Schritten:
|
blockiert). Der Job `publish` besteht aus sechs Schritten:
|
||||||
@@ -264,6 +267,58 @@ Hinweis: Die fruehere Entscheidung D-13 (Images lokal bauen, keine Registry) ist
|
|||||||
ueberholt -- seit der Einfuehrung der Gitea-Registry werden Images gepusht und von
|
ueberholt -- seit der Einfuehrung der Gitea-Registry werden Images gepusht und von
|
||||||
den Servern per `docker compose pull` geholt.
|
den Servern per `docker compose pull` geholt.
|
||||||
|
|
||||||
|
### Job `security`: Sicherheitspruefung (nur Bericht)
|
||||||
|
|
||||||
|
Der Job `security` (quick-261009-p0m) laeuft nach `publish`, damit er die Veroeffentlichung
|
||||||
|
nie verzoegert oder verhindert. Er laeuft nur auf `main` und bei Tags `v*`; die
|
||||||
|
Bedingung steht ausdruecklich am Job, weil ein uebersprungenes `needs` in Gitea nicht
|
||||||
|
blockiert (sonst liefe er auch nach einem Push auf `live`). Er hat `continue-on-error`,
|
||||||
|
ein Zeitlimit von 30 Minuten (ein haengender Scan haelt den einzigen Runner nicht fest)
|
||||||
|
und kein Secret. Alles steckt in einem Skript, das auch lokal laeuft:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh --print-plan # ohne Netz
|
||||||
|
GITHUB_REF=refs/heads/main sh .gitea/scripts/security-scan.sh all
|
||||||
|
```
|
||||||
|
|
||||||
|
**Was geprueft wird:** gitleaks ueber die gesamte Git-Historie (Zugangsdaten), `pnpm audit
|
||||||
|
--prod` und osv-scanner ueber `pnpm-lock.yaml` und die Desktop-`Cargo.lock` (bekannte
|
||||||
|
Schwachstellen), Semgrep (statische Code-Analyse) und Trivy ueber den Quellstand sowie
|
||||||
|
ueber die frisch gebauten Abbilder `api` und `web` (`main` -> `:beta`, Tag `v*` -> `:live`).
|
||||||
|
Die Abbilder liegen nach `publish` im Docker-Daemon des Hosts (Socket-Mount, Abschnitt 5)
|
||||||
|
und werden direkt von dort gelesen.
|
||||||
|
|
||||||
|
**Quellstand:** Die Quellpruefungen lesen einen `git archive HEAD`-Export in einem
|
||||||
|
temporaeren Ordner, gitleaks nur die Git-Historie. Es wird also genau der eingecheckte
|
||||||
|
Stand geprueft; nicht eingecheckte Dateien eines Arbeitsordners erreichen nie ein
|
||||||
|
Werkzeug, einen Bericht oder ein Artefakt.
|
||||||
|
|
||||||
|
**Angepinnte Werkzeuge:** gitleaks 8.30.1, Trivy 0.75.0, osv-scanner 2.6.0, Semgrep 1.180.0
|
||||||
|
(`pipx`), pnpm 9.15.0. Die drei Programme werden von der offiziellen
|
||||||
|
GitHub-Veroeffentlichung geladen und vor jeder Benutzung gegen eine SHA256-Summe im Skript
|
||||||
|
geprueft (auch das zwischengespeicherte Archiv); stimmt die Summe nicht oder fehlt das
|
||||||
|
Netz, wird nur dieses Werkzeug uebersprungen. Marktplatz-Aktionen fuer Scanner werden
|
||||||
|
bewusst nicht benutzt (veraenderliche Etiketten). **Version anheben:** im Kopf von
|
||||||
|
`security-scan.sh` die Version, die URL und die SHA256-Summe aus der offiziellen
|
||||||
|
Pruefsummendatei der Veroeffentlichung gemeinsam austauschen und einmal lokal
|
||||||
|
`sh .gitea/scripts/security-scan.sh all` laufen lassen. Werkzeuge und die Trivy-Datenbank
|
||||||
|
liegen im persistenten Zwischenspeicher des Runners (`/opt/hostedtoolcache/tessera-security`);
|
||||||
|
der Layer-Zwischenspeicher von Trivy wird nach jedem Lauf geloescht, damit die Platte nicht
|
||||||
|
vollaeuft.
|
||||||
|
|
||||||
|
**Ausnahmelisten:** `.gitleaks.toml` (geprueft harmlose Treffer: Testschluessel des
|
||||||
|
Zertifikatsmanagers, Schluessel-Ausschnitte in Tests und Notizen) und `.semgrepignore`
|
||||||
|
(Tests, Testdaten, Planungsnotizen). Ein neuer Treffer wird nie vorsorglich freigegeben,
|
||||||
|
sondern erst geprueft.
|
||||||
|
|
||||||
|
**Wo man das Ergebnis sieht:** Im Protokoll des Jobs stehen je Werkzeug eine Zeile
|
||||||
|
`SECURITY-SUMMARY ...` (nur Zahlen) und am Ende `SECURITY-SUMMARY fertig (nur Bericht,
|
||||||
|
Exit 0)`; dieselben Zeilen stehen in `summary.txt`. Die Rohberichte (JSON) haengen,
|
||||||
|
soweit der Runner es zulaesst, als Artefakt `sicherheitsberichte` (30 Tage) am Lauf. Das
|
||||||
|
Artefakt ist nur "best effort" (Gitea lehnt `upload-artifact@v4` ab, es laeuft `@v3`);
|
||||||
|
verlassen Sie sich auf die Zeilen im Protokoll. Der Job braucht ungefaehr fuenf bis acht
|
||||||
|
Minuten nach `publish`.
|
||||||
|
|
||||||
## 5. Sicherheitshinweise
|
## 5. Sicherheitshinweise
|
||||||
|
|
||||||
### Docker Socket
|
### Docker Socket
|
||||||
@@ -420,3 +475,16 @@ ausdruecklich auf die oeffentliche Adresse gesetzt wird. Fehlende Anhaenge
|
|||||||
lassen sich jederzeit vom Host nachtragen:
|
lassen sich jederzeit vom Host nachtragen:
|
||||||
`GITEA_TOKEN=... DESKTOP_DIST=<Ordner mit manifest.json> sh .gitea/scripts/publish-release.sh --tag vX.Y.Z`
|
`GITEA_TOKEN=... DESKTOP_DIST=<Ordner mit manifest.json> sh .gitea/scripts/publish-release.sh --tag vX.Y.Z`
|
||||||
(die Pakete liegen im API-Abbild unter `/app/desktop-dist`).
|
(die Pakete liegen im API-Abbild unter `/app/desktop-dist`).
|
||||||
|
|
||||||
|
### Job `security` ist rot oder gelb
|
||||||
|
|
||||||
|
Der Job ist ein reiner Bericht und beeinflusst weder Abbilder noch Freigaben (er laeuft
|
||||||
|
erst nach `publish` und steht in keinem `needs`). Rot oder gelb heisst deshalb nie, dass
|
||||||
|
etwas nicht ausgeliefert wurde. Zur Ursache im Job-Protokoll die Zeilen
|
||||||
|
`SECURITY-SUMMARY <werkzeug> skipped reason=...` lesen: `download` oder `checksum` (Werkzeug
|
||||||
|
nicht ladbar bzw. Summe stimmt nicht -- Netz pruefen, nie die Summe ohne Pruefung
|
||||||
|
austauschen), `kein-pipx` / `install` (Semgrep), `kein-pnpm`, `abbild-fehlt` (das Abbild
|
||||||
|
liegt nicht im Docker-Daemon des Hosts), `fehler` (Werkzeug ist abgestuerzt oder hat
|
||||||
|
nichts geschrieben, z. B. Zeitlimit), `kein-bericht`. Ein haengender Lauf endet nach 30
|
||||||
|
Minuten von selbst. Fehlt nur das Artefakt, ist das erwartbar (best effort); die Zahlen
|
||||||
|
stehen trotzdem im Protokoll.
|
||||||
|
|||||||
Reference in New Issue
Block a user