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 skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 18 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

#[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 {
Expand Down Expand Up @@ -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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--wait uses the wrong project identity (breaks --project-name and CI uploads)

ScanUploadResult now carries project_name, but this call discards it:

wait::run(&corgea_config, Some(result.scan_id), result.project_id);

wait::run always re-derives the project from the CWD and unconditionally queries that project's scan list before using the provided scan_id:

pub fn run(config: &Config, scan_id: Option<String>, project_id: Option<String>) {
    let project_name = match utils::generic::get_current_working_directory() { ... };
    let scans_result =
        utils::api::query_scan_list(&config.get_url(), Some(&project_name), Some(1), None);
    // Err => process::exit(1)  — even when scan_id was already known

Impact

  • corgea upload report.json --project-name my-svc --wait uploads to my-svc, then waits against the CWD project. If that CWD project is missing/unreachable, wait exits 1 after a successful upload.
  • In CI, upload_scan names the project {GITHUB_REPOSITORY}-{GITHUB_PR} (src/scan.rs ~204–214), which almost never matches the checkout directory name — so upload --wait in Actions is likely to fail or print the wrong fallback URL when project_id is absent.

Fix

  1. Extend wait::run to accept the upload’s project_name (or skip query_scan_list entirely when scan_id is Some).
  2. Pass result.project_name through from this call site (and ideally from run_semgrep / run_snyk too).
  3. Add a regression test: upload with --project-name ≠ CWD + --wait must not query/exit on the CWD project.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--wait still discards the uploaded project identity

upload_scan chooses the project from --project-name or, in CI, {GITHUB_REPOSITORY}-{GITHUB_PR} (src/scan.rs:209-222) and returns that value as ScanUploadResult.project_name (src/scan.rs:566-570). This call passes only the scan/project IDs. wait::run then derives project_name from the current directory and unconditionally calls query_scan_list for that unrelated project before it examines the supplied scan ID (src/wait.rs:6-32); an error exits 1. It also uses the CWD name for fallback links and reporting (src/wait.rs:54-79).

Impact: corgea upload report.json --project-name svc --wait and the documented CI upload path can successfully upload, then fail while querying the CWD project or print/report against the wrong project.

Concrete fix: pass result.project_name into wait::run, use it for reporting/fallback URLs, and skip the scan-list lookup when a scan_id is already supplied. Add a regression test where the uploaded project differs from the CWD project.

} else {
scan::print_scan_tracking_url(&corgea_config, &result);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--wait fails open when the upload returns no scan id

If upload_scan succeeds but the response has no sast_scan_id (allowed for non-chunked uploads — only chunked treats a missing id as hard failure), result is None and this block is skipped. With --wait set, the process still exits 0 without waiting or printing results.

Impact: Users/CI that pass --wait get a false success: the command claimed it would block until completion, but it did not.

Fix: When *wait is true and result is None, error and exit(1) (e.g. “upload succeeded but server did not return a scan id; cannot wait”). Optionally also harden upload_scan so a missing sast_scan_id on any successful response is always a hard failure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--wait still fails open when a successful response has no scan ID

This gate silently does nothing when result is None. That state is reachable after a 2xx non-chunked upload: upload_scan treats a missing sast_scan_id as an error only for chunked uploads (src/scan.rs:462-467), then maps the absent ID to None (src/scan.rs:566).

Impact: CI can invoke upload --wait, receive exit code 0, and continue even though the command never waited for completion or printed results.

A narrow fix is to fail when waiting was requested but the API did not provide the ID:

Suggested change
}
} else if *wait {
log::error!(
"Upload succeeded but the server did not return a scan ID; cannot wait."
);
std::process::exit(1);
}

Also cover this branch with a CLI-level regression test so --wait cannot become a no-op again.

}
Expand Down
90 changes: 86 additions & 4 deletions src/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,48 @@ pub fn run_command(base_cmd: &String, mut command: Command) -> String {
pub struct ScanUploadResult {
pub scan_id: String,
pub project_id: Option<String>,
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 {
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={}", 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={}",
base_url,
urlencoding::encode(&result.project_name),
result.scan_id
),
Comment thread
Ibrahimrahhal marked this conversation as resolved.
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

/// 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<String>) {
Expand Down Expand Up @@ -85,14 +127,21 @@ pub fn run_snyk(config: &Config, project_name: Option<String>) {
}
}

pub fn read_stdin_report(config: &Config, project_name: Option<String>) {
pub fn read_stdin_report(
config: &Config,
project_name: Option<String>,
) -> Option<ScanUploadResult> {
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<String>) {
pub fn read_file_report(
config: &Config,
file_path: &str,
project_name: Option<String>,
) -> Option<ScanUploadResult> {
let input = match std::fs::read_to_string(file_path) {
Ok(input) => input,
Err(e) => {
Expand All @@ -101,7 +150,7 @@ pub fn read_file_report(config: &Config, file_path: &str, project_name: Option<S
}
};

let _ = parse_scan(config, input, false, project_name);
parse_scan(config, input, false, project_name)
}

pub fn parse_scan(
Expand Down Expand Up @@ -517,5 +566,38 @@ pub fn upload_scan(
sast_scan_id.map(|scan_id| ScanUploadResult {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale generic “Go to {base_url}” undercuts the new tracking URL

upload_scan still always prints Go to {base_url} to see results. before the caller prints the specific /project/.../?scan_id=... link via print_scan_tracking_url. Users now see two different destinations for the same upload.

Impact: Misleading output (and noisier CI logs) right as this PR’s value prop is “print the scan page URL.”

Fix: Drop or gate this generic line when a ScanUploadResult (with scan id) is being returned, and let the new tracking helper be the sole post-upload navigation hint.

scan_id,
project_id,
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"
);
}
}
24 changes: 15 additions & 9 deletions src/scanners/fortify.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::scan::upload_scan;
use crate::scan::{upload_scan, ScanUploadResult};
use crate::Config;
use quick_xml::events::Event;
use quick_xml::reader::Reader;
Expand All @@ -10,56 +10,62 @@ use std::path::PathBuf;
use tempfile::TempDir;
use zip::ZipArchive;

pub fn parse(config: &Config, file_path: &str, project_name: Option<String>) {
pub fn parse(
config: &Config,
file_path: &str,
project_name: Option<String>,
) -> Option<ScanUploadResult> {
let temp_dir = match TempDir::new() {
Ok(dir) => dir,
Err(e) => {
println!("Error creating temporary directory: {}", e);
return;
return None;
}
};

let zip_file = match File::open(file_path) {
Ok(file) => file,
Err(e) => {
println!("Error opening file: {}", e);
return;
return None;
}
};

let mut archive = match ZipArchive::new(zip_file) {
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) {
println!("Error copying file: {}", e);
}

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<String>) {
Expand Down
Loading