xbp-analysis 10.57.0

Language-agnostic static analysis (error-handling / Aspirator-style) for XBP
Documentation
//! SARIF 2.1.0 subset for GitHub code scanning compatibility.

use crate::domain::report::AnalysisReport;
use crate::ports::outbound::DiagnosticRenderer;
use serde_json::{json, Value};

pub struct SarifRenderer;

impl DiagnosticRenderer for SarifRenderer {
    fn render(&self, report: &AnalysisReport) -> Result<String, String> {
        let mut rules = Vec::new();
        let mut seen = std::collections::BTreeSet::new();
        for f in &report.findings {
            if seen.insert(f.rule_id.clone()) {
                rules.push(json!({
                    "id": f.rule_id,
                    "name": f.rule_id,
                    "shortDescription": { "text": f.explanation },
                    "fullDescription": { "text": f.failure_scenario },
                    "help": { "text": f.remediation },
                    "defaultConfiguration": {
                        "level": severity_to_sarif(f.severity)
                    }
                }));
            }
        }

        let results: Vec<Value> = report
            .findings
            .iter()
            .map(|f| {
                json!({
                    "ruleId": f.rule_id,
                    "level": severity_to_sarif(f.severity),
                    "message": { "text": f.explanation },
                    "locations": [{
                        "physicalLocation": {
                            "artifactLocation": {
                                "uri": f.location.file.to_string_lossy()
                            },
                            "region": {
                                "startLine": f.location.start_line,
                                "startColumn": f.location.start_column.max(1),
                                "endLine": f.location.end_line,
                                "endColumn": f.location.end_column.max(1)
                            }
                        }
                    }],
                    "properties": {
                        "confidence": f.confidence.as_str(),
                        "language": f.language.as_str(),
                        "definiteBug": f.definite_bug,
                        "failureScenario": f.failure_scenario,
                        "remediation": f.remediation
                    }
                })
            })
            .collect();

        let doc = json!({
            "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
            "version": "2.1.0",
            "runs": [{
                "tool": {
                    "driver": {
                        "name": "xbp-analysis",
                        "informationUri": "https://github.com/xylex-group/xbp",
                        "rules": rules
                    }
                },
                "results": results,
                "properties": {
                    "filesAnalyzed": report.files_analyzed,
                    "durationMs": report.duration_ms
                }
            }]
        });
        serde_json::to_string_pretty(&doc).map_err(|e| e.to_string())
    }
}

fn severity_to_sarif(s: crate::domain::types::Severity) -> &'static str {
    use crate::domain::types::Severity;
    match s {
        Severity::Info | Severity::Low => "note",
        Severity::Medium => "warning",
        Severity::High | Severity::Critical => "error",
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::domain::types::{
        Confidence, Finding, LanguageId, Severity, SourceLocation,
    };

    #[test]
    fn sarif_has_schema() {
        let mut r = AnalysisReport::default();
        r.findings.push(
            Finding::builder("unsafe-termination", LanguageId::rust())
                .severity(Severity::High)
                .confidence(Confidence::High)
                .location(SourceLocation::single_line("lib.rs", 3, 5))
                .explanation("unwrap")
                .failure_scenario("panic")
                .remediation("handle")
                .build(),
        );
        let s = SarifRenderer.render(&r).unwrap();
        assert!(s.contains("2.1.0"));
        assert!(s.contains("unsafe-termination"));
    }
}