Dieser Artikel führt den Gedankengang fort, den ich in „Test-Driven Security“ und „Turbo-Charging Your PHPUnit Suite“ begonnen habe. Zusammen gelesen, argumentieren die drei Artikel, dass Sicherheit und die Performance der Testsuite keine getrennten Qualitätsmerkmale mehr sind.
Das Plädoyer für schnelle Testsuiten handelte früher von Menschen. Entwicklerinnen und Entwickler verlieren ihren Flow, wenn das Feedback langsam ist. Teams hören auf, Suiten auszuführen, die sie nicht ertragen. In der Summe verschwenden sie Stunden, die sonst in Arbeit fließen würden, auf die es ankommt. Jedes Argument, das ich in „Turbo-Charging Your PHPUnit Suite“ vorgebracht habe, war im Kern ein Argument über die Aufmerksamkeit von Menschen.
Diese Argumentation ist nach wie vor richtig. Sie ist aber nicht mehr die ganze Argumentation.
In letzter Zeit hat sich verändert, wie ernsthafte Sicherheitsarbeit erledigt wird. Sicherheitsforschende, Maintainer und die Sicherheitsteams großer Open-Source-Projekte setzen mittlerweile LLM-basierte Coding-Agenten als eine Art unermüdliche Junior-Reviewer ein. Der Agent liest die Codebasis und bildet Hypothesen darüber, wo sich Schwachstellen verbergen könnten. In den disziplinierteren Arbeitsabläufen versucht er dann, jede Hypothese zu beweisen, indem er einen fehlschlagenden Test schreibt und ausführt. Eine Hypothese mit Reproducer wird zu einem Befund; eine ohne wird verworfen.
Dieser letzte Satz klingt vertraut, und das soll er auch. Es ist genau die Disziplin, für die ich in „Test-Driven Security“ plädiert habe. Die angenehme Überraschung ist, dass ein kompetentes agentisches Security-Review von außen denselben Maßstab erzwingt, den Test-Driven Security von innen heraus zu verankern versucht: keine Behauptung ohne ausführbaren Test.
Die unangenehme Überraschung ist, was das für deine Testsuite bedeutet.
Die neue Form der Feedback-Schleife
Wenn ein Mensch Code auf Schwachstellen prüft, ist die Feedback-Schleife durch Aufmerksamkeit begrenzt. Eine Person schaut sich eine Funktion an, schöpft Verdacht, schreibt vielleicht einen kleinen Reproducer, führt ihn ein- oder zweimal aus und macht weiter. Die reale Laufzeit eines einzelnen Verifikationsschritts spielt eine Rolle, doch die dominierenden Kosten sind menschlicher Natur: Lesen, Verstehen, Entscheiden, was als Nächstes zu betrachten ist. Die Dauer der Testsuite ist eine Steuer, keine Obergrenze.
Wenn ein Agent Code auf Schwachstellen prüft, ist die Feedback-Schleife durch die reale Laufzeit begrenzt. Der Agent wird nicht müde, und er verliert nicht mitten in der dritten Erweiterung den Fokus. Stattdessen führt er die Testsuite aus, dann erneut mit einem hinzugefügten Reproducer-Kandidaten, dann erneut unter AddressSanitizer und schließlich die betroffene Teilmenge unter UndefinedBehaviorSanitizer, und iteriert am Reproducer, bis er sauber auslöst oder aufgibt.
In dieser Schleife ist die Testsuite keine Steuer. Sie ist die Obergrenze. Eine Suite, die neunzig Sekunden braucht, um einen Befund-Kandidaten zu bestätigen oder zu widerlegen, erlaubt einem Agenten, Dutzende Hypothesen pro Stunde zu prüfen. Eine Suite, die fünfundzwanzig Minuten braucht, erlaubt ihm, eine Handvoll pro Tag zu prüfen. Dieselbe Codebasis, vom selben Agenten geprüft, liefert im ersten Fall mehr Sicherheitsbefunde als im zweiten, nicht weil der Agent klüger geworden wäre, sondern weil die Suite ihm aus dem Weg gegangen ist.
Von der Steuer zur Obergrenze
Das ist eine bedeutsame Verschiebung dessen, was langsame Tests kosten, und es lohnt sich, dabei präzise zu sein.
Die Kosten einer langsamen, auf Menschen ausgerichteten Suite sind ungefähr linear zur Anzahl der Läufe. Wenn deine Entwicklerinnen und Entwickler die Suite zwanzigmal am Tag ausführen und jeder Lauf zehn Minuten dauert, kannst du multiplizieren und gelangst zu der unerfreulichen Zahl, die ich im vorigen Artikel genannt habe. Schmerzhaft, aber beherrschbar.
Die Kosten einer langsamen, auf Agenten ausgerichteten Suite sind nicht linear. Sie sind selektiv. Langsame Suiten verlangsamen den Agenten nicht nur; sie verändern, welche Befunde der Agent überhaupt hervorbringt. Hypothesen, deren Verifikationskosten das Geduldsbudget des Agenten übersteigen, fallen still und leise aus dem Ergebnis heraus. Du bekommst sie nie zu Gesicht. Sie erscheinen nicht als „zurückgestellt“ oder „unvollständig“. Sie erscheinen als gar nichts. Das einzige Signal dafür, dass es sie gab, ist, dass ein anderes Team mit einer schnelleren Suite denselben Bug in seiner Codebasis findet und du ihn in deiner nicht findest.
Eine langsame Suite für Menschen ist ein Produktivitätsproblem, das du messen kannst. Eine langsame Suite für Agenten ist ein Abdeckungsproblem, das du nicht messen kannst.
Was der Agent wirklich braucht
„Schnell“ ist für sich genommen kein nützliches Ziel. Verschiedene Nutzende einer Testsuite brauchen Unterschiedliches von ihr, und die Bedürfnisse des Agenten sind spezifisch genug, um sie zu benennen.
Der Agent muss eine schmale, relevante Teilmenge der Suite schnell ausführen können. Selten will er die ganze Suite ausführen. Er will einen Befund-Kandidaten in einer bestimmten Erweiterung, einem bestimmten Modul oder einer bestimmten Komponente verifizieren, mit der passenden Sanitizer-Instrumentierung, und dann weitermachen. Eine monolithische Suite, die sich nur als ein einziger vierzigminütiger Job aufrufen lässt, ist für ihn nutzlos, selbst wenn vierzig Minuten in absoluten Zahlen gar nicht so schlimm sind. Dieselbe Suite, sauber nach Verzeichnis oder Komponente aufgeteilt, sodass der Agent nur das ausführen kann, worauf es ankommt, ist dramatisch nützlicher.
Der Agent braucht deterministische Tests. Ein Test, der sporadisch fehlschlägt, ist für einen Agenten nicht von einem echten Befund zu unterscheiden. Er wird sein Budget damit verbrauchen, einem Gespenst hinterherzujagen. Das ist derselbe Punkt, den ich im vorigen Artikel zur Flakiness gemacht habe, nur in einem Kontext, in dem er richtig wehtut: Die Kosten einer instabilen Suite bemessen sich nicht länger an der Frustration der Entwicklerinnen und Entwickler, sondern an verschwendetem Security-Review.
Der Agent braucht eine Isolation pro Test, die gut genug ist, dass das Hinzufügen eines einzelnen neuen Reproducers nicht das Verständnis der gesamten Setup-Konventionen der Suite erfordert. Testdesign, das von Reihenfolge, geteiltem Zustand oder undokumentierten Wechselwirkungen im Testinventar abhängt, steht automatisierten Reviews im Weg. Lazy initialisiertes Testinventar, In-Memory-Ersatz für schwergewichtige Abhängigkeiten und Tests, die nur das aufbauen, was sie brauchen, sind nicht nur für Menschen gute Praxis. Sie sind der Unterschied zwischen einem Agenten, der einen Reproducer im ersten Versuch hinzufügen kann, und einem Agenten, der nach drei Versuchen aufgibt.
Schließlich braucht der Agent eine Suite, die unter Sanitizern sauber läuft. Für Autorinnen und Autoren von C-Erweiterungen heißt das: Die Suite sollte unter AddressSanitizer mit detect_leaks=1 und unter UndefinedBehaviorSanitizer genauso selbstverständlich bestehen wie in einem normalen Build. Eine Suite, die ein stetiges Hintergrundrauschen „bekannter“ Sanitizer-Warnungen erzeugt, zwingt den Agenten, bei jedem Lauf das Signal vom Rauschen zu trennen. Diese Unterscheidung wird er falsch treffen.
Keine dieser Anforderungen ist neu. Alle sind Wiederholungen von Ratschlägen, die ich schon früher gegeben habe, in Artikeln, die sich an menschliche Leserinnen und Leser richteten. Was sich geändert hat, ist, dass sie von „nice to have“ zu „unverzichtbar“ befördert wurden.
Dieselbe Eigenschaft aus einem anderen Blickwinkel
Wer „Sicherheit durch Chaos“ gelesen hat, wird das zugrunde liegende Muster wiedererkennen. Fuzzing und Property-based Testing haben dieselbe Eigenschaft, dass reale Laufzeit gleich Abdeckung ist, nur noch deutlicher: Ein Fuzzer, der eine Stunde lang läuft, erkundet mehr des Eingaberaums als ein Fuzzer, der eine Minute lang läuft, und zwar auf eine annähernd monotone und gut messbare Weise. Die Geschwindigkeit der Testsuite bestimmt das Budget, innerhalb dessen jeder iterative Entdeckungsprozess arbeitet, ob der Akteur nun ein Mensch, ein Fuzzer oder ein LLM ist.
Das Neue am Fall des LLM ist nicht das Prinzip, sondern wer der Akteur ist. Ein Fuzzer ist ein Werkzeug, das du bewusst ausführst. Ein LLM-basierter Reviewer ist zunehmend die standardmäßige erste Durchsicht deines Codes, vorgenommen von dir, von deinen Mitwirkenden, von deinen nachgelagerten Nutzenden und von allen, die neugierig genug sind, einen solchen auf dein Repository anzusetzen. Die Obergrenze der realen Laufzeit, die früher eine Nischentätigkeit beschränkte, beschränkt nun die Standardtätigkeit.
Wenn du eine PHP-Erweiterung, eine Bibliothek oder irgendeine Codebasis von Bedeutung pflegst, sind die Menschen, die Security-Reviews an deinem Code durchführen, nicht mehr nur die, von denen du weißt. Dazu gehören automatisierte Reviewer, betrieben von Menschen, denen du nie begegnen wirst, die deinen Code gegen Budgets bewerten, die nicht du festlegst. Ob diese Reviewer die echten Bugs in deinem Code finden oder aufgeben, bevor sie zu ihnen vordringen, hängt zum Teil von Entscheidungen ab, die du über deine Testsuite triffst.
Was sich dadurch ändert und was nicht
Die praktischen Ratschläge ändern sich nicht. Miss zuerst, denn Intuition lügt nach wie vor. Bring das Testdesign in Ordnung, bevor du zur Infrastruktur greifst. Teile deine Suite nach Komponenten auf, damit schmale Teilmengen schnell ausgeführt werden können. Beseitige versteckte Integrationstests, die sich als Unit Tests ausgeben. Baue lazy initialisiertes Testinventar. Entferne geteilten Zustand. Lass die Tests unter Sanitizern in der Continuous Integration laufen, nicht nur in der heroischen Woche vor einem Release. Alles, was ich in „Turbo-Charging Your PHPUnit Suite“ geschrieben habe, gilt weiterhin, in derselben Reihenfolge, aus denselben Gründen.
Was sich ändert, ist die Begründung. Das Argument, diese Arbeit zu tun, musste früher an die Developer Experience appellieren, an die Moral, an die langfristige Gesundheit des Teams. Diese Argumente sind real, aber weich, und in Budgetbesprechungen haben sie stets gegen Argumente über Feature-Velocity verloren. Das neue Argument lässt sich schwerer abtun. Eine langsame, verworrene, nicht isolierte Testsuite ist heute ein Sicherheitsrisiko, wie sie es vor fünf Jahren nicht war, weil sie bestimmt, was ein automatisiertes Review deines Codes zu finden imstande ist.
Ich hüte mich davor, hier zu viel zu behaupten. Schnelle Tests machen Software nicht sicher, ebenso wenig, wie schnelle Tests Software korrekt machen. Der Punkt, den ich in „Test-Driven Security“ gemacht habe, bleibt bestehen: Sicherheit ist eine Eigenschaft, die von denselben Disziplinen aufrechterhalten wird, die jede andere Eigenschaft aufrechterhalten, die uns wichtig ist. Geschwindigkeit ist eine dieser Disziplinen, neben den anderen. Sie ist auf der Liste nur weiter nach oben gerückt.
Die Verantwortung der Maintainer
Es gibt eine etwas unbequeme Konsequenz aus all dem, die ich direkt benennen möchte.
Während des größten Teils der Geschichte von Open Source lautete der implizite Vertrag zwischen Maintainern und der übrigen Welt: Wir veröffentlichen den Code, ihr lest ihn, wenn ihr mögt. Die Qualität des Reviews, das dein Code erhält, ist durch die Aufmerksamkeit der Menschen begrenzt, die bereit sind, ihn anzuschauen, und diese war immer knapp. Maintainer konnten vernünftigerweise annehmen, dass die Sicherheit ihres Codes in der Praxis eine interne Angelegenheit war, geprüft von ihnen selbst und einer kleinen Zahl Interessierter.
Diese Annahme ist nun falsch. Das Aufmerksamkeitsbudget, das für das Review deines Codes zur Verfügung steht, ist praktisch betrachtet groß geworden. Was es beschränkt, ist nicht länger „Ist überhaupt jemand bereit hinzusehen“, sondern „Was kann der Reviewer tatsächlich ausführen“. Ein Teil der Antwort auf diese zweite Frage liegt in der Hand des Reviewers. Ein großer Teil liegt in deiner.
Wer als Maintainer die Testsuite schnell, isoliert und in schmalen Teilmengen ausführbar hält, leistet Sicherheitsarbeit, ob er es so sieht oder nicht. Er vergrößert das Fenster, in dem sowohl menschliche als auch automatisierte Reviewer Hypothesen gegen seinen Code prüfen können. Wer die Suite zu einem vierzigminütigen Monolithen verkommen lässt, der sich nur als eine einzige Einheit ausführen lässt, verkleinert dieses Fenster, ebenso unbeabsichtigt. Er führt keine Schwachstellen ein. Er verringert die Wahrscheinlichkeit, dass die bereits vorhandenen Schwachstellen gefunden werden, bevor sie ausgenutzt werden.
Ich glaube nicht, dass das bedeutet, Maintainer schuldeten der Welt eine perfekt konstruierte Testsuite. Die meisten von uns tun das in ihrer Freizeit, und die Maßstäbe, die wir an uns selbst anlegen, sind bereits höher, als irgendjemand von uns verlangen dürfte. Aber ich glaube, die Rechnung hat sich genug verschoben, um sie explizit zu machen. Wenn du das nächste Mal die Arbeit aufschiebst, die Suite aufzuteilen, das geteilte Testinventar zu entfernen oder den instabilen Test zu reparieren, den alle nur noch zu wiederholen gelernt haben, lohnt es sich zu wissen, dass die Kosten dieses Aufschubs nicht mehr nur in Entwicklerstunden bezahlt werden.
Drei Dinge zum Mitnehmen
Die Arithmetik der Testsuite-Dauer hat sich nicht verändert. Verändert hat sich, wer dafür bezahlt.
Die Arbeit, eine Suite schnell zu machen, ist dieselbe Arbeit, die sie immer war, und die Ratschläge in „Turbo-Charging Your PHPUnit Suite“ bleiben der richtige Ausgangspunkt. Neu ist der zweite Grund, sie zu tun. Schnelle Tests waren schon immer ein Merkmal gesunder Codebasen. Sie sind nun auch ein Merkmal sicherer Codebasen.
Wenn du nur drei Dinge aus diesem Artikel mitnimmst, dann diese:
- Die Geschwindigkeit der Testsuite ist nun Teil deiner Sicherheitslage, nicht nur deiner Developer Experience.
- Die Aufteilbarkeit zählt ebenso viel wie die reine Geschwindigkeit, denn automatisierte Reviewer wollen schmale Teilmengen, keine heroischen Einzelläufe.
- Die Disziplin, die eine Suite schnell macht, nämlich die Disziplin des Testdesigns, ist dieselbe Disziplin, die eine Suite für automatisierte Reviews vertrauenswürdig macht.
Das sind nicht drei Verpflichtungen, sondern eine Verpflichtung, von drei Seiten betrachtet.