syncable_cli/analyzer/kubelint/formatter/
sarif.rs

1//! SARIF (Static Analysis Results Interchange Format) formatter.
2//!
3//! SARIF is a standard format for static analysis tool output,
4//! supported by GitHub, VS Code, and other tools.
5
6use crate::analyzer::kubelint::lint::LintResult;
7use serde::Serialize;
8
9/// Format a lint result as SARIF.
10pub fn format(result: &LintResult) -> String {
11    let output = SarifOutput::from(result);
12    serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string())
13}
14
15#[derive(Serialize)]
16struct SarifOutput {
17    #[serde(rename = "$schema")]
18    schema: String,
19    version: String,
20    runs: Vec<SarifRun>,
21}
22
23#[derive(Serialize)]
24struct SarifRun {
25    tool: SarifTool,
26    results: Vec<SarifResult>,
27}
28
29#[derive(Serialize)]
30struct SarifTool {
31    driver: SarifDriver,
32}
33
34#[derive(Serialize)]
35struct SarifDriver {
36    name: String,
37    version: String,
38    #[serde(rename = "informationUri")]
39    information_uri: String,
40    rules: Vec<SarifRule>,
41}
42
43#[derive(Serialize)]
44struct SarifRule {
45    id: String,
46    name: String,
47    #[serde(rename = "shortDescription")]
48    short_description: SarifMessage,
49    #[serde(rename = "defaultConfiguration")]
50    default_configuration: SarifConfiguration,
51}
52
53#[derive(Serialize)]
54struct SarifConfiguration {
55    level: String,
56}
57
58#[derive(Serialize)]
59struct SarifResult {
60    #[serde(rename = "ruleId")]
61    rule_id: String,
62    level: String,
63    message: SarifMessage,
64    locations: Vec<SarifLocation>,
65}
66
67#[derive(Serialize)]
68struct SarifMessage {
69    text: String,
70}
71
72#[derive(Serialize)]
73struct SarifLocation {
74    #[serde(rename = "physicalLocation")]
75    physical_location: SarifPhysicalLocation,
76}
77
78#[derive(Serialize)]
79struct SarifPhysicalLocation {
80    #[serde(rename = "artifactLocation")]
81    artifact_location: SarifArtifactLocation,
82    region: Option<SarifRegion>,
83}
84
85#[derive(Serialize)]
86struct SarifArtifactLocation {
87    uri: String,
88}
89
90#[derive(Serialize)]
91struct SarifRegion {
92    #[serde(rename = "startLine")]
93    start_line: u32,
94}
95
96impl From<&LintResult> for SarifOutput {
97    fn from(result: &LintResult) -> Self {
98        // Collect unique rules
99        let mut rules: Vec<SarifRule> = Vec::new();
100        let mut seen_rules = std::collections::HashSet::new();
101
102        for failure in &result.failures {
103            let rule_id = failure.code.to_string();
104            if !seen_rules.contains(&rule_id) {
105                seen_rules.insert(rule_id.clone());
106                rules.push(SarifRule {
107                    id: rule_id.clone(),
108                    name: rule_id.clone(),
109                    short_description: SarifMessage {
110                        text: failure.message.clone(),
111                    },
112                    default_configuration: SarifConfiguration {
113                        level: severity_to_sarif_level(failure.severity),
114                    },
115                });
116            }
117        }
118
119        let results: Vec<SarifResult> = result
120            .failures
121            .iter()
122            .map(|f| SarifResult {
123                rule_id: f.code.to_string(),
124                level: severity_to_sarif_level(f.severity),
125                message: SarifMessage {
126                    text: format!(
127                        "{} ({}/{}): {}",
128                        f.code, f.object_kind, f.object_name, f.message
129                    ),
130                },
131                locations: vec![SarifLocation {
132                    physical_location: SarifPhysicalLocation {
133                        artifact_location: SarifArtifactLocation {
134                            uri: f.file_path.display().to_string(),
135                        },
136                        region: f.line.map(|l| SarifRegion { start_line: l }),
137                    },
138                }],
139            })
140            .collect();
141
142        Self {
143            schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
144            version: "2.1.0".to_string(),
145            runs: vec![SarifRun {
146                tool: SarifTool {
147                    driver: SarifDriver {
148                        name: "kubelint-rs".to_string(),
149                        version: env!("CARGO_PKG_VERSION").to_string(),
150                        information_uri: "https://github.com/stackrox/kube-linter".to_string(),
151                        rules,
152                    },
153                },
154                results,
155            }],
156        }
157    }
158}
159
160fn severity_to_sarif_level(severity: crate::analyzer::kubelint::types::Severity) -> String {
161    match severity {
162        crate::analyzer::kubelint::types::Severity::Error => "error".to_string(),
163        crate::analyzer::kubelint::types::Severity::Warning => "warning".to_string(),
164        crate::analyzer::kubelint::types::Severity::Info => "note".to_string(),
165    }
166}