open-standards/scripts/check-advisories.py
Transaction Science 5fc8779968
All checks were successful
joule-moq-remote host agent / joule-moq-remote · does this change touch it (pull_request) Successful in 8s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 7s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 8s
wai-app and Create / wai-app · does this change touch it (pull_request) Successful in 9s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 9s
wai-web reach builds / wai-web reach · does this change touch it (pull_request) Successful in 9s
wai-audit collector and player integration / does this change touch it (pull_request) Successful in 7s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (pull_request) Successful in 15s
WAI operation catalogue / does this change touch it (pull_request) Successful in 9s
WAI conformance / does this change touch it (pull_request) Successful in 9s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (pull_request) Successful in 43s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (pull_request) Successful in 51s
wai-audit collector and player integration / collector · linux (pull_request) Successful in 1m11s
WAI energy receipts / does this change touch it (pull_request) Successful in 39s
WAI examples / WAI examples · does this change touch them (pull_request) Successful in 17s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (pull_request) Successful in 1m35s
wai-fleet service / does this change touch it (pull_request) Successful in 25s
WAI JPEG-AI whole decode (pinned inputs, required) / does this change touch it (pull_request) Successful in 24s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (pull_request) Successful in 2m9s
wai-ledger service / does this change touch it (pull_request) Successful in 17s
wai-key-proxy / does this change touch it (pull_request) Successful in 19s
wai-web reach builds / wai-web reach builds · the package of record, every output as recorded (pull_request) Successful in 2m29s
WAI neural sink gates / does this change touch it (pull_request) Successful in 10s
wai-ops-gpu integer operators on GPUs / does this change touch it (pull_request) Successful in 9s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 8s
wai-quantum docs, course and locales / does this change touch it (pull_request) Successful in 8s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 35s
wai-quantum for JavaScript / does this change touch it (pull_request) Successful in 6s
wai-quantum studio and desktop / does this change touch it (pull_request) Successful in 7s
wai-quantum WebGPU member / does this change touch it (pull_request) Successful in 8s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (pull_request) Successful in 2m38s
wai-sink-android native sink library / does this change touch it (pull_request) Successful in 9s
wai-sink-swift native sink package / does this change touch it (pull_request) Successful in 11s
WAI mbt2018_mean integer decode (regenerated vectors) / does this change touch it (pull_request) Successful in 9s
WAI Studio / does this change touch it (pull_request) Successful in 7s
wai-web wasm32 / does this change touch it (pull_request) Successful in 8s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 3m42s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 36s
WAI conformance / fast integer kernels, every x86_64 kernel set (AVX-512 silicon) (pull_request) Successful in 3m7s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 1m17s
WAI conformance / faces from keypoints under wasm32-wasip2 (wasmtime) (pull_request) Successful in 1m45s
wai-web reach builds / wai-web reach builds · this tree's wai-web, every rung on its engine line (pull_request) Successful in 4m53s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (pull_request) Successful in 1m32s
WAI operation catalogue / the catalogue and every door · linux (pull_request) Successful in 4m33s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 3m32s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m59s
wai-fleet service / service · linux (pull_request) Successful in 2m5s
wai-ledger service / service · linux (pull_request) Successful in 1m33s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 4m16s
wai-key-proxy / proxy · linux (pull_request) Successful in 1m7s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 1m1s
WAI neural sink gates / model-free refusals of the neural sink (pull_request) Successful in 1m50s
wai-pack / wai-pack · linux (pull_request) Successful in 2m8s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 11m16s
wai-quantum WebGPU member / member · integrated GPU, Vulkan (framework-128gb) (pull_request) Successful in 8m17s
wai-ops-gpu integer operators on GPUs / builds · no adapter is not a pass (linux) (pull_request) Successful in 4m2s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (pull_request) Successful in 12m13s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 9m1s
wai-fleet service / service · macos (pull_request) Successful in 36s
wai-quantum WebGPU member / member · DX12 and Vulkan (cbio-windows) (pull_request) Successful in 13m6s
wai-ledger service / service · macos (pull_request) Successful in 1m8s
WAI examples / WAI examples · portable · linux (pull_request) Successful in 10m13s
wai-quantum docs, course and locales / the course and the command line's languages · linux (pull_request) Successful in 2m18s
wai-quantum docs, course and locales / the Studio's words in every locale · linux (pull_request) Successful in 8s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 10m40s
WAI examples / WAI examples · the archive, the readiness check, the browser and Apple · macos (pull_request) Successful in 9m42s
WAI JPEG-AI whole decode (pinned inputs, required) / fetch pinned JPEG-AI inputs → whole decode + pixel hash, required (pull_request) Successful in 11m39s
wai-quantum for JavaScript / javascript · linux (pull_request) Successful in 3m5s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 14m0s
wai-quantum WebGPU member / member · Metal (macos) (pull_request) Successful in 2m8s
wai-pack / wai-pack · macos (pull_request) Successful in 4m37s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 16m44s
wai-quantum studio and desktop / studio · browser (pull_request) Successful in 3m45s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (pull_request) Successful in 57s
wai-quantum WebGPU member / member · browser WebGPU (macos) (pull_request) Successful in 1m33s
wai-ops-gpu integer operators on GPUs / Metal GPU and browser WebGPU (macos) (pull_request) Successful in 7m46s
wai-quantum WebGPU member / member · software Vulkan (linux) (pull_request) Successful in 2m46s
wai-quantum studio and desktop / desktop app · macos (pull_request) Successful in 6m50s
wai-quantum WebGPU member / shader checks · browser build · wasip2 inertness · MSRV (pull_request) Successful in 4m10s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 22m30s
WAI mbt2018_mean integer decode (regenerated vectors) / regenerate mbt2018_mean integer vectors → Rust decode, required (pull_request) Successful in 5m40s
wai-quantum studio and desktop / desktop app · linux (pull_request) Successful in 6m24s
wai-quantum studio and desktop / desktop app · windows (pull_request) Successful in 10m59s
wai-web wasm32 / wai-web wasm32 build · corpus, menu and writers (pull_request) Successful in 5m57s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (pull_request) Successful in 9m10s
wai-quantum docs, course and locales / every command and snippet in the docs · linux (pull_request) Successful in 16m9s
wai-quantum WebGPU member / member · GPU, Vulkan (jetson-hub) (pull_request) Successful in 25m5s
WAI Studio / Studio in Chromium and Firefox · macos (pull_request) Successful in 7m37s
wai-quantum WebGPU member / member · GPU, Vulkan (thor) (pull_request) Successful in 25m12s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 23m18s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 12s
WAI conformance / each feature of the independent verifiers' steps compiles alone (pull_request) Successful in 15m57s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 35s
WAI Studio / Studio in WebKit · linux (pull_request) Successful in 10m44s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 2m3s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (pull_request) Successful in 11m36s
WAI examples / WAI examples · android-app · linux-arm64 (pull_request) Successful in 7m25s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 3m3s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 16m37s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 36s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 1m43s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 9m21s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (pull_request) Successful in 3m24s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 1h22m6s
sec: RUSTSEC-2026-0429, a zip archive that aborts the process on extract
`ZipArchive::extract` sizes a symlink target's buffer from the size the
archive declares, so a 112-byte archive aborts the process. Fixed in zip 9.
The audit went red on every open PR when the advisory was published.

