Force-include rules and a --include flag for the scan packager (SAST-01) - #166
Force-include rules and a --include flag for the scan packager (SAST-01)#166Ibrahimrahhal wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
Stale comment
Project include rules never suppress
--skip-if-commit-scanned-recently. The clap conflict only covers the--includeflag, 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
--includeupload field.Sent by Cursor Automation: pr-flow
There was a problem hiding this comment.
Automated review risk: 5/5.
High-risk under-scanning paths and repository credential exposure must be fixed before merge.
Critical or high-priority changes must be addressed.
Automatic approval was not submitted: automated review found critical or high-priority findings.
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>
183121f to
b85a4c5
Compare
Reuse: resolve include rules in run(), before resolve_reusable_scan. Only the --include flag conflicted with --skip-if-commit-scanned-recently, and the rules were read inside start_new_scan — after reuse had already been chosen. So the primary path, a rule configured in the web app with no flag, could reuse a scan that never packaged the files the rule forces in. Project rules and a failed lookup both refuse reuse now, and the refusal still prints CORGEA_SCAN_SKIPPED=false, which the flag promises on every run. Empty target: a force-included file is a complete payload, like an exported image, so --target matching nothing no longer aborts a run whose include rule did match. Credentials: strip userinfo from the git remote before it reaches the /scan-settings query string and the debug log. Only scheme URLs are touched; scp-style git@host:path carries no secret and stripping it would stop the server normalizing it to the stored URL. Also: reject invalid and repo-wide --include patterns up front instead of dropping them locally while still uploading them; sort matches before applying the 5k cap so equivalent runs force in the same files; report traversal errors rather than letting an unreadable directory turn an include into a silent no-match; cap the settings lookup at 15s, since the run continues without it; and print the forced paths, which override .gitignore and **/*.env. Co-authored-by: ibrahim <ibrahim@corgea.com>
| Ok(None) => {} | ||
| Err(e) => { | ||
| lookup_failed = true; | ||
| log::warn!( |
There was a problem hiding this comment.
high: Settings lookup failure silently drops mandatory project rules
A timeout or HTTP error sets lookup_failed but continues with only CLI patterns. A fresh scan therefore still omits files mandated by project include rules while exiting successfully. Preventing scan reuse does not fix this under-reporting; the scan must fail when required settings cannot be retrieved.
Proof or reproduction:
Given a project rule "node_modules/internal/**":
1. Make GET /scan-settings return HTTP 500.
2. Run `corgea scan` with no --include flag.
3. The command warns and succeeds, but node_modules/internal files are absent from the archive.
| matches.len(), | ||
| MAX_FORCE_INCLUDED_FILES | ||
| ); | ||
| matches.truncate(MAX_FORCE_INCLUDED_FILES); |
There was a problem hiding this comment.
high: Include match cap produces a successful partial scan
When rules match more than 5,000 files, the implementation truncates the matched set and continues successfully. Files after the deterministic cutoff are excluded despite explicitly matching an include rule, contradicting the documented guarantee that force-included files are analyzed. Exceeding the safety limit should fail the scan rather than produce false-negative results.
Proof or reproduction:
Create 5,001 files under node_modules/internal and apply `--include 'node_modules/internal/**'`. `matching_files` returns only 5,000 paths, and the remaining matching file is never added to the archive.
| use std::time::{Duration, Instant}; | ||
|
|
||
| /// Overrides how long `wait_for_scan` polls before giving up. | ||
| /// How many force-included paths to name before collapsing the rest to a count. |
There was a problem hiding this comment.
nitpick: Timeout documentation is attached to the preview constant
The existing documentation for the scan timeout now precedes FORCE_INCLUDE_PREVIEW, so it incorrectly claims that this preview-size constant controls wait_for_scan polling. Move that sentence back above SCAN_TIMEOUT_ENV.
Proof or reproduction:
/// Overrides how long `wait_for_scan` polls before giving up.
/// How many force-included paths to name...
const FORCE_INCLUDE_PREVIEW: usize = 20;
There was a problem hiding this comment.
Automated review risk: 4/5.
Include-rule failures can produce successful but incomplete security scans. Previously reported issues appear addressed.
Critical or high-priority changes must be addressed.
Automatic approval was not submitted: automated review found critical or high-priority findings.
There was a problem hiding this comment.
Re-reviewed 18a84a8 after the force-include follow-up. The skip-reuse hole I flagged is gone, and I did not find a remaining merge blocker.
Prior finding — addressed. include_rules::resolve now runs in blast::run before resolve_reusable_scan. IncludeRules::reuse_refusal refuses reuse when project rules exist or the lookup failed, and still prints CORGEA_SCAN_SKIPPED=false. a_project_include_rule_refuses_reuse_and_starts_a_new_scan drives that path through the real upload plan (no commit lookup). Clap still blocks --include + the skip flag.
Also verified on this HEAD (not re-raised):
- Empty
--target+ nonemptyforce_includedis a valid payload (cli_scan_includee2e). - Packager exemption + append of gitignored / default-excluded paths (
create_zip_from_target, project-rules e2e asserts the zip entry and thatinclude_pathsis not resent). - Incremental
including()unions forced paths; overflow falls back to a full scan. GET /scan-settingsis authenticated, 15s-bounded, 404 → no rules, other errors →lookup_failed.strip_remote_credentialson scheme URLs before the query/debug log; scp-style remotes left intact.- Invalid / repo-wide
--includerejected invalidate_cli_patternsbefore packaging. - Matches sorted before the 5k cap; traversal errors counted and warned; forced paths printed.
CI on this SHA is green (rust-tests, platform matrix, native binaries). Remaining related work is the documented doghouse #1983 / fusion #500 pairing, not a CLI defect.
Sent by Cursor Automation: pr-flow
| fn normalize_patterns(patterns: &[String]) -> Vec<String> { | ||
| let mut normalized = Vec::new(); | ||
| for pattern in patterns { | ||
| let trimmed = pattern.trim(); | ||
| if !trimmed.is_empty() { | ||
| push_unique(&mut normalized, trimmed.to_string()); | ||
| } | ||
| } |
There was a problem hiding this comment.
./-prefixed patterns never match root-relative paths, so requested files are skipped; could we normalize leading ./ before matching and uploading and cover it with a test?
| let include_rules = crate::include_rules::resolve( | ||
| config, | ||
| &project_name, | ||
| utils::generic::get_repo_info_for_scan("./") | ||
| .unwrap_or_default() | ||
| .and_then(|info| info.repo_url) | ||
| .as_deref(), | ||
| &include, | ||
| ); |
There was a problem hiding this comment.
get_repo_info_for_scan runs a whole-worktree status check although this path only needs repo_url; could we use get_repo_info here?


Description
A file the CLI leaves out of the archive cannot be scanned whatever the engine later decides about it, so
DEFAULT_EXCLUDE_GLOBSand.gitignoresilently overruled any platform-side attempt to force-scan misclassified proprietary code. This makes the packager honor force-include rules.New
src/include_rules.rs. Reads the project's rules fromGET /api/v1/scan-settingsand unions them with the new repeatable--includeflag. 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.gitignoreand the default excludes hide — skipping.git, sorted, then capped at 5 000 files so a rule like**/*.jscannot drag a wholenode_modulestree into the upload.Rules are resolved in
run(), before the reuse decision. A reusable scan predates any include rule, so its results omit the files the rule forces in.--includerefuses--skip-if-commit-scanned-recentlyat the clap level; project rules are invisible to clap, so they are checked here and refuse reuse too — as does a failed lookup, since not knowing whether the project has rules is not the same as knowing it has none. The refusal still printsCORGEA_SCAN_SKIPPED=false, which the flag promises on every run.Packaging.
create_zip_from_targettakes 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 existingextra_filespath used by--include-image. A force-included file is a complete payload on its own, so a--targetthat matches nothing no longer aborts the run. The forced paths are printed, not just counted: they override.gitignoreand the default excludes, which cover**/*.envamong others.Incremental.
IncrementalPlan::includingadds 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 pastMAX_CHANGED_FILESfalls back to a full scan, which is only slower.Upload. Only the
--includevalues travel with the archive (include_paths, JSON array); the project's own rules are already stored server-side.Validation and failure handling. Invalid and repo-wide patterns are refused up front rather than dropped later: a pattern globset cannot compile packages nothing, so the command would otherwise appear to succeed having ignored what was asked for, and
**would disable every exclude rule including.gitignore. Server-supplied patterns stay tolerant, since one bad rule in the web app must not stop a scan. A failed/scan-settingslookup warns and proceeds on the flag alone; a404is a backend without the endpoint, which has no rules to apply either. Traversal errors are reported rather than swallowed, so an unreadable directory cannot turn an explicit include into a silent no-match. The lookup is capped at 15s, well under the 150s default, because the run continues without it.Credentials. A git origin can embed a token (
https://oauth2:glpat-x@host/...), so userinfo is stripped before the remote reaches the query string or--verboseoutput. Only scheme URLs are touched: scp-stylegit@host:pathcarries no secret, and stripping itsgit@would stop the server recognising the shape and normalizing it to the stored URL.Related PRs
All three are needed for the feature to work end to end:
/scan-settings, acceptsinclude_pathson upload.include_pathswhen classifying extracted files.Requirement ID: SAST-01 (Post-Migration).
Review feedback addressed
run()beforeresolve_reusable_scan; project rules and a failed lookup both refuse reuse--targetblocked force-included filesforce_includedas a valid payloadstrip_remote_credentialson scheme URLs before the requestflatten()--includeglobs uploaded but not appliedTesting
tests/cli_scan_include.rsdrives the real binary against a stubbed API:--includebundles anode_modulesfile while its siblings stay excluded, platform-configured rules apply with nothing sent back, an include rule beats--exclude, a target matching nothing still scans the forced file, a repo-wide or invalid pattern is refused before anything is packaged, and a rule matching nothing warns without failing the scan. Unit tests cover reuse refusal, pattern validation, sort-before-cap, credential stripping, reaching gitignored paths, and the incremental changed-file union. Thecloud_commands_e2econtract asserts the/scan-settingsrequest in every BLAST plan, that a project rule starts a new scan instead of reusing one, and the flag conflict.One suppression added:
#[allow(clippy::large_enum_variant)]onCommands.Scancarries by far the largest flag set, and exactly oneCommandsvalue exists per process — parsed at startup and destructured immediately — so boxing would not recover anything real.