agentshield/output/
sarif.rs1use crate::error::Result;
2use crate::rules::{Finding, Severity};
3
4use serde_json::{json, Value};
5
6pub fn render(findings: &[Finding], target_name: &str) -> Result<String> {
11 let rules: Vec<Value> = findings
12 .iter()
13 .map(|f| &f.rule_id)
14 .collect::<std::collections::BTreeSet<_>>()
15 .into_iter()
16 .map(|rule_id| {
17 let finding = findings.iter().find(|f| &f.rule_id == rule_id).unwrap();
18 let mut rule = json!({
19 "id": finding.rule_id,
20 "name": finding.rule_name,
21 "shortDescription": { "text": finding.rule_name },
22 "defaultConfiguration": {
23 "level": severity_to_sarif_level(finding.severity),
24 },
25 });
26 if let Some(cwe) = &finding.cwe_id {
27 rule["properties"] = json!({
28 "tags": [cwe],
29 });
30 }
31 rule
32 })
33 .collect();
34
35 let results: Vec<Value> = findings
36 .iter()
37 .map(|f| {
38 let mut result = json!({
39 "ruleId": f.rule_id,
40 "level": severity_to_sarif_level(f.severity),
41 "message": { "text": f.message },
42 });
43
44 if let Some(loc) = &f.location {
45 result["locations"] = json!([{
46 "physicalLocation": {
47 "artifactLocation": {
48 "uri": loc.file.display().to_string(),
49 },
50 "region": {
51 "startLine": loc.line,
52 "startColumn": loc.column,
53 },
54 },
55 }]);
56 }
57
58 if let Some(remediation) = &f.remediation {
59 result["fixes"] = json!([{
60 "description": { "text": remediation },
61 }]);
62 }
63
64 result
65 })
66 .collect();
67
68 let sarif = json!({
69 "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
70 "version": "2.1.0",
71 "runs": [{
72 "tool": {
73 "driver": {
74 "name": "AgentShield",
75 "informationUri": "https://github.com/limaronaldo/agentshield",
76 "version": env!("CARGO_PKG_VERSION"),
77 "semanticVersion": env!("CARGO_PKG_VERSION"),
78 "rules": rules,
79 },
80 },
81 "results": results,
82 "automationDetails": {
83 "id": format!("agentshield/{}", target_name),
84 },
85 }],
86 });
87
88 let output = serde_json::to_string_pretty(&sarif)?;
89 Ok(output)
90}
91
92fn severity_to_sarif_level(severity: Severity) -> &'static str {
93 match severity {
94 Severity::Critical | Severity::High => "error",
95 Severity::Medium => "warning",
96 Severity::Low | Severity::Info => "note",
97 }
98}