syncova-backup/packages/alerting/rules.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

235 lines
8.9 KiB
Go

// Package alerting erkennt Zustaende, die jemand ansehen sollte.
//
// Das Meldungswesen hat einen Feind, und es ist nicht der fehlende Alarm: Es ist
// der Alarm, den niemand mehr liest. Ein System, das jede Minute dieselbe
// Meldung erzeugt, wird nach drei Tagen weggeklickt — und dann fehlt die eine,
// auf die es ankam.
//
// Jede Regel dieses Pakets ist deshalb an drei Fragen gemessen: Ist der Zustand
// wirklich handlungsbeduerftig? Verschwindet die Meldung von selbst, wenn er
// vorbei ist? Und sagt sie, was zu tun ist?
package alerting
import "time"
// Severity ist der Schweregrad einer Meldung.
type Severity string
const (
// SeverityInformation ist eine reine Auskunft.
SeverityInformation Severity = "information"
// SeverityWarning verlangt einen Blick bei Gelegenheit.
SeverityWarning Severity = "warning"
// SeverityHigh verlangt einen Blick heute.
SeverityHigh Severity = "high"
// SeverityCritical verlangt sofortiges Handeln.
//
// Sparsam vergeben: Wer nachts geweckt wird, muss auch etwas tun koennen.
// Ein kritischer Alarm ohne moegliche Handlung ist Schlafentzug ohne Nutzen.
SeverityCritical Severity = "critical"
)
// Rank liefert die Ordnungszahl eines Schweregrads.
func (severity Severity) Rank() int {
switch severity {
case SeverityCritical:
return 4
case SeverityHigh:
return 3
case SeverityWarning:
return 2
case SeverityInformation:
return 1
default:
return 0
}
}
// AtLeast meldet, ob dieser Schweregrad den geforderten erreicht.
func (severity Severity) AtLeast(minimumSeverity Severity) bool {
return severity.Rank() >= minimumSeverity.Rank()
}
// Bezeichner der Regeln (SYNCOVA_IMPLEMENTATION_PLAN.md §16).
const (
// RuleBackupFailure meldet einen gescheiterten Sicherungslauf.
RuleBackupFailure = "backup_failure"
// RuleRepeatedBackupFailure meldet mehrere Fehlschlaege in Folge.
RuleRepeatedBackupFailure = "repeated_backup_failure"
// RuleRepositoryUnavailable meldet ein nicht erreichbares Repository.
RuleRepositoryUnavailable = "repository_unavailable"
// RuleRepositoryFilling meldet ein zu 80 Prozent belegtes Repository.
RuleRepositoryFilling = "repository_filling"
// RuleRepositoryFull meldet ein zu 90 Prozent belegtes Repository.
RuleRepositoryFull = "repository_full"
// RuleAgentOffline meldet einen Agenten ohne Lebendmeldung.
RuleAgentOffline = "agent_offline"
// RuleRecoveryPointViolation meldet eine verletzte RPO-Vorgabe.
RuleRecoveryPointViolation = "rpo_violation"
// RuleVerificationFailure meldet eine Pruefung mit Befund.
RuleVerificationFailure = "verification_failure"
// RuleUnusualChangeRate meldet auffaellig viele neue Daten.
RuleUnusualChangeRate = "unusual_change_rate"
// RuleImmutableCopyMissing meldet ein Backup ohne Aufbewahrungsschutz.
RuleImmutableCopyMissing = "immutable_copy_missing"
// RuleCertificateExpiry meldet ein ablaufendes Agentenzertifikat.
RuleCertificateExpiry = "certificate_expiry"
)
// RuleDefinition beschreibt eine Regel.
type RuleDefinition struct {
// Name ist der maschinenlesbare Bezeichner.
Name string `json:"name"`
// Title ist die Bezeichnung.
Title string `json:"title"`
// Description erklaert, worauf die Regel achtet.
Description string `json:"description"`
// Severity ist der Schweregrad ausgeloester Meldungen.
Severity Severity `json:"severity"`
// Available meldet, ob die Regel ueberhaupt ausloesen kann.
Available bool `json:"available"`
// UnavailableReason erklaert eine Regel ohne Datengrundlage.
UnavailableReason string `json:"unavailable_reason,omitempty"`
}
// Schwellen der Regeln.
//
// Sie stehen hier beisammen, weil ihre Wahl den Wert des ganzen Meldungswesens
// bestimmt. Zu empfindlich bedeutet Alarmmuedigkeit, zu traege bedeutet, dass
// die Meldung zu spaet kommt.
const (
// repeatedFailureThreshold ist die Zahl der Fehlschlaege in Folge fuer die Eskalation.
//
// Zwei, nicht drei: Ein einzelner Fehlschlag kann ein Netzwerkzucken sein,
// zwei hintereinander sind ein Zustand.
repeatedFailureThreshold = 2
// fillingThresholdPercent ist die Belegung, ab der gewarnt wird.
fillingThresholdPercent = 80.0
// fullThresholdPercent ist die Belegung, ab der es kritisch wird.
fullThresholdPercent = 90.0
// agentOfflineAfter ist die Zeit ohne Lebendmeldung bis zur Meldung.
//
// Fuenfzehn Minuten bei einem Meldeabstand von einer Minute: Ein
// Netzwerkausfall von zwei Minuten weckt niemanden, eine Viertelstunde
// Schweigen ist ein Ausfall.
agentOfflineAfter = 15 * time.Minute
// Die Schwellen der Ransomware-Erkennung stehen seit Phase 16 im Paket
// ransomware: Sie werden dort gegen einen Basiswert je Kette gemessen, nicht
// gegen eine feste Zahl.
)
// Rules liefert alle Regeln in Anzeigereihenfolge.
func Rules() []RuleDefinition {
return []RuleDefinition{
{
Name: RuleBackupFailure,
Title: "Sicherung gescheitert",
Description: "Ein Sicherungslauf ist gescheitert oder meldete einen Teilfehler. Ein Teilfehler wird nicht wiederholt — er verlangt einen Blick.",
Severity: SeverityHigh,
Available: true,
},
{
Name: RuleRepeatedBackupFailure,
Title: "Sicherung mehrfach gescheitert",
Description: "Ein Auftrag ist zweimal in Folge gescheitert. Ein einzelner Fehlschlag kann ein Netzwerkzucken sein, zwei hintereinander sind ein Zustand.",
Severity: SeverityCritical,
Available: true,
},
{
Name: RuleRepositoryUnavailable,
Title: "Repository nicht erreichbar",
Description: "Ein Repository ist als nicht erreichbar geführt. Solange das gilt, kann weder gesichert noch wiederhergestellt werden.",
Severity: SeverityCritical,
Available: true,
},
{
Name: RuleRepositoryFilling,
Title: "Repository zu 80 Prozent belegt",
Description: "Der Speicher wird knapp. Jetzt gibt es noch Zeit, aufzuräumen oder zu erweitern.",
Severity: SeverityWarning,
Available: true,
},
{
Name: RuleRepositoryFull,
Title: "Repository zu 90 Prozent belegt",
Description: "Der Speicher läuft voll. Eine Sicherung, die keinen Platz mehr findet, scheitert — und zwar genau dann, wenn niemand hinsieht.",
Severity: SeverityCritical,
Available: true,
},
{
Name: RuleAgentOffline,
Title: "Agent meldet sich nicht",
Description: "Ein Agent hat seit einer Viertelstunde keine Lebendmeldung geschickt. Seine Quellen werden derzeit nicht gesichert.",
Severity: SeverityHigh,
Available: true,
},
{
Name: RuleRecoveryPointViolation,
Title: "Wiederherstellungspunkt überfällig",
Description: "Die letzte erfolgreiche Sicherung eines Auftrags liegt länger zurück als seine RPO-Vorgabe. Im Ernstfall gingen mehr Daten verloren als zugesagt.",
Severity: SeverityHigh,
Available: true,
},
{
Name: RuleVerificationFailure,
Title: "Prüfung mit Befund",
Description: "Eine Prüfung fand beschädigte oder fehlende Blöcke. Das betroffene Backup ist nicht wiederherstellbar.",
Severity: SeverityCritical,
Available: true,
},
{
Name: RuleUnusualChangeRate,
Title: "Auffällig viele neue Daten",
Description: "Ein Sicherungslauf weicht statistisch vom üblichen Verlauf seiner Kette ab — gemessen an " +
"sechs Signalen gegen einen Basiswert aus den letzten Läufen, nicht gegen eine feste Schwelle. " +
"Das ist ein **Verdacht**, keine Feststellung: Massenhafte Verschlüsselung sieht so aus — " +
"ein großes Update, eine Datenbankreorganisation oder ein Kettenwechsel aber auch. " +
"Die Anlage unternimmt von sich aus nichts.",
Severity: SeverityHigh,
Available: true,
},
{
Name: RuleImmutableCopyMissing,
Title: "Backup ohne Aufbewahrungsschutz",
Description: "Ein Wiederherstellungspunkt liegt in einem Repository ohne Löschschutz. Wer Zugriff auf die Dateien hat, kann ihn entfernen.",
Severity: SeverityWarning,
Available: true,
},
{
Name: RuleCertificateExpiry,
Title: "Agentenzertifikat läuft ab",
Description: "Ein Zertifikat eines Agenten läuft in Kürze ab.",
Severity: SeverityWarning,
Available: false,
UnavailableReason: "Die Agenten weisen sich über Betriebstokens aus, nicht über Zertifikate. " +
"Die Tabelle agent_certificates wird von keiner Stelle beschrieben; die Regel könnte " +
"deshalb nie auslösen. Eine Regel, die dauerhaft schweigt, ist gefährlicher als keine — " +
"sie erweckt den Eindruck, es werde geprüft.",
},
}
}
// FindRule sucht eine Regel anhand ihres Namens.
func FindRule(ruleName string) (RuleDefinition, bool) {
for _, ruleDefinition := range Rules() {
if ruleDefinition.Name == ruleName {
return ruleDefinition, true
}
}
return RuleDefinition{}, false
}
// AvailableRuleCount zaehlt die ausloesbaren Regeln.
func AvailableRuleCount() int {
availableCount := 0
for _, ruleDefinition := range Rules() {
if ruleDefinition.Available {
availableCount++
}
}
return availableCount
}