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
|
SYNCOVA_DB_SSLMODE: disable
|
||||||
# Nur für diesen CI-Lauf; schützt keine echten Daten.
|
# Nur für diesen CI-Lauf; schützt keine echten Daten.
|
||||||
SYNCOVA_ENCRYPTION_KEYS: 'v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
|
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/')
|
run: go test -race -coverprofile=coverage.out $(go list ./... | grep -v '/node_modules/')
|
||||||
|
|
||||||
- name: Migrationen gegen echte Datenbank prüfen
|
- name: Migrationen gegen echte Datenbank prüfen
|
||||||
@ -107,6 +111,12 @@ jobs:
|
|||||||
- name: Binaries bauen
|
- name: Binaries bauen
|
||||||
run: go build $(go list ./... | grep -v '/node_modules/')
|
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:
|
frontend:
|
||||||
name: Frontend (React/TypeScript)
|
name: Frontend (React/TypeScript)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@ -352,14 +352,12 @@ func TestUnreadableHoldKeepsBackupProtected(testInstance *testing.T) {
|
|||||||
// nicht. Die Daten waren da und weder als Repository erkennbar noch
|
// nicht. Die Daten waren da und weder als Repository erkennbar noch
|
||||||
// entschluesselbar.
|
// entschluesselbar.
|
||||||
func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) {
|
func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) {
|
||||||
if !immutableFlagSupported() {
|
|
||||||
testInstance.Skip("dieses Betriebssystem kennt kein Unveraenderlich-Kennzeichen")
|
|
||||||
}
|
|
||||||
|
|
||||||
createdRepository := newHardenedTestRepository(testInstance, CreateOptions{
|
createdRepository := newHardenedTestRepository(testInstance, CreateOptions{
|
||||||
Name: "angriff", Immutable: true, Retention: 30 * 24 * time.Hour,
|
Name: "angriff", Immutable: true, Retention: 30 * 24 * time.Hour,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
requireMeasuredFilesystemEnforcement(testInstance, createdRepository.RootPath())
|
||||||
|
|
||||||
backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "angriff-backup")
|
backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "angriff-backup")
|
||||||
repositoryRoot := createdRepository.RootPath()
|
repositoryRoot := createdRepository.RootPath()
|
||||||
|
|
||||||
@ -420,14 +418,12 @@ func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) {
|
|||||||
// Repository in einen Fehler — und ein Repository, das nie aufraeumen kann,
|
// Repository in einen Fehler — und ein Repository, das nie aufraeumen kann,
|
||||||
// laeuft irgendwann voll.
|
// laeuft irgendwann voll.
|
||||||
func TestPruneReleasesProtectedOrphanedChunks(testInstance *testing.T) {
|
func TestPruneReleasesProtectedOrphanedChunks(testInstance *testing.T) {
|
||||||
if !immutableFlagSupported() {
|
|
||||||
testInstance.Skip("dieses Betriebssystem kennt kein Unveraenderlich-Kennzeichen")
|
|
||||||
}
|
|
||||||
|
|
||||||
createdRepository := newHardenedTestRepository(testInstance, CreateOptions{
|
createdRepository := newHardenedTestRepository(testInstance, CreateOptions{
|
||||||
Name: "bereinigung", Immutable: true, Retention: time.Minute,
|
Name: "bereinigung", Immutable: true, Retention: time.Minute,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
requireMeasuredFilesystemEnforcement(testInstance, createdRepository.RootPath())
|
||||||
|
|
||||||
backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "bereinigung-backup")
|
backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "bereinigung-backup")
|
||||||
|
|
||||||
// Nach Fristablauf wird das Backup geloescht; seine Bloecke sind danach
|
// 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")
|
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