Skip to main content

sbom_tools/reports/
sarif.rs

1//! SARIF 2.1.0 report generator for CI/CD integration.
2
3use 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
9/// SARIF report generator
10pub struct SarifReporter {
11    /// Include informational results
12    include_info: bool,
13}
14
15impl SarifReporter {
16    /// Create a new SARIF reporter
17    #[must_use]
18    pub const fn new() -> Self {
19        Self { include_info: true }
20    }
21
22    /// Set whether to include informational results
23    #[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        // Add component change results
47        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        // Add vulnerability results
100        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        // Add license change results
158        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        // Add EOL results (from new SBOM)
176        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        // Add CRA compliance results for old and new SBOMs (use pre-computed if available)
223        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        // Report vulnerabilities in the SBOM
262        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        // Add CRA compliance results (use pre-computed if available)
291        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
343/// Generate SARIF output for multiple compliance standards merged into one report.
344pub fn generate_multi_compliance_sarif(
345    results: &[ComplianceResult],
346) -> Result<String, ReportError> {
347    // Merge rules from all standards
348    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
384/// Format SLA status for SARIF message
385fn 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
416/// Map a violation's requirement string to a specific SARIF rule ID.
417fn violation_to_rule_id(requirement: &str) -> &'static str {
418    let req = requirement.to_lowercase();
419
420    // NTIA-specific rules
421    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    // FDA-specific rules
439    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    // NIST SSDF rules
465    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    // EO 14028 rules
487    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    // CRA-specific rules (original logic)
511    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
680/// Get the appropriate compliance rules based on the standard being checked.
681fn 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// SARIF structures
1201
1202#[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}