use crate::models::GenericFinding;
use std::collections::BTreeMap;
fn fnv1a_hash_hex(parts: &[&str]) -> String {
const OFFSET: u64 = 14695981039346656037;
const PRIME: u64 = 1099511628211;
let mut h = OFFSET;
for part in parts {
for &b in part.as_bytes() {
h ^= b as u64;
h = h.wrapping_mul(PRIME);
}
h ^= 0xff;
h = h.wrapping_mul(PRIME);
}
format!("{:016x}", h)
}
pub(crate) fn render_json_generic(
findings: &[GenericFinding<'_>],
) -> Result<String, serde_json::Error> {
let items: Vec<_> = findings.iter().map(GenericFinding::json_value).collect();
serde_json::to_string_pretty(&items)
}
pub(crate) fn render_jsonl_generic(
findings: &[GenericFinding<'_>],
) -> Result<String, serde_json::Error> {
let mut out = Vec::with_capacity(findings.len());
for finding in findings {
out.push(serde_json::to_string(&finding.json_value())?);
}
Ok(out.join("\n"))
}
pub(crate) fn render_sarif_generic(
findings: &[GenericFinding<'_>],
tool_name: &str,
) -> Result<String, serde_json::Error> {
let mut rule_map: BTreeMap<String, serde_json::Value> = BTreeMap::new();
for f in findings {
let key = if f.rule_id.is_empty() {
format!(
"{}/{}",
f.scanner,
f.title.to_ascii_lowercase().replace(' ', "-")
)
} else {
f.rule_id.clone()
};
if !rule_map.contains_key(&key) {
let precision = match f.confidence {
Some(c) if c.is_finite() => {
if c >= 0.9 {
"high"
} else if c >= 0.5 {
"medium"
} else {
"low"
}
}
_ => "medium",
};
rule_map.insert(
key.clone(),
serde_json::json!({
"id": key,
"name": f.title,
"shortDescription": { "text": f.title },
"fullDescription": { "text": f.detail },
"help": {
"text": f.exploit_hint.unwrap_or(f.detail),
"markdown": f.exploit_hint.unwrap_or(f.detail),
},
"properties": {
"tags": f.tags,
"severity": f.severity.to_string(),
"category": format!("{:?}", f.kind),
"precision": precision,
"cwe": f.cwe_ids,
"cve": f.cve_ids,
}
}),
);
}
}
let rules: Vec<serde_json::Value> = rule_map.into_values().collect();
let results: Vec<serde_json::Value> = findings
.iter()
.map(|f| {
let rule_key = if f.rule_id.is_empty() {
format!(
"{}/{}",
f.scanner,
f.title.to_ascii_lowercase().replace(' ', "-")
)
} else {
f.rule_id.clone()
};
let fingerprint = fnv1a_hash_hex(&[&rule_key, &f.target, &f.title, &f.detail]);
serde_json::json!({
"ruleId": rule_key,
"level": f.sarif_level,
"message": { "text": format!("{}\n{}", f.title, f.detail) },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": f.target }
}
}],
"partialFingerprints": {
"primaryLocationLineHash": fingerprint,
},
"codeFlows": [{
"threadFlows": [{
"locations": [{
"location": {
"physicalLocation": {
"artifactLocation": { "uri": f.target }
},
"message": { "text": f.detail }
}
}]
}]
}],
"properties": {
"tags": f.tags,
"severity": f.severity.to_string(),
"kind": format!("{:?}", f.kind),
"confidence": f.confidence,
"cwe_ids": f.cwe_ids,
"cve_ids": f.cve_ids,
"exploit_hint": f.exploit_hint,
"evidence": f.evidence.iter().map(|e| e.to_string()).collect::<Vec<_>>(),
}
})
})
.collect();
serde_json::to_string_pretty(&serde_json::json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": { "driver": { "name": tool_name, "rules": rules } },
"results": results,
}]
}))
}