Patch open Dependabot security advisories on vanguard
Use this playbook when the user asks to address open security advisories or unpin/pin dependencies on getsentry/vanguard. The flow has been validated end-to-end on this repo and bakes in the gotchas that broke previous Dependabot PRs.
Step 0: Inventory before touching anything
Always present a written list and get explicit confirmation before changing files. The user expects to see scope before any commit happens.
# 1. Open Dependabot alerts (the source of truth for "what's vulnerable")
gh api /repos/getsentry/vanguard/dependabot/alerts --paginate \
-q '.[] | select(.state == "open") | {
number, severity: .security_advisory.severity,
package: .dependency.package.name,
summary: .security_advisory.summary,
ghsa: .security_advisory.ghsa_id,
vulnerable_range: .security_vulnerability.vulnerable_version_range,
patched: .security_vulnerability.first_patched_version.identifier
}'
# 2. Open PRs that may already address them
gh pr list --state open --limit 50 \
--json number,title,headRefName,statusCheckRollup,mergeable
# 3. For each vulnerable package, find who pulls it in
pnpm why <package>
Heads-up: the dependabot/alerts API only lists advisories Dependabot has already published for the repo. The Dependency Review GitHub Action that runs on every PR (.github/workflows/dependency-review.yml) can flag fresh advisories with fail-on-severity: high before they appear in the API. After pushing the PR, run gh pr checks <PR> and inspect any dependency-review failure log — it has the format package.json » <pkg>@<version> – <summary> (high severity) ↪ <GHSA-URL>. If found, fold it into the same PR.
For each vulnerable package, classify:
- Direct dep (listed in
package.json) → bump inpackage.json. - Transitive dep (only in lockfile) → fix via
pnpm.overrides. Vanguard's advisories are almost always transitive.
Step 1: Confirm scope with the user
Report back with:
- Table of open advisories: package, current, patched, parent (from
pnpm why), existing PR (if any). - List of open PRs touching deps with current CI status.
- Count of unpinned versions in
package.json(grep -cE '"\^|"~' package.json). - Proposed plan (single PR with all fixes + pinning) + risks (major-jump overrides like
uuidv8 → v11).
Do not proceed without a "go".
Step 2: Branch + edit package.json
git checkout -b chore/security-pin-deps
Pin every direct dependency
Strip every ^ and ~ from dependencies, devDependencies, and pnpm.overrides. Pin to the version currently installed (from pnpm-lock.yaml), not to latest.
Get the installed versions in one shot:
pnpm list --depth 0 --json | jq -r '.[0]
| (.dependencies // {}), (.devDependencies // {})
| to_entries[] | "\(.key)\t\(.value.version)"' | sort
Use the resulting versions verbatim. Common discrepancies: @types/node resolves higher than declared, so pin to the actual resolved version.
Add pnpm.overrides for each transitive advisory
In package.json, under pnpm.overrides, add an entry for each vulnerable transitive dep, set to the patched version.
"pnpm": {
"overrides": {
"form-data": "4.0.4",
"axios": "1.15.0",
"rollup": "4.60.2",
"qs": "6.15.2",
"ws": "8.20.1",
"uuid": "11.1.1",
"brace-expansion@5": "5.0.6" // scoped — see gotcha below
}
}
🚨 Gotcha: scope range-restricted overrides
A global override forces every transitive instance to that version. If the package has multiple major lines in the tree and older majors are still in active use by other deps, a global pin will break them.
Concrete example from this repo: A global "brace-expansion": "5.0.6" override breaks minimatch@9, because minimatch@9 calls brace_expansion.default() — a default export that v5 removed. This is the exact reason Dependabot PRs for brace-expansion keep failing CI on this repo.
Rule: if the vulnerability range is scoped to one major line (e.g. >= 5.0.0, < 5.0.6), scope the override too:
"brace-expansion@5": "5.0.6" // only affects v5 consumers
pnpm supports the <name>@<major> syntax in overrides keys.
When to apply scoping (decision table):
| Situation | Override syntax |
|---|---|
| Vuln spans the entire installed range, no API breakage | "pkg": "x.y.z" (global) |
| Vuln is in one major line only, older majors coexist in tree | "pkg@<major>": "x.y.z" (scoped) |
| Patched version drops APIs older deps still use | scoped override + verify with pnpm typecheck |
🚨 Gotcha: major-jump overrides
When the patched version crosses a major (e.g. uuid 8.3.2 → 11.1.1), verify the upgrade is API-compatible with the actual consumer:
find node_modules/.pnpm/<consumer>* -name "*.js" | xargs grep -l "<package>" | head -5
grep -n "<package>" <file> # inspect actual usage
For uuid on vanguard, the only runtime caller is remix-auth-oauth2 using uuid.v4(), which is API-stable across v8/v9/v11. Safe.
Step 3: Install + verify lockfile
pnpm install
Confirm patched versions are in place and old ones are gone:
for pkg in <vuln-package-1> <vuln-package-2>; do
echo "=== $pkg ==="
grep -E "^\s+${pkg}@" pnpm-lock.yaml | sort -u
done
If a vulnerable version still appears: your override is wrong (probably needs scoping, or a different parent dep is requesting an explicit different version).
Step 4: Run the full verification suite
# Local Postgres is required for tests
docker-compose up -d
pnpm typecheck # tsc -b after react-router typegen
pnpm test # migrates test DB + runs Vitest (163 tests expected to pass)
pnpm build # react-router build
pnpm exec vp check # oxlint + oxfmt
Vanguard-specific notes:
pnpm testrequires the local postgres container; start it withdocker-compose up -dfirst.vp checkwill flag any unrelated unstaged files in the working tree (e.g. a strayindex.htmlat the repo root). Inspect what it flagged — if it's not in your diff, it's not your problem.- A
typecheckfailure mentioningbrace_expansion_1.default is not a functionmeans you have the unscopedbrace-expansionoverride — switch tobrace-expansion@5.
Step 5: Close superseded PRs + commit + PR
If existing Dependabot PRs (e.g. lockfile-only bumps of ws, brace-expansion) are now redundant:
gh pr close <PR-number> --comment "Superseded by #<our-PR>, which applies the fix via pnpm.overrides alongside the other open advisories."
Commit with the commit skill — single subject under 72 chars, body that lists each advisory with its GHSA ID, notes any scoped overrides, and lists what was verified.
git add package.json pnpm-lock.yaml
git commit -m "chore(deps): patch <N> medium-severity advisories and pin versions" \
-m "<detailed body>"
git push -u origin chore/security-pin-deps
gh pr create --base main --title "<same subject>" --body "<body with advisory table + verification + supersedes list>"
Output checklist
Before declaring done, confirm:
- Every open Dependabot alert in the report has a corresponding override or direct bump.
- No
^or~remains inpackage.json(grep -E '"\^|"~' package.jsonreturns nothing). -
pnpm typecheckexits 0. -
pnpm testshows all tests passing. -
pnpm buildexits 0. -
vp checkflags only files unrelated to the diff. - Lockfile no longer contains the vulnerable versions.
- Superseded Dependabot PRs are closed with a comment.
- Commit body lists every GHSA ID patched.
Reference: vanguard's tech context
- Package manager: pnpm 10.x (enforced via
preinstallhook). - Lockfile:
pnpm-lock.yaml(committed). Always update viapnpm install, never edit by hand. - Test DB: local Postgres at
localhost:5432viadocker-compose.yml. - CI:
.github/workflows/ci.ymlrunsvp check,typecheck, and Vitest with a service-container postgres. - Dependabot config:
.github/dependabot.yml(groups not configured — alerts arrive one PR at a time, which is why bundling them ourselves is the right move).
Source: getsentry/vanguard — distributed by TomeVault.