Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 24 additions & 18 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,13 @@ Notes: `deps scan --out-format table|json|sarif` is the report/export selector;
Run a package manager through Corgea's install gate. Install commands with
named targets are resolved against the public registry first, then gated
twice: a version published within `--threshold` (default `2d`) blocks
(exit 1), and each resolved version is checked against Corgea's vuln-api —
known-vulnerable or malicious versions block. CVE checks are public and need
no token; vuln-api lookup outages warn and continue (fail-open). Everything
else passes through with the package manager's own exit code. Git/URL/path
(exit 1), and each resolved version is checked against Corgea's vuln-api.
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 and
resolution errors also block (fail-closed). Everything else passes through
with the package manager's own exit code. Git/URL/path
specs (including `pip install .`, PEP 508 `name @ url` direct references, and
npm GitHub shorthand `user/repo`) are noted, never blocked. The install verb
is found behind global flags (`npm --loglevel silent install x` is still
Expand Down Expand Up @@ -191,22 +194,25 @@ corgea pip list # non-install subcommands pass straight th
| Flag | Short | Description |
|------|-------|-------------|
| `--threshold` | `-t` | Recency threshold (`2d`, `12h`). Younger resolved versions block. |
| `--no-fail` | | Demote a recency block to a warning. Does NOT bypass vulnerable blocks. |
| `--force` | | Proceed despite all findings (vulnerable, recent). Findings still print. Also bypasses the wrong-package-manager refusal and unparsable-lockfile refusals on `uv sync`/`npm ci`. |
| `--no-fail` | | Demote a recency block to a warning. Does NOT bypass vulnerable blocks or authenticated unverifiable blocks. |
| `--force` | | Proceed despite all findings (vulnerable, unverifiable, recent). Findings still print. Also bypasses the wrong-package-manager and PEP 668 refusals, and unparsable-lockfile refusals on `uv sync`/`npm ci`. |
| `--json` | | JSON report instead of text. Per-result `verdict` object + `verdict_mode` + `tree`. Stdout carries only the report; the package manager's output moves to stderr. |

`--json` adds `verdict_mode` (`"public"` from the CLI; `"recency-only"` can
only appear for library callers that disable verdicts) and a `tree` object:
`null` when no
tree pass ran; otherwise `mode` is `"full"` (transitive checked) or
`"named-only"` (with a `reason`), plus `resolved_count` and a `transitive[]`
array of `{name, version, origin, verdict}` for packages beyond the named
targets. Vulnerable `verdict` objects carry a `remediation` field: the safe
version covering every advisory, or `null` when any advisory has no known
fix.

Overrides for testing: `CORGEA_PYPI_REGISTRY`, `CORGEA_NPM_REGISTRY`,
`CORGEA_VULN_API_URL`.
`--json` adds `verdict_mode` (`"public"` or `"authenticated"` from the CLI;
`"recency-only"` can only appear for library callers that disable verdicts)
and a `tree` object: `null` when no tree pass ran; otherwise `mode` is
`"full"` (transitive checked) or `"named-only"` (with a `reason`), plus
`resolved_count` and a `transitive[]` array of `{name, version, origin,
verdict}` for packages beyond the named targets. Vulnerable `verdict`
objects carry a `remediation` field: the safe version covering every
advisory, or `null` when any advisory has no known fix.

Recency gating and 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. Overrides for testing:
`CORGEA_PYPI_REGISTRY`, `CORGEA_NPM_REGISTRY`, `CORGEA_VULN_API_URL`.

#### Limitations

Expand Down
76 changes: 67 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,22 +246,57 @@ struct InstallWrapArgs {
cmd: Vec<String>,
}

fn install_wrap_options(args: &InstallWrapArgs) -> corgea::precheck::PrecheckOptions {
fn install_wrap_options(
args: &InstallWrapArgs,
config: &Config,
) -> corgea::precheck::PrecheckOptions {
let token = config.get_token();
let token = token.trim();
let base_url = config::vuln_api_url();
let custom_vuln_api_url = base_url != config::DEFAULT_VULN_API_URL;
let send_token_to_custom =
utils::generic::get_env_var_if_exists("CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL")
.is_some_and(|v| v.trim() == "1");
let mode = select_verdict_mode(token, custom_vuln_api_url, send_token_to_custom);
corgea::precheck::PrecheckOptions {
threshold: args.threshold,
no_fail: args.no_fail,
force: args.force,
json: args.json,
verdict: Some(corgea::precheck::VerdictConfig {
base_url: config::vuln_api_url(),
base_url,
mode,
public_login_hint: token.is_empty(),
}),
npm_registry: utils::generic::get_env_var_if_exists("CORGEA_NPM_REGISTRY"),
pypi_registry: utils::generic::get_env_var_if_exists("CORGEA_PYPI_REGISTRY"),
}
}

fn run_install_wrap_command(manager: corgea::precheck::PackageManager, args: &InstallWrapArgs) {
let code = corgea::precheck::run_install(manager, &args.cmd, install_wrap_options(args));
/// 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.
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) {
corgea::precheck::VerdictMode::Authenticated {
token: token.to_string(),
}
} else {
corgea::precheck::VerdictMode::Public
}
}

fn run_install_wrap_command(
manager: corgea::precheck::PackageManager,
args: &InstallWrapArgs,
config: &Config,
) {
let code =
corgea::precheck::run_install(manager, &args.cmd, install_wrap_options(args, config));
std::process::exit(code);
}

Expand Down Expand Up @@ -571,19 +606,19 @@ fn main() {
// Install wrappers: no auth gate. Public CVE checks run without a
// token and fail open on lookup outages.
Some(Commands::Npm(args)) => {
run_install_wrap_command(corgea::precheck::PackageManager::Npm, args)
run_install_wrap_command(corgea::precheck::PackageManager::Npm, args, &corgea_config)
}
Some(Commands::Yarn(args)) => {
run_install_wrap_command(corgea::precheck::PackageManager::Yarn, args)
run_install_wrap_command(corgea::precheck::PackageManager::Yarn, args, &corgea_config)
}
Some(Commands::Pnpm(args)) => {
run_install_wrap_command(corgea::precheck::PackageManager::Pnpm, args)
run_install_wrap_command(corgea::precheck::PackageManager::Pnpm, args, &corgea_config)
}
Some(Commands::Pip(args)) => {
run_install_wrap_command(corgea::precheck::PackageManager::Pip, args)
run_install_wrap_command(corgea::precheck::PackageManager::Pip, args, &corgea_config)
}
Some(Commands::Uv(args)) => {
run_install_wrap_command(corgea::precheck::PackageManager::Uv, args)
run_install_wrap_command(corgea::precheck::PackageManager::Uv, args, &corgea_config)
}
None => {
if let Some(message) = corgea::precheck::pip3_alias_message(&cli.args) {
Expand All @@ -608,4 +643,27 @@ mod tests {
assert_eq!(default_log_level(2), "info"); // only ==1 means debug
assert_eq!(default_log_level(-1), "info");
}

#[test]
fn verdict_mode_selection_matrix() {
use corgea::precheck::VerdictMode;

assert_eq!(
select_verdict_mode("token", false, false),
VerdictMode::Authenticated {
token: "token".to_string()
}
);
assert_eq!(select_verdict_mode("", false, false), VerdictMode::Public);
assert_eq!(
select_verdict_mode("token", true, false),
VerdictMode::Public
);
assert_eq!(
select_verdict_mode("token", true, true),
VerdictMode::Authenticated {
token: "token".to_string()
}
);
}
}
97 changes: 95 additions & 2 deletions src/precheck/detect.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
//! Package-manager/project detection: wrong-manager guidance messages.
//! Package-manager/project detection: wrong-manager and
//! externally-managed-pip (PEP 668) guidance messages.

use std::ffi::OsString;
use std::path::Path;
use std::process::Command;

use super::{parse, PackageManager};
use super::{corgea_cmd, parse, PackageManager};

pub(super) fn wrong_package_manager_message(
manager: PackageManager,
Expand Down Expand Up @@ -268,6 +271,96 @@ fn is_plain_pip_target_install(rest: &[String], parsed: &parse::ParsedInstall) -
.all(|(arg, target)| arg == &target.display)
}

pub(super) fn externally_managed_pip_message(
manager: PackageManager,
rest: &[String],
_parsed: &parse::ParsedInstall,
) -> Option<String> {
if manager != PackageManager::Pip
|| pip_install_overrides_external_management(rest)
|| !pip_environment_is_externally_managed()
{
return None;
}

Some(format!(
"error: this Python environment is externally managed (PEP 668).\nCreate and activate a virtualenv, then retry `{}`.",
corgea_cmd(&["pip", "install"], rest)
))
}

fn pip_install_overrides_external_management(args: &[String]) -> bool {
const VALUE_FLAGS: [&str; 4] = ["-t", "--target", "--prefix", "--root"];
args.iter().any(|arg| {
arg == "--break-system-packages"
|| VALUE_FLAGS
.iter()
.any(|flag| arg == flag || arg.starts_with(&format!("{flag}=")))
})
}

fn pip_environment_is_externally_managed() -> bool {
// Every error arm falls open (`false`) deliberately — PEP 668 is a UX
// guard, not the vuln gate, and pip enforces it itself — but each is
// debug-traced so a silent miss is diagnosable.
let Ok(pip) = super::exec::resolve_binary("pip") else {
crate::log::debug("PEP 668 check skipped: pip not resolvable");
return false;
};
// PEP 668 markers live in a system interpreter's stdlib; pip inside an
// active virtualenv can't be externally managed - skip the spawn.
if let Some(venv) = std::env::var_os("VIRTUAL_ENV") {
if pip.starts_with(&venv) {
return false;
}
}
let Some(interpreter) = python_interpreter_from_shebang(&pip) else {
crate::log::debug("PEP 668 check skipped: no python shebang in pip");
return false;
};

let mut command = Command::new(&interpreter[0]);
command.args(&interpreter[1..]);
let Ok(output) = command.arg("-c").arg(EXTERNALLY_MANAGED_PYTHON).output() else {
crate::log::debug("PEP 668 check skipped: python spawn failed");
return false;
};
output.status.success() && String::from_utf8_lossy(&output.stdout).trim() == "1"
}

const EXTERNALLY_MANAGED_PYTHON: &str = r#"
import pathlib
import sysconfig

paths = []
for key in ("stdlib", "platstdlib"):
path = sysconfig.get_path(key)
if path and path not in paths:
paths.append(path)

print("1" if any((pathlib.Path(path) / "EXTERNALLY-MANAGED").is_file() for path in paths) else "0")
"#;

fn python_interpreter_from_shebang(path: &Path) -> Option<Vec<OsString>> {
let content = std::fs::read_to_string(path).ok()?;
let first = content.lines().next()?.strip_prefix("#!")?.trim();
let mut parts: Vec<&str> = first.split_whitespace().collect();
if parts.is_empty() {
return None;
}
if parts[0].ends_with("/env") || parts[0] == "env" {
parts.remove(0);
if parts.first() == Some(&"-S") {
parts.remove(0);
}
}
let executable = parts.first()?;
if !executable.contains("python") {
return None;
}
Some(parts.iter().map(OsString::from).collect())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading