syncova-backup/packages/verification/assurance.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

682 lines
22 KiB
Go

package verification
import (
"fmt"
"sort"
"time"
)
// Classification ist die objektive Einstufung eines Backups.
//
// Sie ist das Exit-Kriterium der Phase (SYNCOVA_IMPLEMENTATION_PLAN.md §12) und
// eine Leiter, keine Menge: Jede Stufe setzt die darunter voraus. Der
// entscheidende Punkt liegt zwischen „erfolgreich" und „wiederherstellbar" —
// dass ein Sicherungslauf durchlief, sagt **nichts** darueber, ob sich die
// Daten zurueckholen lassen. Genau diese Verwechslung soll die Einstufung
// unmoeglich machen.
type Classification string
const (
// ClassificationFailed meldet einen nicht abgeschlossenen Sicherungslauf.
ClassificationFailed Classification = "failed"
// ClassificationCorrupted meldet nachgewiesene Beschaedigung.
//
// Sie steht ausserhalb der Leiter: Ein beschaedigtes Backup ist nicht
// „weniger gut" als ein erfolgreiches, sondern etwas anderes — es sieht
// vollstaendig aus und ist es nicht.
ClassificationCorrupted Classification = "corrupted"
// ClassificationSuccessful meldet einen abgeschlossenen Lauf ohne Pruefung.
//
// Die schwaechste positive Aussage: Es wurde gesichert. Ob die Daten noch
// da sind, weiss niemand.
ClassificationSuccessful Classification = "successful"
// ClassificationVerified meldet ein geprueftes Backup.
//
// Bloecke vorhanden und unbeschaedigt. Ein starkes Indiz — kein Nachweis:
// Ob Entschluesselung, Dekompression und Zusammensetzung ein brauchbares
// Ergebnis liefern, ist damit nicht gesagt.
ClassificationVerified Classification = "verified"
// ClassificationRecoverable meldet ein nachweislich wiederherstellbares Backup.
//
// Es wurde tatsaechlich zurueckgeschrieben und gegen das Original
// verglichen. Nur diese Stufe ist ein Nachweis.
ClassificationRecoverable Classification = "recoverable"
)
// classificationRanks ordnen die Stufen fuer Vergleiche.
var classificationRanks = map[Classification]int{
ClassificationCorrupted: -1,
ClassificationFailed: 0,
ClassificationSuccessful: 1,
ClassificationVerified: 2,
ClassificationRecoverable: 3,
}
// Rank liefert den Zahlenwert einer Einstufung.
func (classification Classification) Rank() int {
return classificationRanks[classification]
}
// Describe beschreibt eine Einstufung in einem Satz.
func (classification Classification) Describe() string {
switch classification {
case ClassificationFailed:
return "Der Sicherungslauf wurde nicht abgeschlossen. Es gibt kein verwendbares Backup."
case ClassificationCorrupted:
return "Eine Pruefung hat Beschaedigungen gefunden. Dieses Backup ist nicht vertrauenswuerdig."
case ClassificationSuccessful:
return "Der Sicherungslauf ist abgeschlossen. Ob sich die Daten zurueckholen lassen, wurde nicht geprueft."
case ClassificationVerified:
return "Die Bloecke sind vorhanden und unbeschaedigt. Eine Wiederherstellung wurde nicht erprobt."
case ClassificationRecoverable:
return "Das Backup wurde zurueckgeschrieben und gegen das Original verglichen. Es ist nachweislich wiederherstellbar."
default:
return "Der Zustand dieses Backups ist unbekannt."
}
}
// BackupFacts sind die bekannten Tatsachen ueber ein Backup.
//
// Alles Unbekannte ist ausdruecklich als solches gefuehrt — ein Zeitpunkt, der
// fehlt, ist ein nil-Zeiger und kein Nullwert. Der Unterschied entscheidet
// ueber die Bewertung: „nie geprueft" darf nicht wie „vor einer Sekunde
// geprueft" aussehen.
type BackupFacts struct {
// RunSucceeded meldet einen vollstaendig gelungenen Sicherungslauf.
RunSucceeded bool
// RunPartial meldet einen Lauf mit uebergangenen Objekten.
RunPartial bool
// CompletedAt ist der Abschluss des Backups in UTC; nil bei unvollstaendig.
CompletedAt *time.Time
// LastVerifiedAt ist die letzte erfolgreiche Integritaetspruefung; nil wenn nie.
LastVerifiedAt *time.Time
// VerificationFoundCorruption meldet eine Pruefung mit Beschaedigungsbefund.
VerificationFoundCorruption bool
// VerificationFoundMissing meldet eine Pruefung mit fehlenden Bloecken.
VerificationFoundMissing bool
// LastRestoreTestAt ist der letzte bestandene Wiederherstellungstest; nil wenn nie.
LastRestoreTestAt *time.Time
// RestoreTestFailed meldet einen nicht bestandenen Wiederherstellungstest.
RestoreTestFailed bool
// IsEncrypted meldet ein verschluesseltes Backup.
IsEncrypted bool
// IsImmutable meldet eine Aufbewahrungspflicht.
IsImmutable bool
// HasOffsiteCopy meldet eine Kopie an einem zweiten Ort.
HasOffsiteCopy bool
// RepositoryHealthy meldet ein einwandfreies Repository.
RepositoryHealthy bool
// RecoveryPointObjective ist der geforderte Wiederherstellungspunkt; 0 wenn keiner.
RecoveryPointObjective time.Duration
// RecoveryTimeObjective ist die geforderte Wiederherstellungsdauer; 0 wenn keine.
RecoveryTimeObjective time.Duration
// MeasuredRestoreDuration ist die gemessene Dauer eines Tests; 0 wenn keine.
MeasuredRestoreDuration time.Duration
// AnomalyDetected meldet eine erkannte Auffaelligkeit.
AnomalyDetected bool
}
// Classify stuft ein Backup anhand der bekannten Tatsachen ein.
//
// Die Reihenfolge der Pruefungen ist festgelegt und nicht beliebig: Eine
// nachgewiesene Beschaedigung schlaegt jede andere Aussage. Ein Backup, das
// erfolgreich gesichert **und** beschaedigt ist, ist beschaedigt.
func Classify(facts BackupFacts) Classification {
if facts.VerificationFoundCorruption || facts.RestoreTestFailed {
return ClassificationCorrupted
}
if !facts.RunSucceeded {
return ClassificationFailed
}
// Fehlende Bloecke sind keine Beschaedigung, machen das Backup aber
// unbrauchbar: Was fehlt, laesst sich nicht zurueckholen.
if facts.VerificationFoundMissing {
return ClassificationCorrupted
}
if facts.LastRestoreTestAt != nil {
return ClassificationRecoverable
}
if facts.LastVerifiedAt != nil {
return ClassificationVerified
}
return ClassificationSuccessful
}
// ScoreInput ist eine einzelne Eingangsgroesse der Bewertung.
type ScoreInput struct {
// Name benennt die Groesse.
Name string `json:"name"`
// Weight ist ihr Gewicht in Punkten.
Weight int `json:"weight"`
// EarnedPoints sind die erreichten Punkte.
EarnedPoints int `json:"earned_points"`
// IsKnown meldet, ob eine Messung vorliegt.
//
// Der wichtigste Wert der ganzen Bewertung: Unbekannt zaehlt niemals als
// gut. Eine Bewertung, die ohne Messung hoch ausfaellt, waere das
// schlimmste Fake-Feature der Anlage — sie beruhigte genau dort, wo man
// hinsehen muesste.
IsKnown bool `json:"is_known"`
// Explanation erklaert die Punktzahl verstaendlich.
Explanation string `json:"explanation"`
}
// AssuranceScore ist die Bewertung der Wiederherstellungssicherheit.
type AssuranceScore struct {
// TotalPoints sind die erreichten Punkte.
TotalPoints int `json:"total_points"`
// MaximumPoints sind die erreichbaren Punkte.
MaximumPoints int `json:"maximum_points"`
// Percentage ist der Anteil in Prozent.
Percentage int `json:"percentage"`
// Classification ist die Einstufung des Backups.
Classification Classification `json:"classification"`
// Inputs sind die einzelnen Eingangsgroessen.
Inputs []ScoreInput `json:"inputs"`
// UnknownInputCount ist die Zahl ungemessener Groessen.
//
// Sie steht neben der Prozentzahl, damit niemand eine Bewertung fuer
// belastbar haelt, die auf lauter Unbekanntem beruht.
UnknownInputCount int `json:"unknown_input_count"`
// CalculatedAt ist der Zeitpunkt der Berechnung in UTC.
CalculatedAt time.Time `json:"calculated_at"`
}
// IsTrustworthy meldet ein Backup, auf das man sich verlassen kann.
//
// Zwei Gruende schliessen das aus. Der erste: Mehr als drei unbekannte Groessen
// machen die Prozentzahl zur Vermutung. Der zweite wiegt schwerer — ein Backup
// mit Befund oder ein gescheiterter Lauf ist unbrauchbar, gleich wie viele
// andere Groessen gut aussehen. Eine hohe Prozentzahl daneben liesse genau die
// Beschoenigung zu, die PROMPT.md §138 verbietet.
func (score *AssuranceScore) IsTrustworthy() bool {
if score.Classification == ClassificationCorrupted || score.Classification == ClassificationFailed {
return false
}
return score.UnknownInputCount <= 3
}
// Summary fasst die Bewertung in einem Satz zusammen.
func (score *AssuranceScore) Summary() string {
if score.Classification == ClassificationCorrupted {
return "0 % — eine Pruefung hat Beschaedigungen gefunden. Dieses Backup ist unbrauchbar, " +
"bis der Befund ausgeraeumt ist."
}
if score.Classification == ClassificationFailed {
return "0 % — der Sicherungslauf ist gescheitert. Es gibt nichts, worauf man sich verlassen koennte."
}
if !score.IsTrustworthy() {
return fmt.Sprintf("%d %% — aber %d von %d Groessen wurden nie gemessen. Die Zahl ist eine "+
"Vermutung, keine Aussage.", score.Percentage, score.UnknownInputCount, len(score.Inputs))
}
return fmt.Sprintf("%d %% (%s)", score.Percentage, score.Classification)
}
// MissingMeasurements liefert die ungemessenen Groessen.
//
// Sie sind die eigentliche Handlungsanweisung: Was hier steht, sollte als
// Naechstes geprueft werden.
func (score *AssuranceScore) MissingMeasurements() []string {
missingNames := make([]string, 0)
for _, scoreInput := range score.Inputs {
if !scoreInput.IsKnown {
missingNames = append(missingNames, scoreInput.Name)
}
}
sort.Strings(missingNames)
return missingNames
}
// freshnessThresholds bestimmen die Bewertung des Backupalters.
var freshnessThresholds = []struct {
// maximumAge ist die Obergrenze dieser Stufe.
maximumAge time.Duration
// pointFraction ist der Anteil der erreichbaren Punkte.
pointFraction float64
}{
{24 * time.Hour, 1.0},
{72 * time.Hour, 0.6},
{7 * 24 * time.Hour, 0.3},
}
// verificationFreshness bestimmt, wie lange eine Pruefung ihre Aussage behaelt.
//
// Eine Pruefung vom letzten Jahr sagt wenig ueber heute: Datentraeger
// verfaelschen Daten still, und ein Repository kann in der Zwischenzeit
// aufgeraeumt worden sein.
const verificationFreshness = 30 * 24 * time.Hour
// restoreTestFreshness bestimmt, wie lange ein Wiederherstellungstest zaehlt.
const restoreTestFreshness = 90 * 24 * time.Hour
// CalculateScore berechnet die Bewertung der Wiederherstellungssicherheit.
//
// Zehn Eingangsgroessen nach SYNCOVA_IMPLEMENTATION_PLAN.md §12. Die Gewichte
// folgen einer Ueberzeugung: Der Wiederherstellungstest wiegt am schwersten,
// weil er als einziger etwas nachweist. Verschluesselung und Unveraenderlichkeit
// schuetzen die Daten, sagen aber nichts darueber, ob sie noch da sind.
func CalculateScore(facts BackupFacts, referenceTime time.Time) *AssuranceScore {
score := &AssuranceScore{
Classification: Classify(facts),
Inputs: make([]ScoreInput, 0, 10),
CalculatedAt: referenceTime.UTC(),
}
score.Inputs = append(score.Inputs,
scoreFreshness(facts, referenceTime),
scoreVerification(facts, referenceTime),
scoreRestoreTest(facts, referenceTime),
scoreRecoveryPoint(facts, referenceTime),
scoreRecoveryTime(facts),
scoreImmutability(facts),
scoreOffsite(facts),
scoreEncryption(facts),
scoreRepositoryHealth(facts),
scoreAnomalies(facts),
)
for _, scoreInput := range score.Inputs {
score.TotalPoints += scoreInput.EarnedPoints
score.MaximumPoints += scoreInput.Weight
if !scoreInput.IsKnown {
score.UnknownInputCount++
}
}
if score.MaximumPoints > 0 {
score.Percentage = score.TotalPoints * 100 / score.MaximumPoints
}
// Ein beschaedigtes oder gescheitertes Backup bekommt null Prozent — und
// zwar unabhaengig davon, wie gut die uebrigen Groessen aussehen.
//
// Der Grund ist eine Beobachtung aus dem Nachweis dieser Phase: Ein Backup
// mit einem beschaedigten Block kam auf 70 %, weil Aktualitaet,
// Verschluesselung und ein frueherer Wiederherstellungstest weiterhin
// zaehlten. Diese Zahl liest sich wie „weitgehend in Ordnung". Das ist sie
// nicht: Ein Backup, aus dem sich ein Block nicht mehr lesen laesst, ist
// nicht zu 70 % wiederherstellbar, sondern nicht wiederherstellbar.
if score.Classification == ClassificationCorrupted || score.Classification == ClassificationFailed {
score.TotalPoints = 0
score.Percentage = 0
}
return score
}
// scoreFreshness bewertet das Alter des Backups.
func scoreFreshness(facts BackupFacts, referenceTime time.Time) ScoreInput {
const weight = 15
if facts.CompletedAt == nil {
return ScoreInput{
Name: "Aktualitaet",
Weight: weight,
IsKnown: false,
Explanation: "Das Backup wurde nie abgeschlossen.",
}
}
backupAge := referenceTime.Sub(*facts.CompletedAt)
for _, threshold := range freshnessThresholds {
if backupAge <= threshold.maximumAge {
return ScoreInput{
Name: "Aktualitaet",
Weight: weight,
EarnedPoints: int(float64(weight) * threshold.pointFraction),
IsKnown: true,
Explanation: fmt.Sprintf("Das Backup ist %s alt.", formatAge(backupAge)),
}
}
}
return ScoreInput{
Name: "Aktualitaet",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: fmt.Sprintf("Das Backup ist %s alt und damit veraltet.", formatAge(backupAge)),
}
}
// scoreVerification bewertet die Integritaetspruefung.
func scoreVerification(facts BackupFacts, referenceTime time.Time) ScoreInput {
const weight = 20
if facts.VerificationFoundCorruption || facts.VerificationFoundMissing {
return ScoreInput{
Name: "Integritaetspruefung",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Die Pruefung fand Beschaedigungen oder fehlende Bloecke.",
}
}
if facts.LastVerifiedAt == nil {
// Nie geprueft: null Punkte, und ausdruecklich als ungemessen gefuehrt.
return ScoreInput{
Name: "Integritaetspruefung",
Weight: weight,
IsKnown: false,
Explanation: "Dieses Backup wurde nie auf Integritaet geprueft.",
}
}
verificationAge := referenceTime.Sub(*facts.LastVerifiedAt)
if verificationAge > verificationFreshness {
return ScoreInput{
Name: "Integritaetspruefung",
Weight: weight,
EarnedPoints: weight / 3,
IsKnown: true,
Explanation: fmt.Sprintf("Die letzte Pruefung liegt %s zurueck und sagt wenig ueber heute.",
formatAge(verificationAge)),
}
}
return ScoreInput{
Name: "Integritaetspruefung",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: fmt.Sprintf("Vor %s geprueft, ohne Beanstandung.", formatAge(verificationAge)),
}
}
// scoreRestoreTest bewertet den Wiederherstellungstest.
//
// Das hoechste Gewicht der Bewertung: Er ist die einzige Groesse, die etwas
// nachweist statt es nahezulegen.
func scoreRestoreTest(facts BackupFacts, referenceTime time.Time) ScoreInput {
const weight = 25
if facts.RestoreTestFailed {
return ScoreInput{
Name: "Wiederherstellungstest",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Ein Wiederherstellungstest ist fehlgeschlagen.",
}
}
// Ein Befund entwertet jeden frueheren Test: Er bezieht sich auf einen
// Zustand des Repositorys, den es nicht mehr gibt.
if facts.VerificationFoundCorruption || facts.VerificationFoundMissing {
return ScoreInput{
Name: "Wiederherstellungstest",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Ein frueherer Test zaehlt nicht mehr: Seither wurden Beschaedigungen " +
"gefunden. Der Test muss wiederholt werden.",
}
}
if facts.LastRestoreTestAt == nil {
return ScoreInput{
Name: "Wiederherstellungstest",
Weight: weight,
IsKnown: false,
Explanation: "Es wurde nie erprobt, ob sich dieses Backup zurueckholen laesst. " +
"Alles Uebrige sind Indizien.",
}
}
testAge := referenceTime.Sub(*facts.LastRestoreTestAt)
if testAge > restoreTestFreshness {
return ScoreInput{
Name: "Wiederherstellungstest",
Weight: weight,
EarnedPoints: weight / 3,
IsKnown: true,
Explanation: fmt.Sprintf("Der letzte Test liegt %s zurueck.", formatAge(testAge)),
}
}
return ScoreInput{
Name: "Wiederherstellungstest",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: fmt.Sprintf("Vor %s erfolgreich zurueckgeschrieben und verglichen.", formatAge(testAge)),
}
}
// scoreRecoveryPoint bewertet die Einhaltung des Wiederherstellungspunkts.
func scoreRecoveryPoint(facts BackupFacts, referenceTime time.Time) ScoreInput {
const weight = 10
if facts.RecoveryPointObjective <= 0 {
return ScoreInput{
Name: "Wiederherstellungspunkt",
Weight: weight,
IsKnown: false,
Explanation: "Fuer diesen Auftrag ist kein Wiederherstellungspunkt festgelegt.",
}
}
if facts.CompletedAt == nil {
return ScoreInput{
Name: "Wiederherstellungspunkt",
Weight: weight,
IsKnown: false,
Explanation: "Ohne abgeschlossenes Backup laesst sich der Wiederherstellungspunkt nicht beurteilen.",
}
}
backupAge := referenceTime.Sub(*facts.CompletedAt)
if backupAge <= facts.RecoveryPointObjective {
return ScoreInput{
Name: "Wiederherstellungspunkt",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: fmt.Sprintf("Eingehalten: %s alt bei einer Vorgabe von %s.",
formatAge(backupAge), formatAge(facts.RecoveryPointObjective)),
}
}
return ScoreInput{
Name: "Wiederherstellungspunkt",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: fmt.Sprintf("VERLETZT: %s alt bei einer Vorgabe von %s.",
formatAge(backupAge), formatAge(facts.RecoveryPointObjective)),
}
}
// scoreRecoveryTime bewertet die Einhaltung der Wiederherstellungsdauer.
func scoreRecoveryTime(facts BackupFacts) ScoreInput {
const weight = 10
if facts.RecoveryTimeObjective <= 0 {
return ScoreInput{
Name: "Wiederherstellungsdauer",
Weight: weight,
IsKnown: false,
Explanation: "Fuer diesen Auftrag ist keine Wiederherstellungsdauer festgelegt.",
}
}
// Ohne gemessene Dauer bleibt die Vorgabe eine Absichtserklaerung. Sie als
// eingehalten zu werten waere die gefaehrlichste Annahme der Bewertung.
if facts.MeasuredRestoreDuration <= 0 {
return ScoreInput{
Name: "Wiederherstellungsdauer",
Weight: weight,
IsKnown: false,
Explanation: "Es wurde nie gemessen, wie lange eine Wiederherstellung dauert. Die Vorgabe " +
"ist eine Absichtserklaerung.",
}
}
if facts.MeasuredRestoreDuration <= facts.RecoveryTimeObjective {
return ScoreInput{
Name: "Wiederherstellungsdauer",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: fmt.Sprintf("Gemessen %s bei einer Vorgabe von %s.",
formatAge(facts.MeasuredRestoreDuration), formatAge(facts.RecoveryTimeObjective)),
}
}
return ScoreInput{
Name: "Wiederherstellungsdauer",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: fmt.Sprintf("VERLETZT: gemessen %s bei einer Vorgabe von %s.",
formatAge(facts.MeasuredRestoreDuration), formatAge(facts.RecoveryTimeObjective)),
}
}
// scoreImmutability bewertet die Unveraenderlichkeit.
func scoreImmutability(facts BackupFacts) ScoreInput {
const weight = 5
if !facts.IsImmutable {
return ScoreInput{
Name: "Unveraenderlichkeit",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Dieses Backup ist nicht gegen Loeschung geschuetzt.",
}
}
return ScoreInput{
Name: "Unveraenderlichkeit",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: "Das Backup unterliegt einer Aufbewahrungspflicht.",
}
}
// scoreOffsite bewertet die Kopie an einem zweiten Ort.
func scoreOffsite(facts BackupFacts) ScoreInput {
const weight = 5
if !facts.HasOffsiteCopy {
return ScoreInput{
Name: "Zweiter Standort",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Es gibt keine Kopie an einem zweiten Ort. Ein Brand vernichtet beides.",
}
}
return ScoreInput{
Name: "Zweiter Standort",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: "Eine Kopie liegt an einem zweiten Ort.",
}
}
// scoreEncryption bewertet die Verschluesselung.
func scoreEncryption(facts BackupFacts) ScoreInput {
const weight = 5
if !facts.IsEncrypted {
return ScoreInput{
Name: "Verschluesselung",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Dieses Backup ist unverschluesselt abgelegt.",
}
}
return ScoreInput{
Name: "Verschluesselung",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: "Das Backup ist verschluesselt.",
}
}
// scoreRepositoryHealth bewertet den Zustand des Repositorys.
func scoreRepositoryHealth(facts BackupFacts) ScoreInput {
const weight = 3
if !facts.RepositoryHealthy {
return ScoreInput{
Name: "Repository-Zustand",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Das Repository meldet einen auffaelligen Zustand.",
}
}
return ScoreInput{
Name: "Repository-Zustand",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: "Das Repository ist einwandfrei.",
}
}
// scoreAnomalies bewertet erkannte Auffaelligkeiten.
func scoreAnomalies(facts BackupFacts) ScoreInput {
const weight = 2
if facts.AnomalyDetected {
return ScoreInput{
Name: "Auffaelligkeiten",
Weight: weight,
EarnedPoints: 0,
IsKnown: true,
Explanation: "Es wurde eine Auffaelligkeit erkannt.",
}
}
return ScoreInput{
Name: "Auffaelligkeiten",
Weight: weight,
EarnedPoints: weight,
IsKnown: true,
Explanation: "Keine Auffaelligkeiten.",
}
}
// formatAge gibt eine Dauer lesbar aus.
func formatAge(duration time.Duration) string {
switch {
case duration >= 24*time.Hour:
return fmt.Sprintf("%.0f Tage", duration.Hours()/24)
case duration >= time.Hour:
return fmt.Sprintf("%.0f Stunden", duration.Hours())
case duration >= time.Minute:
return fmt.Sprintf("%.0f Minuten", duration.Minutes())
default:
return fmt.Sprintf("%.0f Sekunden", duration.Seconds())
}
}