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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "corgea"
version = "1.10.0"
version = "1.10.1"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ Waiting gives up after 10 hours; override with `CORGEA_SCAN_TIMEOUT_SECONDS`.
`--fail`/`--block-on` then wait up to 15 minutes for blocking rules to be
evaluated; override with `CORGEA_BLOCKING_RULES_TIMEOUT_SECONDS`.

`--out-format`/`--out-file` and `--sbom` are honored whether or not a gate
trips: both are written before `--fail`/`--block-on` are evaluated, so a scan
that exits 1 on a blocking rule still leaves its report behind to ingest.

## Dependency Inventory (offline)

`corgea deps` builds a dependency inventory from npm, Python, and Java manifests
Expand Down
5 changes: 5 additions & 0 deletions skills/corgea/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ Scan types: `blast` (base AI), `policy` (PolicyIQ), `malicious`, `secrets`, `pii

`--only-uncommitted` and `--target` are mutually exclusive. `--fail-on`, `--fail`, and `--block-on` are mutually exclusive.

`--out-format`/`--out-file` and `--sbom` are honored regardless of the gate: the report and the SBOM are written before `--fail`/`--block-on` are evaluated, so a scan that exits 1 on a blocking rule still leaves the report file behind for the pipeline to ingest.

### Upload — `corgea upload [report]`

Upload an existing scan report to Corgea.
Expand Down Expand Up @@ -374,6 +376,9 @@ corgea scan --fail-on CR,malicious --out-format sarif --out-file results.sarif
corgea scan --block-on criticals --out-format sarif --out-file results.sarif # gate on a CI blocking rule from the web app
```

The report is written whether or not the gate trips, so a pipeline can both fail
on policy and ingest the results file.

### Upload third-party reports

