Vorab: Ich bin kein Rechtsanwalt, und dieser Artikel ist keine Rechtsberatung. Ich finde das Thema interessant und wichtig genug, um mich darin einzuarbeiten. Dieser Artikel spiegelt den Stand meiner Erkenntnis nach bestem Wissen und Gewissen wider. Wenn du einen Fehler findest, freue ich mich über eine Nachricht.
Composer erzeugt einen großen Teil der Nachweise, die ISO/IEC 18974 verlangt, nebenbei bei jedem Build. Was es nicht erzeugt, ist ein Archiv des Codes, den du ausgeliefert hast, ein zweiter Blick auf Releases, die schon bei deinen Kundinnen und Kunden laufen, und ein Name neben jeder Verantwortung. Für ein PHP-Team liegt genau dort der Aufwand einer Konformität.
Seit dem 11. September 2026 gelten die Meldepflichten aus Artikel 14 des Cyber Resilience Act, und Meldungen laufen über die Single Reporting Platform der ENISA. Die meisten übrigen Pflichten folgen am 11. Dezember 2027. Laut OpenChain-FAQ erfährt eine Organisation in erster Linie im Vertrieb oder in Einkaufsverhandlungen, ob ein Lieferant ISO/IEC 18974 erfüllt. Bei einer PHP-Agentur oder einem Produkthersteller kommt die Frage also eher aus dem Einkauf einer Kundin als aus einem Audit.
Frédéric Noppe hat eine deutschsprachige Einführung geschrieben, die die Norm Abschnitt für Abschnitt durchgeht (Teil 1, Teil 2). Ich wiederhole sie hier nicht. Stattdessen lese ich die Norm aus der Sicht eines Composer-Projekts, und an einer Stelle lese ich sie strenger als er.
Die Norm gilt für „supplied software“, also Software, die eine Organisation „distributes or makes available to third parties“. Für eine Agentur, die einen Shop an ihre Kundin übergibt, und für einen Hersteller, dessen Kundschaft sein Produkt oder Plugin installiert, ist das eindeutig. Für eine Webanwendung, die du selbst betreibst und die deine Kundschaft nur im Browser nutzt, ist es das nicht, und eine maßgebliche Antwort darauf kenne ich nicht. Dieser Artikel ist für den eindeutigen Fall geschrieben.
Eine bewusst enge Norm
ISO/IEC 18974 ist aus der OpenChain Security Assurance Specification 1.1 hervorgegangen und wurde im Dezember 2023 als ISO/IEC-Norm veröffentlicht. Sie hat acht Seiten. Das OpenChain Project veröffentlicht unter CC-BY 4.0 einen funktional identischen Text, sodass du die Anforderungen lesen kannst, ohne das ISO-Dokument zu kaufen. Dieser Text zählt die Anforderungen wie die ISO-Fassung in Abschnitt 4, und ich verwende diese Nummern durchgehend. Die ältere Spezifikation 1.1 zählt sie in Abschnitt 3 und staffelt die Geltungsdauer einer Konformität auf 18, 24 und 36 Monate, während die ISO-Fassung 18 Monate nennt. Wenn du mit Material arbeitest, das auf 1.1 beruht, behalte beide Unterschiede im Blick.
Nach eigener Aussage beschreibt die Norm das „what“ und „why“ eines Programms für Security Assurance und lässt das „how“ und „when“ bewusst offen. Zu jeder Anforderung gehören Verification Materials, und das sind Aufzeichnungen: ein dokumentiertes Verfahren, eine Liste von Namen, der Nachweis einer Überprüfung. Konform ist immer ein Programm, nie ein Paket. Ein Composer-Paket kann ISO/IEC 18974 nicht erfüllen, ein PHAR genauso wenig.
Zwei Definitionen tragen den Rest dieses Artikels. Eine „known vulnerability“ ist eine Schwachstelle, die in öffentlich verfügbaren Open-Source-Komponenten bereits entdeckt wurde, und die Definition zählt „CVEs, GitHub/GitLab vulnerability alerts, and package manager alerts“ ausdrücklich auf. Die Meldungen von Package-Managern sind also namentlich genannt. Was composer audit meldet, ist damit genau die Art von Eingabe, für die die Norm geschrieben wurde, und du musst sie vor niemandem rechtfertigen. „Supplied software“ ist, was du verteilst oder anderen zur Verfügung stellst. Der Abschnitt require-dev deiner composer.json gehört in aller Regel nicht dazu.
Die Einleitung der Norm nennt ihren Gegenstand „a narrow subset of primary concern“: die Prüfung von Open-Source-Software gegen öffentlich bekannte Schwachstellen. Malware in einer Abhängigkeit, ein übernommener Account einer Maintainerin, ein Tag, der auf einen anderen Commit verschoben wurde: Nichts davon ist in dem Moment, in dem es passiert, eine bekannte Schwachstelle. Die Backdoor in XZ Utils, die in diesem Zusammenhang oft genannt wird, war bis zu ihrer Offenlegung auch keine. Die Prozesse der Norm greifen erst nach der Offenlegung. Auch die Build-Umgebung liegt außerhalb der Norm. Was ich über das Härten von GitHub Actions Workflows geschrieben habe, gehört in die Domäne von SLSA, nicht in die von ISO/IEC 18974.
Composer schreibt die meisten Nachweise schon
Anforderung 4.1.5 verlangt für jede von acht Methoden ein dokumentiertes Verfahren, vom Erkennen von Bedrohungen bis zur Weitergabe von Risikoinformationen an Dritte. Anforderung 4.3.2 verlangt, dass auf jede Komponente der Stückliste Maßnahmen zur Security Assurance angewendet werden: bekannte Schwachstellen erkennen, eine Risikobewertung vergeben, entscheiden und dokumentieren, was zu tun ist, und Buch führen „of the identified known vulnerabilities and action(s) taken (including even if no action was required)“.
Seit Composer 2.9 verweigert der Resolver bei update, require und remove Versionen, die von einem bekannten Advisory betroffen sind. Wie das funktioniert, habe ich in „Der Türsteher im Dependency-Resolver“ beschrieben. Gegen die Norm gelesen setzt dieser Türsteher die zweite Methode aus 4.1.5 um, das Erkennen bekannter Schwachstellen in der gelieferten Software, und er tut das, ob jemand daran denkt oder nicht. composer audit ist dieselbe Methode auf Abruf.
Mehr interessiert mich, wo die Entscheidungen landen. Composer konfiguriert seine Dependency Policies unter config.policy in der composer.json, und die composer.json liegt in der Versionskontrolle. Wenn dich ein Advisory nicht betrifft, hältst du das als ignoriertes Advisory mit Begründung fest:
{
"config": {
"policy": {
"advisories": {
"ignore-id": {
"GHSA-xxxx-xxxx-xxxx": "Betrifft nur den SOAP-Client, den wir nicht verwenden. Vor dem Aktivieren der SOAP-Integration neu bewerten.",
"CVE-2026-xxxxx": "Nur unter Windows ausnutzbar. Die gelieferte Software läuft in Linux-Containern (siehe Scope-Erklärung)."
}
}
}
}
}
Jeder Eintrag ist eine Aufzeichnung über eine erkannte bekannte Schwachstelle und die ergriffene Maßnahme, wie 4.3.2.2 sie verlangt, auch für den Fall, dass die Maßnahme „keine“ lautet. Die Begründung sagt, warum. Der Commit, der den Eintrag hinzugefügt hat, sagt, wer und wann. Die Norm definiert „documented evidence“ als „explicitly stored data that outlines, explains or records information related to activities and actions“, und die Ausgabe von git log -p composer.json erfüllt diese Definition.
Die andere Hälfte von 4.3.2.2 ist der Fall, in dem du gehandelt hast. Ein Update, das eine verwundbare Version ersetzt, ist nur dann eine Aufzeichnung, wenn die Commit-Nachricht das Advisory nennt. Diese Commit-Nachricht schreibt Composer nicht für dich.
4.3.2 verlangt außerdem für jede erkannte Schwachstelle eine Bewertung von Risiko oder Auswirkung. composer audit zeigt den Schweregrad aus dem Advisory an. Das ist die Einschätzung derjenigen, die das Advisory veröffentlicht haben, und sie gilt für den allgemeinen Fall. Die Norm verlangt Maßnahmen „suitable for the use-case of the software“, und diese Einschätzung musst du selbst treffen.
Die siebte Methode aus 4.1.5 verlangt ein Verfahren, mit dem du vor dem Release prüfst, ob erkannte Risiken behandelt wurden. In einem Composer-Projekt ist das eine Zeile in der Release-Pipeline:
composer audit --locked --no-dev
--locked prüft, was in der composer.lock steht, unabhängig davon, was gerade in vendor/ liegt. --no-dev beschränkt die Prüfung auf das, was du auslieferst, und das ist der Geltungsbereich der Norm. policy.advisories.audit steht standardmäßig auf fail, sodass ein Advisory, das du nicht behandelt hast, den Befehl mit einem Exit-Code ungleich null beendet und das Release stoppt. Der Exit-Code ist das Freigabekriterium. Derselbe Befehl schlägt auch bei aufgegebenen Paketen und bei Versionen fehl, die als Malware markiert sind, weil diese Policies ebenfalls standardmäßig auf fail stehen. Beides ist keine bekannte Schwachstelle im Sinne der Norm, und ich lasse beides trotzdem im Gate.
Die folgende Tabelle ordnet jeder Anforderung der Norm zu, was Composer und die PHP-Werkzeuge beitragen und was offen bleibt. In den Zeilen zu 4.1.5 ist Composer stark. In den Zeilen darüber und darunter hat es nichts anzubieten.
| Anforderung | Composer und PHP | Was offen bleibt |
|---|---|---|
| 4.1.1 Policy |
Nichts. config.policy ist technische Konfiguration, keine schriftliche Richtlinie
|
Richtlinie und ihre Kommunikation |
| 4.1.2 Competence | Nichts | Rollen, Kompetenzprofile, Nachweise |
| 4.1.3 Awareness | Nichts | Nachweis geprüfter Awareness |
| 4.1.4 Program scope |
--no-dev als Grenze des Geltungsbereichs, eine composer.json pro Produkt
|
Scope-Erklärung, Kennzahlen, Nachweise von Überprüfungen |
| 4.1.5 (1) Strukturelle und technische Bedrohungen |
PHPStan oder Psalm, allow-plugins gegen Codeausführung bei der Installation
|
Threat Modeling |
| 4.1.5 (2) Bekannte Schwachstellen erkennen |
composer audit, Blockieren von Advisories bei update, require und remove
|
Betriebssystem und Laufzeitumgebung |
| 4.1.5 (3) Nachverfolgung |
policy.advisories.ignore-id mit Begründung, versioniert
|
Fristen, Zuständigkeit |
| 4.1.5 (4) Kommunikation an Kundinnen und Kunden | GitHub Security Advisories für Bibliotheken | VEX oder CSAF für Anwendungen |
| 4.1.5 (5) Analyse nach dem Release |
composer audit --locked gegen archivierte Lock-Dateien, Dependency-Track
|
Jemand muss es einplanen; install blockiert keine Advisories
|
| 4.1.5 (6) Wiederholte Sicherheitstests vor dem Release | Audit in der CI, PHPUnit-Regressionstests für behobene Schwachstellen | Nichts |
| 4.1.5 (7) Risiken vor dem Release behandelt |
Exit-Code von composer audit als Release-Gate
|
Nichts |
| 4.1.5 (8) Risikoinformationen für Dritte |
SBOM aus dem CycloneDX-Plugin, --sbom des PHPUnit-PHAR
|
VEX |
| 4.2.1 Access |
SECURITY.md, support.security in der composer.json, Private Vulnerability Reporting auf GitHub, security.txt
|
Internes Verfahren für Antworten |
| 4.2.2 Effectively resourced | Ecosystem Security Team der PHP Foundation (für Maintainerinnen und Maintainer) | Personen, Budget, Zeit |
| 4.3.1 Software Bill of Materials |
composer.lock, CycloneDX-Plugin
|
Archiv der Software selbst; Autor und Zeitstempel |
| 4.3.2 Security Assurance |
Audit, Schweregrade aus den Advisories, ignore-id mit Begründung
|
Zustimmung der Kundschaft; eine Aufzeichnung auch dann, wenn keine Maßnahme nötig war |
| 4.4.1 Completeness | Nichts | Bestätigung, dass alle Anforderungen erfüllt sind |
| 4.4.2 Duration | Nichts | Erneute Bestätigung spätestens nach 18 Monaten |
security.txt nach RFC 9116 ist in dieser Tabelle eine Empfehlung, keine Anforderung der Norm. 4.2.1 verlangt nur einen öffentlich sichtbaren Weg, Anfragen zu Schwachstellen zu stellen, und ein internes Verfahren, sie zu beantworten.
Drei Lücken, die es nur in PHP gibt
Niemand sieht sich ein Release an, nachdem es ausgeliefert ist
composer install aus einer Lock-Datei blockiert keine Advisories. Das ist Absicht: Eine Lock-Datei gibt es, damit zweimal dasselbe installiert wird, und eine Installation, die den Abhängigkeitsgraphen bei jedem neuen Advisory neu auflöst, würde dieses Versprechen brechen. Die Folge ist, dass ein Release, das du 2024 ausgeliefert hast, seine verwundbaren Abhängigkeiten behält, und nichts in deiner täglichen Arbeit sagt es dir. Der Türsteher steht nur an der Tür von composer update, und niemand führt composer update auf einem ausgelieferten Release aus.
Diesen zweiten Blick verlangt die fünfte Methode aus 4.1.5: „analyzing supplied software for newly published known vulnerabilities post release“. Stell dir die Frage vor, wie sie bei einer Agentur ankommt: Für eine verbreitete Bibliothek wurde eine Schwachstelle veröffentlicht, und eine Kundin will wissen, ob der Shop betroffen ist, der 2024 ausgeliefert wurde. Das ist die Art von Anfrage, die du nach 4.2.1 beantworten können musst.
Wenn du composer.json und composer.lock jedes Releases aufbewahrst, das du noch unterstützt, ist die Antwort eine Schleife:
for release in releases/*/; do
composer audit --locked --no-dev --format=summary --working-dir="$release" || echo "$release needs attention"
done
Lass sie regelmäßig laufen, per Cron oder als geplanten Job in der CI. Die ignorierten Advisories, die zum Zeitpunkt des Releases galten, sind mit ihm archiviert, weil sie in seiner composer.json stehen. Dependency-Track leistet dasselbe für SBOMs und braucht dafür keinen Cron-Job. In beiden Fällen findet die Analyse nur statt, wenn jemand sie eingeplant hat, und dieser Plan ist es, den du nach 4.1.5 dokumentieren sollst.
Packagist ist kein Archiv
4.3.1.1 verlangt ein dokumentiertes Verfahren, das die gesamte Open-Source-Software in der gelieferten Software über ihren Lebenszyklus festhält, und ergänzt: „This includes an archive of all open source software used in the supplied software.“ Frédéric Noppe liest das als Archivierung der SBOM. Der Wortlaut verlangt die Software selbst. Für PHP lohnt es sich, die strengere Lesart ernst zu nehmen, denn dieses Archiv führt sonst niemand für dich.
Packagist.org speichert keinen Code. Es speichert Metadaten, die auf ein Git-Repository verweisen, und für die meisten Pakete lädt Composer ein ZIP-Archiv herunter, das GitHub bei Bedarf erzeugt. GitHub garantiert nicht, dass das Archiv zu demselben Commit dauerhaft byte-identisch bleibt. Seit Juli 2026 lässt sich der Commit, auf den eine stabile Version verweist, auf Packagist nicht mehr ändern, wie ich in „Composer und Packagist im Supply Chain-Stresstest“ beschrieben habe. Eine Version kann aber weiterhin per Soft Delete gelöscht werden, danach löst Composer sie nicht mehr auf, und ein Repository kann ganz verschwinden.
Das Archiv muss also deines sein. Die einfachste Form ist das Artefakt, das du auslieferst, samt vendor/: das Archiv, das du deiner Kundin übergibst, oder das Container-Image, das du pushst. Wenn du lieber Pakete als Builds archivierst, kann Private Packagist Repositories von Dritten spiegeln und Kopien der verwendeten Pakete vorhalten. Ein Archiv, das aus Links besteht, zählt nicht.
Was composer.lock nicht sieht
Die composer.lock weiß, was Composer installiert hat. Nicht alles, was in einem PHP-Deployment landet, kam über Composer.
Ein PHAR bündelt seine Abhängigkeiten, und Werkzeuge wie PHPUnit versehen die Namensräume dieser Abhängigkeiten mit einem Präfix, damit sie nicht mit deinen kollidieren. Warum PHPUnit das tut, habe ich in „Eine Version pro Prozess“ erklärt. Der Preis dafür ist, dass die composer.lock das Paket nennt, in dem das PHAR steckt, sofern es überhaupt mit Composer installiert wurde, aber nicht dessen Inhalt. composer audit schaut nicht hinein, und für einen Scanner ist das PHAR eine einzelne Datei, deren gebündelter Code seinen ursprünglichen Namensraum nicht mehr trägt. Werkzeuge, die du mit PHIVE installierst, tauchen in der composer.lock gar nicht auf. Eine Bibliothek, die jemand von Hand ins Repository kopiert hat, ebenso wenig.
Dazu kommt die Plattform. Die composer.lock hält die PHP-Version und die Extensions, die deine Pakete benötigen, als Anforderungen fest, nicht als installierte Versionen. Welches PHP deinen Code tatsächlich ausführt, entscheidet der Server oder das Container-Image. Wenn du ein Container-Image auslieferst, brauchst du für diese Schicht und für die Pakete des Betriebssystems darunter einen Scanner. Für diese Schicht ist ein Scanner wie Trivy oder Syft das richtige Werkzeug, so wie es die Artikel von L3montree empfehlen. Für die Composer-Schicht ist die deklarierte Liste besser als alles, was ein Scanner rekonstruieren kann.
composer.lock ist noch kein Component Record
4.3.1.2 verlangt „component records“, und die Norm definiert einen Component Record über die NTIA-Mindestelemente: Name des Lieferanten, Name der Komponente, Version, weitere eindeutige Kennungen, Abhängigkeitsbeziehung, Autor der SBOM-Daten und Zeitstempel. Die composer.lock hat die ersten fünf. Sie nennt weder einen Autor ihrer Daten noch den Zeitpunkt, zu dem sie erzeugt wurde.
Das Plugin cyclonedx/cyclonedx-php-composer macht aus den Composer-Metadaten ein CycloneDX-Dokument, das die fehlenden Elemente enthält:
composer CycloneDX:make-sbom --spec-version=1.6 --omit=dev --output-format=JSON --output-file=sbom.cdx.json
Gib die Version der Spezifikation ausdrücklich an. Das Plugin erzeugt standardmäßig CycloneDX 1.5, und Version 2.1.0 der Technischen Richtlinie TR-03183-2 des BSI verlangt CycloneDX ab 1.6 oder SPDX ab 3.0.1. Die Richtlinie ist ausdrücklich unverbindlich und begründet keine Konformitätsvermutung für den CRA. Sie ist trotzdem die konkreteste Beschreibung eines SBOM für den CRA, die ich kenne, und ich habe sie für PHPUnit als Maßstab genommen.
Ein Zielkonflikt bleibt, und das Plugin löst ihn nicht. Ein reproduzierbarer Build erzeugt aus derselben Eingabe dieselben Bytes. --output-reproducible erreicht das, indem es jeden Wert weglässt, der von der Zeit oder vom Zufall abhängt, und dazu gehören der Zeitstempel und die Seriennummer. Die NTIA-Mindestelemente verlangen einen Zeitstempel, Abschnitt 5.2.1 der TR ebenso. Mit dem Plugin wählst du zwischen einem SBOM, das reproduzierbar ist, und einem, das vollständig ist. Der Ausweg ist ein deterministischer Zeitstempel: der Zeitpunkt des Commits, auf den das Release-Tag zeigt, übergeben als SOURCE_DATE_EPOCH. Das Plugin liest diese Variable nicht. Das Skript, das das SBOM für PHPUnit erzeugt, liest sie.
Ab dem 11. Dezember 2027 verlangt der CRA selbst ein SBOM „in a commonly used and machine-readable format“, das mindestens die direkten Abhängigkeiten des Produkts abdeckt (Anhang I, Teil II, Nummer 1). Ein Composer-Projekt deckt mehr ab, ohne sich anzustrengen, weil die composer.lock auch die transitiven Abhängigkeiten aufführt. Was ihr fehlt, sind ein geeignetes Format und die zwei fehlenden Elemente.
Was ich am SBOM von PHPUnit geändert habe
Bei der Recherche zu diesem Artikel habe ich PHPUnit an der Norm gemessen. PHPUnit gehört in require-dev und ist damit normalerweise nicht Teil der gelieferten Software von irgendjemandem. Das PHAR ist aber Software, die ich verteile, und im August habe ich das darin eingebettete SBOM als etwas beschrieben, das Supply Chain-Werkzeuge direkt verarbeiten können.
Gemessen an der Definition eines Component Record reichte es nicht. Mit Stand vom 3. Oktober 2026 schrieb das Skript, das es erzeugte, den Namespace von CycloneDX 1.4 und für jede Komponente Gruppe, Name, Version, Beschreibung, Lizenzen und Package-URL. Drei der sieben Mindestelemente fehlten: die Abhängigkeitsbeziehungen, der Autor der SBOM-Daten und der Zeitstempel.
Am 4. Oktober 2026 habe ich das auf allen Branches von 8.5 bis main geändert. Das eingebettete SBOM verwendet jetzt CycloneDX 1.7, nimmt seinen Zeitstempel aus SOURCE_DATE_EPOCH oder vom Release-Tag, nennt seinen Autor und das Werkzeug, das es erzeugt hat, und enthält den vollständigen Abhängigkeitsgraphen. Die Tests des PHAR validieren es gegen das Schema von CycloneDX 1.7. Ein dritter Commit ergänzt die Felder der TR-03183-2: die Urheber jeder Komponente, Lizenzen getrennt nach deklarierten und festgestellten sowie die effektive Lizenz, die URI des Quellcodes, den Ort der security.txt und eine Aussage darüber, wie vollständig die Angaben zu den Abhängigkeiten sind. PHPUnit 8.5.56, 9.6.38, 10.5.66, 11.5.57, 12.5.38 und 13.4.1, alle am 5. Oktober 2026 erschienen, enthalten es.
Jedes Feld stammt aus composer.json, composer.lock oder Git, und nichts wird gescannt. Die neun externen Komponenten sind PHP und die Extensions, die PHPUnit und die gebündelten Pakete benötigen. Sie sind die ersten Komponenten im SBOM, die ich nicht ausliefere: Das PHAR braucht sie, enthält sie aber nicht. CycloneDX 1.7 kennzeichnet solche Komponenten mit isExternal, und das passt zu dem, was ISO/IEC 18974 „supplied software“ nennt.
Ein Element konnte nicht ins PHAR. Ein SBOM in einem PHAR kann den Hash dieses PHAR nicht enthalten, weil sich der Hash ändert, sobald das SBOM ins PHAR geschrieben wird. Deshalb liegt neben jedem neuen PHAR auf phar.phpunit.de jetzt ein zweites SBOM, phpunit-X.Y.Z.phar.cdx.xml. Es enthält das eingebettete SBOM, dazu Dateiname und SHA-512-Hash des PHAR, die Eigenschaften, mit denen die TR die Datei als ausführbar, als Archiv und als strukturiert beschreibt, und eine Seriennummer, die als UUIDv5 aus seiner Download-URL abgeleitet ist, sodass ein erneuter Build dasselbe Dokument erzeugt. Es ist wie das PHAR selbst mit GPG signiert, und phpunit.de/verify.html beschreibt, wie du es prüfst.
Ich habe das veröffentlichte SBOM zu PHPUnit 13.4.1 so geprüft, wie es eine Kundin tun würde: Die Signatur ist gültig, der SHA-512-Hash stimmt mit dem PHAR überein, und das Dokument validiert gegen das Schema von CycloneDX 1.7. Der letzte Schritt ist schwieriger, als er sein sollte. Das offizielle Schema importiert spdx.xsd von einer URL, die xmllint nicht lädt, und so musste ich den Import zuerst in einer lokalen Kopie des Schemas umschreiben.
Wegen des oben beschriebenen Zielkonflikts verwendet PHPUnit das CycloneDX-Plugin nicht. Die eigenen Skripte nehmen den Zeitstempel aus SOURCE_DATE_EPOCH oder vom Release-Tag, und ihre Ausgabe ist dadurch reproduzierbar und vollständig zugleich. phar-site-generator 5.3.0 baut sein eigenes PHAR auf dieselbe Weise, das Vorgehen hängt also nicht an PHPUnit.
Zwei Punkte bleiben offen, und der ChangeLog-Eintrag beansprucht keinen davon. Erstens kennt die TR Komponenten auf Dateiebene, auf der jede ausführbare Datei eine eigene Komponente ist. Das PHAR von PHPUnit 13.4 enthält rund 1.850 PHP-Dateien, und die etwa 650 davon, die aus gebündelten Paketen stammen, hat php-scoper verändert, sodass sie nicht mehr mit ihrem Upstream übereinstimmen. Der ChangeLog beansprucht deshalb nur die Felder, die die TR für logische und identifizierte Komponenten verlangt. Zweitens verlangt die TR eine CPE, und für PHP kann das SBOM nur die Mindestversion aus der composer.json nennen, weil das PHAR nicht weiß, auf welchem PHP es laufen wird. Ein Scanner, der CPEs mit der NVD abgleicht, liest das als installierte Version und meldet für PHPUnit 13.4 die Schwachstellen von PHP 8.4.1 und für PHPUnit 8.5 die von PHP 7.2.0. Eine gute Antwort darauf habe ich noch nicht gefunden.
Ob PHPUnit überhaupt zur gelieferten Software von irgendjemandem gehört, ist eine Frage des Geltungsbereichs, und composer install --no-dev setzt diesen Geltungsbereich durch. Im Dezember 2017 nahm ein Hosting-Anbieter den Onlineshop eines Juweliers vom Netz, weil ein Scan PHPUnits eval-stdin.php (CVE-2017-9841) auf dem Server gefunden hatte: Eine der Komponenten, aus denen der Shop bestand, hatte eine veraltete PHPUnit-Version mit in die Produktion geliefert. Die Geschichte habe ich in „PHPUnit: Ein Sicherheitsrisiko?“ erzählt. Eine Scope-Erklärung, die Entwicklungsabhängigkeiten ausschließt, hilft nicht, wenn ein Release-Prozess sie ignoriert.
Ein Name neben jeder Verantwortung
Für die Anforderungen 4.1.1 bis 4.1.4 und 4.2.2 gibt es in Composer keine Entsprechung. Sie verlangen:
- Eine schriftliche Richtlinie für die Security Assurance der gelieferten Software, die intern kommuniziert und überprüft wird
- Eine Liste von Rollen mit ihren Verantwortlichkeiten, die Kompetenzen, die jede Rolle braucht, und Nachweise, dass die Menschen in diesen Rollen sie haben
- Nachweise, dass diese Menschen die Richtlinie kennen, ihren Beitrag und die Folgen, wenn das Programm nicht eingehalten wird
- Eine schriftliche Erklärung des Geltungsbereichs, Kennzahlen und Nachweise jeder Überprüfung
- Benannte Personen, Gruppen oder Funktionen für jede Rolle, ausreichend Personal und Budget, eingeplante Zeit und Zugang zu Fachwissen über bekannte Schwachstellen
Der letzte Punkt ist 4.2.2, „effectively resourced“, und das ist die Anforderung, die ein kleines Team nicht dadurch erfüllt, dass es ein Dokument schreibt. Die Aufzeichnung, die sie verlangt, ist ein Name, und dieser Name muss einer Person gehören, die dafür Zeit hat. In einem Team aus fünf Leuten, das ein Produkt ausliefert, mag „alle haben die Advisories im Blick“ ehrlich beschreiben, wie es läuft, aber die Anforderung erfüllt das nicht.
4.1.4.2 verlangt „a set of metrics the program shall achieve to improve“. Zwei davon kann ein Composer-Projekt aus Daten erzeugen, die es ohnehin hat. Die erste ist die Zeit von der Veröffentlichung eines Advisory bis zum Deployment der Korrektur. Die zweite ist die Zahl der ignore-id-Einträge, die keine Begründung haben oder älter sind als eine Grenze, die du festlegst; git blame composer.json zeigt dir, wie alt jeder einzelne ist.
Noch eine Bemerkung, die meine Meinung ist und keine Anforderung der Norm. Wer die zweite Methode aus 4.1.5 auf die Advisory-Daten von Packagist stützt, sollte überlegen, Packagist über das Sponsoring-Programm zu unterstützen, das im Mai 2026 angekündigt wurde. 4.2.2 meint deine eigenen Ressourcen. Die Advisory-Daten, von denen dein Audit abhängt, stellt ein kleines Unternehmen bereit, dessen Sicherheitsarbeit zum Teil von außen finanziert wird.
Wo Composer der Norm voraus ist
Die PHP-Vorfälle von 2026 waren keine bekannten Schwachstellen. Im April wurden bösartige Releases von intercom/intercom-php über ein kompromittiertes GitHub-Repository veröffentlicht. Im Mai erreichte ein Credential Stealer die laravel-lang-Pakete über gestohlene Zugangstoken. ISO/IEC 18974 hat zu beidem nichts zu sagen, solange es kein Advisory gibt. Composer und Packagist schon: Die Malware-Policy blockiert markierte Versionen auch bei composer install, Packagist lässt eine stabile Version nicht mehr auf einen anderen Commit zeigen, und das Transparency Log hält fest, was mit einem Paket passiert ist. Nichts davon verlangt die Norm, und alles davon richtet sich gegen Bedrohungen, die sie nicht abdeckt.
Die Cooldown-Policy, die für Composer 2.11 entwickelt wird, zeigt, dass beides in entgegengesetzte Richtungen ziehen kann. Sie hält neu veröffentlichte Versionen für einen einstellbaren Zeitraum zurück, weil viele bösartige Releases innerhalb von Stunden oder Tagen entdeckt werden. Eine Ausnahme für Releases, die eine Schwachstelle beheben, gibt es bewusst nicht. Der Pull Request, mit dem die Policy eingeführt wurde, begründet das: Die Metadaten auf Packagist enthalten Advisories nur teilweise und ohne das Datum ihrer Meldung, und wer ein Release taggen kann, kann auch ein Advisory einreichen. Ein Cooldown verzögert damit auch die Korrektur einer bekannten Schwachstelle, und um bekannte Schwachstellen geht es in ISO/IEC 18974.
Sind alle Versionen nach der Wartezeit durch ein Advisory oder eine Filterliste blockiert, listet Composer jede Version mit der Policy auf, die sie entfernt hat, und nennt die Auswege: einen Eintrag unter policy.cooldown.ignore oder einmalig COMPOSER_POLICY_COOLDOWN_PERIOD=0. Die Dokumentation zeigt den Konflikt in ihrem eigenen Beispiel, mit der Begründung „Security fixes need to be applied immediately“ neben symfony/security-bundle. Sobald Composer 2.11 erschienen ist, würde ich den Cooldown einschalten und die Korrektur einer bekannten Schwachstelle wie jede andere Ausnahme behandeln: ein Eintrag unter policy.cooldown.ignore mit Begründung, committet und wieder entfernt, sobald die Wartezeit vorbei ist. Damit landet die Entscheidung in derselben Aufzeichnung wie jedes ignorierte Advisory.
Der CRA ergänzt eine Pflicht, die weder Composer noch die Norm abdeckt. Ein Hersteller, der eine Schwachstelle in einer integrierten Komponente findet, auch in einer Open-Source-Komponente, muss sie an diejenigen melden, die diese Komponente pflegen (Artikel 13 Absatz 6). Für einen PHP-Lieferanten ist das das andere Ende der Meldung, die der Leitfaden „So you received a security report. Now what?“ der PHP Foundation aus Sicht der Maintainerinnen und Maintainer beschreibt.
Fazit
OpenChain empfiehlt, mit einer Selbstzertifizierung und einem eng geschnittenen Programm zu beginnen, also mit einem einzelnen Produkt statt mit dem ganzen Unternehmen. Selbstzertifizierung heißt, jede Frage einer Checkliste mit Ja zu beantworten, und für diese Antworten steht allein die Organisation ein. Ich würde diese Checkliste zuerst als Lückenanalyse benutzen: Leg die Tabelle aus diesem Artikel daneben und markiere die Zeilen, für die du heute eine Aufzeichnung hast.
In einem Composer-Projekt sind die meisten Zeilen zu 4.1.5 markiert, bevor du anfängst, weil Resolver, Audit und Lock-Datei die ganze Zeit Aufzeichnungen geschrieben haben. Die Zeilen zu 4.1.1 bis 4.1.4 und zu 4.2.2 sind meistens leer. Sie sind es, die jemand neu ausfüllen muss, wenn die 18 Monate um sind und die nächste Kundin fragt.