From 7493ff81a2a31b91d5f401eb8d06d02cea1ffc6e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 10:44:09 +0000 Subject: [PATCH 1/2] Add --wait to upload command and print scan page URL when not waiting Co-authored-by: ibrahim --- skills/corgea/SKILL.md | 3 +++ src/main.rs | 23 ++++++++++++++---- src/scan.rs | 53 +++++++++++++++++++++++++++++++++++++---- src/scanners/fortify.rs | 24 ++++++++++++------- 4 files changed, 85 insertions(+), 18 deletions(-) diff --git a/skills/corgea/SKILL.md b/skills/corgea/SKILL.md index 1f9936d..15e971d 100644 --- a/skills/corgea/SKILL.md +++ b/skills/corgea/SKILL.md @@ -51,10 +51,13 @@ corgea upload path/to/report.json # JSON, SARIF, Coverity XML corgea upload path/to/report.fpr # Fortify FPR corgea upload report.sarif --project-name svc # Custom project name cat report.json | corgea upload # From stdin +corgea upload report.json --wait # Wait for scan to complete and print results ``` Supported: Semgrep JSON, SARIF, Checkmarx (CLI/Web/XML), Coverity, Fortify FPR. +By default `upload` prints the scan page URL so you can track the results. Pass `--wait` to block until the scan completes and print the results (like `corgea scan`). + ### Wait — `corgea wait [scan_id]` ```bash diff --git a/src/main.rs b/src/main.rs index 5023c58..b7ce074 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,6 +65,12 @@ enum Commands { help = "The name of the Corgea project. Defaults to git repository name if found, otherwise to the current directory name." )] project_name: Option, + + #[arg( + long, + help = "Wait for the uploaded scan to complete and print the results. Without this flag, the command prints the scan page URL so you can track the results." + )] + wait: bool, }, /// Scan the current directory. Supports blast, semgrep and snyk. Scan { @@ -469,18 +475,25 @@ fn main() { Some(Commands::Upload { report, project_name, + wait, }) => { verify_token_and_exit_when_fail(&corgea_config); - match report { + let result = match report { Some(report) => { if report.ends_with(".fpr") { - fortify_parse(&corgea_config, report, project_name.clone()); + fortify_parse(&corgea_config, report, project_name.clone()) } else { - scan::read_file_report(&corgea_config, report, project_name.clone()); + scan::read_file_report(&corgea_config, report, project_name.clone()) } } - None => { - scan::read_stdin_report(&corgea_config, project_name.clone()); + None => scan::read_stdin_report(&corgea_config, project_name.clone()), + }; + + if let Some(result) = result { + if *wait { + wait::run(&corgea_config, Some(result.scan_id), result.project_id); + } else { + scan::print_scan_tracking_url(&corgea_config, &result); } } } diff --git a/src/scan.rs b/src/scan.rs index af1fd4a..8ae4661 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -49,6 +49,43 @@ pub fn run_command(base_cmd: &String, mut command: Command) -> String { pub struct ScanUploadResult { pub scan_id: String, pub project_id: Option, + pub project_name: String, +} + +/// Build the URL to the Corgea scan page so users can track results. +pub fn build_scan_url(config: &Config, result: &ScanUploadResult) -> String { + match &result.project_id { + Some(pid) => format!( + "{}/project/{}/?scan_id={}", + config.get_url(), + pid, + result.scan_id + ), + None => format!( + "{}/project/{}?scan_id={}", + config.get_url(), + result.project_name, + result.scan_id + ), + } +} + +/// Print the scan page URL so the user can track the scan results without +/// waiting for it to complete. +pub fn print_scan_tracking_url(config: &Config, result: &ScanUploadResult) { + let scan_url = build_scan_url(config, result); + print!( + "\n\nScan has started with ID: {}.\n\nYou can view it populate at the link:\n{}\n\n", + result.scan_id, + utils::terminal::set_text_color(&scan_url, utils::terminal::TerminalColor::Green) + ); + print!( + "{}", + utils::terminal::set_text_color( + "Your scan will continue securely in the Corgea cloud.\n", + utils::terminal::TerminalColor::Blue + ) + ); } pub fn run_semgrep(config: &Config, project_name: Option) { @@ -85,14 +122,21 @@ pub fn run_snyk(config: &Config, project_name: Option) { } } -pub fn read_stdin_report(config: &Config, project_name: Option) { +pub fn read_stdin_report( + config: &Config, + project_name: Option, +) -> Option { let mut input = String::new(); let _ = io::stdin().read_to_string(&mut input); - let _ = parse_scan(config, input, false, project_name); + parse_scan(config, input, false, project_name) } -pub fn read_file_report(config: &Config, file_path: &str, project_name: Option) { +pub fn read_file_report( + config: &Config, + file_path: &str, + project_name: Option, +) -> Option { let input = match std::fs::read_to_string(file_path) { Ok(input) => input, Err(e) => { @@ -101,7 +145,7 @@ pub fn read_file_report(config: &Config, file_path: &str, project_name: Option) { +pub fn parse( + config: &Config, + file_path: &str, + project_name: Option, +) -> Option { let temp_dir = match TempDir::new() { Ok(dir) => dir, Err(e) => { println!("Error creating temporary directory: {}", e); - return; + return None; } }; @@ -23,7 +27,7 @@ pub fn parse(config: &Config, file_path: &str, project_name: Option) { Ok(file) => file, Err(e) => { println!("Error opening file: {}", e); - return; + return None; } }; @@ -31,17 +35,17 @@ pub fn parse(config: &Config, file_path: &str, project_name: Option) { Ok(archive) => archive, Err(e) => { println!("Error reading zip archive: {}", e); - return; + return None; } }; - if let Ok(mut file) = archive.by_name("audit.fvdl") { + let result = if let Ok(mut file) = archive.by_name("audit.fvdl") { let outpath = temp_dir.path().join("audit.fvdl"); let mut outfile = match File::create(&outpath) { Ok(f) => f, Err(e) => { println!("Error creating output file: {}", e); - return; + return None; } }; if let Err(e) = io::copy(&mut file, &mut outfile) { @@ -49,17 +53,19 @@ pub fn parse(config: &Config, file_path: &str, project_name: Option) { } let (scan_data, paths) = extract_file_path(outpath); - let _scan_id = upload_scan( + upload_scan( config, paths, "fortify".to_string(), scan_data, false, project_name, - ); + ) } else { println!("File 'audit.fvdl' not found in the archive"); + None }; + result } fn extract_file_path(scan_file: PathBuf) -> (String, Vec) { From 9c18d3dd79953b224c777da4d45bb54968774b40 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 30 Jul 2026 06:22:19 +0000 Subject: [PATCH 2/2] Percent-encode project name in scan tracking URL fallback Co-authored-by: ibrahim --- src/scan.rs | 53 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/src/scan.rs b/src/scan.rs index 8ae4661..c533ac7 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -54,17 +54,22 @@ pub struct ScanUploadResult { /// Build the URL to the Corgea scan page so users can track results. pub fn build_scan_url(config: &Config, result: &ScanUploadResult) -> String { + build_scan_url_from_base(&config.get_url(), result) +} + +/// Build the scan page URL from an explicit base URL. Split out from +/// `build_scan_url` so the project-segment encoding contract can be unit +/// tested without constructing a `Config`. +fn build_scan_url_from_base(base_url: &str, result: &ScanUploadResult) -> String { match &result.project_id { - Some(pid) => format!( - "{}/project/{}/?scan_id={}", - config.get_url(), - pid, - result.scan_id - ), + Some(pid) => format!("{}/project/{}/?scan_id={}", base_url, pid, result.scan_id), + // The project name is a free-form path segment. In CI it is + // `{owner/repo}-{pr}`, so it must be percent-encoded or the `/` + // would route the tracking link to the wrong project. None => format!( "{}/project/{}?scan_id={}", - config.get_url(), - result.project_name, + base_url, + urlencoding::encode(&result.project_name), result.scan_id ), } @@ -564,3 +569,35 @@ pub fn upload_scan( project_name: project.clone(), }) } + +#[cfg(test)] +mod tests { + use super::*; + + fn result(project_id: Option<&str>, project_name: &str) -> ScanUploadResult { + ScanUploadResult { + scan_id: "scan-123".to_string(), + project_id: project_id.map(|p| p.to_string()), + project_name: project_name.to_string(), + } + } + + #[test] + fn scan_url_prefers_project_id_when_present() { + let url = + build_scan_url_from_base("https://www.corgea.app", &result(Some("42"), "some/name")); + assert_eq!(url, "https://www.corgea.app/project/42/?scan_id=scan-123"); + } + + #[test] + fn scan_url_percent_encodes_project_name_fallback() { + // CI project names are `{owner/repo}-{pr}`; the `/` must be encoded so + // the link resolves to the intended project rather than a nested path. + let url = + build_scan_url_from_base("https://www.corgea.app", &result(None, "corgea/cli-15")); + assert_eq!( + url, + "https://www.corgea.app/project/corgea%2Fcli-15?scan_id=scan-123" + ); + } +}