syncova-backup/packages/platform/secretscan/secretscan_test.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

171 lines
5.6 KiB
Go

package secretscan
import (
"os"
"path/filepath"
"strings"
"testing"
)
// TestRepositoryContainsNoSecrets durchsucht den gesamten Quellbestand.
//
// **Dies ist der eigentliche Zweck des Pakets.** Der Test läuft bei jedem
// `make test` mit; ein Fremdwerkzeug in einer Anleitung wird nach zwei Wochen
// nicht mehr ausgeführt, und ein Geheimnis, das einmal in der
// Versionsgeschichte liegt, holt niemand mehr zurück.
func TestRepositoryContainsNoSecrets(testInstance *testing.T) {
repositoryRoot := findRepositoryRoot(testInstance)
findings, scanError := Scan(Options{RootPath: repositoryRoot})
if scanError != nil {
testInstance.Fatalf("der Quellbestand ließ sich nicht durchsuchen: %v", scanError)
}
for _, finding := range findings {
testInstance.Errorf("mögliches Geheimnis: %s", finding)
}
if len(findings) > 0 {
testInstance.Logf("Ist ein Fund ein Fehlalarm, kennzeichnen Sie die Zeile mit %q.",
ignoreMarker)
}
}
// TestScannerFindsRealSecrets prueft, dass der Scanner ueberhaupt anschlaegt.
//
// Ohne diesen Test wäre der Test oben wertlos: Ein Scanner, der nichts findet,
// weil seine Muster nicht greifen, ist von einem sauberen Quellbestand nicht zu
// unterscheiden.
func TestScannerFindsRealSecrets(testInstance *testing.T) {
temporaryRoot := testInstance.TempDir()
secretSamples := map[string]string{
"config.go": `var databasePassword = "s3hrGeheim2026!"`,
"deploy.sh": `export AWS_ACCESS_KEY_ID=AKIA4YFAKEKEY7QWERTZ`,
"key.pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAK\n",
"connection.go": `dsn := "postgres://syncova:echtesPasswort@db:5432/syncova"`,
}
for fileName, fileContent := range secretSamples {
if writeError := os.WriteFile(filepath.Join(temporaryRoot, fileName),
[]byte(fileContent), 0o600); writeError != nil {
testInstance.Fatalf("die Datei %q ließ sich nicht anlegen: %v", fileName, writeError)
}
}
findings, scanError := Scan(Options{RootPath: temporaryRoot})
if scanError != nil {
testInstance.Fatalf("der Scan schlug fehl: %v", scanError)
}
foundFiles := make(map[string]bool, len(findings))
for _, finding := range findings {
foundFiles[finding.FilePath] = true
}
for fileName := range secretSamples {
if !foundFiles[fileName] {
testInstance.Errorf("das Geheimnis in %q wurde nicht gefunden", fileName)
}
}
}
// TestScannerIgnoresPlaceholders prueft die Vermeidung von Fehlalarmen.
//
// Ein Prüfwerkzeug, das bei jeder Beispielkonfiguration anschlägt, wird nach
// einer Woche übergangen — und dann fällt auch der echte Fund nicht mehr auf.
func TestScannerIgnoresPlaceholders(testInstance *testing.T) {
temporaryRoot := testInstance.TempDir()
harmlessSamples := map[string]string{
"env.example": `SYNCOVA_DB_PASSWORD=<hier-ihr-passwort-eintragen>`,
"readme.md": "password = \"changeme12345\"",
"template.yaml": `api_key: "${SYNCOVA_API_KEY}"`,
"doc.md": `token = "your-token-here"`,
"compose.yml": `POSTGRES_PASSWORD: beispielpasswort`,
}
for fileName, fileContent := range harmlessSamples {
if writeError := os.WriteFile(filepath.Join(temporaryRoot, fileName),
[]byte(fileContent), 0o600); writeError != nil {
testInstance.Fatalf("die Datei %q ließ sich nicht anlegen: %v", fileName, writeError)
}
}
findings, scanError := Scan(Options{RootPath: temporaryRoot})
if scanError != nil {
testInstance.Fatalf("der Scan schlug fehl: %v", scanError)
}
for _, finding := range findings {
testInstance.Errorf("Fehlalarm auf einen Platzhalter: %s", finding)
}
}
// TestIgnoreMarkerWorks prueft den ausdruecklichen Ausweg.
func TestIgnoreMarkerWorks(testInstance *testing.T) {
temporaryRoot := testInstance.TempDir()
markedContent := `testPassword := "einEchtesTestpasswort" // ` + ignoreMarker
if writeError := os.WriteFile(filepath.Join(temporaryRoot, "test.go"),
[]byte(markedContent), 0o600); writeError != nil {
testInstance.Fatalf("die Datei ließ sich nicht anlegen: %v", writeError)
}
findings, scanError := Scan(Options{RootPath: temporaryRoot})
if scanError != nil {
testInstance.Fatalf("der Scan schlug fehl: %v", scanError)
}
if len(findings) > 0 {
testInstance.Errorf("die gekennzeichnete Zeile wurde trotzdem gemeldet: %s", findings[0])
}
}
// TestExcerptIsShortened prueft, dass ein Fund nicht vollstaendig ausgegeben wird.
//
// Ein Scanner, der das gefundene Geheimnis in voller Länge in ein Prüfprotokoll
// schreibt, hat es soeben ein zweites Mal veröffentlicht — und Prüfprotokolle
// landen in Logdateien, Ticketsystemen und Chats.
func TestExcerptIsShortened(testInstance *testing.T) {
longSecret := strings.Repeat("A", 200)
shortenedExcerpt := shortenExcerpt(longSecret)
if len(shortenedExcerpt) > maximumExcerptLength+len("…") {
testInstance.Errorf("der Ausschnitt ist %d Zeichen lang", len(shortenedExcerpt))
}
if !strings.HasSuffix(shortenedExcerpt, "…") {
testInstance.Error("die Kürzung ist nicht erkennbar")
}
}
// findRepositoryRoot sucht das Wurzelverzeichnis des Projekts.
func findRepositoryRoot(testInstance *testing.T) string {
testInstance.Helper()
currentDirectory, workingDirectoryError := os.Getwd()
if workingDirectoryError != nil {
testInstance.Fatalf("das Arbeitsverzeichnis ließ sich nicht bestimmen: %v",
workingDirectoryError)
}
for depth := 0; depth < 8; depth++ {
if _, statError := os.Stat(filepath.Join(currentDirectory, "go.mod")); statError == nil {
return currentDirectory
}
parentDirectory := filepath.Dir(currentDirectory)
if parentDirectory == currentDirectory {
break
}
currentDirectory = parentDirectory
}
testInstance.Fatal("das Wurzelverzeichnis des Projekts wurde nicht gefunden")
return ""
}