keyhog-verifier 0.5.43

keyhog-verifier: parallel async credential verification framework
Documentation
use keyhog_core::{dedup_matches, DedupScope, DetectorSpec, MatchLocation, RawMatch, Severity};
use keyhog_verifier::testing::{TestApi, VerifierTestApi};
use keyhog_verifier::{VerificationEngine, VerifyConfig};
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpListener;

#[tokio::test]
async fn test_verify_all_logic() {
    let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
    let addr = listener.local_addr().unwrap();
    let requests = Arc::new(AtomicUsize::new(0));
    let requests_clone = requests.clone();

    tokio::spawn(async move {
        loop {
            let Ok((mut stream, _)) = listener.accept().await else {
                break;
            };
            let count = requests_clone.clone();
            tokio::spawn(async move {
                let mut buf = [0u8; 1024];
                let _ = stream.read(&mut buf).await;
                count.fetch_add(1, Ordering::SeqCst);
                let _ = stream
                    .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK")
                    .await;
            });
        }
    });

    let detector = DetectorSpec {
        tests: Vec::new(),
        id: "test".into(),
        name: "Test".into(),
        service: "test".into(),
        severity: Severity::High,
        patterns: vec![],
        companions: vec![],
        verify: Some(keyhog_core::VerifySpec {
            service: "test".into(),
            method: Some(keyhog_core::HttpMethod::Get),
            url: Some(format!("http://127.0.0.1:{}/verify", addr.port())),
            auth: Some(keyhog_core::AuthSpec::None {}),
            headers: vec![],
            body: None,
            success: Some(keyhog_core::SuccessSpec {
                status: Some(200),
                ..Default::default()
            }),
            metadata: vec![],
            timeout_ms: None,
            steps: vec![],
            // Test mock HTTP server binds 127.0.0.1; the new domain
            // allowlist (kimi-wave1 4.1) requires the test to declare
            // its target. Production paths use the per-service builtin
            // map and never need this.
            allowed_domains: vec!["127.0.0.1".into()],
            oob: None,
        }),
        keywords: vec![],
        min_confidence: None,
        ..Default::default()
    };

    let mut config = VerifyConfig::default();
    config.danger_allow_private_ips = true;
    // Mock HTTP server is bound to 127.0.0.1, not behind TLS - opt into
    // plaintext HTTP for the test. Production paths default to HTTPS-only.
    config.danger_allow_http = true;

    let engine = VerificationEngine::new(&[detector], config).unwrap();
    let m = RawMatch {
        detector_id: "test".into(),
        detector_name: "Test".into(),
        service: "test".into(),
        severity: Severity::High,
        credential: "same-credential".into(),
        credential_hash: [0u8; 32].into(),
        companions: HashMap::new(),
        location: MatchLocation {
            source: "fs".into(),
            file_path: Some("a.txt".into()),
            line: Some(1),
            offset: 0,
            commit: None,
            author: None,
            date: None,
        },
        entropy: None,
        confidence: Some(0.9),
    };

    let group = dedup_matches(vec![m], &DedupScope::Credential)
        .pop()
        .unwrap();
    let findings = engine.verify_all(vec![group]).await;
    assert_eq!(findings.len(), 1);
    assert_eq!(requests.load(Ordering::SeqCst), 1);
    assert_eq!(
        TestApi.engine_inflight_count(&engine),
        0,
        "inflight counter must return to zero after a real verify_all request completes"
    );
}

#[test]
fn inflight_capacity_check_uses_atomic_counter_not_dashmap_len() {
    let src = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/src/verify/mod.rs"))
        .expect("verify/mod.rs must be readable");
    assert!(
        src.contains("inflight_count: Arc<AtomicUsize>"),
        "verify task shared state must carry the cheap inflight counter"
    );
    assert!(
        src.contains("fn try_reserve_inflight_slot(")
            && src.contains("compare_exchange_weak")
            && src.contains("current >= max_inflight_keys"),
        "new inflight keys must reserve capacity through an atomic compare-exchange"
    );
    assert!(
        !src.contains("inflight.len() >= max_inflight_keys"),
        "hot verification wait loop must not call DashMap::len() to enforce max_inflight_keys"
    );
    let entry_match = src
        .find("match inflight.entry(key.clone())")
        .expect("inflight entry match must exist");
    let reserve = src
        .find("try_reserve_inflight_slot(&inflight_count, max_inflight_keys)")
        .expect("atomic reserve call must exist");
    assert!(
        entry_match < reserve,
        "duplicate in-flight keys must join the existing Notify before applying the new-key cap"
    );
}