Two lockfiles move:

  wai-quantum-desktop: tauri-plugin-updater 2.14.0 -> 2.14.1. Its source is
    unchanged; it asks for zip 9. This was the only copy that runs `extract`
    in a shipped program, on Windows, on an update package whose signature
    has already been checked.
  wai-rs: c2pa 0.90 -> 0.91, behind `c2pa_emit`. This also takes
    elliptic-curve 0.14, so the workspace holds no crypto-bigint 0.5.

Four copies stay, each read at its call site and allowed at its exact
version: open_board_format and candle-core never call `extract`, and
webrtc-sys-build calls it in a build script, on its own download. The build
crate's next releases change which libwebrtc it downloads, so it moves with
livekit.

The comment over the older zip entries said nothing in this tree extracts an
archive. It was wrong: the updater did, and the build script does. It now
says what is true.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-02 06:31:12 -05:00

360 lines
18 KiB
Python
Executable file

#!/usr/bin/env python3
#
# check-advisories.py — no workspace carries a security advisory nobody has read.
#
# This tree holds many workspaces, and a vulnerable crate usually arrives
# transitively, in a lockfile nobody edited, for a reason nobody chose. Without
# a gate the backlog only grows, and it grew: a sweep in October 2026 found 142
# vulnerability instances across 47 lockfiles, among them a WASM runtime with a
# filesystem sandbox escape in the one component whose job is confinement.
# Nothing had failed, because nothing was looking.
#
# For every tracked Cargo.lock this runs
#
# cargo audit --file <lock> --json
#
# and fails on any advisory that is not in ALLOW below. It reads the advisory
# database and the lockfile; it builds nothing.
#
# ALLOW is the whole point. An advisory may be left in place — upstream has no
# fixed release, or the vulnerable call is unreachable here — but only in
# writing, with the reason next to the id. Two rules keep that honest:
#
# * an advisory that is not listed fails the run, so a new one has to be
# looked at rather than accumulated;
# * a listed advisory that no longer appears anywhere ALSO fails the run, so
# an entry cannot outlive the exposure it excused. An allow list that is
# never pruned stops describing the tree and starts hiding it.
#
# Warnings (unmaintained, unsound, yanked) are counted and printed but do not
# fail the run, and the count is printed so the number stays visible rather
# than unknown. For `unmaintained` that is easy: it is advice about a crate's
# future, not a reachable defect.
#
# `unsound` and `yanked` deserve a straighter answer, because neither is mere
# maintenance advice and both have been worth acting on:
#
# * unsound-with-a-fix is a real defect a bump would close — one of them was
# a use-after-free in `LruCache::pop()`. It is not gated **today** only
# because every current instance is either pinned by a dependent
# (zstd-safe under wasmtime 49's module cache, which nothing here
# configures) or needs a semver-major move of a transitive crate (lru,
# rand, glib). A gate that is red for things nobody can act on teaches
# people to bypass it, which costs more than it catches. Gate it once
# those can actually move.
# * yanked means the author withdrew a release, which can happen to a
# version that was correct when it was chosen: `crypto-bigint 0.7.6` was
# picked here to fix RUSTSEC-2026-0387 and was yanked afterwards, along
# with every 0.7.x below 0.7.9. Gating it would turn every PR red for a
# decision made outside the tree, so it stays a warning — but it is worth
# re-reading after any advisory work, which is how that one was caught.
#
# Both are therefore a standing review item, not an oversight.
#
# check-advisories.py every tracked Cargo.lock, less SKIP
# check-advisories.py DIR ... exactly these directories
# check-advisories.py --from-json F... audit JSON already captured (self-test)
#
# Exit 0: every advisory found is allowed, and every allowed advisory was found.
# Exit 1: otherwise — including when a lockfile could not be audited at all,
# which is reported under its own heading so an error is never read as "clean".
import json
import os
import subprocess
import sys
# Advisories left in place on purpose. Each needs the reason, and each must
# still be reported by at least one lockfile or this check fails.
#
# An entry is either
#
# "RUSTSEC-...": "why it can be left"
#
# which excuses the advisory at ANY version — correct when the reason does not
# depend on the version, such as a call we never make — or
#
# "RUSTSEC-...": {"why": "...", "only": ("crate 1.2.3", ...)}
#
# which excuses it ONLY at those exact crate/version pairs. Use the second form
# whenever the reason is "this particular line cannot move yet", because the
# first form is version-blind: it would go on excusing the advisory if a
# patched line later regressed to an unpatched release. Every pair named in
# `only` must also still appear, or the entry is stale and the run fails.
ALLOW = {
# ── Unreachable here ────────────────────────────────────────────────────
# All three are faults in `extract()`: SUID/SGID bits preserved from the
# archive, symlinks whose targets escape the destination, and permissions
# applied through those symlinks. Nothing that runs in this tree extracts an
# archive, and no crate here calls zip directly. The one call left in the
# graph is a build script's, on its own download; RUSTSEC-2026-0429 below
# names it.
"RUSTSEC-2026-0367": "zip: extract-only; nothing here that runs extracts an archive",
"RUSTSEC-2026-0368": "zip: extract-only; nothing here that runs extracts an archive",
"RUSTSEC-2026-0369": "zip: extract-only; nothing here that runs extracts an archive",
"RUSTSEC-2025-0168": "zip: extract-only path canonicalization; nothing here that runs extracts",
# `ZipArchive::extract` sizes a symlink target's buffer from the size the
# archive declares, so a 112-byte archive aborts the process. Fixed in 9.0.0,
# which wai-rs and wai-quantum-desktop take. The copies left, by call site:
#
# aac-rs, 8.6.0: open_board_format 0.1.1 is its newest release and asks for
# ^8.6. It unpacks a board with `by_index` and `read_to_end`.
# jouleclaw-rs, 1.1.4, and openpay, 1.3.1: candle-core 0.8.4 reads npz and
# pth weights by name. burn 0.17 holds openpay's copy there.
# jouledesk-rs, 0.6.6: webrtc-sys-build 0.3.18 does call `extract`, in a
# build script, on the libwebrtc archive it downloads itself. The worst
# this does is abort a build. The crate's next releases change which
# libwebrtc it downloads, so it moves with livekit, not alone.
#
# Version-pinned, so a copy at any other version fails the run and is read.
"RUSTSEC-2026-0429": {
"why": "zip: extract-only abort; three copies never extract, the fourth is a build script on its own download",
"only": ("zip 8.6.0", "zip 1.1.4", "zip 1.3.1", "zip 0.6.6"),
},
# The Marvin attack recovers an RSA key from the timing of **private-key
# decryption** (PKCS#1 v1.5). There is no fixed release of `rsa` at all, so
# this one cannot be bumped away and the reason has to carry it. Both
# remaining copies were checked by call site, not by assumption:
#
# sandbox-rs: `sandbox-tee/src/amd_kds.rs` is the only direct user in the
# tree. It verifies AMD's KDS chain (ARK -> ASK -> VCEK, RSASSA-PSS /
# SHA-384) with `RsaPublicKey` + `pss::VerifyingKey` + `Verifier`.
# Public-key verification only; there is no decryption anywhere, and
# `RsaPrivateKey` appears solely inside `#[cfg(test)] mod tests`, which
# mints throwaway keys to build a synthetic chain.
# wai-rs: reaches it only through `c2pa`, which is an optional dependency
# behind the `c2pa_emit` feature and off by default, so a default build
# does not compile it. WAI's own signing is Ed25519 (`ed25519_dalek`),
# which uses neither `rsa` nor `crypto-bigint`.
#
# openpay's copy is gone: the dependency was declared and never used, so it
# was removed outright rather than excused.
"RUSTSEC-2023-0071": "rsa: no fixed release; verification only, no decryption, private keys test-only",
# ── Pinned by a dependency; needs an upstream release ──────────────────
# zstd's four October 2026 advisories (a stream's data leaking into the
# next on a reused context; Seekable::decompress past the end; uninitialised
# memory from a failed legacy-frame stream; the wasm allocator shim). Every
# direct use here moved to zstd 0.14.3 / zstd-safe 8.2.0 / zstd-sys 2.1.2.
# The one copy left is wasmtime 49's: wasmtime-internal-cache compresses
# compiled modules for wasmtime's on-disk cache, and sandbox-exec-wasm
# builds its engine from Config::new() with fuel metering only and never
# configures a cache, so none of zstd's code runs in the sandbox. Pinned to
# those exact versions: a direct use regressing to them is not excused.
"RUSTSEC-2026-0391": {
"why": "zstd 0.13.3 only in wasmtime 49's module cache, which the sandbox never configures",
"only": ("zstd 0.13.3",),
},
"RUSTSEC-2026-0398": {
"why": "zstd-safe 7.2.4 only under wasmtime 49's module cache, which the sandbox never configures",
"only": ("zstd-safe 7.2.4",),
},
"RUSTSEC-2026-0403": {
"why": "zstd-safe 7.2.4 only under wasmtime 49's module cache, which the sandbox never configures",
"only": ("zstd-safe 7.2.4",),
},
"RUSTSEC-2026-0410": {
"why": "zstd-sys 2.0.16 only under wasmtime 49's module cache, never configured, and native: the shim is wasm's",
"only": ("zstd-sys 2.0.16+zstd.1.5.7",),
},
# Secret-dependent branches in operations meant to be constant-time
# (subtraction, zero counting, division, Montgomery multiplication), so
# timing can leak intermediate arithmetic on a secret integer. Fixed in
# >=0.7.6, and smart-byte's 0.7.x copy is patched.
#
# Every remaining 0.5.5 copy arrives under `elliptic-curve` — it is the only
# dependent in all five lockfiles — which pins the 0.5 line, so reaching
# >=0.7.6 means moving the whole p256/elliptic-curve stack. That is an
# upgrade, not a bump, and this entry is that debt.
#
# What bounds it: a timing attack of this class recovers a key by observing
# many operations under the *same* secret, and none of our own signing runs
# through this crate. WAI, sandbox and jouleclaw sign with Ed25519
# (`ed25519_dalek` / curve25519-dalek, which does not use crypto-bigint),
# and wai-rs holds no 0.5.5 copy since `c2pa` 0.91 took elliptic-curve 0.14.
# So the debt is real but it is not sitting under a long-lived key of ours.
#
# Version-pinned on purpose: a 0.7.x copy must never be excused by it.
"RUSTSEC-2026-0387": {
"why": "crypto-bigint 0.5.5 pinned by elliptic-curve 0.13; our signing is Ed25519; 0.7.x is patched",
"only": ("crypto-bigint 0.5.5",),
},
# hpke-rs-libcrux pins `=0.7.0`, which pins libcrux-kem 0.0.9. Both are
# panics on short or malformed hybrid keys — denial of service, not memory
# corruption. Fixed by an openmls/hpke-rs release, not from here.
"RUSTSEC-2026-0330": "libcrux-kem: pinned by hpke-rs-libcrux =0.7.0; upstream",
"RUSTSEC-2026-0331": "libcrux-kem: pinned by hpke-rs-libcrux =0.7.0; upstream",
# quick-xml is 0.41 everywhere the version is ours to choose, which includes
# every call that actually parses: op-iso20022's codec binds the workspace
# 0.41. Two older copies remain in the graph, neither on a parse path.
# openpay resolves 0.37.5 through open-payments-iso20022 1.0.10, which
# declares quick-xml but never calls it — the crate ships serde-annotated
# ISO 20022 types (3 files, ~2.2k lines, no `quick_xml` reference), and
# the parsing is ours. Compiled, never executed.
# smart-byte resolves 0.39.4 through plist -> netdev -> netwatch -> iroh,
# which parses macOS plists, not counterparty XML.
# -0195 additionally needs `NsReader`, which appears nowhere in this tree.
"RUSTSEC-2026-0194": "quick-xml 0.37.5/0.39.4: not on a parse path; our parsing is on 0.41",
"RUSTSEC-2026-0195": "quick-xml 0.37.5/0.39.4: NsReader unused, and neither copy parses",
# h2 0.3.27 under jouleclaw and 0.4.14 under smart-byte, both transitive;
# the fix is a major move for the dependent.
"RUSTSEC-2026-0258": "h2: transitive; fixed version is a major move for the dependent",
# hickory-proto 0.25.2 under smart-byte via iroh. -0118 has no fixed
# release; -0119 needs 0.26, which iroh does not take yet.
"RUSTSEC-2026-0118": "hickory-proto: no fixed release; transitive under iroh",
"RUSTSEC-2026-0119": "hickory-proto: 0.26 not taken by iroh yet",
# pyo3 0.28.3 under eoc-rs. Both need 0.29, a breaking move for the binding.
"RUSTSEC-2026-0176": "pyo3: 0.29 is a breaking move for the binding",
"RUSTSEC-2026-0177": "pyo3: 0.29 is a breaking move for the binding",
}
# Lockfiles cargo does not read: a vendored crate's own lock, kept with the
# vendored source. Each must still exist, or the entry is stale.
SKIP = (
"jouledesk/jouledesk-rs/vendor/libwebrtc",
"joule-code/joule-code-rs/vendor/wtransport",
)
def discover():
root = subprocess.run(
["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True
).stdout.strip()
if not root:
sys.exit("check-advisories: not in a git checkout.")
os.chdir(root)
for d in SKIP:
if not os.path.isfile(os.path.join(d, "Cargo.lock")):
sys.exit(
f"check-advisories: the skip list names {d}, which holds no "
"Cargo.lock. Remove the entry."
)
out = subprocess.run(["git", "ls-files"], capture_output=True, text=True).stdout
locks = [
p
for p in out.splitlines()
if p == "Cargo.lock" or p.endswith("/Cargo.lock")
if os.path.dirname(p) not in SKIP
]
return sorted(locks)
def audit(lock):
"""Return (parsed_json, error_text). Exactly one is None."""
try:
p = subprocess.run(
["cargo", "audit", "--file", lock, "--json", "--color", "never"],
capture_output=True,
text=True,
timeout=600,
)
except FileNotFoundError:
return None, "cargo is not on PATH"
except subprocess.TimeoutExpired:
return None, "cargo audit timed out"
# cargo audit exits non-zero when it finds something, so the exit status is
# not the signal; absence of parseable JSON is.
try:
return json.loads(p.stdout), None
except json.JSONDecodeError:
tail = (p.stderr or p.stdout or "").strip().splitlines()
return None, "; ".join(tail[-3:]) or "no JSON on stdout"
def main():
args = sys.argv[1:]
from_json = False
if args and args[0] == "--from-json":
from_json, args = True, args[1:]
if from_json:
sources = [(f, None) for f in args]
elif args:
sources = [(os.path.join(d, "Cargo.lock"), None) for d in args]
else:
sources = [(l, None) for l in discover()]
if not sources:
sys.exit("check-advisories: found no Cargo.lock to check.")
found = {} # id -> [(crate, version, lock)]
unchecked = [] # (lock, why)
warn_count = 0
for lock, _ in sources:
if from_json:
try:
with open(lock) as fh:
data, err = json.load(fh), None
except Exception as e: # noqa: BLE001
data, err = None, str(e)
else:
data, err = audit(lock)
if data is None:
unchecked.append((lock, err))
continue
for v in (data.get("vulnerabilities") or {}).get("list") or []:
adv, pkg = v.get("advisory") or {}, v.get("package") or {}
found.setdefault(adv.get("id", "?"), []).append(
(pkg.get("name", "?"), pkg.get("version", "?"), lock)
)
for kind, items in (data.get("warnings") or {}).items():
warn_count += len(items or [])
def covered(adv, crate, ver):
entry = ALLOW.get(adv)
if entry is None:
return False
if isinstance(entry, str):
return True
return f"{crate} {ver}" in entry["only"]
unexpected = {}
for adv, occurrences in found.items():
loose = [o for o in occurrences if not covered(adv, o[0], o[1])]
if loose:
unexpected[adv] = loose
stale = [k for k in ALLOW if k not in found]
# A version-pinned entry is also stale when a pair it names is gone: the
# pin stops describing the tree the moment that line moves.
for adv, entry in ALLOW.items():
if adv in found and isinstance(entry, dict):
seen = {f"{c} {v}" for c, v, _ in found[adv]}
for pair in entry["only"]:
if pair not in seen:
stale.append(f"{adv} (no longer at {pair})")
if unexpected:
print("Advisories that are not in the allow list:\n")
for adv in sorted(unexpected):
for crate, ver, lock in unexpected[adv]:
print(f" {adv} {crate} {ver}")
print(f" {lock}")
print(
"\nFix each, or add it to ALLOW in scripts/check-advisories.py with the\n"
"reason it can be left — unreachable, dev-only, or pinned upstream."
)
if stale:
print("\nAllow-list entries that no longer match anything:\n")
for adv in sorted(stale):
entry = ALLOW.get(adv.split(" (")[0])
why = entry if isinstance(entry, str) else (entry or {}).get("why", "")
print(f" {adv} — {why}")
print(
"\nThe exposure is gone. Remove the entry, so the list keeps describing\n"
"the tree instead of hiding it."
)
if unchecked:
print("\nCould not be audited (this is not a verdict on the lockfile):\n")
for lock, why in unchecked:
print(f" {lock}: {why}")
if unexpected or stale or unchecked:
return 1
print(
f"check-advisories: {len(sources)} lockfiles, "
f"{len(found)} allowed advisories, {warn_count} warnings (not gated)."
)
return 0
if __name__ == "__main__":
sys.exit(main())