Skip to main content

keyhog_verifier/
lib.rs

1//! Live credential verification: confirms whether detected secrets are actually
2//! active by making HTTP requests to the service's API endpoint as specified in
3//! each detector's `[detector.verify]` configuration.
4
5#![allow(clippy::too_many_arguments)]
6#![allow(clippy::type_complexity)]
7
8/// Local HTTP compatibility shim backed by reqwest..
9pub mod reqwest {
10    pub use reqwest::*;
11}
12
13/// Shared in-memory verification cache.
14pub mod cache;
15mod interpolate;
16pub mod rate_limit;
17mod ssrf;
18mod verify;
19
20use std::collections::HashMap;
21use std::sync::Arc;
22use std::time::Duration;
23
24use keyhog_core::{DedupedMatch, DetectorSpec, VerificationResult, VerifiedFinding, redact};
25use parking_lot::Mutex;
26
27// Re-export dedup types from core so existing consumers (`use keyhog_verifier::DedupedMatch`)
28// continue to work without source changes.
29use crate::reqwest::{Client, Error as ReqwestError};
30pub use keyhog_core::{DedupScope, dedup_matches};
31use thiserror::Error;
32use tokio::sync::{Notify, Semaphore};
33
34/// Errors returned while constructing or executing live verification.
35#[derive(Debug, Error)]
36pub enum VerifyError {
37    #[error(
38        "failed to send HTTP request: {0}. Fix: check network access, proxy settings, and the verification endpoint"
39    )]
40    Http(#[from] ReqwestError),
41    #[error(
42        "failed to build configured HTTP client: {0}. Fix: use a valid timeout and supported TLS/network configuration"
43    )]
44    ClientBuild(ReqwestError),
45    #[error(
46        "failed to resolve verification field: {0}. Fix: use `match` or `companion.<name>` fields that exist in the detector spec"
47    )]
48    FieldResolution(String),
49}
50
51/// Live-verification engine with shared client, cache, and concurrency limits.
52pub struct VerificationEngine {
53    client: Client,
54    detectors: Arc<HashMap<Arc<str>, DetectorSpec>>,
55    /// Per-service concurrency limit to avoid hammering APIs.
56    service_semaphores: Arc<HashMap<Arc<str>, Arc<Semaphore>>>,
57    /// Global concurrency limit.
58    global_semaphore: Arc<Semaphore>,
59    timeout: Duration,
60    /// Response cache to avoid re-verifying the same credential.
61    cache: Arc<cache::VerificationCache>,
62    /// One in-flight request per (detector_id, credential).
63    pub(crate) inflight: Arc<Mutex<HashMap<(Arc<str>, Arc<str>), Arc<Notify>>>>,
64    pub(crate) max_inflight_keys: usize,
65    pub(crate) danger_allow_private_ips: bool,
66}
67
68/// Runtime configuration for live verification.
69pub struct VerifyConfig {
70    /// End-to-end timeout for one verification attempt.
71    pub timeout: Duration,
72    /// Maximum concurrent requests allowed per service.
73    pub max_concurrent_per_service: usize,
74    /// Maximum concurrent verification tasks overall.
75    pub max_concurrent_global: usize,
76    /// Upper bound for distinct in-flight deduplication keys.
77    pub max_inflight_keys: usize,
78    /// Whether to skip SSRF protection for private IP addresses.
79    pub danger_allow_private_ips: bool,
80}
81
82impl Default for VerifyConfig {
83    fn default() -> Self {
84        Self {
85            timeout: Duration::from_secs(5),
86            max_concurrent_per_service: 5,
87            max_concurrent_global: 20,
88            max_inflight_keys: 10_000,
89            danger_allow_private_ips: false,
90        }
91    }
92}
93
94/// Convert a [`DedupedMatch`] into a [`VerifiedFinding`] with the given verification result.
95pub(crate) fn into_finding(
96    group: DedupedMatch,
97    verification: VerificationResult,
98    metadata: HashMap<String, String>,
99) -> VerifiedFinding {
100    VerifiedFinding {
101        detector_id: group.detector_id,
102        detector_name: group.detector_name,
103        service: group.service,
104        severity: group.severity,
105        credential_redacted: redact(&group.credential),
106        credential_hash: group.credential_hash,
107        location: group.primary_location,
108        verification,
109        metadata,
110        additional_locations: group.additional_locations,
111        confidence: group.confidence,
112    }
113}