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>
216 lines
8.2 KiB
Go
216 lines
8.2 KiB
Go
// Package ransomware erkennt statistische Auffaelligkeiten in Sicherungslaeufen.
|
|
//
|
|
// Die Vorgabe des Implementierungsplans (§18) steht in vier Worten:
|
|
//
|
|
// Do not make destructive decisions automatically. Alert first.
|
|
//
|
|
// Dieses Paket loescht nichts, sperrt nichts und haelt nichts an. Es beschreibt,
|
|
// was auffaellig ist, und ueberlaesst jede Entscheidung einem Menschen. Der
|
|
// Grund ist nicht Vorsicht, sondern Erfahrung: Eine Heuristik, die
|
|
// selbsttaetig handelt, macht aus jedem Fehlalarm einen Schaden — und
|
|
// Fehlalarme wird es geben, weil ein Betriebssystem-Update von aussen genauso
|
|
// aussieht wie ein Verschluesselungsangriff.
|
|
package ransomware
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
// minimumBaselineSamples ist die Zahl der Laeufe fuer einen belastbaren Basiswert.
|
|
//
|
|
// Fuenf, nicht drei: Mit drei Messwerten ist ein Median eine Formalitaet und die
|
|
// Streuung Zufall. Darunter gibt es **keine Aussage** — und keine geratene
|
|
// Schwelle, die so tut als gaebe es eine.
|
|
const minimumBaselineSamples = 5
|
|
|
|
// maximumBaselineSamples begrenzt die Zahl betrachteter Laeufe.
|
|
//
|
|
// Zwanzig: Genug fuer einen stabilen Wert, kurz genug, dass sich der Basiswert
|
|
// an einen veraenderten Betrieb anpasst. Ein Basiswert aus einem Jahr
|
|
// beschriebe eine Anlage, die es so nicht mehr gibt.
|
|
const maximumBaselineSamples = 20
|
|
|
|
// Baseline ist der statistische Normalwert einer Groesse.
|
|
//
|
|
// Median und mittlere absolute Abweichung statt Mittelwert und
|
|
// Standardabweichung: Bei wenigen Laeufen macht ein einziger Ausreisser den
|
|
// Mittelwert unbrauchbar — und genau ein Ausreisser ist der Fall, den wir
|
|
// erkennen wollen. Wuerde er den Basiswert mitbestimmen, hobe er die Schwelle
|
|
// an, gegen die er gemessen wird.
|
|
type Baseline struct {
|
|
// SampleCount ist die Zahl eingeflossener Laeufe.
|
|
SampleCount int `json:"sample_count"`
|
|
// Median ist der mittlere Wert.
|
|
Median float64 `json:"median"`
|
|
// AbsoluteDeviation ist die mittlere absolute Abweichung vom Median.
|
|
AbsoluteDeviation float64 `json:"absolute_deviation"`
|
|
// Minimum ist der kleinste beobachtete Wert.
|
|
Minimum float64 `json:"minimum"`
|
|
// Maximum ist der groesste beobachtete Wert.
|
|
Maximum float64 `json:"maximum"`
|
|
}
|
|
|
|
// IsUsable meldet einen belastbaren Basiswert.
|
|
func (baseline Baseline) IsUsable() bool {
|
|
return baseline.SampleCount >= minimumBaselineSamples
|
|
}
|
|
|
|
// BuildBaseline bildet den Basiswert aus beobachteten Werten.
|
|
//
|
|
// Die juengsten Werte stehen vorn; ueberzaehlige aeltere fallen weg.
|
|
func BuildBaseline(observedValues []float64) Baseline {
|
|
if len(observedValues) == 0 {
|
|
return Baseline{}
|
|
}
|
|
|
|
consideredValues := observedValues
|
|
if len(consideredValues) > maximumBaselineSamples {
|
|
consideredValues = consideredValues[:maximumBaselineSamples]
|
|
}
|
|
|
|
sortedValues := make([]float64, len(consideredValues))
|
|
copy(sortedValues, consideredValues)
|
|
sort.Float64s(sortedValues)
|
|
|
|
medianValue := medianOf(sortedValues)
|
|
|
|
deviations := make([]float64, 0, len(sortedValues))
|
|
for _, sortedValue := range sortedValues {
|
|
deviations = append(deviations, math.Abs(sortedValue-medianValue))
|
|
}
|
|
|
|
sort.Float64s(deviations)
|
|
|
|
return Baseline{
|
|
SampleCount: len(sortedValues),
|
|
Median: medianValue,
|
|
AbsoluteDeviation: medianOf(deviations),
|
|
Minimum: sortedValues[0],
|
|
Maximum: sortedValues[len(sortedValues)-1],
|
|
}
|
|
}
|
|
|
|
// medianOf liefert den Median einer sortierten Reihe.
|
|
func medianOf(sortedValues []float64) float64 {
|
|
valueCount := len(sortedValues)
|
|
if valueCount == 0 {
|
|
return 0
|
|
}
|
|
|
|
middleIndex := valueCount / 2
|
|
|
|
if valueCount%2 == 1 {
|
|
return sortedValues[middleIndex]
|
|
}
|
|
|
|
return (sortedValues[middleIndex-1] + sortedValues[middleIndex]) / 2
|
|
}
|
|
|
|
// deviationMultiplier ist die Zahl der Abweichungen bis zur Auffaelligkeit.
|
|
//
|
|
// Drei: Bei normalverteilten Werten liegen darunter rund 99 Prozent. Ein
|
|
// niedrigerer Wert erzeugte staendig Fehlalarme, ein hoeherer liesse den
|
|
// eigentlichen Fall durchgehen.
|
|
const deviationMultiplier = 3.0
|
|
|
|
// minimumRelativeChange ist die kleinste beachtete relative Veraenderung.
|
|
//
|
|
// Ohne diese Untergrenze schluege die Erkennung bei jeder winzigen Schwankung
|
|
// an, sobald die Streuung des Basiswerts nahe null liegt — etwa bei einer
|
|
// Quelle, die sich nie aendert.
|
|
const minimumRelativeChange = 0.5
|
|
|
|
// DeviationResult beschreibt die Abweichung eines Werts vom Basiswert.
|
|
type DeviationResult struct {
|
|
// ObservedValue ist der beobachtete Wert.
|
|
ObservedValue float64 `json:"observed_value"`
|
|
// Baseline ist der zugrunde liegende Basiswert.
|
|
Baseline Baseline `json:"baseline"`
|
|
// IsUnusual meldet eine Abweichung ueber der Schwelle.
|
|
IsUnusual bool `json:"is_unusual"`
|
|
// DeviationFactor ist die Abweichung in Vielfachen der mittleren Abweichung.
|
|
//
|
|
// Null, wenn sich kein Faktor bilden laesst — etwa bei einer Streuung von
|
|
// null. Das Feld traegt dann keine Aussage, und IsUnusual entscheidet.
|
|
DeviationFactor float64 `json:"deviation_factor"`
|
|
// Explanation erklaert die Einordnung in einem Satz.
|
|
Explanation string `json:"explanation"`
|
|
}
|
|
|
|
// EvaluateDeviation misst einen Wert gegen seinen Basiswert.
|
|
//
|
|
// Betrachtet wird ausschliesslich die Abweichung **nach oben**: Weniger
|
|
// geaenderte Dateien als sonst sind kein Angriffszeichen, und eine
|
|
// zweiseitige Pruefung erzeugte bei jedem ruhigen Wochenende einen Befund.
|
|
//
|
|
// minimumAbsoluteChange ist die Untergrenze, unterhalb derer nichts auffaellt.
|
|
// Sie ist je Signal verschieden und deshalb ein Parameter: Bei einer Quelle mit
|
|
// zwei geaenderten Dateien je Lauf ist „drei statt zwei" eine Steigerung um
|
|
// fuenfzig Prozent — und vollkommen belanglos. Bei einer Datenmenge waeren
|
|
// dieselben fuenfzig Prozent dagegen aussagekraeftig, sobald es um Gigabyte
|
|
// geht. Eine gemeinsame Grenze fuer beide gibt es nicht.
|
|
func EvaluateDeviation(observedValue float64, baseline Baseline, minimumAbsoluteChange float64) DeviationResult {
|
|
result := DeviationResult{ObservedValue: observedValue, Baseline: baseline}
|
|
|
|
if !baseline.IsUsable() {
|
|
result.Explanation = fmt.Sprintf("Es liegen erst %d von %d noetigen Laeufen vor. "+
|
|
"Ohne Basiswert gibt es keine Aussage — eine geratene Schwelle waere schlechter "+
|
|
"als keine.", baseline.SampleCount, minimumBaselineSamples)
|
|
|
|
return result
|
|
}
|
|
|
|
if observedValue <= baseline.Median {
|
|
result.Explanation = fmt.Sprintf("Der Wert liegt bei %.1f und damit nicht ueber dem "+
|
|
"ueblichen Stand von %.1f.", observedValue, baseline.Median)
|
|
|
|
return result
|
|
}
|
|
|
|
absoluteChange := observedValue - baseline.Median
|
|
|
|
// Unterhalb der absoluten Untergrenze faellt nichts auf, egal wie gross der
|
|
// Faktor rechnerisch waere.
|
|
if absoluteChange < minimumAbsoluteChange {
|
|
// Die Untergrenze wird ausdruecklich genannt. Ohne sie liest sich der Satz
|
|
// bei einem tausendfachen Anstieg wie ein Fehler des Werkzeugs — und wer
|
|
// dem Werkzeug einmal misstraut, liest auch den echten Befund nicht mehr.
|
|
result.Explanation = fmt.Sprintf("Der Wert stieg von %.1f auf %.1f. Der Zuwachs von %.1f "+
|
|
"bleibt unter der Untergrenze von %.1f, ab der diese Groesse etwas aussagt.",
|
|
baseline.Median, observedValue, absoluteChange, minimumAbsoluteChange)
|
|
|
|
return result
|
|
}
|
|
|
|
// Eine Streuung von null bedeutet: Die Groesse war bisher immer gleich.
|
|
// Dann zaehlt die relative Veraenderung, nicht das Vielfache einer
|
|
// Abweichung, die es nicht gibt.
|
|
if baseline.AbsoluteDeviation <= 0 {
|
|
relativeChange := absoluteChange / math.Max(baseline.Median, 1)
|
|
|
|
result.IsUnusual = relativeChange >= minimumRelativeChange
|
|
result.Explanation = fmt.Sprintf("Bisher lag der Wert stets bei %.1f, jetzt bei %.1f "+
|
|
"(%.0f Prozent mehr).", baseline.Median, observedValue, relativeChange*100)
|
|
|
|
return result
|
|
}
|
|
|
|
result.DeviationFactor = absoluteChange / baseline.AbsoluteDeviation
|
|
relativeChange := absoluteChange / math.Max(baseline.Median, 1)
|
|
|
|
// Beide Bedingungen muessen zutreffen. Der Faktor allein schluege bei sehr
|
|
// gleichmaessigen Werten schon bei geringfuegigen Aenderungen an; die
|
|
// relative Aenderung allein uebersaehe eine Quelle, die ohnehin stark
|
|
// schwankt.
|
|
result.IsUnusual = result.DeviationFactor >= deviationMultiplier &&
|
|
relativeChange >= minimumRelativeChange
|
|
|
|
result.Explanation = fmt.Sprintf("Der Wert liegt bei %.1f, ueblich sind %.1f "+
|
|
"(%.1f-fache der ueblichen Schwankung, %.0f Prozent mehr).",
|
|
observedValue, baseline.Median, result.DeviationFactor, relativeChange*100)
|
|
|
|
return result
|
|
}
|