keyhog-scanner 0.5.73

keyhog-scanner: high-performance SIMD-accelerated secret detection engine
#![cfg(feature = "simd")]

use keyhog_core::{Chunk, DetectorSpec, PatternSpec, Severity};
use keyhog_scanner::{CompiledScanner, GpuInitPolicy, ScanBackend, ScannerTuningConfig};

fn scanner(backend: ScanBackend) -> CompiledScanner {
    CompiledScanner::compile_for_backend(
        vec![DetectorSpec {
            id: "simd-lazy-peer".into(),
            name: "SIMD lazy peer".into(),
            service: "test".into(),
            severity: Severity::High,
            patterns: vec![PatternSpec {
                regex: "KHSIMDLAZY_[A-Za-z0-9]{20}".into(),
                ..PatternSpec::default()
            }],
            keywords: vec!["KHSIMDLAZY".into()],
            ..keyhog_scanner::testing::named_detector_fixture_defaults()
        }],
        backend,
    )
    .expect("compile selected scanner plan")
}

#[test]
fn scalar_execution_does_not_materialize_hyperscan_but_selected_simd_does() {
    let scalar_scanner = scanner(ScanBackend::CpuFallback);
    assert!(!scalar_scanner.simd_backend_available());
    assert!(!scalar_scanner.simd_backend_initialized());

    let simd_scanner = scanner(ScanBackend::SimdCpu);
    assert!(simd_scanner.simd_backend_available());
    assert!(!simd_scanner.simd_backend_initialized());

    let chunk = Chunk::from("token=KHSIMDLAZY_A1b2C3d4E5f6G7h8I9j0");
    let scalar = scalar_scanner
        .scan_coalesced_with_backend(std::slice::from_ref(&chunk), ScanBackend::CpuFallback)
        .expect("scalar lazy-materialization scan should succeed");
    assert_eq!(scalar[0].len(), 1);
    assert!(
        !scalar_scanner.simd_backend_initialized(),
        "a scalar route must not pay Hyperscan initialization"
    );

    assert!(simd_scanner.warm_backend(ScanBackend::SimdCpu));
    assert!(simd_scanner.simd_backend_initialized());
    assert!(simd_scanner
        .simd_initialization_ns()
        .is_some_and(|ns| ns > 0));
    let simd = simd_scanner
        .scan_coalesced_with_backend(&[chunk], ScanBackend::SimdCpu)
        .expect("selected SIMD lazy-materialization scan should succeed");
    assert_eq!(simd, scalar);
}

#[test]
fn selected_simd_without_a_plan_returns_the_exact_typed_error() {
    let scanner = CompiledScanner::compile_for_backend(Vec::new(), ScanBackend::SimdCpu)
        .expect("compile empty SIMD detector corpus");
    let error = scanner
        .scan_coalesced_with_backend_and_admission(
            &[Chunk::from("abc")],
            ScanBackend::SimdCpu,
            None,
        )
        .expect_err("a selected SIMD route without a plan must fail");

    assert!(matches!(error, keyhog_scanner::ScanError::Simd(_)));
    assert!(
        error
            .to_string()
            .contains("detector corpus produced no Hyperscan phase-one plan"),
        "initialization error must preserve the exact missing-plan reason: {error}"
    );
}

#[test]
fn scalar_route_does_not_borrow_the_phase_two_hyperscan_engine() {
    let detectors = vec![
        DetectorSpec {
            id: "phase1-owner".into(),
            name: "Phase-one owner".into(),
            service: "test".into(),
            severity: Severity::High,
            patterns: vec![PatternSpec {
                regex: r"PHASE1_[A-Z0-9]{16}".into(),
                ..PatternSpec::default()
            }],
            keywords: vec!["PHASE1_".into()],
            ..keyhog_scanner::testing::named_detector_fixture_defaults()
        },
        DetectorSpec {
            id: "phase2-owner".into(),
            name: "Phase-two owner".into(),
            service: "test".into(),
            severity: Severity::High,
            patterns: vec![PatternSpec {
                regex: r"([A-Z][a-z][0-9][A-Z][a-z][0-9]{12})".into(),
                group: Some(1),
                ..PatternSpec::default()
            }],
            keywords: Vec::new(),
            ..keyhog_scanner::testing::named_detector_fixture_defaults()
        },
    ];
    let tuning = ScannerTuningConfig {
        no_candidate_gate: Some(false),
        ..ScannerTuningConfig::default()
    };
    let scalar_scanner = CompiledScanner::compile_with_gpu_policy_and_tuning(
        detectors.clone(),
        GpuInitPolicy::SelectedBackend(ScanBackend::CpuFallback),
        &tuning,
    )
    .expect("compile scalar detector plan");
    let simd_scanner = CompiledScanner::compile_with_gpu_policy_and_tuning(
        detectors,
        GpuInitPolicy::SelectedBackend(ScanBackend::SimdCpu),
        &tuning,
    )
    .expect("compile SIMD detector plan");
    let chunk = Chunk::from("value = Qx7Rt9123456789012");
    keyhog_scanner::testing::initialize_phase2_hyperscan_for_test(&simd_scanner);

    let scalar = scalar_scanner
        .scan_coalesced_with_backend(std::slice::from_ref(&chunk), ScanBackend::CpuFallback)
        .expect("scalar phase-two ownership scan should succeed");
    assert!(
        !keyhog_scanner::testing::phase2_hyperscan_initialized(&scalar_scanner),
        "the scalar route must retain the portable phase-two owner"
    );

    let simd = simd_scanner
        .scan_coalesced_with_backend(&[chunk], ScanBackend::SimdCpu)
        .expect("SIMD phase-two ownership scan should succeed");
    assert_eq!(simd, scalar);
    assert!(
        keyhog_scanner::testing::phase2_hyperscan_initialized(&simd_scanner),
        "the selected SIMD route must own its phase-two Hyperscan engine"
    );
}

#[test]
fn explicit_route_rejects_a_residual_backend_from_another_candidate() {
    let detectors =
        keyhog_core::load_embedded_detectors_or_fail().expect("embedded detector corpus must load");
    let scanner = CompiledScanner::compile(detectors).expect("compile embedded detector plan");
    let chunk = Chunk::from("const api_key = \"sk_live_0123456789abcdefghijklmnopqrstuv\";\n");
    let mismatched = keyhog_scanner::ScanExecutionRoute {
        decode_backend: ScanBackend::SimdCpu,
        ..scanner.execution_route_for_backend(ScanBackend::CpuFallback)
    };

    let error = scanner
        .scan_coalesced_with_backend_admission_and_route(
            &[chunk],
            ScanBackend::CpuFallback,
            None,
            mismatched,
        )
        .expect_err("a scalar route must not borrow SIMD residual execution");
    assert!(
        error.to_string().contains(
            "cpu-fallback route declares simd-regex residual execution, expected cpu-fallback"
        ),
        "route mismatch must identify both backends and the fix: {error}"
    );
}