| name | dependency-vulnerability-remediation |
| description | 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. |
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)
- 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.
- 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.
- 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.
- Validate every batch with
yarn build (all workspaces) + targeted suites for packages
whose runtime deps moved.
- 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
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:
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)}'
- 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:
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
yarn build
yarn workspace @cardano-sdk/<pkg> test
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:
git show master:yarn.lock > /tmp/base.lock
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
npm audit signatures
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):
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]'
- 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:
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.