Skip to content

Add --skip-if-commit-scanned-recently to reuse a recent scan of the commit - #157

Open
Ibrahimrahhal wants to merge 1 commit into
mainfrom
cursor/skip-if-commit-scanned-recently-ec52
Open

Add --skip-if-commit-scanned-recently to reuse a recent scan of the commit#157
Ibrahimrahhal wants to merge 1 commit into
mainfrom
cursor/skip-if-commit-scanned-recently-ec52

Conversation

@Ibrahimrahhal

@Ibrahimrahhal Ibrahimrahhal commented Aug 13, 2026

Copy link
Copy Markdown
Member

What

Two new flags on corgea scan:

  • --skip-if-commit-scanned-recently — do not start a new scan when the project already has a completed scan of the current commit inside the window.
  • --scanned-within <DURATION> — what "recently" means (90s, 30m, 24h, 7d; a bare number is hours). Defaults to 24h, because unchanged code is still exposed to advisories published since it was last scanned.

Why

This replaces the duplicate-scan skip logic a customer hand-rolled in their Harness pipeline: corgea list --json, compare git_sha against CI_COMMIT_SHA, apply a 24h staleness window, paginate list --issues for prior counts, then re-derive a pass/fail from those counts. That last step is the broken part — it has no idea which blocking rules apply to CI or which findings had valid exceptions, and with --block-on driving enforcement there was no way to replay the prior scan's verdict.

How it behaves

Skipping the scan is only half the job, so the reused scan takes the new scan's place for the rest of the command rather than short-circuiting it:

  • the results table, the --block-on gate and its exit code, --out-file/--out-format, --fail-on, and --sbom all run against the reused scan
  • blocking rules are evaluated server-side against that scan with the same slug filter, so CI-vs-PR rule scoping and exceptions apply exactly as they would to a fresh scan
  • the report-before-the-gate guarantee from Write the scan report and SBOM before the blocking-rule gates exit #156 holds on this path too: a skipped scan that exits 1 on a blocking rule still leaves its report file behind

Two lines are printed once per run so a later pipeline step (e.g. an ingest) can branch on the outcome:

CORGEA_SCAN_SKIPPED=true
CORGEA_SCAN_ID=<scan-id>

…or CORGEA_SCAN_SKIPPED=false when a scan actually ran.

When reuse is refused

Every one of these runs a normal scan, because scanning again is the safe outcome when the evidence is incomplete:

  • no scan of the commit inside the window
  • the commit's newest scan failed or is still running (an older completed scan of the same commit is still eligible)
  • the prior scan ran against a worktree with uncommitted changes
  • the working tree is dirty now, so the commit does not describe what would be scanned
  • the lookup failed (network, auth, 5xx) or the timestamp could not be read

The one hard failure is an unresolvable commit (not a git repo, no commits): that exits 1, because the flag asks a question about the commit and quietly scanning would hide that the pipeline is not getting the behavior it asked for.

Reuse is for whole-commit scans, so the flag is rejected alongside --only-uncommitted, --target, and --exclude rather than silently gating on a superset of the requested files.

Implementation notes

  • The lookup is GET /api/v1/scans?project=…&sha=…, which doghouse already filters server-side. No backend change is needed.
  • A backend predating that filter ignores the unknown parameter and answers with every scan of the project, so the returned git_sha is re-checked client-side — otherwise one commit's scan would be skipped because a different commit was scanned.
  • ScanResponse gains worktree_dirty (tri-state). Only a known-dirty scan is disqualified; a scan that never reported the flag (older CLI, platform integration) is not evidence of local edits.
  • The post-scan half of blast::run (results, report, SBOM, gates) is now shared by both paths; packaging/upload/wait moved into start_new_scan. The moved block is byte-for-byte the one Write the scan report and SBOM before the blocking-rule gates exit #156 left behind, ordering included — no behavior change on the normal path.
  • Version bumped to 1.11.0: a new flag is a minor bump per SemVer, on top of 1.10.1.

Testing

