Add install vulnerability gate#99
Draft
juangaitanv wants to merge 29 commits into
Draft
Conversation
Port the vuln-api client from #89 onto main and reconcile its types against the authoritative Cloudflare Worker contract. - src/vuln_api/mod.rs: standalone reqwest::blocking client for /v1/.../check and /v1/advisories/:id with per-call auth (JWT->Bearer, else CORGEA-TOKEN), 404->clean, 401/403/429/5xx handling, a single 429 retry honoring Retry-After, and confused-deputy/identity guards. - Fix AdvisoryResponse field mapping: the server emits 'summary' and 'severity', so title/severity_level now carry #[serde(rename)]. VulnCheckResponse/VulnMatch already match toCheckMatch, unchanged. - Wire the module into the library crate (pub mod vuln_api) with a private 'mod log;' facade so crate::log::debug resolves there. - Add five committed fixtures (clean, unknown, vulnerable CVE, malware, advisory detail) built to the server serialization, with deserialization tests proving every /check variant the gate depends on.
Run package managers through a Corgea gate: install commands with named targets are resolved against the public registry first, and a resolved version published within --threshold (default 2d) blocks the install (exit 1) before the package manager runs. --no-fail demotes the block to a warning; --json emits a per-target report. Non-install subcommands, requirements files, bare installs, and unverifiable specs (git/URL/path) pass through with a printed note and the package manager's own exit code. No token required. Ported from PR #89 (cursor/dependency-freshness-check-89d1), slimmed to the named-target path: src/precheck (arg parsing + gate + exec) and src/verify_deps (threshold parsing + npm/PyPI resolvers) live in the library crate with no lockfile verification, check-only mode, or terminal-color dependency. Resolution errors warn but never block; fail-closed semantics arrive with the vuln-api verdict chunk. Also lands Config.vuln_api_url + get_vuln_api_url() (env -> config -> default) for the upcoming vuln-api chunk, hermetic integration tests driven by CORGEA_PYPI_REGISTRY/CORGEA_NPM_REGISTRY stub overrides, a shared tests/common harness, and SKILL.md docs for the new commands.
Wire vuln_api::check_package_version into the install wrappers: after the recency pass, each resolved name@version gets a verdict. is_vulnerable blocks; any API failure (network/timeout/5xx/auth) blocks fail-closed as unverifiable. --force proceeds despite all findings (still printed, manager exit code propagated); --no-fail keeps demoting recency only. Tokenless runs degrade to recency-only with a corgea login prompt. JSON output gains per-result verdict objects, vulnerable/unverifiable counts, and a top-level verdict_mode. Ports the PR #89 vuln-api test stub into the crate (corgea::vuln_api_stub plus a standalone vuln-api-stub bin) and pins the behavior with hermetic e2e tests in tests/cli_verdict.rs.
Expand corgea pip|npm install from named-target verification to every package the install would pull in: pip resolves via 'pip install --dry-run --quiet --report - --only-binary :all:' (the --only-binary guard keeps sdist build backends from running before the verdict, pypa/pip#13091); npm resolves in a throwaway temp dir via 'npm install --package-lock-only --ignore-scripts' and parses the generated lockfile (upstream --dry-run --json emits only counts, npm/cli#6558). Verdicts for the deduped tree + named union run through a bounded std::thread::scope pool (--concurrency, default 8); fail-closed and --force semantics extend to transitive findings unchanged. When resolution is unavailable (yarn/pnpm/uv have no safe dry-run) or fails, the gate verifies named targets only and prints a mandatory "transitive dependencies not checked" warning; --json carries the same fact in a new additive `tree` object (null in recency-only mode). PyPI name matching now shares deps' PEP 503 normalizer via PackageManager::normalize_name. The vuln-api test stub now sends Connection: close — it serves one response per connection, and without the header reqwest's pooled sockets raced the close once the gate began making several requests per run.
On a flagged package, annotate each advisory line with its fix (— fixed in X / — no fixed version known) and print a '→ safe version: name@X' steer when every advisory has a fix — single distinct value as-is, several distinct picked by lenient semver, and no certification when any advisory lacks a fix or a candidate doesn't parse. --json vulnerable verdicts carry a 'remediation' field (safe version or null), shared by named and transitive entries via verdict_json. Render-only (design D7, vuln-verdict spec 'Remediation steering'): no client, flag, or blocking changes. normalize_for_semver widened to pub(crate) for the cross-ecosystem version ordering. Covered by 7 safe_version unit tests + 4 hermetic e2e tests (cli_remediation).
git exports an absolute GIT_DIR to hooks in linked worktrees; tests that spawn 'git init' in tempdirs inherited it and reinitialized the shared gitdir (setting core.bare=true, breaking every checkout). Unset GIT_* in cmd_pre_commit so the test suite is hermetic.
The install gate never consults GET /v1/advisories/:id — verdicts come entirely from the package-check route. Drop AdvisoryResponse, get_advisory, the stub's advisories map/route, and the fixture.
When `which::which("pip")` fails, exec resolution retries `pip3` — pip
is the one manager with a conventional alias. The missing-binary error
now names the binary and the fallback tried:
error: 'pip' not found on PATH (also tried 'pip3')
Exit 127 unchanged. The post-resolution exec-failure message names the
resolved path instead of the requested binary so it stays accurate when
the fallback was taken.
New hermetic e2e suite (tests/cli_exec_fallback.rs): a controlled PATH
with only a fake pip3 runs the install through it; a PATH with neither
exits 127 with the message; npm's error names the binary without a
fallback hint.
Add a 'Testing the gate' subsection to the corgea skill: staging vuln-api URL, known-vulnerable npm/PyPI targets with their fixes, a verified copy-paste command with real observed output, and the recent-CVEs-only caveat for the staging PyPI seed.
Zero-spec `corgea npm install` with a package.json and a token now runs the existing tree pass: the lockfile-resolved set (0 named, N transitive) is verdicted, a vulnerable lockfile blocks fail-closed, --force escapes, and a resolution failure degrades to the named-only warning + exec as before. Bare yarn/pnpm/uv install-shaped commands print one stderr line (`note: bare '<pm> <sub>' is not gated …`) instead of silently running unchecked. The gated report header no longer renders a trailing space when the arg list is empty. SKILL.md offline-inputs sentence rewritten to match.
Tree-pass findings were all labeled (transitive), which misled when the flagged package came from the user's own requirements file or was already a direct dep of the project. - TreePackage carries pip's per-item "requested" report flag (npm: false). - New TreeOrigin on TreeOutcome: Requested / PreExisting / Transitive. Requested = pip-requested leftovers (-r files); PreExisting = npm leftovers named in the project package.json's direct deps (all four dep groups), read from cwd; Transitive otherwise. - Text labels: (from requirements), (already in package.json), (transitive). JSON tree entries gain an "origin" field. - PreExisting findings with an advertised fix get a hint: fix with: corgea npm install <name>@<fix> (advertised fix). advertised_fix() takes the max parseable fixed_version across matches, ignoring fixless matches — deliberately weaker than safe_version's certification and independent of it. should_block_install semantics unchanged. New hermetic e2e coverage in tests/cli_provenance.rs (fake PM + registry stub + vuln-api stub).
start_port + 50 panics in debug builds when the start port is above 65485 — the port-search test binds an ephemeral port and trips this whenever the OS hands out one near u16::MAX. Saturate instead.
Re-verdict each proposed '→ safe version' against vuln-api through the
existing verdict_pool before any output. A clean re-check prints the
steer; a flagged one prints '→ advertised fix {v} is also flagged — no
safe version to suggest'; a failed re-check suppresses the steer quietly
and never moves counts or exit codes. JSON 'remediation' now emits only
on a Verified steer. Proposals dedup by normalized (name, version) and
requests fire only when a vulnerable verdict exists with a token
configured.
When every vulnerable finding sits in the resolved tree beyond the named targets (and no named target is unverifiable), the refusal now says the existing dependency tree is the problem instead of implying the package the user typed is at fault. The text summary line gains a "(N from existing tree)" parenthetical on the vulnerable/unverifiable counts when the tree contributed findings. Messaging only — should_block_install semantics are unchanged.
…d outage noise - Tokenless note becomes a warning that states the consequence: known-vulnerable packages will NOT be blocked (recency-only). - verdict_pool prints 'checking N packages against Corgea vuln-api…' to stderr when more than 8 jobs run, so big tree passes don't look hung. - print_text collapses >3 unverifiable findings sharing an error-prefix (the vuln-api outage case) into one line; counts and exit codes unchanged.
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.
…install-vuln-gate
…tall-vuln-gate # Conflicts: # src/precheck/mod.rs
…ntegration # Conflicts: # src/precheck/mod.rs
…vg-integration # Conflicts: # src/precheck/mod.rs
…ration # Conflicts: # src/precheck/mod.rs
…plumbing Post-merge cleanup across the eight install-gate units: - Consolidate safe_version/advertised_fix into one highest_fix core (strict vs lenient parsing); the pre-existing fix hint's hedge now follows the steer re-check: Verified drops '(advertised fix)', Rejected suppresses the hint, Unverified keeps the hedge. - The existing-tree refusal only fires when every vulnerable finding predates the command: Requested (pip -r) findings and transitives pulled in by named targets get the generic refusal. Summary parenthetical reworded to 'from resolved tree'. - CORGEA_NO_NPM_AUDIT moves out of tree.rs into PrecheckOptions.npm_audit, read in main like the registry overrides; env semantics unchanged. - Audit collect window tightened to 1s: the warn-only signal never changes the outcome, so a finished gate shouldn't stall for it.
Two fixes from the PR #108 review: - refusal_blames_existing_tree filtered tree findings to Vulnerable before the origin test, but should_block_install refuses on Unverifiable too — a command-added unverifiable transitive alongside a pre-existing vulnerable dep printed 'none were added by this command'. Every blocking tree finding now passes the origin test. - The audit thread was detached with the Child trapped inside it; a fast gate exit orphaned a slow 'npm audit' past the CLI's lifetime. spawn_audit now returns an AuditHandle whose collect() does the 1s recv, then kills whatever is left in the shared child slot and joins the thread — no orphan, TempDir drops deterministically, happy-path latency unchanged. The hang test's fake now execs its sleep (a grandchild would dodge the kill) and asserts the child is dead after the CLI exits.
A requirements-only install (pip install -r reqs.txt) has no named outcomes — exactly like a bare install — so outcomes.is_empty() wrongly let a vulnerable transitive of a clean requirements entry blame the existing tree. PrecheckReport now carries bare_install (no CLI targets AND no requirements files), set from the parsed command, and the Transitive arm tests that instead of inferring from outcomes. Regressions: requirements-only e2e + full origin/named/bare unit matrix.
The bin existed for live e2e dogfooding, now done. Every consumer in the repo (7 integration test files + precheck unit tests) uses the in-process vuln_api_stub::spawn_with_statuses, so the bin-only surface goes with it: the fixtures.rs file loader, spawn_from_file, VulnApiStub::block, and the StubFixtures/spawn_on_port indirection (the ephemeral-port bind now lives directly in spawn_with_statuses).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds install-time vulnerability gating for package-manager installs.
corgea pip|npm|yarn|pnpm|uv installwrappers that parse requested targets, enforce recency thresholds, and call the vuln API before executing package manager commands.vuln-api-stubfixture server for clean, vulnerable, malware, unknown, and advisory responses.Test plan
git push -u origin install-vuln-gate./harness checkcompleted clippy fix, format, and strict clippy successfully./harness checkpasses end-to-endauthorize::tests::find_available_port_skips_ports_that_are_in_usewithattempt to add with overflowatsrc/authorize.rs:97, outside this PR diff.