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:
2026-10-09 19:20:58 +02:00
parent d15a470c5a
commit 3d00be8292
7 changed files with 782 additions and 3 deletions
+71 -3
View File
@@ -119,7 +119,7 @@ hardcoden.
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
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,
siehe WINDOWS #35; der Type-Check ist echt)
@@ -128,9 +128,12 @@ aus vier aufeinander aufbauenden Jobs:
und bei Tags `v*`, siehe unten)
4. **publish** -- Docker Images mit Versionsstempel bauen und in die Gitea-Registry
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
des vorherigen; `desktop` selbst laeuft nur, wenn die `if`-Bedingung zutrifft
Ablauf: `quality` -> `test` -> `desktop` -> `publish` -> `security` (jeder Job nur
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
diesem Fall trotzdem, weil `needs: desktop` bei einem uebersprungenen Job nicht
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
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
### Docker Socket
@@ -420,3 +475,16 @@ ausdruecklich auf die oeffentliche Adresse gesetzt wird. Fehlende Anhaenge
lassen sich jederzeit vom Host nachtragen:
`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`).
### 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.