use std::path::Path;
use crate::error::Result;
use crate::rules::{Finding, Severity};
use serde_json::{json, Value};
pub fn render(findings: &[Finding], target_name: &str, scan_root: &Path) -> Result<String> {
let rules: Vec<Value> = findings
.iter()
.map(|f| &f.rule_id)
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.map(|rule_id| {
let finding = findings.iter().find(|f| &f.rule_id == rule_id).unwrap();
let mut rule = json!({
"id": finding.rule_id,
"name": finding.rule_name,
"shortDescription": { "text": finding.rule_name },
"defaultConfiguration": {
"level": severity_to_sarif_level(finding.severity),
},
});
if let Some(cwe) = &finding.cwe_id {
rule["properties"] = json!({
"tags": [cwe],
});
}
rule
})
.collect();
let results: Vec<Value> = findings
.iter()
.filter_map(|f| {
let loc = f.location.as_ref()?;
let mut result = json!({
"ruleId": f.rule_id,
"level": severity_to_sarif_level(f.severity),
"message": { "text": f.message },
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": loc.file.display().to_string(),
},
"region": {
"startLine": loc.line,
"startColumn": loc.column.max(1),
},
},
}],
});
let fingerprint = f.fingerprint(scan_root);
result["properties"] = match &f.remediation {
Some(remediation) => json!({
"fingerprint": fingerprint,
"remediation": remediation,
}),
None => json!({ "fingerprint": fingerprint }),
};
Some(result)
})
.collect();
let sarif = json!({
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "AgentShield",
"informationUri": "https://github.com/limaronaldo/agentshield",
"version": env!("CARGO_PKG_VERSION"),
"semanticVersion": env!("CARGO_PKG_VERSION"),
"rules": rules,
},
},
"results": results,
"automationDetails": {
"id": format!("agentshield/{}", target_name),
},
}],
});
let output = serde_json::to_string_pretty(&sarif)?;
Ok(output)
}
fn severity_to_sarif_level(severity: Severity) -> &'static str {
match severity {
Severity::Critical | Severity::High => "error",
Severity::Medium => "warning",
Severity::Low | Severity::Info => "note",
}
}