Issue: #1104 — Add dependency provenance checks
Workstream: Security Hardening
Status: Implemented
Dependency provenance verification answers the question: "Can we trust where this package came from?"
It goes beyond a vulnerability audit — it checks that each dependency:
- Was downloaded from a known, trusted registry (npmjs.com, crates.io).
- Carries an integrity hash (SRI for npm, SHA-256 checksum for Cargo) so the download can be verified.
- Uses a strong hash algorithm (sha512 for npm; sha256 for Cargo).
Provenance checks protect against supply-chain attacks where a malicious actor publishes a package to a shadow registry, tricks a developer into using a custom registry, or tampers with a downloaded tarball.
libs/security/
├── index.ts # Re-exports all public symbols
└── provenance/
├── interfaces.ts # Types: ProvenanceRecord, ProvenanceViolation, …
├── dependency-provenance-checker.service.ts # Main checker service
├── index.ts # Barrel export for the provenance sub-module
└── __tests__/
└── dependency-provenance-checker.spec.ts # 26 unit tests
Register DependencyProvenanceChecker in any NestJS module:
import { Module } from '@nestjs/common';
import { DependencyProvenanceChecker } from '@security/provenance';
@Module({
providers: [DependencyProvenanceChecker],
exports: [DependencyProvenanceChecker],
})
export class SecurityModule {}Then inject and call it:
@Injectable()
export class MySecurityService {
constructor(private readonly provenance: DependencyProvenanceChecker) {}
async checkDeps() {
const result = await this.provenance.checkNpmLockfile('pnpm-lock.yaml', {
failOnSeverity: 'high',
requireRepositoryUrl: false,
});
if (!result.passed) {
result.violations.forEach((v) => console.error(v.message));
throw new Error('Dependency provenance check failed');
}
}
}# Check pnpm lockfile (default: looks for pnpm-lock.yaml or package-lock.json)
pnpm run provenance:check
# Check explicit files with JSON output
ts-node scripts/check-dependency-provenance.ts \
--npm pnpm-lock.yaml \
--cargo Cargo.lock \
--fail-on high \
--json
# Skip internal workspace packages
ts-node scripts/check-dependency-provenance.ts \
--npm package-lock.json \
--exclude "@gasguard/engine,@gasguard/rules"| Code | Meaning |
|---|---|
0 |
All checks passed (no violations at or above the fail threshold) |
1 |
One or more violations at or above the fail threshold |
2 |
Usage error / lockfile not found |
All options are defined in ProvenanceCheckerConfig:
| Option | Type | Default | Description |
|---|---|---|---|
failOnSeverity |
'critical' | 'high' | 'medium' | 'low' | 'info' |
'high' |
Minimum severity that causes passed = false |
requireRepositoryUrl |
boolean |
false |
Flag packages without a source repository URL |
requireNpmAttestation |
boolean |
false |
Flag npm packages without a provenance attestation |
exclude |
string[] |
[] |
Package names to skip (e.g. internal monorepo packages) |
maxPackages |
number |
unlimited | Cap the number of packages checked per run |
| Severity | npm Example | Cargo Example |
|---|---|---|
critical |
Missing integrity field |
crates.io package missing checksum |
high |
Unknown registry host | Unrecognised source registry or malformed checksum |
medium |
Weak hash algorithm (sha1) | (reserved for future checks) |
low |
(reserved) | (reserved) |
info |
(reserved) | Path/workspace dependency (no verifiable source) |
Each violation includes a remediation message explaining how to fix it.
A dedicated dependency-provenance job runs in .github/workflows/ci.yml:
dependency-provenance:
name: Dependency Provenance Checks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run dependency provenance check (npm)
run: ts-node scripts/check-dependency-provenance.ts --npm pnpm-lock.yaml --fail-on high
continue-on-error: true
- name: Run dependency provenance check (Cargo)
run: ts-node scripts/check-dependency-provenance.ts --cargo Cargo.lock --fail-on high
continue-on-error: trueReports are uploaded as CI artifacts (provenance-reports) for audit trail purposes.
Note:
continue-on-error: trueis used initially so existing CI pipelines are not broken while the team triages any pre-existing violations. Set tofalseonce all violations are resolved.
| Check | Violation Severity | Description |
|---|---|---|
| Registry host allowlist | high |
resolved URL must be registry.npmjs.org, *.npmjs.org, or an allowlisted private registry |
| Integrity presence | critical |
Every package must have an integrity field |
| Integrity algorithm | medium |
Only sha512 is accepted; sha1 / sha384 are flagged |
| Check | Violation Severity | Description |
|---|---|---|
| Source presence | info |
Packages without a source field (path deps) are noted |
| Source allowlist | high |
Source must be crates.io-index or a github.com / gitlab.com git URL |
| Checksum presence | critical |
crates.io packages must have a checksum field |
| Checksum format | high |
Must be a valid 64-character hex string (SHA-256) |
The checker exposes verifyFileIntegrity(filePath, sriHash) for verifying downloaded
tarballs or build artifacts against their expected SRI hash:
const ok = checker.verifyFileIntegrity('./dist/gasguard.js', 'sha512-abc…');To add support for a new ecosystem (e.g. Python/pip):
- Add a new
Ecosystemvariant tointerfaces.ts. - Implement a
checkPipLockfile()method following the same pattern ascheckNpmLockfile(). - Add unit tests in
__tests__/dependency-provenance-checker.spec.ts. - Register the new check in
scripts/check-dependency-provenance.ts.
- SECURITY.md — Responsible disclosure process
- README.md § Privilege Boundaries — Trust boundary documentation
- CI workflow:
.github/workflows/ci.yml - Issue: MDTechLabs/GasGuard#1104