Repository navigation
fix(ci): scope the npm audit gate to production dependencies (#1166) - #1172
Conversation
.npmrc include=dev (added #428 for reproducible installs) overrides --omit in npm's config, so `npm audit --omit=dev` silently audited the dev tree. The dev-only braces advisory GHSA-vfj7-8cjw-p6xm (all versions <=3.0.3, no patched release; reached via eslint-config-next -> fast-glob -> micromatch) turned the Security Audit gate red on main. - audit script: explicit --include=prod replaces the .npmrc value so --omit=dev takes effect; production and optional deps stay audited (verified: a prod- or optional-installed vulnerable package is still reported) - security-audit.yaml: non-blocking dev-inclusive audit step keeps dev advisories visible without turning main red - package-audit.test.ts: regression guards for the flag pair, rationale note, and workflow wiring - SECURITY-ACCEPTED-RISKS.md: accept the dev-only braces advisory, correct the shipping-surface claims (main server image excludes it; -mcp image carries it as inert tooling), record the resolution
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: glm-5.3-flash (openai) — routed smart (risk match: linked_priority_p1) · pr-reviewer-action v3.2.0
Partial coverage: required-check coverage is incomplete — this review did not resolve every required check and must not be read as a complete pass.
Recommendation: Approve. This is a well-evidenced, narrowly scoped CI fix for the P1 Security Audit red-on-main (PR 1166). Every quantified claim in the PR body held under verification against the lockfile, the advisory database, the Dockerfile, and the authoritative CI results for the head commit. No blocking or major issues found.
Claims to Falsify — Results
- Claim 1 (advisory range
<=3.0.3, no patched release,audit fix --forceonly offers a breaking downgrade) — held: 7/7 items. The GitHub advisory API for GHSA-vfj7-8cjw-p6xm confirmsvulnerable_version_range: "<= 3.0.3"withfirst_patched_version: null(severity high, CVSS 7.5). The lockfile pinsbraces@3.0.3(package-lock.json:4967), i.e. the latest published version is installed and no override/upgrade can clear it. The issue PR 1166 log showsnpm audit fix --forcewould installeslint-config-next@14.2.35(a breaking downgrade), andeslint-config-nextis a devDependency (package.jsondevDependencies), so the "fix" stays inside the same dev-only chain. All citedSECURITY-ACCEPTED-RISKS.mdlines,package.json:67, and the test'spackageJsonPathwere verified in the diff/head files. - Claim 2 (
.npmrcsetsinclude=dev, intentional per PR 428) — held on all verifiable items. Direct reads of.npmrcwere blocked by the review tooling as a sensitive path, butgit_grepforinclude=dev|omit=devmatched two lines inside.npmrc(content redacted by the harness), repository history showsd6d4fb5 chore: fix npmrc dev dependency config (#428),.github/actions/setup-node/action.yml:17runsnpm ci --include=devexplicitly, and the before/after CI behavior (below) corroborates the include-overrides-omit mechanism. Workflow lines 18/26, theSECURITY-ACCEPTED-RISKS.mdreferences, the test'sreadJson/audit/workflow assertions, andpackage.json:67all verified. - Claim 3 (the unfixable advisory is dev-only, which is what exposed the latent gate bug) — held: 5/5 items. Every lockfile entry in the chain is marked
"dev": true:braces@3.0.3(:4967),micromatch@4.0.8(:8507),fast-glob@3.3.1(:6687),@next/eslint-plugin-next@16.3.8(:2063),eslint-config-next@16.3.8(:6135). The failing command in the issue log wasnpm audit --omit=devreporting the dev chain — direct evidence the omit flag was being cancelled. - Claim 4 (new non-blocking dev-inclusive audit step; blocking gate retained) — held: 12/12 items. The head
.github/workflows/security-audit.yamlretains- run: npm run auditas a blocking step (line 20) and adds the dev-inclusive pass withcontinue-on-error: true(lines 24–26). The workflow triggers on push and PR tomain. A repo-wide grep fornpm run audit|run: npm auditfound no other callers, so no sibling call site was missed. - Claim 5 (accepted-risks doc updated with precise shipping-surface claims) — held: 9/9 items. The new
Dev-Only Advisoriessection, the updated gate command in the Mitigation line, and the new Previous Resolution History row are all present. The shipping-surface claims check out against the actualDockerfile: theprod-depsstage runsnpm ci --omit=devand copies onlypackage.json/package-lock.json(no.npmrc), and therunnerstage copies onlyprod-depsnode_modules — so the main server image is prod-only. The-mcpcaveat is also accurate: themcpstage copies thedepsstage tree (npm ci, dev included) andimage.yamlbuildstarget: mcpas a separate image; a grep ofsrc/found nomicromatch/braces/fast-globusage, supporting the "inert tooling, never invoked" claim.
Overall: 5/5 claims held; no counterexample found. The strongest end-to-end evidence is the authoritative CI status for head f11da3d: the npm audit check succeeded with the dev tree installed (.npmrc include=dev + npm ci --include=dev in setup-node), which empirically confirms the --include=prod CLI flag cancels the .npmrc include value and lets --omit=dev take effect on this exact lockfile — the same configuration that failed twice on main before the fix.
Change-by-Change Findings
package.json— theauditscript gains--include=prodbetween--omit=devand--audit-level=high; the"//".auditrationale follows the existing"//".overridesconvention (mirrored assertion style in the new test). No dependency or lockfile changes, as claimed..github/workflows/security-audit.yaml— blocking gate unchanged in behavior except for the scoping fix delivered via the script; the new informational dev-inclusive step is correctlycontinue-on-error: trueand reuses the same fetch-retry flags. (Pre-existing, unchanged: the workflow runs plainnpm ciafter the setup-node composite action already rannpm ci --include=dev— a redundant second install, but harmless and out of scope for this PR.)package-audit.test.ts— new root-level test mirroringpackage-overrides.test.tsconventions (@vitest-environment node,process.cwd()root resolution,readJsonhelper). It guards the flag pair, the"//".auditrationale, and the workflow wiring (blocking gate present, dev pass non-blocking). Deliberately not pinning.npmrcis documented and reasonable given the gate is now robust to.npmrcat any level and CI installs pass--include=devexplicitly.SECURITY-ACCEPTED-RISKS.md— accurate and precise; notably honest about the-mcpimage carrying the dev chain as inert tooling.
Standards Compliance
- Lint/typecheck must pass (AGENTS.md): satisfied — the authoritative CI results for head
f11da3dshow Lint, Typecheck, Tests, and Build all succeeded. "//"rationale convention: the new"//".auditentry matches the established"//".overridespattern and is regression-guarded.- No secrets committed; no
.env/build output: the diff touches only CI config, docs, package metadata, and a config-reading test. No token handling is introduced. - Trivy advisory-only
continue-on-errorprecedent (AGENTS.md): the new non-blocking dev-audit step is consistent with the repo's existing pattern of advisory-only security visibility steps.
Linked Issue Fit
Issue PR 1166 (priority/p1, type/bug) reports Security Audit failing repeatedly on main because npm run audit (npm audit --omit=dev ...) reports 5 high on the dev-only braces chain. The PR addresses exactly this: it makes --omit=dev effective (root cause: .npmrc include=dev winning over omit), keeps dev-only advisories visible via a non-blocking pass, documents the acceptance, and adds regression tests. The issue's evidence (failing command, chain, npm audit fix --force suggestion) matches the PR's root-cause analysis line for line. The PR body also references the duplicate filing PR 1162; I could not fetch it (GitHub API transport unavailable in this environment), but it does not affect the fix's correctness. No acceptance criteria in the issue are left unmet.
Tool Harness Findings
29 tool calls ran. Key results: head security-audit.yaml, setup-node/action.yml, package.json, Dockerfile, package-overrides.test.ts, and vitest.config.ts were read successfully and match the PR's descriptions; the GitHub advisory API confirmed the <=3.0.3 range with no patched version; lockfile spot-checks confirmed all five chain entries are dev: true; a src/ grep found no production use of the vulnerable chain; image.yaml confirmed the runner and mcp image targets. Failures: .npmrc reads/greps were blocked or redacted as a sensitive path (grep still matched include|omit patterns on two lines), gh_api endpoints returned "platform transport not configured" (issue PR 1162, npm config docs), and a raw.githubusercontent fetch was redirect-blocked. None of these failures undermines the verdict; see Unknowns.
Unknowns or Needs Verification
.npmrcexact content could not be read directly (tooling blocks it as sensitive). The include-overrides-omit mechanism is nonetheless corroborated three ways: grep pattern matches inside the file, the PR 428 history, and — decisively — the before/after CI behavior on this lockfile (failing--omit=devrun in the issue log vs. passingnpm auditcheck on head with the new flag pair).- npm's documented include/omit precedence could not be fetched from npm docs (transport unavailable). The empirical CI result on the head commit is sufficient evidence for this repository's configuration; no action needed.
- The PR body's scratch-project experiments (prod-installed and optionalDependency
bracesstill reported under--include=prod) are outside what this corpus can reproduce; they are consistent with the observed CI outcome and the documented npm semantics, and the invariant they protect (prod + optional stay in scope) is the status quo the diff does not weaken.
Requirement trace
3 of 3 requirement(s) not fully traced to enforcement and a test:
req-e589bfe1ad7c— unverifiable (no valid enforcement location)req-ad902b2a08da— unverifiable (no valid enforcement location)req-7c2ada56c159— unverifiable (no valid enforcement location)
Approval withheld: this review's coverage is incomplete — required-check coverage or the tool-loop investigation did not finish, so it is publishing as an advisory comment rather than an approval.
Problem
Security Auditfailed twice onmain(b0863de9,34f01623):npm run auditreports 5 high onbraces(GHSA-vfj7-8cjw-p6xm) througheslint-config-next → @next/eslint-plugin-next → fast-glob → micromatch → braces. Every lockfile entry in that chain isdev:true. The advisory range is<=3.0.3and the latest publishedbracesis 3.0.3 — there is no patched release, so no override/upgrade can fix it, andnpm audit fix --force's only "fix" is a breakingeslint-config-nextdowngrade that lands in the same fast-glob chain anyway.Closes #1166 (and the identical earlier filing #1162).
Root cause (empirically verified, npm 11.19 / node 24, this lockfile)
.npmrcsetsinclude=dev(intentional, #428:npm cimust install devDeps even with a globalomit=dev). In npm's config theincludeset wins overomit, so the gate's--omit=devwas silently ignored and the audit covered the dev tree the whole time. The unfixable dev-only advisory is what exposed it.Evidence (full-tree runs):
npm audit --omit=dev→ reports the braces chain, exit 1npm audit --omit=dev --include=prod→found 0 vulnerabilities, exit 0bracesas a real production dependency (devDeps installed,.npmrc include=dev) →--include=prodstill reports it, exit 1bracesas an optionalDependency →--include=prodstill reports it, exit 1npm ls --omit=dev --include=prod braces→ emptyFix
package.jsonauditscript: explicit CLI--include=prodreplaces the.npmrcinclude value (same-key CLI precedence), so--omit=devfinally takes effect. Adjacent pair--omit=dev --include=prodis the empirically validated combination.package.json"//".audit: rationale comment (matches repo convention for"//".overrides)..github/workflows/security-audit.yaml: new non-blocking (continue-on-error: true) dev-inclusive audit step so future dev-only advisories stay visible without turningmainred.package-audit.test.ts(new, root, mirrorspackage-overrides.test.tsconventions): regression-guards the flag pair, the"//".auditrationale, and the workflow wiring (blocking gate present, dev pass non-blocking). Deliberately does not pin.npmrc— the gate is robust to it at any level, and CI installs pass--include=devexplicitly via the setup-node action.SECURITY-ACCEPTED-RISKS.md: accepts the dev-onlybracesadvisory with precise shipping-surface claims, updates the documented gate command, records the resolution in Previous Resolution History.No dependency or lockfile changes.
Invariants this change must keep — and how each is kept
--omit=dev --include=prodpair: optional deps are controlled byomit=optional, not the include set, and stay in scope — verified empirically (a vulnerableoptionalDependencyis still reported, exit 1; a vulnerable production dep is still reported, exit 1).package-audit.test.ts(flag-pair + wiring assertions) and kept visible by the new non-blocking dev-inclusive step.npm ciin this repo keeps installing devDependencies (the whole point of.npmrc include=dev, chore: fix npmrc dev dependency config #428; lint/typecheck/test depend on it). Kept by leaving.npmrcand.github/actions/setup-node/action.yml(npm ci --include=dev) untouched — the fix changes only the audit invocation, not installs.Dockerfileuntouched: theprod-depsstage copies onlypackage.json/package-lock.json(no.npmrc), so itsnpm ci --omit=devwas never affected; therunnerstage copies onlyprod-depsnode_modules.overridesremediations (postcss/sharp/deepmerge-ts/mysql2) and their rationale comment stay intact. Kept byte-identical;package-overrides.test.tspasses.npm run auditand the dev pass stays advisory. Kept and asserted inpackage-audit.test.ts(npm run auditpresent;continue-on-error: trueprecedesnpm audit --include=dev).Other code paths enforcing the same / adjacent rules (audited)
.github/actions/setup-node/action.yml(npm ci --include=dev, prisma generate) — install path for every other workflow; untouched; audit flags do not affect installs..npmrc(include=dev) — install-time only; untouched; the audit script is now immune to it (CLI--includereplaces the project value for the audit process only).Dockerfiledeps/prod-deps/mcpstages — untouched;prod-deps's--omit=devgenuinely omits devDeps because the stage has no.npmrc. Note (documented, not claimed away): the separately published-mcpimage runs off thedepsstage tree (entrypointtsx, a devDependency), so the dev lint chain is present there as inert tooling — flagged as follow-up, out of scope here.package-overrides.test.ts— same file-family conventions; passes unchanged.pr-smoke.yaml— consumes setup-node only; unaffected.npm audit/omit=dev/include=devfinds no other consumers.Verification
npm run auditexit 0; lint clean;tsc --noEmitclean; full vitest suite 3789 passed / 16 skipped / 2 todo (165 files), including the 6 new assertions.Related
#1169 (for the duplicate #1162) independently diagnosed the same root cause and chose
--include=optional. It overlaps this diff (audit script + accepted-risks doc), so the two PRs are alternatives, not stackable — whichever merges first, the other will need a rebase or should be closed in favor. This PR additionally adds the regression test, the non-blocking dev-visibility step, and corrects the shipping-surface claim for the-mcpimage.