Skip to content
Open
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
51 changes: 39 additions & 12 deletions src/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ use crate::utils;
use serde_json::json;
use std::path::Path;

#[allow(clippy::too_many_arguments)]
pub fn run(
config: &Config,
issues: &bool,
sca_issues: &bool,
code_quality: &bool,
json: &bool,
page: &Option<u16>,
page_size: &Option<u16>,
scan_id: &Option<String>,
project_name: &Option<String>,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

--project-name is ignored in SCA mode, so corgea list --sca-issues --project-name foo does not filter by foo. Handling this combination explicitly would prevent the flag from implying filtering that does not occur.

) {
let project_name =
utils::generic::get_current_working_directory().unwrap_or("unknown".to_string());
// 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(
Expand Down Expand Up @@ -108,14 +116,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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

--code-quality inherits security blocking-rule side effects (including hard failure).

Folding CQ into the *issues branch means corgea list --code-quality --scan-id ... still runs check_blocking_rules (lines 162-189) and exits 1 on any failure, even after get_quality_issues already succeeded.

Impact: A blocking-rules API blip (or security-only block: true) makes CQ listing unusable, or adds empty Blocking columns driven by non-CQ findings.

Fix: Gate enrichment on security listing only, e.g. if scan_id.is_some() && !*code_quality { ... }. Or, if CQ rows can legitimately be blocked, soft-fail the enrichment (log + continue) and set render_blocking_rules only when at least one returned CQ issue id is in the blocking map. Add a regression test for --code-quality --scan-id that stubs blocking-rules failure and asserts the CQ table/JSON still prints.

let fetch_result = if *code_quality {
utils::api::get_quality_issues(
&config.get_url(),
&project_name,
Comment thread
Ibrahimrahhal marked this conversation as resolved.
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));
Expand All @@ -127,7 +151,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 {}",
Expand All @@ -141,7 +165,10 @@ pub fn run(
let mut blocking_rules: std::collections::HashMap<String, String> =
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(
Expand Down
31 changes: 28 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

Expand All @@ -159,6 +167,12 @@ enum Commands {

#[arg(long, value_parser = clap::value_parser!(u16), help = "Number of items per page")]
page_size: Option<u16>,

#[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<String>,
},
/// Inspect something, by default it will inspect a scan
Inspect {
Expand Down Expand Up @@ -612,24 +626,35 @@ fn main() {
page_size,
scan_id,
sca_issues,
code_quality,
project_name,
}) => {
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);
}
list::run(
&corgea_config,
issues,
sca_issues,
code_quality,
json,
page,
page_size,
scan_id,
project_name,
);
}
Some(Commands::Inspect {
Expand Down
98 changes: 98 additions & 0 deletions src/utils/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,62 @@ pub fn get_scan_issues(
}
}

pub fn get_quality_issues(
url: &str,
project: &str,
page: Option<u16>,
page_size: Option<u16>,
scan_id: Option<String>,
) -> Result<ProjectIssuesResponse, Box<dyn std::error::Error>> {
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()?;
Comment on lines +546 to +553

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

New client ignores HTTP status; non-JSON errors become opaque parse/auth failures.

get_quality_issues always reads the body and serde_json::from_strs with no status().is_success() check (unlike get_sca_issues in this same file, which maps 404/non-2xx explicitly).

Impact: During companion rollout (or any 401/403/404/500 HTML/JSON error body), users get Failed to parse response then the generic check-token/connection path in list.rs, and the 404 string match for scan/project-does-not-exist never fires. A wrong path (/issues/quality vs /issues/code-quality) is easy to miss.

Fix: Mirror get_sca_issues: inspect status before parse; map 404/401/5xx to distinct errors that list.rs can surface. Prefer client.get(&endpoint).query(...) so project names are encoded and page_size cannot be appended as &page_size= without a ? when page is None.

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<ScanResponse, Box<dyn std::error::Error>> {
let url = format!("{}{}/scan/{}", url, API_BASE, scan_id);

Expand Down Expand Up @@ -1136,6 +1192,48 @@ 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
// 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",
"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");
Comment on lines +1196 to +1227

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Test passes without exercising the new behavior (endpoint contract / CLI wiring).

deserializes_code_quality_issue_response only round-trips a hand-written JSON fixture into ProjectIssuesResponse. It would still pass if:

  • scan path were /scan/{id}/issues/code-quality (or project path /issues/quality) — the PR uses asymmetric /scan/.../issues/quality vs /issues/code-quality with zero assertion
  • --issues / --sca-issues / --code-quality mutual exclusion regressed
  • --code-quality --scan-id wrongly called get_scan_issues

Impact: False confidence for a net-new production API surface.

Fix: Add a unit/integration test that builds the request URL (or uses a mock HTTP server) and asserts both endpoint variants + query params; add a clap/CLI test that --issues --code-quality exits 1 and that --code-quality selects get_quality_issues.

assert_eq!(parsed.status, "ok");
let issues = parsed.issues.expect("issues present");
assert_eq!(issues.len(), 1);
let issue = &issues[0];
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();
Expand Down
Loading