use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use keyhog_core::{
CredentialHash, DedupedMatch, MatchLocation, MetadataSpec, ProviderEvidenceSensitivity,
SensitiveString, Severity, SuccessSpec, VerificationResult, VerifySpec,
};
use keyhog_profile::{AnnotationId, Stage};
use keyhog_verifier::testing::{
TestApi, TestVerificationCache, VerifierTestApi, VerifierTestCache,
};
use keyhog_verifier::{VerificationEngine, VerifyConfig};
fn measure(f: impl FnOnce()) -> Vec<keyhog_profile::StageMeasurement> {
keyhog_profile::reset();
let runtime = keyhog_profile::Runtime::new();
let measurements = runtime.scope(|| {
f();
keyhog_profile::take_stage_measurements()
});
keyhog_profile::reset();
measurements
}
fn stage_calls(measurements: &[keyhog_profile::StageMeasurement], stage: Stage) -> u64 {
measurements
.iter()
.filter(|measurement| measurement.stage == stage)
.map(|measurement| measurement.calls)
.sum()
}
fn offline_group() -> DedupedMatch {
DedupedMatch {
detector_id: Arc::from("profiling-test-detector"),
detector_name: Arc::from("Profiling Test Detector"),
service: Arc::from("profiling-test-service"),
severity: Severity::High,
credential: SensitiveString::from("profiling-test-secret"),
credential_hash: CredentialHash::ZERO,
companions: HashMap::new(),
primary_location: MatchLocation {
source: Arc::from("profiling-test"),
file_path: Some(Arc::from("fixture.txt")),
line: Some(1),
offset: 0,
commit: None,
author: None,
date: None,
},
additional_locations: Vec::new(),
entropy: None,
confidence: Some(0.9),
}
}
#[test]
fn cache_hit_store_and_miss_record_exact_stages() {
let measurements = measure(|| {
let cache = TestVerificationCache::new(Duration::from_secs(60));
cache.put(
"secret",
"detector",
VerificationResult::Live,
HashMap::new(),
);
assert!(cache.get("secret", "detector").is_some());
assert!(cache.get("absent", "detector").is_none());
});
assert_eq!(stage_calls(&measurements, Stage::ResultMerge), 1);
assert_eq!(stage_calls(&measurements, Stage::IncrementalLookup), 1);
}
#[test]
fn domain_allowlist_evaluation_records_suppression() {
let measurements = measure(|| {
let spec = VerifySpec {
service: "github".into(),
allowed_domains: vec![],
..VerifySpec::default()
};
assert!(TestApi
.check_url_against_spec("https://api.github.com/user", &spec)
.is_ok());
assert!(TestApi
.check_url_against_spec("https://attacker.example.com/exfil", &spec)
.is_err());
});
assert_eq!(stage_calls(&measurements, Stage::Suppression), 2);
}
#[test]
fn response_parse_sites_record_live_verification() {
let measurements = measure(|| {
assert!(TestApi.body_indicates_error_for_test("{\"error\": \"boom\"}"));
let success = SuccessSpec {
status: Some(200),
json_path: Some("$.ok".into()),
..SuccessSpec::default()
};
assert!(TestApi.evaluate_success_for_test(&success, 200, "{\"ok\": true}"));
let specs = [MetadataSpec {
name: "account_id".into(),
json_path: "$.account".into(),
sensitivity: ProviderEvidenceSensitivity::Public,
}];
assert!(TestApi
.extract_metadata_for_test(&specs, "{\"account\": \"12345\"}")
.is_ok());
let sts_body = "{\"GetCallerIdentityResponse\":{\"GetCallerIdentityResult\":\
{\"Arn\":\"arn:aws:iam::123456789012:user/test\",\
\"Account\":\"123456789012\",\"UserId\":\"AIDATEST\"}}}";
assert!(TestApi.parse_aws_sts_success_metadata(sts_body).is_ok());
let (verdict, _) = TestApi.classify_aws_sts_failure(403, "AccessDenied");
assert!(matches!(verdict, VerificationResult::Dead));
});
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 5);
}
#[test]
fn request_construction_records_live_verification() {
let measurements = measure(|| {
let (headers, body) = TestApi.built_request_header_body_for_test(
&[("authorization", "Bearer {{match}}")],
Some("token={{match}}"),
"secret",
&HashMap::new(),
);
assert_eq!(headers.len(), 1);
assert!(body.is_some());
});
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 1);
}
#[test]
fn pinned_client_build_records_live_verification() {
let measurements = measure(|| {
TestApi.clear_pinned_request_client_cache();
let addrs = [SocketAddr::from(([127, 0, 0, 1], 443))];
assert!(TestApi
.pinned_request_client_for_test(
"profiling-pin-keyhog.invalid",
&addrs,
Duration::from_millis(10),
false,
)
.is_ok());
});
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 1);
}
#[tokio::test]
async fn instrument_future_records_async_verify_work() {
keyhog_profile::reset();
let runtime = keyhog_profile::Runtime::new();
let guard = runtime.enter();
let value = keyhog_profile::instrument_future(Stage::LiveVerification, async { 7_u8 }).await;
assert_eq!(value, 7);
let measurements = keyhog_profile::take_stage_measurements();
drop(guard);
keyhog_profile::reset();
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn engine_verify_all_records_queue_and_worker_stages() {
keyhog_profile::reset();
let runtime = keyhog_profile::Runtime::new();
let guard = runtime.enter();
let engine = VerificationEngine::new(&[], VerifyConfig::default())
.expect("offline engine construction must succeed");
let findings = engine.verify_all(vec![offline_group()]).await;
let measurements = keyhog_profile::take_stage_measurements();
let (_, annotations, _) = runtime.take_session_typed_events();
drop(guard);
keyhog_profile::reset();
assert_eq!(findings.len(), 1);
assert!(matches!(
findings[0].verification,
VerificationResult::Unverifiable
));
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 3);
assert_eq!(stage_calls(&measurements, Stage::ResultMerge), 1);
assert_eq!(stage_calls(&measurements, Stage::IncrementalLookup), 0);
let queue_depths: Vec<u64> = annotations
.iter()
.filter(|annotation| annotation.annotation_id == AnnotationId::QueueDepth)
.map(|annotation| annotation.value)
.collect();
assert_eq!(queue_depths, vec![1, 1]);
}
#[tokio::test]
async fn retry_loop_records_retry_attempt_annotation() {
keyhog_profile::reset();
let runtime = keyhog_profile::Runtime::new();
let guard = runtime.enter();
let (result, _) = TestApi.retry_loop_preserves_metadata_on_exhaustion().await;
let (_, annotations, _) = runtime.take_session_typed_events();
drop(guard);
keyhog_profile::reset();
assert!(matches!(result, VerificationResult::Error(_)));
let retries: Vec<u64> = annotations
.iter()
.filter(|annotation| annotation.annotation_id == AnnotationId::RetryAttempt)
.map(|annotation| annotation.value)
.collect();
assert_eq!(retries, vec![1]);
}
#[test]
fn sync_paths_are_silent_without_runtime() {
keyhog_profile::reset();
let cache = TestVerificationCache::new(Duration::from_secs(60));
cache.put(
"secret",
"detector",
VerificationResult::Live,
HashMap::new(),
);
assert!(cache.get("secret", "detector").is_some());
let spec = VerifySpec {
service: "github".into(),
allowed_domains: vec![],
..VerifySpec::default()
};
assert!(TestApi
.check_url_against_spec("https://api.github.com/user", &spec)
.is_ok());
assert!(TestApi.body_indicates_error_for_test("{\"error\": \"boom\"}"));
let (headers, _) = TestApi.built_request_header_body_for_test(
&[("authorization", "Bearer {{match}}")],
None,
"secret",
&HashMap::new(),
);
assert_eq!(headers.len(), 1);
let measurements = keyhog_profile::take_stage_measurements();
keyhog_profile::reset();
assert_eq!(stage_calls(&measurements, Stage::IncrementalLookup), 0);
assert_eq!(stage_calls(&measurements, Stage::ResultMerge), 0);
assert_eq!(stage_calls(&measurements, Stage::Suppression), 0);
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 0);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn async_engine_path_is_silent_without_runtime() {
keyhog_profile::reset();
let engine = VerificationEngine::new(&[], VerifyConfig::default())
.expect("offline engine construction must succeed");
let findings = engine.verify_all(vec![offline_group()]).await;
assert_eq!(findings.len(), 1);
let measurements = keyhog_profile::take_stage_measurements();
keyhog_profile::reset();
assert_eq!(stage_calls(&measurements, Stage::LiveVerification), 0);
assert_eq!(stage_calls(&measurements, Stage::ResultMerge), 0);
assert_eq!(stage_calls(&measurements, Stage::IncrementalLookup), 0);
}