use super::shared::{bounded_findings, convert_location, report_safe};
use super::ReportContext;
use crate::model::{stable_fingerprint, AnalysisResult, Severity, SourceLocation};
use anyhow::{Context, Result};
use std::path::Path;
pub fn render_sarif(result: &AnalysisResult, root: &Path) -> Result<String> {
render_sarif_with_threshold(result, root, 0.0)
}
pub fn render_sarif_with_threshold(
result: &AnalysisResult,
root: &Path,
threshold: f64,
) -> Result<String> {
render_sarif_context(&ReportContext::new(result, root, threshold))
}
pub(super) fn render_sarif_context(context: &ReportContext<'_>) -> Result<String> {
let result = context.result;
let root = context.root;
let selected = bounded_findings(result, true);
let findings_truncated = selected.truncated;
let rules = result
.findings
.iter()
.filter(|finding| finding.is_active())
.map(|finding| finding.rule)
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.map(|rule| {
serde_json::json!({
"id": rule.to_string(),
"name": rule.to_string(),
"shortDescription": { "text": rule.to_string() },
"helpUri": "https://github.com/figueiredoluiz/cuke-dedup#rules",
})
})
.collect::<Vec<_>>();
let findings = selected
.findings
.into_iter()
.map(|finding| {
let primary = sarif_location(&finding.primary, root, None);
let related = finding
.related
.iter()
.enumerate()
.map(|(index, location)| sarif_location(location, root, Some(index + 1)))
.collect::<Vec<_>>();
let fingerprint_input = std::iter::once(&finding.primary)
.chain(&finding.related)
.map(|location| location.display(root))
.collect::<Vec<_>>()
.join("|");
serde_json::json!({
"ruleId": finding.rule.to_string(),
"level": match finding.severity {
Severity::Error => "error",
Severity::Warning => "warning",
Severity::Off => "note",
},
"message": { "text": report_safe(&finding.message) },
"locations": [primary],
"relatedLocations": related,
"partialFingerprints": {
"cukeDedupFingerprint/v1": stable_fingerprint(
&format!("{}|{fingerprint_input}", finding.rule)
)
},
"properties": {
"suggestedAction": report_safe(&finding.suggested_action),
"matcherSimilarity": finding.evidence.matcher_similarity,
"handlerSimilarity": finding.evidence.handler_similarity,
}
})
})
.collect::<Vec<_>>();
let retained_findings = findings.len();
let sarif = serde_json::json!({
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [{
"automationDetails": { "id": "cuke-dedup/" },
"columnKind": "unicodeCodePoints",
"tool": {
"driver": {
"name": "CukeDedup",
"semanticVersion": context.tool_version,
"informationUri": "https://github.com/figueiredoluiz/cuke-dedup",
"rules": rules,
}
},
"invocations": [{
"executionSuccessful": true,
"properties": {
"threshold": context.threshold,
"duplicatedDefinitions": context.summary.duplication.duplicated_definitions,
"totalDefinitions": context.summary.duplication.total_definitions,
"duplicationPercentage": context.summary.duplication.percentage,
"thresholdPassed": context.summary.duplication.passed,
"contributingRules": context.summary.duplication.rules,
"activeFindings": context.summary.findings,
"suppressedFindings": context.summary.suppressed,
"retainedFindings": retained_findings,
"findingsTruncated": findings_truncated,
}
}],
"results": findings,
}]
});
serde_json::to_string_pretty(&sarif).context("failed to serialize SARIF report")
}
fn sarif_location(location: &SourceLocation, root: &Path, id: Option<usize>) -> serde_json::Value {
let converted = convert_location(location, root);
let mut value = serde_json::json!({
"physicalLocation": {
"artifactLocation": { "uri": converted.path },
"region": {
"startLine": converted.line,
"startColumn": converted.column,
"endLine": converted.end_line,
"endColumn": converted.end_column,
}
}
});
if let Some(id) = id {
value["id"] = serde_json::json!(id);
}
value
}