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=`, "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 "" }