Skip to content

Install gate: warn-only npm audit second opinion#107

Merged
juangaitanv merged 1 commit into
install-vuln-gatefrom
ivg/u6-npm-audit
Jun 11, 2026
Merged

Install gate: warn-only npm audit second opinion#107
juangaitanv merged 1 commit into
install-vuln-gatefrom
ivg/u6-npm-audit

Conversation

@juangaitanv

Copy link
Copy Markdown
Contributor

Summary

Unit 6 of the install-vuln-gate dogfood frictions: run npm audit as a concurrent, warn-only second opinion during the gated corgea npm install flow. npm's own signal comes first but never decides anything — the vuln-api verdict alone blocks.

  • resolve_tree now returns TreeResolution { packages, audit: Option<mpsc::Receiver<AuditSummary>> } (pip: None). After the lockfile parse, the npm resolver moves its dry-run temp dir into a detached thread (spawn_audit) that runs npm audit --json --package-lock-only with a 5s deadline (kill on timeout). Stdout is parsed regardless of exit code — audit exits 1 when it finds advisories, which is the success case. Stdout goes through a file, not a pipe, so the deadline poll can't deadlock.
  • run_tree_pass collects the summary via recv_timeout(2s) only after verdict_pool, so audit and verdicts truly overlap. Any failure (spawn error, timeout, unparsable output, disconnect) is a silent skip.
  • TreeReport::Full gains audit: Option<AuditSummary { total, critical, high, moderate, low, info, top }>. should_block_install never consults it — exit codes are unchanged in both directions (covered by tests).
  • Output: stderr note only when total > 0note: npm audit reports N advisories (M high/critical) — supplementary signal, not blocking; --json carries an npm_audit object (or null) in the tree arm.
  • Kill switch: CORGEA_NO_NPM_AUDIT=1 skips the audit subprocess entirely. Documented in the SKILL.md overrides sentence.

Testing

  • ./harness check green (clippy -D warnings, fmt, 263 tests).
  • New hermetic e2e suite tests/cli_npm_audit.rs (7 tests): note-without-blocking, clean-report silence, JSON object shape, env kill switch, broken-audit silent skip, hung-audit bounded by the 2s collect window, and audit-never-unblocks-a-vulnerable-verdict.
  • Unit tests for the report parser (counts, top capped at 5 severest-first, missing total summed, garbage rejected).
  • Live staging smoke (minimist@0.0.8 in a scratch project): stderr shows note: npm audit reports 1 advisories (1 high/critical) — supplementary signal, not blocking; exit code stayed the verdict's block. With CORGEA_NO_NPM_AUDIT=1 the note disappears.

Notes

  • The audit runs with --package-lock-only because the throwaway work dir holds only manifests plus the generated lockfile, never a node_modules.
  • Staging caveat: from the smoke environment the staging vuln-api was unreachable over HTTPS (reqwest send error), so the named target came back unverifiable → fail-closed block rather than a vulnerable block. The audit-note and kill-switch assertions were unaffected; the stub tests are the source of truth per the recipe.

resolve_tree now returns TreeResolution{packages, audit}: for npm, the
dry-run temp dir moves into a detached thread that runs `npm audit --json
--package-lock-only` (5s deadline, kill on timeout; stdout parsed
regardless of exit code — audit exits 1 when it finds advisories).
run_tree_pass collects the summary via recv_timeout(2s) after the
verdict pool so the two overlap; any failure is a silent skip.

The signal is supplementary only: should_block_install never consults
it. When total>0 a note prints to stderr; --json carries an `npm_audit`
object (or null) in the tree arm. CORGEA_NO_NPM_AUDIT=1 disables.
Comment thread src/precheck/tree.rs
let stdout_path = work.join("corgea-npm-audit.json");
let stdout_file = std::fs::File::create(&stdout_path).ok()?;
let mut child = Command::new(npm)
.args(["audit", "--json", "--package-lock-only"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the audit subprocess ignores npm CLI configuration from the install command. The lockfile resolver above runs npm install with .args(install_args), so --registry, --userconfig, --scope:registry, and auth-related flags can affect the resolved lockfile. This audit invocation is always just npm audit --json --package-lock-only, so a command like corgea npm install --registry https://registry.company @scope/pkg can resolve against the private registry but then run audit against the user's default/public registry. Impact: private package metadata can be sent to the wrong registry, or the second-opinion audit silently disappears because it cannot authenticate to the intended registry. Fix by carrying the relevant npm config/auth flags (or the same sanitized npm config/env used for resolution) into the audit command, and add an e2e test proving --registry/--userconfig reaches the audit invocation.

Comment thread src/precheck/mod.rs
// Collect the warn-only npm audit second opinion only after the verdict
// pool so the two truly overlap; any failure (timeout, disconnected
// sender) is a silent skip.
let audit = audit_rx.and_then(|rx| rx.recv_timeout(Duration::from_secs(2)).ok());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: after this 2s timeout, the code drops the only audit handle and proceeds, but the audit thread/subprocess is still running. spawn_audit returns only an mpsc::Receiver, so the main path cannot cancel or join the thread; if the real install is fast, the corgea process can exit before run_audit reaches its 5s kill path, leaving a hidden npm audit child doing network work and leaking the temp dir. The new hang test only asserts the wrapper returns quickly; it does not prove the audit child is killed or cleaned up. Fix by returning a cancellable/joinable audit handle whose timeout/drop path kills the child process (ideally the process group on Unix) and joins/cleans the temp dir before the wrapper exits, and add a test that observes the hung audit process is actually terminated.

@juangaitanv juangaitanv merged commit d00706e into install-vuln-gate Jun 11, 2026
17 checks passed
@juangaitanv juangaitanv deleted the ivg/u6-npm-audit branch June 11, 2026 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant