# BAK-08 – Prüfprotokoll: Checksum-basierte Objekt-Integritätsprüfung Voraussetzungen BAK-05, FDN-04, FDN-09, OPS-05 – alle erledigt, siehe eigene Protokolle. Vor Start zwei offene Rückfragen geklärt (siehe unten). ## Grundsatzentscheidung: eigener Zustand statt file_revisions.created_at `created_at` als Alterskriterium hätte immer dieselben "ältesten" Objekte gescrubbt und den Rest nie erreicht — kein echtes Rotationsverhalten. Stattdessen eigene Archive-Tabelle `scrub_state` (`storage_key` → `last_scrubbed_at`, `last_result`), Migration `migrations/0001_scrub_state.up.sql`. `internal/scrub.Sample` ist eine reine Funktion: nimmt BAK-05s `existing_in_storage` (deterministisch sortiert) entgegen, filtert Objekte innerhalb der konfigurierbaren Cooldown-Frist heraus, priorisiert danach nach `last_scrubbed_at` aufsteigend (nie geprüft = ältestmöglicher Wert), begrenzt auf die konfigurierte Stichprobengröße — kein Voll-Sort über den gesamten Bestand bei jedem Lauf (Nutzerhinweis zum Kostenfaktor bei 10⁵+ Objekten: die WHERE-artige Cooldown-Filterung reduziert die Kandidatenmenge VOR der Sortierung, nur die Kandidaten selbst werden sortiert, nicht der komplette Bestand). ## Nachtrag: zwei Rückfragen vor Implementierungsbeginn geklärt 1. **OPS-05-Anbindung ist Pull, nicht Push.** OPS-05 (`internal/alerting`, Core) ist real implementiert, aber Core OPS-03 scrapt `/metrics`-URLs registrierter Module (`metrics_sources`-Tabelle in der Core-Registry- DB, `SourceStore.RegisterSource`) — kein Push-API. Für BAK-08 daher ein eigener, DAUERHAFT laufender Endpunkt (`cmd/scrub-metrics`, getrennt vom Oneshot-`scrub-cli`, dessen Prozess nach jedem Lauf endet und zum Scrape-Zeitpunkt nicht erreichbar wäre). Metrik als Counter (`nexarch_archive_storage_integrity_failures_total`), monoton steigend — kein Gauge, kein Rücksetzen bei behobenem Befund. Kein Befund = Wert bleibt unverändert (kein Dauer-Alarm durch andauernden "Fehler"-Zustand). Scope-Trennung gewahrt: `scrub-cli`/`scrub-metrics` erzeugen selbst KEIN Alert-Objekt — Schwellwert/Drosselung bleiben OPS-05-eigene Konfiguration (Alert-Regel wird separat über `alerting.RuleStore.CreateRule` angelegt, nicht Teil dieses Tickets). **CFG-04 war eine Verwechslung** (das ist die Benachrichtigungs-Einstellungen-Oberfläche, ein anderes Ticket) — die tatsächlich nötige "Config"-Aktion ist ein `INSERT` in `metrics_sources` (Core-Registry-DB), kein UI/Ticket-Abhängigkeit. Real ausgeführt (siehe „Echte Verdrahtung" unten). 2. **Sampling-Kriterium.** Siehe Grundsatzentscheidung oben — `scrub_state.last_scrubbed_at` statt `file_revisions.created_at`, Cooldown-Filterung vor Sortierung, feste Stichprobengröße (Top-N, deterministisch, keine Zufallsstichprobe — Nutzerpräferenz für Reproduzierbarkeit im Protokoll). ## Umsetzung - `migrations/0001_scrub_state.up.sql`/`.down.sql` — `scrub_state`, `scrub_counters` (Einzelzeile, monotoner Zähler). - `internal/scrub.Sample` — reine Funktion, Cooldown-Filter + Alt- Priorisierung + Stichprobenbegrenzung. - `internal/scrub.LoadLastScrubbed`/`MarkScrubbed`/`RecordFinding`/ `FindingsTotal` — DB-Zugriff auf `scrub_state`/`scrub_counters`, `MarkScrubbed` idempotent (`ON CONFLICT`) für unterbrechbare Läufe. - `internal/scrub.ExpectedChecksums` — eigene, minimale Abfrage gegen `file_revisions` (keine Erweiterung von `reconcile.DBEntry` — BAK-05 bleibt existenz-only). - `internal/scrub.ActualChecksum` — echtes Lesen der Datei + SHA-256, kein Header-/Größenvergleich. - `cmd/scrub-cli` — Oneshot: BAK-05-Reconcile → `Sample` → pro Kandidat Checksum-Vergleich → `MarkScrubbed` + bei Abweichung `RecordFinding` → JSON-Bericht auf stdout, Exit-Code 1 bei Befunden (gemeldet, nicht automatisch repariert). - `cmd/scrub-metrics` — dauerhafter `/metrics`-Endpunkt, liest `scrub_counters.findings_total`. ## Prüfungen | # | Prüfung | Ergebnis | |---|---|---| | 1 | Absichtlich veränderter Objektinhalt wird als Abweichung erkannt | **bestanden** — real: Testobjekt mit absichtlich falscher `checksum_sha256` in `dms_tenant_test` angelegt, echte Datei ins Storage-Verzeichnis gelegt, `scrub-cli` real über systemd ausgelöst: Befund im JSON-Bericht, Exit-Code 1, `scrub_counters.findings_total` real von 0 auf 1 erhöht (siehe Journal-Auszug unten) | | 2 | Sampling priorisiert alte/nie geprüfte Objekte, nicht neue | **bestanden** — `TestSample_PrioritizesNeverScrubbedAndOldest`: nie geprüftes Objekt kommt vor einem vor 30 Tagen geprüften, dieses vor einem vor 1 Tag geprüften | | 3 | Wiederholter Lauf ohne neue Objekte meldet nichts erneut (kein Spam) / idempotent bei Unterbrechung | **bestanden** — real: zweiter `scrub-cli`-Lauf direkt nach dem ersten liefert `sampled: 0` (Cooldown greift), `TestMarkScrubbed_IsIdempotent` beweist wiederholtes Markieren ohne Duplikat | Zusätzlich: `TestSample_RespectsCooldown`, `TestSample_LimitsToSampleSize`, `TestSample_DeterministicForIdenticalInput`, `TestRecordFinding_IsMonotonicallyIncreasing`, `TestActualChecksum_MatchesRealFileContent` (echter Dateiinhalt, echtes SHA-256), `TestExpectedChecksums_ReadsRealFileRevisions` (echtes Postgres, kein Mock). ## Echte Verdrahtung auf 192.168.1.131 - `scrub-cli`, `scrub-metrics` gebaut nach `/opt/nexarch-archive/bin/` - `/etc/nexarch/archive-scrub.env`, `/etc/nexarch/archive-scrub-metrics.env` (0600) - Migration real gegen `dms_tenant_test` angewendet (`psql -f migrations/0001_scrub_state.up.sql`) - `nexarch-archive-scrub.timer` installiert/aktiviert (täglich 06:00 UTC), `nexarch-archive-scrub-metrics.service` installiert/aktiviert (dauerhaft, `Restart=on-failure`) — beide `systemctl status`: aktiv - **Reales `INSERT` in `metrics_sources`** (Core-Registry-DB `nexarch_registry`): `('archive', 'http://127.0.0.1:8090/metrics')` — bestätigt über `SELECT * FROM metrics_sources` - **End-to-End über OPS-03 bestätigt**: `curl http://127.0.0.1:8085/metrics` (Core-Aggregator) zeigt `nexarch_module_archive_nexarch_archive_storage_integrity_failures_total` — reale Umbenennung gemäß OPS-03-Namenskonvention, kein synthetischer Wert - Realer Befund-Durchlauf: Testobjekt mit absichtlich falscher Prüfsumme angelegt → `scrub-cli` real via `systemctl start` ausgelöst → Befund im Journal, `scrub_counters.findings_total` real 0→1, sichtbar sowohl auf `scrub-metrics` als auch über den Core-Aggregator → Testdaten anschließend bereinigt (`file_revisions`/`documents`/`users`-Zeilen gelöscht, `scrub_state`/`scrub_counters` zurückgesetzt, Testdatei entfernt) ## Build/Test-Ergebnis (192.168.1.131, `make check`) ``` go build ./... -> clean go vet ./... -> clean golangci-lint run ./... -> 0 issues go test ./... -p 1 -count=1 -> 4/4 Pakete mit Tests ok (internal/backup, internal/objectbackup, internal/reconcile, internal/scrub), 0 Fehlschläge ``` `internal/scrub`-Tests separat mit gesetzter `TEST_TENANT_DSN` gegen `dms_tenant_test` verifiziert: 8/8 Tests bestanden. ## Bekannte Grenze (aus Ticket übernommen, nicht Teil der Abnahme) Der Job erkennt Abweichungen nur bei Objekten, die gelesen und erneut geprüft werden können. Ersetzt keine storage-seitige WORM-/ Versionierungsstrategie und keine Zugriffs-/Audit-Logs des Storage-Providers (`STORAGE-KONZEPT.md` Abschnitt 6.1) — bei extern eingebundenem, nicht-kompatiblem Kunden-Storage (Betriebsmodus 3, ohne Versioning/Object Lock/Audit-Logs) bleibt eine Lücke, die BAK-08 technisch nicht schließen kann. ## Gesamtergebnis **Bestanden.** Alle sechs Akzeptanzkriterien und alle drei Pflicht- prüfungen real erfüllt — inklusive echtem Ende-zu-Ende-Nachweis über Core OPS-03/OPS-05 (kein Stub, reale `/metrics`-Registrierung und -Aggregation). Beide vor Implementierungsbeginn gestellten Rückfragen (OPS-05-Anbindungsmechanismus, Sampling-Kriterium) im Protokoll dokumentiert und in der Umsetzung berücksichtigt.