Supply-chain audit design

Verification principles behind the committed supply-chain audit test

The supply-chain audit proves the repository’s dependency controls from committed files alone. This page records the design principles that keep the audit itself trustworthy: an audit that can be fooled (or that passes when it checks nothing) is worse than none, because green then vouches for an unverified state. The principles come from adversarial review rounds against this audit and its Docsy predecessor, each round hunting inputs that violate a control yet pass the test.

For which controls exist and why, see Supply-chain security; for what to do when the audit fails on your PR, see the audit section of the dependency docs.

Principles

Each principle answers a way a verifier can lie; adversarial review found concrete instances of most of them in earlier drafts.

  1. Prove from committed files alone. The audit reads the lock, manifests, .npmrc, and netlify.toml (never the network or the installed tree), so it is fast, offline, and can’t be swayed by the state it is meant to vet.
  2. Allowlist whole shapes; don’t denylist patterns. Every denylist regular expression over a config format eventually met a valid spelling it didn’t anticipate (quoted, dotted, and inline-table TOML keys all bypassed an env-key denylist). Pinning the entire reviewed shape (exact key sets, exact values) is stronger and usually shorter.
  3. Parse; don’t line-scan. A format’s parser defines its semantics. A line regular expression mismodels them silently: a context table it doesn’t recognize still means something to Netlify.
  4. Exact pins; no prefix or flag matching. Prefix matching accepts an appended && npm install ... rider on a script the audit trusts by name.
  5. Fail closed on absence. Every counting check carries a floor assertion, so an empty input can’t pass vacuously; entries missing an expected field fail rather than being skipped.
  6. Bind to the identity npm trusts. npm derives a package’s identity from its resolved registry URL, not from the lock key or version field, so the audit binds all three together; checking only the fields npm distrusts green-lights a mismatch npm would act on.
  7. One home per invariant. A list asserted in two files drifts; the audit imports shared values from their owning module (the Hugo installer’s env-override names come from the rebuild helper), and that module’s unit test pins the content.
  8. Red-first. A new check is trusted only after a deliberately broken input has made it fail; every closure in the audit’s history was proven red before its green counted. A false green is worse than red.
  9. Assertions name the expected condition and, for routine fires, the fix: the allowScripts assertion names the version a dependency bump must move the entry to, so the failure message is the remediation.
  10. State the scope boundary. Surfaces the audit deliberately does not cover (workflow files, the theme’s own install, build-half scripts) are named in the audit and the docs, so absent coverage is never mistaken for verified coverage.

Principles in the audit

One exemplar per principle; the test file is the authoritative inventory of assertions.

PrincipleExemplar in the audit
Committed files aloneEvery input is read from the checkout; the suite runs with no network
Allowlist whole shapesnetlify.toml: top-level tables, build keys, and env key sets are deepEqualed
Parse, don’t line-scannetlify.toml is parsed with smol-toml before anything is asserted
Exact pinsThe install-closure scripts are compared with assert.equal, never match
Fail closed on absenceregistryPackages > 0 floors; a version-less lock entry fails the IOC check
Identity npm trustsEach registry entry’s resolved URL must name its own package and version
One home per invariantUNSAFE_HUGO_ENV is imported from rebuild-hugo-extended.mjs
Red-firstEach hardening commit’s PR notes the broken input that first made it fail
Assertions name the fixallowScripts covers hugo-extended at its locked version X
Stated scope boundaryThe audit’s header comment names the excluded surfaces