Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

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

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "corgea"
version = "1.10.1"
version = "1.11.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,48 @@ evaluated; override with `CORGEA_BLOCKING_RULES_TIMEOUT_SECONDS`.
trips: both are written before `--fail`/`--block-on` are evaluated, so a scan
that exits 1 on a blocking rule still leaves its report behind to ingest.

### Skipping a re-scan of the same commit

A pipeline that re-runs on an unchanged commit can reuse the scan it already
has instead of paying for a duplicate:

```bash
corgea scan --skip-if-commit-scanned-recently --block-on criticals
corgea scan --skip-if-commit-scanned-recently --scanned-within 4h # 90s, 30m, 4h, 7d
```

When the project already has a completed scan of the current commit inside the
window (24h by default), that scan takes the new scan's place: the results
table, the `--block-on` gate and its exit code, and any `--out-file` report all
come from it, so the pipeline behaves the same whether or not a scan ran. The
window exists because unchanged code is still exposed to advisories published
since it was last scanned.

Two lines make the outcome scriptable — `CORGEA_SCAN_SKIPPED=true` plus
`CORGEA_SCAN_ID=<id>` when a scan was reused, `CORGEA_SCAN_SKIPPED=false` when
one ran — so a later step (an ingest, say) can branch on it.

Only a scan that answers the same question is reused, which is stricter than
"same commit". A candidate has to be a completed BLAST scan of that commit, on a
branch rather than a pull request, from an explicitly clean worktree, with no
scanner problems reported — and this run has to be a default whole-commit scan
itself. Anything else runs a real scan: nothing inside the window, only a failed
or still-running scan, a worktree that does not match the commit (including
files the index hides from `git status`), or a lookup the platform could not
answer.

Two things are hard errors instead. An unresolvable commit (not a git
repository, or no commits yet) exits 1 rather than silently scanning. And a run
that changes what gets scanned cannot be matched against a candidate — the API
exposes neither a scan's configured scan types and target policies nor whether
it bundled a container image — so the flag is rejected alongside `--scan-type`,
`--policy`, `--include-image`, `--only-uncommitted`, and `--target`.

`--exclude` is allowed, and warns on a skip. A reused scan is one of the whole
commit (an `--exclude` upload is recorded as not matching the commit exactly, so
it is never itself reusable), which means the results and the gate can cover
files this run would have skipped — over-reporting, never under-reporting.

## Dependency Inventory (offline)

`corgea deps` builds a dependency inventory from npm, Python, and Java manifests
Expand Down
7 changes: 7 additions & 0 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ corgea scan --sbom sbom.cdx.json # SBOM to a custom file
corgea scan --include-image myapp:1.2.3 # Also scan a fully built container image
corgea scan --include-image myapp:1.2.3 --include-image ghcr.io/acme/api:latest # Repeatable
corgea scan --project-name my-service # Override project name
corgea scan --skip-if-commit-scanned-recently # Reuse a recent scan of this commit instead of scanning again
corgea scan --skip-if-commit-scanned-recently --scanned-within 4h # Window for "recently" (default 24h)
```

Scan types: `blast` (base AI), `policy` (PolicyIQ), `malicious`, `secrets`, `pii`.
Expand All @@ -62,6 +64,10 @@ An included image is enough on its own: when it is combined with `--only-uncommi

`--out-format`/`--out-file` and `--sbom` are honored regardless of the gate: the report and the SBOM are written before `--fail`/`--block-on` are evaluated, so a scan that exits 1 on a blocking rule still leaves the report file behind for the pipeline to ingest.

`--skip-if-commit-scanned-recently` reuses the project's most recent reusable scan of the current commit instead of starting a duplicate, when one ran inside the `--scanned-within` window (default `24h`; accepts `90s`, `30m`, `4h`, `7d`, and a bare number as hours). The reused scan takes the new scan's place for the rest of the command — results table, `--block-on` gate and its exit code, `--out-file` report — so the pipeline behaves the same either way. It prints `CORGEA_SCAN_SKIPPED=true` plus `CORGEA_SCAN_ID=<id>` on a reuse and `CORGEA_SCAN_SKIPPED=false` when a scan runs, so a later step can branch on it.

