Skip to content

Force-include rules and a --include flag for the scan packager (SAST-01) - #166

Open
Ibrahimrahhal wants to merge 2 commits into
mainfrom
cursor/force-include-scan-rules-b1ac
Open

Force-include rules and a --include flag for the scan packager (SAST-01)#166
Ibrahimrahhal wants to merge 2 commits into
mainfrom
cursor/force-include-scan-rules-b1ac

Conversation

@Ibrahimrahhal

Copy link
Copy Markdown
Member

Description

A file the CLI leaves out of the archive cannot be scanned whatever the engine later decides about it, so DEFAULT_EXCLUDE_GLOBS and .gitignore silently overruled any platform-side attempt to force-scan misclassified proprietary code. This makes the packager honor force-include rules.

New src/include_rules.rs. Before packaging, reads the project's rules from GET /api/v1/scan-settings and unions them with the new repeatable --include flag. Patterns accept a path, a directory, or a glob; a bare path also matches its descendants, so the same pattern means the same thing here as in the platform's ignore rules. Expansion walks with the standard ignore filters off — the point is to reach files .gitignore and the default excludes hide — skipping .git, and capped at 5 000 files so a rule like **/*.js cannot drag a whole node_modules tree into the upload.

Packaging. create_zip_from_target takes the matched repo-relative paths and exempts them from the default excludes, --exclude, and the walk's gitignore pruning, appending any the walk never reached. Same shape as the existing extra_files path used by --include-image.

Incremental. IncrementalPlan::including adds force-included files to the changed-file list. The server carries findings forward for every file the diff omits, and an include rule exists precisely because the file was never scanned — so there is nothing to carry forward, and without this the rule would never get the file looked at. Blowing past MAX_CHANGED_FILES falls back to a full scan, which is only slower.

Upload. Only the --include values travel with the archive (include_paths, JSON array); the project's own rules are already stored server-side.

Failure handling. A failed /scan-settings lookup warns and proceeds on the flag alone — that is exactly today's behavior, and refusing to scan would be worse. A 404 is a backend without the endpoint, which has no rules to apply either. An unparseable pattern is dropped without discarding the rest: silently scanning less than asked is the failure this feature exists to fix.

Flag conflict. --include joins --target and --include-image in refusing --skip-if-commit-scanned-recently. A reused scan predates the include rule, so skipping would leave the very file the run mandated unscanned — under-reporting, unlike --exclude's over-reporting, which still only warns.

Related PRs

All three are needed for the feature to work end to end:

  • Corgea/doghouse#1983 — stores project include rules, serves /scan-settings, accepts include_paths on upload.
  • Corgea/fusion#500 — honors include_paths when classifying extracted files.

Requirement ID: SAST-01 (Post-Migration).

Testing

New tests/cli_scan_include.rs drives the real binary against a stubbed API: --include bundles a node_modules file while its siblings stay excluded, platform-configured rules apply with nothing sent back, an include rule beats --exclude, and a rule matching nothing warns without failing the scan. Unit tests in include_rules.rs cover normalization, unparseable patterns, and reaching gitignored paths; incremental.rs covers the changed-file union and its cap; generic.rs covers the zip exemption. The cloud_commands_e2e contract now asserts the /scan-settings request in the BLAST plan, and scan_skip.rs asserts the new flag conflict.

./harness check → Clippy fix, Format, Clippy (strict), Tests (787 passed), Deps skill drift — all OK

One suppression added: #[allow(clippy::large_enum_variant)] on Commands. Scan carries by far the largest flag set, and exactly one Commands value exists per process — parsed at startup and destructured immediately — so boxing would not recover anything real.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 31, 2026 11:55
A file the CLI leaves out of the archive cannot be scanned whatever the engine
later decides about it, so DEFAULT_EXCLUDE_GLOBS and .gitignore silently
overruled any platform-side attempt to force-scan misclassified proprietary
code. Read the project's include rules from GET /api/v1/scan-settings before
packaging, union them with the new repeatable --include flag, and add every
matched file to the zip regardless of the default excludes, --exclude, or
.gitignore.

Force-included files also join the incremental changed-file list. The server
carries findings forward for whatever the diff omits, and an include rule exists
precisely because the file was never scanned, so there is nothing to carry.

Only the --include values travel with the upload; the project's own rules are
already stored server-side.

Co-authored-by: ibrahim <ibrahim@corgea.com>
A reused scan predates the include rule, so skipping would leave the very file
the run mandated unscanned. Under-reporting, unlike --exclude's over-reporting,
so clap refuses the combination rather than warning.

Co-authored-by: ibrahim <ibrahim@corgea.com>
@Ibrahimrahhal
Ibrahimrahhal marked this pull request as ready for review August 31, 2026 14:01
@Ibrahimrahhal
Ibrahimrahhal requested review from juangaitanv, leenk7991 and yhoztak and removed request for leenk7991 August 31, 2026 14:02

@cursor cursor Bot 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.

Project include rules never suppress --skip-if-commit-scanned-recently. The clap conflict only covers the --include flag, so the primary SAST-01 path (rules from the web app, no flag) can reuse a scan that never packaged those files — the same under-reporting this PR already refuses for the flag.

One inline comment with the fix. I did not find a second merge-blocking issue in the packager exemption, the incremental union, or the --include upload field.

Open in Web View Automation 

Sent by Cursor Automation: pr-flow

Comment thread src/main.rs
long = "skip-if-commit-scanned-recently",
conflicts_with_all = ["only_uncommitted", "target", "scan_type", "policy", "include_image", "disable_incremental"],
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, --include-image and --disable-incremental; with --exclude it warns instead, since a reused scan covers files this run would have skipped."
conflicts_with_all = ["only_uncommitted", "target", "scan_type", "policy", "include_image", "include", "disable_incremental"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--skip-if-commit-scanned-recently still reuses a scan when the project has File Include Rules.

This conflict list is the right policy for --include: a reused scan predates the rule, so skipping would leave the mandated file unscanned. Project-level rules from GET /api/v1/scan-settings have the same semantics and are the main SAST-01 path, but clap cannot see them.

Reuse is decided in blast::run before start_new_scan, which is the only place include_rules::resolve runs:

    let reused_scan = skip_recent.as_ref().and_then(|skip| {
        crate::skip_scan::resolve_reusable_scan(
            config,
            &project_name,
            skip,
            exclude.as_deref(),
            *ignore_dirty_worktree,
        )
    });

Successful-skip e2e plans (scan_skip.rs) go verify → commit lookup and never hit /scan-settings, so this is untested.

Impact: Adding include rules in the web app and re-running CI on the same SHA (retry, promotion, or a verification re-run) reuses the old scan. The files this feature exists to force in stay out. New commits still scan; retries and first-verification runs do not.

Fix: Resolve include rules once in run() before resolve_reusable_scan. If !include_rules.is_empty(), do not reuse (same fail-closed stance as --scan-type / --include). On a /scan-settings error, also skip reuse — skip_scan already treats an incomplete lookup as “run a new scan.” Pass the resolved IncludeRules into start_new_scan so the endpoint is not fetched twice.

Cover it with an e2e: --skip-if-commit-scanned-recently plus a stub that returns include_paths: ["vendor/our-fork/**"] must start a new scan (upload present), not reuse.

Comment thread src/include_rules.rs
Comment on lines +48 to +69
let walker = WalkBuilder::new(root)
.standard_filters(false)
.filter_entry(|entry| entry.file_name() != ".git")
.build();
for entry in walker.flatten() {
if !entry.file_type().is_some_and(|kind| kind.is_file()) {
continue;
}
let Ok(relative) = entry.path().strip_prefix(root) else {
continue;
};
if glob_set.is_match(relative) {
matches.push(relative.to_path_buf());
}
if matches.len() >= MAX_FORCE_INCLUDED_FILES {
log::warn!(
"Include rules matched more than {} files; only the first {} are forced into this scan.",
MAX_FORCE_INCLUDED_FILES,
MAX_FORCE_INCLUDED_FILES
);
break;
}

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.

the 5,000-file cap uses walk order; could we sort matches before applying it so equivalent runs select the same paths?

Comment thread src/scanners/blast.rs
Comment on lines +389 to +394
} else if !force_included.is_empty() {
println!(
"Force-including {} file(s) Corgea would otherwise skip.",
force_included.len()
);
}

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.

force-included paths override ignore and exclude rules, including **/*.env; could we print a bounded path list with the count for archive review?

