diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 589a680..78c8027 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,10 @@ jobs: SYNCOVA_DB_SSLMODE: disable # Nur für diesen CI-Lauf; schützt keine echten Daten. SYNCOVA_ENCRYPTION_KEYS: 'v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=' + # Ohne diese Variable überspringen neun Testdateien ihre Datenbanktests + # still — darunter der Upgrade- und der Rollback-Test. Ein übersprungener + # Test sieht in der Zusammenfassung aus wie ein bestandener. + SYNCOVA_TEST_DATABASE_URL: 'postgres://syncova_test:ci-only-ephemeral-password@127.0.0.1:5432/syncova_test?sslmode=disable' run: go test -race -coverprofile=coverage.out $(go list ./... | grep -v '/node_modules/') - name: Migrationen gegen echte Datenbank prüfen @@ -107,6 +111,12 @@ jobs: - name: Binaries bauen run: go build $(go list ./... | grep -v '/node_modules/') + - name: Übersetzbarkeit aller Zielplattformen + # Der Agent liess sich in Phase 5 monatelang gar nicht für Windows + # übersetzen, ohne dass es jemandem auffiel: unix.Statfs gibt es dort + # nicht. Der Bau auf der eigenen Plattform bemerkt das nie. + run: make cross-build + frontend: name: Frontend (React/TypeScript) runs-on: ubuntu-latest diff --git a/packages/repository/immutability_test.go b/packages/repository/immutability_test.go index 0dd4d95..2c79be4 100644 --- a/packages/repository/immutability_test.go +++ b/packages/repository/immutability_test.go @@ -352,14 +352,12 @@ func TestUnreadableHoldKeepsBackupProtected(testInstance *testing.T) { // nicht. Die Daten waren da und weder als Repository erkennbar noch // entschluesselbar. func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) { - if !immutableFlagSupported() { - testInstance.Skip("dieses Betriebssystem kennt kein Unveraenderlich-Kennzeichen") - } - createdRepository := newHardenedTestRepository(testInstance, CreateOptions{ Name: "angriff", Immutable: true, Retention: 30 * 24 * time.Hour, }) + requireMeasuredFilesystemEnforcement(testInstance, createdRepository.RootPath()) + backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "angriff-backup") repositoryRoot := createdRepository.RootPath() @@ -420,14 +418,12 @@ func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) { // Repository in einen Fehler — und ein Repository, das nie aufraeumen kann, // laeuft irgendwann voll. func TestPruneReleasesProtectedOrphanedChunks(testInstance *testing.T) { - if !immutableFlagSupported() { - testInstance.Skip("dieses Betriebssystem kennt kein Unveraenderlich-Kennzeichen") - } - createdRepository := newHardenedTestRepository(testInstance, CreateOptions{ Name: "bereinigung", Immutable: true, Retention: time.Minute, }) + requireMeasuredFilesystemEnforcement(testInstance, createdRepository.RootPath()) + backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "bereinigung-backup") // Nach Fristablauf wird das Backup geloescht; seine Bloecke sind danach @@ -451,3 +447,40 @@ func TestPruneReleasesProtectedOrphanedChunks(testInstance *testing.T) { testInstance.Error("die Bereinigung meldete keinen freigegebenen Speicher") } } + +// requireMeasuredFilesystemEnforcement ueberspringt einen Test ohne echten Schutz. +// +// **Der Unterschied zu immutableFlagSupported() ist der Kern der Phase 11.** +// Jene Funktion sagt nur, ob das Betriebssystem das Kennzeichen *kennt* — unter +// Linux gibt sie immer "ja" zurueck. Ob es auch *durchgesetzt* wird, haengt am +// Dateisystem und an der Berechtigung CAP_LINUX_IMMUTABLE. In einem Container +// auf overlayfs ist beides typischerweise nicht gegeben: Das Setzen scheitert +// still, und ein rm -rf gelingt vollstaendig. +// +// Genau daran ist dieser Test in der CI gescheitert — zu Recht. Er behauptete +// Schutz in einer Umgebung, die keinen leisten kann. Die Anlage selbst macht es +// richtig: Sie ist beim Setzen nachsichtig (ein Backup ohne technischen +// Loeschschutz ist besser als gar keines) und sagt die Wahrheit ueber die +// **gemessene** Stufe. +// +// Der Test tut jetzt dasselbe: messen, und nur dort pruefen, wo es etwas zu +// pruefen gibt. Ein uebersprungener Test ist ehrlich; ein gruener, der nichts +// geprueft hat, waere es nicht. +func requireMeasuredFilesystemEnforcement(testInstance *testing.T, repositoryRoot string) { + testInstance.Helper() + + enforcementReport, measureError := MeasureEnforcement(context.Background(), repositoryRoot, newTestLogger()) + if measureError != nil { + testInstance.Fatalf("die Durchsetzungsstufe liess sich nicht messen: %v", measureError) + } + + if enforcementReport.Level == EnforcementFilesystem { + return + } + + testInstance.Skipf("dieses Dateisystem setzt den Loeschschutz nicht durch (gemessen: %s).\n"+ + " Das ist keine Panne, sondern die Auskunft: In einem Container auf overlayfs "+ + "oder ohne CAP_LINUX_IMMUTABLE gibt es keinen technischen Loeschschutz.\n"+ + " Der Angriffsversuch braucht ein Dateisystem, das ihn abwehren kann.", + enforcementReport.Level) +}