syncova-backup/packages/security/findings.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

218 lines
7.4 KiB
Go

// Package security beurteilt die Sicherheitslage der Anlage.
//
// Die Regel dieses Pakets steht in SYNCOVA_IMPLEMENTATION_PLAN.md §17 und ist
// ungewoehnlich konkret: **Jeder Befund traegt Schweregrad, Erklaerung,
// betroffenes Objekt und Empfehlung.**
//
// Der letzte Punkt ist der wichtigste. Ein Befund ohne Empfehlung ist eine
// Beunruhigung: Er sagt, dass etwas nicht stimmt, und laesst den Betreiber damit
// allein. Wer „MFA fehlt" liest, weiss noch nicht, ob er es selbst einrichten
// kann oder einen Administrator braucht.
package security
import (
"fmt"
"time"
)
// Severity ist der Schweregrad eines Befunds.
type Severity string
const (
// SeverityInformation ist eine Auskunft ohne Handlungsbedarf.
SeverityInformation Severity = "information"
// SeverityWarning verlangt einen Blick bei Gelegenheit.
SeverityWarning Severity = "warning"
// SeverityHigh verlangt eine Aenderung.
SeverityHigh Severity = "high"
// SeverityCritical bedeutet: Die Anlage ist so nicht sicher zu betreiben.
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
}
}
// Bezeichner der Pruefbereiche (SYNCOVA_IMPLEMENTATION_PLAN.md §17).
const (
// AreaMultiFactor prueft den zweiten Faktor der Konten.
AreaMultiFactor = "mfa"
// AreaEncryption prueft die Verschluesselung der Backups.
AreaEncryption = "encryption"
// AreaImmutability prueft den Loeschschutz der Repositories.
AreaImmutability = "immutability"
// AreaOffsite prueft die Kopie an einem zweiten Ort.
AreaOffsite = "offsite"
// AreaAudit prueft das Auditprotokoll.
AreaAudit = "audit"
// AreaCertificates prueft die Zertifikate der Agenten.
AreaCertificates = "certificates"
// AreaAgentSecurity prueft die Absicherung der Agenten.
AreaAgentSecurity = "agent_security"
// AreaUpdates prueft den Versionsstand.
AreaUpdates = "updates"
// AreaRansomwareRisk prueft die Widerstandsfaehigkeit gegen Verschluesselung.
AreaRansomwareRisk = "ransomware_risk"
// AreaAccessControl prueft die Verteilung destruktiver Rechte.
AreaAccessControl = "access_control"
)
// Finding ist ein einzelner Sicherheitsbefund.
//
// Die vier Pflichtfelder aus dem Plan sind nicht optional: Ein Befund ohne
// Empfehlung waere ein Alarm ohne Ausweg, und ein Befund ohne benanntes Objekt
// liesse den Betreiber suchen.
type Finding struct {
// Area ist der Pruefbereich.
Area string `json:"area"`
// Severity ist der Schweregrad.
Severity Severity `json:"severity"`
// Title ist die Ueberschrift.
Title string `json:"title"`
// Explanation erklaert, warum der Zustand ein Problem ist.
//
// Nicht „MFA fehlt", sondern warum das zaehlt: Ein gestohlenes Passwort
// genuegt dann, um Backups zu loeschen.
Explanation string `json:"explanation"`
// AffectedObject benennt das betroffene Objekt.
AffectedObject string `json:"affected_object"`
// Recommendation nennt die naechste Handlung.
Recommendation string `json:"recommendation"`
}
// Validate prueft einen Befund auf Vollstaendigkeit.
//
// Die Pruefung existiert, weil ein unvollstaendiger Befund erst im Betrieb
// auffaellt — dann, wenn jemand ratlos vor „Sicherheitsproblem erkannt" steht.
func (finding Finding) Validate() error {
if finding.Title == "" {
return fmt.Errorf("dem befund im bereich %s fehlt die ueberschrift", finding.Area)
}
if finding.Explanation == "" {
return fmt.Errorf("dem befund %q fehlt die erklaerung", finding.Title)
}
if finding.AffectedObject == "" {
return fmt.Errorf("dem befund %q fehlt das betroffene objekt", finding.Title)
}
if finding.Recommendation == "" {
return fmt.Errorf("dem befund %q fehlt die empfehlung", finding.Title)
}
return nil
}
// AreaResult ist das Ergebnis eines Pruefbereichs.
type AreaResult struct {
// Area ist der Bezeichner des Bereichs.
Area string `json:"area"`
// Title ist die Bezeichnung.
Title string `json:"title"`
// Available meldet, ob der Bereich geprueft werden konnte.
//
// Der Unterschied zu „keine Befunde" ist der Kern der ehrlichen Bewertung:
// „Wir haben nichts gefunden" und „wir haben nicht nachgesehen" sind zwei
// verschiedene Aussagen, und nur die erste ist beruhigend.
Available bool `json:"available"`
// UnavailableReason erklaert einen nicht pruefbaren Bereich.
UnavailableReason string `json:"unavailable_reason,omitempty"`
// Findings sind die Befunde des Bereichs.
Findings []Finding `json:"findings"`
// Summary fasst den Bereich in einem Satz zusammen.
Summary string `json:"summary"`
// Weight ist das Gewicht des Bereichs in der Gesamtbewertung.
Weight int `json:"weight"`
// EarnedPoints sind die erreichten Punkte.
EarnedPoints int `json:"earned_points"`
}
// WorstSeverity liefert den hoechsten Schweregrad des Bereichs.
func (result *AreaResult) WorstSeverity() Severity {
worstSeverity := Severity("")
for _, finding := range result.Findings {
if finding.Severity.Rank() > worstSeverity.Rank() {
worstSeverity = finding.Severity
}
}
return worstSeverity
}
// Assessment ist die vollstaendige Sicherheitslage.
type Assessment struct {
// Score ist die Gesamtbewertung in Prozent.
Score int `json:"score"`
// MaximumScore sind die erreichbaren Punkte der gepruefte Bereiche.
MaximumScore int `json:"maximum_score"`
// Grade ist die Einstufung der Lage.
Grade string `json:"grade"`
// Areas sind die Ergebnisse je Bereich.
Areas []AreaResult `json:"areas"`
// UncheckedAreaCount ist die Zahl nicht pruefbarer Bereiche.
//
// Sie steht neben der Prozentzahl, damit niemand eine Bewertung fuer
// belastbar haelt, die auf lauter Ungeprueftem beruht — dieselbe
// Ueberlegung wie bei der Recovery Assurance (Phase 10).
UncheckedAreaCount int `json:"unchecked_area_count"`
// CriticalFindingCount ist die Zahl kritischer Befunde.
CriticalFindingCount int `json:"critical_finding_count"`
// HighFindingCount ist die Zahl ernster Befunde.
HighFindingCount int `json:"high_finding_count"`
// Summary fasst die Lage in einem Satz zusammen.
Summary string `json:"summary"`
// UncheckedAreas nennt die nicht gepruefte Bereiche als Handlungsanweisung.
UncheckedAreas []string `json:"unchecked_areas"`
// EvaluatedAt ist der Zeitpunkt der Bewertung in UTC.
EvaluatedAt time.Time `json:"evaluated_at"`
}
// AllFindings liefert alle Befunde nach Schweregrad geordnet.
func (assessment *Assessment) AllFindings() []Finding {
allFindings := make([]Finding, 0, 16)
// Die Reihenfolge folgt dem Schweregrad, nicht dem Bereich: Wer die Liste
// oeffnet, soll oben lesen, was zuerst zu tun ist.
for _, severityLevel := range []Severity{
SeverityCritical, SeverityHigh, SeverityWarning, SeverityInformation,
} {
for _, areaResult := range assessment.Areas {
for _, finding := range areaResult.Findings {
if finding.Severity == severityLevel {
allFindings = append(allFindings, finding)
}
}
}
}
return allFindings
}
// IsTrustworthy meldet eine Bewertung, die auf genug Pruefungen beruht.
//
// Mehr als zwei ungeprueft Bereiche machen die Prozentzahl zur Vermutung. Ein
// kritischer Befund schliesst Vertrauenswuerdigkeit ohnehin aus — dieselbe
// Regel wie bei der Recovery Assurance, wo ein beschaedigtes Backup nie „gut"
// sein kann.
func (assessment *Assessment) IsTrustworthy() bool {
if assessment.CriticalFindingCount > 0 {
return false
}
return assessment.UncheckedAreaCount <= 2
}