Comment thread src/scanners/blast.rs
.and_then(|info| info.repo_url.as_deref()),
&include,
);
let force_included = include_rules.matching_files(Path::new("."));

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 zero-file target can still have force-included matches; could we treat a nonempty force_included list as a valid payload before the guard?

Comment thread src/utils/api.rs
Comment on lines +1000 to +1005
if let Some(repo_url) = repo_url {
query.push(("repo_url", repo_url.to_string()));
}
debug(&format!(
"Reading project scan settings from {} ({:?})",
request_url, query

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.

the origin can contain credentials and is sent in the query and debug logs; could we derive a credential-free repository identity before the lookup?

Comment thread src/include_rules.rs
Comment on lines +53 to +55
if !entry.file_type().is_some_and(|kind| kind.is_file()) {
continue;
}

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.

discarded traversal errors can turn an explicit include into a no-match; could we propagate them or distinguish them from no matches?

Comment thread src/include_rules.rs
Comment on lines +145 to +148
IncludeRules {
patterns,
cli_patterns,
}

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.

invalid CLI globs are skipped locally but uploaded in cli_patterns; could we validate them consistently or reject them during argument handling?

Comment thread src/utils/api.rs
"Reading project scan settings from {} ({:?})",
request_url, query
));
let response = client.get(&request_url).query(&query).send()?;

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.

the settings lookup inherits the 150-second timeout and can delay CLI fallback; could we set a shorter per-request timeout?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants