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
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ serde_yaml_ng = "0.10"

[target.'cfg(not(target_os = "windows"))'.dependencies]
openssl = { version = "0.10", features = ["vendored"] }

[dev-dependencies]
tempfile = "3.12.0"
13 changes: 13 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ enum Commands {
)]
target: Option<String>,

#[arg(
long,
help = "Exclude files matching glob patterns from the scan. Accepts comma-separated glob patterns. Examples: 'tests/**', 'src/**/*.test.ts,**/*.spec.js', '*.md'."
)]
exclude: Option<String>,

#[arg(
long,
help = "The name of the Corgea project. Defaults to git repository name if found, otherwise to the current directory name."
Expand Down Expand Up @@ -353,6 +359,7 @@ fn main() {
out_format,
out_file,
target,
exclude,
project_name,
}) => {
verify_token_and_exit_when_fail(&corgea_config);
Expand Down Expand Up @@ -438,6 +445,11 @@ fn main() {
::log::warn!("\nWarning: you didn't specify an only policy scan, so all other types of scans will run as well.");
}
}
if exclude.is_some() && *scanner != Scanner::Blast {
::log::error!("exclude is only supported with blast scanner.");
std::process::exit(1);
}

match scanner {
Scanner::Snyk => scan::run_snyk(&corgea_config, project_name.clone()),
Scanner::Semgrep => scan::run_semgrep(&corgea_config, project_name.clone()),
Expand All @@ -451,6 +463,7 @@ fn main() {
out_format.clone(),
out_file.clone(),
target.clone(),
exclude.clone(),
project_name.clone(),
),
}
Expand Down
30 changes: 14 additions & 16 deletions src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,22 +322,18 @@ pub fn upload_scan(
Ok(res) => {
if !res.status().is_success() {
true
} else {
if let Some(server_offset) = res.headers().get("Upload-Offset") {
let expected_offset = offset + chunk.len();
if let Ok(server_offset_str) = server_offset.to_str() {
if let Ok(server_offset_val) = server_offset_str.parse::<usize>() {
if server_offset_val != expected_offset {
log::error!(
"Upload offset mismatch on chunk {}/{}: server has {} bytes but expected {}. \
This may indicate that chunks are being routed to different server instances. \
Please contact support.",
index + 1, total_chunks, server_offset_val, expected_offset
);
true
} else {
false
}
} else if let Some(server_offset) = res.headers().get("Upload-Offset") {
let expected_offset = offset + chunk.len();
if let Ok(server_offset_str) = server_offset.to_str() {
if let Ok(server_offset_val) = server_offset_str.parse::<usize>() {
if server_offset_val != expected_offset {
log::error!(
"Upload offset mismatch on chunk {}/{}: server has {} bytes but expected {}. \
This may indicate that chunks are being routed to different server instances. \
Please contact support.",
index + 1, total_chunks, server_offset_val, expected_offset
);
true
} else {
false
}
Expand All @@ -347,6 +343,8 @@ pub fn upload_scan(
} else {
false
}
} else {
false
}
}
Err(_) => true,
Expand Down
9 changes: 7 additions & 2 deletions src/scanners/blast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub fn run(
out_format: Option<String>,
out_file: Option<String>,
target: Option<String>,
exclude: Option<String>,
project_name: Option<String>,
) {
// Validate that only_uncommitted and target are not used together
Expand Down Expand Up @@ -86,8 +87,12 @@ pub fn run(
target.as_deref()
};

if target_str.is_none() && exclude.is_some() {
println!("Excluding files matching: {}", exclude.as_deref().unwrap());
}

if let Some(target_value) = target_str {
match targets::resolve_targets(target_value) {
match targets::resolve_targets_with_exclude(target_value, exclude.as_deref()) {
Ok(result) => {
if result.files.is_empty() {
*stop_signal.lock().unwrap() = true;
Expand Down Expand Up @@ -159,7 +164,7 @@ pub fn run(
}
}

match utils::generic::create_zip_from_target(target_str, &zip_path, None) {
match utils::generic::create_zip_from_target(target_str, &zip_path, None, exclude.as_deref()) {
Ok(added_files) => {
if added_files.is_empty() {
*stop_signal.lock().unwrap() = true;
Expand Down
201 changes: 200 additions & 1 deletion src/targets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ pub struct TargetSegmentResult {
pub error: Option<String>,
}

pub fn resolve_targets(target_value: &str) -> Result<TargetResolutionResult, String> {
pub fn resolve_targets_with_exclude(
target_value: &str,
exclude: Option<&str>,
) -> Result<TargetResolutionResult, String> {
let segments: Vec<String> = target_value
.split(',')
.map(|s| s.trim().to_string())
Expand All @@ -40,6 +43,8 @@ pub fn resolve_targets(target_value: &str) -> Result<TargetResolutionResult, Str
}
}

let exclude_glob_set = build_exclude_glob_set(exclude)?;

let mut all_files = Vec::new();
let mut seen_files = HashSet::new();
let mut segment_results = Vec::new();
Expand All @@ -58,6 +63,9 @@ pub fn resolve_targets(target_value: &str) -> Result<TargetResolutionResult, Str
for file in result {
match normalize_path(&file, &repo_root) {
Ok(normalized) => {
if is_excluded_by_glob(&normalized, &repo_root, &exclude_glob_set) {
continue;
}
if seen_files.insert(normalized.clone()) {
all_files.push(normalized);
}
Expand Down Expand Up @@ -101,6 +109,63 @@ pub fn resolve_targets(target_value: &str) -> Result<TargetResolutionResult, Str
})
}

fn build_exclude_glob_set(exclude: Option<&str>) -> Result<Option<globset::GlobSet>, String> {
let exclude_str = match exclude {
Some(s) if !s.trim().is_empty() => s,
_ => return Ok(None),
};

let patterns: Vec<&str> = exclude_str
.split(',')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if patterns.is_empty() {
return Ok(None);
}

let mut builder = GlobSetBuilder::new();
for pattern in &patterns {
let glob = Glob::new(pattern)
.map_err(|e| format!("Invalid exclude glob pattern '{}': {}", pattern, e))?;
builder.add(glob);
}
let glob_set = builder
.build()
.map_err(|e| format!("Failed to build exclude glob set: {}", e))?;
Ok(Some(glob_set))
}

fn is_excluded_by_glob(
file: &Path,
repo_root: &Path,
exclude_glob_set: &Option<globset::GlobSet>,
) -> bool {
let glob_set = match exclude_glob_set {
Some(gs) => gs,
None => return false,
};

if let Ok(relative) = file.strip_prefix(repo_root) {
return glob_set.is_match(relative);
}
glob_set.is_match(file)
}

pub fn build_user_exclude_glob_set(
exclude: Option<&str>,
) -> Result<Option<globset::GlobSet>, String> {
build_exclude_glob_set(exclude)
}

pub fn is_file_excluded(
file: &Path,
base_dir: &Path,
exclude_glob_set: &Option<globset::GlobSet>,
) -> bool {
is_excluded_by_glob(file, base_dir, exclude_glob_set)
}

fn resolve_segment(segment: &str, repo_root: &Path) -> Result<Vec<PathBuf>, String> {
if segment == "-" {
return read_stdin_files(false);
Expand Down Expand Up @@ -480,3 +545,137 @@ fn find_repo_root() -> Result<PathBuf, String> {
fn is_git_repo(dir: &Path) -> bool {
Repository::discover(dir).is_ok()
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;

fn setup_test_dir() -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let base = dir.path();

Repository::init(base).unwrap();

fs::create_dir_all(base.join("src")).unwrap();
fs::create_dir_all(base.join("tests")).unwrap();
fs::create_dir_all(base.join("docs")).unwrap();

fs::write(base.join("src/main.rs"), "fn main() {}").unwrap();
fs::write(base.join("src/lib.rs"), "pub fn hello() {}").unwrap();
fs::write(base.join("tests/test_main.rs"), "// test").unwrap();
fs::write(base.join("docs/readme.md"), "# readme").unwrap();
fs::write(base.join("config.toml"), "[config]").unwrap();

dir
}

#[test]
fn build_exclude_glob_set_returns_none_for_none() {
let result = build_exclude_glob_set(None).unwrap();
assert!(result.is_none());
}

#[test]
fn build_exclude_glob_set_returns_none_for_empty() {
let result = build_exclude_glob_set(Some("")).unwrap();
assert!(result.is_none());
}

#[test]
fn build_exclude_glob_set_returns_some_for_valid_pattern() {
let result = build_exclude_glob_set(Some("tests/**")).unwrap();
assert!(result.is_some());
}

#[test]
fn build_exclude_glob_set_handles_comma_separated() {
let result = build_exclude_glob_set(Some("tests/**,docs/**")).unwrap();
assert!(result.is_some());
let gs = result.unwrap();
assert!(gs.is_match("tests/foo.rs"));
assert!(gs.is_match("docs/readme.md"));
assert!(!gs.is_match("src/main.rs"));
}

#[test]
fn build_exclude_glob_set_returns_error_for_invalid() {
let result = build_exclude_glob_set(Some("[invalid"));
assert!(result.is_err());
}

#[test]
fn is_excluded_by_glob_matches_relative_path() {
let gs = build_exclude_glob_set(Some("tests/**")).unwrap();
let repo_root = Path::new("/repo");
let file = Path::new("/repo/tests/test_main.rs");
assert!(is_excluded_by_glob(file, repo_root, &gs));
}

#[test]
fn is_excluded_by_glob_does_not_match_non_excluded() {
let gs = build_exclude_glob_set(Some("tests/**")).unwrap();
let repo_root = Path::new("/repo");
let file = Path::new("/repo/src/main.rs");
assert!(!is_excluded_by_glob(file, repo_root, &gs));
}

#[test]
fn is_excluded_by_glob_returns_false_for_none() {
let gs: Option<globset::GlobSet> = None;
let file = Path::new("/repo/tests/test_main.rs");
assert!(!is_excluded_by_glob(file, Path::new("/repo"), &gs));
}

#[test]
fn is_excluded_by_glob_wildcard_extension() {
let gs = build_exclude_glob_set(Some("**/*.md")).unwrap();
let repo_root = Path::new("/repo");
assert!(is_excluded_by_glob(
Path::new("/repo/docs/readme.md"),
repo_root,
&gs
));
assert!(!is_excluded_by_glob(
Path::new("/repo/src/main.rs"),
repo_root,
&gs
));
}

#[test]
fn is_excluded_filters_directory_files_correctly() {
let dir = setup_test_dir();
let base = dir.path();
let gs = build_exclude_glob_set(Some("tests/**,**/*.md")).unwrap();

assert!(!is_excluded_by_glob(&base.join("src/main.rs"), base, &gs));
assert!(!is_excluded_by_glob(&base.join("src/lib.rs"), base, &gs));
assert!(!is_excluded_by_glob(&base.join("config.toml"), base, &gs));
assert!(is_excluded_by_glob(
&base.join("tests/test_main.rs"),
base,
&gs
));
assert!(is_excluded_by_glob(&base.join("docs/readme.md"), base, &gs));
}

#[test]
fn is_excluded_with_none_includes_all() {
let dir = setup_test_dir();
let base = dir.path();
let gs: Option<globset::GlobSet> = None;

assert!(!is_excluded_by_glob(&base.join("src/main.rs"), base, &gs));
assert!(!is_excluded_by_glob(
&base.join("tests/test_main.rs"),
base,
&gs
));
assert!(!is_excluded_by_glob(
&base.join("docs/readme.md"),
base,
&gs
));
}
}
Loading
Loading