```bash
Expand Down
235 changes: 124 additions & 111 deletions src/scanners/blast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,22 @@ pub fn run(
std::process::exit(1);
}
};
// The report and the SBOM are produced before the blocking-rule gates: a
// tripped gate exits 1, and a pipeline that fails on policy is exactly the
// one that needs the report, to ingest the findings it failed on.
write_scan_report(
config,
&project_name,
&scan_id,
&classifications,
out_format.as_deref(),
out_file.as_deref(),
);

if let Some(sbom_file) = sbom {
write_sbom(&sbom_file);
}

if *fail {
log::warn!(
"\n--fail is deprecated: it evaluates every active blocking rule regardless of whether it applies to pull requests or CI. Use --block-on <slug> to name the CI blocking rules this pipeline should enforce."
Expand Down Expand Up @@ -381,117 +397,6 @@ pub fn run(
);
}

if let Some(out_file) = out_file {
if let Some(out_format) = out_format {
let stop_signal = Arc::new(Mutex::new(false));
let stop_signal_clone = Arc::clone(&stop_signal);
let results_thread = thread::spawn(move || {
utils::terminal::show_loading_message(
"Generating scan report... ([T]s)",
stop_signal_clone,
);
});

if out_format == "json" {
let issues = match utils::api::get_all_issues(
&config.get_url(),
&project_name,
Some(scan_id.clone()),
) {
Ok(issues) => issues,
Err(e) => {
log::error!("\n\nFailed to fetch issues: {}\n\n", e);
std::process::exit(1);
}
};
let sca_issues = match utils::api::get_all_sca_issues(
&config.get_url(),
&project_name,
Some(scan_id.clone()),
) {
Ok(issues) => issues,
Err(e) => {
log::error!("\n\nFailed to fetch SCA issues: {}\n\n", e);
std::process::exit(1);
}
};
let json = serde_json::to_string_pretty(&issues).unwrap();
let sca_json = serde_json::to_string_pretty(&sca_issues).unwrap();
let report_json = serde_json::to_string_pretty(&classifications).unwrap();
let results_json = format!(
"{{\"issues\": {}, \"sca_issues\": {}, \"report\": {}}}",
json, sca_json, report_json
);
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
fs::write(out_file.clone(), results_json).expect("Failed to write JSON file, check if the file path is valid and you have the necessary permissions to write to it.");
utils::terminal::clear_previous_line();
println!("\n\nScan results written to: {}\n\n", out_file.clone());
} else if out_format == "html" {
let report = match utils::api::get_scan_report(&config.get_url(), &scan_id, None) {
Ok(html) => html,
Err(e) => {
log::error!("\n\nFailed to fetch scan report: {}\n\n", e);
std::process::exit(1);
}
};
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
fs::write(out_file.clone(), report).expect("\n\nFailed to write HTML file, check if the file path is valid and you have the necessary permissions to write to it.");
utils::terminal::clear_previous_line();
println!("\n\nScan report written to: {}\n\n", out_file.clone());
} else if out_format == "sarif" {
let report =
match utils::api::get_scan_report(&config.get_url(), &scan_id, Some("sarif")) {
Ok(sarif) => sarif,
Err(e) => {
log::error!("\n\nFailed to fetch SARIF report: {}\n\n", e);
std::process::exit(1);
}
};
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
fs::write(out_file.clone(), report).expect("\n\nFailed to write SARIF file, check if the file path is valid and you have the necessary permissions to write to it.");
utils::terminal::clear_previous_line();
println!("\n\nScan report written to: {}\n\n", out_file.clone());
} else if out_format == "markdown" {
let report = match utils::api::get_scan_report(
&config.get_url(),
&scan_id,
Some("markdown"),
) {
Ok(markdown) => markdown,
Err(e) => {
log::error!("\n\nFailed to fetch Markdown report: {}\n\n", e);
std::process::exit(1);
}
};
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
fs::write(out_file.clone(), report).expect("\n\nFailed to write Markdown file, check if the file path is valid and you have the necessary permissions to write to it.");
utils::terminal::clear_previous_line();
println!("\n\nScan report written to: {}\n\n", out_file.clone());
}
}
}

if let Some(sbom_file) = sbom {
match corgea::deps::report::sbom(std::path::Path::new(".")) {
Ok(doc) => {
let json = serde_json::to_string_pretty(&doc).expect("serialize SBOM");
if let Err(e) = fs::write(&sbom_file, json) {
log::error!("\n\nFailed to write SBOM to '{}': {}\n\n", sbom_file, e);
std::process::exit(1);
}
println!("CycloneDX SBOM written to: {}\n", sbom_file);
}
Err(e) => {
log::error!("\n\nFailed to generate SBOM: {}\n\n", e);
std::process::exit(1);
}
}
}

print!("\n\nThank you for using Corgea! 🐕\n\n");

if let Some(fail_on) = fail_on {
Expand Down Expand Up @@ -538,6 +443,114 @@ pub fn run(
}
}

/// Write the `--out-format` report for a completed scan to `--out-file`.
///
/// Does nothing unless both are set; `main` rejects one without the other.
fn write_scan_report(
config: &Config,
project_name: &str,
scan_id: &str,
classifications: &HashMap<String, usize>,
out_format: Option<&str>,
out_file: Option<&str>,
) {
let (Some(out_format), Some(out_file)) = (out_format, out_file) else {
return;
};

let stop_signal = Arc::new(Mutex::new(false));
let stop_signal_clone = Arc::clone(&stop_signal);
let results_thread = thread::spawn(move || {
utils::terminal::show_loading_message(
"Generating scan report... ([T]s)",
stop_signal_clone,
);
});
let stop_spinner = move || {
*stop_signal.lock().unwrap() = true;
let _ = results_thread.join();
};

if out_format == "json" {
let issues =
match utils::api::get_all_issues(&config.get_url(), project_name, Some(scan_id.into()))
{
Ok(issues) => issues,
Err(e) => {
stop_spinner();
log::error!("\n\nFailed to fetch issues: {}\n\n", e);
std::process::exit(1);
}
};
let sca_issues = match utils::api::get_all_sca_issues(
&config.get_url(),
project_name,
Some(scan_id.into()),
) {
Ok(issues) => issues,
Err(e) => {
stop_spinner();
log::error!("\n\nFailed to fetch SCA issues: {}\n\n", e);
std::process::exit(1);
}
};
let json = serde_json::to_string_pretty(&issues).unwrap();
let sca_json = serde_json::to_string_pretty(&sca_issues).unwrap();
let report_json = serde_json::to_string_pretty(classifications).unwrap();
let results_json = format!(
"{{\"issues\": {}, \"sca_issues\": {}, \"report\": {}}}",
json, sca_json, report_json
);
stop_spinner();
fs::write(out_file, results_json).expect("Failed to write JSON file, check if the file path is valid and you have the necessary permissions to write to it.");
utils::terminal::clear_previous_line();
println!("\n\nScan results written to: {}\n\n", out_file);
return;
}

// The server renders these; `None` is its HTML default.
let (report_format, label) = match out_format {
"html" => (None, "HTML"),
"sarif" => (Some("sarif"), "SARIF"),
"markdown" => (Some("markdown"), "Markdown"),
_ => {
stop_spinner();
log::error!("\n\nUnsupported out_format: {}\n\n", out_format);
std::process::exit(1);
}
};
let report = match utils::api::get_scan_report(&config.get_url(), scan_id, report_format) {
Ok(report) => report,
Err(e) => {
stop_spinner();
log::error!("\n\nFailed to fetch {} report: {}\n\n", label, e);
std::process::exit(1);
}
};
stop_spinner();
fs::write(out_file, report).unwrap_or_else(|_| panic!("\n\nFailed to write {label} file, check if the file path is valid and you have the necessary permissions to write to it."));
utils::terminal::clear_previous_line();
println!("\n\nScan report written to: {}\n\n", out_file);
}

Comment on lines +497 to +535

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Quality - The code uses multiple "unwrap()" calls on fallible operations like "to_string_pretty", which can panic and crash the CLI instead of handling errors gracefully. This breaks the consistent error handling style in the file. View in Corgea ↗

More Details
🎟️Issue Explanation: The code uses multiple "unwrap()" calls on fallible operations like "to_string_pretty", which can panic and crash the CLI instead of handling errors gracefully. This breaks the consistent error handling style in the file.

- Crashes caused by "unwrap()" lose error context, making debugging harder than using structured error logs as seen elsewhere.

- Unexpected panics reduce CLI predictability, harming user experience and automation workflows relying on exit codes.

- Inconsistent error handling increases maintenance complexity, confusing developers about standard error patterns in the codebase.

🪄Fix Explanation: The report writer now handles serialization and file-write failures explicitly instead of panicking or silently ignoring detected errors. It logs actionable messages, stops the spinner, and exits with a failure status for consistent CLI behavior.
-Replaces "unwrap()" with "match" blocks for issues, SCA issues, and classifications, making serialization failures explicit and maintainable.
-Logs the original error through "log::error!", giving users actionable context about which report component could not be serialized.
-Replaces panic-based writes with "if let Err(e) = fs::write(...)", avoiding uncontrolled panic output and handling filesystem failures consistently.
-Calls "stop_spinner()" before serialization failure exits, ensuring terminal UI state is cleaned up before the process terminates.
-Uses "std::process::exit(1)" for all handled failures, providing a reliable nonzero status to scripts and CI pipelines.
diff --git a/src/scanners/blast.rs b/src/scanners/blast.rs
index 05997dd..9bf7fe3 100644
--- a/src/scanners/blast.rs
+++ b/src/scanners/blast.rs
@@ -494,15 +494,39 @@ fn write_scan_report(
                 std::process::exit(1);
             }
         };
-        let json = serde_json::to_string_pretty(&issues).unwrap();
-        let sca_json = serde_json::to_string_pretty(&sca_issues).unwrap();
-        let report_json = serde_json::to_string_pretty(classifications).unwrap();
+        let json = match serde_json::to_string_pretty(&issues) {
+            Ok(json) => json,
+            Err(e) => {
+                stop_spinner();
+                log::error!("\n\nFailed to serialize issues: {}\n\n", e);
+                std::process::exit(1);
+            }
+        };
+        let sca_json = match serde_json::to_string_pretty(&sca_issues) {
+            Ok(json) => json,
+            Err(e) => {
+                stop_spinner();
+                log::error!("\n\nFailed to serialize SCA issues: {}\n\n", e);
+                std::process::exit(1);
+            }
+        };
+        let report_json = match serde_json::to_string_pretty(classifications) {
+            Ok(json) => json,
+            Err(e) => {
+                stop_spinner();
+                log::error!("\n\nFailed to serialize scan report: {}\n\n", e);
+                std::process::exit(1);
+            }
+        };
         let results_json = format!(
             "{{\"issues\": {}, \"sca_issues\": {}, \"report\": {}}}",
             json, sca_json, report_json
         );
         stop_spinner();
-        fs::write(out_file, results_json).expect("Failed to write JSON file, check if the file path is valid and you have the necessary permissions to write to it.");
+        if let Err(e) = fs::write(out_file, results_json) {
+            log::error!("\n\nFailed to write JSON file: {}\n\n", e);
+            std::process::exit(1);
+        }
         utils::terminal::clear_previous_line();
         println!("\n\nScan results written to: {}\n\n", out_file);
         return;
@@ -528,7 +552,10 @@ fn write_scan_report(
         }
     };
     stop_spinner();
-    fs::write(out_file, report).unwrap_or_else(|_| panic!("\n\nFailed to write {label} file, check if the file path is valid and you have the necessary permissions to write to it."));
+    if let Err(e) = fs::write(out_file, report) {
+        log::error!("\n\nFailed to write {} file: {}\n\n", label, e);
+        std::process::exit(1);
+    }
     utils::terminal::clear_previous_line();
     println!("\n\nScan report written to: {}\n\n", out_file);
 }

To apply the fix, Download .patch.

/// Write a CycloneDX SBOM of the working directory to `sbom_file`.
fn write_sbom(sbom_file: &str) {
match corgea::deps::report::sbom(std::path::Path::new(".")) {
Ok(doc) => {
let json = serde_json::to_string_pretty(&doc).expect("serialize SBOM");
if let Err(e) = fs::write(sbom_file, json) {
log::error!("\n\nFailed to write SBOM to '{}': {}\n\n", sbom_file, e);
std::process::exit(1);
}
println!("CycloneDX SBOM written to: {}\n", sbom_file);
}
Err(e) => {
log::error!("\n\nFailed to generate SBOM: {}\n\n", e);
std::process::exit(1);
}
}
}

pub const VALID_FAIL_ON_TOKENS: [&str; 5] = ["CR", "HI", "ME", "LO", "malicious"];

/// Parse and validate a comma-separated --fail-on value.
Expand Down
Loading
Loading