Blog / Entwicklung
Entwicklung

Eine 300.000-Einträge-Schwachstellendatenbank, die aktuell bleibt.

Eine Schwachstellendatenbank ist nur nützlich, wenn sie aktuell, präzise ist und der Art entspricht, wie Code tatsächlich ausgeliefert wird. Hier beschreiben wir, wie wir Ingestion, Deduplizierung und Versionsabgleich im Maßstab öffentlicher Bedrohungs-Feeds angegangen sind und was wir gelernt haben, um False Positives unter 4 % zu halten.

300k+
Indizierte Schwachstellen-Einträge
Über öffentliche Bedrohungs-Feeds
<4%
False-Positive-Rate bei Treffern
Gemessen an kuratiertem Testkorpus
~Minuten
Von öffentlicher Veröffentlichung bis Dashboard
Median der letzten 90 Tage

Die meisten Schwachstellentools teilen ein gemeinsames Versagen. Sie liefern eine lange Liste von Befunden, von denen die meisten für Ihren Code gar nicht gelten, und einige wenige kritische, die in der Masse untergehen. Das Team gibt die Liste innerhalb einer Woche auf, und ein Jahr später wird etwas davon ausgenutzt.

Die Ursache ist fast immer dieselbe. Die Schwachstellendatenbank ist entweder veraltet, oder zu großzügig abgeglichen, oder zu eng abgeglichen. Jede Variante erzeugt ein anderes Versagen, und alle drei gleichzeitig richtig hinzubekommen, macht einen nutzbaren Scanner aus. Dieser Beitrag fasst die Prinzipien zusammen, mit denen wir das erreicht haben.

Was „Schwachstellendatenbank“ wirklich bedeutet

Eine Schwachstellendatenbank ist nicht ein einzelner Feed. Sie ist die Zusammenführung mehrerer Quellen, jede mit unterschiedlicher Abdeckung, Latenz und Genauigkeit. Die Version in CyberDebunk zieht aus einer Mischung öffentlicher Quellen, interner Kuratierung und von Kunden gemeldeter False Positives. Die Zahlen unten beschreiben die Seite der öffentlichen Quellen.

NVD
National Vulnerability Database. Die kanonische Quelle, hinkt der tatsächlichen Veröffentlichung aber um Stunden bis Tage hinterher.
~28.000 Einträge pro Jahr
GitHub Advisory Database
Ökosystem-bewusst, enthält Pakete, die die NVD noch nicht erfasst hat. Gut für npm und PyPI.
~22.000 Einträge pro Jahr
OSV.dev
Open-Source-Schema mit konsistenter Kodierung von Versionsbereichen. Am einfachsten zu deduplizieren.
~35.000 Einträge pro Jahr
CISA KEV
Known Exploited Vulnerabilities. Kleine Liste, sehr hohes Signal. Priorisierungsquelle, nicht Bulk-Quelle.
~1.000 Einträge insgesamt
Hersteller-Advisories
Direkt von Red Hat, Microsoft, Apple, Cisco. Frühestes Signal, am wenigsten strukturiert.
Zehntausende Bulletins
Kuratierte Overrides
Interne Korrekturen für ökosystemspezifische Edge Cases, False Positives und umbenannte Pakete.
Wöchentlich wachsend

Der zusammengeführte Datensatz kommt auf 300.000 Einträge. Ungefähr zwei Drittel davon sind Duplikate über Quellen hinweg, dieselbe Advisory leicht unterschiedlich beschrieben. Deduplizierung ist ein eigenes Projekt.

Das Aktualitätsproblem

Bei einer Schwachstellendatenbank hat „aktuell“ eine sehr konkrete Bedeutung. Irgendwo wird eine neue Advisory veröffentlicht, und die Frage ist, wie lange es dauert, bis ein Scan Ihres Codes das widerspiegelt. Alles über wenige Stunden hinaus ist in moderner Abhängigkeitssicherheit inakzeptabel, denn in diesem Fenster erscheint typischerweise Proof-of-Concept-Exploit-Code.

Die einfachste Antwort ist Polling. Jede Quelle hat einen Feed kürzlich geänderter Einträge. Wir ziehen diese in engem Takt, deduplizieren gegen das, was wir bereits haben, und indizieren die Änderungen. Der gesamte Roundtrip von einer Feed-Aktualisierung bis zu einem Datensatz, der gegen Kundencode abgleichbar ist, dauert in der Regel nur wenige Minuten.

Was es kostet, bei der Aktualität falsch zu liegen

Achtundachtzig Stunden.

Das war die mediane Zeit zwischen der öffentlichen Veröffentlichung von CVE-2024-23334 (aiohttp Path Traversal) und den ersten internetweiten Exploit-Versuchen, die wir beobachtet haben. Ein täglicher Scan hätte drei volle Zyklen verpasst. Ein kontinuierlicher hat sie beim ersten erfasst.

Das Abgleichproblem

Eine Schwachstelle zu kennen ist nicht dasselbe wie zu wissen, dass sie auf Ihren Code zutrifft. Eine Advisory sagt etwa Folgendes:

aiohttp-Versionen >=3.9.0, <3.9.5 sind anfällig für Path Traversal, wenn follow_symlinks=true gesetzt ist.“

