Write the scan report and SBOM before the blocking-rule gates exit - #156
Conversation
--out-format/--out-file and --sbom ran after the --fail and --block-on gates, and a tripped gate calls exit(1), so a scan that violated a blocking rule produced no report at all. That is backwards: a pipeline that gates on policy is precisely the one that needs the report file, to ingest the findings it just failed on. Only a passing gate left a report behind, so the flags appeared to work until a rule actually tripped. Both now run immediately after the scan results are collected, ahead of every gate. Rather than reorder the bodies in place, they moved into write_scan_report and write_sbom, which also collapses the four near-identical out-format branches into one server-rendered path plus the JSON case, and stops the spinner thread on the error exits that previously left it running. The e2e plans are ordered, so they pin the sequence: a reorder back puts the report request after check_blocking_rules and fails the plan. Covered for --block-on (with an SBOM), for the deprecated --fail, and for a gate that passes, which must still write the report and exit 0. corgea deps scan already emits its out-file before its --fail-on gate can return non-zero, and no other command pairs an output file with a gate. Co-authored-by: ibrahim <ibrahim@corgea.com>
A report that was silently dropped when a gate tripped is a bug fix, not a new flag, so SemVer puts this at a patch bump. Cargo.toml is the single source of truth: PyPI reads it via maturin and npm takes its version from the release tag. Co-authored-by: ibrahim <ibrahim@corgea.com>
There was a problem hiding this comment.
No actionable findings.
Verified against merge-base df234a426a9120789bc6052b4ea292b9f7880425 that report and SBOM writes now complete before both blocking-rule exit paths, while --fail-on retains its prior ordering. The ordered e2e stub fails if the report request moves behind gate evaluation, and the blocked-path assertions require exit 1 plus persisted SARIF and CycloneDX artifacts. I also checked the JSON/HTML/SARIF/Markdown refactor against the prior branches, CLI format/pair validation, report API authentication and error paths, blocking-rule timeout behavior, and the unchanged non-gated path. The Rust CI gate passed strict clippy, formatting, dependency audit, and all 685 tests.
Sent by Cursor Automation: pr-flow
| 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); | ||
| } | ||
|
|
There was a problem hiding this comment.
🧹 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.
corgea-security
left a comment
There was a problem hiding this comment.
Automated review risk: 2/5.
The artifact generation is correctly moved before both blocking-rule exit paths and covered by ordered end-to-end tests. The existing unwrap-related comment concerns pre-existing code moved into a helper, not a regression or high-priority defect.
No critical or high-priority changes were found.
corgea-security
left a comment
There was a problem hiding this comment.
Approved by Dennis: high policy risk and automated risk 2/5.


The report and the SBOM now run immediately after the scan results are collected, ahead of every gate. Rather than shuffle the bodies in place, they moved into
write_scan_reportandwrite_sbom. That drops the diff's noise and buys two things: the four near-identical--out-formatbranches collapse into one server-rendered path plus the JSON case, and the spinner thread is now stopped on the error exits that previously left it spinning into the process teardown.Resulting order, with the three gates unchanged behind it:
Nothing can exit between the results and the report, so there is no path left where an artifact the caller asked for is skipped by a non-zero exit.
Completeness
I went looking for the same shape elsewhere rather than assuming this was the only instance:
corgea deps scan— already correct:emit_outputwrites the out-file before--fail-oncanreturn Ok(1).corgea wait/corgea upload --wait— no output flags to drop.corgea scan semgrep|snyk—--out-file/--out-formatare rejected for non-blast scanners.exit(1)lives inside the gate, which is now downstream of the report.--fail-on— always ran after the report; unchanged.One behavior note worth a reviewer's eye: if the report request itself fails, the scan still exits 1, but it now reports the report failure instead of the blocking-rule reason, since the fetch happens first. The gate outcome (exit 1) is the same either way, and a failed report fetch was already a hard error.
Testing
Three e2e tests, all using ordered stub plans so the sequence itself is pinned — reordering the report back after the gate puts the request out of order and fails the plan:
--block-ontrips: exits 1, prints the blocking-rule reason, and bothresults.sarifandbom.jsonexist.--fail(deprecated) trips: same guarantee, and asserts the check is not narrowed byblock_on.--block-onpasses: still writes the report and exits 0, so the fix didn't regress the path that already worked../harness cipasses in full locally: strict clippy, format check, dep audit, 685 tests, coverage gate.Version bumped to 1.10.1 — a dropped report file is a bug fix, not a new flag.
Not in this PR
The other half of his thread, a
blocking_verdictoncorgea list --jsonso a duplicate-skip path can read a past scan's verdict, is separate and deferred.