Files
nexarch/internal/migration/verify/hashcompare.go
T
patrickandClaude Sonnet 5.5 5b8c20777d refactor: Pruefgate-Pakete nach Funktion benannt, migration* gruppiert
- dms/ai/workflow: qa0x-Pakete nach internal/gates/<thema> verschoben, Paketname = Thema
- internal/migration{acl,audit,...} -> internal/migration/{acl,audit,...}
- Importpfade, Gate-Skripte und zwei relative Migrationspfade angepasst
- keine Logikaenderung; mail-Gates bewusst unveraendert (pfadabhaengig)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-27 12:44:21 +01:00

134 lines
4.3 KiB
Go

// Package migrationverify implementiert VAL-02: Stichprobenweiser
// Hash-Vergleich migrierter Binaerdateien (Objekt-Storage) gegen die
// Quelle. Source und ObjectStore sind bewusst generische Interfaces statt
// konkreter Typen aus dms/mail (Go "internal"-Sichtbarkeit verbietet den
// Import von dms/internal/... bzw. mail/internal/... aus dem Root-Modul
// heraus) — ein Aufrufer in dms oder mail kann dmscrypto.Service bzw. den
// jeweiligen storage.Service strukturell als Decryptor/ObjectStore
// uebergeben, ohne dass dieses Paket davon abhaengt.
package migrationverify
import (
"context"
"crypto/sha256"
"fmt"
"io"
"strings"
)
type Sample struct {
Label string
SourceObjectKey string
StorageKey string
WrappedDEK []byte
}
type ObjectSource interface {
OpenObject(ctx context.Context, key string) (io.ReadCloser, error)
}
type ObjectStore interface {
Get(ctx context.Context, key string) (io.ReadCloser, error)
}
// Decryptor entschluesselt ein Objekt, das im Ziel-Storage verschluesselt
// abgelegt wurde (z.B. dms/internal/crypto.Service.Open), nicht jedes
// Objekt im Ziel liegt verschluesselt vor (siehe mail: Storage-Service
// statt encstorage, MAI-02).
type Decryptor interface {
Open(ctx context.Context, tenantSlug string, wrappedDEK []byte, ciphertext io.Reader) (io.Reader, error)
}
type FileResult struct {
Label string
SourceHash string
StorageHash string
Match bool
Err error
}
func CompareSample(ctx context.Context, tenantSlug string, source ObjectSource, store ObjectStore, decrypt Decryptor, sample Sample) FileResult {
result := FileResult{Label: sample.Label}
srcReader, err := source.OpenObject(ctx, sample.SourceObjectKey)
if err != nil {
result.Err = fmt.Errorf("migrationverify: quellobjekt %q oeffnen: %w", sample.Label, err)
return result
}
defer func() { _ = srcReader.Close() }()
srcHash, err := hashAll(srcReader)
if err != nil {
result.Err = fmt.Errorf("migrationverify: quellobjekt %q hashen: %w", sample.Label, err)
return result
}
result.SourceHash = srcHash
storedReader, err := store.Get(ctx, sample.StorageKey)
if err != nil {
result.Err = fmt.Errorf("migrationverify: ziel-objekt %q lesen: %w", sample.Label, err)
return result
}
defer func() { _ = storedReader.Close() }()
var plain io.Reader = storedReader
if sample.WrappedDEK != nil {
if decrypt == nil {
result.Err = fmt.Errorf("migrationverify: objekt %q ist verschluesselt abgelegt, aber kein Decryptor uebergeben", sample.Label)
return result
}
plain, err = decrypt.Open(ctx, tenantSlug, sample.WrappedDEK, storedReader)
if err != nil {
result.Err = fmt.Errorf("migrationverify: objekt %q entschluesseln: %w", sample.Label, err)
return result
}
}
dstHash, err := hashAll(plain)
if err != nil {
result.Err = fmt.Errorf("migrationverify: ziel-objekt %q hashen: %w", sample.Label, err)
return result
}
result.StorageHash = dstHash
result.Match = srcHash == dstHash
return result
}
func CompareSamples(ctx context.Context, tenantSlug string, source ObjectSource, store ObjectStore, decrypt Decryptor, samples []Sample) []FileResult {
results := make([]FileResult, 0, len(samples))
for _, s := range samples {
results = append(results, CompareSample(ctx, tenantSlug, source, store, decrypt, s))
}
return results
}
func hashAll(r io.Reader) (string, error) {
h := sha256.New()
if _, err := io.Copy(h, r); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
// RenderReport erzeugt den in VAL-02 geforderten Hash-Vergleichsbericht je
// Mandant als Markdown-Tabelle (Akzeptanzkriterium 1).
func RenderReport(tenantSlug string, results []FileResult) string {
var mismatches, errored int
var b strings.Builder
fmt.Fprintf(&b, "# Hash-Vergleichsbericht: Mandant %s\n\n", tenantSlug)
fmt.Fprintf(&b, "| Datei | Quell-Hash (SHA-256) | Ziel-Hash (SHA-256) | Ergebnis |\n|---|---|---|---|\n")
for _, r := range results {
status := "OK"
switch {
case r.Err != nil:
status = "FEHLER: " + r.Err.Error()
errored++
case !r.Match:
status = "ABWEICHUNG"
mismatches++
}
fmt.Fprintf(&b, "| %s | %s | %s | %s |\n", r.Label, r.SourceHash, r.StorageHash, status)
}
fmt.Fprintf(&b, "\nStichprobe: %d Dateien, %d Abweichungen, %d Fehler.\n", len(results), mismatches, errored)
return b.String()
}