./harness ci — strict clippy, format, dep audit, 710 tests, coverage gate.

  • Unit tests in src/skip_scan.rs cover window parsing (including the values that would silently disable the check), newest-first selection, fall-through past a failed scan, the wrong-commit guard, dirty/unknown worktree state, every timestamp shape the API emits, window edges, and clock skew.
  • E2E tests in tests/cloud_commands_e2e/scan_skip.rs assert the exact request sequence, so "no new scan was started" is proven by the absence of the upload calls: a skipped scan still exits 1 on the prior scan's blocking rules and still writes its SARIF report first, a stale scan and a shorter --scanned-within both fall back to a real scan, a dirty worktree never even performs the lookup, an unresolvable commit fails before any upload, and --scanned-within alone is rejected.
Open in Web Open in Cursor 

@Ibrahimrahhal
Ibrahimrahhal marked this pull request as ready for review August 13, 2026 13:11
Comment thread src/skip_scan.rs
}

impl SkipRecentScan {
/// Build from the raw `--scanned-within` value, defaulting when absent.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality - The code comments mention the flag "--scanned-within", but module docs say "--skip-if-commit-scanned-recently". This inconsistency confuses what flag controls the behavior. View in Corgea ↗

More Details
🎟️Issue Explanation: The code comments mention the flag "--scanned-within", but module docs say "--skip-if-commit-scanned-recently". This inconsistency confuses what flag controls the behavior.

- Inconsistent flag names in "/// Build from the raw --scanned-within value" vs module docs cause unclear interface understanding.
- Developers may misinterpret or misuse flags, increasing onboarding time and errors in CLI interactions.
- Maintenance is harder as flag name mismatches require extra effort to track actual supported flags in code vs docs.

We could not generate a fix for this.

Comment thread src/skip_scan.rs
}
}

/// Parse a `--scanned-within` value: `90s`, `30m`, `24h`, `7d`. A bare number

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality - The code uses inconsistent flag names: module docs mention "--skip-if-commit-scanned-recently", but comments and errors use "--scanned-within". This mismatch confuses users and developers alike. View in Corgea ↗

More Details
🎟️Issue Explanation: The code uses inconsistent flag names: module docs mention "--skip-if-commit-scanned-recently", but comments and errors use "--scanned-within". This mismatch confuses users and developers alike.

- Inconsistent flag names in "--skip-if-commit-scanned-recently" docs vs "--scanned-within" comments make the CLI interface unclear.
- Developers maintaining or extending flag logic must guess which name is accurate, raising error risk and slowing updates.
- Users reading docs or errors might attempt unsupported flags due to conflicting descriptions, hurting usability and trust.

We could not generate a fix for this.

Comment thread src/scanners/blast.rs
Comment on lines +251 to +262
/// Returns the new scan's id and, when the server reported one, its project id.
#[allow(clippy::too_many_arguments)]
fn start_new_scan(
config: &Config,
project_name: &str,
only_uncommitted: &bool,
metadata: Option<String>,
scan_type: Option<String>,
policy: Option<String>,
target: Option<String>,
exclude: Option<String>,
) -> (String, Option<String>) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality - The "start_new_scan" function has many parameters, including multiple optional flags like "target", "exclude", and "only_uncommitted". This makes the signature complex and hard to manage. View in Corgea ↗

More Details
🎟️Issue Explanation: The "start_new_scan" function has many parameters, including multiple optional flags like "target", "exclude", and "only_uncommitted". This makes the signature complex and hard to manage.

- Large parameter lists reduce code readability and make the function harder to use correctly, seen in "start_new_scan" with 8 parameters.
- Optional and related parameters increase complexity in managing function logic and increase the chance of errors.
- Maintenance becomes expensive as every new option requires changing the "start_new_scan" signature, impacting scalability and team velocity.

🪄Fix Explanation: Groups scan-related parameters into "StartNewScanOptions", reducing "start_new_scan" from many arguments to one cohesive options value. This removes the lint suppression and makes the API easier to read and extend.
<bullet_point>"StartNewScanOptions" groups related optional settings, making the scan configuration explicit and easier to understand. <bullet_point>The function now accepts only "config", "project_name", and "options", reducing call-site complexity and preventing argument-order mistakes. <bullet_point>Destructuring "options" at the function boundary keeps the existing implementation readable while preserving direct access to each setting. <bullet_point>Passing "only_uncommitted" as a "bool" avoids the unnecessary "&bool" reference and removes the need for "#[allow(clippy::too_many_arguments)]".</bullet_point>

