// 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 }