Skip to content
Draft
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
7 changes: 7 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ http-body-util = "0.1"
url = "2.5"
open = "5.0"
urlencoding = "2.1"
semver = "1"

[target.'cfg(not(target_os = "windows"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }
83 changes: 83 additions & 0 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,62 @@ 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`.

### Verify Deps — `corgea verify-deps`

Supply-chain tripwire: looks up every pinned dependency in the project against the public registry (npm or PyPI) and flags anything whose installed version was published within a configurable recency window. Useful for catching very-recent malicious version pushes before they get baked into a build.

```bash
corgea verify-deps # 2-day window, prod deps, both ecosystems
corgea verify-deps --threshold 7d # widen the window to 7 days
corgea verify-deps --threshold 48h --fail # exit 1 if any recent dep is found (CI gate)
corgea verify-deps --fail-unpinned # exit 1 if any dep can't be verified because it isn't pinned
corgea verify-deps --ecosystem npm # only check npm deps
corgea verify-deps --ecosystem python --include-dev # python only, include dev deps
corgea verify-deps --path ./services/api # check a different project
corgea verify-deps --json # machine-readable output
```

| Flag | Short | Description |
|------|-------|-------------|
| `--ecosystem` | `-e` | `npm`, `python`, or `all` (default) |
| `--threshold` | `-t` | Recency window: `2d`, `48h`, `30m`, `1w`, etc. (default `2d`) |
| `--include-dev` | | Include development dependencies |
| `--fail` | `-f` | Exit non-zero if any recent dep is detected |
| `--fail-unpinned` | | Exit non-zero if any dep is unpinned (manifest with no lockfile, or unpinned `requirements.txt` line) |
| `--json` | | JSON output instead of human text |
| `--path` | `-p` | Project directory (default: `.`) |

Supported lockfiles (preferred → fallback): npm: `package-lock.json`, `npm-shrinkwrap.json`, `pnpm-lock.yaml` (v5/v6/v9), `yarn.lock`. Python: `poetry.lock`, `Pipfile.lock`, `uv.lock`, `requirements.txt` (only `==`-pinned lines).

### Precheck — `corgea precheck <pkg-mgr> <subcommand> [args...]`

Wraps an install command (`npm install`, `yarn add`, `pnpm add`, `pip install`), resolves what the package manager *would* install against the public registry, and refuses to run the install when a resolved version was published within `--threshold`. Use it as a thin replacement for the bare command in CI scripts or interactive shells.

```bash
corgea precheck npm install axios@^1.0.0 --save-dev
corgea precheck pnpm add @types/node@latest
corgea precheck yarn add lodash
corgea precheck pip install requests==2.31.0
corgea precheck pip install -r requirements.txt
corgea precheck npm install # bare install — verifies the lockfile
```

| Flag | Description |
|------|-------------|
| `--threshold <T>` (`-t`) | Recency window (`2d`, `48h`, `30m`, `1w`). Default `2d`. |
| `--no-fail` | Demote a recent finding from a hard block to a warning (install runs anyway). |
| `--check-only` | Run the verification but never exec the install. |
| `--fail-unpinned` | Also fail on unverifiable specs (URL/git/file/editable) and unpinned `requirements.txt` lines pulled in by `-r`. |
| `--json` | Machine-readable output. |

Spec resolution:

* **npm / yarn / pnpm** — `pkg`, `pkg@latest`, `pkg@1.2.3`, `pkg@^1.0.0`, `pkg@>=1.0.0 <2.0.0`, `pkg@next` (any dist-tag), and scoped names (`@types/node@...`). Ranges are resolved against the registry's full version list using `semver` semantics.
* **pip** — `pkg`, `pkg==1.2.3`, `pkg>=1,<2`, `pkg~=1.4`, `pkg[extras]==X`. Exact `==` pins are honoured precisely; other PEP 440 specifiers are resolved against PyPI's release list with a best-effort comparison.
* **Skipped (warning, not blocked)** — `git+...`, `file:...`, `./local`, `http(s)://...`, `npm:alias@...`, `workspace:*`, `pip -e`. These are explicit out-of-band sources we can't verify against a registry.

Subcommands other than `install` / `add` / `i` are forwarded straight through to the package manager unchanged, so `corgea precheck npm view ...` and similar just work.

## Common Workflows

### Scan full project
Expand Down Expand Up @@ -148,6 +204,33 @@ corgea scan --fail-on CR --out-format sarif --out-file results.sarif
corgea upload report.json --project-name my-app
```

### Block builds that pull in a freshly-published dependency

```bash
corgea verify-deps --threshold 2d --fail
```

### Require pinned, lockfile-resolved dependencies

```bash
corgea verify-deps --fail-unpinned
```

Use this together with `--fail` to gate both freshness and pinning in one CI step:

```bash
corgea verify-deps --threshold 2d --fail --fail-unpinned
```

### Pre-check an install before letting it run

```bash
corgea precheck npm install axios@^1.0.0
corgea precheck pip install -r requirements.txt --fail-unpinned
```

`corgea precheck` resolves the actual version a package manager would install, blocks if it was published within the threshold, and otherwise transparently runs the install (preserving the package manager's exit code).

### Export results

```bash
Expand Down
180 changes: 180 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ mod cicd;
mod log;
mod setup_hooks;
mod authorize;
mod verify_deps;
mod precheck;
mod scanners {
pub mod fortify;
pub mod blast;
Expand Down Expand Up @@ -156,6 +158,101 @@ enum Commands {
#[arg(long, short, help = "Include default config (scan types are pii, secrets and fail on levels are CR, HI, ME, LO).")]
default_config: bool,
},
/// Verify installed dependencies against the registry to flag recently published versions.
/// Useful as a supply-chain tripwire: any dep whose installed version was published within
/// the configured threshold will be reported. Currently supports npm and Python.
VerifyDeps {
#[arg(
long,
short = 'e',
default_value = "all",
help = "Which ecosystem(s) to verify. Valid options are 'npm', 'python', or 'all' (default)."
)]
ecosystem: String,

#[arg(
long,
short = 't',
default_value = "2d",
help = "Recency threshold. Any dependency published within this window is flagged. Examples: '2d' (default), '48h', '30m', '1w'. Bare numbers are interpreted as days."
)]
threshold: String,

