Blog
Erst Abdeckung, dann Entwarnung
Einen stillen Sicherheitsbericht betrachte ich als Frage nach der Beobachtung, nicht als Sicherheitszusage.
Was wurde beobachtet?
Eine leere Befundliste kann bedeuten, dass Prüfungen liefen und nichts fanden. Sie kann ebenso bedeuten, dass Eingaben fehlten, übersprungen wurden oder nicht unterstützt waren. Ich möchte in einem Bericht lesen, was versucht, tatsächlich untersucht und nicht untersucht wurde. Ohne diese Unterscheidung kann die sauberste Zusammenfassung die unbrauchbarste sein. Das Ausbleiben einer Warnung ist nur innerhalb einer abgeschlossenen, passenden Prüfung aussagekräftig.
Außerdem soll der Bericht sagen, ob eine Prüfung überhaupt anwendbar war. Eine Abhängigkeitsprüfung in einem Projekt ohne Abhängigkeiten hat nichts zu untersuchen, und das ist etwas anderes als eine Prüfung, die ihre Eingabe nicht lesen konnte. Nicht anwendbar von nicht geprüft zu trennen hält die Zusammenfassung ehrlich, ohne jedes stille Ergebnis zur Warnung zu machen.
Meist bricht diese Disziplin bei der Zusammenfassung. Eine Übersicht, die nur Befunde zählt, presst einen unvollständigen Lauf bereitwillig in eine saubere Seite. Mir ist eine Zusammenfassung lieber, die je Zustand eine Anzahl und für jeden übersprungenen Schritt einen kurzen Grund trägt, damit Lesende sehen, wie viel des beabsichtigten Feldes tatsächlich abgedeckt wurde.
{
"checks": [
{ "name": "dependencies", "state": "inspected", "findings": 0 },
{ "name": "permissions", "state": "not-scanned", "reason": "input missing" },
{ "name": "licenses", "state": "not-applicable", "reason": "no bundled assets" }
],
"summary": { "inspected": 1, "notScanned": 1, "notApplicable": 1 }
}Lücken sichtbar lassen
Für nicht geprüft brauche ich einen eigenen Zustand, statt ihn unter bestanden einzuordnen. Kann eine Prüfung ihre Eingabe nicht lesen, ändert sich ihr Umfang oder endet sie vorzeitig, muss diese Information in der Zusammenfassung erhalten bleiben. Das macht den Bericht auf den ersten Blick weniger beruhigend. Dafür kann jemand entscheiden, ob ein erneuter Lauf, fehlender Kontext oder eine andere Art der Prüfung nötig ist.
In der Praxis modelliere ich eine Prüfung als kleinen Zustandsautomaten: versucht, untersucht und danach ein Ergebnis wie sauber, Befund oder übersprungen mit Grund. Die Zustände sind keine Zierde; sie bestimmen, was die nächste Person tun sollte. Eine übersprungene Prüfung lädt dazu ein, fehlende Eingaben zu liefern oder eine andere Prüfungsart zu wählen, eine saubere dazu, weiterzugehen.
Dieselbe Unterscheidung muss bis in die Automatisierung reichen. Behandelt eine Pipeline unvollständige Abdeckung als Erfolg, ist die sorgfältige Formulierung des Berichts verschwendet. Ich ordne den Zuständen vorsichtige Rückgabecodes zu: einen für Befunde, einen anderen für einen unvollständigen Lauf und Erfolg nur dann, wenn jede anwendbare Prüfung ihre Eingabe tatsächlich untersucht hat.
#!/usr/bin/env bash
set -euo pipefail
report="report.json"
gaps=$(jq '[.checks[] | select(.state == "not-scanned")] | length' "$report")
findings=$(jq '[.checks[].findings // 0] | add' "$report")
# An incomplete run must not be reported as success.
if [ "$gaps" -gt 0 ]; then
echo "coverage incomplete: $gaps check(s) did not inspect their input" >&2
exit 2
fi
if [ "$findings" -gt 0 ]; then
echo "$findings finding(s) need review" >&2
exit 1
fi
echo "all checks inspected their input"
Den Berichtsweg prüfen
Ich würde den Berichtsweg mit einer geeigneten, einer ungeeigneten und einer bewusst nicht verfügbaren Eingabe testen. Das Ergebnis muss ‚kein Befund‘ von ‚keine Untersuchung‘ unterscheiden, ohne eine Schwachstelle zu erfinden. Damit ist nicht bewiesen, dass jede Gefahr erkannt wird. Es ist nur geprüft, dass ein Ausfall der Abdeckung nicht als sauberes Ergebnis erscheint. Danach frage ich weiter, welche Risiken grundsätzlich außerhalb der Prüfungen liegen.
Eine nützliche Übung ist, den Berichtsweg gegen wenige Beispiele laufen zu lassen: eine geeignete Eingabe, eine nicht anwendbare und eine nicht lesbare. Danach prüfe ich die Zusammenfassung statt die Prüfung. Erscheint die fehlende Eingabe als Lücke oder verschwindet sie? Bleibt die nicht anwendbare Prüfung neutral? Solche Zusicherungen sind billig und schützen die wichtigste Eigenschaft des Berichts.
Ich halte die Aussage bescheiden. Damit ist belegt, dass der Berichtsweg fehlende Befunde von fehlender Untersuchung unterscheidet; nicht, dass die Prüfungen alles finden oder dass die Liste der Prüfungen die wichtigen Risiken abdeckt. Ich frage weiter, was außerhalb des Werkzeugs liegt, denn Abdeckung beginnt bei der Wahl, wohin man schaut.
type CheckState = "inspected" | "not-scanned" | "not-applicable";
interface CheckResult {
name: string;
state: CheckState;
findings: number;
reason?: string;
}
// The summary keeps gaps visible instead of folding every
// quiet result into "pass".
export function summarize(results: readonly CheckResult[]) {
const inspected = results.filter((r) => r.state === "inspected");
const notScanned = results.filter((r) => r.state === "not-scanned");
return {
findings: inspected.reduce((sum, r) => sum + r.findings, 0),
inspected: inspected.length,
gaps: notScanned.map((r) => r.reason ?? r.name),
};
}