#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
pub mod reqwest {
pub use reqwest::*;
}
pub mod bogon;
pub mod cache;
pub mod domain_allowlist;
pub mod interpolate;
pub mod oob;
pub mod rate_limit;
pub mod ssrf;
mod verify;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use dashmap::DashMap;
use keyhog_core::{redact, DedupedMatch, DetectorSpec, VerificationResult, VerifiedFinding};
use crate::reqwest::{Client, Error as ReqwestError};
pub use keyhog_core::{dedup_matches, DedupScope};
use thiserror::Error;
use tokio::sync::{Notify, Semaphore};
#[derive(Debug, Error)]
pub enum VerifyError {
#[error(
"failed to send HTTP request: {0}. Fix: check network access, proxy settings, and the verification endpoint"
)]
Http(#[from] ReqwestError),
#[error(
"failed to build configured HTTP client: {0}. Fix: use a valid timeout and supported TLS/network configuration"
)]
ClientBuild(ReqwestError),
#[error(
"invalid verifier proxy configuration: {0}. Fix: use a valid http://, https://, or socks5:// URL, or set 'off' to disable proxying entirely"
)]
ProxyConfig(String),
#[error(
"failed to resolve verification field: {0}. Fix: use `match` or `companion.<name>` fields that exist in the detector spec"
)]
FieldResolution(String),
}
pub struct VerificationEngine {
client: Client,
detectors: Arc<HashMap<Arc<str>, DetectorSpec>>,
service_semaphores: Arc<HashMap<Arc<str>, Arc<Semaphore>>>,
global_semaphore: Arc<Semaphore>,
timeout: Duration,
cache: Arc<cache::VerificationCache>,
pub(crate) inflight: Arc<DashMap<(Arc<str>, Arc<str>), Arc<Notify>>>,
pub(crate) max_inflight_keys: usize,
pub(crate) danger_allow_private_ips: bool,
pub(crate) danger_allow_http: bool,
pub(crate) insecure_tls: bool,
pub(crate) proxy_in_use: bool,
pub(crate) oob_session: Option<Arc<oob::OobSession>>,
}
pub struct VerifyConfig {
pub timeout: Duration,
pub max_concurrent_per_service: usize,
pub max_concurrent_global: usize,
pub max_inflight_keys: usize,
pub danger_allow_private_ips: bool,
pub danger_allow_http: bool,
pub proxy: Option<String>,
pub insecure_tls: bool,
}
impl Default for VerifyConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(5),
max_concurrent_per_service: 5,
max_concurrent_global: 20,
max_inflight_keys: 10_000,
danger_allow_private_ips: false,
danger_allow_http: false,
proxy: None,
insecure_tls: false,
}
}
}
pub(crate) fn apply_proxy_config(
builder: reqwest::ClientBuilder,
explicit: Option<&str>,
) -> Result<reqwest::ClientBuilder, String> {
let resolved = if let Some(p) = explicit {
Some(p.to_string())
} else {
std::env::var("KEYHOG_PROXY").ok().filter(|s| !s.is_empty())
};
match resolved.as_deref() {
Some("off") | Some("none") | Some("") => Ok(builder.no_proxy()),
Some(url) => {
let proxy = reqwest::Proxy::all(url)
.map_err(|e| format!("invalid verifier proxy URL {url:?}: {e}"))?;
Ok(builder.proxy(proxy))
}
None => Ok(builder),
}
}
pub fn proxy_is_active(explicit: Option<&str>) -> bool {
let resolved = if let Some(p) = explicit {
Some(p.to_string())
} else {
std::env::var("KEYHOG_PROXY").ok().filter(|s| !s.is_empty())
};
match resolved.as_deref() {
Some("off") | Some("none") | Some("") => return false,
Some(_) => return true,
None => {}
}
for var in [
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
"ALL_PROXY",
"all_proxy",
] {
if std::env::var(var)
.ok()
.is_some_and(|v| !v.trim().is_empty())
{
return true;
}
}
false
}
pub(crate) fn into_finding(
group: DedupedMatch,
verification: VerificationResult,
metadata: HashMap<String, String>,
) -> VerifiedFinding {
VerifiedFinding {
detector_id: group.detector_id,
detector_name: group.detector_name,
service: group.service,
severity: group.severity,
credential_redacted: redact(&group.credential),
credential_hash: group.credential_hash,
location: group.primary_location,
verification,
metadata,
additional_locations: group.additional_locations,
confidence: group.confidence,
}
}
#[doc(hidden)]
pub mod testing {
pub use crate::bogon::ip_addr_is_bogon;
pub use crate::interpolate::sanitize_oob_value;
pub use crate::interpolate::sanitize_raw_value;
pub use crate::oob::redact_interactsh_error;
pub use crate::verify::format_sigv4_timestamps;
}