use gossan_keyhog_lite::{
dedup_matches, DedupScope, MatchLocation, RawMatch, Severity as KhSeverity, VerificationEngine,
VerificationResult, VerifyConfig,
};
use secfinding::{Finding, Severity};
use std::collections::HashMap;
use std::sync::Arc;
pub struct VerifierEngine {
engine: Arc<VerificationEngine>,
}
impl Default for VerifierEngine {
fn default() -> Self {
Self::new()
}
}
impl VerifierEngine {
#[must_use]
pub fn new() -> Self {
let detectors = gossan_keyhog_lite::embedded_detectors();
let engine = VerificationEngine::new(&detectors, VerifyConfig::default())
.unwrap_or_else(|_| unreachable!("VerificationEngine::new is infallible"));
Self {
engine: Arc::new(engine),
}
}
pub async fn verify_all(&self, findings: &mut [Finding]) {
if findings.is_empty() {
return;
}
let mut raw_matches: Vec<RawMatch> = Vec::new();
for f in findings.iter() {
if !f.tags().iter().any(|t| t.as_ref() == "secret") {
continue;
}
let detector_id = f
.tags()
.iter()
.find(|t| t.starts_with("det:"))
.map(|t| t[4..].to_string());
let hash = f
.tags()
.iter()
.find(|t| t.starts_with("hash:"))
.map(|t| t[5..].to_string());
let (Some(detector_id), Some(hash)) = (detector_id, hash) else {
continue;
};
let Some(secret) = crate::secrets::take_raw_secret(&hash) else {
continue;
};
if raw_matches
.iter()
.any(|m| m.detector_id == detector_id && m.credential == secret)
{
continue;
}
raw_matches.push(RawMatch {
detector_id: detector_id.clone(),
detector_name: f.title().to_string(),
service: detector_id,
severity: map_severity(f.severity()),
credential: secret,
credential_hash: hash,
companions: HashMap::new(),
location: MatchLocation {
source: "js".into(),
file_path: Some(f.target().to_string()),
line: Some(0),
offset: 0,
commit: None,
author: None,
date: None,
},
entropy: None,
confidence: Some(1.0),
});
}
if raw_matches.is_empty() {
return;
}
let deduped = dedup_matches(raw_matches, &DedupScope::Credential);
let verified = self.engine.verify_all(deduped).await;
for vf in verified {
let hash_tag = format!("hash:{}", vf.credential_hash);
for slot in findings.iter_mut() {
let is_match = slot.tags().iter().any(|t| t.as_ref() == hash_tag.as_str());
if !is_match {
continue;
}
match &vf.verification {
VerificationResult::Live => {
let new_detail = format!(
"{}\n\n[Verification]: This credential was successfully verified as active.",
slot.detail()
);
if let Some(new_f) = rebuild_with(slot, Some(Severity::Critical), |b| {
b.tag("verified-live").detail(new_detail)
}) {
*slot = new_f;
}
}
VerificationResult::Dead => {
let new_detail = format!(
"{}\n\n[Verification]: This credential appears to be inactive or revoked.",
slot.detail()
);
if let Some(new_f) =
rebuild_with(slot, None, |b| b.tag("verified-dead").detail(new_detail))
{
*slot = new_f;
}
}
VerificationResult::Error(e) => {
let new_detail =
format!("{}\n\n[Verification Error]: {}", slot.detail(), e);
if let Some(new_f) =
rebuild_with(slot, None, |b| b.tag("verified-error").detail(new_detail))
{
*slot = new_f;
}
}
VerificationResult::Unknown => {}
}
}
}
}
}
fn rebuild_with<F>(
orig: &Finding,
severity_override: Option<Severity>,
decorate: F,
) -> Option<Finding>
where
F: FnOnce(secfinding::FindingBuilder) -> secfinding::FindingBuilder,
{
let severity = severity_override.unwrap_or_else(|| orig.severity());
let mut b = Finding::builder(
orig.scanner().to_string(),
orig.target().to_string(),
severity,
)
.title(orig.title().to_string())
.detail(orig.detail().to_string())
.kind(orig.kind());
for ev in orig.evidence() {
b = b.evidence(ev.clone());
}
for tag in orig.tags() {
let s = tag.as_ref();
if s.starts_with("raw:") || s.starts_with("det:") || s.starts_with("hash:") {
continue;
}
b = b.tag(s.to_string());
}
for cve in orig.cve_ids() {
b = b.cve(cve.to_string());
}
if let Some(hint) = orig.exploit_hint() {
b = b.exploit_hint(hint.to_string());
}
decorate(b).build().ok()
}
fn map_severity(s: Severity) -> KhSeverity {
match s {
Severity::Info => KhSeverity::Info,
Severity::Low => KhSeverity::Low,
Severity::Medium => KhSeverity::Medium,
Severity::High => KhSeverity::High,
Severity::Critical => KhSeverity::Critical,
_ => KhSeverity::High,
}
}