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
27 changes: 21 additions & 6 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,24 @@ 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
gated). Bare installs (no named targets) and `-r requirements.txt` files are
noted, not gated. `npm ci` passes through ungated.
gated). Bare `npm install` (zero specs, project `package.json` found like npm
finds it — nearest ancestor) is gated too: the full lockfile-resolved tree is
verdicted, so a vulnerable lockfile blocks. `npm ci` (and aliases) is gated
from the project lockfile directly.

The vuln check covers the **full would-install set**, not just the named
targets: `pip` and `npm` resolve the complete tree (named + transitive) via a
safe dry-run (`pip install --dry-run …`; an isolated
`npm install --package-lock-only` in a temp dir, never touching your
lockfile); every resolved package is verdicted, so a flagged **transitive**
dependency blocks the install too, labeled by provenance (`(transitive)`,
`(from requirements)`, `(already in package.json)`, `(locked)`). Whenever a
dry-run fails or an npm flag redirects the project root (`--prefix`, `-g`),
the gate falls back to named-only and prints
`warning: transitive dependencies not checked (…); only named packages were verified.`
— for pip, entries of `-r requirements.txt` files are still parsed and
verified in that fallback. Verdict requests run in a bounded pool
(8 parallel).

Wrapper flags (`--force`, `--no-fail`, `-t`) are read between the manager
name and the install verb (`corgea npm --force install x`); flags after the
Expand Down Expand Up @@ -182,10 +198,9 @@ The gate is a wrapper, not an enforcement boundary. By design it cannot catch:
`pip.conf` overrides change where packages resolve from. The gate still
verdicts each `name@version`, but it cannot vouch that a substituted
registry serves the same artifact those advisories describe.
- **Transitive dependencies** — only the named install targets are verified;
the rest of the resolved tree installs unchecked.
- **Bare installs and lockfiles** — `npm install` with no targets, `npm ci`,
and `-r requirements.txt` files run unchecked after a note.
- **Named-only fallback** — when a dry-run fails (old pip, broken resolution)
or `--prefix`/`-g` redirects npm's root, transitive dependencies install
unchecked behind the printed warning.

Hard enforcement needs org-level controls — lockfile review, registry
allow-listing — alongside the wrapper.
Expand Down
35 changes: 18 additions & 17 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,15 @@ impl Config {
/// Base URL for the vuln-api service: `CORGEA_VULN_API_URL` env var,
/// then the public default. Pure env/constant — no config file field.
pub fn vuln_api_url() -> String {
crate::utils::generic::get_env_var_if_exists("CORGEA_VULN_API_URL")
resolve_vuln_api_url(crate::utils::generic::get_env_var_if_exists(
"CORGEA_VULN_API_URL",
))
}

/// Pure resolution rule, split out so tests never mutate process-global
/// env (`set_var` races concurrent `getenv` under the parallel harness).
fn resolve_vuln_api_url(override_url: Option<String>) -> String {
override_url
.unwrap_or_else(|| DEFAULT_VULN_API_URL.to_string())
.trim()
.trim_end_matches('/')
Expand All @@ -118,23 +126,16 @@ pub fn vuln_api_url() -> String {
mod tests {
use super::*;

/// All `vuln_api_url` cases in one test fn: the env-var cases
/// mutate process-global state, so they must not run concurrently
/// with each other under the parallel test harness.
#[test]
fn vuln_api_url_resolution_order() {
env::remove_var("CORGEA_VULN_API_URL");

// Default when the env var is unset.
assert_eq!(vuln_api_url(), DEFAULT_VULN_API_URL);

// Env var wins; whitespace and trailing slash trimmed.
env::set_var("CORGEA_VULN_API_URL", " https://env.example.com/ ");
assert_eq!(vuln_api_url(), "https://env.example.com");

// Empty / whitespace-only env var is treated as unset.
env::set_var("CORGEA_VULN_API_URL", " ");
assert_eq!(vuln_api_url(), DEFAULT_VULN_API_URL);
env::remove_var("CORGEA_VULN_API_URL");
// Default when the env var is unset (`get_env_var_if_exists`
// already maps empty/whitespace-only values to None).
assert_eq!(resolve_vuln_api_url(None), DEFAULT_VULN_API_URL);

// Override wins; whitespace and trailing slash trimmed.
assert_eq!(
resolve_vuln_api_url(Some(" https://env.example.com/ ".to_string())),
"https://env.example.com"
);
}
}
6 changes: 5 additions & 1 deletion src/deps/ecosystems/npm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,11 @@ fn parse_npm_lock(path: &Path) -> Result<HashMap<String, LockPackage>, DepsError
Ok(out)
}

fn package_name_from_lock_key(key: &str) -> &str {
/// Package name from a lockfile `packages` key: the path after the last
/// `node_modules/` (or the whole key), truncated to one component — two for
/// scoped names. Also shared with the install gate's lockfile parse
/// (`precheck::tree`).
pub(crate) fn package_name_from_lock_key(key: &str) -> &str {
let package_path = key
.rsplit_once("node_modules/")
.map(|(_, name)| name)
Expand Down
Loading
Loading