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>
682 lines
22 KiB
Go
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())
|
|
}
|
|
}
|