Wie wir mit Rust, Kafka, In-Memory-Lookups, PostgreSQL, RustFS und ClickHouse sich ständig ändernde Security Advisories im großen Maßstab abgleichen.
Christoph Ebeling, Gründer von Farion
Auf dem Papier wirken Schwachstellendaten wie ein statischer Katalog. Ein Fehler wird entdeckt, eine CVE-ID vergeben und ein Schweregrad zugeordnet. Wer ein Sicherheitssystem auf dieser Annahme aufbaut, kommt mit einer einfachen Architektur aus: einen Snapshot öffentlicher Feeds in einen Scanner-Container packen, ihn während des Builds mit den Abhängigkeiten einer Anwendung abgleichen und einen Report ausgeben.
In Produktionsumgebungen verhält sich Vulnerability Intelligence jedoch weniger wie ein statischer Katalog als vielmehr wie ein eventually consistent Strom widersprüchlicher Zustandsänderungen.
Häufig veröffentlicht zuerst ein Ökosystem-Feed wie RustSec oder der Security Tracker eines Herstellers ein Advisory. Der zugehörige CVE-Eintrag folgt später, manchmal erst am nächsten Tag. Threat Scores wie EPSS kommen mit einem weiteren Update, während Linux-Distributionen unabhängig davon ältere Release-Zweige patchen und ihre betroffenen Versionsbereiche überarbeiten. Während dieser ganzen Abfolge hat sich die ausgerollte Anwendung nicht verändert – und doch haben sich die Eingaben, die zur Bewertung ihrer Abhängigkeiten nötig sind, mehrfach verschoben.
Wenn eine Bewertungspipeline Abhängigkeiten nur dann prüft, wenn ein Scan ausgelöst wird, ist die Genauigkeit ihrer Findings an den Zeitpunkt des letzten Scans gekoppelt.
Löst man die Schwachstellenbewertung vom Scan-Zeitpunkt, verändert sich das systemtechnische Problem. Statt während CI/CD einen Batch-Lookup-Job auszuführen, müssen wir eine kontinuierliche Matching-Pipeline gegen bekannte Abhängigkeitszustände betreiben und gleichzeitig hochparallele Lookups für neue Scans bedienen. Aufbau und Betrieb dieses Systems haben uns gezwungen, vier zusammenhängende Probleme zu lösen: Daten aus Primärquellen zu erfassen und zu versionieren, ohne ihre Herkunft zu verlieren; paketspezifischen Kontext für große Software Bills of Materials (SBOMs) aus dem Speicher auszuliefern; unseren PostgreSQL-Cluster vor Replication Lag zu schützen, als unser historisches Archiv auf 500 GB zuging; und die so angesammelte Revisionshistorie zu nutzen, um zu untersuchen, wie Advisories mit der Zeit reifen.
Direkte Erfassung und das Provenienzproblem
Wer sich auf sekundäre Aggregatoren oder einen einzelnen Upstream-Feed verlässt, handelt sich für die automatisierte Analyse zwei Fehlerquellen ein. Erstens hat man keine Kontrolle darüber, wann Einträge aktualisiert werden oder ob historische Revisionen überschrieben werden. Zweitens enthält eine generische CVE-Zusammenfassung selten den Kontext auf Paketebene, der nötig ist, um festzustellen, ob ein bestimmter Build einer Bibliothek tatsächlich betroffen ist.
Betrachten wir, wie Upstream-Maintainer und Linux-Distributionen Fixes für dieselbe Schwachstelle dokumentieren. Im Upstream-Advisory von curl zu CVE-2024-7264 (ein Out-of-Bounds-Read im ASN.1-Datumsparser von libcurl) nennt das Projekt einen betroffenen Gesamtbereich von 7.32.0 bis 8.9.0, wobei die Erreichbarkeit vom TLS-Backend abhängt, und gibt 8.9.1 als korrigiertes Release an. Eine Analyse-Engine, die nur Upstream-Versionsbereiche verwendet, könnte daher ein gepatchtes Paket in Debian 12 (Bookworm) als verwundbar einstufen, ohne den Backport der Distribution oder das TLS-Backend des Builds zu berücksichtigen. Der Debian Security Tracker zu CVE-2024-7264 verzeichnet jedoch, dass das Bookworm-Paket per Backport in Version 7.88.1-10+deb12u7 gepatcht wurde. Beide Einträge sind innerhalb ihrer jeweiligen Grenzen korrekt – führt man sie zu einem einzigen, flachen Versionsbereich zusammen, wird die Bewertung für eine der beiden Umgebungen falsch.
Um diese Unterschiede direkt aus den Primärquellen zu erfassen, betreiben wir Dutzende unabhängiger Worker und Scraper. Sie decken Security Advisories von Herstellern ab, Ökosysteme von Programmiersprachen wie RustSec, Tracker von Linux-Distributionen, öffentliche Paket-Registries sowie GitHub- und GitLab-Repositories (wo wir auch die Originaltexte von Softwarelizenzen erfassen), dazu Threat-Scoring-Feeds wie EPSS. Die wichtigsten Advisory-Feeds werden kontinuierlich gescrapt, sekundäre Quellen laufen in gestaffelten Intervallen.
Dass wir diese Erfassungsschicht selbst betreiben, ist auch eine bewusste Entscheidung für Souveränität. Da unsere nachgelagerten Analysedienste von dieser Datenbank abhängen, brauchen wir die volle Kontrolle darüber, welche Einträge wir aufbewahren, wie ihre Versionshistorien gespeichert werden und wie interne Consumer sie abrufen – statt uns auf Daten zu verlassen, die in ein externes Scanner-Image gebündelt sind.
Ingestion, explizite Konsolidierungsregeln und Versionierung
Die Scraper veröffentlichen Rohdatensätze in Kafka, das als Ingestion-Backbone für unsere Konsolidierungs-, Schwachstellen- und Paketierungsdienste dient. Unser operatives Ziel im Schwachstellendienst ist es, eingehende Updates innerhalb eines Latenzfensters von maximal zehn Minuten zu verarbeiten.
Wenn über Kafka sich überschneidende Einträge eintreffen, muss die Konsolidierungspipeline sie abgleichen, ohne die zugrunde liegenden Quellbelege zu zerstören. Statt ein undurchsichtiges Modell raten zu lassen, welcher Feed recht hat, folgt unsere Konsolidierungslogik expliziten, deterministischen Regeln, die sich auf laufende operative Reviews der Quellenqualität stützen. Wir verfolgen, wie sich Advisories verschiedener Anbieter über die Zeit entwickeln und welche Quellen für bestimmte Technologien durchgängig vollständige, korrekte Metadaten liefern. Wenn bestehende Feeds für ein Ökosystem nicht detailliert genug sind, ergänzen wir direkte Scraper für die Primärquellen in diesem Bereich.
Jeder Rohdatensatz wird aufbewahrt und versioniert. Aus diesen Rohdaten erzeugt die Pipeline zwei getrennte konsolidierte Sichten:
| Konsolidierte Sicht | Technischer Zweck |
|---|---|
| Allgemeine Sicht | Liefert eine klare, für Menschen lesbare Erklärung der Schwachstelle über alle betroffenen Produkte hinweg. |
| Technologiespezifische Sicht | Bewahrt die exakten Parameter zu Paket, Ökosystem und Distribution, die nachgelagerte Analyse-Agents benötigen. |
Weil wir die Rohdatenhistorie neben den konsolidierten Ergebnissen behalten, sind Merge-Entscheidungen umkehrbar. Überarbeitet ein Hersteller ein Advisory Tage später oder passen wir aufgrund unserer Reviews der Quellenqualität unsere Konsolidierungsregeln an, können wir den Eintrag aus den ursprünglichen Eingaben neu bewerten, statt zu versuchen, den Zustand nach einem destruktiven Überschreiben zu rekonstruieren. Außerdem können Engineers, die einen Vorfall untersuchen, genau den Stand der Intelligence einsehen, der zu einem früheren Zeitpunkt erfasst war – bevor spätere CVE-Updates oder Änderungen am Schweregrad eintrafen.
Kontinuierliches Matching vs. Neubewertung der Ausnutzbarkeit
Wenn ein neu konsolidiertes Update unseren Kenntnisstand zu einer Schwachstelle verändert, gleicht die Pipeline dieses Update mit den Abhängigkeitsdatensätzen ab, die dem System bereits bekannt sind. Kunden müssen keinen neuen Scan auslösen, um zu sehen, dass sich ein Advisory geändert hat, das ihre Software betrifft.
Beim Entwurf dieses Workflows haben wir drei Vorgänge getrennt, die in Tools für Schwachstellenmanagement oft vermischt werden: die Aktualisierung des zugrunde liegenden Finding-Zustands, die Benachrichtigung der Nutzer und die Neubewertung der Ausnutzbarkeit im Code.
-
Zustandsupdates erfolgen kontinuierlich: Sobald neue Intelligence abgeglichen ist, werden die betroffenen Findings und ihre historischen Zeitleisten aktualisiert.
-
Benachrichtigungen sind von der Ingestion entkoppelt: Wer Engineering-Teams bei jeder kleinen Anpassung eines Versionsbereichs in einem Upstream-Tracker alarmiert, erzeugt Alert Fatigue. Standardmäßig werden Änderungen zu einem täglichen Update am Morgen gebündelt; Teams, die sofort benachrichtigt werden möchten, können die Zustellung in Echtzeit aktivieren.
-
Die Neubewertung der Ausnutzbarkeit wird gezielt ausgelöst: Unsere Bewertungen der Ausnutzbarkeit beruhen auf statischer Datenfluss- und Taint-Analyse der Anwendung. Da der Datenflussgraph die Struktur des Codes selbst abbildet, erfordern routinemäßige Metadaten-Updates an einem Advisory in der Regel keinen erneuten Durchlauf der aufwendigen Datenflussanalyse. Ändern neue Details im Advisory jedoch die relevanten Auslösebedingungen, kann optional eine Neubewertung der Ausnutzbarkeit durchgeführt werden, ohne das Repository vollständig neu zu scannen.
Parallele SBOM-Lookups aus dem Speicher mit Rust
Während Kafka die asynchrone Ingestion- und Update-Pipeline übernimmt, brauchen interne Dienste und Exploitability-Agents anfragegesteuerten Zugriff auf aktuelle Intelligence. Ein Enterprise-SBOM enthält routinemäßig mehrere tausend Bibliotheken. Wenn mehrere Scans parallel laufen, die jeweils Tausende von Package URLs (PURLs) auflösen, während Hintergrunddienste zusätzlich Advisory-Kontext abfragen, bestimmt die Lookup-Latenz unmittelbar den Durchsatz der gesamten Analysepipeline.
Damit die Leselatenz niedrig bleibt, halten wir den aktiven Serving-Bestand an Schwachstellen und Advisories im Speicher und stellen ihn internen Consumern über eine GraphQL-API bereit. Eine typische Abfrage übergibt eine PURL an die GraphQL-Schicht, die Daten aus unseren dahinterliegenden Speichern zusammenführt und den technologiespezifischen Advisory-Kontext zurückgibt, den der aufrufende Analysedienst oder Exploitability-Agent benötigt. Für hohe Verfügbarkeit und um zonenübergreifenden Netzwerkverkehr zu begrenzen, betreiben wir unsere Serving-Infrastruktur und Read Replicas über drei AWS Availability Zones hinweg.
Die Wahl der Programmiersprache für diese Dienste ergab sich aus einem früheren Workload. Vor rund achtzehn Monaten haben wir Rust für unsere Taint-Analyse-Engine eingeführt. Sie führt Fixpunktberechnungen und Breitensuchen über Graphen aus – Aufgaben, bei denen ein vorhersagbares Speicherlayout und das Fehlen von Garbage-Collection-Pausen entscheidend waren.
Als wir die Scraper-Flotte und die Serving-Schicht für Schwachstellen aufbauten, haben wir uns aus workloadspezifischen Gründen auf Rust als Standard festgelegt:
-
Flottendichte und Leerlauf-Footprint: Wer Dutzende unabhängiger Scraper-Dienste hochverfügbar betreibt, merkt schnell, wie sich der Overhead pro Dienst summiert. Werden die Scraper zu kleinen nativen Binaries in minimalen Containern kompiliert, bleibt der Speicherverbrauch im Leerlauf niedrig, während die Worker zwischen ihren geplanten Läufen warten – verglichen mit unseren bisherigen Erfahrungen mit dynamischen Sprachen und Go-Runtimes.
-
Vorhersagbares Speicherverhalten in der GraphQL-Schicht: Der Schwachstellendienst muss aktive Lookup-Strukturen im Speicher halten und gleichzeitig komplexe GraphQL-Resolver ausführen, die Datensätze aus PostgreSQL und Object Storage zusammensetzen. Das Ownership-Modell und die expliziten Lebensdauern von Allokationen in Rust erlauben es uns, den Speicherverbrauch auch bei parallelen Scan-Spitzen zu beurteilen, ohne Kapazität für den Overhead eines Laufzeit-Collectors vorhalten zu müssen.
Mit der Kontrolle über Speicher und Rechenleistung in der Anwendungsschicht hatten wir unsere Lookup-Latenz im Griff – doch dabei trat ein deutlich härterer Engpass in unserer Speicherschicht zutage.
Das schwierigste operative Problem: PostgreSQL unter Ingestion-Druck
Als unsere Scraper-Abdeckung wuchs und unser Archiv versionierter Advisories und Softwarelizenzen auf 500 GB zuging, wurde es zu unserer größten technischen Herausforderung, den PostgreSQL-Cluster gesund zu halten.
Anfangs diente PostgreSQL als einziger persistenter Speicher für aktuelle operative Intelligence, vollständige historische Advisory-Revisionen, gescrapte Lizenztexte und Zeitreihenmetriken. Da wir die lesenden Instanzen über drei AWS Availability Zones verteilen, damit lokale Lesezugriffe schnell bleiben, müssen Schreibspitzen auf der Primärinstanz über die Zonen hinweg repliziert und eingespielt werden. Wie in der Streaming-Replication-Architektur von PostgreSQL dokumentiert, setzt ein dauerhaft hohes Schreibvolumen bei gleichzeitig komplexen Lesezugriffen das Versenden und Einspielen des Write-Ahead-Logs (WAL) unter Druck. In unserer Umgebung zeigte sich das als erheblicher Replication Lag auf den Read Replicas.
Faktisch ließen wir zu, dass Massenschreibvorgänge für das Archiv und analytische Abfragen genau die Replicas ausbremsten, die die laufenden Sicherheitsbewertungen bedienten. Wir haben das mit zwei Architekturänderungen gelöst: dringlichkeitsbasiertem Drosseln der Ingestion und einer Trennung der Speicher nach Workload.
1. Prioritätsbasierte Backpressure auf den Writern
Nicht jeder Datensatz, der in Kafka landet, ist operativ gleich dringend. Ein neu veröffentlichtes Security Advisory oder ein aktualisierter Bereich betroffener Versionen muss schnell in der Serving-Schicht ankommen. Ein gescrapter Lizenztext oder ein routinemäßiges Dokumentations-Update muss das nicht.
Wir haben unsere Writer-Pipeline so angepasst, dass sie die Ingestion aus Kafka abhängig von der Datenbanklast regelt. Steigen Schreibdruck oder Replica Lag, drosseln die Writer Ingestion-Streams mit niedrigerer Priorität und halten Lizenztexte und Dokumentations-Updates für ein bis zwei Minuten in der Queue. Indem wir eine kurze Verzögerung bei Archivmaterial in Kauf nehmen, sichern wir Schreibkapazität und Replikationsreserven für dringende Advisory-Updates.
2. Speicher nach Zugriffsmuster aufteilen
Das Drosseln glättete die Ingestion-Spitzen, doch der zugrunde liegende Datenbestand war immer noch zu groß und zu heterogen für das Profil einer einzigen relationalen Datenbank. Wir haben die Persistenzschicht danach aufgeteilt, wie die jeweiligen Datentypen geschrieben und abgefragt werden:
| Speicherkomponente | Aufgabe im Workload |
|---|---|
| In-Memory-Datenbestand | Heiße Lookups von Schwachstellen und Advisories per PURL für parallele Scans und interne Analyse-Agents. |
| PostgreSQL | Operativer Zustand für aktuelle Threat Intelligence, aktive Dashboards und Finding-Zeitleisten. |
| RustFS | Verteilter, S3-kompatibler Object Storage für ausgelagerte historische Lizenztexte sowie umfangreiche oder ältere Advisory-Revisionen. |
| ClickHouse | Indexiert Zeitreihenmetriken und beantwortet deren Abfragen direkt, sodass analytische Scans nicht mehr auf PostgreSQL laufen. |
Indem wir umfangreiche historische Lizenztexte und ältere Advisory-Inhalte nach RustFS ausgelagert haben, ist der operative Footprint in PostgreSQL geschrumpft; das Routing von Zeitreihenabfragen zu ClickHouse hat aufwendige analytische Lesezugriffe von den transaktionalen Replicas verdrängt. Fordert ein Engineer oder ein interner Workflow den Volltext eines historischen Advisorys an, ruft der Schwachstellendienst die detaillierte Payload bei Bedarf ab, ohne jedes historische Dokument in den primären relationalen Tabellen vorzuhalten.
Das Revisionsarchiv für die Modellforschung nutzen
Den operativen Aufwand, fast 500 GB historischer Advisory-Stände aufzubewahren, statt Zeilen einfach mit den neuesten Feed-Daten zu überschreiben, haben wir ebenso wegen unserer Forschungs-Roadmap auf uns genommen wie wegen der Nachvollziehbarkeit.
Mit der Zeit ist dieses Archiv zu einem unserer wertvollsten Assets geworden, um Knowledge Graphs für Agents aufzubauen und interne Modelle zur Sicherheitsbewertung zu trainieren. Es erlaubt uns, nicht nur zu untersuchen, welche Schwachstellen heute existieren, sondern auch, wie sich Informationen über Schwachstellen über verschiedene Ökosysteme und Anbieter hinweg entwickeln.
Ein aktives Forschungsproblem, an dem wir arbeiten, betrifft die Frage, wie frühe Unsicherheit und Schreibstil die automatisierte Risikopriorisierung verzerren. Wenn eine Schwachstelle erstmals offengelegt wird, ist das erste Advisory häufig unvollständig. Je nach Autor und Quelle ist ein unsicherer Day-0-Bericht womöglich in alarmierender Sprache verfasst, um zur Vorsicht anzuhalten. Umgekehrt kann ein zwei Wochen später überarbeitetes Advisory deutlich gefährlicher klingen, einfach weil Forschende die konkrete technische Mechanik und die Exploit-Bedingungen ergänzt haben – oder es grenzt das tatsächliche Risiko ein, indem es die betroffenen Konfigurationen einschränkt.
Verlässt sich ein Bewertungsmodell naiv auf die Formulierung an der Oberfläche, driftet seine Einschätzung mit rhetorischem Stil und Reifegrad der Berichterstattung statt mit der zugrunde liegenden technischen Evidenz.
Da unsere Datenbank die Abfolge der Revisionen so bewahrt, wie wir sie beobachtet haben, können wir Trainingspaare bilden, die ein Advisory am Tag der ersten Offenlegung mit seiner Fassung rund zwei Wochen später vergleichen. Das Delta zwischen Day 0 und Day 14 über verschiedene Advisory-Quellen hinweg zu untersuchen, liefert uns das Trainingsmaterial, um Modellen dabei zu helfen, unvollständige frühe Berichte zu berücksichtigen und unterschiedliche Schreibstile zu normalisieren.
Die architektonischen Trade-offs im Überblick
Jede Schicht dieser Pipeline spiegelt einen Trade-off wider, den der Workload vorgibt:
-
Direkte Erfassung statt Feed-Aggregation: Dutzende Scraper für Primärquellen zu pflegen, erfordert kontinuierliche Datenhygiene und Reviews der Quellenqualität, verschafft uns aber paketspezifischen Kontext und Souveränität über unseren historischen Datenbestand.
-
In-Memory-Lesezugriffe über GraphQL statt direkter Datenbankabfragen: Aktive Advisory-Zuordnungen in Rust-Diensten im Speicher zu halten, bringt zusätzlichen operativen Zustand mit sich, den wir verwalten müssen, verhindert aber, dass SBOM-Lookups mit mehreren tausend Paketen parallele Scans ausbremsen.
-
Selektives Drosseln der Ingestion statt einheitlicher FIFO-Verarbeitung: Lizenz- und Dokumentationsschreibvorgänge unter Datenbanklast um ein bis zwei Minuten zu verzögern, bedeutet einen geringen Rückstand bei Archivdaten – im Gegenzug bleiben die PostgreSQL Read Replicas über alle Availability Zones hinweg reaktionsschnell.
-
Polyglotte Persistenz statt einer einzigen Datenbank: Die Aufteilung der Daten auf PostgreSQL, RustFS und ClickHouse macht die Architektur umfangreicher, passt aber jede Storage-Engine an ihr tatsächliches Zugriffsmuster an.
Vulnerability Intelligence ist am ersten Tag selten vollständig. Weil wir unsere Infrastruktur auf der Annahme aufbauen, dass sich Sicherheitsdaten verändern werden, können wir Bewertungen aktuell halten, sobald neue Evidenz auftaucht – und bewahren zugleich die historischen Daten, die wir brauchen, um bessere Analysewerkzeuge zu entwickeln.