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"));
}
}