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 production-dependency audit (deploy-blocking)
audit-prod-deps job in build-check.yml:
npm audit --omit=dev --audit-level=highRuns on every PR. Fails the PR if ANY high or critical CVE is detected in production-runtime dependencies (the ones that ship in the Docker image and execute at request time). Dev-only deps (build tooling, test frameworks, linters) are intentionally excluded — see Layer 3 policy below.
This job is part of branch-protection required-status-checks. PRs cannot merge with prod-dep CVEs.
Layer 3 — Accepted-finding policy (documented exceptions)
npm audit (without --omit=dev) reports a non-zero baseline of findings from dev/build-time deps that don't run in production:
vite(high) — transitive via@hubspot/cli. Vite is a dev-only build tool; we don't ship Vite in our production bundle. The CVEs (path traversal in dev server, dev-server WebSocket arbitrary file read) only apply when runningviteas a dev server, which we don't do.@hubspot/cliis a developer-only tool for managing HubSpot integrations.tmp(low) — transitive via@hubspot/serverless-dev-runtime. Same dev-tooling-only reasoning.- Other
@hubspot/clitransitives — accepted on the same basis. postcss <8.5.10(moderate) — transitive vianext. Advisory GHSA-qx2v-qp2m-jg93: XSS via unescaped</style>in CSS Stringify Output.npm audit fix --forcewould downgrade Next.js to 9.3.3 (breaking change, no upgrade path). Accepted until Next.js bumps its bundled postcss. Below high+ threshold so the PR audit gate doesn't block on it. Re-check on every Next.js patch upgrade.
Accepted findings are NOT a permission to ignore real CVEs. They're a documented exception with concrete production-attack-surface rationale. Quarterly review (or whenever @hubspot/cli releases a fix): re-evaluate the accepted list, drop any that are no longer present, and verify nothing new has crept in that doesn't fit the dev-only exception.
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.
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 deploy-blocking gate —
audit-prod-depsPR-time job - 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