Was wir bauen
Der Reviewer ist ein kleines Python-Projekt mit einigen klaren Teilen:| Teil | Was es tut |
|---|---|
| Pydantic-Modelle | Definieren Evidence, Finding und Chain und schaffen eine harte Validierungsgrenze zwischen dem LLM und dem Rest des Programms |
| Venice-Client | Umschließt das OpenAI-Python-SDK, gerichtet auf Venices OpenAI-kompatiblen Endpoint |
| AST-Repo-Map | Läuft mit Pythons ast-Modul durch den Zielbaum und baut eine deterministische Karte der öffentlichen Symbole und Import-Kanten jedes Moduls |
| Scanner-Agent | Liest eine Python-Datei plus einen pro Datei zugeschnittenen Nachbarschafts-Slice der Repo-Map und liefert atomare Findings mit Datei:Zeile-Evidenz |
| Chainer-Agent | Liest die Vereinigung der Findings plus eine kondensierte vollständige Repo-Map und liefert Exploit-Chains, die zwei oder mehr Findings kombinieren |
| Referenz-Validator | Verwirft jede Chain, die eine Finding-ID referenziert, die der Scanner nicht erzeugt hat, oder eine Datei nennt, aus der keines ihrer referenzierten Findings tatsächlich stammt |
| Markdown-Report | Rendert Findings und Chains in einen menschenlesbaren Bericht |
| CLI | Verdrahtet alles mit Typer |
- Den Zielordner nach
.py-Dateien durchwandern. - Eine deterministische Repo-Map bauen (Imports, öffentliche Symbole, Signaturen).
- Für jede Datei dem Scanner ihren Quelltext plus einen pro Datei zugeschnittenen Nachbarschafts-Slice der Map senden und atomare Findings sammeln.
- Die Vereinigung der Findings plus die kondensierte Repo-Map dem Chainer senden und Exploit-Chains sammeln.
- Jede Chain verwerfen, die eine Finding-ID referenziert, die der Scanner nicht erzeugt hat, oder eine Datei nennt, aus der keines ihrer Findings stammt.
- Einen Markdown-Report schreiben.
ast durch den Zielbaum und bauen eine strukturelle Map. Der Scanner sieht eine pro Datei zugeschnittene Nachbarschaft (wer von dieser Datei importiert, was diese Datei importiert, Signaturen dieser externen Symbole). Der Chainer sieht eine kondensierte vollständige Map (jedes Modul, jedes öffentliche Symbol, jede Import-Kante, kein Quelltext). Das ist das kleinste Context-Engineering, das wir gefunden haben, das es dem Chainer erlaubt, Chains zu bauen, deren Datenfluss Modulgrenzen überschreitet, ohne die Token-Kosten zu zahlen, die ganze Codebase in jeden Prompt zu stopfen.
Voraussetzungen
- Python 3.12+
- Ein Venice-API-Schlüssel von venice.ai
- Grundverständnis von Pydantic, Pythons
ast-Modul und dem OpenAI-Python-SDK
uv für Dependency-Management; eine reguläre Virtual Environment funktioniert genauso.
Projekt einrichten
Neues Projekt anlegen und Abhängigkeiten installieren:pip nutzt, legt stattdessen eine Virtual Environment an:
.env-Datei für die lokale Entwicklung an:
src/venice_security_reviewer/ ab, damit er als Paket importierbar ist, und die Prompts unter prompts/ im Repo-Root, damit sie wie jede andere Quelldatei reviewbar und diff-bar sind:
Venice-Client aufsetzen
Venice ist OpenAI-kompatibel, also können wir das offizielle OpenAI-Python-SDK nutzen und nur diebase_url auf Venice zeigen lassen. Die Client-Erstellung in einer Datei zu zentralisieren bedeutet, dass der Rest des Codes nie wissen muss, mit welchem Provider er spricht: Ein Backend-Wechsel betrifft nur dieses Modul.
Erstelle src/venice_security_reviewer/client.py:
- Wir nehmen standardmäßig
zai-org-glm-5, weil es ein starkes generisches Venice-Modell ist; per UmgebungsvariableVENICE_MODELkannst du das überschreiben. Für größere oder feinere Codebasen kann ein stärkeres Modell den Chainer in der erzählerischen Qualität spürbar besser machen. build_clientgibt Client und Modell-ID zurück, sodass Caller keine Env-Vars selbst lesen müssen und Tests eine Fake-Config ohne Monkey-Patching injizieren können.
Datenmodelle definieren
Der ganze Sinn, hier Pydantic statt rohe Dicts zu nutzen, ist eine harte Validierungsgrenze zwischen LLM und dem Rest des Programms. Liefert das Modell fehlerhaftes JSON oder erfindet eine Finding-ID, schlägt das Parsen laut fehl und wir propagieren die Halluzination nie in den Report. Erstellesrc/venice_security_reviewer/models.py:
Finding.idundChain.idsind auf eine Regex wieF-001,C-001festgelegt. Wird das Modell kreativ, scheitert die Validierung.Chain.findingsverlangt mindestens zwei Einträge: Eine „Chain” mit einem Finding ist nur ein Finding.Chain.severityist aufhighodercriticalbeschränkt. Eine Kombination, die den Impact nicht über die höchste Einzel-Severity hebt, ist keine berichtenswerte Chain.Evidenceerzwingtend_line >= start_line, sodass das Modell keine unsinnigen Zeilenbereiche liefern kann.
models.py:
Die AST-Repo-Map bauen
Die Repo-Map ist das strukturelle Skelett einer Python-Codebase: jede öffentliche Oberfläche eines Moduls, jede Import-Kante und ein Reverse-Index von „Modul M” zu „Modulen, die von M importieren”. Sie wird einmal pro Scan-Lauf mit Pythonsast gebaut, nie per Ausführung – sicher auch bei adversärem Code, weil der Parser nichts aus dem gescannten Baum importiert oder ausführt.
Wir konsumieren die Map in zwei Formen. Der Scanner bekommt einen pro Datei zugeschnittenen Nachbarschafts-Slice, damit die Prompts in der Größe begrenzt bleiben. Der Chainer bekommt eine kondensierte vollständige Map, um Chains über Dateien hinweg zu konstruieren.
Erstelle src/venice_security_reviewer/repo_map.py und beginne mit den Pydantic-Modellen für die Map:
__all__-Liste. Funktions-Signaturen und Class-Header werden zu kompakten Strings gerendert, die das LLM direkt lesen kann:
_SIGNATURE_CHAR_CAP von 200 erhält typische echte Signaturen (inkl. Type-Hints) und verhindert pathologische Fälle wie eine 200-zeilige typisierte Union, die den Prompt sprengen würde.
Als Nächstes der Extraktor, der die strukturellen Daten aus einem geparsten Modul herauszieht. Wir behandeln ast.FunctionDef, ast.ClassDef, top-level ast.Assign und ast.AnnAssign für Konstanten und sowohl ast.Import als auch ast.ImportFrom für die Import-Kanten. Relative Imports werden in ihre absolute, punktierte Form aufgelöst, damit der Chainer sie später gegen Modulnamen matchen kann:
tree.body und gibt pro top-level Knoten SymbolDef- und ImportEdge-Einträge aus. Die _extract-Funktion in repo_map.py des Referenz-Repos zeigt die vollständige Implementierung. Heraus kommt eine Liste von ModuleEntry-Objekten, eines pro Datei.
Der interessante Teil ist, was wir mit diesen Einträgen tun. Wickeln wir sie in eine RepoMap mit zwei Consumer-Methoden:
neighborhood(path) ruft der Scanner pro Datei auf. Es liefert ein ModuleNeighborhood-Objekt mit dem Modul selbst, jedem anderen Modul, das von ihm importiert, und jedem in-repo-Symbol, das es anderswo importiert (mit aufgelösten Signaturen). Damit hat der Scanner genug Kontext, um Findings zu flaggen, die nur im Cross-File-Kontext offensichtlich sind, ohne die ganze Codebase in den Prompt zu ziehen.
condensed_dict() bekommt der Chainer. Snippets und Signaturen entfallen; nur Pfade, Modulnamen, öffentliche Exports und Import-Kanten bleiben. Das ist die kleinste Darstellung, mit der der Chainer trotzdem über modulübergreifenden Datenfluss räsonieren kann.
Schließlich der Entry Point, der das Ganze baut:
Den Scanner-Agenten schreiben
Der Scanner läuft einen Zielpfad ab, sammelt Python-Quelldateien und bittet Venice, atomare Schwachstellen Datei für Datei zu identifizieren. Per-Datei-Scanning hält den Prompt klein und macht Fehler lokal: Eine kaputte Datei kippt nicht den ganzen Lauf. Wir halten den Prompt in einer separaten Datei, damit er wie jede andere Quelldatei reviewbar und diff-bar ist. Erstelleprompts/scanner.md:
- Wir weisen das Modell an, ausschließlich JSON ohne Prosa oder Fences zu liefern. Das OpenAI-SDK unterstützt
response_format={"type": "json_object"}, das das API-seitig erzwingt – die Verstärkung im Prompt reduziert aber Edge-Cases. - Wir verbieten dem Scanner ausdrücklich, Cross-File-Chains zu produzieren. Chains sind die Aufgabe des Chainers; beides verlangen würde die Verantwortung verwischen.
- Das Snippet muss verbatim kopiert sein. So kann der Report die genauen Bytes zitieren, die das Modell gesehen haben will, und ein Reviewer kann ein Finding stichprobenartig prüfen.
src/venice_security_reviewer/scanner.py und beginne mit File-Walker und Prompt-Loader:
MAX_FILE_BYTES ist eine Sicherheitsobergrenze. Jenseits ~200 KB überspringen wir die Datei, statt einen riesigen, wahrscheinlich teuren und qualitativ schwachen Prompt zu senden.
Als Nächstes der Prompt-Builder. Das Template nutzt {filename}, {source} und {neighborhood} als Platzhalter; wir verwenden str.replace statt .format(), weil das Template JSON-Beispiele mit literalen geschweiften Klammern enthält:
F-001 pro Datei, aber der Chainer muss Findings über das ganze Repo hinweg referenzieren. Wir vergeben die IDs gegen einen monotonen Zähler neu, damit sie global eindeutig sind:
response_format={"type": "json_object"} und niedriger Temperatur an Venice und parsen das Ergebnis:
- Wir setzen den Evidence-Dateipfad nach dem Parsen relativ zu
repo_root, weil das Modell den übergebenen Dateinamen zurückgibt, wir aber im ganzen Report eine kanonische Form wollen. temperature=0.1ist bewusst niedrig. Der Scanner soll konservativ und über Läufe konsistent sein; Kreativität ist die Aufgabe des Chainers.
Den Chainer-Agenten schreiben
Der Chainer nimmt die Vereinigung der Scanner-Findings plus die kondensierte Repo-Map und fragt Venice, ob eine Teilmenge der Findings zu einer realen Exploit-Chain kombiniert werden kann. Zwei deterministische Leitplanken stehen zwischen LLM und Report:- Jede Chain darf nur Finding-IDs referenzieren, die der Scanner erzeugt hat.
- Jede Chain darf nur Dateien benennen, die mindestens ein referenziertes Finding tatsächlich berührt.
prompts/chainer.md. Der Kern davon:
files_involved gehört, und vor allem, wann nicht zu chainen. Dem Modell zu sagen „it is correct and expected for many codebases to have findings that do not chain” hält es davon ab, Chains zu erfinden, um produktiv zu wirken.
Jetzt der Agent-Code. Erstelle src/venice_security_reviewer/chainer.py:
MAX_REPO_MAP_CHARS = 8000 ist eine weiche Obergrenze für den als JSON gerenderten Repo-Map-Block im Chainer-Prompt. Bei rund 4 Zeichen pro Token entspricht das ~2000 Tokens und passt komfortabel in jedes Venice-Modellkontext, selbst mit Findings und Erzähl-Budget obendrauf.
Wir serialisieren Findings in einen kompakten JSON-Block. Wir streichen das snippet aus Evidence bewusst: Der Chainer braucht keine rohen Bytes, um zu entscheiden, ob zwei Findings kombinieren, und ihre Aufnahme verdoppelt grob den Token-Kostenpunkt auf realen Codebasen:
_pruned-, _kept- und _total-Markern, sodass der Chainer-Prompt das Modell warnen kann, wenn die Map beschnitten wurde.
Das Response-Parsen ist wie beim Scanner: deserialisieren, jede Chain über Pydantic validieren, fehlerhafte verwerfen:
- Wir brechen vor dem Modell-Aufruf ab, wenn es weniger als zwei Findings gibt. Eine Chain aus einem einzelnen Finding gibt es nicht, und so verbrennen wir keine Tokens auf garantiert leerem Ergebnis.
temperature=0.2ist leicht höher als die0.1des Scanners. Der Chainer profitiert von etwas mehr Kreativität, um nicht-offensichtliche Kombinationen zu finden, soll aber in den gegebenen Findings und der Map verankert bleiben.- Nach dem Parsen läuft
validate_chain_referencesdurch – die deterministische Cross-Reference-Prüfung von oben. Was übrig bleibt, ist sicher zum Rendern; was nicht, wird geloggt, damit der Operator weiß, dass das Modell etwas erfinden wollte.
Den Markdown-Report rendern
Das Rendern von der Agent-Logik zu trennen heißt: DieselbenFinding- und Chain-Objekte können später in andere Formate (JSON, SARIF, HTML) fließen, ohne Scanner oder Chainer anzufassen.
Wir nutzen Jinja2 mit einer kleinen Template-Datei. Erstelle src/venice_security_reviewer/templates/report.md.j2:
src/venice_security_reviewer/report.py:
.html-Templates aktiviert.
Das CLI verdrahten
Das CLI ist der Orchestrator: Repo-Map bauen, scannen, chainen, rendern. Wir nutzen Typer für Argumente und Rich für eine schöne Summary-Tabelle. Erstellesrc/venice_security_reviewer/cli.py:
pyproject.toml ergänzen:
Die Guardrails testen
Wir haben uns durchgehend auf eine Idee gestützt: Die deterministischen Leitplanken trennen ein nützliches Security-Tool von einem selbstbewusst-falschen. Diese Behauptung ist nur wertvoll, wenn wir nachweisen können, dass die Leitplanken halten – deshalb rufen die wertvollsten Tests dieses Projekts gar nicht Venice auf. Sie zurren die Pydantic-Grenze und das Prompt-Assembly-Plumbing fest, laufen offline in Millisekunden, ohne API-Key und ohne Token-Kosten. Zuerst Dev-Abhängigkeiten:tests/test_models.py:
F-###-Muster passt, und eine „Chain” aus einem einzelnen Finding. Falls einer jemals aufhört zu raisen, ist eine ganze Halluzinationsklasse stillschweigend wieder möglich.
Der wichtigste Test deckt den Cross-Reference-Validator ab, weil das die Funktion ist, die erfundene Chains wirklich verwirft:
F-999 wurde vom Scanner nie erzeugt, daher landet die Chain, die das referenziert, in dropped und erreicht den Report nie. Der zugehörige Test im Referenz-Repo, test_validate_chain_references_drops_unknown_files, macht dasselbe für eine Chain, die eine Datei behauptet, aus der keines ihrer Findings stammt.
Als Zweites lohnt es, das Plumbing zu testen, das den Chainer füttert. Es ist leicht, das Prompt-Assembly zu refaktorieren und still aufzuhören, Cross-File-Kontext mitzugeben – woraufhin der Chainer weiterläuft, aber leise schlechter wird. Dieser Test baut ein Zwei-Modul-Fixture, rendert den Prompt und behauptet, dass die Cross-File-Information tatsächlich vorhanden ist – ohne Venice-Round-Trip. Erstelle tests/test_cross_file_chain.py:
tests/test_scanner_parse.py, tests/test_chainer_parse.py und tests/test_repo_map.py, die JSON-Parsing-Edge-Cases (fehlerhafte Einträge verwerfen statt zu crashen) und den AST-Repo-Map-Builder abdecken.
Das Projekt ausführen
Auf einer echten Codebase: das CLI auf ein Verzeichnis mit Python-Quellcode richten:pip install -e . ins Virtualenv installieren und venice-security-reviewer scan path/to/your/code ausführen.
Der Output sieht ungefähr so aus:
examples/vulnerable_app— eine Multi-File-Flask-App mit drei „low”-Findings, von denen zwei zu einer kritischen Privilege-Escalation-Chain über Dateien hinweg kombinieren. Testet, ob der Chainer selektiv kombiniert.examples/url_preview— ein Multi-File-URL-Fetcher mit einer defensiven Allowlist, die nicht pro Iteration angewendet wird. Testet Cross-File-Datenfluss kombiniert mit Deployment-Topologie (Link-Local-IPs sind Cloud-Credential-Gateways).examples/csv_query— ein Single-File-CSV-Filter miteval-Sandbox-Escape via__class__.__base__.__subclasses__(). Testet Sprach-Level-Argumentation statt HTTP-Flow.examples/webhook_handler— ein Single-File-HMAC-Verifier mit einer JSON-Parser-Differential-Schwachstelle. Testet Argumentation über mehrere Spezifikationen hinweg.
chainer referenced N unknown finding id(s) or file(s); chains dropped loggt, hat der Cross-Reference-Validator das Modell beim Erfinden einer Chain erwischt. Die verworfenen Chains schaffen es nie in den Report; du bekommst nur eine Warnung, mit der du den Prompt anpassen oder weitere Chainer-Läufe sampeln kannst.
Dieses Beispiel erweitern
Die Zwei-Agenten-Form lässt sich gut verallgemeinern. Ein paar lohnenswerte Richtungen:- Mehr Sprachen. Der Scanner ist auf Prompt-Ebene sprachagnostisch; nur der AST-Builder ist Python-spezifisch. Tausche ihn gegen
tree-sitteraus und du bekommst dieselbe Nachbarschafts-/Kondensiert-Map-Form für TypeScript, Go oder Rust. - Ein dritter Agent für Fixes. Ist eine Chain einmal da, kann ein Patcher-Agent einen Unified Diff vorschlagen, der eines der konstituierenden Findings entschärft. Den Diff per Pydantic gegen denselben Evidence-File-Satz validieren und du bekommst denselben Halluzinationsschutz gratis.
- Output-Formate. Nur
render_reportkennt Markdown. Ergänze einen SARIF-Renderer und dieselben Findings fließen in GitHub Code Scanning. Ein JSON-Renderer pipest Ergebnisse in ein nachgelagertes System. - Caching per Datei-Hash. Die Per-Datei-Aufrufe des Scanners sind unabhängig und idempotent. Caching nach
(file_hash, prompt_hash, model)bedeutet, dass ein Rescan eines Repos, in dem nur eine Datei geändert wurde, nur den Scanner für diese eine Datei erneut ausführt. - Sampling für den Chainer. Bei kritischen Läufen den Chainer N-mal bei leicht höherer Temperatur aufrufen und die Ergebnisse schneiden. Chains, die das Modell konsistent findet, sind eher real; Chains, die einmal auftauchen und nie wieder, sind eher Rauschen.
- Stärkere Modelle.
zai-org-glm-5ist der Default, weil es ein gutes Preis-/Qualitätsverhältnis bei kombinatorischer Argumentation bietet. Für härtere Codebasen kann ein stärkeres Venice-Modell (überVENICE_MODEL) die Narrative des Chainers merklich verdichten.