# Dependency Supply Chain Review

> Use when you need to review dependencies, scripts, lockfiles, package provenance, and install-time risks.

- Skill: `fluxonlab/dependency-supply-chain-review` (Agent Skill)
- Install (CLI): `npx skillmds@latest add fluxonlab/dependency-supply-chain-review`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fluxonlab/dependency-supply-chain-review/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: FluxonLab (https://skillmd.com/u/fluxonlab)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/fluxonlab/dependency-supply-chain-review

---


# Dependency Supply Chain Review

## Purpose

Audit the project's package dependency graph for known CVEs, suspicious install-time scripts, lockfile integrity issues, typosquatting candidates, outdated packages with breaking changes, and transitive dependency risks. Every finding gets a severity, a concrete package reference, and a prioritized remediation step. The audit is read-only and non-destructive — no packages are installed, upgraded, or removed without explicit user approval.

A dependency review that only runs `npm audit` and lists the output is not sufficient. The goal is to assess exploitability in context (a server-side RCE in a browser-only bundle is different from one in a server process), to check install-time code execution, to verify lockfile integrity, and to identify structural supply-chain risks such as namespace confusion or unreviewed postinstall scripts.

## When to use

- `npm audit`, `yarn audit`, or `pip-audit` has flagged vulnerabilities and you need a structured remediation plan.
- A PR adds or upgrades packages and you want to verify supply-chain safety before merge.
- The project has not had a dependency review in more than 90 days.
- A postinstall or prepare script in a dependency is executing code at install time.
- The lockfile (`package-lock.json`, `yarn.lock`, `poetry.lock`, `pnpm-lock.yaml`) is missing or was recently deleted and regenerated.
- A security incident or CVE disclosure has named a package in the project's dependency tree.

## When not to use

- The project has no third-party dependencies (single-file scripts, standard-library only).
- You need to audit application runtime logic unrelated to packages.
- A dedicated security scanning tool (Snyk, Dependabot, Socket) has already triaged all findings and the task is to implement fixes — use a code-editing skill instead.

## Procedure

1. **Confirm lockfile presence and integrity.** Check that `package-lock.json` / `yarn.lock` / `pnpm-lock.yaml` / `poetry.lock` exists and is committed. A missing lockfile means installs are non-deterministic. Verify the lockfile was generated by the matching tool and version (check `lockfileVersion` in `package-lock.json` — v3 for npm 7+).

2. **Run the audit non-destructively.** Execute `npm audit --json` or `yarn audit --json` and capture the output to a temp file. Do not run `npm audit fix --force` without reviewing the proposed changes. For Python, run `pip-audit --format json`. For Go, run `govulncheck ./...`.

3. **Classify each CVE by exploitability in this project.** For every finding, determine:
   - Severity (critical/high/moderate/low) as reported by the registry.
   - Is the vulnerable code path reachable in this project? A server-side RCE in a package only imported in a browser bundle that never runs on the server is unexploitable in that context.
   - Is a non-breaking fix version available? Record whether the fix is a patch, minor, or major bump.

4. **Inspect install-time scripts.** For every dependency with `postinstall`, `prepare`, `preinstall`, or `install` scripts in its `package.json`, read what the script does. Flag any that download binaries, execute `curl`/`wget`, modify system files, or phone home. Legitimate packages like `esbuild` and `puppeteer` download platform-specific binaries — verify the download uses a checksum.

5. **Check for typosquatting candidates.** Compare recently added package names against their intended counterparts: `lodash` vs `l0dash`, `express` vs `expres`. Pay special attention to packages added in the last PR with few public downloads or a short publish history. Use the npm registry API to check download counts and creation dates.

6. **Audit transitive dependency pinning.** For CVEs in transitive (indirect) dependencies where a direct upgrade cannot resolve the issue, check whether `overrides` (npm 8+) or `resolutions` (yarn) can pin the safe transitive version. Document which CVEs are unresolvable without a major dependency change.

7. **Verify CI uses a reproducible install command.** `npm ci` fails when the lockfile is out of sync with `package.json`, ensuring reproducibility. `npm install` silently updates the lockfile. CI pipelines must use `npm ci` (or `yarn install --frozen-lockfile` / `pnpm install --frozen-lockfile`).

8. **Check for outdated major versions.** Run `npm outdated` and flag packages more than one major version behind, especially those with known end-of-life status (e.g., a runtime version past its LTS window, a framework version with no security patches).

9. **Review private registry configuration.** If `.npmrc` configures a private registry scope, confirm it is scoped to the organization's namespace (e.g., `@company/*`) and does not redirect all traffic through an untrusted registry. Unscoped private registry config enables dependency confusion attacks where a public package with the same name and a higher version takes precedence.

10. **Document approved exceptions.** For CVEs assessed as not exploitable in context, create or update an audit exception file (e.g., `npm-audit-exceptions.json` or `.auditignore`) with the CVE ID, the package, the reason for the exception, the reviewer's name, and the review date.

## Concrete checks

- [ ] Lockfile present, committed, and matches the package manager version (check `lockfileVersion` field).
- [ ] `npm audit` (or equivalent) returns zero critical or high findings, or all findings have documented exceptions with justification.
- [ ] No postinstall scripts download remote code, execute `curl`/`wget`, or modify paths outside the package directory.
- [ ] No package names match known typosquatting patterns (one character off from a popular package).
- [ ] Vulnerable transitive deps are addressed via `overrides`/`resolutions` where a direct upgrade cannot fix them.
- [ ] CI pipeline uses `npm ci` / `--frozen-lockfile` / `--immutable`, not `npm install`.
- [ ] `.npmrc` scopes any private registry to `@org-scope/*` only; no full-redirect to an untrusted registry.
- [ ] All packages with known EOL runtime or framework status have a documented upgrade timeline.
- [ ] `package.json` `dependencies` ranges are not `*` or `>=0.0.0` for production packages.
- [ ] Audit exceptions file exists with CVE ID, package, reason, reviewer, and date for each waived finding.
- [ ] `devDependencies` are not `require()`-d in application code that ships to production.

## Commands

```bash
# Detect package manager from lockfile
ls package-lock.json 2>/dev/null && echo "npm" || true
ls pnpm-lock.yaml    2>/dev/null && echo "pnpm" || true
ls yarn.lock         2>/dev/null && echo "yarn" || true
ls bun.lockb         2>/dev/null && echo "bun"  || true
ls poetry.lock       2>/dev/null && echo "poetry (python)" || true

# Non-destructive npm audit (capture JSON; do NOT run fix yet)
npm audit --json > /tmp/npm-audit.json 2>&1
cat /tmp/npm-audit.json | python3 -c "import json,sys; d=json.load(sys.stdin); print('critical:', d.get('metadata',{}).get('vulnerabilities',{}).get('critical',0), 'high:', d.get('metadata',{}).get('vulnerabilities',{}).get('high',0))"

# Python / pip
pip-audit --format json --output /tmp/pip-audit.json 2>/dev/null || echo "pip-audit not installed"

# Go
govulncheck ./... 2>/dev/null || echo "govulncheck not installed"

# Check lockfile version
node -e "const l=require('./package-lock.json'); console.log('lockfileVersion:', l.lockfileVersion)"

# Inspect all postinstall / prepare scripts in direct dependencies
node -e "
const pkg = require('./package.json');
const deps = {...(pkg.dependencies||{}), ...(pkg.devDependencies||{})};
Object.keys(deps).forEach(name => {
  try {
    const p = require('./node_modules/' + name + '/package.json');
    const s = p.scripts || {};
    const hooks = ['postinstall','preinstall','install','prepare'].filter(k => s[k]);
    if (hooks.length) console.log(name + '@' + p.version + ':', hooks.map(k => k + '=' + s[k]).join(' | '));
  } catch(e) {}
});
"

# Check for outdated packages (read-only)
npm outdated 2>/dev/null || true

# Check private registry config
cat .npmrc 2>/dev/null | grep -E "registry|scope" || echo "no .npmrc"

# Check whether CI uses npm ci or npm install
rg -n "npm install\b" .github/workflows/ .gitlab-ci.yml Makefile 2>/dev/null || true
rg -n "npm ci\b" .github/workflows/ .gitlab-ci.yml Makefile 2>/dev/null || true

# Check for wildcard or loose version ranges in production deps
node -e "
const pkg = require('./package.json');
Object.entries(pkg.dependencies||{}).forEach(([k,v]) => {
  if (v === '*' || v.startsWith('>=0') || v.startsWith('x')) console.log('LOOSE RANGE:', k, v);
});
"

# Check devDependencies used in production code (node_modules audit)
# List devDep names, then grep for require/import of those names in src/
node -e "const p=require('./package.json'); Object.keys(p.devDependencies||{}).slice(0,20).forEach(n=>console.log(n))" \
  | xargs -I{} sh -c 'rg -l "require.*{}\|from.*{}" src/ 2>/dev/null && echo "DEV IN PROD: {}" || true'
```

```bash
# Typosquatting quick check: compare package names against npm registry
# (manual step — check these with: npm view <suspected-package> time --json | head -5)
# Look for: very recent creation, <1000 weekly downloads, owner with no other packages

# Transitive CVE: identify the dependency chain for a specific package
npm ls <vulnerable-package> 2>/dev/null | head -20

# Check overrides/resolutions in package.json
node -e "const p=require('./package.json'); console.log(JSON.stringify(p.overrides||p.resolutions||{}, null,2))"

# Verify the lockfile is what was actually installed (hash check)
npm ci --dry-run 2>/dev/null | tail -5 || echo "dry-run not supported; run npm ci in CI only"
```

## Severity rubric

| Severity | Example |
|----------|---------|
| **Critical** | Remotely exploitable CVE (RCE, SQL injection) in a package reachable from a production request handler. Postinstall script downloads and executes arbitrary code. Confirmed typosquatting package installed. |
| **High** | XSS or path traversal CVE in a package handling untrusted user input. Lockfile deleted and regenerated without audit of version changes. `npm install` (not `npm ci`) used in CI, allowing lockfile drift. |
| **Medium** | ReDoS or DoS CVE in a package reachable from user input. Loose production version range (`^1.0.0` is fine; `>=1 <99` is not). devDependency found in production code. Outdated major version with known vulnerabilities in that major. |
| **Low** | Informational CVE not reachable in this project's code paths, with a documented exception. Outdated minor version with no known vulnerability. Missing audit exception documentation for a known, assessed CVE. |

## Common issues & anti-patterns

- **Deleted and regenerated lockfile**: dependency versions silently drift; every transitive version must be re-reviewed as if all packages were newly added.
- **`npm install` in CI**: the lockfile can mutate between CI runs; always use `npm ci`. A failing `npm ci` is not a problem to paper over — it signals a real inconsistency between `package.json` and the lockfile.
- **Unpinned `devDependencies` promoted to production**: a `require()` in application code that pulls a dev-only package will silently fail in a production Docker image built from `npm ci --omit=dev`.
- **`--legacy-peer-deps` flag in CI**: this flag silently installs incorrect peer versions. Its presence signals an outdated dependency graph that needs resolution, not suppression.
- **Scoped package namespace confusion**: if your org namespace `@acme` is not claimed on the public registry, an attacker can publish `@acme/internal-lib` publicly with a higher version and npm will prefer it (dependency confusion attack).
- **Binary download postinstall without checksum verification**: `puppeteer`, `cypress`, `esbuild`, and similar packages download platform binaries at install time. Verify the download script checks a hash against a bundled manifest — unchecked binary downloads are a supply chain risk.
- **Audit false negatives from bundling**: `npm audit` inspects `node_modules`, but if a vulnerable package is bundled into a client-side chunk via webpack/rollup and then dropped from `node_modules` (e.g., via tree-shaking), audit may not flag it. Check the final bundle's included packages separately.
- **Exception file without dates**: a "not exploitable" exception from 18 months ago may no longer be valid if the package's usage has changed. All exceptions must have a review date and a re-review interval.

## Required output

```
## Dependency Supply Chain Review

### Critical CVEs requiring immediate fix
| Package | Installed | Safe version | CVE | Exploitable in this project |
|---------|-----------|-------------|-----|----------------------------|

### High CVEs — fix before next release
| Package | Installed | Safe version | CVE | Notes |

### Suspicious install-time scripts
- package@version: script content summary, risk level, recommendation.

### Outdated packages (1+ major behind)
| Package | Current | Latest | EOL? | Upgrade priority |
|---------|---------|--------|------|-----------------|

### Lockfile status
- Present: yes/no. Generator: npm/yarn/pnpm. Version: X. CI uses reproducible install: yes/no.

### Private registry config
- .npmrc present: yes/no. Scoped to org namespace only: yes/no. Risk: none/low/high.

### Audit exceptions (assessed not exploitable)
| CVE | Package | Reason | Reviewer | Date |

### Recommended next commands
1. npm audit fix  # review diff before committing — run npm audit again after to confirm
2. ...

### Summary
- Total direct dependencies: N. Total audit findings: N (critical: N, high: N).
- Single highest-priority action: one sentence.
```

## Safety

- Run `npm audit --json` (read-only) only. Do not run `npm audit fix --force`, `npm install`, or any script that modifies `node_modules` or the lockfile without explicit user approval.
- Do not print package registry tokens or `.npmrc` auth values.
- Do not run postinstall scripts or execute any downloaded binary as part of the review.
- Do not approve or merge a PR that adds a package with an unreviewed postinstall script — surface it as a finding first.

