Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf8f905767 | ||
|
|
f02de2b6c8 | ||
|
|
9de84f6994 | ||
|
|
77ecf42365 | ||
|
|
ab7a03386c | ||
|
|
87f5fe20f9 |
@@ -0,0 +1,38 @@
|
|||||||
|
name: Supply-Chain-Scan (Go)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "**/*.go"
|
||||||
|
- "go.mod"
|
||||||
|
- "go.sum"
|
||||||
|
pull_request: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
govulncheck:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "1.22"
|
||||||
|
- name: govulncheck installieren
|
||||||
|
# @latest kann eine govulncheck-Version verlangen, die neuer ist als
|
||||||
|
# die hier verwendete Go-Toolchain (z.B. "requires go >= 1.25.0") —
|
||||||
|
# feste, bekannt kompatible Version statt @latest (siehe
|
||||||
|
# scripts/verify-supply-chain-gate.sh, Fund vom 2026-08-28 auf dem Testhost).
|
||||||
|
run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.3
|
||||||
|
- name: Go-Module auf bekannte Schwachstellen pruefen
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# pipefail ist Pflicht: sonst liefert "govulncheck | tee" den Exit-Code
|
||||||
|
# von tee (immer 0) statt den von govulncheck zurueck — der Gate-Zweck
|
||||||
|
# (Akzeptanzkriterium 3: Fund blockiert den Merge) waere sonst wirkungslos.
|
||||||
|
set -o pipefail
|
||||||
|
govulncheck ./... | tee govulncheck-report.txt
|
||||||
|
- name: Scan-Bericht als Artefakt ablegen
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: govulncheck-report
|
||||||
|
path: govulncheck-report.txt
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: Supply-Chain-Scan (npm)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "web/**/package.json"
|
||||||
|
- "web/**/package-lock.json"
|
||||||
|
pull_request: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
npm-audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
- name: Alle Next.js-Frontends auf bekannte Schwachstellen pruefen
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
status=0
|
||||||
|
for pkg in $(find web -maxdepth 2 -name package.json); do
|
||||||
|
dir=$(dirname "$pkg")
|
||||||
|
echo "=== npm audit: $dir ==="
|
||||||
|
(cd "$dir" && npm install --package-lock-only --no-audit --no-fund \
|
||||||
|
&& npm audit --audit-level=high) || status=1
|
||||||
|
done
|
||||||
|
# Erst nach Durchlauf ALLER Frontends fehlschlagen (Akzeptanzkriterium 2/3):
|
||||||
|
# ein einzelner Fund darf nicht verhindern, dass die uebrigen Frontends
|
||||||
|
# ebenfalls geprueft und im Bericht sichtbar werden.
|
||||||
|
exit $status
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
name: Core-Pflichttest-Gate
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pflichttest-gate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.22"
|
|
||||||
- name: Gate bauen
|
|
||||||
run: go build -o /tmp/pflichttestgate ./cmd/pflichttestgate
|
|
||||||
- name: Geänderte Dateien gegen Pflichttest-Regel pruefen
|
|
||||||
run: |
|
|
||||||
git diff --name-only "origin/${{ github.base_ref }}...HEAD" | /tmp/pflichttestgate
|
|
||||||
@@ -44,20 +44,53 @@ Keine Commits in dieser Session.
|
|||||||
Keine Änderungen ermittelbar.
|
Keine Änderungen ermittelbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
## 2026-08-28 22:37 – 22:41 (3m)
|
## 2026-08-28 23:00 – 23:01 (0m)
|
||||||
**Beschreibung:** Claude Code Session
|
**Beschreibung:** Claude Code Session
|
||||||
**Projekt:** nexarch
|
**Projekt:** nexarch
|
||||||
|
|
||||||
### Commits
|
### Commits
|
||||||
- 5a0d074 QA-01: teststrategie-core (pflichttest-gate, testdb-isolationsnachweis, ci-workflow)
|
- ab7a033 OPS-06: fix — fixture ruft tatsaechlich verwundbaren symbolpfad auf (ParseAcceptLanguage statt Parse, GO-2022-1059 statt falscher advisory-id)
|
||||||
|
|
||||||
### Geänderte Dateien
|
### Geänderte Dateien
|
||||||
- .gitea/workflows/pflichttest-gate.yml | 20 ++++++++++++++++++++
|
- testdata/vulnfixture-go/go.mod | 8 +++++---
|
||||||
- cmd/pflichttestgate/main.go | 41 +++++++++++++++++++++++++++++++++++++++++
|
- testdata/vulnfixture-go/main.go | 7 +++++--
|
||||||
- docs/TESTSTRATEGIE-CORE.md | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
- internal/pflichttestgate/gate.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
---
|
||||||
- internal/pflichttestgate/gate_test.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
## 2026-08-28 23:02 – 23:03 (0m)
|
||||||
- internal/testdbisolation/isolation.go | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
**Beschreibung:** Claude Code Session
|
||||||
- internal/testdbisolation/isolation_test.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
**Projekt:** code
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 77ecf42 OPS-06: fix — verify-supply-chain-gate.sh unterscheidet jetzt echten Fund von Werkzeugfehler (pruefte vorher nur exit-code, 'command not found' galt faelschlich als bestanden)
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- DEVLOG.md | 12 ++++++++++++
|
||||||
|
- scripts/verify-supply-chain-gate.sh | 52 +++++++++++++++++++++++++++++++++++++---------------
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-08-28 23:04 – 23:05 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** code
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
- 9de84f6 OPS-06: fix — fehlendes fail=1 im govulncheck-unavailable-zweig, govulncheck-version gepinnt statt @latest (Go-Versionskonflikt auf 131 gefunden)
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- .gitea/workflows/govulncheck.yml | 6 +++++-
|
||||||
|
- DEVLOG.md | 12 ++++++++++++
|
||||||
|
- scripts/verify-supply-chain-gate.sh | 13 +++++++++++--
|
||||||
|
|
||||||
|
---
|
||||||
|
## 2026-08-28 23:05 – 23:05 (0m)
|
||||||
|
**Beschreibung:** Claude Code Session
|
||||||
|
**Projekt:** code
|
||||||
|
|
||||||
|
### Commits
|
||||||
|
Keine Commits in dieser Session.
|
||||||
|
|
||||||
|
### Geänderte Dateien
|
||||||
|
- .gitea/workflows/govulncheck.yml | 6 +++++-
|
||||||
|
- DEVLOG.md | 12 ++++++++++++
|
||||||
|
- scripts/verify-supply-chain-gate.sh | 13 +++++++++++--
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
// Command pflichttestgate ist das CI-Gate aus docs/TESTSTRATEGIE-CORE.md Abschnitt 5.
|
|
||||||
// Aufruf: pflichttestgate <geänderte-Dateien-getrennt-durch-Zeilenumbruch-via-stdin>
|
|
||||||
// Beendet sich mit Exit-Code 1 und einer Liste der Verstöße, wenn ein sicherheitskritisches
|
|
||||||
// Package ohne begleitende Testdatei geändert wurde.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"gitea.perlbach24.de/scripte/nexarch/internal/pflichttestgate"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
var changedFiles []string
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if line != "" {
|
|
||||||
changedFiles = append(changedFiles, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "pflichttestgate: Eingabe konnte nicht gelesen werden: %v\n", err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
violations := pflichttestgate.CheckDiff(changedFiles)
|
|
||||||
if len(violations) == 0 {
|
|
||||||
fmt.Println("pflichttestgate: bestanden — alle sicherheitskritischen Änderungen haben begleitende Tests.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "pflichttestgate: FEHLGESCHLAGEN — Pflichttest fehlt für:")
|
|
||||||
for _, v := range violations {
|
|
||||||
fmt.Fprintf(os.Stderr, " - Package %q (Datei %q hat keine begleitende _test.go-Änderung)\n", v.Package, v.ChangedFile)
|
|
||||||
}
|
|
||||||
fmt.Fprintln(os.Stderr, "\nSiehe docs/TESTSTRATEGIE-CORE.md Abschnitt 3.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# NEXARCH Core – Teststrategie
|
|
||||||
|
|
||||||
Stand: 2026-08-28. Ticket: QA-01.
|
|
||||||
|
|
||||||
## 1. Warum dieses Dokument existiert
|
|
||||||
|
|
||||||
archivdms hatte zur Zeit der Bestandsaufnahme nur 2 `_test.go`-Dateien in ganz `internal/`, davon 0 für
|
|
||||||
`auth`/`storage`/`permissions.go`. archivmail testete 2 von 18 Modulen. Beide Lücken wurden erst im
|
|
||||||
Betrieb entdeckt, nicht vor dem Merge. NEXARCH Core übernimmt daraus einen Grundsatz: **Testpflicht für
|
|
||||||
Auth, Tenant-Scoping und Policy-Enforcement ist ein Merge-Gate, keine Nachrüstung.**
|
|
||||||
|
|
||||||
## 2. Testpyramide
|
|
||||||
|
|
||||||
| Ebene | Werkzeug | Umfang |
|
|
||||||
|---|---|---|
|
|
||||||
| Unit | `go test` (Standardbibliothek) | Einzelne Funktionen/Typen, keine externe Abhängigkeit (DB, Netzwerk) |
|
|
||||||
| Integration | `go test` gegen echte PostgreSQL-Instanz (`nexarch_test`-Rolle) | Repository-/Handler-Schicht, Tenant-Scoping, Policy-Enforcement |
|
|
||||||
| Vertragstests | `internal/contracttest` (siehe QA-07) | Öffentliche API-Verträge zwischen Core und Modulen |
|
|
||||||
| Last-/Leistungstests | `internal/loadtest` (siehe QA-08) | Mehrmodul-Last, JWT-Verifikation, Connection-Pooling |
|
|
||||||
| Extern | Core `QA-06` Penetrationstest | Vor Produktivbetrieb, außerhalb dieses CI-Gates |
|
|
||||||
|
|
||||||
Diese Kachel (QA-01) legt die Pflichtebenen fest und erzwingt sie technisch für die drei sicherheitskritischsten Bereiche; sie ersetzt nicht QA-07/QA-08, die eigene, bereits umgesetzte CI-Gates haben.
|
|
||||||
|
|
||||||
## 3. Pflichttests als Merge-Gate
|
|
||||||
|
|
||||||
Verbindlich für jeden Pull Request, der Dateien in einem der folgenden Bereiche ändert:
|
|
||||||
|
|
||||||
- **Auth** (`internal/auth/`, `internal/iam/`, Login/Session/2FA/SSO-Pakete)
|
|
||||||
- **Tenant-Scoping** (`internal/tenant/`, jede Repository-Schicht mit `tenant_id`-Filterung)
|
|
||||||
- **Policy-Enforcement** (`internal/rbac/`, `internal/policy/`, jede Autorisierungsprüfung)
|
|
||||||
|
|
||||||
Regel: **jede geänderte `.go`-Datei in einem dieser Bereiche muss von einer geänderten oder neuen
|
|
||||||
`_test.go`-Datei im selben Package begleitet sein.** Das CI-Gate (Abschnitt 5) prüft das automatisiert
|
|
||||||
und blockiert den Merge, wenn die Regel verletzt ist — analog zum bereits etablierten Sprintf-Verbot für
|
|
||||||
SQL (siehe `SICHERHEITSKONZEPT.md`), nur als technisch erzwungene statt nur dokumentierte Regel.
|
|
||||||
|
|
||||||
Diese Regel gilt projektweit für alle sieben Boards, nicht nur Core — siehe die entsprechenden
|
|
||||||
Akzeptanzkriterien in den `QA-01`-Tickets von DMS, Mail, Archive, Workflow, AI, Connect
|
|
||||||
(`SICHERHEITSKONZEPT.md` Abschnitt 11, Punkt 8, 2026-08-28 geklärt).
|
|
||||||
|
|
||||||
## 4. Testdatenbank-Strategie
|
|
||||||
|
|
||||||
Isolation zwischen parallelen Testläufen ist die zentrale Lehre aus dem bisherigen Testbetrieb
|
|
||||||
(siehe Projekt-Testinfrastruktur): mehrere Go-Testpakete teilen sich dieselbe physische PostgreSQL-Instanz
|
|
||||||
auf dem Testhost, aber jedes Paket braucht einen isolierten Datenbestand.
|
|
||||||
|
|
||||||
- **Rolle `nexarch_test`**: `CREATEDB`, kein Superuser, einmalig eingerichtet über `scripts/setup-test-env.sh`.
|
|
||||||
- **Reset vor jedem Testlauf**: `scripts/reset-test-env.sh` droppt die geteilte `tenants`-Registry-Tabelle
|
|
||||||
und alle `tenant_*`-Datenbanken in der `postgres`-Wartungs-DB. Nötig, weil verschiedene Branches
|
|
||||||
unterschiedliche Registry-Schemata erwarten, aber dieselbe physische Instanz teilen.
|
|
||||||
- **`-p 1` ist Pflicht** für `go test ./...`, sobald mehrere Pakete gegen die geteilte Registry-Tabelle
|
|
||||||
testen (z. B. `internal/tenant` + `internal/migrate`). Ohne `-p 1` laufen Paket-Testbinaries parallel
|
|
||||||
gegen dieselbe physische PostgreSQL-Instanz, ihre Registry-Einträge/DBs kollidieren
|
|
||||||
(falsche Tenant-Zählungen, „database already exists"-Fehler).
|
|
||||||
- **Isolation innerhalb eines Testlaufs**: jeder Test, der eine Tenant-Datenbank braucht, provisioniert
|
|
||||||
seine eigene, eindeutig benannte `tenant_*`-DB über dieselbe Provisionierungs-Logik wie die
|
|
||||||
Anwendung selbst (`TEN-01`) und räumt sie in einem `t.Cleanup()` wieder ab — keine geteilten
|
|
||||||
Fixture-Datenbanken zwischen Testfällen.
|
|
||||||
|
|
||||||
Siehe `internal/testdbisolation/isolation_test.go` (dieses Ticket) für den automatisierten Nachweis,
|
|
||||||
dass zwei parallel laufende Tenant-Provisionierungen sich nicht gegenseitig sehen.
|
|
||||||
|
|
||||||
## 5. CI-Gate
|
|
||||||
|
|
||||||
`.gitea/workflows/pflichttest-gate.yml` führt `cmd/pflichttestgate` gegen den PR-Diff aus
|
|
||||||
(`git diff --name-only origin/<base>...HEAD`). Das Programm:
|
|
||||||
|
|
||||||
1. Filtert die geänderten Dateien auf die in Abschnitt 3 genannten Pfad-Muster.
|
|
||||||
2. Prüft je betroffenem Go-Package, ob mindestens eine `_test.go`-Datei desselben Packages ebenfalls
|
|
||||||
im Diff enthalten ist.
|
|
||||||
3. Beendet sich mit Exit-Code 1 und einer Liste der betroffenen Packages ohne Teständerung, wenn die
|
|
||||||
Regel verletzt ist — der CI-Job schlägt dann fehl, der Merge ist blockiert.
|
|
||||||
|
|
||||||
Negativtest des Gates selbst: `internal/pflichttestgate/gate_test.go` enthält einen Testfall, der einen
|
|
||||||
Diff mit geänderter `internal/auth/login.go` ohne begleitende Testdatei simuliert und erwartet, dass das
|
|
||||||
Gate das als Verstoß erkennt (Prüfung 1 dieses Tickets).
|
|
||||||
|
|
||||||
## 6. Dokumentation & Gegenlesen
|
|
||||||
|
|
||||||
Dieses Dokument ist von einer zweiten Person gegenzulesen, bevor die Kachel als abgeschlossen gilt
|
|
||||||
(Akzeptanzkriterium 3 / Prüfung 3). Fund/Freigabe wird im Pull Request vermerkt.
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// Package pflichttestgate erzwingt die in docs/TESTSTRATEGIE-CORE.md Abschnitt 3 festgelegte Regel:
|
|
||||||
// jede geänderte Go-Datei in einem sicherheitskritischen Bereich (Auth, Tenant-Scoping, Policy)
|
|
||||||
// muss von einer geänderten oder neuen _test.go-Datei im selben Package begleitet sein.
|
|
||||||
package pflichttestgate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// sensitivePathPatterns beschreibt die Bereiche aus TESTSTRATEGIE-CORE.md Abschnitt 3.
|
|
||||||
var sensitivePathPatterns = []*regexp.Regexp{
|
|
||||||
regexp.MustCompile(`(^|/)internal/auth/`),
|
|
||||||
regexp.MustCompile(`(^|/)internal/iam/`),
|
|
||||||
regexp.MustCompile(`(^|/)internal/tenant/`),
|
|
||||||
regexp.MustCompile(`(^|/)internal/rbac/`),
|
|
||||||
regexp.MustCompile(`(^|/)internal/policy/`),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Violation beschreibt ein Package mit sicherheitskritischer Änderung ohne begleitende Testdatei.
|
|
||||||
type Violation struct {
|
|
||||||
Package string
|
|
||||||
ChangedFile string
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSensitive meldet, ob eine geänderte Datei in einen der Pflichttest-Bereiche fällt.
|
|
||||||
func isSensitive(file string) bool {
|
|
||||||
if !strings.HasSuffix(file, ".go") || strings.HasSuffix(file, "_test.go") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, re := range sensitivePathPatterns {
|
|
||||||
if re.MatchString(file) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// CheckDiff prüft eine Liste geänderter Dateipfade (z. B. aus `git diff --name-only`) gegen die
|
|
||||||
// Pflichttest-Regel und gibt für jedes betroffene Package ohne begleitende Testdatei eine Violation
|
|
||||||
// zurück. Ein leeres Ergebnis bedeutet: Gate besteht.
|
|
||||||
func CheckDiff(changedFiles []string) []Violation {
|
|
||||||
sensitiveByPkg := map[string]string{} // package -> erste sensible Datei (fürs Reporting)
|
|
||||||
testTouchedPkgs := map[string]bool{}
|
|
||||||
|
|
||||||
for _, f := range changedFiles {
|
|
||||||
pkg := path.Dir(f)
|
|
||||||
if strings.HasSuffix(f, "_test.go") {
|
|
||||||
testTouchedPkgs[pkg] = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isSensitive(f) {
|
|
||||||
if _, seen := sensitiveByPkg[pkg]; !seen {
|
|
||||||
sensitiveByPkg[pkg] = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var violations []Violation
|
|
||||||
for pkg, file := range sensitiveByPkg {
|
|
||||||
if !testTouchedPkgs[pkg] {
|
|
||||||
violations = append(violations, Violation{Package: pkg, ChangedFile: file})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return violations
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
package pflichttestgate
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
// Prüfung 1 (QA-01): CI-Lauf mit absichtlich fehlendem Pflichttest schlägt kontrolliert fehl —
|
|
||||||
// Negativtest des Gates selbst.
|
|
||||||
func TestCheckDiff_BlocksAuthChangeWithoutTest(t *testing.T) {
|
|
||||||
changed := []string{
|
|
||||||
"internal/auth/login.go",
|
|
||||||
"internal/apiserver/handler.go", // unkritisch, keine Testpflicht
|
|
||||||
}
|
|
||||||
|
|
||||||
violations := CheckDiff(changed)
|
|
||||||
|
|
||||||
if len(violations) != 1 {
|
|
||||||
t.Fatalf("erwartet 1 Verstoß, bekommen %d: %+v", len(violations), violations)
|
|
||||||
}
|
|
||||||
if violations[0].Package != "internal/auth" {
|
|
||||||
t.Errorf("erwartetes Package internal/auth, bekommen %q", violations[0].Package)
|
|
||||||
}
|
|
||||||
if violations[0].ChangedFile != "internal/auth/login.go" {
|
|
||||||
t.Errorf("erwartete Datei internal/auth/login.go, bekommen %q", violations[0].ChangedFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckDiff_PassesWhenTestFileAccompaniesChange(t *testing.T) {
|
|
||||||
changed := []string{
|
|
||||||
"internal/auth/login.go",
|
|
||||||
"internal/auth/login_test.go",
|
|
||||||
}
|
|
||||||
|
|
||||||
violations := CheckDiff(changed)
|
|
||||||
|
|
||||||
if len(violations) != 0 {
|
|
||||||
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", violations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckDiff_PassesForNewTestFileEvenWithoutSourceChange(t *testing.T) {
|
|
||||||
// Nachtraeglich ergaenzter Test fuer bestehenden Code ist erlaubt/erwuenscht.
|
|
||||||
changed := []string{"internal/tenant/registry_test.go"}
|
|
||||||
|
|
||||||
violations := CheckDiff(changed)
|
|
||||||
|
|
||||||
if len(violations) != 0 {
|
|
||||||
t.Fatalf("erwartet keinen Verstoß, bekommen %+v", violations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckDiff_IgnoresUnrelatedPackages(t *testing.T) {
|
|
||||||
changed := []string{"internal/config/config.go", "internal/db/db.go"}
|
|
||||||
|
|
||||||
violations := CheckDiff(changed)
|
|
||||||
|
|
||||||
if len(violations) != 0 {
|
|
||||||
t.Fatalf("erwartet keinen Verstoß fuer unkritische Pakete, bekommen %+v", violations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckDiff_CoversAllSensitiveAreas(t *testing.T) {
|
|
||||||
cases := []string{
|
|
||||||
"internal/auth/session.go",
|
|
||||||
"internal/iam/user.go",
|
|
||||||
"internal/tenant/provisioner.go",
|
|
||||||
"internal/rbac/enforcer.go",
|
|
||||||
"internal/policy/decision.go",
|
|
||||||
}
|
|
||||||
for _, f := range cases {
|
|
||||||
violations := CheckDiff([]string{f})
|
|
||||||
if len(violations) != 1 {
|
|
||||||
t.Errorf("Datei %q sollte als sensibel erkannt werden, Verstöße: %+v", f, violations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckDiff_IgnoresNonGoFiles(t *testing.T) {
|
|
||||||
changed := []string{"internal/auth/README.md"}
|
|
||||||
|
|
||||||
violations := CheckDiff(changed)
|
|
||||||
|
|
||||||
if len(violations) != 0 {
|
|
||||||
t.Fatalf("Nicht-Go-Dateien duerfen keine Testpflicht ausloesen, bekommen %+v", violations)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
// Package testdbisolation demonstriert und prüft die in docs/TESTSTRATEGIE-CORE.md Abschnitt 4
|
|
||||||
// beschriebene Testdatenbank-Isolation: jeder Test, der eine Tenant-Datenbank braucht,
|
|
||||||
// provisioniert seine eigene, eindeutig benannte DB und räumt sie über Cleanup wieder ab —
|
|
||||||
// keine geteilten Fixture-Datenbanken zwischen parallelen Testläufen.
|
|
||||||
package testdbisolation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProvisionEphemeralDB legt unter dem gegebenen Admin-DSN eine neue, eindeutig benannte Datenbank an
|
|
||||||
// und liefert eine Cleanup-Funktion, die sie wieder droppt. adminDSN muss auf eine Wartungs-DB
|
|
||||||
// (z. B. "postgres") zeigen, name muss ein gültiger, bereits eindeutiger Bezeichner sein.
|
|
||||||
func ProvisionEphemeralDB(ctx context.Context, adminDSN, name string) (cleanup func(context.Context) error, err error) {
|
|
||||||
conn, err := pgx.Connect(ctx, adminDSN)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("verbindung zur wartungs-db fehlgeschlagen: %w", err)
|
|
||||||
}
|
|
||||||
defer conn.Close(ctx)
|
|
||||||
|
|
||||||
if _, err := conn.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s", pgx.Identifier{name}.Sanitize())); err != nil {
|
|
||||||
return nil, fmt.Errorf("datenbank %q konnte nicht angelegt werden: %w", name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup = func(cleanupCtx context.Context) error {
|
|
||||||
adminConn, err := pgx.Connect(cleanupCtx, adminDSN)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("cleanup: verbindung fehlgeschlagen: %w", err)
|
|
||||||
}
|
|
||||||
defer adminConn.Close(cleanupCtx)
|
|
||||||
_, err = adminConn.Exec(cleanupCtx, fmt.Sprintf("DROP DATABASE IF EXISTS %s", pgx.Identifier{name}.Sanitize()))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return cleanup, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteMarker schreibt einen eindeutigen Beleg-Datensatz in die angegebene Datenbank (verbindet
|
|
||||||
// per dsnForDB), um im Test nachzuweisen, dass zwei parallele Provisionierungen sich nicht sehen.
|
|
||||||
func WriteMarker(ctx context.Context, dsn, table, value string) error {
|
|
||||||
pool, err := pgxpool.New(ctx, dsn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
if _, err := pool.Exec(ctx, fmt.Sprintf(
|
|
||||||
"CREATE TABLE IF NOT EXISTS %s (value text primary key)", pgx.Identifier{table}.Sanitize(),
|
|
||||||
)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err = pool.Exec(ctx, fmt.Sprintf("INSERT INTO %s (value) VALUES ($1)", pgx.Identifier{table}.Sanitize()), value)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountMarkers zählt die Belegzeilen in der angegebenen Datenbank/Tabelle.
|
|
||||||
func CountMarkers(ctx context.Context, dsn, table string) (int, error) {
|
|
||||||
pool, err := pgxpool.New(ctx, dsn)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
defer pool.Close()
|
|
||||||
|
|
||||||
if _, err := pool.Exec(ctx, fmt.Sprintf(
|
|
||||||
"CREATE TABLE IF NOT EXISTS %s (value text primary key)", pgx.Identifier{table}.Sanitize(),
|
|
||||||
)); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var n int
|
|
||||||
err = pool.QueryRow(ctx, fmt.Sprintf("SELECT count(*) FROM %s", pgx.Identifier{table}.Sanitize())).Scan(&n)
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
package testdbisolation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestParallelProvisioning_AreIsolated ist der automatisierte Nachweis für QA-01 Prüfung 2:
|
|
||||||
// Testdatenbank-Isolation zwischen parallelen Testläufen. Zwei Provisionierungen laufen
|
|
||||||
// gleichzeitig, jede schreibt einen Beleg-Datensatz in ihre eigene DB — keine darf den
|
|
||||||
// Beleg der jeweils anderen sehen.
|
|
||||||
//
|
|
||||||
// Braucht TEST_ADMIN_DSN (siehe Projekt-Testinfrastruktur), läuft auf dem Testhost, nicht lokal
|
|
||||||
// auf der Entwicklungsmaschine (siehe "no installs"-Regel) — wird ohne gesetzte Variable übersprungen.
|
|
||||||
func TestParallelProvisioning_AreIsolated(t *testing.T) {
|
|
||||||
adminDSN := os.Getenv("TEST_ADMIN_DSN")
|
|
||||||
if adminDSN == "" {
|
|
||||||
t.Skip("TEST_ADMIN_DSN nicht gesetzt — Test läuft nur auf dem Testhost, siehe docs/TESTSTRATEGIE-CORE.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
const numParallel = 2
|
|
||||||
dbNames := make([]string, numParallel)
|
|
||||||
for i := range dbNames {
|
|
||||||
dbNames[i] = fmt.Sprintf("qa01_isolation_%d_%d", time.Now().UnixNano(), i)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errs := make([]error, numParallel)
|
|
||||||
cleanups := make([]func(context.Context) error, numParallel)
|
|
||||||
|
|
||||||
for i, name := range dbNames {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(idx int, dbName string) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
cleanup, err := ProvisionEphemeralDB(ctx, adminDSN, dbName)
|
|
||||||
if err != nil {
|
|
||||||
errs[idx] = fmt.Errorf("provisionierung %d fehlgeschlagen: %w", idx, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cleanups[idx] = cleanup
|
|
||||||
|
|
||||||
dbDSN := dsnForDB(adminDSN, dbName)
|
|
||||||
// Jede Goroutine schreibt einen für sie eindeutigen Beleg.
|
|
||||||
if err := WriteMarker(ctx, dbDSN, "isolation_marker", fmt.Sprintf("marker-von-goroutine-%d", idx)); err != nil {
|
|
||||||
errs[idx] = fmt.Errorf("markierung %d fehlgeschlagen: %w", idx, err)
|
|
||||||
}
|
|
||||||
}(i, name)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
for _, cleanup := range cleanups {
|
|
||||||
if cleanup != nil {
|
|
||||||
_ = cleanup(context.Background())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for i, err := range errs {
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("goroutine %d: %v", i, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Isolationsnachweis: jede DB enthält genau ihren eigenen Beleg (1), keinen fremden.
|
|
||||||
for i, name := range dbNames {
|
|
||||||
dbDSN := dsnForDB(adminDSN, name)
|
|
||||||
count, err := CountMarkers(ctx, dbDSN, "isolation_marker")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("zaehlung in db %d fehlgeschlagen: %v", i, err)
|
|
||||||
}
|
|
||||||
if count != 1 {
|
|
||||||
t.Errorf("db %d (%s): erwartet genau 1 eigenen Beleg, gefunden %d — Isolation verletzt", i, name, count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dsnForDB ersetzt den Datenbanknamen im Wartungs-DSN durch den gegebenen Namen.
|
|
||||||
func dsnForDB(adminDSN, dbName string) string {
|
|
||||||
idx := strings.LastIndex(adminDSN, "/")
|
|
||||||
if idx == -1 {
|
|
||||||
return adminDSN
|
|
||||||
}
|
|
||||||
base := adminDSN[:idx+1]
|
|
||||||
rest := adminDSN[idx+1:]
|
|
||||||
if q := strings.Index(rest, "?"); q != -1 {
|
|
||||||
return base + dbName + rest[q:]
|
|
||||||
}
|
|
||||||
return base + dbName
|
|
||||||
}
|
|
||||||
Executable
+79
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# OPS-06 Pruefung 1: beweist, dass govulncheck und npm audit eine absichtlich
|
||||||
|
# verwundbare Testabhaengigkeit tatsaechlich ERKENNEN (nicht nur irgendeinen
|
||||||
|
# Fehler werfen) und mit Exit-Code != 0 enden (= wuerden den Merge blockieren).
|
||||||
|
# Laeuft auf dem Testhost (Go/npm noetig), nicht auf der Entwicklungsmaschine.
|
||||||
|
#
|
||||||
|
# Wichtig: ein Exit-Code != 0 allein ist KEIN Beweis — ein fehlendes Tool
|
||||||
|
# ("command not found", exit 127) sieht fuer ein reines Exit-Code-Gate genauso
|
||||||
|
# aus wie ein echter Fund. Deshalb prueft dieses Skript zusaetzlich, dass die
|
||||||
|
# erwartete Advisory-Kennung tatsaechlich in der Ausgabe steht.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# "go install" legt Binaries in $(go env GOPATH)/bin ab — auf frischen Hosts
|
||||||
|
# ist das nicht zwangslaeufig im PATH (gefunden 2026-08-28 auf dem Testhost:
|
||||||
|
# govulncheck installierte erfolgreich, "command -v govulncheck" schlug danach
|
||||||
|
# trotzdem fehl, weil /root/go/bin nicht im PATH stand). Defensiv ergaenzen,
|
||||||
|
# statt stillschweigend als "Werkzeug fehlt" fehlzuschlagen.
|
||||||
|
export PATH="$PATH:$(go env GOPATH 2>/dev/null)/bin"
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
echo "=== Go-Fixture: golang.org/x/text v0.3.7, GO-2022-1059 (Symbolpfad language.ParseAcceptLanguage) ==="
|
||||||
|
if ! command -v govulncheck >/dev/null 2>&1; then
|
||||||
|
echo "govulncheck fehlt, installiere..."
|
||||||
|
# @latest kann eine govulncheck-Version verlangen, die neuer ist als die
|
||||||
|
# lokal installierte Go-Toolchain (z. B. "requires go >= 1.25.0"). Feste,
|
||||||
|
# bekannt kompatible Version statt @latest, damit die Installation nicht
|
||||||
|
# von der jeweiligen Go-Version des Hosts abhaengt.
|
||||||
|
if ! go install golang.org/x/vuln/cmd/govulncheck@v1.1.3; then
|
||||||
|
echo "FEHLER: govulncheck konnte nicht installiert werden — Pruefung nicht durchfuehrbar, kein Ersatz-'OK'."
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v govulncheck >/dev/null 2>&1; then
|
||||||
|
go_output=$(cd testdata/vulnfixture-go && go mod tidy && govulncheck ./... 2>&1)
|
||||||
|
go_exit=$?
|
||||||
|
echo "$go_output"
|
||||||
|
if [ "$go_exit" -eq 0 ]; then
|
||||||
|
echo "FEHLER: govulncheck hat die bekannte Schwachstelle NICHT erkannt (exit 0 erwartet != 0)"
|
||||||
|
fail=1
|
||||||
|
elif ! grep -q "GO-2022-1059" <<<"$go_output"; then
|
||||||
|
echo "FEHLER: exit code $go_exit ist != 0, aber die erwartete Advisory GO-2022-1059 steht NICHT in der Ausgabe — das ist vermutlich ein Werkzeugfehler (z. B. fehlendes govulncheck, Netzwerkproblem), kein echter Fund. Kein 'OK'."
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "OK: govulncheck hat GO-2022-1059 tatsaechlich als Fund gemeldet, exit code $go_exit (!= 0, Gate wuerde blockieren)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Bug (gefunden 2026-08-28): dieser Zweig druckte vorher nur eine Meldung,
|
||||||
|
# setzte aber "fail" NICHT — das Skript endete trotzdem mit Exit-Code 0 und
|
||||||
|
# "PRUEFUNG 1 BESTANDEN", obwohl der Go-Teil real nicht lief. Exakt derselbe
|
||||||
|
# Fehlerklasse (Werkzeugfehler zaehlt als Erfolg), nur eine Ebene hoeher.
|
||||||
|
echo "FEHLER: govulncheck weiterhin nicht verfuegbar — Go-Teil der Pruefung nicht durchgefuehrt, kein Ersatz-'OK'."
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== npm-Fixture: lodash 4.17.4 (mehrere bekannte kritische CVEs) ==="
|
||||||
|
npm_output=$(cd testdata/vulnfixture-npm && npm install --package-lock-only --no-audit --no-fund 2>&1 \
|
||||||
|
&& npm audit --audit-level=high 2>&1)
|
||||||
|
npm_exit=$?
|
||||||
|
echo "$npm_output"
|
||||||
|
if [ "$npm_exit" -eq 0 ]; then
|
||||||
|
echo "FEHLER: npm audit hat die bekannte Schwachstelle NICHT erkannt (exit 0 erwartet != 0)"
|
||||||
|
fail=1
|
||||||
|
elif ! grep -qiE "severity|vulnerabilit" <<<"$npm_output"; then
|
||||||
|
echo "FEHLER: exit code $npm_exit ist != 0, aber die Ausgabe enthaelt keinen erkennbaren Schwachstellen-Hinweis — vermutlich ein Werkzeugfehler (z. B. fehlendes npm, Netzwerkproblem), kein echter Fund. Kein 'OK'."
|
||||||
|
fail=1
|
||||||
|
else
|
||||||
|
echo "OK: npm audit hat die Schwachstelle tatsaechlich gemeldet, exit code $npm_exit (!= 0, Gate wuerde blockieren)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
if [ "$fail" -eq 0 ]; then
|
||||||
|
echo "PRUEFUNG 1 BESTANDEN: beide Gates erkennen eine absichtlich verwundbare Testabhaengigkeit inhaltlich (nicht nur per Exit-Code) und wuerden blockieren."
|
||||||
|
else
|
||||||
|
echo "PRUEFUNG 1 FEHLGESCHLAGEN: siehe FEHLER oben."
|
||||||
|
fi
|
||||||
|
exit $fail
|
||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
// Absichtlich verwundbares Fixture-Modul fuer OPS-06 Pruefung 1: beweist, dass
|
||||||
|
// der govulncheck-CI-Schritt eine bekannte Schwachstelle tatsaechlich erkennt
|
||||||
|
// und den Lauf mit Exit-Code != 0 beendet. Eigenes go.mod, damit die
|
||||||
|
// veraltete, verwundbare Abhaengigkeit NICHT im Hauptmodul landet.
|
||||||
|
module gitea.perlbach24.de/scripte/nexarch/testdata/vulnfixture-go
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
// golang.org/x/text v0.3.7: GO-2022-1059 — Denial of Service durch
|
||||||
|
// uebermaessigen Ressourcenverbrauch beim Parsen von Accept-Language-Headern
|
||||||
|
// (language.ParseAcceptLanguage). Bewusst auf dieser verwundbaren Version
|
||||||
|
// gepinnt, siehe scripts/verify-supply-chain-gate.sh — main.go ruft gezielt
|
||||||
|
// den verwundbaren Symbolpfad auf, nicht nur irgendeine Funktion des Pakets.
|
||||||
|
require golang.org/x/text v0.3.7
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
// Fixture fuer OPS-06 Pruefung 1 — ruft tatsaechlich in die verwundbare
|
||||||
|
// Funktion hinein, damit govulncheck den Aufrufpfad (nicht nur die
|
||||||
|
// Modul-Abhaengigkeit) als erreichbar erkennt.
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// ParseAcceptLanguage ist der tatsaechlich verwundbare Aufrufpfad
|
||||||
|
// (GO-2022-1059), im Unterschied zu Parse() — govulncheck bewertet
|
||||||
|
// Erreichbarkeit auf Symbol-, nicht nur Paket-Ebene.
|
||||||
|
tags, _, err := language.ParseAcceptLanguage("de-DE,de;q=0.9,en;q=0.8")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(tags)
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "nexarch-vulnfixture-npm",
|
||||||
|
"private": true,
|
||||||
|
"description": "Absichtlich verwundbares Fixture-Paket fuer OPS-06 Pruefung 1 — beweist, dass npm audit eine bekannte Schwachstelle erkennt und den Lauf blockiert. Nicht Teil eines echten NEXARCH-Frontends.",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash": "4.17.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user