syncable_cli/analyzer/hadolint/formatter/
sarif.rs1use crate::analyzer::hadolint::formatter::Formatter;
9use crate::analyzer::hadolint::lint::LintResult;
10use crate::analyzer::hadolint::types::Severity;
11use serde::Serialize;
12use std::io::Write;
13
14#[derive(Debug, Clone, Default)]
16pub struct SarifFormatter;
17
18impl SarifFormatter {
19 pub fn new() -> Self {
21 Self
22 }
23}
24
25#[derive(Debug, Serialize)]
27#[serde(rename_all = "camelCase")]
28struct SarifReport {
29 #[serde(rename = "$schema")]
30 schema: &'static str,
31 version: &'static str,
32 runs: Vec<SarifRun>,
33}
34
35#[derive(Debug, Serialize)]
36#[serde(rename_all = "camelCase")]
37struct SarifRun {
38 tool: SarifTool,
39 results: Vec<SarifResult>,
40}
41
42#[derive(Debug, Serialize)]
43#[serde(rename_all = "camelCase")]
44struct SarifTool {
45 driver: SarifDriver,
46}
47
48#[derive(Debug, Serialize)]
49#[serde(rename_all = "camelCase")]
50struct SarifDriver {
51 name: &'static str,
52 information_uri: &'static str,
53 version: &'static str,
54 rules: Vec<SarifRule>,
55}
56
57#[derive(Debug, Serialize)]
58#[serde(rename_all = "camelCase")]
59struct SarifRule {
60 id: String,
61 name: String,
62 short_description: SarifMessage,
63 #[serde(skip_serializing_if = "Option::is_none")]
64 help_uri: Option<String>,
65 default_configuration: SarifRuleConfiguration,
66}
67
68#[derive(Debug, Serialize)]
69#[serde(rename_all = "camelCase")]
70struct SarifRuleConfiguration {
71 level: &'static str,
72}
73
74#[derive(Debug, Serialize)]
75#[serde(rename_all = "camelCase")]
76struct SarifMessage {
77 text: String,
78}
79
80#[derive(Debug, Serialize)]
81#[serde(rename_all = "camelCase")]
82struct SarifResult {
83 rule_id: String,
84 level: &'static str,
85 message: SarifMessage,
86 locations: Vec<SarifLocation>,
87}
88
89#[derive(Debug, Serialize)]
90#[serde(rename_all = "camelCase")]
91struct SarifLocation {
92 physical_location: SarifPhysicalLocation,
93}
94
95#[derive(Debug, Serialize)]
96#[serde(rename_all = "camelCase")]
97struct SarifPhysicalLocation {
98 artifact_location: SarifArtifactLocation,
99 region: SarifRegion,
100}
101
102#[derive(Debug, Serialize)]
103#[serde(rename_all = "camelCase")]
104struct SarifArtifactLocation {
105 uri: String,
106}
107
108#[derive(Debug, Serialize)]
109#[serde(rename_all = "camelCase")]
110struct SarifRegion {
111 start_line: u32,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 start_column: Option<u32>,
114}
115
116fn severity_to_sarif_level(severity: Severity) -> &'static str {
117 match severity {
118 Severity::Error => "error",
119 Severity::Warning => "warning",
120 Severity::Info => "note",
121 Severity::Style => "note",
122 Severity::Ignore => "none",
123 }
124}
125
126fn get_rule_help_uri(code: &str) -> Option<String> {
127 if code.starts_with("DL") {
128 Some(format!(
129 "https://github.com/hadolint/hadolint/wiki/{}",
130 code
131 ))
132 } else if code.starts_with("SC") {
133 Some(format!("https://www.shellcheck.net/wiki/{}", code))
134 } else {
135 None
136 }
137}
138
139impl Formatter for SarifFormatter {
140 fn format<W: Write>(&self, result: &LintResult, filename: &str, writer: &mut W) -> std::io::Result<()> {
141 let mut rules: Vec<SarifRule> = Vec::new();
143 let mut seen_rules = std::collections::HashSet::new();
144
145 for failure in &result.failures {
146 let code = failure.code.to_string();
147 if !seen_rules.contains(&code) {
148 seen_rules.insert(code.clone());
149 rules.push(SarifRule {
150 id: code.clone(),
151 name: code.clone(),
152 short_description: SarifMessage {
153 text: failure.message.clone(),
154 },
155 help_uri: get_rule_help_uri(&code),
156 default_configuration: SarifRuleConfiguration {
157 level: severity_to_sarif_level(failure.severity),
158 },
159 });
160 }
161 }
162
163 let results: Vec<SarifResult> = result
165 .failures
166 .iter()
167 .map(|f| SarifResult {
168 rule_id: f.code.to_string(),
169 level: severity_to_sarif_level(f.severity),
170 message: SarifMessage {
171 text: f.message.clone(),
172 },
173 locations: vec![SarifLocation {
174 physical_location: SarifPhysicalLocation {
175 artifact_location: SarifArtifactLocation {
176 uri: filename.to_string(),
177 },
178 region: SarifRegion {
179 start_line: f.line,
180 start_column: f.column,
181 },
182 },
183 }],
184 })
185 .collect();
186
187 let report = SarifReport {
188 schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
189 version: "2.1.0",
190 runs: vec![SarifRun {
191 tool: SarifTool {
192 driver: SarifDriver {
193 name: "hadolint-rs",
194 information_uri: "https://github.com/syncable-dev/syncable-cli",
195 version: env!("CARGO_PKG_VERSION"),
196 rules,
197 },
198 },
199 results,
200 }],
201 };
202
203 let json = serde_json::to_string_pretty(&report)
204 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
205
206 writeln!(writer, "{}", json)
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use crate::analyzer::hadolint::types::CheckFailure;
214
215 #[test]
216 fn test_sarif_output() {
217 let mut result = LintResult::new();
218 result.failures.push(CheckFailure::new(
219 "DL3008",
220 Severity::Warning,
221 "Pin versions in apt get install",
222 5,
223 ));
224
225 let formatter = SarifFormatter::new();
226 let output = formatter.format_to_string(&result, "Dockerfile");
227
228 assert!(output.contains("\"$schema\""));
229 assert!(output.contains("\"version\": \"2.1.0\""));
230 assert!(output.contains("hadolint-rs"));
231 assert!(output.contains("DL3008"));
232 assert!(output.contains("warning"));
233 }
234}