From 0e5beb4b3a26bf283f3783501196bd9097355451 Mon Sep 17 00:00:00 2001 From: juangaitanv Date: Fri, 12 Jun 2026 13:31:10 +0200 Subject: [PATCH 1/6] Scrub inherited GIT_* env in cli_deps test git helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running the suite from a git hook (e.g. pre-commit in a worktree) leaks GIT_DIR into the tests' subprocesses, pointing their git init at the developer's repo — locked mid-commit — instead of the temp dir. --- tests/cli_deps.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/cli_deps.rs b/tests/cli_deps.rs index 7723e8c..bffb1fe 100644 --- a/tests/cli_deps.rs +++ b/tests/cli_deps.rs @@ -898,8 +898,15 @@ fn commit_all(repo: &std::path::Path, message: &str) { } fn run_git(repo: &std::path::Path, args: &[&str]) { + // Scrub the GIT_* env a parent git process injects (e.g. when these + // tests run from a pre-commit hook): an inherited GIT_DIR would point + // `git init` at the developer's repo instead of `repo`. let output = Command::new("git") .current_dir(repo) + .env_remove("GIT_DIR") + .env_remove("GIT_WORK_TREE") + .env_remove("GIT_COMMON_DIR") + .env_remove("GIT_INDEX_FILE") .args(args) .output() .expect("run git"); From 72216d44ce1ab3534df861860829f6e524cab386 Mon Sep 17 00:00:00 2001 From: juangaitanv Date: Fri, 12 Jun 2026 13:31:21 +0200 Subject: [PATCH 2/6] Phase 0: vuln-api contract client, in-process stub, test scaffold The vuln-api client and its versioned contract (clean / vulnerable / malware / unknown verdicts, remediation data), harvested from the install-vuln-gate spike (dfac68e) and trimmed to phase scope: public unauthenticated lookups only, no retries, no user-facing command. - src/vuln_api: blocking client for /v1/packages/.../check with status mapping, identity guard, and PEP 503 request-time normalization - src/vuln_api_stub: in-process TCP stub, gated out of release builds via the test-stub feature + self dev-dependency - tests/common: shared GateHarness scaffold for later phases - tests/vuln_api_contract.rs: contract tests against the stub (hermetic) and the staging worker (#[ignore], deterministic targets documented in tests/fixtures/vuln_api/README.md) --- Cargo.lock | 1 + Cargo.toml | 12 + src/deps/ecosystems/pypi.rs | 5 +- src/lib.rs | 10 + src/vuln_api/mod.rs | 559 ++++++++++++++++++ src/vuln_api_stub/mod.rs | 271 +++++++++ tests/common/mod.rs | 274 +++++++++ tests/fixtures/vuln_api/README.md | 32 + tests/fixtures/vuln_api/check_clean.json | 1 + tests/fixtures/vuln_api/check_malware.json | 15 + tests/fixtures/vuln_api/check_unknown.json | 1 + tests/fixtures/vuln_api/check_vulnerable.json | 15 + tests/vuln_api_contract.rs | 172 ++++++ 13 files changed, 1367 insertions(+), 1 deletion(-) create mode 100644 src/vuln_api/mod.rs create mode 100644 src/vuln_api_stub/mod.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/fixtures/vuln_api/README.md create mode 100644 tests/fixtures/vuln_api/check_clean.json create mode 100644 tests/fixtures/vuln_api/check_malware.json create mode 100644 tests/fixtures/vuln_api/check_unknown.json create mode 100644 tests/fixtures/vuln_api/check_vulnerable.json create mode 100644 tests/vuln_api_contract.rs diff --git a/Cargo.lock b/Cargo.lock index 2b9c8e7..58bd8d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -344,6 +344,7 @@ version = "1.8.8" dependencies = [ "chrono", "clap", + "corgea", "dirs", "env_logger", "git2", diff --git a/Cargo.toml b/Cargo.toml index d60edad..5228045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,18 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "corgea" +path = "src/main.rs" + +[features] +# Compiles the in-crate vuln-api test stub (`vuln_api_stub`). Enabled for all +# test builds via the self dev-dependency below; never part of release builds. +test-stub = [] + +[dev-dependencies] +corgea = { path = ".", features = ["test-stub"] } + [dependencies] clap = { version = "4.4.13", features = ["derive"] } dirs = "5.0.1" diff --git a/src/deps/ecosystems/pypi.rs b/src/deps/ecosystems/pypi.rs index 062f13c..3a77faa 100644 --- a/src/deps/ecosystems/pypi.rs +++ b/src/deps/ecosystems/pypi.rs @@ -367,7 +367,10 @@ fn exact_version_from_declared(name: &str, declared: &str) -> Option { Some(declared.trim_start_matches('=').trim().to_string()) } -fn normalize_pypi_name(name: &str) -> String { +/// PEP 503 name normalization: lowercase, runs of `-`/`_`/`.` collapse to `-`. +/// Also used by the vuln-api client (`vuln_api`) so both features share one +/// canonical pypi name form. +pub(crate) fn normalize_pypi_name(name: &str) -> String { let mut out = String::new(); let mut last_was_separator = false; for c in name.trim().chars() { diff --git a/src/lib.rs b/src/lib.rs index 49bc6d0..2f8423e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1 +1,11 @@ pub mod deps; +// Also declared in the binary crate (src/main.rs); re-declared here so library modules +// (e.g. vuln_api) can use `crate::log::debug`. src/log.rs is a thin `::log` facade that +// compiles cleanly in both crates. +mod log; +pub mod vuln_api; +// Test-only HTTP stub for the vuln-api. Gated out of release builds; the +// `test-stub` feature is enabled for every test build by the self +// dev-dependency in Cargo.toml, so integration tests can use it too. +#[cfg(any(test, feature = "test-stub"))] +pub mod vuln_api_stub; diff --git a/src/vuln_api/mod.rs b/src/vuln_api/mod.rs new file mode 100644 index 0000000..222567c --- /dev/null +++ b/src/vuln_api/mod.rs @@ -0,0 +1,559 @@ +//! Corgea vuln-api client. +//! +//! Deliberately independent of `utils::api::SHARED_CLIENT` because: +//! * the vuln-api host is user-configurable via `CORGEA_VULN_API_URL`, +//! so we must never silently replay Corgea cookies or auth headers +//! via redirect following or the shared cookie jar. +//! * the shared client's `check_for_warnings` exits the process on +//! HTTP 410, which is wrong for per-dep CVE lookups. +//! +//! Lookups are public and unauthenticated: no Corgea credential is ever +//! attached, so a user-configured host can never see one. + +use serde::Deserialize; +use std::sync::OnceLock; +use std::time::Duration; + +use crate::log::debug; + +const REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +/// Cap on how much of an error response body we splice into the +/// user-facing error message. Fits a CLI line, captures +/// `{"error":"…"}`-class messages comfortably, and truncates +/// Cloudflare HTML before it gets ugly. +const ERROR_BODY_SNIPPET_LEN: usize = 300; + +/// Registry ecosystem a package check targets. Typed so the URL path +/// segment and the per-ecosystem name encoding can't drift apart on a +/// string spelling. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Ecosystem { + Npm, + Pypi, +} + +impl Ecosystem { + pub fn path_segment(self) -> &'static str { + match self { + Ecosystem::Npm => "npm", + Ecosystem::Pypi => "pypi", + } + } + + /// Canonical package name for requests and comparisons: PEP 503 for + /// pypi (shared with `deps`), verbatim for npm (names are + /// case-sensitive). The one definition of the per-ecosystem rule. + pub fn normalize_name(self, name: &str) -> String { + match self { + Ecosystem::Npm => name.to_string(), + Ecosystem::Pypi => crate::deps::ecosystems::pypi::normalize_pypi_name(name), + } + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VulnCheckResponse { + pub ecosystem: String, + pub package_name: String, + pub version: String, + pub is_vulnerable: bool, + pub matches: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +pub struct VulnMatch { + pub advisory_id: String, + pub severity_level: String, + pub tier: u8, + pub vulnerable_version_range: Option, + pub fixed_version: Option, +} + +/// `corgea-cli/ (