# Dependency Vulnerability Remediation

> Remediate Dependabot / npm advisory vulnerabilities in this Yarn-3 monorepo with a low-risk, prefer-relock-over-resolution strategy, then produce a per-dependency security audit (vulnerability status, license, prod/dev + consumer reachability, blast radius, and supply-chain provenance/publisher-continuity). Use when asked to patch security alerts, bump vulnerable dependencies, assess dependency blast radius, or do a supply-chain review.

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

---


# Dependency vulnerability remediation & supply-chain audit

A repeatable process for closing dependency vulnerabilities here with minimal blast radius,
and for producing the audit artifacts that make the PR easy to approve with confidence.

Environment: **Yarn 3 (berry), `nodeLinker: node-modules`**, workspaces under `packages/*`,
published packages are the non-`private` `@cardano-sdk/*`. Generated docs go to `typedoc/`;
`docs/` holds tracked documentation (security audits live in `docs/security/`).

## Principles (in priority order)

1. **In-range relock > manifest bump > resolution.** If a package's existing semver range
   already permits the patched version, just relock — it's semver-safe by definition and needs
   no manifest change. Resolutions are a **last resort**, used only when an upstream pins a
   vulnerable version exactly and no shippable release reaches the fix.
2. **Prioritise by consumer impact.** What ships in the production closure of published
   packages matters most; dev/build-only deps are real supply-chain surface but rank lower.
3. **Defer, don't force.** Major parent-bumps, scoped multi-major resolutions, and
   no-upstream-fix packages go to tracked issues with reasoning — never jammed into a low-risk patch.
4. **Validate every batch** with `yarn build` (all workspaces) + targeted suites for packages
   whose runtime deps moved.
5. **Never trust a "fix" blindly.** Check provenance — a deprecated package republished under a
   new version (e.g. vm2) or a publisher-account change is a supply-chain signal, not a green light.

## Process

### 1. Triage alerts by severity
```bash
gh api repos/<org>/<repo>/dependabot/alerts -X GET -f state=open -f severity=critical --paginate \
| jq -r 'group_by(.dependency.package.name)[] | "\(.[0].dependency.package.name)\t[\(.[0].dependency.scope)]\tx\(length)\tpatched>=\([.[].security_vulnerability.first_patched_version.identifier//"none"]|unique|max)\tvuln: \([.[].security_vulnerability.vulnerable_version_range]|unique|join(" ; "))"'
```
Repeat per severity. Note packages with `patched=none` (no upstream fix → defer/monitor).

### 2. Classify each package
For each vulnerable package, read the lockfile entries and the requesting ranges:
```bash
node -e 'const fs=require("fs");for(const b of fs.readFileSync("yarn.lock","utf8").split("\n\n")){const k=b.split("\n").find(l=>l&&!l.startsWith("#")&&!l.startsWith(" "));const v=b.split("\n").find(l=>l.trim().startsWith("version:"));if(k&&v&&/^"?PKG@/.test(k))console.log(v.trim(),"<-",k)}' # replace PKG
```
- Range already permits patch → **relock** (Step 3).
- First-party direct dep → **bump the manifest range**.
- Upstream pins vulnerable version exactly, no release reaches fix → **resolution** (last resort).
- Only fix is a major bump of a transitive parent, or no fix exists → **defer to an issue**.

### 3. In-range relock (the workhorse — no manifest change)
`yarn up` only targets workspace-declared deps. For transitives, drop their lockfile blocks and
reinstall; Yarn re-resolves each to the highest version its existing ranges allow:
```bash
node -e '
const fs=require("fs");const targets=["pkg-a","pkg-b"]; // packages to relock
const esc=s=>s.replace(/[.*+?^${}()|[\]\\\/]/g,"\\$&");
const kept=fs.readFileSync("yarn.lock","utf8").split("\n\n").filter(b=>{
  const f=b.split("\n").find(l=>l.trim()&&!l.trim().startsWith("#"))||"";
  return !targets.some(t=>new RegExp("(^|, )\"?"+esc(t)+"@npm:").test(f));});
fs.writeFileSync("yarn.lock",kept.join("\n\n"));'
YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install
```
Then verify each resolved version clears its advisory range (use the `semver` package, required
from the repo root so it resolves). Multi-major packages relock each major line independently;
old-major lines with no backported fix simply won't move — those are the defer cases.

### 4. Resolution (last resort, documented)
Add to root `package.json` `resolutions` only when justified, and open an issue to remove it once
the proper direct bump becomes possible. Example justification: `protobufjs` — `@trezor/protobuf`
pins it exactly and only the latest `@trezor/connect` reaches the patched line (a heavy bump
deferred), so a `"protobufjs": "<patched>"` resolution is the low-risk same-day fix.

