1use std::collections::{HashMap, HashSet};
11
12use rustsec::{Report, Vulnerability, Warning, WarningKind, advisory};
13use serde::{Serialize, Serializer, ser::SerializeStruct};
14
15#[derive(Debug)]
17pub struct SarifLog {
18 runs: Vec<Run>,
20}
21
22impl SarifLog {
23 pub fn from_report(report: &Report, cargo_lock_path: &str) -> Self {
25 Self {
26 runs: vec![Run::from_report(report, cargo_lock_path)],
27 }
28 }
29}
30
31impl Serialize for SarifLog {
32 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
33 let mut state = Serializer::serialize_struct(serializer, "SarifLog", 3)?;
34 state.serialize_field("$schema", "https://json.schemastore.org/sarif-2.1.0.json")?;
35 state.serialize_field("version", "2.1.0")?;
36 state.serialize_field("runs", &self.runs)?;
37 state.end()
38 }
39}
40
41#[derive(Debug, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct Run {
45 tool: Tool,
47 results: Vec<SarifResult>,
49}
50
51impl Run {
52 fn from_report(report: &Report, cargo_lock_path: &str) -> Self {
53 let mut rules = Vec::new();
54 let mut seen_rules = HashSet::new();
55 let mut results = Vec::new();
56
57 for vuln in &report.vulnerabilities.list {
58 let rule_id = vuln.advisory.id.to_string();
59
60 if seen_rules.insert(rule_id.clone()) {
61 rules.push(ReportingDescriptor::from_advisory(&vuln.advisory, true));
62 }
63
64 results.push(SarifResult::from_vulnerability(vuln, cargo_lock_path));
65 }
66
67 for (warning_kind, warnings) in &report.warnings {
68 for warning in warnings {
69 let rule_id = if let Some(advisory) = &warning.advisory {
70 advisory.id.to_string()
71 } else {
72 format!("{warning_kind:?}").to_lowercase()
73 };
74
75 if seen_rules.insert(rule_id) {
76 rules.push(match &warning.advisory {
77 Some(advisory) => ReportingDescriptor::from_advisory(advisory, false),
78 None => ReportingDescriptor::from_warning_kind(*warning_kind),
79 });
80 }
81
82 results.push(SarifResult::from_warning(warning, cargo_lock_path));
83 }
84 }
85
86 Self {
87 tool: Tool {
88 driver: ToolComponent { rules },
89 },
90 results,
91 }
92 }
93}
94
95#[derive(Debug, Serialize)]
97#[serde(rename_all = "camelCase")]
98struct Tool {
99 driver: ToolComponent,
101}
102
103#[derive(Debug)]
105struct ToolComponent {
106 rules: Vec<ReportingDescriptor>,
108}
109
110impl Serialize for ToolComponent {
111 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112 let mut state = serializer.serialize_struct("ToolComponent", 4)?;
113 state.serialize_field("name", "cargo-audit")?;
114 state.serialize_field("version", env!("CARGO_PKG_VERSION"))?;
115 state.serialize_field("semanticVersion", env!("CARGO_PKG_VERSION"))?;
116 state.serialize_field("rules", &self.rules)?;
117 state.end()
118 }
119}
120
121#[derive(Debug, Serialize)]
123#[serde(rename_all = "camelCase")]
124struct ReportingDescriptor {
125 id: String,
127 name: String,
129 short_description: MultiformatMessageString,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 full_description: Option<MultiformatMessageString>,
134 default_configuration: ReportingConfiguration,
136 #[serde(skip_serializing_if = "Option::is_none")]
138 help: Option<MultiformatMessageString>,
139 properties: RuleProperties,
141}
142
143impl ReportingDescriptor {
144 fn from_advisory(metadata: &advisory::Metadata, is_vulnerability: bool) -> Self {
146 let tags = if is_vulnerability {
147 &[Tag::Security, Tag::Vulnerability]
148 } else {
149 &[Tag::Security, Tag::Warning]
150 };
151
152 let security_severity = metadata
153 .cvss
154 .as_ref()
155 .map(|cvss| format!("{:.1}", cvss.score()));
156
157 ReportingDescriptor {
158 id: metadata.id.to_string(),
159 name: metadata.id.to_string(),
160 short_description: MultiformatMessageString {
161 text: metadata.title.clone(),
162 markdown: None,
163 },
164 full_description: if metadata.description.is_empty() {
165 None
166 } else {
167 Some(MultiformatMessageString {
168 text: metadata.description.clone(),
169 markdown: None,
170 })
171 },
172 default_configuration: ReportingConfiguration {
173 level: match is_vulnerability {
174 true => ReportingLevel::Error,
175 false => ReportingLevel::Warning,
176 },
177 },
178 help: metadata.url.as_ref().map(|url| MultiformatMessageString {
179 text: format!("For more information, see: {url}"),
180 markdown: Some(format!(
181 "For more information, see: [{}]({url})",
182 metadata.id
183 )),
184 }),
185 properties: RuleProperties {
186 tags,
187 precision: Precision::VeryHigh,
188 problem_severity: if !is_vulnerability {
189 Some(ProblemSeverity::Warning)
190 } else {
191 None
192 },
193 security_severity,
194 },
195 }
196 }
197
198 fn from_warning_kind(kind: WarningKind) -> Self {
200 let (name, description) = match kind {
201 WarningKind::Unmaintained => (
202 "unmaintained",
203 "Package is unmaintained and may have unaddressed security vulnerabilities",
204 ),
205 WarningKind::Unsound => (
206 "unsound",
207 "Package has known soundness issues that may lead to memory safety problems",
208 ),
209 WarningKind::Yanked => (
210 "yanked",
211 "Package version has been yanked from the registry",
212 ),
213 _ => ("unknown", "Unknown warning type"),
214 };
215
216 ReportingDescriptor {
217 id: name.to_string(),
218 name: name.to_string(),
219 short_description: MultiformatMessageString {
220 text: description.to_string(),
221 markdown: None,
222 },
223 full_description: None,
224 default_configuration: ReportingConfiguration {
225 level: ReportingLevel::Warning,
226 },
227 help: None,
228 properties: RuleProperties {
229 tags: &[Tag::Security, Tag::Warning],
230 precision: Precision::High,
231 problem_severity: Some(ProblemSeverity::Warning),
232 security_severity: None,
233 },
234 }
235 }
236}
237
238#[derive(Debug, Serialize)]
240#[serde(rename_all = "camelCase")]
241struct RuleProperties {
242 #[serde(skip_serializing_if = "<[Tag]>::is_empty")]
244 tags: &'static [Tag],
245 precision: Precision,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 #[serde(rename = "problem.severity")]
250 problem_severity: Option<ProblemSeverity>,
251 #[serde(skip_serializing_if = "Option::is_none")]
253 #[serde(rename = "security-severity")]
254 security_severity: Option<String>,
255}
256
257#[derive(Debug, Serialize)]
258#[serde(rename_all = "lowercase")]
259enum ProblemSeverity {
260 Warning,
261}
262
263#[derive(Debug, Serialize)]
265#[serde(rename_all = "camelCase")]
266struct ReportingConfiguration {
267 level: ReportingLevel,
269}
270
271#[derive(Debug, Serialize)]
272#[serde(rename_all = "camelCase")]
273enum ReportingLevel {
274 Error,
275 Warning,
276}
277
278#[derive(Debug, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct MultiformatMessageString {
282 text: String,
284 #[serde(skip_serializing_if = "Option::is_none")]
286 markdown: Option<String>,
287}
288
289#[derive(Debug, Serialize)]
291#[serde(rename_all = "camelCase")]
292pub struct SarifResult {
293 rule_id: String,
295 message: Message,
297 level: ResultLevel,
299 locations: Vec<Location>,
301 partial_fingerprints: HashMap<String, String>,
303}
304
305impl SarifResult {
306 fn from_vulnerability(vuln: &Vulnerability, cargo_lock_path: &str) -> Self {
308 let fingerprint = format!(
309 "{}:{}:{}",
310 vuln.advisory.id, vuln.package.name, vuln.package.version
311 );
312
313 SarifResult {
314 rule_id: vuln.advisory.id.to_string(),
315 message: Message {
316 text: format!(
317 "{} {} is vulnerable to {} ({})",
318 vuln.package.name, vuln.package.version, vuln.advisory.id, vuln.advisory.title
319 ),
320 },
321 level: ResultLevel::Error,
322 locations: vec![Location::new(cargo_lock_path)],
323 partial_fingerprints: {
324 let mut fingerprints = HashMap::new();
325 fingerprints.insert("cargo-audit/advisory-fingerprint".to_string(), fingerprint);
328 fingerprints
329 },
330 }
331 }
332
333 fn from_warning(warning: &Warning, cargo_lock_path: &str) -> Self {
335 let rule_id = if let Some(advisory) = &warning.advisory {
336 advisory.id.to_string()
337 } else {
338 format!("{:?}", warning.kind).to_lowercase()
339 };
340
341 let message_text = if let Some(advisory) = &warning.advisory {
342 format!(
343 "{} {} has a {} warning: {}",
344 warning.package.name,
345 warning.package.version,
346 warning.kind.as_str(),
347 advisory.title
348 )
349 } else {
350 format!(
351 "{} {} has a {} warning",
352 warning.package.name,
353 warning.package.version,
354 warning.kind.as_str()
355 )
356 };
357
358 let fingerprint = format!(
359 "{rule_id}:{}:{}",
360 warning.package.name, warning.package.version
361 );
362
363 SarifResult {
364 rule_id,
365 message: Message { text: message_text },
366 level: ResultLevel::Warning,
367 locations: vec![Location::new(cargo_lock_path)],
368 partial_fingerprints: {
369 let mut fingerprints = HashMap::new();
370 fingerprints.insert("cargo-audit/advisory-fingerprint".to_string(), fingerprint);
373 fingerprints
374 },
375 }
376 }
377}
378
379#[derive(Debug, Serialize)]
380#[serde(rename_all = "camelCase")]
381enum ResultLevel {
382 Error,
383 Warning,
384}
385
386#[derive(Debug, Serialize)]
388#[serde(rename_all = "camelCase")]
389struct Message {
390 text: String,
392}
393
394#[derive(Debug, Serialize)]
396#[serde(rename_all = "camelCase")]
397struct Location {
398 physical_location: PhysicalLocation,
400}
401
402impl Location {
403 fn new(cargo_lock_path: &str) -> Self {
404 Self {
405 physical_location: PhysicalLocation {
406 artifact_location: ArtifactLocation {
407 uri: cargo_lock_path.to_string(),
408 },
409 region: Region { start_line: 1 },
410 },
411 }
412 }
413}
414
415#[derive(Debug, Serialize)]
417#[serde(rename_all = "camelCase")]
418struct PhysicalLocation {
419 artifact_location: ArtifactLocation,
421 region: Region,
423}
424
425#[derive(Debug, Serialize)]
427#[serde(rename_all = "camelCase")]
428struct ArtifactLocation {
429 uri: String,
431}
432
433#[derive(Debug, Serialize)]
435#[serde(rename_all = "camelCase")]
436struct Region {
437 start_line: u32,
439}
440
441#[derive(Debug, Serialize)]
442#[serde(rename_all = "camelCase")]
443enum Tag {
444 Security,
445 Vulnerability,
446 Warning,
447}
448
449#[derive(Debug, Serialize)]
450#[serde(rename_all = "kebab-case")]
451enum Precision {
452 High,
453 VeryHigh,
454}