Reuse requires a candidate that answers the same question: a completed `corgea-blast` scan of that commit, on a branch rather than a pull request, from an explicitly clean worktree, reporting no scanner problems. Anything else runs a real scan (nothing in the window, a failed or still-running scan, a worktree that does not match the commit including files hidden from `git status`, or a failed lookup). An unresolvable commit is a hard error (exit 1). Because the API exposes neither a scan's configured scan types and target policies nor whether it bundled a container image, a run that changes what gets scanned cannot be matched against a candidate, so the flag cannot be combined with `--scan-type`, `--policy`, `--include-image`, `--only-uncommitted`, or `--target`. `--exclude` is allowed but warns on a skip: what gets reused is a scan of the whole commit, so the results and the gate can cover files the run would have skipped (over-reporting, never under-reporting).

### Upload — `corgea upload [report]`

Upload an existing scan report to Corgea.
Expand Down Expand Up @@ -387,6 +393,7 @@ corgea inspect --issue --diff ISSUE_ID
corgea scan --fail-on CR --out-format sarif --out-file results.sarif
corgea scan --fail-on CR,malicious --out-format sarif --out-file results.sarif # also block malicious dependencies
corgea scan --block-on criticals --out-format sarif --out-file results.sarif # gate on a CI blocking rule from the web app
corgea scan --block-on criticals --skip-if-commit-scanned-recently # re-runs of an already-scanned commit gate on that scan instead of rescanning
```

The report is written whether or not the gate trips, so a pipeline can both fail
Expand Down
38 changes: 38 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod log;
mod scan;
mod setup_hooks;
mod skill;
mod skip_scan;
mod wait;
mod scanners {
pub mod blast;
Expand Down Expand Up @@ -169,6 +170,21 @@ enum Commands {
help = "Scan a fully built container image (repeatable), e.g. --include-image myapp:1.2.3 --include-image ghcr.io/acme/api:latest. Each image is exported with docker (or podman), pulled first if it isn't available locally, and uploaded with your project. Corgea scans the images you pass instead of searching your code for base images. Requires container scanning to be enabled for your account."
)]
include_image: Vec<String>,

#[arg(
long = "skip-if-commit-scanned-recently",
conflicts_with_all = ["only_uncommitted", "target", "scan_type", "policy", "include_image"],
help = "Do not start a new scan when this commit already has a recent completed scan in the project. That scan then drives the rest of the command — results table, --block-on gate, --out-file report — so the pipeline behaves the same either way. Prints CORGEA_SCAN_SKIPPED=true/false so a pipeline can tell the two apart, and fails if no git commit can be resolved. What can be reused is a scan of the whole commit, and no API tells this run how a past scan was scoped or configured, so the flag is refused with --only-uncommitted, --target, --scan-type, --policy and --include-image; with --exclude it warns instead, since a reused scan covers files this run would have skipped."
)]
skip_if_commit_scanned_recently: bool,

#[arg(
long = "scanned-within",
value_name = "DURATION",
requires = "skip_if_commit_scanned_recently",
help = "How recent a prior scan of the same commit must be for --skip-if-commit-scanned-recently to reuse it, e.g. 90s, 30m, 24h, 7d (a bare number means hours). Defaults to 24h, because unchanged code is still exposed to advisories published since it was last scanned."
)]
scanned_within: Option<String>,
},
/// Wait for the latest in progress scan
Wait {
Expand Down Expand Up @@ -648,6 +664,8 @@ fn main() {
project_name,
sbom,
include_image,
skip_if_commit_scanned_recently,
scanned_within,
}) => {
verify_token_and_exit_when_fail(&corgea_config);
if let Some(level) = fail_on {
Expand Down Expand Up @@ -784,6 +802,25 @@ fn main() {
}
};

if *skip_if_commit_scanned_recently && *scanner != Scanner::Blast {
::log::error!(
"skip-if-commit-scanned-recently is only supported with blast scanner."
);
std::process::exit(1);
}

let skip_recent = if *skip_if_commit_scanned_recently {
match skip_scan::SkipRecentScan::new(scanned_within.as_deref()) {
Ok(skip) => Some(skip),
Err(msg) => {
::log::error!("{}", msg);
std::process::exit(1);
}
}
} else {
None
};

match scanner {
Scanner::Snyk => scan::run_snyk(&corgea_config, project_name.clone()),
Scanner::Semgrep => scan::run_semgrep(&corgea_config, project_name.clone()),
Expand All @@ -803,6 +840,7 @@ fn main() {
project_name.clone(),
sbom.clone(),
include_images,
skip_recent,
),
}
}
Expand Down
Loading
Loading