npm-trust-downgrade: npm-Lockfiles auf Provenance-Downgrades prüfen

Ein kleines Werkzeug, das eine package-lock.json samt transitiver Abhängigkeiten liest und Versionen meldet, die mit schwächeren Vertrauensnachweisen veröffentlicht wurden als frühere Releases desselben Pakets.

Das Problem

Wer ein npm-Token oder den Rechner eines Maintainers hat, aber nicht die CI-Pipeline des Projekts, veröffentlicht von Hand. Die Schadversion hat dann keine Provenance, frühere Versionen schon. So war es im März 2026 bei axios 1.14.1 und 0.30.4.

pnpm kann eine solche Version mit trustPolicy: no-downgrade bei der Installation ablehnen. npm kann das nicht:

  • npm/cli#9242 fordert dieselbe Einstellung, hat 68 Reaktionen und ist seit April 2026 offen.
  • PR #9902 setzt sie um, ruht seit September, und der Ansatz ist umstritten.
  • npq erkennt eine Provenance-Regression, deckt laut README aber nur die direkten Abhängigkeiten aus der package.json ab und liest keine Lockfiles.

Wie das Werkzeug arbeitet

npm-trust-downgrade liest die package-lock.json, holt die Registry-Metadaten jedes Pakets und vergleicht jede gesperrte Version mit der Historie des Pakets. Es gibt drei Stufen: Trusted Publisher, Provenance-Attestierung und keine. Das Werkzeug hat keine Laufzeit-Abhängigkeiten, was bei einem Prüfwerkzeug dieser Art eine Rolle spielt.

Gemeldet wird eine Version, wenn ein Release stärkere Nachweise hat, das sowohl früher veröffentlicht als auch in der Semver-Ordnung kleiner ist. pnpm vergleicht nur nach Datum. Die Semver-Bedingung folgt einem Einwand aus der Diskussion zu PR #9902: Ein Wartungsrelease einer alten Linie soll nicht an einer neueren Linie gemessen werden, die Provenance erst später eingeführt hat.

pino@9.13.1 hat eine transitive Abhängigkeit, an der man die Ausgabe sieht:

$ npx --yes github:Jwrede/npm-trust-downgrade#v0.1.0
DOWNGRADE  slow-redact@0.3.2
  has: none; 0.3.1 had: trusted publisher
  0.3.2 published 2025-10-11 by matteo.collina
  0.3.1 published 2025-10-03 by GitHub Actions

13 checked, 1 finding(s), 0 not checked

Das ist eine Aufforderung zur Prüfung. Die beiden Publisher-Zeilen zeigen, dass ein Maintainer von Hand veröffentlicht hat, und nichts daran besagt, dass das Release schädlich ist. Mit --since origin/main prüft das Werkzeug nur, was ein Pull Request ändert, und mit --exclude hält man einen geprüften Befund fest. Außerdem meldet es Versionen, auf die eine Lockfile noch verweist, die aber aus der Registry entfernt wurden.

Tests

Die Tests führen die CLI gegen eine lokale Mock-Registry mit gekürzten Kopien echter Metadaten aus. Dazu gehört die axios-Historie, rekonstruiert für die Stunden, in denen die Schadversion online war. Eine zweite Testreihe erzeugt Lockfiles mit echtem npm und vergleicht die Urteile mit echtem pnpm. Das CI führt beides aus, auf Node 20, 22 und 24 (die Live-Tests auf 22).

Bei einer Lockfile aus 43 verbreiteten Paketen mit 2815 verschiedenen Versionen meldete das Werkzeug 2 Versionen, die Datumsregel 5. Die drei übrigen, @octokit/endpoint@9.0.6, ua-parser-js@1.0.41 und css-declaration-sorter@6.4.1, sind Releases einer älteren Linie, und pnpm 10.34.9 lehnte jede davon ab. Der Lauf dauerte 73 Sekunden. Die Stichprobe enthält keine bekannte Schadversion. Sie misst also, wie oft die Prüfung eine Durchsicht verlangt, und sagt nichts über Erkennungsraten.

Grenzen

  • Provenance ist ein Signal, und es gibt ein ernsthaftes Argument, sie gar nicht als Sicherheitsprüfung zu verwenden. Maintainer können sie aus legitimen Gründen aufgeben, und auch das wird gemeldet.
  • Wer Zugriff auf die CI-Pipeline hat, veröffentlicht mit vollen Nachweisen und wird nicht erkannt. Ein Paket, das nie Nachweise hatte, kann nicht herabgestuft werden.
  • Die Semver-Bedingung hat einen Preis. Ein neues Release unterhalb der ersten attestierten Version kommt durch, wo pnpm es ablehnen würde.
  • Attestierungen werden nicht kryptografisch geprüft. Das macht npm audit signatures.
  • Es ist ein eigener Schritt und hält npm install nur auf, wenn die Pipeline ihn vorher ausführt. Gelesen werden nur npm-Lockfiles, und Anfragen enthalten keine Zugangsdaten für private Registries.

Der Code liegt unter github.com/Jwrede/npm-trust-downgrade.