💡Important Instructions: Update every caller of start_new_scan to construct a StartNewScanOptions value, keeping option initialization grouped and consistently ordered.
Suggested change
/// Returns the new scan's id and, when the server reported one, its project id.
#[allow(clippy::too_many_arguments)]
fn start_new_scan(
config: &Config,
project_name: &str,
only_uncommitted: &bool,
metadata: Option<String>,
scan_type: Option<String>,
policy: Option<String>,
target: Option<String>,
exclude: Option<String>,
) -> (String, Option<String>) {
struct StartNewScanOptions {
only_uncommitted: bool,
metadata: Option<String>,
scan_type: Option<String>,
policy: Option<String>,
target: Option<String>,
exclude: Option<String>,
}
/// Returns the new scan's id and, when the server reported one, its project id.
fn start_new_scan(
config: &Config,
project_name: &str,
options: StartNewScanOptions,
) -> (String, Option<String>) {
let StartNewScanOptions {
only_uncommitted,
metadata,
scan_type,
policy,
target,
exclude,
} = options;

Comment thread src/scanners/blast.rs Outdated
out_format: Option<String>,
out_file: Option<String>,
) {
if let Some(out_file) = out_file {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality - The "write_scan_report" function silently skips output when only one of "out_file" or "out_format" is set, or when "out_format" is invalid, misleading users. This breaks expected control flow and UX. View in Corgea ↗

More Details
🎟️Issue Explanation: The "write_scan_report" function silently skips output when only one of "out_file" or "out_format" is set, or when "out_format" is invalid, misleading users. This breaks expected control flow and UX.

- Silent skipping in "if let Some(out_file)" with missing "out_format" causes misleading success perception.
- Unknown "out_format" values bypass writing logic, confusing users and breaking output expectations.
- Control flow lacks explicit validation/error handling, increasing debugging complexity and risking silent pipeline failures.

🪄Fix Explanation: The update aligns control flow with the output-generation algorithm by validating options before processing them. It prevents incomplete output configuration and unsupported formats from reaching the report-writing logic.
""out_file.is_some() != out_format.is_some()" detects when only one output option is supplied, preventing invalid partial configuration."
""matches!(out_format, "json" | "html" | "sarif")" restricts formats to supported values and avoids undefined downstream behavior."
"Immediate error logging and process termination provide fail-fast behavior, making configuration errors clear and easier to diagnose."
"Validation occurs before "if let Some(out_file)", keeping the main output path focused on valid report generation."
Suggested change
if let Some(out_file) = out_file {
if out_file.is_some() != out_format.is_some() {
log::error!("--out-file and --out-format must be provided together.");
std::process::exit(1);
}
if let Some(out_format) = out_format.as_deref() {
if !matches!(out_format, "json" | "html" | "sarif") {
log::error!("Unsupported output format '{}'. Supported formats are: json, html, sarif.", out_format);
std::process::exit(1);
}
}
if let Some(out_file) = out_file {

Comment thread src/scanners/blast.rs
Comment on lines +100 to +116
let classifications = match report_scan_status(&config.get_url(), &project_name, &scan_id) {
Ok(issues_classes) => {
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
println!(
"\n\nYou can view the scan results at the following link:\n{}",
utils::terminal::set_text_color(&scan_url, utils::terminal::TerminalColor::Green)
);
issues_classes
}
Err(e) => {
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
log::error!(
"\r{}\n\n{}\n\n\
However, the scan results may still be accessible at the following link:\n\n\
{}\n\n\

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Quality - The code duplicates cleanup logic like &quot;*stop_signal.lock().unwrap() = true;&quot; and &quot;results_thread.join()&quot; in multiple branches. This repetition risks inconsistencies and harder updates. View in Corgea ↗

More Details
🎟️Issue Explanation: The code duplicates cleanup logic like `"*stop_signal.lock().unwrap() = true;"` and `"results_thread.join()"` in multiple branches. This repetition risks inconsistencies and harder updates.

- Duplicated `"stop_signal.lock().unwrap() = true;"` risks forgetting to stop the spinner, causing UI glitches or resource leaks.
- Multiple `"results_thread.join()"` calls increasing chance of missing thread joins, leading to potential deadlocks or thread panics.
- Scattered cleanup logic makes it hard to maintain and extend, especially with early returns that could skip essential terminal resets.

🪄Fix Explanation: The repeated spinner shutdown logic is extracted into the "stop_spinner" closure and reused across success and error paths. This reduces duplication, improves consistency, and makes future changes easier.
- "stop_spinner" centralizes setting "stop_signal" and joining "results_thread" in one location.
- Both "Ok" and "Err" branches now call "stop_spinner()", ensuring identical cleanup behavior.
- Removing duplicated synchronization and thread-joining code improves readability and reduces maintenance risk.
- Future changes to spinner shutdown only need to be made inside the closure.
diff --git a/src/scanners/blast.rs b/src/scanners/blast.rs
index 03515aa..839df2c 100644
--- a/src/scanners/blast.rs
+++ b/src/scanners/blast.rs
@@ -97,10 +97,13 @@ pub fn run(
         );
     });
 
+    let stop_spinner = || {
+        *stop_signal.lock().unwrap() = true;
+        let _ = results_thread.join();
+    };
     let classifications = match report_scan_status(&config.get_url(), &project_name, &scan_id) {
         Ok(issues_classes) => {
-            *stop_signal.lock().unwrap() = true;
-            let _ = results_thread.join();
+            stop_spinner();
             println!(
                 "\n\nYou can view the scan results at the following link:\n{}",
                 utils::terminal::set_text_color(&scan_url, utils::terminal::TerminalColor::Green)
@@ -108,8 +111,7 @@ pub fn run(
             issues_classes
         }
         Err(e) => {
-            *stop_signal.lock().unwrap() = true;
-            let _ = results_thread.join();
+            stop_spinner();
             log::error!(
                 "\r{}\n\n{}\n\n\
                 However, the scan results may still be accessible at the following link:\n\n\

To apply the fix, Download .patch.

Comment thread src/scanners/blast.rs Outdated
match corgea::deps::report::sbom(std::path::Path::new(".")) {
Ok(doc) => {
let json = serde_json::to_string_pretty(&doc).expect("serialize SBOM");
if let Err(e) = fs::write(&sbom_file, json) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Path Traversal (🔒 Security, 🔴 High) - The code uses a file path from external input without properly cleaning it, which can allow accessing files outside the intended directory. View in Corgea ↗

More Details
🎟️Issue Explanation: The code uses a file path from external input without properly cleaning it, which can allow accessing files outside the intended directory.

- Using "sbom_file" directly lets attackers add "../" to access parent folders, escaping the restricted directory.
- This can lead to overwriting or reading sensitive files, since "fs::write" writes to any resolved path.
- An attacker could exploit this by giving a path like "../../etc/passwd" to break directory limits and affect critical files.

🪄Fix Explanation: The fix prevents SBOM writes outside the current working directory by rejecting traversal and absolute path components, requiring a JSON filename, and validating the canonical target before writing.
This blocks attacker-controlled paths from escaping the intended output directory.
- "requested_path.components()" rejects "ParentDir" and "RootDir", blocking ".." traversal and absolute paths.
- "file_name()" limits the output to a single filename, while the extension check requires a ".json" file.
- The current directory is canonicalized as "canonical_root", and the requested filename is joined beneath it.
- Existing targets are canonicalized to resolve symlinks; "starts_with(&canonical_root)" rejects paths that escape the output directory.
- The write uses "canonical_target" rather than the untrusted "sbom_file" path.
Suggested change
if let Err(e) = fs::write(&sbom_file, json) {
let requested_path = std::path::Path::new(&sbom_file);
if requested_path.components().any(|component| matches!(component, std::path::Component::ParentDir | std::path::Component::RootDir)) {
log::error!("拒Invalid SBOM output path '{}': path traversal is not allowed", sbom_file);
std::process::exit(1);
}
let file_name = match requested_path.file_name() {
Some(file_name) if requested_path.extension().and_then(|extension| extension.to_str()) == Some("json") => file_name,
_ => {
log::error!("Invalid SBOM output file '{}': a JSON file name is required", sbom_file);
std::process::exit(1);
}
};
let canonical_root = match env::current_dir().and_then(|path| fs::canonicalize(path)) {
Ok(path) => path,
Err(e) => {
log::error!("Failed to determine the SBOM output directory: {}", e);
std::process::exit(1);
}
};
let joined_path = canonical_root.join(file_name);
let canonical_target = match fs::canonicalize(&joined_path) {
Ok(path) => path,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => joined_path,
Err(e) => {
log::error!("Failed to resolve SBOM output path '{}': {}", sbom_file, e);
std::process::exit(1);
}
};
if !canonical_target.starts_with(&canonical_root) {
log::error!("Invalid SBOM output path '{}': path escapes the output directory", sbom_file);
std::process::exit(1);
}
if let Err(e) = fs::write(&canonical_target, json) {

Comment thread src/skip_scan.rs
let commit = utils::generic::get_repo_info_for_scan("./")
.ok()
.flatten()
.and_then(|info| Some((info.sha?, info.status_dirty)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RepoInfo deliberately has two dirtiness signals, but reuse checks the weaker one. status_dirty only reflects git status; dirty also covers assume-unchanged/skip-worktree files, dirty submodules, and index-read failures (worktree_dirty_flags), and that broader value is what a real upload sends. The existing generic tests even prove dirty == true && status_dirty == false for assume-unchanged/skip-worktree. In those states this branch can reuse a clean prior scan and let --block-on pass even though a fresh upload would contain different files. Gate reuse on the upload signal and add the hidden-index case to the skip E2E test.

Suggested change
.and_then(|info| Some((info.sha?, info.status_dirty)));
.and_then(|info| Some((info.sha?, info.dirty)));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this finding and think it should be addressed.

high: Reuse checks the weaker worktree dirtiness signal

status_dirty only reflects normal git-status changes, while the upload path's dirty signal also detects assume-unchanged/skip-worktree files, dirty submodules, and index-read failures. Reuse can therefore select an old clean scan although a fresh upload would contain different content.

Proof or reproduction:

Create a tracked modified file marked assume-unchanged so `info.status_dirty == false` and `info.dirty == true`; the current code proceeds to lookup and can reuse a prior clean scan. Use `info.dirty` for this decision.

Comment thread src/utils/api.rs
("page", "1".to_string()),
("page_size", page_size.to_string()),
("project", project.to_string()),
("sha", sha.to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lookup does not constrain file scope. GET /scans supports scan_type=full|partial, but a partial --target/--exclude scan of this SHA is returned here too. Modern CLI partial scans happen to carry worktree_dirty=true, but older/platform scans have null, and scan_age_if_reusable intentionally accepts None; such a scan can therefore stand in for a whole-commit run and produce a false-clean gate/report. Request only full scans (and assert the parameter in commit_lookup); if legacy backends may ignore this filter, the response also needs enough scope evidence to reject partial candidates client-side.

Suggested change
("sha", sha.to_string()),
("sha", sha.to_string()),
("scan_type", "full".to_string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this finding and think it should be addressed.

high: Partial scans can replace whole-commit scans

The lookup filters only by project and SHA. A completed partial scan produced with --target or --exclude can therefore be selected for a whole-commit run. Candidates with missing worktree_dirty are explicitly accepted, so legacy or platform partial scans can yield incomplete reports and false-clean gates.

Proof or reproduction:

Return a completed partial scan with the requested SHA and `worktree_dirty: null` from `/api/v1/scans`; `select_reusable_scan` accepts it. The request should constrain `scan_type=full`, and candidates must be rejected client-side if older servers can ignore that filter.

Comment thread src/scanners/blast.rs
// whichever scan id this resolves to.
let reused_scan = skip_recent
.as_ref()
.and_then(|skip| crate::skip_scan::resolve_reusable_scan(config, &project_name, skip));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse is not equivalent to the requested scan configuration. scan_type and policy are only passed to start_new_scan, so on a reuse hit the current --scan-type/--policy is silently ignored; conversely, the selected prior scan may itself have been secrets-only or used different target policies because selection checks only SHA/status/age/dirty. For example, a recent secrets-only scan can be reused by a default --block-on CI run and let base/policy findings go unevaluated. The file-scope GET /scans?scan_type=full filter does not solve this: CLI --scan-type is uploaded separately as scan_configs. Require an exact normalized scan_configs/target_policies match in the lookup/response before reuse. Until the API exposes that identity, reject reuse with current custom config and restrict candidates server-side to default-config scans.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this finding and think it should be addressed.

high: Reuse ignores scanner and policy configuration

scan_type and policy are passed only to start_new_scan, which is bypassed on a reuse hit. Candidate selection also does not check engine, scan configuration, or target policies. A completed secrets-only or differently configured scan of the same SHA can therefore replace the requested default/policy scan and omit findings used by --block-on.

Proof or reproduction:

Given a recent completed scan with the same SHA but `engine` or scan configuration limited to secrets, `resolve_reusable_scan` returns it because selection checks only SHA, status, dirtiness, and age; `start_new_scan(..., scan_type, policy, ...)` is never called.

…ommit

A pipeline that re-runs on an unchanged commit currently pays for a full
scan that can only reproduce the previous run's findings. Teams work around
it by querying `corgea list --json` for the last scan's SHA and re-deriving
a verdict from issue counts, which has no idea which blocking rules apply or
which findings carry exceptions.

Skipping the scan is only half the job: the run still has to gate on
--block-on and still has to write --out-file. So the reused scan takes the
new scan's place for the rest of the command rather than short-circuiting
it, and the blocking rules are evaluated against it exactly as they would be
against a fresh scan.

Recency is a policy, not a technicality: unchanged code is still exposed to
advisories published since it was last scanned, so --scanned-within (24h by
default) bounds how old a reusable scan may be.

Reuse is refused whenever the evidence is incomplete — a failed or running
scan, a scan of a dirty worktree, an unreadable timestamp, a lookup the
platform could not answer — since scanning again is the safe outcome. The
one hard failure is an unresolvable commit, because the flag asks a question
about the commit and quietly scanning would hide that the pipeline is not
getting the behavior it asked for. The scan list is re-checked client-side
against the requested SHA: a backend predating the `sha` filter answers with
every scan of the project, and acting on that would skip one commit's scan
because a different commit was scanned.

CORGEA_SCAN_SKIPPED=true/false (plus CORGEA_SCAN_ID on a reuse) is printed
once per run so a later pipeline step can branch on whether a scan happened.

Co-authored-by: ibrahim <ibrahim@corgea.com>
Comment thread src/skip_scan.rs
if scan.worktree_dirty == Some(true) {
return Err("it scanned a worktree with uncommitted changes".to_string());
}
let created_at = parse_timestamp(&scan.created_at)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high: Unknown historical dirtiness is treated as clean

The code rejects only Some(true) and deliberately accepts None, although None means the client did not report dirtiness, not that the tree was clean. An older dirty-worktree scan can thus be reused, contradicting the documented guarantee and potentially gating against findings from uncommitted historical content.

Proof or reproduction:

let mut legacy = scan("legacy", "complete", Some(SHA), "2026-01-01T23:00:00Z");
legacy.worktree_dirty = None;
assert!(select_reusable_scan(&[legacy], SHA, now(), DAY).is_none()); // currently returns Some

@corgea-security corgea-security added the dennis-reviewed Dennis completed an automated review label Aug 13, 2026

@corgea-security corgea-security left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review risk: 4/5.

High risk: reuse can substitute scans that do not represent the current worktree, requested scope, scanner configuration, or policy, allowing security gates to pass on incomplete results.

Critical or high-priority changes must be addressed.

Automatic approval was not submitted: automated review found critical or high-priority findings.

@cursor
cursor Bot force-pushed the cursor/skip-if-commit-scanned-recently-ec52 branch from d403c24 to 2d8162f Compare August 13, 2026 13:28
Comment thread src/scanners/blast.rs
Comment on lines +70 to +82
let (scan_id, project_id) = match reused_scan {
Some(scan) => (scan.id, None),
None => start_new_scan(
config,
&project_name,
only_uncommitted,
metadata,
scan_type,
policy,
target,
exclude,
),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reused scan-list records omit scanner errors, so degraded findings can be gated without fresh-scan warnings; could we fetch the scan by ID and reject it or surface those warnings?

Comment thread src/skip_scan.rs
short, project_name, skip.label
);

let scans = match utils::api::query_scans_for_commit(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a reusable completed scan beyond the first 30 records is missed, causing a duplicate scan; should we paginate until we find a candidate or exhaust the relevant pages?

@yhoztak yhoztak left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

once some comments are addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dennis-reviewed Dennis completed an automated review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants