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>
171 lines
5.6 KiB
Go
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 ""
|
|
}
|