dependency-cve-gate
Use when a dependency-CVE gate fails a pull request, when adding or renewing an entry in an osv-scanner ignore policy, or when designing, reviewing or porting such a gate. Triggers include "CVE gate",
SKILL.mdplugins/react-frontend-sdlc/skills/dependency-cve-gate/SKILL.md
GitHubDependency CVE gate
Profile keys consumed
ci.providerci.workflows
Overview
The blocking scan is differential: it compares the base branch's lockfile against the head's and fails only on advisories the change itself introduces. A separate advisory run reports the whole backlog into a tracking issue without failing. The design keeps the gate meaningful instead of turning every unrelated pull request red.
When to use
- A pull request fails on an advisory and the dependency is one it added or bumped.
- Writing, renewing or removing an ignore entry.
- The gate exits 127 on a config the author believes is valid.
- Reviewing a proposal to switch the gate to an absolute (whole-tree) baseline.
- Porting the gate into a repository that has none.
- Not for: license policy, lockfile-provenance checks, or application-code SAST.
Applicability by repository shape
- React SPA shape (feature modules under the source root, a bootable app, an aggregate CI target): no — no dependency-CVE gate today.
- Next.js app shape (routed pages, no aggregate duplication gate): yes — a host-only blocking
scan target (it provisions the pinned scanner into
./bin) plus a separate advisory-census target, policy inconfig/osv-scanner.toml, logic inscripts/ci/osv-ignores.ts,scripts/ci/write-effective-osv-config.ts,scripts/ci/osv-report.tsandscripts/ci/check-osv-report.ts, wired by the dependency-CVE workflow listed underci.workflowson the provider named byci.provider. - Component-library shape (Storybook-first, no bootable app, published package): no — no dependency-CVE gate today.
Neither scan target has a logical key in the profile. In the Next.js shape they are:
make lint-vulns # blocking differential scan (host-only)
make scan-vulns-census # advisory run over the whole backlog, non-blocking
# wired by .github/workflows/osv-scanner.yml
Why differential, not absolute
An absolute baseline fails on the existing backlog from day one, and new advisories are published against untouched code continuously — so unrelated pull requests keep going red and reviewers learn to click past a security check. Differential scanning points the author at the risk they actually control: their own dependency change.
The ignore policy
One construct is permitted, [[IgnoredVulns]], and each entry must carry three fields: the advisory
id exactly as the scanner reports it, a reason saying why it is accepted and what unblocks the
real fix, and an ignoreUntil re-triage date written as a bare TOML local date. Every other table
and every top-level key is rejected outright, because a package-level override with ignore = true
silences findings with no reason and no expiry, and a config-loading key would pull in policy this
check never sees.
Mechanics that matter when editing the policy:
- The date is validated as a real calendar date, not merely a date-shaped string. Comparison is lexicographic, so an impossible month or day would sort after every real date and become an allowance that never expires.
- An expired entry fails the gate. Upgrade the dependency; do not push the date out to stay green.
- The header match tolerates the whitespace TOML permits inside the brackets, so a spaced-out table header cannot slip an unvalidated entry past the reader.
- Quoted, literal and bare values are all read and unquoted, so an empty reason cannot satisfy the "must have a reason" rule and two spellings of one id cannot defeat duplicate detection.
- A reason may contain a double quote; values are escaped when the effective policy is rendered, because emitting one raw produces invalid TOML and the scanner exits 127 on a valid config.
- Run the gate locally before proposing an entry. The reader fails closed, so one malformed value reddens the whole gate rather than degrading quietly, and the reason has to read to a reviewer.
Bypasses the design closes
The blocking scan runs under the intersection of the base ref's ignores and the working tree's:
membership is decided by the base, while the content of each surviving entry — its reason and its
ignoreUntil — is taken from the working tree, so a shortened expiry or an edited reason takes
effect immediately. That single rule closes two otherwise-real bypasses: adding a vulnerable
dependency together with its own ignore, and removing an ignore that a newly added dependency also
relies on. Land the ignore first, then the dependency.
Extending an expiry is not closed by that rule, and the doc should not claim it is: because the
expiry read is the working tree's, pushing an ignoreUntil further out on an id the base ref
already ignores takes effect on the very pull request that does it. Pinning the date to the base ref
instead would close it, but it would also freeze the useful direction — a reviewer could no longer
shorten an expiry or retire an entry early without a base-branch round trip — so the gate keeps the
working-tree date and leaves extension to review. Treat an ignoreUntil that moves later as a
change requiring the same scrutiny as a new entry: it needs a reason that says what unblocks the
real fix, and the upgrade is still the answer.
Two more are closed elsewhere: a malformed report is distinguished from a clean one (a clean scan
emits a results array, so a bare empty object is an error, not zero findings), and the workflow sets
pipefail before piping the scan through tee, since the default shell would otherwise exit with
tee's status and the gate would fail open.
Common mistakes
- Silencing the pre-existing backlog with ignore entries — that is what the advisory census is for.
- Adding an entry in the same change as the dependency and expecting it to apply — the base ref has to carry it first.
- Comparing dates as strings without validating them as calendar dates — parse the value as a real calendar date before any lexicographic comparison.
- Reading the intersection rule as blocking a pushed-out expiry — it does not; review the later
ignoreUntilas if it were a brand-new entry. - Letting the pass/fail decision live in the shell wrapper instead of a unit-tested script.
Porting checklist
Reproduce all four pieces or the gate is decorative: the differential base/head diff, the intersection of ignore policies, the three mandatory fields with expiry enforcement, and the strict one-construct config reader.