Add pre-install npm dependency guard

This commit is contained in:
2026-08-27 12:46:55 +01:00
parent 7574b31b8c
commit f28b9ee87d
22 changed files with 1321 additions and 137 deletions
+169 -1
View File
@@ -1,3 +1,171 @@
# dependency-guard
Supply-chain verification for npm dependency updates
`dependency-guard` is a zero-dependency Node.js 20+ CLI for reviewing npm dependency
updates before installation. It checks the lockfile, npm registry metadata, upstream
GitHub history, package contents, and npm's registry signatures, then writes a JSON
evidence report.
It never intentionally runs dependency lifecycle scripts. Installation uses
`npm ci --ignore-scripts`, and package inspection uses
`npm pack --dry-run --json --ignore-scripts`.
## Threat model
The guard is intended to catch:
- lock entries redirected away from the npm registry or missing cryptographic integrity;
- newly introduced transitive install scripts;
- unexpected direct-package lifecycle hooks;
- unsigned npm registry metadata;
- repository substitution and unexpected npm maintainers;
- provenance that names a different repository or a commit without a matching
upstream version tag;
- packages whose `gitHead` cannot be tied to an upstream version tag;
- unusually fresh releases and unexpectedly large package archives;
- failures reported by `npm audit signatures` or `npm audit --omit=dev`.
The lockfile and registry metadata are parsed before `npm ci` runs. Any pre-install
failure still produces the requested report.
## Limitations
- The CLI cannot prove that an official upstream GitHub account, npm maintainer,
release workflow, signing identity, or source repository itself is uncompromised.
- Presence of an npm registry signature is checked from metadata. Cryptographic
signature and Sigstore attestation verification is delegated to
`npm audit signatures` after the script-disabled install.
- Provenance validation checks the decoded SLSA statement's repository and requires
its source commit to match an upstream version tag. It does not implement Sigstore
cryptography independently.
- Upstream tag verification runs `git ls-remote` against public GitHub repositories.
Private upstream repositories require Git credentials configured outside the tool.
- Only GitHub upstream repositories are currently verifiable. `init` warns and does
not silently trust packages hosted elsewhere.
- `npm audit --omit=dev` follows npm's advisory database and may miss unknown issues.
This tool is one review gate, not a replacement for source review, least-privilege
CI credentials, lockfile review, or artifact isolation.
## Usage
No install is required from a checkout:
```sh
node bin/dependency-guard.js init \
--path ../my-project \
--output ../my-project/dependency-guard.policy.json
node bin/dependency-guard.js check \
--path ../my-project \
--policy ../my-project/dependency-guard.policy.json \
--report ../my-project/dependency-guard-report.json
```
When installed as a CLI, use `dependency-guard` in place of
`node bin/dependency-guard.js`.
`--policy` is optional and uses the defaults below. `--report` is required. Use
`--skip-install` only when package installation and both npm audits are performed by
a separate, equivalent CI step. Skipping installation reduces assurance.
`init` refuses to overwrite an existing output file. Review every generated trust
entry before committing the policy; generation is a starting point, not approval.
## Gitea action
The repository includes a composite `action.yml`. Pin actions to the reviewed full
commit SHA, never a branch or mutable tag:
```yaml
steps:
- uses: actions/checkout@<reviewed-40-character-commit-sha>
- uses: https://git.nocker.cloud/tony/dependency-guard@<reviewed-40-character-commit-sha>
with:
path: .
policy: dependency-guard.policy.json
report: dependency-guard-report.json
```
The action invokes `node "$GITHUB_ACTION_PATH/bin/dependency-guard.js"` directly, so
it does not install the action's own dependencies. Configure a Node.js 20+ runner.
For private upstream repositories, configure minimally scoped Git credentials on the
runner without placing them in the policy or report.
## Policy
```json
{
"allowInstallScripts": ["fsevents"],
"allowDirectLifecycleScripts": [],
"sourceEvidenceExceptions": {},
"trustedRepositories": {
"example-package": "https://github.com/example/example-package"
},
"trustedMaintainers": {
"example-package": ["expected-npm-user"]
},
"minimumReleaseAgeHours": 72,
"freshReleaseAction": "warn",
"maxTarballSizeBytes": 20971520,
"maxUnpackedSizeBytes": 104857600
}
```
- `allowInstallScripts` permits `hasInstallScript` on any matching lock entry. The
default contains only `fsevents`.
- `allowDirectLifecycleScripts` separately permits direct packages whose registry
manifest declares `preinstall`, `install`, or `postinstall`.
- `sourceEvidenceExceptions` permits only an exact package version to proceed when
source provenance or tag evidence is unavailable. Each entry requires a detailed
`reason` and an `expiresAt` ISO date, and produces a warning.
- `trustedRepositories` pins normalized GitHub repositories by package name.
- `trustedMaintainers` pins the complete npm maintainer set expected in exact-version
metadata. Additions and removals both fail the check.
- `minimumReleaseAgeHours` defines the freshness threshold.
- `freshReleaseAction` is `warn` by default and may be changed to `fail`.
- Archive limits apply to the values emitted by `npm pack --dry-run --json`.
Unknown policy fields and malformed values fail closed.
## Exceptions
Treat allowlists as reviewed security exceptions:
1. Confirm the exact package and locked version.
2. Inspect the lifecycle script and every executable or downloaded artifact it uses.
3. Confirm the repository, npm maintainers, release tag, and commit independently.
4. Prefer a package-specific exception over broad threshold changes.
5. Record the rationale and reviewer beside the policy change in the pull request.
6. Remove the exception when the package or dependency is removed.
Do not add a package to both lifecycle allowlists automatically. A transitive
`hasInstallScript` exception and a direct manifest-hook exception represent separate
trust decisions. Scripts remain disabled during the guard's own `npm ci`; an
allowlist permits review to pass but does not execute the script.
## Evidence report
The report contains:
- report version, timestamp, project path, status, and counts;
- lockfile version and number of registry package entries;
- exact direct package versions, repository, provenance path, release age,
maintainer names, archive sizes, failures, and warnings;
- each npm command, exit code, timeout state, and redacted/truncated output;
- aggregate actionable failures and warnings.
Reports are created with owner-only permissions where supported. They intentionally
exclude environment variables, authentication headers, registry response bodies,
attestation bundles, and GitHub response bodies. Command output is redacted for
common credential labels and URL user information, but reports should still be
handled as CI evidence rather than published indiscriminately.
## Development
```sh
npm test
npm run check
```
Tests use `node:test`, temporary fixtures, and injected HTTP/process implementations.
They do not access the live network.