use std::sync::OnceLock;
pub fn file_path_to_sarif_uri(path: &str) -> String {
if let Some(rel) = relative_to_scan_root(path) {
return percent_encode_path(&rel);
}
if path.starts_with('/') {
format!("file://{}", percent_encode_path(path))
} else if is_windows_absolute(path) {
let normalised = path.replace('\\', "/");
format!("file:///{}", percent_encode_path(&normalised))
} else {
percent_encode_path(path)
}
}
fn scan_root() -> Option<&'static std::path::Path> {
static ROOT: OnceLock<Option<std::path::PathBuf>> = OnceLock::new();
ROOT.get_or_init(|| std::env::current_dir().ok()).as_deref()
}
fn relative_to_scan_root(path: &str) -> Option<String> {
if !path.starts_with('/') && !is_windows_absolute(path) {
return None; }
relative_to(path, scan_root()?)
}
pub fn relative_to(path: &str, root: &std::path::Path) -> Option<String> {
let normalised = path.replace('\\', "/");
std::path::Path::new(&normalised)
.strip_prefix(root)
.ok()
.map(|r| r.to_string_lossy().replace('\\', "/"))
}
pub fn apply_code_scanning_props(
props: &mut serde_json::Map<String, serde_json::Value>,
severity: crate::Severity,
) {
use crate::Severity as S;
let score = match severity {
S::Critical => "9.5",
S::High => "8.0",
S::Medium => "5.0",
S::Low => "2.0",
S::ClientSafe => "1.0",
S::Info => "0.0",
};
props.insert(
"security-severity".to_string(),
serde_json::Value::String(score.to_string()),
);
props.insert(
"tags".to_string(),
serde_json::Value::Array(vec![serde_json::Value::String("security".to_string())]),
);
}
pub fn credential_fingerprints(
credential_hash: &[u8; 32],
) -> Option<std::collections::BTreeMap<String, String>> {
if credential_hash == &[0u8; 32] {
return None;
}
let mut fp = std::collections::BTreeMap::new();
fp.insert(
"keyhog/credentialHash/v1".to_string(),
crate::finding::hex_encode(credential_hash),
);
Some(fp)
}
fn is_windows_absolute(s: &str) -> bool {
let b = s.as_bytes();
b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && (b[2] == b'/' || b[2] == b'\\')
}
fn percent_encode_path(path: &str) -> String {
let mut out = String::with_capacity(path.len());
for byte in path.bytes() {
let safe =
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b'~' | b'/' | b':');
if safe {
out.push(byte as char);
} else {
out.push('%');
const HEX: &[u8; 16] = b"0123456789ABCDEF";
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0F) as usize] as char);
}
}
out
}