From a6e126e3bf9bfbb5daec68d33a10da68123d7491 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 11 Mar 2026 20:47:13 +0000 Subject: [PATCH] Support project-name for upload Co-authored-by: Ahmad Sadeddin --- src/main.rs | 207 ++++++++++++++++++++++++++++++---------- src/scan.rs | 156 +++++++++++++++++++++--------- src/scanners/fortify.rs | 27 ++++-- 3 files changed, 285 insertions(+), 105 deletions(-) diff --git a/src/main.rs b/src/main.rs index e5cb7a9..08b5387 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,28 +1,28 @@ +mod authorize; +mod cicd; mod config; -mod scan; -mod wait; -mod list; mod inspect; -mod cicd; +mod list; mod log; +mod scan; mod setup_hooks; -mod authorize; +mod wait; mod scanners { - pub mod fortify; pub mod blast; + pub mod fortify; pub mod parsers; } mod utils { - pub mod terminal; - pub mod generic; pub mod api; + pub mod generic; + pub mod terminal; } mod targets; -use std::str::FromStr; -use clap::{Parser, Subcommand, CommandFactory}; +use clap::{CommandFactory, Parser, Subcommand}; use config::Config; use scanners::fortify::parse as fortify_parse; +use std::str::FromStr; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] @@ -32,26 +32,38 @@ struct Cli { command: Option, #[arg(required = false)] - args: Vec, + args: Vec, } #[derive(Subcommand, Debug)] enum Commands { /// Authenticate to Corgea - Login { + Login { #[arg(help = "API token (if not provided, will use OAuth flow)")] token: Option, - #[arg(long, help = "The url of the corgea instance to use. defaults to https://www.corgea.app")] + #[arg( + long, + help = "The url of the corgea instance to use. defaults to https://www.corgea.app" + )] url: Option, - #[arg(long, help = "Scope to use for custom domain (e.g., 'ikea' for ikea.corgea.app). Only used with OAuth flow")] + #[arg( + long, + help = "Scope to use for custom domain (e.g., 'ikea' for ikea.corgea.app). Only used with OAuth flow" + )] scope: Option, }, /// Upload a scan report to Corgea via STDIN or a file Upload { /// Option path to JSON report to upload report: Option, + + #[arg( + long, + help = "The name of the Corgea project. Defaults to the existing upload behavior when omitted." + )] + project_name: Option, }, /// Scan the current directory. Supports blast, semgrep and snyk. Scan { @@ -59,13 +71,20 @@ enum Commands { #[arg(default_value = "blast")] scanner: Scanner, - #[arg(long, help = "Fail on (exits with error code 1) a specific severity level . Valid options are CR, HI, ME, LO.")] + #[arg( + long, + help = "Fail on (exits with error code 1) a specific severity level . Valid options are CR, HI, ME, LO." + )] fail_on: Option, #[arg(long, help = "Only scan uncommitted changes.")] only_uncommitted: bool, - #[arg(short, long, help = "Fail on (exits with error code 1) based on blocking rules defined in the web app.")] + #[arg( + short, + long, + help = "Fail on (exits with error code 1) based on blocking rules defined in the web app." + )] fail: bool, #[arg( @@ -82,10 +101,17 @@ enum Commands { )] scan_type: Option, - #[arg(long, help = "Output the result to a file in a specific format. Valid options are json, html, sarif, markdown.")] + #[arg( + long, + help = "Output the result to a file in a specific format. Valid options are json, html, sarif, markdown." + )] out_format: Option, - #[arg(short, long, help = "Output the result to a file. you can use the out_format option to specify the format of the output file.")] + #[arg( + short, + long, + help = "Output the result to a file. you can use the out_format option to specify the format of the output file." + )] out_file: Option, #[arg( @@ -101,16 +127,18 @@ enum Commands { project_name: Option, }, /// Wait for the latest in progress scan - Wait { - scan_id: Option, - }, + Wait { scan_id: Option }, /// List something, by default it lists the scans #[command(alias = "ls")] List { #[arg(short, long, help = "List issues instead of scans")] issues: bool, - #[arg(long, short = 'c', help = "List SCA (Software Composition Analysis) issues instead of regular issues")] + #[arg( + long, + short = 'c', + help = "List SCA (Software Composition Analysis) issues instead of regular issues" + )] sca_issues: bool, #[arg(short, long, help = "Specify the scan id to list issues for.")] @@ -123,7 +151,7 @@ enum Commands { json: bool, #[arg(long, value_parser = clap::value_parser!(u16), help = "Number of items per page")] - page_size: Option + page_size: Option, }, /// Inspect something, by default it will inspect a scan Inspect { @@ -134,20 +162,36 @@ enum Commands { #[arg(long, help = "Output the result in JSON format.")] json: bool, - #[arg(long, short, help = "Display a summary only of the issue in the output (only if --issue is true).")] + #[arg( + long, + short, + help = "Display a summary only of the issue in the output (only if --issue is true)." + )] summary: bool, - #[arg(long, short, help = "Display the fix explanations only in the output (only if --issue is true).")] + #[arg( + long, + short, + help = "Display the fix explanations only in the output (only if --issue is true)." + )] fix: bool, - #[arg(long, short, help = "Display the diff of the fix only in the output (only if --issue is true).")] + #[arg( + long, + short, + help = "Display the diff of the fix only in the output (only if --issue is true)." + )] diff: bool, id: String, }, /// Setup a git hook, currently only pre-commit is supported SetupHooks { - #[arg(long, short, help = "Include default config (scan types are pii, secrets and fail on levels are CR, HI, ME, LO).")] + #[arg( + long, + short, + help = "Include default config (scan types are pii, secrets and fail on levels are CR, HI, ME, LO)." + )] default_config: bool, }, } @@ -175,7 +219,7 @@ impl FromStr for Scanner { fn main() { let cli = Cli::parse(); let mut corgea_config = Config::load().expect("Failed to load config"); - fn verify_token_and_exit_when_fail (config: &Config) { + fn verify_token_and_exit_when_fail(config: &Config) { if config.get_token().is_empty() { eprintln!("No token set.\nPlease run 'corgea login' to authenticate.\nFor more info checkout our docs at Check out our docs at https://docs.corgea.app/install_cli#login-with-the-cli"); std::process::exit(1); @@ -187,7 +231,7 @@ fn main() { Ok(false) => { println!("Invalid token provided.\nPlease run 'corgea login' to authenticate.\nFor more info checkout our docs at Check out our docs at https://docs.corgea.app/install_cli#login-with-the-cli"); std::process::exit(1); - }, + } Err(e) => { eprintln!("Error occurred: {}", e); std::process::exit(1); @@ -196,18 +240,34 @@ fn main() { } match &cli.command { Some(Commands::Login { token, url, scope }) => { - let effective_token = token.clone().or_else(|| utils::generic::get_env_var_if_exists("CORGEA_TOKEN")); - + let effective_token = token + .clone() + .or_else(|| utils::generic::get_env_var_if_exists("CORGEA_TOKEN")); + match effective_token { Some(token_value) => { - let token_source = if token.is_some() { "parameter" } else { "CORGEA_TOKEN environment variable" }; - match utils::api::verify_token(&token_value, url.as_deref().unwrap_or(corgea_config.get_url().as_str())) { + let token_source = if token.is_some() { + "parameter" + } else { + "CORGEA_TOKEN environment variable" + }; + match utils::api::verify_token( + &token_value, + url.as_deref().unwrap_or(corgea_config.get_url().as_str()), + ) { Ok(true) => { - corgea_config.set_token(token_value.clone()).expect("Failed to set token"); + corgea_config + .set_token(token_value.clone()) + .expect("Failed to set token"); if let Some(url) = url { - corgea_config.set_url(url.clone()).expect("Failed to set url"); + corgea_config + .set_url(url.clone()) + .expect("Failed to set url"); } - println!("Successfully authenticated to Corgea using token from {}.", token_source) + println!( + "Successfully authenticated to Corgea using token from {}.", + token_source + ) } Ok(false) => println!("Invalid token provided from {}.", token_source), Err(e) => { @@ -217,7 +277,7 @@ fn main() { } eprintln!("Error occurred: {}", e); std::process::exit(1); - }, + } } } // No token available - use OAuth flow @@ -225,9 +285,9 @@ fn main() { if url.is_some() && scope.is_some() { eprintln!("Warning: --url option is ignored when using OAuth flow with --scope. The scope determines the domain."); } - + match authorize::run(scope.clone(), url.clone()) { - Ok(()) => {}, + Ok(()) => {} Err(e) => { eprintln!("Authorization failed: {}", e); std::process::exit(1); @@ -236,22 +296,36 @@ fn main() { } } } - Some(Commands::Upload { report }) => { + Some(Commands::Upload { + report, + project_name, + }) => { verify_token_and_exit_when_fail(&corgea_config); match report { Some(report) => { if report.ends_with(".fpr") { - fortify_parse(&corgea_config, report); + fortify_parse(&corgea_config, report, project_name.as_deref()); } else { - scan::read_file_report(&corgea_config, report); + scan::read_file_report(&corgea_config, report, project_name.as_deref()); } } None => { - scan::read_stdin_report(&corgea_config); + scan::read_stdin_report(&corgea_config, project_name.as_deref()); } } } - Some(Commands::Scan { scanner , fail_on, fail, only_uncommitted, scan_type, policy, out_format, out_file, target, project_name }) => { + Some(Commands::Scan { + scanner, + fail_on, + fail, + only_uncommitted, + scan_type, + policy, + out_format, + out_file, + target, + project_name, + }) => { verify_token_and_exit_when_fail(&corgea_config); if let Some(level) = fail_on { if *scanner != Scanner::Blast { @@ -284,7 +358,9 @@ fn main() { std::process::exit(1); } - if out_file.is_some() && !out_format.is_some() || !out_file.is_some() && out_format.is_some() { + if out_file.is_some() && !out_format.is_some() + || !out_file.is_some() && out_format.is_some() + { eprintln!("out_file and out_format must be used together."); std::process::exit(1); } @@ -334,14 +410,32 @@ fn main() { match scanner { Scanner::Snyk => scan::run_snyk(&corgea_config), Scanner::Semgrep => scan::run_semgrep(&corgea_config), - Scanner::Blast => scanners::blast::run(&corgea_config, fail_on.clone(), fail, only_uncommitted, scan_type.clone(), policy.clone(), out_format.clone(), out_file.clone(), target.clone(), project_name.clone()) + Scanner::Blast => scanners::blast::run( + &corgea_config, + fail_on.clone(), + fail, + only_uncommitted, + scan_type.clone(), + policy.clone(), + out_format.clone(), + out_file.clone(), + target.clone(), + project_name.clone(), + ), } } Some(Commands::Wait { scan_id }) => { verify_token_and_exit_when_fail(&corgea_config); wait::run(&corgea_config, scan_id.clone(), None); } - Some(Commands::List { issues , json, page, page_size, scan_id, sca_issues}) => { + Some(Commands::List { + issues, + json, + page, + page_size, + scan_id, + sca_issues, + }) => { verify_token_and_exit_when_fail(&corgea_config); if *issues && *sca_issues { eprintln!("Cannot use both --issues and --sca-issues at the same time."); @@ -351,9 +445,24 @@ fn main() { println!("scan_id option is only supported for issues list command."); std::process::exit(1); } - list::run(&corgea_config, issues, sca_issues, json, page, page_size, scan_id); + list::run( + &corgea_config, + issues, + sca_issues, + json, + page, + page_size, + scan_id, + ); } - Some(Commands::Inspect { issue, json, id, summary, fix, diff }) => { + Some(Commands::Inspect { + issue, + json, + id, + summary, + fix, + diff, + }) => { verify_token_and_exit_when_fail(&corgea_config); inspect::run(&corgea_config, issue, json, summary, fix, diff, id) } diff --git a/src/scan.rs b/src/scan.rs index 767fa6e..7d3d196 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -1,14 +1,14 @@ +use crate::cicd::*; +use crate::log::debug; +use crate::scanners::parsers::ScanParserFactory; +use crate::{utils, Config}; +use reqwest::header; +use serde_json::Value; use std::collections::HashSet; use std::io::{self, Read}; -use crate::{utils, Config}; -use uuid::Uuid; use std::path::Path; use std::process::Command; -use crate::cicd::{*}; -use crate::log::debug; -use reqwest::header; -use crate::scanners::parsers::ScanParserFactory; -use serde_json::Value; +use uuid::Uuid; pub fn run_command(base_cmd: &String, mut command: Command) -> String { match which::which(base_cmd) { @@ -55,13 +55,17 @@ pub fn run_semgrep(config: &Config) { println!("Scanning with semgrep..."); let base_command = "semgrep"; let mut command = std::process::Command::new(base_command); - command.arg("scan").arg("--config").arg("auto").arg("--json"); + command + .arg("scan") + .arg("--config") + .arg("auto") + .arg("--json"); println!("Running \"semgrep scan --config auto --json\""); let output = run_command(&base_command.to_string(), command); - if let Some(result) = parse_scan(config, output, true) { + if let Some(result) = parse_scan(config, output, true, None) { crate::wait::run(config, Some(result.scan_id), result.project_id); } } @@ -76,19 +80,19 @@ pub fn run_snyk(config: &Config) { let output = run_command(&base_command.to_string(), command); - if let Some(result) = parse_scan(config, output, true) { + if let Some(result) = parse_scan(config, output, true, None) { crate::wait::run(config, Some(result.scan_id), result.project_id); } } -pub fn read_stdin_report(config: &Config) { +pub fn read_stdin_report(config: &Config, project_name: Option<&str>) { let mut input = String::new(); let _ = io::stdin().read_to_string(&mut input); - let _ = parse_scan(config, input, false); + let _ = parse_scan(config, input, false, project_name); } -pub fn read_file_report(config: &Config, file_path: &str) { +pub fn read_file_report(config: &Config, file_path: &str, project_name: Option<&str>) { let input = match std::fs::read_to_string(file_path) { Ok(input) => input, Err(e) => { @@ -97,10 +101,15 @@ pub fn read_file_report(config: &Config, file_path: &str) { } }; - let _ = parse_scan(config, input, false); + let _ = parse_scan(config, input, false, project_name); } -pub fn parse_scan(config: &Config, input: String, save_to_file: bool) -> Option { +pub fn parse_scan( + config: &Config, + input: String, + save_to_file: bool, + project_name: Option<&str>, +) -> Option { debug("Parsing the scan report"); // Remove BOM (Byte Order Mark) if present @@ -115,7 +124,14 @@ pub fn parse_scan(config: &Config, input: String, save_to_file: bool) -> Option< std::process::exit(0); } - return upload_scan(config, parse_result.paths, parse_result.scanner, cleaned_input.to_string(), save_to_file); + return upload_scan( + config, + parse_result.paths, + parse_result.scanner, + cleaned_input.to_string(), + save_to_file, + project_name, + ); } Err(error_message) => { @@ -125,7 +141,14 @@ pub fn parse_scan(config: &Config, input: String, save_to_file: bool) -> Option< } } -pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: String, save_to_file: bool) -> Option { +pub fn upload_scan( + config: &Config, + paths: Vec, + scanner: String, + input: String, + save_to_file: bool, + project_name: Option<&str>, +) -> Option { let in_ci = running_in_ci(); let ci_platform = which_ci(); let github_env_vars = get_github_env_vars(); @@ -134,21 +157,35 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: let token = config.get_token(); let base_url = config.get_url(); let current_dir = std::env::current_dir().expect("Failed to get current directory"); - let project; - - if in_ci { + let project = if let Some(project_name) = project_name { + utils::generic::determine_project_name(Some(project_name)) + } else if in_ci { debug("Running in CI"); - project = format!("{}-{}", - github_env_vars.get("GITHUB_REPOSITORY").expect("Failed to get GITHUB_REPOSITORY").to_string(), - github_env_vars.get("GITHUB_PR").expect("Failed to get GITHUB_REPOSITORY").to_string()) + format!( + "{}-{}", + github_env_vars + .get("GITHUB_REPOSITORY") + .expect("Failed to get GITHUB_REPOSITORY") + .to_string(), + github_env_vars + .get("GITHUB_PR") + .expect("Failed to get GITHUB_REPOSITORY") + .to_string() + ) } else { - project = current_dir.file_name().expect("Failed to get directory name").to_str().expect("Failed to convert OsStr to str").to_string(); - } + current_dir + .file_name() + .expect("Failed to get directory name") + .to_str() + .expect("Failed to convert OsStr to str") + .to_string() + }; let repo_data = std::env::var("REPO_DATA").unwrap_or_else(|_| "".to_string()); //encoded data to forward. let scan_upload_url = if repo_data.is_empty() { format!( - "{}/api/cli/scan-upload?token={}&engine={}&run_id={}&project={}&ci={}&ci_platform={}", base_url, token, scanner, run_id, project, in_ci, ci_platform + "{}/api/cli/scan-upload?token={}&engine={}&run_id={}&project={}&ci={}&ci_platform={}", + base_url, token, scanner, run_id, project, in_ci, ci_platform ) } else { format!( @@ -157,7 +194,8 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: }; let git_config_upload_url = format!( - "{}/api/cli/git-config-upload?token={}&run_id={}", base_url, token, run_id + "{}/api/cli/git-config-upload?token={}&run_id={}", + base_url, token, run_id ); let client = utils::api::http_client(); @@ -169,7 +207,10 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: for path in &paths { if !Path::new(&path).exists() { - eprintln!("Required file {} not found which is required for the scan, exiting.", path); + eprintln!( + "Required file {} not found which is required for the scan, exiting.", + path + ); std::process::exit(1); } @@ -178,7 +219,8 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: } let src_upload_url = format!( - "{}/api/cli/code-upload?token={}&run_id={}&path={}", base_url, token, run_id, path + "{}/api/cli/code-upload?token={}&run_id={}&path={}", + base_url, token, run_id, path ); debug(&format!("Uploading file: {}", path)); let fp = Path::new(&path); @@ -192,14 +234,16 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: .expect("Failed to read file"); debug(&format!("POST: {}", src_upload_url)); - let res = client.post(&src_upload_url) - .multipart(form) - .send(); + let res = client.post(&src_upload_url).multipart(form).send(); match res { Ok(response) => { if !response.status().is_success() { - eprintln!("Failed to upload file {} {}... retrying", response.status(), path); + eprintln!( + "Failed to upload file {} {}... retrying", + response.status(), + path + ); std::thread::sleep(std::time::Duration::from_secs(1)); attempts += 1; } else { @@ -217,7 +261,10 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: if attempts == 3 && !success { upload_error_count += 1; - eprintln!("Failed to upload file: {} after 3 attempts. skipping...", path); + eprintln!( + "Failed to upload file: {} after 3 attempts. skipping...", + path + ); } } @@ -240,8 +287,14 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: let mut last_response = None; for (index, chunk) in input_bytes.chunks(chunk_size).enumerate() { - debug(&format!("POST: {} (chunk {}/{})", scan_upload_url, index + 1, total_chunks)); - let response = client.post(&scan_upload_url) + debug(&format!( + "POST: {} (chunk {}/{})", + scan_upload_url, + index + 1, + total_chunks + )); + let response = client + .post(&scan_upload_url) .header(header::CONTENT_TYPE, "application/json") .header("Upload-Offset", offset.to_string()) .header("Upload-Length", input_size.to_string()) @@ -261,7 +314,8 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: last_response.expect("Failed to upload scan.") } else { debug(&format!("POST: {}", scan_upload_url)); - client.post(&scan_upload_url) + client + .post(&scan_upload_url) .header(header::CONTENT_TYPE, "application/json") .body(input.clone()) .send() @@ -316,7 +370,6 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: } } - let git_config_path = Path::new(".git/config"); if git_config_path.exists() { @@ -326,9 +379,7 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: .expect("Failed to read file"); debug(&format!("POST: {}", git_config_upload_url)); - let res = client.post(&git_config_upload_url) - .multipart(form) - .send(); + let res = client.post(&git_config_upload_url).multipart(form).send(); match res { Ok(response) => { @@ -344,7 +395,8 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: if in_ci { let ci_data_upload_url = format!( - "{}/api/cli/ci-data-upload?token={}&run_id={}&platform={}", base_url, token, run_id, ci_platform + "{}/api/cli/ci-data-upload?token={}&run_id={}&platform={}", + base_url, token, run_id, ci_platform ); let mut github_env_vars_json = serde_json::Map::new(); @@ -361,7 +413,8 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: }; debug(&format!("POST: {}", ci_data_upload_url)); - let _res = client.post(ci_data_upload_url) + let _res = client + .post(ci_data_upload_url) .header(header::CONTENT_TYPE, "application/json") .body(github_env_vars_json_string) .send(); @@ -373,17 +426,26 @@ pub fn upload_scan(config: &Config, paths: Vec, scanner: String, input: match std::fs::write(&file_path, input.clone()) { Ok(_) => println!("Successfully saved scan to {}", file_path.display()), - Err(e) => eprintln!("Failed to save scan to {}: {}", file_path.display(), e) + Err(e) => eprintln!("Failed to save scan to {}: {}", file_path.display(), e), } } - println!("Successfully scanned using {} and uploaded to Corgea.", scanner); + println!( + "Successfully scanned using {} and uploaded to Corgea.", + scanner + ); if upload_error_count > 0 { - println!("Failed to upload {} files, you may not see all fixes in Corgea.", upload_error_count); + println!( + "Failed to upload {} files, you may not see all fixes in Corgea.", + upload_error_count + ); } println!("Go to {base_url} to see results."); - sast_scan_id.map(|scan_id| ScanUploadResult { scan_id, project_id }) + sast_scan_id.map(|scan_id| ScanUploadResult { + scan_id, + project_id, + }) } diff --git a/src/scanners/fortify.rs b/src/scanners/fortify.rs index f155a16..1b39b44 100644 --- a/src/scanners/fortify.rs +++ b/src/scanners/fortify.rs @@ -1,15 +1,15 @@ +use crate::scan::upload_scan; +use crate::Config; +use quick_xml::events::Event; +use quick_xml::reader::Reader; use std::fs::File; use std::io; +use std::io::{BufReader, Read}; use std::path::PathBuf; -use zip::ZipArchive; use tempfile::TempDir; -use std::io::{Read, BufReader}; -use quick_xml::events::Event; -use quick_xml::reader::Reader; -use crate::Config; -use crate::scan::upload_scan; +use zip::ZipArchive; -pub fn parse(config: &Config, file_path: &str) { +pub fn parse(config: &Config, file_path: &str, project_name: Option<&str>) { let temp_dir = match TempDir::new() { Ok(dir) => dir, Err(e) => { @@ -48,7 +48,14 @@ pub fn parse(config: &Config, file_path: &str) { } let (scan_data, paths) = extract_file_path(outpath); - let _scan_id = upload_scan(config, paths, "fortify".to_string(), scan_data, false); + let _scan_id = upload_scan( + config, + paths, + "fortify".to_string(), + scan_data, + false, + project_name, + ); } else { println!("File 'audit.fvdl' not found in the archive"); }; @@ -61,7 +68,9 @@ fn extract_file_path(scan_file: PathBuf) -> (String, Vec) { let mut reader = BufReader::new(file); let mut contents = String::new(); - reader.read_to_string(&mut contents).expect("Unable to read file"); + reader + .read_to_string(&mut contents) + .expect("Unable to read file"); let mut xml_reader = Reader::from_str(&contents); xml_reader.config_mut().trim_text(true);