From 6a2e52d192c77afd2b3b1a54818e29b4c6a95c69 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 28 Jul 2026 16:37:26 +0200 Subject: [PATCH 1/5] COR-1549: require opt-in before sending tokens to a non-production vuln-api default The token-isolation rule tested trust as "is this the built-in default?", so the default was treated as inherently trustworthy. That default is staging, so a real `corgea login` token was sent to cve-worker-staging with no opt-in and fail-closed enforcement rested on staging auth and seed data. Add DEFAULT_VULN_API_URL_IS_PRODUCTION alongside the URL and reframe the test as "is this an endpoint the token belongs to?". The existing CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL opt-in now covers both cases, so staging validation runs still work. Malicious blocking is unaffected: public mode is still a verdict pass, so known malicious and known vulnerable both block. Only lookup errors now fail open instead of closed. Flip the URL and the constant in the same change at prod cutover. --- src/config.rs | 6 ++++++ src/main.rs | 49 +++++++++++++++++++++++++++++++++++++------------ 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/config.rs b/src/config.rs index 5d94e27..4e4d191 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,12 @@ use std::{env, fs, io}; pub const DEFAULT_VULN_API_URL: &str = "https://cve-worker-staging.corgea.workers.dev"; +/// Whether `DEFAULT_VULN_API_URL` points at a production endpoint. While it is +/// staging, a Corgea token must never be sent there without an explicit opt-in, +/// so authenticated (fail-closed) verdicts stay off by default. Flip this to +/// `true` in the same change that points the default at production. +pub const DEFAULT_VULN_API_URL_IS_PRODUCTION: bool = false; + #[derive(Serialize, Deserialize, Clone)] pub struct Config { pub(crate) url: String, diff --git a/src/main.rs b/src/main.rs index 9618721..127ca22 100644 --- a/src/main.rs +++ b/src/main.rs @@ -269,8 +269,8 @@ struct VulnApiAccess { } /// Resolve the shared vuln-api access policy once so both surfaces apply the -/// same base-URL/token-isolation rule (never send a token to a custom URL -/// without explicit opt-in). +/// same base-URL/token-isolation rule (never send a token to an endpoint it +/// does not belong to without explicit opt-in). fn resolve_vuln_api_access(config: &Config) -> VulnApiAccess { let token = config.get_token(); let token = token.trim(); @@ -323,15 +323,17 @@ fn advisories_options(config: &Config) -> corgea::advisories::AdvisoriesOptions } } -/// A token enables authenticated (fail-closed) verdicts — but never against -/// a custom vuln-api URL unless the user explicitly opts in to sending the -/// token there. +/// A token enables authenticated (fail-closed) verdicts — but only against a +/// vuln-api the token belongs to. That means neither a custom URL nor a +/// non-production built-in default, unless the user explicitly opts in to +/// sending the token there. fn select_verdict_mode( token: &str, custom_vuln_api_url: bool, send_token_to_custom: bool, ) -> corgea::precheck::VerdictMode { - if !token.is_empty() && (!custom_vuln_api_url || send_token_to_custom) { + let trusted_default = !custom_vuln_api_url && config::DEFAULT_VULN_API_URL_IS_PRODUCTION; + if !token.is_empty() && (trusted_default || send_token_to_custom) { corgea::precheck::VerdictMode::Authenticated { token: token.to_string(), } @@ -785,12 +787,18 @@ mod tests { fn verdict_mode_selection_matrix() { use corgea::precheck::VerdictMode; - assert_eq!( - select_verdict_mode("token", false, false), - VerdictMode::Authenticated { - token: "token".to_string() - } - ); + // Built-in default: authenticated only when that default is production. + let default_mode = select_verdict_mode("token", false, false); + if config::DEFAULT_VULN_API_URL_IS_PRODUCTION { + assert_eq!( + default_mode, + VerdictMode::Authenticated { + token: "token".to_string() + } + ); + } else { + assert_eq!(default_mode, VerdictMode::Public); + } assert_eq!(select_verdict_mode("", false, false), VerdictMode::Public); assert_eq!( select_verdict_mode("token", true, false), @@ -803,4 +811,21 @@ mod tests { } ); } + + /// A token only ever reaches an endpoint it belongs to. The built-in + /// default is staging today, so it needs the same opt-in as a custom URL + /// — see COR-1549. + #[test] + fn token_needs_opt_in_for_non_production_default() { + use corgea::precheck::VerdictMode; + + // The opt-in still works, for staging validation runs. + assert_eq!( + select_verdict_mode("token", false, true), + VerdictMode::Authenticated { + token: "token".to_string() + } + ); + assert_eq!(select_verdict_mode("", false, true), VerdictMode::Public); + } } From dbc9577baa96ed7ffec7e89fd1fc0e0b8d0e6ce1 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 28 Jul 2026 17:02:47 +0200 Subject: [PATCH 2/5] COR-1549: correct in-repo docs that promised fail-closed after login Addresses cursor[bot] on src/config.rs:11. README.md and skills/corgea/SKILL.md are the shipped operator and agent contracts, and both still said a `corgea login` token on the default vuln-api enables authenticated fail-closed enforcement. After this PR it does not: the token is withheld from the staging default, so those instructions would leave an operator or coding agent believing lookup failures block when they now warn and continue. Restate the rule in all four places: a token goes only to a vuln-api it belongs to, which today excludes both a custom URL and the built-in default, and CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1 covers both cases. Known-vulnerable and known-malicious packages still block either way. --- README.md | 12 ++++++++---- skills/corgea/SKILL.md | 27 ++++++++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ccecc68..661c955 100644 --- a/README.md +++ b/README.md @@ -79,10 +79,14 @@ hijacks before advisory feeds catch up. It is on by default; turn it off with `recency_gate = false` in `~/.corgea/config.toml`, retune the window with `recency_threshold_days`, or pass `--force` for a one-off install. -Logging in (`corgea login`) upgrades the gate to authenticated enforcement — -unverifiable packages, resolution errors, and lookup failures then fail closed -(public mode warns and continues). Wrapper flags (`--force`, `--json`) go between -the manager and its command: `corgea npm --force install `. +Authenticated enforcement makes unverifiable packages, resolution errors, and +lookup failures fail closed (public mode warns and continues). Corgea only sends +your token to a vulnerability API the token belongs to, and the built-in default +is a staging service today, so `corgea login` alone does **not** enable it — set +`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send the token and turn it on. +Known-vulnerable and known-malicious packages block either way. Wrapper flags +(`--force`, `--json`) go between the manager and its command: +`corgea npm --force install `. See [the CLI docs](https://docs.corgea.app/cli) for the full flag and exit-code reference. diff --git a/skills/corgea/SKILL.md b/skills/corgea/SKILL.md index 07a7977..0d55448 100644 --- a/skills/corgea/SKILL.md +++ b/skills/corgea/SKILL.md @@ -136,10 +136,12 @@ Notes: `deps scan --out-format table|json|sarif` is the report/export selector; Query Corgea's vulnerability database for a package **before** choosing or installing it. Read-only: it reports, never blocks. Network access to the vuln-api is required. The package-level form needs no token; the versioned -form uses the same auth story as the install gate (a Corgea token is -attached automatically when logged in on the default vuln-api, and the -production version-check route may require one — a tokenless 401 exits 2 -with a clear message). +form uses the same auth story as the install gate (a Corgea token is sent +only to a vuln-api the token belongs to — not the built-in default while it +is a staging service, so requests are tokenless unless +`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` — and the production +version-check route may require one, where a tokenless 401 exits 2 with a +clear message). **Before adding or choosing a dependency version, run `corgea advisories check ` to see its advisory history, @@ -186,8 +188,10 @@ package's publish time is shown for provenance (`published ago at `), but it never blocks. Baseline public CVE checks need no token: known-vulnerable or malicious versions block, but vuln-api lookup outages warn and continue because public -mode is fail-open. A Corgea token on the default vuln-api enables -authenticated enforcement; in that mode, verdict lookup failures, resolution +mode is fail-open. Authenticated enforcement requires the token to actually +be sent, which the built-in staging default does not do — it is off by +default today (see the token rule below); in that mode, verdict lookup +failures, resolution errors, and unverifiable git/URL/path specs (including `pip install .`, PEP 508 `name @ url` direct references, and npm GitHub shorthand `user/repo`) all block (fail-closed) unless `--force`. In public mode those same specs are @@ -270,11 +274,12 @@ not upgraded. A top-level `recency_threshold_days` reports the active recency window (or `null` when the recency gate is off); pair it with each result's `age_seconds`. -Baseline CVE checks need no token. The default vuln-api -uses `CORGEA_TOKEN` (or the `corgea login` token) when present. A custom -`CORGEA_VULN_API_URL` is public by default, even when a token exists; set -`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send the token to that -custom URL and make lookup failures fail closed. Recency gate: +Baseline CVE checks need no token. A token is sent only to a vuln-api it +belongs to: neither a custom `CORGEA_VULN_API_URL` nor the built-in default +while that default is a staging service — which is the case today — so both +stay public even when `CORGEA_TOKEN` or a `corgea login` token exists. Set +`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send the token to such an +endpoint and make lookup failures fail closed. Recency gate: `recency_gate` / `recency_threshold_days` in `~/.corgea/config.toml`, overridden by `CORGEA_RECENCY_GATE` and `CORGEA_RECENCY_THRESHOLD_DAYS`. Overrides for testing: `CORGEA_PYPI_REGISTRY`, `CORGEA_NPM_REGISTRY`, `CORGEA_VULN_API_URL`. From 158fb846f2d56cbca3c64cc7d2a95d5aaa1c1b87 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 28 Jul 2026 17:19:10 +0200 Subject: [PATCH 3/5] COR-1549: disclose public mode when a token is withheld MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses cursor[bot] on src/main.rs:335. The hint was gated on `public_login_hint: !access.has_token`, so the one cohort this PR creates — logged in, built-in staging default, no opt-in — got fail-open verdicts and no warning at all. The existing copy would also have been wrong for them, since login alone no longer enables authentication. Replace the bool with `public_hint: Option` so the flag cannot disagree with its reason, and print copy per case: NoToken keeps the login prompt, TokenWithheld explains the token is not sent to this vuln-api and names CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1. Both emission sites (parsed installs and lockfile installs) share it. Regression test asserts token + built-in non-production default + no opt-in yields VerdictMode::Public and PublicHint::TokenWithheld. --- src/main.rs | 30 ++++++++++++++++++++++- src/precheck/mod.rs | 46 ++++++++++++++++++++++++++---------- src/precheck/test_support.rs | 4 ++-- src/precheck/verdict.rs | 2 +- 4 files changed, 66 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index 127ca22..51c8305 100644 --- a/src/main.rs +++ b/src/main.rs @@ -297,7 +297,7 @@ fn install_wrap_options( verdict: Some(corgea::precheck::VerdictConfig { base_url: access.base_url, mode: access.mode, - public_login_hint: !access.has_token, + public_hint: Some(public_hint_for(access.has_token)), }), npm_registry: utils::generic::get_env_var_if_exists("CORGEA_NPM_REGISTRY"), pypi_registry: utils::generic::get_env_var_if_exists("CORGEA_PYPI_REGISTRY"), @@ -323,6 +323,17 @@ fn advisories_options(config: &Config) -> corgea::advisories::AdvisoriesOptions } } +/// Which public-mode disclosure to print. A withheld token is not the same +/// situation as no token, and telling a logged-in user to log in is useless. +/// Only consulted in public mode — authenticated runs print no hint. +fn public_hint_for(has_token: bool) -> corgea::precheck::PublicHint { + if has_token { + corgea::precheck::PublicHint::TokenWithheld + } else { + corgea::precheck::PublicHint::NoToken + } +} + /// A token enables authenticated (fail-closed) verdicts — but only against a /// vuln-api the token belongs to. That means neither a custom URL nor a /// non-production built-in default, unless the user explicitly opts in to @@ -812,6 +823,23 @@ mod tests { ); } + /// A logged-in user on the built-in non-production default gets public + /// verdicts, and must be told so — the old hint keyed off "has a token" + /// and went silent for exactly this cohort. See COR-1549. + #[test] + fn withheld_token_still_discloses_public_mode() { + use corgea::precheck::{PublicHint, VerdictMode}; + + // token + built-in default + no opt-in + assert_eq!( + select_verdict_mode("token", false, false), + VerdictMode::Public + ); + assert_eq!(public_hint_for(true), PublicHint::TokenWithheld); + // No token is a different situation with different advice. + assert_eq!(public_hint_for(false), PublicHint::NoToken); + } + /// A token only ever reaches an endpoint it belongs to. The built-in /// default is staging today, so it needs the same opt-in as a custom URL /// — see COR-1549. diff --git a/src/precheck/mod.rs b/src/precheck/mod.rs index 8e0d06d..eb7c97c 100644 --- a/src/precheck/mod.rs +++ b/src/precheck/mod.rs @@ -144,8 +144,34 @@ impl VerdictMode { pub struct VerdictConfig { pub base_url: String, pub mode: VerdictMode, - /// Print the tokenless public-mode hint after a check is attempted. - pub public_login_hint: bool, + /// Disclosure printed after a check is attempted in public mode, so a + /// fail-open run never looks like an enforced one. `None` suppresses it. + pub public_hint: Option, +} + +/// Why the gate is running public (fail-open) checks. Both cases warn, but a +/// user who never logged in needs different advice from one whose token is +/// deliberately withheld from this endpoint. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PublicHint { + /// No token at all — logging in is the next step. + NoToken, + /// A token exists but is not sent to this vuln-api, so it cannot enable + /// authenticated enforcement. + TokenWithheld, +} + +impl PublicHint { + fn message(self) -> &'static str { + match self { + PublicHint::NoToken => { + "warning: using public CVE checks; login enables authenticated enforcement and private Corgea intelligence." + } + PublicHint::TokenWithheld => { + "warning: using public CVE checks; your token is not sent to this vuln-api, so lookup failures warn instead of blocking. Set CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1 to enable authenticated enforcement." + } + } + } } /// Threat verdict for one resolved target. @@ -845,10 +871,8 @@ fn run_parsed_install( "warning: transitive dependencies not checked ({reason}); only named packages were verified." ); } - if verdict::public_verdict(&opts).is_some_and(|cfg| cfg.public_login_hint) { - eprintln!( - "warning: using public CVE checks; login enables authenticated enforcement and private Corgea intelligence." - ); + if let Some(hint) = verdict::public_verdict(&opts).and_then(|cfg| cfg.public_hint) { + eprintln!("{}", hint.message()); } let report = PrecheckReport { @@ -877,12 +901,10 @@ fn run_locked_install( // Direct callers may still disable verdicts completely. return exec(); }; - // Same disclosure as run_parsed_install: a tokenless `npm ci`/`uv sync` - // runs public checks — say so rather than gate silently. - if verdict::public_verdict(opts).is_some_and(|cfg| cfg.public_login_hint) { - eprintln!( - "warning: using public CVE checks; login enables authenticated enforcement and private Corgea intelligence." - ); + // Same disclosure as run_parsed_install: an `npm ci`/`uv sync` running + // public checks says so rather than gating silently. + if let Some(hint) = verdict::public_verdict(opts).and_then(|cfg| cfg.public_hint) { + eprintln!("{}", hint.message()); } let jobs = match lock { Ok(jobs) => jobs, diff --git a/src/precheck/test_support.rs b/src/precheck/test_support.rs index 9341649..2b2d277 100644 --- a/src/precheck/test_support.rs +++ b/src/precheck/test_support.rs @@ -32,7 +32,7 @@ pub(crate) fn verdict_opts(base_url: &str) -> PrecheckOptions { mode: VerdictMode::Authenticated { token: "test-token".to_string(), }, - public_login_hint: false, + public_hint: None, }), ..stub_opts() } @@ -44,7 +44,7 @@ pub(crate) fn public_opts(force: bool) -> PrecheckOptions { verdict: Some(VerdictConfig { base_url: "http://127.0.0.1:9".to_string(), mode: VerdictMode::Public, - public_login_hint: true, + public_hint: Some(super::PublicHint::NoToken), }), ..stub_opts() } diff --git a/src/precheck/verdict.rs b/src/precheck/verdict.rs index 26e9e7f..1746caf 100644 --- a/src/precheck/verdict.rs +++ b/src/precheck/verdict.rs @@ -786,7 +786,7 @@ mod tests { mode: super::super::VerdictMode::Authenticated { token: "test-token".to_string(), }, - public_login_hint: false, + public_hint: None, }; let jobs: Vec = ["a", "b", "evil", "c", "d", "e"] From b8acb234f7e602ef7e238145bc782daab7afab0c Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 28 Jul 2026 22:28:17 +0200 Subject: [PATCH 4/5] COR-1549: make the tokenless hint conditional, cover disclosure end to end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two cursor[bot] follow-ups. precheck/mod.rs: the NoToken copy promised that login enables authenticated enforcement, which this PR makes false — logging in moves that user to TokenWithheld and select_verdict_mode still returns Public. State both requirements, a token and an endpoint it can be sent to. tests/cli_verdict.rs: tokenless_public_check_discloses_mode asserted the old unconditional promise, so it pinned the falsehood in place; it now also requires the opt-in to be named. The assertion is case-insensitive because the benefit now opens a sentence. The unit test asserted select_verdict_mode and public_hint_for separately, so it stayed green if install_wrap_options dropped the hint or an eprintln! were deleted. Add CLI regressions driving the real binary with a token: withheld_token_discloses_public_mode_and_names_opt_in asserts the withheld copy, the opt-in name, and the absence of the useless "login enables" advice, and withheld_token_reports_public_verdict_mode_in_json pins verdict_mode. Renamed the unit test to say what it covers: the harness always points CORGEA_VULN_API_URL at a stub, so the built-in-default branch it pins is the one cohort no integration test can reach. --- src/main.rs | 11 +++++--- src/precheck/mod.rs | 2 +- tests/cli_verdict.rs | 63 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 51c8305..fcb83c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -823,11 +823,14 @@ mod tests { ); } - /// A logged-in user on the built-in non-production default gets public - /// verdicts, and must be told so — the old hint keyed off "has a token" - /// and went silent for exactly this cohort. See COR-1549. + /// Selection only: a logged-in user on the built-in non-production default + /// gets public verdicts and the withheld-token hint. This is the one cohort + /// no integration test can pin, since the test harness always points + /// `CORGEA_VULN_API_URL` at a stub and so exercises the custom-URL branch. + /// The disclosure wiring itself is covered end-to-end by + /// `tests/cli_verdict.rs::withheld_token_discloses_public_mode_and_names_opt_in`. #[test] - fn withheld_token_still_discloses_public_mode() { + fn withheld_token_selects_public_mode_and_withheld_hint() { use corgea::precheck::{PublicHint, VerdictMode}; // token + built-in default + no opt-in diff --git a/src/precheck/mod.rs b/src/precheck/mod.rs index eb7c97c..c69e041 100644 --- a/src/precheck/mod.rs +++ b/src/precheck/mod.rs @@ -165,7 +165,7 @@ impl PublicHint { fn message(self) -> &'static str { match self { PublicHint::NoToken => { - "warning: using public CVE checks; login enables authenticated enforcement and private Corgea intelligence." + "warning: using public CVE checks, so lookup failures warn instead of blocking. Authenticated enforcement and private Corgea intelligence need both a token (`corgea login`) and a vuln-api it can be sent to — with the current default endpoint that also means CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1." } PublicHint::TokenWithheld => { "warning: using public CVE checks; your token is not sent to this vuln-api, so lookup failures warn instead of blocking. Set CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1 to enable authenticated enforcement." diff --git a/tests/cli_verdict.rs b/tests/cli_verdict.rs index 9f685b9..af8f3e0 100644 --- a/tests/cli_verdict.rs +++ b/tests/cli_verdict.rs @@ -268,11 +268,70 @@ fn tokenless_public_check_discloses_mode() { stderr.contains("using public CVE checks"), "tokenless mode must disclose public CVE checks: {stderr}" ); + let lower = stderr.to_lowercase(); assert!( - stderr.contains("authenticated enforcement") - && stderr.contains("private Corgea intelligence"), + lower.contains("authenticated enforcement") + && lower.contains("private corgea intelligence"), "tokenless warning must name the authenticated benefit: {stderr}" ); + // The benefit is conditional: a token alone does not buy enforcement while + // the endpoint is one the token is withheld from. Naming the opt-in keeps + // the hint from promising fail-closed behavior login cannot deliver. + assert!( + stderr.contains("CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL"), + "tokenless warning must state the endpoint requirement, not just login: {stderr}" + ); +} + +#[test] +fn withheld_token_discloses_public_mode_and_names_opt_in() { + // The cohort this PR creates: a token exists, but it is not sent to this + // vuln-api, so verdicts stay public and lookup failures warn. The old hint + // keyed off "has a token" and went silent here. + let mut checks = HashMap::new(); + checks.insert( + key("pypi", "oldpkg", "1.0.0"), + vulnerable_body("pypi", "oldpkg", "1.0.0", "CVE-2024-0001", Some("2.0.0")), + ); + let mut h = pip_harness(checks, HashMap::new(), Some("opaque-token"), 0); + let out = h + .cmd + .args(["pip", "install", "oldpkg==1.0.0"]) + .output() + .expect("run corgea"); + assert_eq!(out.status.code(), Some(1)); + let stderr = String::from_utf8_lossy(&out.stderr); + assert!( + stderr.contains("your token is not sent to this vuln-api"), + "a withheld token must be disclosed, not silently downgraded: {stderr}" + ); + assert!( + stderr.contains("CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL"), + "withheld-token warning must name the opt-in: {stderr}" + ); + assert!( + !stderr.contains("login enables"), + "a logged-in user must not be told to log in: {stderr}" + ); +} + +#[test] +fn withheld_token_reports_public_verdict_mode_in_json() { + let mut checks = HashMap::new(); + checks.insert( + key("pypi", "oldpkg", "1.0.0"), + vulnerable_body("pypi", "oldpkg", "1.0.0", "CVE-2024-0001", Some("2.0.0")), + ); + let mut h = pip_harness(checks, HashMap::new(), Some("opaque-token"), 0); + let out = h + .cmd + .args(["pip", "--json", "install", "oldpkg==1.0.0"]) + .output() + .expect("run corgea"); + assert_eq!(out.status.code(), Some(1)); + let parsed: serde_json::Value = + serde_json::from_slice(&out.stdout).expect("stdout must be valid JSON"); + assert_eq!(parsed["verdict_mode"], "public"); } #[test] From dc93969d400565dd2ef9c4ad9e56535efe1ff3b3 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 29 Jul 2026 09:47:12 +0200 Subject: [PATCH 5/5] COR-1549: the default vuln-api is production; mark it trusted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `-staging` hostname is historical — that worker serves as the production vuln-api, so Corgea tokens belong there. `DEFAULT_VULN_API_URL_IS_PRODUCTION` was set from the hostname rather than from how the endpoint is deployed, which made this PR withhold tokens from an endpoint that legitimately receives them and turned off fail-closed enforcement for every default install and CI job. Set the constant to `true`, restoring authenticated enforcement on the default, and revert the README/SKILL edits that told users login no longer enables it. The `NoToken` hint goes back to naming login as the next step, since on the default endpoint that is once again true. What the PR still buys: token-send is now decided by a property of the endpoint rather than by whether it happens to be the compiled-in default, so repointing the default at an untrusted endpoint withholds the token instead of silently shipping it. Custom URLs keep the pre-existing opt-in, and the withheld-token disclosure — which never existed before — now covers that cohort, which previously got public verdicts with no warning at all. Verified against the real binary: token+default -> authenticated with no warning; no token -> public + login hint; token+custom -> public + withheld hint; token+custom+opt-in -> authenticated. --- README.md | 12 ++++-------- skills/corgea/SKILL.md | 27 +++++++++++---------------- src/config.rs | 12 +++++++----- src/main.rs | 25 ++++++++++--------------- src/precheck/mod.rs | 2 +- tests/cli_verdict.rs | 12 ++---------- 6 files changed, 35 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index 661c955..ccecc68 100644 --- a/README.md +++ b/README.md @@ -79,14 +79,10 @@ hijacks before advisory feeds catch up. It is on by default; turn it off with `recency_gate = false` in `~/.corgea/config.toml`, retune the window with `recency_threshold_days`, or pass `--force` for a one-off install. -Authenticated enforcement makes unverifiable packages, resolution errors, and -lookup failures fail closed (public mode warns and continues). Corgea only sends -your token to a vulnerability API the token belongs to, and the built-in default -is a staging service today, so `corgea login` alone does **not** enable it — set -`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send the token and turn it on. -Known-vulnerable and known-malicious packages block either way. Wrapper flags -(`--force`, `--json`) go between the manager and its command: -`corgea npm --force install `. +Logging in (`corgea login`) upgrades the gate to authenticated enforcement — +unverifiable packages, resolution errors, and lookup failures then fail closed +(public mode warns and continues). Wrapper flags (`--force`, `--json`) go between +the manager and its command: `corgea npm --force install `. See [the CLI docs](https://docs.corgea.app/cli) for the full flag and exit-code reference. diff --git a/skills/corgea/SKILL.md b/skills/corgea/SKILL.md index 0d55448..07a7977 100644 --- a/skills/corgea/SKILL.md +++ b/skills/corgea/SKILL.md @@ -136,12 +136,10 @@ Notes: `deps scan --out-format table|json|sarif` is the report/export selector; Query Corgea's vulnerability database for a package **before** choosing or installing it. Read-only: it reports, never blocks. Network access to the vuln-api is required. The package-level form needs no token; the versioned -form uses the same auth story as the install gate (a Corgea token is sent -only to a vuln-api the token belongs to — not the built-in default while it -is a staging service, so requests are tokenless unless -`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` — and the production -version-check route may require one, where a tokenless 401 exits 2 with a -clear message). +form uses the same auth story as the install gate (a Corgea token is +attached automatically when logged in on the default vuln-api, and the +production version-check route may require one — a tokenless 401 exits 2 +with a clear message). **Before adding or choosing a dependency version, run `corgea advisories check ` to see its advisory history, @@ -188,10 +186,8 @@ package's publish time is shown for provenance (`published ago at `), but it never blocks. Baseline public CVE checks need no token: known-vulnerable or malicious versions block, but vuln-api lookup outages warn and continue because public -mode is fail-open. Authenticated enforcement requires the token to actually -be sent, which the built-in staging default does not do — it is off by -default today (see the token rule below); in that mode, verdict lookup -failures, resolution +mode is fail-open. A Corgea token on the default vuln-api enables +authenticated enforcement; in that mode, verdict lookup failures, resolution errors, and unverifiable git/URL/path specs (including `pip install .`, PEP 508 `name @ url` direct references, and npm GitHub shorthand `user/repo`) all block (fail-closed) unless `--force`. In public mode those same specs are @@ -274,12 +270,11 @@ not upgraded. A top-level `recency_threshold_days` reports the active recency window (or `null` when the recency gate is off); pair it with each result's `age_seconds`. -Baseline CVE checks need no token. A token is sent only to a vuln-api it -belongs to: neither a custom `CORGEA_VULN_API_URL` nor the built-in default -while that default is a staging service — which is the case today — so both -stay public even when `CORGEA_TOKEN` or a `corgea login` token exists. Set -`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send the token to such an -endpoint and make lookup failures fail closed. Recency gate: +Baseline CVE checks need no token. The default vuln-api +uses `CORGEA_TOKEN` (or the `corgea login` token) when present. A custom +`CORGEA_VULN_API_URL` is public by default, even when a token exists; set +`CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send the token to that +custom URL and make lookup failures fail closed. Recency gate: `recency_gate` / `recency_threshold_days` in `~/.corgea/config.toml`, overridden by `CORGEA_RECENCY_GATE` and `CORGEA_RECENCY_THRESHOLD_DAYS`. Overrides for testing: `CORGEA_PYPI_REGISTRY`, `CORGEA_NPM_REGISTRY`, `CORGEA_VULN_API_URL`. diff --git a/src/config.rs b/src/config.rs index 4e4d191..5e4986c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,11 +4,13 @@ use std::{env, fs, io}; pub const DEFAULT_VULN_API_URL: &str = "https://cve-worker-staging.corgea.workers.dev"; -/// Whether `DEFAULT_VULN_API_URL` points at a production endpoint. While it is -/// staging, a Corgea token must never be sent there without an explicit opt-in, -/// so authenticated (fail-closed) verdicts stay off by default. Flip this to -/// `true` in the same change that points the default at production. -pub const DEFAULT_VULN_API_URL_IS_PRODUCTION: bool = false; +/// Whether `DEFAULT_VULN_API_URL` is an endpoint Corgea tokens belong to. +/// The `-staging` hostname is historical: this worker serves as the production +/// vuln-api, so a token may be sent to it and authenticated (fail-closed) +/// verdicts apply by default. Set to `false` if the default is ever repointed +/// at an endpoint that customer tokens should not reach — token-send and +/// fail-closed then both require the explicit opt-in. +pub const DEFAULT_VULN_API_URL_IS_PRODUCTION: bool = true; #[derive(Serialize, Deserialize, Clone)] pub struct Config { diff --git a/src/main.rs b/src/main.rs index fcb83c3..35a976a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -823,19 +823,16 @@ mod tests { ); } - /// Selection only: a logged-in user on the built-in non-production default - /// gets public verdicts and the withheld-token hint. This is the one cohort - /// no integration test can pin, since the test harness always points - /// `CORGEA_VULN_API_URL` at a stub and so exercises the custom-URL branch. - /// The disclosure wiring itself is covered end-to-end by - /// `tests/cli_verdict.rs::withheld_token_discloses_public_mode_and_names_opt_in`. + /// A token reaches only an endpoint it belongs to. A custom vuln-api is + /// not one, so it stays public and says so — the withheld hint exists for + /// exactly that cohort. See COR-1549. #[test] fn withheld_token_selects_public_mode_and_withheld_hint() { use corgea::precheck::{PublicHint, VerdictMode}; - // token + built-in default + no opt-in + // token + custom URL + no opt-in assert_eq!( - select_verdict_mode("token", false, false), + select_verdict_mode("token", true, false), VerdictMode::Public ); assert_eq!(public_hint_for(true), PublicHint::TokenWithheld); @@ -843,20 +840,18 @@ mod tests { assert_eq!(public_hint_for(false), PublicHint::NoToken); } - /// A token only ever reaches an endpoint it belongs to. The built-in - /// default is staging today, so it needs the same opt-in as a custom URL - /// — see COR-1549. + /// The opt-in is what makes an otherwise untrusted endpoint eligible for + /// the token — and it never manufactures a token that does not exist. #[test] - fn token_needs_opt_in_for_non_production_default() { + fn opt_in_enables_authenticated_for_untrusted_endpoints() { use corgea::precheck::VerdictMode; - // The opt-in still works, for staging validation runs. assert_eq!( - select_verdict_mode("token", false, true), + select_verdict_mode("token", true, true), VerdictMode::Authenticated { token: "token".to_string() } ); - assert_eq!(select_verdict_mode("", false, true), VerdictMode::Public); + assert_eq!(select_verdict_mode("", true, true), VerdictMode::Public); } } diff --git a/src/precheck/mod.rs b/src/precheck/mod.rs index c69e041..eb7c97c 100644 --- a/src/precheck/mod.rs +++ b/src/precheck/mod.rs @@ -165,7 +165,7 @@ impl PublicHint { fn message(self) -> &'static str { match self { PublicHint::NoToken => { - "warning: using public CVE checks, so lookup failures warn instead of blocking. Authenticated enforcement and private Corgea intelligence need both a token (`corgea login`) and a vuln-api it can be sent to — with the current default endpoint that also means CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1." + "warning: using public CVE checks; login enables authenticated enforcement and private Corgea intelligence." } PublicHint::TokenWithheld => { "warning: using public CVE checks; your token is not sent to this vuln-api, so lookup failures warn instead of blocking. Set CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1 to enable authenticated enforcement." diff --git a/tests/cli_verdict.rs b/tests/cli_verdict.rs index af8f3e0..228fb24 100644 --- a/tests/cli_verdict.rs +++ b/tests/cli_verdict.rs @@ -268,19 +268,11 @@ fn tokenless_public_check_discloses_mode() { stderr.contains("using public CVE checks"), "tokenless mode must disclose public CVE checks: {stderr}" ); - let lower = stderr.to_lowercase(); assert!( - lower.contains("authenticated enforcement") - && lower.contains("private corgea intelligence"), + stderr.contains("authenticated enforcement") + && stderr.contains("private Corgea intelligence"), "tokenless warning must name the authenticated benefit: {stderr}" ); - // The benefit is conditional: a token alone does not buy enforcement while - // the endpoint is one the token is withheld from. Naming the opt-in keeps - // the hint from promising fail-closed behavior login cannot deliver. - assert!( - stderr.contains("CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL"), - "tokenless warning must state the endpoint requirement, not just login: {stderr}" - ); } #[test]