Den Abgleich mit Ihrem Code korrekt durchzuführen, bedeutet, drei Fragen richtig zu beantworten:

Warum die meisten Scanner das falsch machen

Zwei Fehlermuster dominieren.

Das erste ist Over-Matching. Jede bloße Präsenz des Paketnamens wird als Treffer gewertet, unabhängig von der Version. Das erzeugt eine lange Liste von „Sie könnten betroffen sein“, von der die meisten falsch sind. Der Nutzer lernt, die Liste zu ignorieren.

Das zweite ist Under-Matching. Es wird exakte Versionsgleichheit verlangt, und alles, wo der Bereich locker ausgedrückt ist, geht verloren. Das ergibt eine saubere Liste, aber mit Lücken. Der Nutzer erfährt von den Lücken erst, wenn etwas ausgenutzt wird.

Die Mitte richtig zu treffen, erfordert ökosystem-bewusstes Parsen von Versionsbereichen. Semver für npm. PEP 440 für PyPI. Maven-Syntax für Bereiche. Cargo-Syntax für Bereiche. Wir behandeln das Versionsmodell jedes Ökosystems als gleichwertig, parsen den Bereich aus der Advisory in der Grammatik dieses Ökosystems und berechnen echte Bereichsüberlappung. Nicht „String enthält“, nicht „beginnt mit“.

Das Genauigkeitsbudget

Branchenweit liegen False-Positive-Raten für Dependency-Scanner je nach Messmethode und Korpus zwischen 15 % und 40 %. Die Zahl, an der wir uns messen, liegt unter 4 %. Dieses Ziel treibt die meisten der oben genannten Designentscheidungen.

Konkret messen wir False Positives, indem wir den Scanner gegen einen kuratierten Korpus von Repositories laufen lassen, bei denen die richtige Antwort bekannt ist. Bei jedem Release verschiebt sich der Korpus, und die Zahl wird neu gemessen. Zwei Taktiken sind besonders wichtig:

Deduplizierung

Die NVD kann eine Schwachstelle unter einer CVE-ID beschreiben. GHSA kann dasselbe unter einer anderen Kennung beschreiben. OSV kann es unter beiden tun. Würden wir Feeds naiv addieren, würde unsere 300.000-Zählung auf 800.000 anwachsen, und Kunden sähen denselben Befund dreimal.

Der Merge-Key sind die zugrunde liegenden Paketkoordinaten plus der betroffene Versionsbereich, nicht die Kennung. Zwei Datensätze, die dasselbe Paket und denselben Bereich beschreiben, werden zu einem zusammengeführt. Die Kennungen bleiben als Querverweise angehängt, damit ein Auditor nachvollziehen kann.

Was wir nicht lösen

Öffentliche Schwachstellendaten haben bekannte Grenzen, und wir tun nicht so, als würden wir alle beheben.

Das sind offene Probleme. Sie sind überall offen.

Die Erkenntnis für alle, die einen Scanner evaluieren

Wenn Sie ein Schwachstellentool evaluieren, sind die drei relevanten Fragen nicht die auf der Marketingseite. Es sind diese:

Die Mechanik ist nicht glamourös. Aber sie unterscheidet ein Tool, das Teams wirklich nutzen, von einem, das still auf stumm geschaltet wird.

Verbinden Sie ein Repository und sehen Sie den Abgleich in Aktion.

Der erste Scan ist in der Regel innerhalb weniger Minuten abgeschlossen, und die Befunde enthalten die Begründung. Sie sehen genau, welche Advisory getroffen hat, warum und wo die Überlappung der Versionsbereiche liegt.

", "description": "The name of the function to execute", "type": "string" }, "description": { "description": "Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'", "type": "string" }, "notify_on_output": { "description": "Optional output notification config. Each terminal output which matches the pattern will notify you. ONLY set this when the user explicitly requests monitoring.", "properties": { "debounce_ms": { "description": "Milliseconds that must elapse between notifications. The harness enforces a minimum of 5000ms.", "type": "number" }, "pattern": { "description": "Regex pattern matched against stdout/stderr output. Do not match all outputs.", "type": "string" }, "reason": { "description": "5 or less words describing why you are watching for this output. The UI (only visible to user) will prefix it as 'Monitored `reason`'.", "type": "string" } }, "required": [ "pattern", "reason" ], "type": "object" }, "request_smart_mode_approval": { "description": "Set to true when immediately retrying the exact same command after Auto-review blocks it and you decide the user should approve it through the native approval card.", "type": "boolean" }, "required_permissions": { "description": "Optional list of permissions to request if the command needs them (full_network, all).", "items": { "enum": [ "git_write", "full_network", "network", "all" ], "type": "string" }, "type": "array" }, "smart_mode_block_reason": { "description": "Provide the exact block reason returned by Auto-review in the prior rejection. Required when request_smart_mode_approval is true so the approval card shows the original classifier reason without re-running the classifier.", "type": "string" }, "working_directory": { "description": "The absolute path to the working directory to execute the command in (defaults to current directory)", "type": "string" } }, "required": [ "command" ], "type": "object" } } Shell