From 05e2c1891e52a128637f14f78cb5f7724726a7fe Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 09:17:35 +0000 Subject: [PATCH 1/5] Add code quality issue listing to CLI Add 'corgea list --code-quality' (alias --quality) to list code quality findings, mirroring --issues but hitting the code quality endpoints (/scan//issues/quality and /issues/code-quality). - Add get_quality_issues() to the API client. - Deserialize the new 'type' discriminator on issues (optional). - Make --issues, --sca-issues, and --code-quality mutually exclusive. - Add a deserialization test for code quality issue responses. Co-authored-by: ibrahim --- src/list.rs | 36 ++++++++++++---- src/main.rs | 23 +++++++++-- src/utils/api.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 12 deletions(-) diff --git a/src/list.rs b/src/list.rs index e50d476..db726f2 100644 --- a/src/list.rs +++ b/src/list.rs @@ -8,6 +8,7 @@ pub fn run( config: &Config, issues: &bool, sca_issues: &bool, + code_quality: &bool, json: &bool, page: &Option, page_size: &Option, @@ -108,14 +109,30 @@ pub fn run( Some(sca_issues_response.page), Some(sca_issues_response.total_pages), ); - } else if *issues { - let issues_response = match utils::api::get_scan_issues( - &config.get_url(), - &project_name, - Some((*page).unwrap_or(1)), - *page_size, - scan_id.clone(), - ) { + } else if *issues || *code_quality { + let fetch_result = if *code_quality { + utils::api::get_quality_issues( + &config.get_url(), + &project_name, + Some((*page).unwrap_or(1)), + *page_size, + scan_id.clone(), + ) + } else { + utils::api::get_scan_issues( + &config.get_url(), + &project_name, + Some((*page).unwrap_or(1)), + *page_size, + scan_id.clone(), + ) + }; + let issue_kind = if *code_quality { + "code quality issues" + } else { + "scan issues" + }; + let issues_response = match fetch_result { Ok(response) => response, Err(e) => { debug(&format!("Error Sending Request: {}", e)); @@ -127,7 +144,7 @@ pub fn run( } } else { log::error!( - "Unable to fetch scan issues. Please check your connection and ensure that:\n\ + "Unable to fetch {issue_kind}. Please check your connection and ensure that:\n\ - The server URL is reachable.\n\ - Your authentication token is valid.\n\n\ Check out our docs at https://docs.corgea.app/install_cli#login-with-the-cli {}", @@ -185,6 +202,7 @@ pub fn run( .map(|issue| { serde_json::json!(utils::api::IssueWithBlockingRules { id: issue.id.clone(), + issue_type: issue.issue_type.clone(), scan_id: issue.scan_id.clone(), status: issue.status.clone(), urgency: issue.urgency.clone(), diff --git a/src/main.rs b/src/main.rs index 5023c58..b237c34 100644 --- a/src/main.rs +++ b/src/main.rs @@ -148,6 +148,14 @@ enum Commands { )] sca_issues: bool, + #[arg( + long, + short = 'q', + visible_alias = "quality", + help = "List code quality issues instead of scans" + )] + code_quality: bool, + #[arg(short, long, help = "Specify the scan id to list issues for.")] scan_id: Option, @@ -612,13 +620,21 @@ fn main() { page_size, scan_id, sca_issues, + code_quality, }) => { verify_token_and_exit_when_fail(&corgea_config); - if *issues && *sca_issues { - ::log::error!("Cannot use both --issues and --sca-issues at the same time."); + if [*issues, *sca_issues, *code_quality] + .iter() + .filter(|flag| **flag) + .count() + > 1 + { + ::log::error!( + "Cannot use more than one of --issues, --sca-issues, and --code-quality at the same time." + ); std::process::exit(1); } - if scan_id.is_some() && !*issues && !*sca_issues { + if scan_id.is_some() && !*issues && !*sca_issues && !*code_quality { println!("scan_id option is only supported for issues list command."); std::process::exit(1); } @@ -626,6 +642,7 @@ fn main() { &corgea_config, issues, sca_issues, + code_quality, json, page, page_size, diff --git a/src/utils/api.rs b/src/utils/api.rs index 47a68bc..dc08805 100644 --- a/src/utils/api.rs +++ b/src/utils/api.rs @@ -513,6 +513,62 @@ pub fn get_scan_issues( } } +pub fn get_quality_issues( + url: &str, + project: &str, + page: Option, + page_size: Option, + scan_id: Option, +) -> Result> { + let mut seperator = "?"; + let mut url = match scan_id { + Some(scan_id) => format!("{}{}/scan/{}/issues/quality", url, API_BASE, scan_id), + None => { + seperator = "&"; + format!( + "{}{}/issues/code-quality?project={}", + url, API_BASE, project + ) + } + }; + if let Some(p) = page { + url.push_str(&format!("{}page={}", seperator, p)); + } + if let Some(p_size) = page_size { + url.push_str(&format!("&page_size={}", p_size)); + } else { + url.push_str("&page_size=30"); + } + let client = http_client(); + + debug(&format!("Sending request to URL: {}", url)); + + let response = match client.get(&url).send() { + Ok(res) => { + check_for_warnings(res.headers(), res.status()); + res + } + Err(e) => return Err(format!("Failed to send request: {}", e).into()), + }; + let response_text = response.text()?; + let project_issues_response: ProjectIssuesResponse = serde_json::from_str(&response_text) + .map_err(|e| { + debug(&format!( + "Failed to parse response: {}. Response body: {}", + e, response_text + )); + format!("Failed to parse response: {}", e) + })?; + + if project_issues_response.status == "ok" { + Ok(project_issues_response) + } else if project_issues_response.status == "no_project_found" { + Err("Project not found 404".into()) + } else { + Err("Server error 500".into()) + } +} + pub fn get_scan(url: &str, scan_id: &str) -> Result> { let url = format!("{}{}/scan/{}", url, API_BASE, scan_id); @@ -968,6 +1024,8 @@ pub struct FullIssueResponse { #[derive(Serialize, Deserialize, Debug)] pub struct Issue { pub id: String, + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + pub issue_type: Option, pub scan_id: Option, pub status: String, pub urgency: String, @@ -982,6 +1040,8 @@ pub struct Issue { #[derive(Serialize, Deserialize, Debug)] pub struct IssueWithBlockingRules { pub id: String, + #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] + pub issue_type: Option, pub scan_id: Option, pub status: String, pub urgency: String, @@ -1136,6 +1196,51 @@ mod tests { assert!(headers.get("CORGEA-SOURCE").is_some()); } + #[test] + fn deserializes_code_quality_issue_response() { + // Code quality issues carry a free-form classification label (no CWE) and + // a `type` discriminator, and must deserialize into the same Issue struct + // used for security issues. + let body = r#"{ + "status": "ok", + "page": 1, + "total_pages": 1, + "total_issues": 1, + "issues": [ + { + "id": "11111111-1111-1111-1111-111111111111", + "type": "code_quality", + "urgency": "ME", + "created_at": "2026-01-01T00:00:00Z", + "status": "open", + "classification": { + "id": "Maintainability", + "name": "Maintainability", + "description": null + }, + "location": { + "file": {"name": "app.py", "language": "python", "path": "app/app.py"}, + "project": {"name": "proj", "branch": "main", "git_sha": "abc"}, + "line_number": 20 + }, + "auto_triage": {"false_positive_detection": {"status": "valid"}}, + "auto_fix_suggestion": {"status": "no_fix"} + } + ] + }"#; + + let parsed: ProjectIssuesResponse = + serde_json::from_str(body).expect("should parse code quality response"); + assert_eq!(parsed.status, "ok"); + let issues = parsed.issues.expect("issues present"); + assert_eq!(issues.len(), 1); + let issue = &issues[0]; + assert_eq!(issue.issue_type.as_deref(), Some("code_quality")); + assert_eq!(issue.classification.id, "Maintainability"); + assert_eq!(issue.classification.name, "Maintainability"); + assert!(issue.classification.description.is_none()); + } + #[test] fn should_warn_deprecated_false_when_no_warning_header() { let headers = HeaderMap::new(); From ecf51c0f0ab84b777ea0900c05ff203c4a38719b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 09:25:25 +0000 Subject: [PATCH 2/5] Drop redundant issue type field from CLI Code quality and security issues come from separate endpoints, so the CLI does not need a 'type' discriminator on the Issue struct. Keeps the CLI in sync with the API response, which no longer emits the field. Co-authored-by: ibrahim --- src/list.rs | 1 - src/utils/api.rs | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/list.rs b/src/list.rs index db726f2..0c77801 100644 --- a/src/list.rs +++ b/src/list.rs @@ -202,7 +202,6 @@ pub fn run( .map(|issue| { serde_json::json!(utils::api::IssueWithBlockingRules { id: issue.id.clone(), - issue_type: issue.issue_type.clone(), scan_id: issue.scan_id.clone(), status: issue.status.clone(), urgency: issue.urgency.clone(), diff --git a/src/utils/api.rs b/src/utils/api.rs index dc08805..8196448 100644 --- a/src/utils/api.rs +++ b/src/utils/api.rs @@ -1024,8 +1024,6 @@ pub struct FullIssueResponse { #[derive(Serialize, Deserialize, Debug)] pub struct Issue { pub id: String, - #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] - pub issue_type: Option, pub scan_id: Option, pub status: String, pub urgency: String, @@ -1040,8 +1038,6 @@ pub struct Issue { #[derive(Serialize, Deserialize, Debug)] pub struct IssueWithBlockingRules { pub id: String, - #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")] - pub issue_type: Option, pub scan_id: Option, pub status: String, pub urgency: String, @@ -1199,8 +1195,7 @@ mod tests { #[test] fn deserializes_code_quality_issue_response() { // Code quality issues carry a free-form classification label (no CWE) and - // a `type` discriminator, and must deserialize into the same Issue struct - // used for security issues. + // must deserialize into the same Issue struct used for security issues. let body = r#"{ "status": "ok", "page": 1, @@ -1209,7 +1204,6 @@ mod tests { "issues": [ { "id": "11111111-1111-1111-1111-111111111111", - "type": "code_quality", "urgency": "ME", "created_at": "2026-01-01T00:00:00Z", "status": "open", @@ -1235,7 +1229,6 @@ mod tests { let issues = parsed.issues.expect("issues present"); assert_eq!(issues.len(), 1); let issue = &issues[0]; - assert_eq!(issue.issue_type.as_deref(), Some("code_quality")); assert_eq!(issue.classification.id, "Maintainability"); assert_eq!(issue.classification.name, "Maintainability"); assert!(issue.classification.description.is_none()); From c72c3d6a09b5902531615e18333a1baebc828ef8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 09:41:32 +0000 Subject: [PATCH 3/5] Fix clippy too_many_arguments on list::run CI runs clippy with -D warnings; adding the --code-quality flag pushed list::run to 8 arguments (limit 7). Allow the lint here, matching the existing pattern used in blast.rs and deps/ecosystems/evaluate.rs. Co-authored-by: ibrahim --- src/list.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/list.rs b/src/list.rs index 0c77801..77c007f 100644 --- a/src/list.rs +++ b/src/list.rs @@ -4,6 +4,7 @@ use crate::utils; use serde_json::json; use std::path::Path; +#[allow(clippy::too_many_arguments)] pub fn run( config: &Config, issues: &bool, From e368bd4153435fc2dd0b9079ccc8b8015c8efa7e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 13:20:37 +0000 Subject: [PATCH 4/5] Resolve list project name via determine_project_name corgea list used the current directory basename as the project key, while corgea scan stores projects under determine_project_name (which prefers the Git remote repository name). This caused 'Project not found' when the checkout directory differed from the repo name, including Git worktrees. Use the same determine_project_name helper for list so the lookup key matches what scans are stored under. Applies to --issues, --sca-issues, --code-quality, and the default scan listing. Co-authored-by: ibrahim --- src/list.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/list.rs b/src/list.rs index 77c007f..9b121cf 100644 --- a/src/list.rs +++ b/src/list.rs @@ -15,8 +15,12 @@ pub fn run( page_size: &Option, scan_id: &Option, ) { - let project_name = - utils::generic::get_current_working_directory().unwrap_or("unknown".to_string()); + // Resolve the project name the same way `corgea scan` does (prefer the Git + // remote repository name, then fall back to the directory name) so lookups + // match the project key scans are stored under. Using the bare directory + // basename here caused "Project not found" whenever the checkout directory + // differed from the repository name (e.g. Git worktrees). + let project_name = utils::generic::determine_project_name(None); println!(); if *sca_issues { let sca_issues_response = match utils::api::get_sca_issues( From 6b56917357ad82a036319902a506429154c31794 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 13:31:13 +0000 Subject: [PATCH 5/5] Add --project-name to list and stop CQ inheriting blocking rules - Add a --project-name flag to 'corgea list', matching 'corgea scan', and thread it through determine_project_name so users can override the resolved project key explicitly. - Gate blocking-rules enrichment to security listings only. Previously 'corgea list --code-quality --scan-id' ran check_blocking_rules and hard-exited on any failure (even after the CQ fetch succeeded), and could render Blocking columns driven by non-CQ findings. Co-authored-by: ibrahim --- src/list.rs | 19 ++++++++++++------- src/main.rs | 8 ++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/list.rs b/src/list.rs index 9b121cf..e10d1a7 100644 --- a/src/list.rs +++ b/src/list.rs @@ -14,13 +14,15 @@ pub fn run( page: &Option, page_size: &Option, scan_id: &Option, + project_name: &Option, ) { - // Resolve the project name the same way `corgea scan` does (prefer the Git - // remote repository name, then fall back to the directory name) so lookups - // match the project key scans are stored under. Using the bare directory - // basename here caused "Project not found" whenever the checkout directory - // differed from the repository name (e.g. Git worktrees). - let project_name = utils::generic::determine_project_name(None); + // Resolve the project name the same way `corgea scan` does: honor an explicit + // --project-name, otherwise prefer the Git remote repository name and fall + // back to the directory name. This matches the project key scans are stored + // under; using the bare directory basename caused "Project not found" + // whenever the checkout directory differed from the repository name (e.g. + // Git worktrees). + let project_name = utils::generic::determine_project_name(project_name.as_deref()); println!(); if *sca_issues { let sca_issues_response = match utils::api::get_sca_issues( @@ -163,7 +165,10 @@ pub fn run( let mut blocking_rules: std::collections::HashMap = std::collections::HashMap::new(); - if scan_id.is_some() { + // Blocking rules are a security-listing concern. Skip the enrichment for + // code quality so a blocking-rules API failure can't take down the CQ + // listing and so Blocking columns aren't driven by non-CQ findings. + if scan_id.is_some() && !*code_quality { let mut page: u32 = 1; loop { match utils::api::check_blocking_rules( diff --git a/src/main.rs b/src/main.rs index b237c34..e94a53f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -167,6 +167,12 @@ enum Commands { #[arg(long, value_parser = clap::value_parser!(u16), help = "Number of items per page")] page_size: Option, + + #[arg( + long, + help = "The name of the Corgea project. Defaults to git repository name if found, otherwise to the current directory name." + )] + project_name: Option, }, /// Inspect something, by default it will inspect a scan Inspect { @@ -621,6 +627,7 @@ fn main() { scan_id, sca_issues, code_quality, + project_name, }) => { verify_token_and_exit_when_fail(&corgea_config); if [*issues, *sca_issues, *code_quality] @@ -647,6 +654,7 @@ fn main() { page, page_size, scan_id, + project_name, ); } Some(Commands::Inspect {