use gossan_core::Target;
use gossan_keyhog_lite::{Chunk, ChunkMetadata, CompiledScanner};
use secfinding::{Evidence, Finding, Severity};
use std::collections::HashMap;
use std::sync::OnceLock;
use std::sync::RwLock;
static KEYHOG_SCANNER: OnceLock<CompiledScanner> = OnceLock::new();
static RAW_STORE: OnceLock<RwLock<HashMap<String, String>>> = OnceLock::new();
pub(crate) fn store_raw_secret(hash: &str, secret: &str) {
let map = RAW_STORE.get_or_init(|| RwLock::new(HashMap::new()));
if let Ok(mut w) = map.write() {
w.insert(hash.to_string(), secret.to_string());
}
}
pub fn take_raw_secret(hash: &str) -> Option<String> {
RAW_STORE
.get()
.and_then(|map| map.write().ok().and_then(|mut w| w.remove(hash)))
}
fn get_scanner() -> Option<&'static CompiledScanner> {
KEYHOG_SCANNER.get_or_init(|| {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
let detector_dir = if !manifest_dir.is_empty() {
let path =
std::path::Path::new(&manifest_dir).join("../../../../software/keyhog/detectors");
if path.exists() {
path
} else {
std::path::PathBuf::from("../../../../software/keyhog/detectors")
}
} else {
std::path::PathBuf::from("../../../../software/keyhog/detectors")
};
let empty_fallback = || -> CompiledScanner {
match CompiledScanner::compile(Vec::new()) {
Ok(s) => s,
Err(e) => {
tracing::error!("failed to compile empty keyhog scanner: {e}");
CompiledScanner::compile(Vec::new()).unwrap_or_else(|e2| {
tracing::error!(
"keyhog scanner cannot compile even with zero detectors: {e2}"
);
std::process::exit(1);
})
}
}
};
if !detector_dir.exists() {
tracing::warn!(
"KeyHog detectors directory not found at {:?}, secret detection will be skipped",
detector_dir
);
return empty_fallback();
}
let detectors = match gossan_keyhog_lite::load_detectors(&detector_dir) {
Ok(d) => d,
Err(e) => {
tracing::error!(
"failed to load KeyHog detectors from {:?}: {e}",
detector_dir
);
return empty_fallback();
}
};
match CompiledScanner::compile(detectors) {
Ok(s) => s,
Err(e) => {
tracing::error!("failed to compile KeyHog scanner: {e}");
empty_fallback()
}
}
});
KEYHOG_SCANNER.get()
}
use sha2::{Digest, Sha256};
pub fn scan(js_url: &str, body: &str, target: &Target) -> Vec<Finding> {
let Some(scanner) = get_scanner() else {
return Vec::new();
};
let mut findings = Vec::new();
let chunk = Chunk {
data: body.to_string(),
metadata: ChunkMetadata {
source_type: "js".into(),
path: Some(js_url.to_string()),
..Default::default()
},
};
let matches = scanner.scan(&chunk);
for m in matches {
let severity = map_severity(m.severity);
let mut hasher = Sha256::new();
hasher.update(m.credential.as_bytes());
let hash = hex::encode(hasher.finalize());
store_raw_secret(&hash, &m.credential);
let builder = Finding::builder("js", target.domain().unwrap_or("?"), severity)
.title(format!("Hardcoded {} identified", m.detector_name))
.detail(format!(
"A potential {} was found in {}. Verified credentials represent a high risk of account takeover.",
m.detector_name, js_url
))
.evidence(Evidence::JsSnippet {
url: std::sync::Arc::from(js_url),
line: m.location.line.unwrap_or(0),
snippet: std::sync::Arc::from(
gossan_keyhog_lite::redact(&m.credential).as_str(),
),
})
.tag("secret")
.tag("keyhog")
.tag(format!("det:{}", m.detector_id))
.tag(format!("hash:{}", hash))
.tag(m.service.to_string())
.kind(secfinding::FindingKind::SecretLeak);
if let Some(f) = builder.build_or_log() {
findings.push(f);
}
}
findings
}
fn map_severity(s: gossan_keyhog_lite::Severity) -> Severity {
match s {
gossan_keyhog_lite::Severity::Info => Severity::Info,
gossan_keyhog_lite::Severity::Low => Severity::Low,
gossan_keyhog_lite::Severity::Medium => Severity::Medium,
gossan_keyhog_lite::Severity::High => Severity::High,
gossan_keyhog_lite::Severity::Critical => Severity::Critical,
}
}