#[arg(
long,
help = "Include development dependencies (default: production only)."
)]
include_dev: bool,

#[arg(
long,
short = 'f',
help = "Exit with a non-zero status code if any recently published dependency is found."
)]
fail: bool,

#[arg(
long,
help = "Exit with a non-zero status code if any dependency is unpinned (e.g. package.json without a lockfile, pyproject.toml/Pipfile without a matching lockfile, or unpinned `requirements.txt` lines). Independent of --fail."
)]
fail_unpinned: bool,

#[arg(
long,
help = "Output the result as JSON instead of human-readable text."
)]
json: bool,

#[arg(
long,
short = 'p',
help = "Path to the project to verify. Defaults to the current directory."
)]
path: Option<String>,
},
/// Pre-check a package install command against the registry, then run it.
/// Wraps `npm install`, `yarn add`, `pnpm add`, or `pip install` and refuses
/// to run when a resolved version was published within --threshold.
/// Examples:
/// corgea precheck npm install axios@^1.0.0 --save-dev
/// corgea precheck pip install requests
/// corgea precheck pnpm add @types/node@latest
Precheck {
#[arg(
long,
short = 't',
default_value = "2d",
help = "Recency threshold. Resolved versions younger than this are flagged. Same syntax as `verify-deps --threshold`."
)]
threshold: String,

#[arg(
long,
help = "Demote a recent finding from a hard block to a printed warning. The install still runs."
)]
no_fail: bool,

#[arg(
long,
help = "Run the verification but never exec the install command."
)]
check_only: bool,

#[arg(
long,
help = "Also fail when an unpinned/unverifiable spec (URL, git, file:, editable) is in the install command."
)]
fail_unpinned: bool,

#[arg(long, help = "Output the result as JSON instead of human-readable text.")]
json: bool,

/// Everything after `precheck` is forwarded to the package manager.
/// First positional must name the package manager: npm, yarn,
/// pnpm, pip.
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
cmd: Vec<String>,
},
}

#[derive(Subcommand, Debug, Clone, PartialEq)]
Expand Down Expand Up @@ -368,6 +465,89 @@ fn main() {
Some(Commands::SetupHooks { default_config }) => {
setup_hooks::setup_pre_commit_hook(*default_config);
}
Some(Commands::VerifyDeps { ecosystem, threshold, include_dev, fail, fail_unpinned, json, path }) => {
let parsed_ecosystem = match verify_deps::Ecosystem::parse(ecosystem) {
Ok(e) => e,
Err(e) => {
eprintln!("{}", e);
std::process::exit(2);
}
};
let parsed_threshold = match verify_deps::parse_threshold(threshold) {
Ok(t) => t,
Err(e) => {
eprintln!("Invalid --threshold: {}", e);
std::process::exit(2);
}
};
let project_path = std::path::PathBuf::from(path.clone().unwrap_or_else(|| ".".to_string()));
let opts = verify_deps::VerifyOptions {
ecosystem: parsed_ecosystem,
threshold: parsed_threshold,
include_dev: *include_dev,
fail: *fail,
fail_unpinned: *fail_unpinned,
json: *json,
path: project_path,
npm_registry: utils::generic::get_env_var_if_exists("CORGEA_NPM_REGISTRY"),
pypi_registry: utils::generic::get_env_var_if_exists("CORGEA_PYPI_REGISTRY"),
};

match verify_deps::run(&opts) {
Ok(report) => {
if opts.json {
verify_deps::report::print_json(&report);
} else {
verify_deps::report::print_text(&report);
}
let recent = !report.recent().is_empty();
let errors = !report.errors().is_empty();
let unpinned = report.has_unpinned();
if (recent || errors) && opts.fail {
std::process::exit(1);
}
if unpinned && opts.fail_unpinned {
std::process::exit(1);
}
}
Err(e) => {
eprintln!("verify-deps failed: {}", e);
std::process::exit(2);
}
}
}
Some(Commands::Precheck { threshold, no_fail, check_only, fail_unpinned, json, cmd }) => {
if cmd.is_empty() {
eprintln!("usage: corgea precheck <pkg-manager> <subcommand> [args...]");
std::process::exit(2);
}
let manager = match precheck::PackageManager::parse(&cmd[0]) {
Ok(m) => m,
Err(e) => {
eprintln!("{}", e);
std::process::exit(2);
}
};
let parsed_threshold = match verify_deps::parse_threshold(threshold) {
Ok(t) => t,
Err(e) => {
eprintln!("Invalid --threshold: {}", e);
std::process::exit(2);
}
};
let opts = precheck::PrecheckOptions {
manager,
threshold: parsed_threshold,
no_fail: *no_fail,
check_only: *check_only,
fail_unpinned: *fail_unpinned,
json: *json,
npm_registry: utils::generic::get_env_var_if_exists("CORGEA_NPM_REGISTRY"),
pypi_registry: utils::generic::get_env_var_if_exists("CORGEA_PYPI_REGISTRY"),
};
let exit_code = precheck::run(cmd, opts);
std::process::exit(exit_code);
}
None => {
utils::terminal::show_welcome_message();
let _ = Cli::command().print_help();
Expand Down
Loading
Loading