### 5. Validate
```bash
yarn build                                            # all workspaces (type-level)
yarn workspace @cardano-sdk/<pkg> test                # packages whose runtime deps moved
```
A bump that breaks (or can't be validated for) a consumer-facing runtime package is **not**
low-risk → defer it (e.g. `axios`, whose HttpProvider tests need real sockets — push to CI).

### 6. Commit per severity batch, then create deferral issues
One commit per severity tier. For deferred work, open issues grouped by owning direct dependency
(e.g. "bump the Express stack", "bump artillery to drop vm2"), and a tracking issue for
no-upstream-fix packages (monitor/risk-accept).

## Audit artifacts (`docs/security/dependency-vulnerability-audit-<date>.md`)

### Changed-dependency set
Diff the lockfile vs the base branch to get the exact set being updated:
```bash
git show master:yarn.lock > /tmp/base.lock
# parse both into name -> sorted version set, report names whose set differs
```

### Production / consumer reachability (BFS over `dependencies`-only edges)
A package is consumer-facing iff reachable via production edges from a **non-private** workspace's
`dependencies`. Walk `node_modules` with `require.resolve(<dep>/package.json, {paths:[fromDir]})`,
following only `dependencies`+`optionalDependencies`. Compare prod-closure vs dev-inclusive closure
to split prod vs dev-only impact.

### Blast radius — one dependency tree per sensitivity tier
Assign packages to sensitivity tiers (Tier 1 = keys/crypto/consensus/tx: `crypto`, `key-management`,
`core`, `tx-construction`, `input-selection`, `governance`; Tier 2 = wallet/hardware/dApp; Tier 3 =
services/data; Tier 4 = utils/dev). Render **one Mermaid tree per tier**: edges from each updated
security-relevant dependency to the tier's packages that pull it in production. Fan-out = blast
radius; fan-in = a package's exposure; green node = untouched. The headline is usually that Tier 1
is nearly untouched.

### Supply-chain provenance & publisher continuity
```bash
npm audit signatures            # registry signature + SLSA provenance coverage (works under node-modules)
```
Per changed dependency, from the npm registry:
- **Provenance:** `npm view <pkg>@<newver> --json | jq '.dist.attestations.provenance'` (SLSA via OIDC).
- **Publisher:** `_npmUser` is a **string** `"name <email>"` in the full manifest — parse it; or query
  `npm view <pkg>@<ver> _npmUser.name`. Compare publisher of the **new** vs **base** version.
- **Classify changes:** publisher → CI/OIDC bot = benign provenance adoption; human→human = surface
  for review (annotate known governance transitions, e.g. jshttp/Express-TC; flag anonymous/new
  accounts or single-version hijacks as high risk).

Columns per dependency in the audit table: version (base→new), vuln status (CLOSED/PARTIAL/OPEN),
scope (direct|transitive · prod|dev · consumer:yes/no), license, provenance/publisher.

### Cross-reference external vulnerability databases
Don't rely on a single source. Cross-check the GitHub Advisory DB (Dependabot) + `npm audit` against:
- **OSV.dev** — aggregates GHSA, the npm registry, GitLab, etc. Free, no auth, accepts package+version
  (often returns *more* records than Dependabot — vm2@3.9.18 returned 31):
  ```bash
  curl -s -X POST https://api.osv.dev/v1/query -H 'Content-Type: application/json' \
    -d '{"package":{"ecosystem":"npm","name":"<pkg>"},"version":"<ver>"}' | jq -r '[.vulns[].id]'
  # or scan the whole lockfile:  osv-scanner --lockfile=yarn.lock
  ```
- **CISA KEV** (actively-exploited catalogue) — prioritisation signal; intersect your CVEs with it.
  If an open CVE is in KEV, it is being exploited in the wild → do not defer:
  ```bash
  curl -s https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json | jq -r '[.vulnerabilities[].cveID]'
  ```
- **NVD** (CVE/CVSS authority; API rate-limited), **Snyk DB** (often earliest), **Sonatype OSS Index**,
  **EPSS** (exploit-likelihood score), **Trivy** / **Grype** (lockfile scanners with their own aggregated DBs).

Record the cross-reference in the audit (e.g. "0 of N open CVEs appear in CISA KEV; OSV corroborates").

## Gotchas

- **`docs/` is the TypeDoc output** (gitignored, Pages-published, wiped each build) unless moved —
  tracked docs must not live there. Here the output was relocated to `typedoc/` so `docs/` is tracked.
- **BSD awk lacks `gensub`** — use Node for lockfile parsing, not awk.
- **`semver` / per-package scripts** must run from the repo root so `require("semver")` resolves.
- **vm2 lesson:** a deprecated package that reappears un-deprecated with a new release cadence is a
  supply-chain red flag — eliminate it (bump the parent), don't pin to the suspicious line.
- **Relock dedupe can regress** a shared transitive to an older copy (e.g. dropped a patched
  `serialize-javascript` 6.0.2 for 6.0.0) — always diff the changed set vs base and check for
  unintended downgrades.

## Reference
A worked example of all of the above: `docs/security/dependency-vulnerability-audit-2026-06-19.md`
and PR #1709, with deferral issues #1701–#1708.

