Repository navigation
Show scanner metadata in corgea inspect --issue - #183
Conversation
Request include_metadata=true from the issue API and keep both the issue's scanner identifiers (metadata) and the fix analysis copy (scanner_metadata) on the Issue model, so --json passes them through and the summary prints a Scanner Metadata section. Co-authored-by: ibrahim <ibrahim@corgea.com>
corgea-security
left a comment
There was a problem hiding this comment.
Automated review risk: 1/5.
The change consistently requests, deserializes, merges, displays, and tests scanner metadata. No actionable defects are evident in the supplied diff.
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 1/5.
There was a problem hiding this comment.
scanner_metadata is deserialized onto the shared Issue struct and then serialized again by corgea list --json and corgea scan --out-format json. That changes those outputs for every issue the API attaches scanner metadata to, and the blocking-rules list path drops the field. inspect --issue itself is fine.
Sent by Cursor Automation: pr-flow
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub scanner_metadata: Option<serde_json::Map<String, Value>>, | ||
| /// Identifiers the originating scanner reported for the finding (e.g. | ||
| /// Fortify `instance_id`, `class_id`, `kingdom`). Sent only when the issue | ||
| /// is fetched with `include_metadata=true`. | ||
| #[serde(default, skip_serializing_if = "Option::is_none")] | ||
| pub metadata: Option<serde_json::Map<String, Value>>, |
There was a problem hiding this comment.
skip_serializing_if = "Option::is_none" only drops these maps when they are missing. The list schema (Issue, used by GET /issues, GET /scan/{id}/issues, and GET /issues/code-quality) includes scanner_metadata. corgea list --json then re-serializes that struct:
let mut json = serde_json::json!({
"page": issues_response.page,
"total_pages": issues_response.total_pages,
"results": &issues_response.issues
});(src/list.rs around line 228). corgea scan --out-format json does the same with serde_json::to_string_pretty(&issues) in src/scanners/blast.rs around line 773. A Fortify or Checkmarx issue that has scanner metadata therefore grows a scanner_metadata object in both outputs. The new tests stub GET /issue/{id} only, so they stay green.
When the scan has blocking rules, list rebuilds each row as IssueWithBlockingRules (src/list.rs around line 239), which has no such field. The same issue then includes scanner_metadata on a non-blocking list and loses it on a blocking one.
Keep the maps for inspect display, but do not let Issue's serialize impl emit them. Apply this together with the inspect --json change below; the attribute change alone fails inspect_issue_json_includes_scanner_metadata.
| #[serde(default, skip_serializing_if = "Option::is_none")] | |
| pub scanner_metadata: Option<serde_json::Map<String, Value>>, | |
| /// Identifiers the originating scanner reported for the finding (e.g. | |
| /// Fortify `instance_id`, `class_id`, `kingdom`). Sent only when the issue | |
| /// is fetched with `include_metadata=true`. | |
| #[serde(default, skip_serializing_if = "Option::is_none")] | |
| pub metadata: Option<serde_json::Map<String, Value>>, | |
| /// Scanner identifiers recorded by the fix analysis. Only the single-issue | |
| /// endpoint sends it. | |
| #[serde(default, skip_serializing)] | |
| pub scanner_metadata: Option<serde_json::Map<String, Value>>, | |
| /// Identifiers the originating scanner reported for the finding (e.g. | |
| /// Fortify `instance_id`, `class_id`, `kingdom`). Sent only when the issue | |
| /// is fetched with `include_metadata=true`. | |
| #[serde(default, skip_serializing)] | |
| pub metadata: Option<serde_json::Map<String, Value>>, |
In src/inspect.rs, copy the maps back onto the printed object (this is the only user-facing JSON that should contain them):
if *json {
let mut body = serde_json::to_value(&issue_details).unwrap();
if let Some(issue) = body
.get_mut("issue")
.and_then(|value| value.as_object_mut())
{
if let Some(scanner_metadata) = &issue_details.issue.scanner_metadata {
issue.insert(
"scanner_metadata".to_string(),
serde_json::Value::Object(scanner_metadata.clone()),
);
}
if let Some(metadata) = &issue_details.issue.metadata {
issue.insert(
"metadata".to_string(),
serde_json::Value::Object(metadata.clone()),
);
}
}
println!("{}", serde_json::to_string_pretty(&body).unwrap());
return;
}Issue is re-serialized by list --json and scan --out-format json. skip_serializing_if still wrote scanner_metadata when the list API sent it, and the blocking-rules path dropped the field again. inspect --json copies the maps back onto its own payload. Co-authored-by: Juan Gaitán <juan@corgea.com>


Summary
The issue API (
GET /api/v1/issue/<id>) returns scanner identifiers such as Fortify'sinstance_id,class_id,kingdom,category,analyzerandbuild_id, but the CLI'sIssuemodel didn't declare those fields. Serde dropped them, so they were missing even from--jsonoutput.Changes
get_issuenow sendsinclude_metadata=true, so doghouse also returnsissue.metadata, the identifiers stored on the issue when it was ingested.scanner_metadata, the fix-analysis copy, is always returned. Backends that don't recognize the parameter ignore it.Issuegains two optional fields,scanner_metadataandmetadata, socorgea inspect --issue --jsonpasses both through. Both useskip_serializing_if, socorgea list --jsonoutput doesn't change, because the list endpoints don't send them.inspect --issuesummary (both the default view and--summary) prints a "Scanner Metadata" section that merges the two sources. When a key appears in both, the issue's own value wins. Null and empty values are skipped.skills/corgea/SKILL.mddocuments the new output.Example:
Testing
./harness checkpasses: strict clippy, format and all 944 tests.tests/cloud_commands_e2e/inspect.rscheck that the request sendsinclude_metadata=true, that--jsonincludes both metadata objects, and that--summaryprints the merged section with the expected precedence and skips nulls.