1use super::{ReportConfig, ReportError, ReportFormat, ReportGenerator, ReportType};
4use crate::diff::{DiffResult, SlaStatus, VulnerabilityDetail};
5use crate::model::NormalizedSbom;
6use crate::quality::{ComplianceChecker, ComplianceLevel, ComplianceResult, ViolationSeverity};
7use serde::Serialize;
8
9pub struct SarifReporter {
11 include_info: bool,
13}
14
15impl SarifReporter {
16 #[must_use]
18 pub const fn new() -> Self {
19 Self { include_info: true }
20 }
21
22 #[must_use]
24 pub const fn include_info(mut self, include: bool) -> Self {
25 self.include_info = include;
26 self
27 }
28}
29
30impl Default for SarifReporter {
31 fn default() -> Self {
32 Self::new()
33 }
34}
35
36impl ReportGenerator for SarifReporter {
37 fn generate_diff_report(
38 &self,
39 result: &DiffResult,
40 old_sbom: &NormalizedSbom,
41 new_sbom: &NormalizedSbom,
42 config: &ReportConfig,
43 ) -> Result<String, ReportError> {
44 let mut results = Vec::new();
45
46 if config.includes(ReportType::Components) {
48 for comp in &result.components.added {
49 if self.include_info {
50 results.push(SarifResult {
51 rule_id: "SBOM-TOOLS-001".to_string(),
52 level: SarifLevel::Note,
53 message: SarifMessage {
54 text: format!(
55 "Component added: {} {}",
56 comp.name,
57 comp.new_version.as_deref().unwrap_or("")
58 ),
59 },
60 locations: vec![],
61 });
62 }
63 }
64
65 for comp in &result.components.removed {
66 results.push(SarifResult {
67 rule_id: "SBOM-TOOLS-002".to_string(),
68 level: SarifLevel::Warning,
69 message: SarifMessage {
70 text: format!(
71 "Component removed: {} {}",
72 comp.name,
73 comp.old_version.as_deref().unwrap_or("")
74 ),
75 },
76 locations: vec![],
77 });
78 }
79
80 for comp in &result.components.modified {
81 if self.include_info {
82 results.push(SarifResult {
83 rule_id: "SBOM-TOOLS-003".to_string(),
84 level: SarifLevel::Note,
85 message: SarifMessage {
86 text: format!(
87 "Component modified: {} {} -> {}",
88 comp.name,
89 comp.old_version.as_deref().unwrap_or("unknown"),
90 comp.new_version.as_deref().unwrap_or("unknown")
91 ),
92 },
93 locations: vec![],
94 });
95 }
96 }
97 }
98
99 if config.includes(ReportType::Vulnerabilities) {
101 for vuln in &result.vulnerabilities.introduced {
102 let depth_label = match vuln.component_depth {
103 Some(1) => " [Direct]",
104 Some(_) => " [Transitive]",
105 None => "",
106 };
107 let sla_label = format_sla_label(vuln);
108 let vex_label = format_vex_label(vuln.vex_state.as_ref());
109 results.push(SarifResult {
110 rule_id: "SBOM-TOOLS-005".to_string(),
111 level: severity_to_level(&vuln.severity),
112 message: SarifMessage {
113 text: format!(
114 "Vulnerability introduced: {} ({}){}{}{} in {} {}",
115 vuln.id,
116 vuln.severity,
117 depth_label,
118 sla_label,
119 vex_label,
120 vuln.component_name,
121 vuln.version.as_deref().unwrap_or("")
122 ),
123 },
124 locations: vec![],
125 });
126 }
127
128 for vuln in &result.vulnerabilities.resolved {
129 if self.include_info {
130 let depth_label = match vuln.component_depth {
131 Some(1) => " [Direct]",
132 Some(_) => " [Transitive]",
133 None => "",
134 };
135 let sla_label = format_sla_label(vuln);
136 let vex_label = format_vex_label(vuln.vex_state.as_ref());
137 results.push(SarifResult {
138 rule_id: "SBOM-TOOLS-006".to_string(),
139 level: SarifLevel::Note,
140 message: SarifMessage {
141 text: format!(
142 "Vulnerability resolved: {} ({}){}{}{} was in {}",
143 vuln.id,
144 vuln.severity,
145 depth_label,
146 sla_label,
147 vex_label,
148 vuln.component_name
149 ),
150 },
151 locations: vec![],
152 });
153 }
154 }
155 }
156
157 if config.includes(ReportType::Licenses) {
159 for license in &result.licenses.new_licenses {
160 results.push(SarifResult {
161 rule_id: "SBOM-TOOLS-004".to_string(),
162 level: SarifLevel::Warning,
163 message: SarifMessage {
164 text: format!(
165 "New license introduced: {} in components: {}",
166 license.license,
167 license.components.join(", ")
168 ),
169 },
170 locations: vec![],
171 });
172 }
173 }
174
175 for comp in new_sbom.components.values() {
177 if let Some(eol) = &comp.eol {
178 match eol.status {
179 crate::model::EolStatus::EndOfLife => {
180 let eol_date_str = eol
181 .eol_date
182 .map_or_else(String::new, |d| format!(" (EOL: {d})"));
183 results.push(SarifResult {
184 rule_id: "SBOM-EOL-001".to_string(),
185 level: SarifLevel::Error,
186 message: SarifMessage {
187 text: format!(
188 "Component '{}' version '{}' has reached end-of-life{} (product: {})",
189 comp.name,
190 comp.version.as_deref().unwrap_or("unknown"),
191 eol_date_str,
192 eol.product,
193 ),
194 },
195 locations: vec![],
196 });
197 }
198 crate::model::EolStatus::ApproachingEol => {
199 let days_str = eol
200 .days_until_eol
201 .map_or_else(String::new, |d| format!(" ({d} days remaining)"));
202 results.push(SarifResult {
203 rule_id: "SBOM-EOL-002".to_string(),
204 level: SarifLevel::Warning,
205 message: SarifMessage {
206 text: format!(
207 "Component '{}' version '{}' is approaching end-of-life{} (product: {})",
208 comp.name,
209 comp.version.as_deref().unwrap_or("unknown"),
210 days_str,
211 eol.product,
212 ),
213 },
214 locations: vec![],
215 });
216 }
217 _ => {}
218 }
219 }
220 }
221
222 let cra_old = config
224 .old_cra_compliance
225 .clone()
226 .unwrap_or_else(|| ComplianceChecker::new(ComplianceLevel::CraPhase2).check(old_sbom));
227 let cra_new = config
228 .new_cra_compliance
229 .clone()
230 .unwrap_or_else(|| ComplianceChecker::new(ComplianceLevel::CraPhase2).check(new_sbom));
231 results.extend(compliance_results_to_sarif(&cra_old, Some("Old SBOM")));
232 results.extend(compliance_results_to_sarif(&cra_new, Some("New SBOM")));
233
234 let sarif = SarifReport {
235 schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
236 version: "2.1.0".to_string(),
237 runs: vec![SarifRun {
238 tool: SarifTool {
239 driver: SarifDriver {
240 name: "sbom-tools".to_string(),
241 version: env!("CARGO_PKG_VERSION").to_string(),
242 information_uri: "https://github.com/binarly-io/sbom-tools".to_string(),
243 rules: get_sarif_rules(),
244 },
245 },
246 results,
247 }],
248 };
249
250 serde_json::to_string_pretty(&sarif)
251 .map_err(|e| ReportError::SerializationError(e.to_string()))
252 }
253
254 fn generate_view_report(
255 &self,
256 sbom: &NormalizedSbom,
257 config: &ReportConfig,
258 ) -> Result<String, ReportError> {
259 let mut results = Vec::new();
260
261 for (comp, vuln) in sbom.all_vulnerabilities() {
263 let severity_str = vuln
264 .severity
265 .as_ref()
266 .map_or_else(|| "Unknown".to_string(), std::string::ToString::to_string);
267 let vex_state = vuln
268 .vex_status
269 .as_ref()
270 .map(|v| &v.status)
271 .or_else(|| comp.vex_status.as_ref().map(|v| &v.status));
272 let vex_label = format_vex_label(vex_state);
273 results.push(SarifResult {
274 rule_id: "SBOM-VIEW-001".to_string(),
275 level: severity_to_level(&severity_str),
276 message: SarifMessage {
277 text: format!(
278 "Vulnerability {} ({}){} in {} {}",
279 vuln.id,
280 severity_str,
281 vex_label,
282 comp.name,
283 comp.version.as_deref().unwrap_or("")
284 ),
285 },
286 locations: vec![],
287 });
288 }
289
290 let cra_result = config
292 .view_cra_compliance
293 .clone()
294 .unwrap_or_else(|| ComplianceChecker::new(ComplianceLevel::CraPhase2).check(sbom));
295 results.extend(compliance_results_to_sarif(&cra_result, None));
296
297 let sarif = SarifReport {
298 schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
299 version: "2.1.0".to_string(),
300 runs: vec![SarifRun {
301 tool: SarifTool {
302 driver: SarifDriver {
303 name: "sbom-tools".to_string(),
304 version: env!("CARGO_PKG_VERSION").to_string(),
305 information_uri: "https://github.com/binarly-io/sbom-tools".to_string(),
306 rules: get_sarif_view_rules(),
307 },
308 },
309 results,
310 }],
311 };
312
313 serde_json::to_string_pretty(&sarif)
314 .map_err(|e| ReportError::SerializationError(e.to_string()))
315 }
316
317 fn format(&self) -> ReportFormat {
318 ReportFormat::Sarif
319 }
320}
321
322pub fn generate_compliance_sarif(result: &ComplianceResult) -> Result<String, ReportError> {
323 let rules = get_sarif_rules_for_standard(result.level);
324 let sarif = SarifReport {
325 schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
326 version: "2.1.0".to_string(),
327 runs: vec![SarifRun {
328 tool: SarifTool {
329 driver: SarifDriver {
330 name: "sbom-tools".to_string(),
331 version: env!("CARGO_PKG_VERSION").to_string(),
332 information_uri: "https://github.com/binarly-io/sbom-tools".to_string(),
333 rules,
334 },
335 },
336 results: compliance_results_to_sarif(result, None),
337 }],
338 };
339
340 serde_json::to_string_pretty(&sarif).map_err(|e| ReportError::SerializationError(e.to_string()))
341}
342
343pub fn generate_multi_compliance_sarif(
345 results: &[ComplianceResult],
346) -> Result<String, ReportError> {
347 let mut all_rules = Vec::new();
349 let mut all_results = Vec::new();
350
351 for result in results {
352 let rules = get_sarif_rules_for_standard(result.level);
353 all_rules.extend(rules);
354 all_results.extend(compliance_results_to_sarif(result, None));
355 }
356
357 let sarif = SarifReport {
358 schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json".to_string(),
359 version: "2.1.0".to_string(),
360 runs: vec![SarifRun {
361 tool: SarifTool {
362 driver: SarifDriver {
363 name: "sbom-tools".to_string(),
364 version: env!("CARGO_PKG_VERSION").to_string(),
365 information_uri: "https://github.com/binarly-io/sbom-tools".to_string(),
366 rules: all_rules,
367 },
368 },
369 results: all_results,
370 }],
371 };
372
373 serde_json::to_string_pretty(&sarif).map_err(|e| ReportError::SerializationError(e.to_string()))
374}
375
376fn severity_to_level(severity: &str) -> SarifLevel {
377 match severity.to_lowercase().as_str() {
378 "critical" | "high" => SarifLevel::Error,
379 "low" | "info" => SarifLevel::Note,
380 _ => SarifLevel::Warning,
381 }
382}
383
384fn format_sla_label(vuln: &VulnerabilityDetail) -> String {
386 match vuln.sla_status() {
387 SlaStatus::Overdue(days) => format!(" [SLA: {days}d late]"),
388 SlaStatus::DueSoon(days) | SlaStatus::OnTrack(days) => format!(" [SLA: {days}d left]"),
389 SlaStatus::NoDueDate => vuln
390 .days_since_published
391 .map(|d| format!(" [Age: {d}d]"))
392 .unwrap_or_default(),
393 }
394}
395
396fn format_vex_label(vex_state: Option<&crate::model::VexState>) -> String {
397 match vex_state {
398 Some(crate::model::VexState::NotAffected) => " [VEX: Not Affected]".to_string(),
399 Some(crate::model::VexState::Fixed) => " [VEX: Fixed]".to_string(),
400 Some(crate::model::VexState::Affected) => " [VEX: Affected]".to_string(),
401 Some(crate::model::VexState::UnderInvestigation) => {
402 " [VEX: Under Investigation]".to_string()
403 }
404 None => String::new(),
405 }
406}
407
408const fn violation_severity_to_level(severity: ViolationSeverity) -> SarifLevel {
409 match severity {
410 ViolationSeverity::Error => SarifLevel::Error,
411 ViolationSeverity::Warning => SarifLevel::Warning,
412 ViolationSeverity::Info => SarifLevel::Note,
413 }
414}
415
416fn violation_to_rule_id(requirement: &str) -> &'static str {
418 let req = requirement.to_lowercase();
419
420 if req.starts_with("ntia") {
422 if req.contains("author") {
423 return "SBOM-NTIA-AUTHOR";
424 } else if req.contains("component name") {
425 return "SBOM-NTIA-NAME";
426 } else if req.contains("version") {
427 return "SBOM-NTIA-VERSION";
428 } else if req.contains("supplier") {
429 return "SBOM-NTIA-SUPPLIER";
430 } else if req.contains("unique identifier") {
431 return "SBOM-NTIA-IDENTIFIER";
432 } else if req.contains("dependency") {
433 return "SBOM-NTIA-DEPENDENCY";
434 }
435 return "SBOM-NTIA-GENERAL";
436 }
437
438 if req.starts_with("fda") {
440 if req.contains("author") || req.contains("creator") {
441 return "SBOM-FDA-CREATOR";
442 } else if req.contains("serial") || req.contains("namespace") {
443 return "SBOM-FDA-NAMESPACE";
444 } else if req.contains("name") || req.contains("title") {
445 return "SBOM-FDA-NAME";
446 } else if req.contains("supplier") || req.contains("manufacturer") {
447 return "SBOM-FDA-SUPPLIER";
448 } else if req.contains("hash") {
449 return "SBOM-FDA-HASH";
450 } else if req.contains("identifier") {
451 return "SBOM-FDA-IDENTIFIER";
452 } else if req.contains("version") {
453 return "SBOM-FDA-VERSION";
454 } else if req.contains("dependency") || req.contains("orphan") {
455 return "SBOM-FDA-DEPENDENCY";
456 } else if req.contains("support") || req.contains("contact") {
457 return "SBOM-FDA-SUPPORT";
458 } else if req.contains("vulnerabilit") || req.contains("security") {
459 return "SBOM-FDA-SECURITY";
460 }
461 return "SBOM-FDA-GENERAL";
462 }
463
464 if req.starts_with("nist ssdf") {
466 if req.contains("ps.1") {
467 return "SBOM-SSDF-PS1";
468 } else if req.contains("ps.2") {
469 return "SBOM-SSDF-PS2";
470 } else if req.contains("ps.3") {
471 return "SBOM-SSDF-PS3";
472 } else if req.contains("po.1") {
473 return "SBOM-SSDF-PO1";
474 } else if req.contains("po.3") {
475 return "SBOM-SSDF-PO3";
476 } else if req.contains("pw.4") {
477 return "SBOM-SSDF-PW4";
478 } else if req.contains("pw.6") {
479 return "SBOM-SSDF-PW6";
480 } else if req.contains("rv.1") {
481 return "SBOM-SSDF-RV1";
482 }
483 return "SBOM-SSDF-GENERAL";
484 }
485
486 if req.starts_with("eo 14028") {
488 if req.contains("machine-readable") || req.contains("format") {
489 return "SBOM-EO14028-FORMAT";
490 } else if req.contains("auto") || req.contains("generation") {
491 return "SBOM-EO14028-AUTOGEN";
492 } else if req.contains("creator") {
493 return "SBOM-EO14028-CREATOR";
494 } else if req.contains("unique ident") {
495 return "SBOM-EO14028-IDENTIFIER";
496 } else if req.contains("dependency") || req.contains("relationship") {
497 return "SBOM-EO14028-DEPENDENCY";
498 } else if req.contains("version") {
499 return "SBOM-EO14028-VERSION";
500 } else if req.contains("integrity") || req.contains("hash") {
501 return "SBOM-EO14028-INTEGRITY";
502 } else if req.contains("disclosure") || req.contains("vulnerab") {
503 return "SBOM-EO14028-DISCLOSURE";
504 } else if req.contains("supplier") {
505 return "SBOM-EO14028-SUPPLIER";
506 }
507 return "SBOM-EO14028-GENERAL";
508 }
509
510 if req.contains("art. 13(3)") || req.contains("art.13(3)") {
512 "SBOM-CRA-ART-13-3"
513 } else if req.contains("art. 13(4)") || req.contains("art.13(4)") {
514 "SBOM-CRA-ART-13-4"
515 } else if req.contains("art. 13(6)") || req.contains("art.13(6)") {
516 "SBOM-CRA-ART-13-6"
517 } else if req.contains("art. 13(7)") || req.contains("art.13(7)") {
518 "SBOM-CRA-ART-13-7"
519 } else if req.contains("art. 13(8)") || req.contains("art.13(8)") {
520 "SBOM-CRA-ART-13-8"
521 } else if req.contains("art. 13(11)") || req.contains("art.13(11)") {
522 "SBOM-CRA-ART-13-11"
523 } else if req.contains("art. 13(12)") || req.contains("art.13(12)") {
524 "SBOM-CRA-ART-13-12"
525 } else if req.contains("art. 13(15)") || req.contains("art.13(15)") {
526 "SBOM-CRA-ART-13-15"
527 } else if req.contains("art. 13(5)") || req.contains("art.13(5)") {
528 "SBOM-CRA-ART-13-5"
529 } else if req.contains("art. 13(9)") || req.contains("art.13(9)") {
530 "SBOM-CRA-ART-13-9"
531 } else if req.contains("annex vii") {
532 "SBOM-CRA-ANNEX-VII"
533 } else if req.contains("annex iii") {
534 "SBOM-CRA-ANNEX-III"
535 } else if req.contains("annex i") || req.contains("annex_i") {
536 "SBOM-CRA-ANNEX-I"
537 } else {
538 "SBOM-CRA-GENERAL"
539 }
540}
541
542fn compliance_results_to_sarif(result: &ComplianceResult, label: Option<&str>) -> Vec<SarifResult> {
543 let prefix = label.map(|l| format!("{l} - ")).unwrap_or_default();
544 result
545 .violations
546 .iter()
547 .map(|v| {
548 let element = v.element.as_deref().unwrap_or("unknown");
549 SarifResult {
550 rule_id: violation_to_rule_id(&v.requirement).to_string(),
551 level: violation_severity_to_level(v.severity),
552 message: SarifMessage {
553 text: format!(
554 "{}{}: {} (Requirement: {}) [Element: {}]",
555 prefix,
556 result.level.name(),
557 v.message,
558 v.requirement,
559 element
560 ),
561 },
562 locations: vec![],
563 }
564 })
565 .collect()
566}
567
568fn get_sarif_rules() -> Vec<SarifRule> {
569 let mut rules = vec![
570 SarifRule {
571 id: "SBOM-TOOLS-001".to_string(),
572 name: "ComponentAdded".to_string(),
573 short_description: SarifMessage {
574 text: "A new component was added to the SBOM".to_string(),
575 },
576 default_configuration: SarifConfiguration {
577 level: SarifLevel::Note,
578 },
579 },
580 SarifRule {
581 id: "SBOM-TOOLS-002".to_string(),
582 name: "ComponentRemoved".to_string(),
583 short_description: SarifMessage {
584 text: "A component was removed from the SBOM".to_string(),
585 },
586 default_configuration: SarifConfiguration {
587 level: SarifLevel::Warning,
588 },
589 },
590 SarifRule {
591 id: "SBOM-TOOLS-003".to_string(),
592 name: "VersionChanged".to_string(),
593 short_description: SarifMessage {
594 text: "A component version was changed".to_string(),
595 },
596 default_configuration: SarifConfiguration {
597 level: SarifLevel::Note,
598 },
599 },
600 SarifRule {
601 id: "SBOM-TOOLS-004".to_string(),
602 name: "LicenseChanged".to_string(),
603 short_description: SarifMessage {
604 text: "A license was added or changed".to_string(),
605 },
606 default_configuration: SarifConfiguration {
607 level: SarifLevel::Warning,
608 },
609 },
610 SarifRule {
611 id: "SBOM-TOOLS-005".to_string(),
612 name: "VulnerabilityIntroduced".to_string(),
613 short_description: SarifMessage {
614 text: "A new vulnerability was introduced".to_string(),
615 },
616 default_configuration: SarifConfiguration {
617 level: SarifLevel::Error,
618 },
619 },
620 SarifRule {
621 id: "SBOM-TOOLS-006".to_string(),
622 name: "VulnerabilityResolved".to_string(),
623 short_description: SarifMessage {
624 text: "A vulnerability was resolved".to_string(),
625 },
626 default_configuration: SarifConfiguration {
627 level: SarifLevel::Note,
628 },
629 },
630 SarifRule {
631 id: "SBOM-TOOLS-007".to_string(),
632 name: "SupplierChanged".to_string(),
633 short_description: SarifMessage {
634 text: "A component supplier was changed".to_string(),
635 },
636 default_configuration: SarifConfiguration {
637 level: SarifLevel::Warning,
638 },
639 },
640 SarifRule {
641 id: "SBOM-EOL-001".to_string(),
642 name: "ComponentEndOfLife".to_string(),
643 short_description: SarifMessage {
644 text: "A component has reached end-of-life".to_string(),
645 },
646 default_configuration: SarifConfiguration {
647 level: SarifLevel::Error,
648 },
649 },
650 SarifRule {
651 id: "SBOM-EOL-002".to_string(),
652 name: "ComponentApproachingEol".to_string(),
653 short_description: SarifMessage {
654 text: "A component is approaching end-of-life".to_string(),
655 },
656 default_configuration: SarifConfiguration {
657 level: SarifLevel::Warning,
658 },
659 },
660 ];
661 rules.extend(get_sarif_compliance_rules());
662 rules
663}
664
665fn get_sarif_view_rules() -> Vec<SarifRule> {
666 let mut rules = vec![SarifRule {
667 id: "SBOM-VIEW-001".to_string(),
668 name: "VulnerabilityPresent".to_string(),
669 short_description: SarifMessage {
670 text: "A vulnerability is present in a component".to_string(),
671 },
672 default_configuration: SarifConfiguration {
673 level: SarifLevel::Warning,
674 },
675 }];
676 rules.extend(get_sarif_compliance_rules());
677 rules
678}
679
680fn get_sarif_rules_for_standard(level: ComplianceLevel) -> Vec<SarifRule> {
682 match level {
683 ComplianceLevel::NtiaMinimum => get_sarif_ntia_rules(),
684 ComplianceLevel::FdaMedicalDevice => get_sarif_fda_rules(),
685 ComplianceLevel::NistSsdf => get_sarif_ssdf_rules(),
686 ComplianceLevel::Eo14028 => get_sarif_eo14028_rules(),
687 _ => get_sarif_compliance_rules(),
688 }
689}
690
691fn get_sarif_ntia_rules() -> Vec<SarifRule> {
692 vec![
693 SarifRule {
694 id: "SBOM-NTIA-AUTHOR".to_string(),
695 name: "NtiaAuthor".to_string(),
696 short_description: SarifMessage {
697 text: "NTIA Minimum Elements: Author/creator information".to_string(),
698 },
699 default_configuration: SarifConfiguration {
700 level: SarifLevel::Error,
701 },
702 },
703 SarifRule {
704 id: "SBOM-NTIA-NAME".to_string(),
705 name: "NtiaComponentName".to_string(),
706 short_description: SarifMessage {
707 text: "NTIA Minimum Elements: Component name".to_string(),
708 },
709 default_configuration: SarifConfiguration {
710 level: SarifLevel::Error,
711 },
712 },
713 SarifRule {
714 id: "SBOM-NTIA-VERSION".to_string(),
715 name: "NtiaVersion".to_string(),
716 short_description: SarifMessage {
717 text: "NTIA Minimum Elements: Component version string".to_string(),
718 },
719 default_configuration: SarifConfiguration {
720 level: SarifLevel::Warning,
721 },
722 },
723 SarifRule {
724 id: "SBOM-NTIA-SUPPLIER".to_string(),
725 name: "NtiaSupplier".to_string(),
726 short_description: SarifMessage {
727 text: "NTIA Minimum Elements: Supplier name".to_string(),
728 },
729 default_configuration: SarifConfiguration {
730 level: SarifLevel::Warning,
731 },
732 },
733 SarifRule {
734 id: "SBOM-NTIA-IDENTIFIER".to_string(),
735 name: "NtiaUniqueIdentifier".to_string(),
736 short_description: SarifMessage {
737 text: "NTIA Minimum Elements: Unique identifier (PURL/CPE/SWID)".to_string(),
738 },
739 default_configuration: SarifConfiguration {
740 level: SarifLevel::Warning,
741 },
742 },
743 SarifRule {
744 id: "SBOM-NTIA-DEPENDENCY".to_string(),
745 name: "NtiaDependency".to_string(),
746 short_description: SarifMessage {
747 text: "NTIA Minimum Elements: Dependency relationship".to_string(),
748 },
749 default_configuration: SarifConfiguration {
750 level: SarifLevel::Error,
751 },
752 },
753 SarifRule {
754 id: "SBOM-NTIA-GENERAL".to_string(),
755 name: "NtiaGeneralRequirement".to_string(),
756 short_description: SarifMessage {
757 text: "NTIA Minimum Elements: General requirement".to_string(),
758 },
759 default_configuration: SarifConfiguration {
760 level: SarifLevel::Warning,
761 },
762 },
763 ]
764}
765
766fn get_sarif_fda_rules() -> Vec<SarifRule> {
767 vec![
768 SarifRule {
769 id: "SBOM-FDA-CREATOR".to_string(),
770 name: "FdaCreator".to_string(),
771 short_description: SarifMessage {
772 text: "FDA Medical Device: SBOM creator/manufacturer information".to_string(),
773 },
774 default_configuration: SarifConfiguration {
775 level: SarifLevel::Warning,
776 },
777 },
778 SarifRule {
779 id: "SBOM-FDA-NAMESPACE".to_string(),
780 name: "FdaNamespace".to_string(),
781 short_description: SarifMessage {
782 text: "FDA Medical Device: SBOM serial number or document namespace".to_string(),
783 },
784 default_configuration: SarifConfiguration {
785 level: SarifLevel::Warning,
786 },
787 },
788 SarifRule {
789 id: "SBOM-FDA-NAME".to_string(),
790 name: "FdaDocumentName".to_string(),
791 short_description: SarifMessage {
792 text: "FDA Medical Device: SBOM document name/title".to_string(),
793 },
794 default_configuration: SarifConfiguration {
795 level: SarifLevel::Warning,
796 },
797 },
798 SarifRule {
799 id: "SBOM-FDA-SUPPLIER".to_string(),
800 name: "FdaSupplier".to_string(),
801 short_description: SarifMessage {
802 text: "FDA Medical Device: Component supplier/manufacturer information".to_string(),
803 },
804 default_configuration: SarifConfiguration {
805 level: SarifLevel::Error,
806 },
807 },
808 SarifRule {
809 id: "SBOM-FDA-HASH".to_string(),
810 name: "FdaHash".to_string(),
811 short_description: SarifMessage {
812 text: "FDA Medical Device: Component cryptographic hash".to_string(),
813 },
814 default_configuration: SarifConfiguration {
815 level: SarifLevel::Error,
816 },
817 },
818 SarifRule {
819 id: "SBOM-FDA-IDENTIFIER".to_string(),
820 name: "FdaIdentifier".to_string(),
821 short_description: SarifMessage {
822 text: "FDA Medical Device: Component unique identifier (PURL/CPE/SWID)".to_string(),
823 },
824 default_configuration: SarifConfiguration {
825 level: SarifLevel::Error,
826 },
827 },
828 SarifRule {
829 id: "SBOM-FDA-VERSION".to_string(),
830 name: "FdaVersion".to_string(),
831 short_description: SarifMessage {
832 text: "FDA Medical Device: Component version information".to_string(),
833 },
834 default_configuration: SarifConfiguration {
835 level: SarifLevel::Error,
836 },
837 },
838 SarifRule {
839 id: "SBOM-FDA-DEPENDENCY".to_string(),
840 name: "FdaDependency".to_string(),
841 short_description: SarifMessage {
842 text: "FDA Medical Device: Dependency relationships".to_string(),
843 },
844 default_configuration: SarifConfiguration {
845 level: SarifLevel::Error,
846 },
847 },
848 SarifRule {
849 id: "SBOM-FDA-SUPPORT".to_string(),
850 name: "FdaSupport".to_string(),
851 short_description: SarifMessage {
852 text: "FDA Medical Device: Component support/contact information".to_string(),
853 },
854 default_configuration: SarifConfiguration {
855 level: SarifLevel::Note,
856 },
857 },
858 SarifRule {
859 id: "SBOM-FDA-SECURITY".to_string(),
860 name: "FdaSecurity".to_string(),
861 short_description: SarifMessage {
862 text: "FDA Medical Device: Security vulnerability information".to_string(),
863 },
864 default_configuration: SarifConfiguration {
865 level: SarifLevel::Warning,
866 },
867 },
868 SarifRule {
869 id: "SBOM-FDA-GENERAL".to_string(),
870 name: "FdaGeneralRequirement".to_string(),
871 short_description: SarifMessage {
872 text: "FDA Medical Device: General SBOM requirement".to_string(),
873 },
874 default_configuration: SarifConfiguration {
875 level: SarifLevel::Warning,
876 },
877 },
878 ]
879}
880
881fn get_sarif_ssdf_rules() -> Vec<SarifRule> {
882 vec![
883 SarifRule {
884 id: "SBOM-SSDF-PS1".to_string(),
885 name: "SsdfProvenance".to_string(),
886 short_description: SarifMessage {
887 text: "NIST SSDF PS.1: Provenance and creator identification".to_string(),
888 },
889 default_configuration: SarifConfiguration {
890 level: SarifLevel::Error,
891 },
892 },
893 SarifRule {
894 id: "SBOM-SSDF-PS2".to_string(),
895 name: "SsdfBuildIntegrity".to_string(),
896 short_description: SarifMessage {
897 text: "NIST SSDF PS.2: Build integrity — component cryptographic hashes"
898 .to_string(),
899 },
900 default_configuration: SarifConfiguration {
901 level: SarifLevel::Warning,
902 },
903 },
904 SarifRule {
905 id: "SBOM-SSDF-PS3".to_string(),
906 name: "SsdfSupplierIdentification".to_string(),
907 short_description: SarifMessage {
908 text: "NIST SSDF PS.3: Supplier identification for components".to_string(),
909 },
910 default_configuration: SarifConfiguration {
911 level: SarifLevel::Warning,
912 },
913 },
914 SarifRule {
915 id: "SBOM-SSDF-PO1".to_string(),
916 name: "SsdfSourceProvenance".to_string(),
917 short_description: SarifMessage {
918 text: "NIST SSDF PO.1: Source code provenance — VCS references".to_string(),
919 },
920 default_configuration: SarifConfiguration {
921 level: SarifLevel::Warning,
922 },
923 },
924 SarifRule {
925 id: "SBOM-SSDF-PO3".to_string(),
926 name: "SsdfBuildMetadata".to_string(),
927 short_description: SarifMessage {
928 text: "NIST SSDF PO.3: Build provenance — build system metadata".to_string(),
929 },
930 default_configuration: SarifConfiguration {
931 level: SarifLevel::Note,
932 },
933 },
934 SarifRule {
935 id: "SBOM-SSDF-PW4".to_string(),
936 name: "SsdfDependencyManagement".to_string(),
937 short_description: SarifMessage {
938 text: "NIST SSDF PW.4: Dependency management — relationships".to_string(),
939 },
940 default_configuration: SarifConfiguration {
941 level: SarifLevel::Error,
942 },
943 },
944 SarifRule {
945 id: "SBOM-SSDF-PW6".to_string(),
946 name: "SsdfVulnerabilityInfo".to_string(),
947 short_description: SarifMessage {
948 text: "NIST SSDF PW.6: Vulnerability information and security references"
949 .to_string(),
950 },
951 default_configuration: SarifConfiguration {
952 level: SarifLevel::Note,
953 },
954 },
955 SarifRule {
956 id: "SBOM-SSDF-RV1".to_string(),
957 name: "SsdfComponentIdentification".to_string(),
958 short_description: SarifMessage {
959 text: "NIST SSDF RV.1: Component identification — unique identifiers".to_string(),
960 },
961 default_configuration: SarifConfiguration {
962 level: SarifLevel::Warning,
963 },
964 },
965 SarifRule {
966 id: "SBOM-SSDF-GENERAL".to_string(),
967 name: "SsdfGeneralRequirement".to_string(),
968 short_description: SarifMessage {
969 text: "NIST SSDF: General secure development requirement".to_string(),
970 },
971 default_configuration: SarifConfiguration {
972 level: SarifLevel::Warning,
973 },
974 },
975 ]
976}
977
978fn get_sarif_eo14028_rules() -> Vec<SarifRule> {
979 vec![
980 SarifRule {
981 id: "SBOM-EO14028-FORMAT".to_string(),
982 name: "Eo14028MachineReadable".to_string(),
983 short_description: SarifMessage {
984 text: "EO 14028 Sec 4(e): Machine-readable SBOM format requirement".to_string(),
985 },
986 default_configuration: SarifConfiguration {
987 level: SarifLevel::Error,
988 },
989 },
990 SarifRule {
991 id: "SBOM-EO14028-AUTOGEN".to_string(),
992 name: "Eo14028AutoGeneration".to_string(),
993 short_description: SarifMessage {
994 text: "EO 14028 Sec 4(e): Automated SBOM generation".to_string(),
995 },
996 default_configuration: SarifConfiguration {
997 level: SarifLevel::Warning,
998 },
999 },
1000 SarifRule {
1001 id: "SBOM-EO14028-CREATOR".to_string(),
1002 name: "Eo14028Creator".to_string(),
1003 short_description: SarifMessage {
1004 text: "EO 14028 Sec 4(e): SBOM creator identification".to_string(),
1005 },
1006 default_configuration: SarifConfiguration {
1007 level: SarifLevel::Error,
1008 },
1009 },
1010 SarifRule {
1011 id: "SBOM-EO14028-IDENTIFIER".to_string(),
1012 name: "Eo14028Identifier".to_string(),
1013 short_description: SarifMessage {
1014 text: "EO 14028 Sec 4(e): Component unique identification".to_string(),
1015 },
1016 default_configuration: SarifConfiguration {
1017 level: SarifLevel::Error,
1018 },
1019 },
1020 SarifRule {
1021 id: "SBOM-EO14028-DEPENDENCY".to_string(),
1022 name: "Eo14028Dependency".to_string(),
1023 short_description: SarifMessage {
1024 text: "EO 14028 Sec 4(e): Dependency relationship information".to_string(),
1025 },
1026 default_configuration: SarifConfiguration {
1027 level: SarifLevel::Error,
1028 },
1029 },
1030 SarifRule {
1031 id: "SBOM-EO14028-VERSION".to_string(),
1032 name: "Eo14028Version".to_string(),
1033 short_description: SarifMessage {
1034 text: "EO 14028 Sec 4(e): Component version information".to_string(),
1035 },
1036 default_configuration: SarifConfiguration {
1037 level: SarifLevel::Error,
1038 },
1039 },
1040 SarifRule {
1041 id: "SBOM-EO14028-INTEGRITY".to_string(),
1042 name: "Eo14028Integrity".to_string(),
1043 short_description: SarifMessage {
1044 text: "EO 14028 Sec 4(e): Component integrity verification (hashes)".to_string(),
1045 },
1046 default_configuration: SarifConfiguration {
1047 level: SarifLevel::Warning,
1048 },
1049 },
1050 SarifRule {
1051 id: "SBOM-EO14028-DISCLOSURE".to_string(),
1052 name: "Eo14028Disclosure".to_string(),
1053 short_description: SarifMessage {
1054 text: "EO 14028 Sec 4(g): Vulnerability disclosure process".to_string(),
1055 },
1056 default_configuration: SarifConfiguration {
1057 level: SarifLevel::Warning,
1058 },
1059 },
1060 SarifRule {
1061 id: "SBOM-EO14028-SUPPLIER".to_string(),
1062 name: "Eo14028Supplier".to_string(),
1063 short_description: SarifMessage {
1064 text: "EO 14028 Sec 4(e): Supplier identification".to_string(),
1065 },
1066 default_configuration: SarifConfiguration {
1067 level: SarifLevel::Warning,
1068 },
1069 },
1070 SarifRule {
1071 id: "SBOM-EO14028-GENERAL".to_string(),
1072 name: "Eo14028GeneralRequirement".to_string(),
1073 short_description: SarifMessage {
1074 text: "EO 14028: General SBOM requirement".to_string(),
1075 },
1076 default_configuration: SarifConfiguration {
1077 level: SarifLevel::Warning,
1078 },
1079 },
1080 ]
1081}
1082
1083fn get_sarif_compliance_rules() -> Vec<SarifRule> {
1084 vec![
1085 SarifRule {
1086 id: "SBOM-CRA-ART-13-3".to_string(),
1087 name: "CraUpdateFrequency".to_string(),
1088 short_description: SarifMessage {
1089 text: "CRA Art. 13(3): SBOM update frequency — timely regeneration after changes".to_string(),
1090 },
1091 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1092 },
1093 SarifRule {
1094 id: "SBOM-CRA-ART-13-4".to_string(),
1095 name: "CraMachineReadableFormat".to_string(),
1096 short_description: SarifMessage {
1097 text: "CRA Art. 13(4): SBOM must be in a machine-readable format (CycloneDX 1.4+ or SPDX 2.3+)".to_string(),
1098 },
1099 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1100 },
1101 SarifRule {
1102 id: "SBOM-CRA-ART-13-6".to_string(),
1103 name: "CraVulnerabilityDisclosure".to_string(),
1104 short_description: SarifMessage {
1105 text: "CRA Art. 13(6): Vulnerability disclosure contact and metadata completeness".to_string(),
1106 },
1107 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1108 },
1109 SarifRule {
1110 id: "SBOM-CRA-ART-13-5".to_string(),
1111 name: "CraLicensedComponentTracking".to_string(),
1112 short_description: SarifMessage {
1113 text: "CRA Art. 13(5): Licensed component tracking — license information for all components".to_string(),
1114 },
1115 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1116 },
1117 SarifRule {
1118 id: "SBOM-CRA-ART-13-7".to_string(),
1119 name: "CraCoordinatedDisclosure".to_string(),
1120 short_description: SarifMessage {
1121 text: "CRA Art. 13(7): Coordinated vulnerability disclosure policy reference".to_string(),
1122 },
1123 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1124 },
1125 SarifRule {
1126 id: "SBOM-CRA-ART-13-8".to_string(),
1127 name: "CraSupportPeriod".to_string(),
1128 short_description: SarifMessage {
1129 text: "CRA Art. 13(8): Support period and security update end date".to_string(),
1130 },
1131 default_configuration: SarifConfiguration { level: SarifLevel::Note },
1132 },
1133 SarifRule {
1134 id: "SBOM-CRA-ART-13-11".to_string(),
1135 name: "CraComponentLifecycle".to_string(),
1136 short_description: SarifMessage {
1137 text: "CRA Art. 13(11): Component lifecycle and end-of-support status".to_string(),
1138 },
1139 default_configuration: SarifConfiguration { level: SarifLevel::Note },
1140 },
1141 SarifRule {
1142 id: "SBOM-CRA-ART-13-12".to_string(),
1143 name: "CraProductIdentification".to_string(),
1144 short_description: SarifMessage {
1145 text: "CRA Art. 13(12): Product name and version identification".to_string(),
1146 },
1147 default_configuration: SarifConfiguration { level: SarifLevel::Error },
1148 },
1149 SarifRule {
1150 id: "SBOM-CRA-ART-13-15".to_string(),
1151 name: "CraManufacturerIdentification".to_string(),
1152 short_description: SarifMessage {
1153 text: "CRA Art. 13(15): Manufacturer identification and contact information".to_string(),
1154 },
1155 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1156 },
1157 SarifRule {
1158 id: "SBOM-CRA-ART-13-9".to_string(),
1159 name: "CraKnownVulnerabilities".to_string(),
1160 short_description: SarifMessage {
1161 text: "CRA Art. 13(9): Known vulnerabilities statement — vulnerability data or assertion".to_string(),
1162 },
1163 default_configuration: SarifConfiguration { level: SarifLevel::Note },
1164 },
1165 SarifRule {
1166 id: "SBOM-CRA-ANNEX-I".to_string(),
1167 name: "CraTechnicalDocumentation".to_string(),
1168 short_description: SarifMessage {
1169 text: "CRA Annex I: Technical documentation (unique identifiers, dependencies, primary component)".to_string(),
1170 },
1171 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1172 },
1173 SarifRule {
1174 id: "SBOM-CRA-ANNEX-III".to_string(),
1175 name: "CraDocumentIntegrity".to_string(),
1176 short_description: SarifMessage {
1177 text: "CRA Annex III: Document signature/integrity — serial number, hash, or digital signature".to_string(),
1178 },
1179 default_configuration: SarifConfiguration { level: SarifLevel::Note },
1180 },
1181 SarifRule {
1182 id: "SBOM-CRA-ANNEX-VII".to_string(),
1183 name: "CraDeclarationOfConformity".to_string(),
1184 short_description: SarifMessage {
1185 text: "CRA Annex VII: EU Declaration of Conformity reference".to_string(),
1186 },
1187 default_configuration: SarifConfiguration { level: SarifLevel::Note },
1188 },
1189 SarifRule {
1190 id: "SBOM-CRA-GENERAL".to_string(),
1191 name: "CraGeneralRequirement".to_string(),
1192 short_description: SarifMessage {
1193 text: "CRA general SBOM readiness requirement".to_string(),
1194 },
1195 default_configuration: SarifConfiguration { level: SarifLevel::Warning },
1196 },
1197 ]
1198}
1199
1200#[derive(Serialize)]
1203#[serde(rename_all = "camelCase")]
1204struct SarifReport {
1205 #[serde(rename = "$schema")]
1206 schema: String,
1207 version: String,
1208 runs: Vec<SarifRun>,
1209}
1210
1211#[derive(Serialize)]
1212#[serde(rename_all = "camelCase")]
1213struct SarifRun {
1214 tool: SarifTool,
1215 results: Vec<SarifResult>,
1216}
1217
1218#[derive(Serialize)]
1219#[serde(rename_all = "camelCase")]
1220struct SarifTool {
1221 driver: SarifDriver,
1222}
1223
1224#[derive(Serialize)]
1225#[serde(rename_all = "camelCase")]
1226struct SarifDriver {
1227 name: String,
1228 version: String,
1229 information_uri: String,
1230 rules: Vec<SarifRule>,
1231}
1232
1233#[derive(Serialize)]
1234#[serde(rename_all = "camelCase")]
1235struct SarifRule {
1236 id: String,
1237 name: String,
1238 short_description: SarifMessage,
1239 default_configuration: SarifConfiguration,
1240}
1241
1242#[derive(Serialize)]
1243#[serde(rename_all = "camelCase")]
1244struct SarifConfiguration {
1245 level: SarifLevel,
1246}
1247
1248#[derive(Serialize)]
1249#[serde(rename_all = "camelCase")]
1250struct SarifResult {
1251 rule_id: String,
1252 level: SarifLevel,
1253 message: SarifMessage,
1254 locations: Vec<SarifLocation>,
1255}
1256
1257#[derive(Serialize)]
1258#[serde(rename_all = "camelCase")]
1259struct SarifMessage {
1260 text: String,
1261}
1262
1263#[derive(Serialize)]
1264#[serde(rename_all = "camelCase")]
1265struct SarifLocation {
1266 physical_location: Option<SarifPhysicalLocation>,
1267}
1268
1269#[derive(Serialize)]
1270#[serde(rename_all = "camelCase")]
1271struct SarifPhysicalLocation {
1272 artifact_location: SarifArtifactLocation,
1273}
1274
1275#[derive(Serialize)]
1276#[serde(rename_all = "camelCase")]
1277struct SarifArtifactLocation {
1278 uri: String,
1279}
1280
1281#[derive(Serialize, Clone, Copy)]
1282#[serde(rename_all = "lowercase")]
1283enum SarifLevel {
1284 #[allow(dead_code)]
1285 None,
1286 Note,
1287 Warning,
1288 Error,
1289}