Appearance
Dependency vulnerability policy
Three-layer scanning posture for npm + GitHub Actions dependencies. Layered defense, multi-source evidence for SOC 2 + EDE Phase 3 reviews.
Source: ENG-286 audit finding L10 / ENG-332.
Three layers
Layer 1 — Dependabot (continuous, automatic PRs)
.github/dependabot.yml monitors:
npm(root) — runtime + dev deps for the Next.js app. Weekly check, max 5 open PRs.npm(docs) — VitePress docs build. Weekly check, max 3 open PRs.github-actions— workflow action versions. Weekly check, max 5 open PRs.
Dependabot opens upgrade PRs as advisories drop or as deps fall behind their major-minor windows. Each PR runs through the standard PR CI (including Layer 2 audit job below).
Triage cadence: review Dependabot PRs at least weekly. Merge security-flagged upgrades quickly when they're clean (passing CI + small surface). Hold non-security upgrades for the regular merge queue.
Layer 2 — PR-time web-production-closure audit
audit-prod-deps job in build-check.yml runs scripts/audit/web-prod-vuln-scan.ts on every PR + push to main.
Why a script and not npm audit --omit=dev --audit-level=high. That plain command was correct when this repo was a single Next.js app. Since apps/mobile (Expo / React Native, ENG-394) joined the npm workspace, it stopped being correct:
- The mobile app's build tooling (
metro,@expo/*,xcode,image-size, …) is declared asdependenciesof the mobile package, so a rootnpm audit --omit=devcounts it as "production" even though none of it runs on the web service. The gate went permanently red on React Native tooling CVEs with zero web attack surface (from ~2026-07-31, ~4 weeks unbroken). npm audit --workspace=apps/webdoes not fix this — npm audits the hoisted install-tree metadata, not the workspace's strict production closure, so it still over-reports hoisted packages (js-yaml,brace-expansion, …) whose realnpm ls --workspace=apps/web --omit=devclosure is empty.
The script instead intersects npm audit findings with the actual apps/web production closure (npm ls --workspace=apps/web --omit=dev --all). It fails the PR only on a high/critical CVE in a package that genuinely ships in the web production runtime, and prints the mobile / dev-tooling findings informationally (never silently dropped). Self-maintaining — no static allowlist to rot: if a package later enters the web prod closure it starts blocking automatically; a package only in mobile/dev tooling never blocks the web gate.
Enforcement status (honest current state). This job is not currently in branch-protection required-status-checks. It was silently dropped from the required set when it went permanently red on the monorepo noise above (the doc previously claimed it was required — that drift is what this policy update corrects). The 7 required checks today are: the four static guards, plus TypeScript strict, Next.js production build, and Docs (VitePress) build. Follow-up: once this fix has been stably green on main, re-add Vulnerability scan (production deps) to required-status-checks so the deploy-blocking guarantee is real again (a branch-protection change — requires founder approval).
Layer 3 — Accepted-finding policy (documented exceptions)
Since Layer 2 became closure-scoped, dev/build/mobile-only findings are excluded structurally (they're not in the apps/web production closure), not by a hand-maintained allowlist. The web-prod-vuln-scan.ts output prints them under "OUTSIDE the web-prod closure" so they stay visible — but they never block the web gate. That covers the entire @hubspot/cli dev-tooling chain (vite, tmp, …) and the entire apps/mobile Expo / React Native chain (metro, @expo/*, js-yaml, brace-expansion, image-size, …).
An accepted finding is now only needed for a CVE that IS in the apps/web production closure but cannot yet be bumped. Current accepted web-prod findings: none.
Recently resolved (kept here for the audit trail):
next(high, 5 advisories) — SSRF in rewrites, image-optimization DoS, Server-Action DoS, cache confusion, Server Function endpoint disclosure. Resolved 2026-08 by bumpingapps/webtonext@^16.3.3.postcss(high) — source-map arbitrary-file-read +</style>XSS chain (GHSA-qx2v-qp2m-jg93 et al.). Resolved:next@16.3.3pullspostcss@8.5.23.sharp <0.35.0(high) — inherited libvips CVEs (CVE-2026-33327/33328/35590/35591). Resolved:next@16.3.3pullssharp@0.35.4.nanoid <3.3.18(high) — infinite loop on zero size. Resolved vianpm update nanoid(floats to3.3.18within postcss's existing^3.3.11range — no override needed).dompurify <=3.4.12(moderate) — viaposthog-js. Resolved vianpm update dompurify→3.4.14.
Accepted findings are NOT a permission to ignore real CVEs. They're a documented exception with concrete production-attack-surface rationale, and require founder review. Quarterly review: re-evaluate any accepted entries, drop those no longer present, and confirm the closure-scoping still cleanly separates web-prod from tooling.
What to do when an audit finding lands
audit-prod-deps job fails on a PR
A new high-or-critical CVE landed in a production-runtime dep. Options:
- Wait for a fix. Sometimes the advisory drops before the fix release. If the affected version isn't deployable yet (no patch), check whether the affected code path is exercised in production. Most CVEs are conditional on specific feature use.
- Upgrade to a fixed version. Bump the affected dep's version in
package.json, re-runnpm install, push the lockfile change. The Dependabot PR may already exist. - Add an exception (last resort). Document the rationale here under "Accepted findings" and reference the specific advisory ID + production attack-surface analysis. This requires founder review — exceptions are a one-way ratchet.
Dependabot opens a security-flagged PR
Standard review process:
- Read the advisory linked in the PR body
- Verify the CI checks pass (typecheck + build + audit)
- For low-risk upgrades (patch versions, security-only): merge after CI green
- For breaking changes: read the changelog, run extended smoke tests, then merge
A new dev-dep CVE appears that doesn't fit the accepted list
Re-evaluate. If genuinely dev-only and not in our prod bundle, add to the accepted list above with rationale. If it touches production indirectly (e.g., a vite plugin that processes user-supplied content at build time), treat as a Layer 2 failure and resolve via upgrade.
Why this multi-layer approach (vs npm audit on every deploy)
The ENG-286 audit's original recommendation was to add npm audit --audit-level=high to deploy workflows. Reviewed under 2026-05-14 velocity lens and reframed:
- Verified state at audit time: 19 vulnerabilities including 11 high-severity, all transitive via
@hubspot/clidev tooling - Shipping
npm audit --audit-level=highliteral would have blocked every deploy on dev-dep noise - Better pattern: scope to
--omit=dev(prod-runtime only) + run at PR time (not deploy time) so deploys never get unexpectedly blocked + complement with Dependabot for continuous monitoring
This achieves the audit's intent (catch prod-runtime CVEs) without operational friction.
Monorepo update (2026-08, ENG-394 fallout)
When apps/mobile joined the workspace, --omit=dev alone stopped isolating "web production runtime" (the mobile app's Expo/RN build tooling is declared as dependencies, so it counted as prod). The gate was red for ~4 weeks straight and was quietly dropped from required-status-checks. Fixed by making the gate closure-scoped (web-prod-vuln-scan.ts, Layer 2) rather than trusting --omit=dev at the workspace root. The intent is unchanged — catch high/critical CVEs in what actually ships to the web service — but it's now correct in a multi-app workspace.
Follow-up: mobile audit lane
apps/mobile deps are currently only surfaced as informational output of the web scan (so they're visible, not silently dropped). When the mobile app moves toward a shippable build, add a dedicated audit-mobile-deps lane scoped to the apps/mobile production closure, with its own threat model (an app-store binary, not a request-time server). Tracked as a follow-up; not blocking the web deploy gate.
SOC 2 / EDE Phase 3 evidence story
For auditors asking "do you scan dependencies for vulnerabilities?":
- Yes — Dependabot continuous monitoring (.github/dependabot.yml)
- Yes, with a closure-scoped PR-time gate —
audit-prod-depsjob runsweb-prod-vuln-scan.tson every PR + push. Currently reported (not yet a required check) pending the re-add to branch protection noted in Layer 2; the follow-up restores the hard block. - Yes, with documented policy — this doc, including accepted-finding rationale
Evidence ID hooks (for future SOC 2 vendor wiring):
EVID-DEP-001—.github/dependabot.ymlEVID-DEP-002—.github/workflows/build-check.ymlaudit-prod-deps jobEVID-DEP-003— this policy docEVID-DEP-004— quarterly accepted-findings re-review (link to most recent review)
Cross-references
- ENG-286 — audit doc
- ENG-332 — this work
- ENG-284 — established
build-check.ymlworkflow that this layer extends - GitHub Dependabot docs — https://docs.github.com/en/code-security/dependabot