use serde_json::Value;
use tracing::warn;
use crate::hashing::sha256_prefixed;
use crate::{CodeLoreError, Result};
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct ExternalFinding {
pub engine: String,
pub engine_version: String,
pub rule_id: String,
pub path: String,
pub start_line: Option<u32>,
pub end_line: Option<u32>,
pub level: String,
pub fingerprint: String,
pub message: String,
}
pub fn parse_sarif_with_engines(raw: &str) -> Result<(Vec<ExternalFinding>, Vec<String>)> {
let doc: Value = serde_json::from_str(raw)
.map_err(|e| CodeLoreError::Analysis(format!("SARIF parse: not valid JSON: {e}")))?;
if doc.get("version").is_none() || doc.get("runs").is_none() {
return Err(CodeLoreError::Analysis(
"not a SARIF document: missing `version` or `runs`".into(),
));
}
let Some(runs) = doc["runs"].as_array() else {
return Err(CodeLoreError::Analysis(
"SARIF `runs` is not an array".into(),
));
};
let mut findings = Vec::new();
let mut engines: Vec<String> = Vec::new();
for run in runs {
let driver = &run["tool"]["driver"];
let engine = driver["name"].as_str().unwrap_or("unknown").to_owned();
let engine_version = driver["version"].as_str().unwrap_or("").to_owned();
if !engines.contains(&engine) {
engines.push(engine.clone());
}
let rules: Vec<&Value> = driver["rules"]
.as_array()
.map(|v| v.iter().collect())
.unwrap_or_default();
let Some(results) = run["results"].as_array() else {
continue;
};
for result in results {
match parse_result(result, &engine, &engine_version, &rules) {
Ok(finding) => findings.push(finding),
Err(reason) => {
warn!("skipping unparseable SARIF result: {reason}");
}
}
}
}
Ok((findings, engines))
}
fn parse_result(
result: &Value,
engine: &str,
engine_version: &str,
rules: &[&Value],
) -> std::result::Result<ExternalFinding, String> {
let rule_id = resolve_rule_id(result, rules)?;
let (path, start_line, end_line) = resolve_location(result)?;
let level = resolve_level(result, &rule_id, rules);
let message = result["message"]["text"].as_str().unwrap_or("").to_owned();
let fingerprint = resolve_fingerprint(result, engine, &rule_id, &path, start_line);
Ok(ExternalFinding {
engine: engine.to_owned(),
engine_version: engine_version.to_owned(),
rule_id,
path,
start_line,
end_line,
level,
fingerprint,
message,
})
}
fn resolve_rule_id(result: &Value, rules: &[&Value]) -> std::result::Result<String, String> {
if let Some(id) = result["ruleId"].as_str() {
return Ok(id.to_owned());
}
if let Some(idx) = result["ruleIndex"].as_u64() {
let idx = usize::try_from(idx).unwrap_or(usize::MAX);
if let Some(rule) = rules.get(idx)
&& let Some(id) = rule["id"].as_str()
{
return Ok(id.to_owned());
}
return Err(format!("ruleIndex {idx} out of bounds or rule has no id"));
}
Ok(String::new())
}
fn resolve_location(
result: &Value,
) -> std::result::Result<(String, Option<u32>, Option<u32>), String> {
let loc = result["locations"]
.as_array()
.and_then(|arr| arr.first())
.ok_or_else(|| "result has no locations".to_owned())?;
let uri = loc["physicalLocation"]["artifactLocation"]["uri"]
.as_str()
.ok_or_else(|| "location missing uri".to_owned())?;
let path = normalize_path(uri);
let region = &loc["physicalLocation"]["region"];
let start_line = region["startLine"]
.as_u64()
.and_then(|n| u32::try_from(n).ok());
let end_line = region["endLine"]
.as_u64()
.and_then(|n| u32::try_from(n).ok());
Ok((path, start_line, end_line))
}
fn normalize_path(uri: &str) -> String {
let without_scheme = if let Some(rest) = uri.strip_prefix("file://") {
rest
} else {
uri
};
let without_dotslash = without_scheme.strip_prefix("./").unwrap_or(without_scheme);
without_dotslash.to_owned()
}
fn resolve_level(result: &Value, rule_id: &str, rules: &[&Value]) -> String {
if let Some(lvl) = result["level"].as_str() {
return normalize_level(lvl);
}
for rule in rules {
if rule["id"].as_str() == Some(rule_id)
&& let Some(lvl) = rule["defaultConfiguration"]["level"].as_str()
{
return normalize_level(lvl);
}
}
"warning".to_owned()
}
fn normalize_level(s: &str) -> String {
match s {
"error" | "warning" | "note" | "none" => s.to_owned(),
_ => "warning".to_owned(),
}
}
fn resolve_fingerprint(
result: &Value,
engine: &str,
rule_id: &str,
path: &str,
start_line: Option<u32>,
) -> String {
if let Some(v) = result["partialFingerprints"]["primaryLocationLineHash"].as_str()
&& !v.is_empty()
{
return v.to_owned();
}
if let Some(map) = result["partialFingerprints"].as_object() {
for (_key, val) in map {
if let Some(s) = val.as_str()
&& !s.is_empty()
{
return s.to_owned();
}
}
}
if let Some(map) = result["fingerprints"].as_object() {
for (_key, val) in map {
if let Some(s) = val.as_str()
&& !s.is_empty()
{
return s.to_owned();
}
}
}
self_hash_fingerprint(engine, rule_id, path, start_line)
}
#[must_use]
pub fn self_hash_fingerprint(
engine: &str,
rule_id: &str,
path: &str,
start_line: Option<u32>,
) -> String {
let start_line_str = start_line.map(|n| n.to_string()).unwrap_or_default();
sha256_prefixed(&[engine, rule_id, path, &start_line_str])
}