CI: Loeschschutz messen statt annehmen, Datenbanktests wirklich fahren
Der Angriffstest auf ein gehaertetes Repository scheiterte in der CI: "rm -rf auf ein geschuetztes Repository gelang vollstaendig". Der Befund ist richtig — und der Fehler lag im Test, nicht im Produktivcode. immutableFlagSupported() sagt nur, ob das Betriebssystem das Unveraenderlich-Kennzeichen *kennt*; unter Linux gibt es immer "ja" zurueck. Ob es auch *durchgesetzt* wird, haengt am Dateisystem und an CAP_LINUX_IMMUTABLE. In einem Container auf overlayfs ist beides nicht gegeben: Das Setzen scheitert still, und der Angriff gelingt. 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 das jetzt auch: Er misst zuerst und prueft nur dort, wo es etwas zu pruefen gibt. Nachgewiesen in beide Richtungen — auf macOS laeuft der Angriff wirklich, im Container wird mit Begruendung uebersprungen. Zwei Luecken in der CI dabei gefunden: - SYNCOVA_TEST_DATABASE_URL fehlte. Neun Testdateien uebersprangen ihre Datenbanktests still, darunter der Upgrade- und der Rollback-Test. Ein uebersprungener Test sieht in der Zusammenfassung aus wie ein bestandener. - make cross-build lief nicht mit. Genau daran ist in Phase 5 monatelang unbemerkt geblieben, dass der Agent sich fuer Windows gar nicht uebersetzen liess. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
610719c316
commit
9c48cb1c1c
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user