Skip to main content

agentshield/output/
sarif.rs

1use crate::error::Result;
2use crate::rules::{Finding, Severity};
3
4use serde_json::{json, Value};
5
6/// Render findings as SARIF 2.1.0.
7///
8/// Produces a self-contained SARIF log compatible with GitHub Code Scanning
9/// and other SARIF consumers.
10pub 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}