Add pre-install npm dependency guard
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user