Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
de1e13b
Add vuln-api client + verified contract (install-vuln-gate spike)
juangaitanv Jun 9, 2026
5c303c5
Add corgea pip|npm|yarn|pnpm|uv install wrappers with recency gate
juangaitanv Jun 9, 2026
65b062b
Block vulnerable/unverifiable installs via vuln-api verdict
juangaitanv Jun 10, 2026
37deb2d
Gate the full would-install set via dry-run tree resolution
juangaitanv Jun 10, 2026
55544f4
Steer blocked installs to a safe version from verdict fix data
juangaitanv Jun 10, 2026
10693db
Scrub git hook env in harness pre-commit before running tests
juangaitanv Jun 11, 2026
a1212cd
Remove unused advisory-detail client path and stub route
juangaitanv Jun 11, 2026
6d90ca3
Fall back to pip3 when pip is missing; clearer missing-binary error
juangaitanv Jun 11, 2026
ce74ca5
Document deterministic staging targets for testing the install gate
juangaitanv Jun 11, 2026
3cc9d08
Gate bare npm installs; honest ungated note for yarn/pnpm/uv
juangaitanv Jun 11, 2026
4f30eb1
Label tree findings by provenance instead of blanket (transitive)
juangaitanv Jun 11, 2026
fdfa472
Fix u16 overflow in find_available_port range arithmetic
juangaitanv Jun 11, 2026
f322354
Verify the safe-version steer before printing it
juangaitanv Jun 11, 2026
aae9113
Distinguish existing-tree findings from named targets in install refusal
juangaitanv Jun 11, 2026
fa14fa0
Polish gate output: honest tokenless warning, progress line, collapse…
juangaitanv Jun 11, 2026
8a41c5e
Add warn-only npm audit second opinion to the install gate
juangaitanv Jun 11, 2026
9793e4b
Merge remote-tracking branch 'origin/ivg/u7-skill-testing-docs' into …
juangaitanv Jun 11, 2026
837e437
Merge remote-tracking branch 'origin/ivg/u2-bare-npm-gate' into insta…
juangaitanv Jun 11, 2026
d51d165
Merge remote-tracking branch 'origin/ivg/u8-refusal-context' into ins…
juangaitanv Jun 11, 2026
0a485ec
Merge remote-tracking branch 'origin/ivg/u1-steer-verify' into ivg-in…
juangaitanv Jun 11, 2026
52b20fa
Merge remote-tracking branch 'origin/ivg/u4-output-polish' into ivg-i…
juangaitanv Jun 11, 2026
665c610
Merge remote-tracking branch 'origin/ivg/u3-provenance-labels' into i…
juangaitanv Jun 11, 2026
d00706e
Merge remote-tracking branch 'origin/ivg/u6-npm-audit' into ivg-integ…
juangaitanv Jun 11, 2026
032761f
Integration cleanup: coherent fix hints, honest refusal blame, audit …
juangaitanv Jun 11, 2026
eb68a5e
Address review: unverifiable findings in refusal blame; reap audit child
juangaitanv Jun 11, 2026
c708958
Carry explicit bare-install context into the refusal blame
juangaitanv Jun 11, 2026
b6c2e83
Drop the standalone vuln-api-stub binary
juangaitanv Jun 11, 2026
adba36d
Refine install vuln gate precheck flow
juangaitanv Jun 11, 2026
bd877a9
Tighten install parsing helpers
juangaitanv Jun 11, 2026
c5a8d7b
Dedup integration-test scaffolding into common
juangaitanv Jun 11, 2026
2ef26e6
Consolidate vuln-api test stubs and collapse operator parsing
juangaitanv Jun 11, 2026
0f6b2b2
Collapse duplicated parsing and test-option boilerplate in precheck
juangaitanv Jun 11, 2026
7cb12f8
Deflake port_is_available test under parallel suite load
juangaitanv Jun 11, 2026
a3fee90
Dedup is_jwt across crates and drop the stub readiness sleep
juangaitanv Jun 11, 2026
0df65f5
Gate the vuln-api test stub out of release builds
juangaitanv Jun 11, 2026
bfc8cf1
Drop the --concurrency flag; fix verdict parallelism at 8
juangaitanv Jun 11, 2026
e62399c
Drop the steer re-verification pass
juangaitanv Jun 11, 2026
ccceb7a
Remove the warn-only npm audit second opinion
juangaitanv Jun 11, 2026
204fb47
Drop the persisted vuln_api_url config override; env var only
juangaitanv Jun 11, 2026
a928bcf
Skip yanked PyPI releases for non-exact resolution; fail closed on un…
juangaitanv Jun 11, 2026
5e28b1e
Close install-gate bypasses: normalize vuln-api names, fail closed on…
juangaitanv Jun 11, 2026
345b1a2
Point default vuln-api at staging worker
juangaitanv Jun 11, 2026
bb2a043
Improve install wrapper command guidance
juangaitanv Jun 11, 2026
6905b2f
Refine install gate verdict handling
juangaitanv Jun 11, 2026
928bdc0
Extract precheck detection module
juangaitanv Jun 11, 2026
1041bf5
Extract precheck exec module
juangaitanv Jun 11, 2026
67e6e85
Extract precheck render module
juangaitanv Jun 11, 2026
522dda1
Extract precheck verdict module
juangaitanv Jun 11, 2026
f5d8b79
Extract precheck uv module
juangaitanv Jun 11, 2026
e93a284
Deduplicate PyPI name extraction
juangaitanv Jun 11, 2026
ed60f96
Consolidate install gate test harnesses
juangaitanv Jun 11, 2026
a9951d7
Refine install gate verdict handling
juangaitanv Jun 11, 2026
7547a9e
Consolidate install gate vuln API handling
juangaitanv Jun 11, 2026
002dc66
Tighten install gate helpers
juangaitanv Jun 11, 2026
f4851db
Refine vuln verdict handling
juangaitanv Jun 12, 2026
6a484ad
Block bare externally managed pip installs
juangaitanv Jun 12, 2026
bb57f24
test node package manager lockfile guards
juangaitanv Jun 12, 2026
793f1ca
Ignore pip constraint files in precheck parsing
juangaitanv Jun 12, 2026
b2aea51
Keep JSON install output parseable
juangaitanv Jun 12, 2026
e227b4f
Fix dependency file detection robustness
juangaitanv Jun 12, 2026
438dc24
Gate lockfile installs through dependency checks
juangaitanv Jun 12, 2026
02cdda0
Improve pip requirements fallback checks
juangaitanv Jun 12, 2026
e149d6b
Retry transient vuln-api send failures
juangaitanv Jun 12, 2026
c879e11
Fail safe on verdict pool shortfall
juangaitanv Jun 12, 2026
4e53948
Test unverifiable spec passthrough end-to-end
juangaitanv Jun 12, 2026
dfac68e
Document install gate limitations
juangaitanv Jun 12, 2026
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
39 changes: 39 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,25 @@ 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"
reqwest = { version = "0.12.23", default-features = false, features = [
"blocking",
"cookies",
"gzip",
"json",
"multipart",
"native-tls",
Expand All @@ -19,6 +32,7 @@ reqwest = { version = "0.12.23", default-features = false, features = [
toml = "0.8.8"
log = "0.4"
env_logger = "0.11"
semver = "1"
serde = { version = "1.0.195", features = ["derive"] }
serde_json = "1.0.111"
serde_derive = "1.0.195"
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ pip install corgea-cli
You can get the latest binaries for your OS from https://github.com/Corgea/cli/releases.

### Setup
Once the binary is installed, login with your token from the Corgea app.
Once the binary is installed, login with your token from the Corgea app to enable
authenticated enforcement and private Corgea intelligence.
```
corgea login <token>
```
Expand Down Expand Up @@ -50,6 +51,19 @@ corgea deps policy init --exist-ok # write starter policy, or keep existing

See [Dependency Scanning (CLI)](https://docs.corgea.app/cli/deps) for the full flag and exit-code reference.

## Install Wrappers

`corgea pip|npm|yarn|pnpm|uv <args...>` runs package-manager install commands
through Corgea's install gate. Baseline CVE checks need no token: known vulnerable
or malicious package versions block, while vuln-api lookup outages warn and
continue in public fail-open mode.

Logging in enables authenticated enforcement against the default Corgea vuln-api,
including fail-closed behavior for lookup failures and any private Corgea
intelligence. A custom `CORGEA_VULN_API_URL` is public by default, even when
`CORGEA_TOKEN` exists. Set `CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` to send
the token to a custom URL and make lookup failures fail closed.

## Development Setup

### Prerequisites
Expand Down
6 changes: 6 additions & 0 deletions harness
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ $output"
}

cmd_pre_commit() {
# git exports GIT_DIR/GIT_INDEX_FILE/… to hooks. From a linked
# worktree GIT_DIR is absolute, so any `git init`/`git add` a test
# spawns in a tempdir would resolve to the shared gitdir and
# corrupt the real repo. Scrub the hook env before running tests.
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_OBJECT_DIRECTORY \
GIT_COMMON_DIR GIT_PREFIX
local staged; staged="$(staged_rs_files)"
if [ -z "$staged" ]; then
printf "No staged Rust files — skipping checks\n"
Expand Down
132 changes: 132 additions & 0 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,138 @@ corgea setup-hooks --default-config # Default: secrets + PII, fail on

Installs a pre-commit hook running `corgea scan blast --only-uncommitted`. Bypass with `git commit --no-verify`.

### Install Wrappers — `corgea pip|npm|yarn|pnpm|uv <args...>`

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. 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 and private Corgea intelligence; in
that mode, verdict lookup failures 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 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, like `uv sync`. Bare `yarn` (with or without the
`install` verb) and bare `pnpm` installs have no safe dry-run; they run
unchecked after a stderr note (`note: bare '<pm> <sub>' is not gated …`).
`-r requirements.txt` files get a printed note when the tree pass doesn't
cover them.

Wrapper flags (`--force`, `--no-fail`, `--json`, `-t`) are read between the
manager name and the install verb (`corgea npm --force install x`); flags
after the verb belong to the package manager and are forwarded untouched.

Blocked findings steer to the fix: each advisory line shows `fixed in <version>` (or
`no fixed version known`). When every advisory on a package has a fix, the gate
prints `→ safe version: <name>@<version>` — the highest fix covering every advisory.

The vuln check covers the **full would-install set** where the manager has a safe
resolver, 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), and
`uv pip install` / `uv add` / `uv pip sync` resolve theirs via `uv pip compile`;
every resolved package is verdicted, so a flagged **transitive** dependency blocks
the install too. `uv sync` is gated from `uv.lock` (found like uv finds it —
nearest ancestor). `yarn` and `pnpm` have no safe dry-run, so they verify the
named targets only and print
`warning: transitive dependencies not checked (…); only named packages were verified.`
The same warning is emitted (and the gate falls back to named-only) whenever a
dry-run fails or an npm flag redirects the project root (`--prefix`, `-g`).
Verdict requests run in a bounded pool (8 parallel).

```bash
corgea pip install requests==2.31.0 # resolves, checks recency + vuln verdict, then runs pip
corgea npm install axios@^1.0.0 # same gate for npm ranges
corgea pip --no-fail install newpkg # demote a recency block to a warning (vuln blocks still apply)
corgea pip --force install badpkg # print findings but install anyway (overrides every block)
corgea pip --json install newpkg # machine-readable per-target report incl. verdicts
corgea pip list # non-install subcommands pass straight through
```

| 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 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"` or `"authenticated"`) 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, 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` when present. A custom `CORGEA_VULN_API_URL` is public by default, even
when `CORGEA_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

The gate is a wrapper, not an enforcement boundary. By design it cannot catch:

- **Direct invocation** — running the package manager itself (`pip`, `npm`,
`python -m pip`) skips the gate entirely.
- **Custom indexes/registries** — `--index-url`, `--registry`, and `.npmrc`/
`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.
- **Ungated managers** — bare `yarn`/`pnpm` installs run unchecked (see the
bare-install note above); only their named targets are verified.

Hard enforcement needs org-level controls — lockfile review, registry
allow-listing — alongside the wrapper.

#### Testing the gate

The staging vuln-api (`https://cve-worker-staging.corgea.workers.dev`) serves
deterministic verdicts for dogfooding and is currently the default endpoint, so
with `CORGEA_TOKEN` set it runs authenticated with no extra setup. The explicit
`CORGEA_VULN_API_URL` + `CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1` below keep
that true even if the default endpoint moves (a custom URL is public-mode unless
the opt-in is set). Known-vulnerable targets:

| Ecosystem | Target | Verdict |
|-----------|--------|---------|
| npm | `axios@0.21.0` | vulnerable — fixed in 0.21.2 |
| npm | `minimist@0.0.8` | vulnerable — fixed in 1.2.2 |
| npm | `node-fetch@2.6.0` | vulnerable — fixed in 2.6.7 |
| PyPI | `mezzanine==6.0.0` | vulnerable — no fixed version known |

Verify the gate end-to-end:

```bash
CORGEA_TOKEN=dogfood-dummy \
CORGEA_VULN_API_URL=https://cve-worker-staging.corgea.workers.dev \
CORGEA_VULN_API_SEND_TOKEN_TO_CUSTOM_URL=1 \
corgea npm install axios@0.21.0
```

Expected output (exit code 1; nothing is installed):

```
Pre-checking `npm install axios@0.21.0` (threshold 2d)
1 ok, 0 recent, 1 vulnerable, 0 unverifiable, 0 skipped, 0 errors
tree: 2 packages resolved, 1 transitive checked
✗ axios@0.21.0 → axios@0.21.0 known vulnerable:
CVE-2021-3749 (high) — fixed in 0.21.2
CVE-2020-28168 (medium) — fixed in 0.21.1
→ safe version: axios@0.21.2
Refusing to run install. Pass --force to proceed despite findings.
```

Caveat: the staging PyPI seed covers recent CVEs only. Decade-old classics
(`pyyaml==5.1`, `django==2.2`) return clean **by design** — a clean verdict on
those does not mean the gate is broken.

<!-- BEGIN GENERATED CORGEA DEPS SKILL -->
### Deps — `corgea deps <command>`

Expand Down
14 changes: 12 additions & 2 deletions src/authorize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,8 @@ pub fn run(scope: Option<String>, url: Option<String>) -> Result<(), Box<dyn std
fn find_available_port(start_port: u16) -> Result<u16, Box<dyn std::error::Error>> {
// Try a more reliable approach - start from a higher range that's less likely to be used
let search_ranges = vec![
(start_port, start_port + 50),
// Saturate: a start port near u16::MAX must clamp, not overflow.
(start_port, start_port.saturating_add(50)),
(9000, 9100),
(8000, 8100),
(7000, 7100),
Expand Down Expand Up @@ -632,7 +633,16 @@ mod tests {

assert!(!port_is_available(port));
drop(listener);
assert!(port_is_available(port));
// The freed port returns to the OS ephemeral pool, where a parallel
// test's `bind(":0")` can snatch it before the re-check — so accept
// any of several freshly freed ports reading available. The chain is
// lazy: fresh ports are only reserved after a collision.
assert!(
std::iter::once(port)
.chain((0..4).map(|_| reserve_ephemeral_port()))
.any(port_is_available),
"five consecutive freed ports all read unavailable"
);
}

#[test]
Expand Down
37 changes: 37 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::{env, fs, io};

pub const DEFAULT_VULN_API_URL: &str = "https://cve-worker-staging.corgea.workers.dev";

#[derive(Serialize, Deserialize, Clone)]
pub struct Config {
pub(crate) url: String,
Expand Down Expand Up @@ -101,3 +103,38 @@ impl Config {
self.debug
}
}

/// 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")
.unwrap_or_else(|| DEFAULT_VULN_API_URL.to_string())
.trim()
.trim_end_matches('/')
.to_string()
}

#[cfg(test)]
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");
}
}
Loading
Loading