package ransomware import ( "strings" "testing" ) // TestBaselineNeedsEnoughSamples prueft die Mindestzahl an Laeufen. // // 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. func TestBaselineNeedsEnoughSamples(testInstance *testing.T) { thinBaseline := BuildBaseline([]float64{100, 105, 98}) if thinBaseline.IsUsable() { testInstance.Error("ein Basiswert aus drei Laeufen gilt als belastbar") } deviation := EvaluateDeviation(10000, thinBaseline, 20) if deviation.IsUnusual { testInstance.Error("ohne Basiswert wurde ein Wert als auffaellig gemeldet") } // Die Erklaerung muss sagen, warum es keine Aussage gibt. if !strings.Contains(deviation.Explanation, "Basiswert") { testInstance.Errorf("die Erklaerung nennt den fehlenden Basiswert nicht: %q", deviation.Explanation) } solidBaseline := BuildBaseline([]float64{100, 105, 98, 102, 99, 101}) if !solidBaseline.IsUsable() { testInstance.Error("ein Basiswert aus sechs Laeufen gilt nicht als belastbar") } } // TestMedianResistsSingleOutlier ist der Kerntest der robusten Statistik. // // Ein einziger Ausreisser ist genau der Fall, den wir erkennen wollen. Wuerde er // den Basiswert mitbestimmen, hobe er die Schwelle an, gegen die er gemessen // wird — und der naechste Angriff derselben Groesse faellt nicht mehr auf. func TestMedianResistsSingleOutlier(testInstance *testing.T) { // Fuenf normale Laeufe und ein Ausreisser, wie ihn ein Angriff erzeugt. withOutlier := BuildBaseline([]float64{100, 102, 98, 101, 99, 50000}) withoutOutlier := BuildBaseline([]float64{100, 102, 98, 101, 99}) // Der Median darf sich kaum bewegen. Ein Mittelwert laege bei ueber 8000. medianShift := withOutlier.Median - withoutOutlier.Median if medianShift > 5 || medianShift < -5 { testInstance.Errorf("der Median verschob sich um %.1f; ein einzelner Ausreisser "+ "bestimmt den Basiswert mit", medianShift) } if withOutlier.Median > 1000 { testInstance.Errorf("der Basiswert liegt bei %.1f — das ist ein Mittelwert, kein Median", withOutlier.Median) } } // TestUnusualValueIsDetected prueft die Erkennung einer echten Abweichung. func TestUnusualValueIsDetected(testInstance *testing.T) { // Ein Bestand, der sich taeglich um wenige Prozent aendert. normalBaseline := BuildBaseline([]float64{100, 105, 98, 102, 99, 101, 103}) // Ein Lauf, bei dem ploetzlich das Hundertfache neu ist. deviation := EvaluateDeviation(10000, normalBaseline, 20) if !deviation.IsUnusual { testInstance.Fatalf("eine hundertfache Abweichung wurde nicht erkannt: %q", deviation.Explanation) } if deviation.DeviationFactor < deviationMultiplier { testInstance.Errorf("der Abweichungsfaktor ist %.1f, erwartet wurde mindestens %.1f", deviation.DeviationFactor, deviationMultiplier) } } // TestSmallFluctuationIsNotUnusual prueft die Vermeidung von Fehlalarmen. // // **Der wichtigste Test des Pakets.** Falsch-Positive sind der Grund, warum // solche Systeme abgeschaltet werden — und ein abgeschaltetes System erkennt // gar nichts. func TestSmallFluctuationIsNotUnusual(testInstance *testing.T) { normalBaseline := BuildBaseline([]float64{100, 105, 98, 102, 99, 101, 103}) // Ein Lauf mit zwanzig Prozent mehr — ein groesserer Arbeitstag. for _, ordinaryValue := range []float64{110, 120, 125} { deviation := EvaluateDeviation(ordinaryValue, normalBaseline, 20) if deviation.IsUnusual { testInstance.Errorf("ein Wert von %.0f bei einem ueblichen Stand von %.0f wurde als "+ "auffaellig gemeldet: %q", ordinaryValue, normalBaseline.Median, deviation.Explanation) } } } // TestLowerValuesAreNeverUnusual prueft die Einseitigkeit der Pruefung. // // Weniger geaenderte Dateien als sonst sind kein Angriffszeichen. Eine // zweiseitige Pruefung erzeugte bei jedem ruhigen Wochenende einen Befund. func TestLowerValuesAreNeverUnusual(testInstance *testing.T) { normalBaseline := BuildBaseline([]float64{100, 105, 98, 102, 99, 101, 103}) for _, quietValue := range []float64{0, 1, 50} { deviation := EvaluateDeviation(quietValue, normalBaseline, 20) if deviation.IsUnusual { testInstance.Errorf("ein Wert von %.0f unterhalb des ueblichen Stands wurde als "+ "auffaellig gemeldet", quietValue) } } } // TestConstantBaselineUsesRelativeChange prueft den Fall ohne Streuung. // // Eine Quelle, die sich nie aendert, hat eine Streuung von null. Ohne // Sonderbehandlung waere dort jede noch so kleine Aenderung unendlich viele // Abweichungen entfernt — aus „von 2 auf 3 Dateien" wuerde ein Alarm. func TestConstantBaselineUsesRelativeChange(testInstance *testing.T) { constantBaseline := BuildBaseline([]float64{100, 100, 100, 100, 100, 100}) if constantBaseline.AbsoluteDeviation != 0 { testInstance.Fatalf("die Streuung ist %.1f, erwartet wurde null", constantBaseline.AbsoluteDeviation) } // Zehn Prozent mehr: kein Befund. if EvaluateDeviation(110, constantBaseline, 5).IsUnusual { testInstance.Error("eine Aenderung um zehn Prozent wurde als auffaellig gemeldet") } // Verdopplung: Befund. if !EvaluateDeviation(200, constantBaseline, 5).IsUnusual { testInstance.Error("eine Verdopplung wurde nicht erkannt") } } // TestTinyNumbersDoNotTriggerAlarms prueft die absolute Untergrenze. // // Bei einer Quelle mit zwei geaenderten Dateien je Lauf ist „drei statt zwei" // eine Verfuenfzigprozentung — und vollkommen belanglos. func TestTinyNumbersDoNotTriggerAlarms(testInstance *testing.T) { tinyBaseline := BuildBaseline([]float64{2, 2, 3, 2, 2, 3}) // Von zwei auf drei: Der Median liegt bei 2, die Streuung bei 0. deviation := EvaluateDeviation(3, tinyBaseline, 20) if deviation.IsUnusual { testInstance.Errorf("'drei statt zwei Dateien' wurde als auffaellig gemeldet: %q", deviation.Explanation) } } // TestBaselineIsLimitedToRecentRuns prueft die Begrenzung der Historie. // // Ein Basiswert aus einem Jahr beschriebe eine Anlage, die es so nicht mehr gibt. func TestBaselineIsLimitedToRecentRuns(testInstance *testing.T) { manyValues := make([]float64, 0, 100) // Die juengsten Werte stehen vorn. for valueIndex := 0; valueIndex < maximumBaselineSamples; valueIndex++ { manyValues = append(manyValues, 1000) } for valueIndex := 0; valueIndex < 80; valueIndex++ { manyValues = append(manyValues, 1) } limitedBaseline := BuildBaseline(manyValues) if limitedBaseline.SampleCount != maximumBaselineSamples { testInstance.Errorf("%d Werte flossen ein, erlaubt sind %d", limitedBaseline.SampleCount, maximumBaselineSamples) } // Die alten Werte duerfen den Basiswert nicht bestimmen. if limitedBaseline.Median != 1000 { testInstance.Errorf("der Basiswert ist %.1f; alte Laeufe wirken noch nach", limitedBaseline.Median) } }