1#![allow(clippy::too_many_arguments)]
6#![allow(clippy::type_complexity)]
7
8mod bogon;
9mod cache;
11mod domain_allowlist;
12mod interpolate;
13pub mod oob;
14pub mod rate_limit;
15pub mod sigv4;
16pub mod ssrf;
17mod verify;
18
19use std::collections::HashMap;
20use std::sync::atomic::AtomicUsize;
21use std::sync::Arc;
22use std::time::Duration;
23
24use dashmap::DashMap;
25use keyhog_core::{DedupedMatch, DetectorSpec, VerificationResult, VerifiedFinding};
26
27pub use keyhog_core::{dedup_matches, DedupScope};
30use reqwest::{Client, Error as ReqwestError};
31use thiserror::Error;
32use tokio::sync::{Notify, Semaphore};
33
34#[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 "invalid verifier proxy configuration: {0}. Fix: use a valid http://, https://, or socks5:// URL, or set 'off' to disable proxying entirely"
47 )]
48 ProxyConfig(String),
49 #[error(
50 "failed to resolve verification field: {0}. Fix: use `match` or `companion.<name>` fields that exist in the detector spec"
51 )]
52 FieldResolution(String),
53 #[error(
54 "invalid detector verification response contract: {0}. Fix: correct the owning detector TOML before enabling live verification"
55 )]
56 DetectorConfig(String),
57}
58
59pub struct VerificationEngine {
61 client: Client,
62 detectors: Arc<HashMap<Arc<str>, DetectorSpec>>,
63 service_semaphores: Arc<HashMap<Arc<str>, Arc<Semaphore>>>,
65 pub(crate) max_concurrent_per_service: usize,
69 global_semaphore: Arc<Semaphore>,
71 timeout: Duration,
72 cache: Arc<cache::VerificationCache>,
74 pub(crate) inflight: Arc<DashMap<cache::VerificationIdentity, Arc<Notify>>>,
78 pub(crate) inflight_count: Arc<AtomicUsize>,
79 pub(crate) max_inflight_keys: usize,
80 pub(crate) danger_allow_private_ips: bool,
81 pub(crate) danger_allow_http: bool,
82 pub(crate) insecure_tls: bool,
88 pub(crate) proxy_in_use: bool,
92 pub(crate) allow_script_verify: bool,
95 pub(crate) oob_session: Option<Arc<oob::OobSession>>,
101}
102
103pub struct VerifyConfig {
119 pub timeout: Duration,
121 pub max_concurrent_per_service: usize,
123 pub max_concurrent_global: usize,
125 pub max_inflight_keys: usize,
127 pub danger_allow_private_ips: bool,
129 pub danger_allow_http: bool,
133 pub proxy: Option<String>,
140 pub insecure_tls: bool,
144 pub allow_script_verify: bool,
150}
151
152impl Default for VerifyConfig {
153 fn default() -> Self {
154 Self {
155 timeout: Duration::from_secs(5),
156 max_concurrent_per_service: 5,
157 max_concurrent_global: 20,
158 max_inflight_keys: 10_000,
159 danger_allow_private_ips: false,
160 danger_allow_http: false,
161 proxy: None,
162 insecure_tls: false,
163 allow_script_verify: false,
164 }
165 }
166}
167
168pub(crate) fn apply_proxy_config(
177 builder: reqwest::ClientBuilder,
178 explicit: Option<&str>,
179) -> Result<reqwest::ClientBuilder, String> {
180 match resolve_proxy_mode(explicit) {
181 ProxyMode::Disabled => Ok(builder.no_proxy()),
182 ProxyMode::Explicit(url) => {
183 let proxy = reqwest::Proxy::all(&url)
184 .map_err(|e| format!("invalid verifier proxy URL {url:?}: {e}"))?;
185 Ok(builder.proxy(proxy))
186 }
187 }
188}
189
190enum ProxyMode {
191 Disabled,
192 Explicit(String),
193}
194
195fn resolve_proxy_mode(explicit: Option<&str>) -> ProxyMode {
199 match explicit {
200 Some(raw) => proxy_mode_from_raw(raw),
201 None => ProxyMode::Disabled,
202 }
203}
204
205fn proxy_mode_from_raw(raw: &str) -> ProxyMode {
206 match raw {
207 "off" | "none" | "" => ProxyMode::Disabled,
208 url => ProxyMode::Explicit(url.to_string()),
209 }
210}
211
212pub fn proxy_is_active(explicit: Option<&str>) -> bool {
223 matches!(resolve_proxy_mode(explicit), ProxyMode::Explicit(_))
224}
225
226pub(crate) const DEFAULT_HTTPS_PORT: u16 = 443;
230
231pub(crate) fn harden_verifier_client_builder(
239 builder: reqwest::ClientBuilder,
240) -> reqwest::ClientBuilder {
241 builder
242 .no_gzip()
243 .no_brotli()
244 .no_zstd()
245 .no_deflate()
246 .redirect(reqwest::redirect::Policy::none())
247}
248
249pub(crate) fn build_pinned_verifier_client(
254 host: &str,
255 pinned_addrs: &[std::net::SocketAddr],
256 timeout: std::time::Duration,
257 insecure_tls: bool,
258) -> Result<reqwest::Client, reqwest::Error> {
259 harden_verifier_client_builder(
260 reqwest::Client::builder()
261 .timeout(timeout)
262 .danger_accept_invalid_certs(insecure_tls)
263 .no_proxy(),
264 )
265 .resolve_to_addrs(host, pinned_addrs)
266 .build()
267}
268
269pub(crate) fn into_finding(
271 group: DedupedMatch,
272 verification: VerificationResult,
273 mut metadata: HashMap<String, String>,
274) -> VerifiedFinding {
275 let severity = match verification {
286 VerificationResult::Dead | VerificationResult::Revoked => group.severity.downgrade_one(),
287 _ => group.severity,
288 };
289 let credential = group.credential.as_ref();
290 metadata.retain(|_, value| {
292 (credential.is_empty() || !value.contains(credential))
293 && group
294 .companions
295 .values()
296 .all(|secret| secret.is_empty() || !value.contains(secret))
297 });
298 VerifiedFinding::from_deduped(group, severity, verification, metadata)
299}
300
301#[doc(hidden)]
303pub mod testing {
304 use std::collections::HashMap;
305 use std::sync::Arc;
306 use std::time::Duration;
307
308 pub use crate::cache::oldest_eviction_batch;
309 pub use crate::interpolate::{missing_companion_refs, MAX_TEMPLATE_TOKENS};
310 pub use crate::oob::redact_interactsh_error;
311
312 pub fn evict_oldest_dashmap_survivors_for_test(ages_secs: &[u64], count: usize) -> Vec<u64> {
321 let base = std::time::Instant::now();
322 let cache: dashmap::DashMap<u64, (std::time::Instant, ())> = dashmap::DashMap::new();
323 for &s in ages_secs {
324 cache.insert(s, (base + std::time::Duration::from_secs(s), ()));
325 }
326 crate::cache::evict_oldest_dashmap_entries(&cache, count, |(t, _)| *t);
327 let mut survivors: Vec<u64> = cache.iter().map(|e| *e.key()).collect();
328 survivors.sort_unstable();
329 survivors
330 }
331 pub use crate::verify::aws::INVALID_AWS_REGION_ERROR;
332 pub use crate::verify::credential::MAX_RETRIES_ERROR;
333 pub use crate::verify::request::{
334 invalid_url_error, CONNECTION_FAILED_ERROR, DNS_NO_ADDRESSES_ERROR, HTTPS_ONLY_ERROR,
335 PRIVATE_URL_ERROR, REDIRECT_LIMIT_ERROR, REQUEST_FAILED_ERROR, TIMEOUT_ERROR,
336 };
337
338 pub fn canonical_pinned_addrs(addrs: &[std::net::SocketAddr]) -> Vec<std::net::SocketAddr> {
344 crate::verify::request::canonical_pinned_addrs(addrs)
345 }
346 pub fn pinned_keys_equal_for_test(
347 host: &str,
348 addrs_a: &[std::net::SocketAddr],
349 addrs_b: &[std::net::SocketAddr],
350 timeout: std::time::Duration,
351 insecure_tls: bool,
352 ) -> bool {
353 crate::verify::request::pinned_keys_equal_for_test(
354 host,
355 addrs_a,
356 addrs_b,
357 timeout,
358 insecure_tls,
359 )
360 }
361
362 pub fn oob_poller_is_degraded(consecutive_errors: u32) -> bool {
368 crate::oob::poller_is_degraded(consecutive_errors)
369 }
370 pub fn oob_elapsed_verdict(poller_degraded: bool) -> crate::oob::OobObservation {
371 crate::oob::elapsed_verdict(poller_degraded)
372 }
373 pub fn oob_degraded_error_threshold() -> u32 {
374 crate::oob::OOB_DEGRADED_ERROR_THRESHOLD
375 }
376
377 pub fn aws_uri_encode(input: &str) -> String {
379 crate::sigv4::aws_uri_encode(input)
380 }
381 pub fn canonical_query_string(pairs: &[(String, String)]) -> String {
382 crate::sigv4::canonical_query_string(pairs)
383 }
384
385 pub fn oob_combined_verdict(
388 policy: keyhog_core::OobPolicy,
389 http_only_result: keyhog_core::VerificationResult,
390 http_live: bool,
391 observed: bool,
392 ) -> keyhog_core::VerificationResult {
393 crate::verify::credential::oob_combined_verdict(
394 policy,
395 http_only_result,
396 http_live,
397 observed,
398 )
399 }
400
401 pub fn body_capacity_hint(content_length: Option<u64>) -> usize {
404 crate::verify::response::body_capacity_hint(content_length)
405 }
406 pub const MAX_RESPONSE_BODY_BYTES: usize = crate::verify::response::MAX_RESPONSE_BODY_BYTES;
407 pub use crate::verify::response::{
408 BODY_NOT_UTF8_ERROR, BODY_READ_FAILED_ERROR, RESPONSE_TOO_LARGE_ERROR,
409 };
410 pub use crate::verify::tracked_join_error_preservation_for_test;
411
412 pub struct TestApi;
413
414 #[derive(Debug, Clone)]
415 pub struct TestMintedUrl {
416 pub unique_id: String,
417 pub host: String,
418 pub url: String,
419 }
420
421 fn test_minted_url(minted: crate::oob::MintedUrl) -> TestMintedUrl {
422 TestMintedUrl {
423 unique_id: minted.unique_id,
424 host: minted.host,
425 url: minted.url,
426 }
427 }
428
429 pub struct TestVerificationCache(crate::cache::VerificationCache);
430
431 pub trait VerifierTestCache {
432 fn new(ttl: Duration) -> Self;
433 fn with_max_entries(ttl: Duration, max_entries: usize) -> Self;
434 fn default_ttl() -> Self;
435 fn get(
436 &self,
437 credential: &str,
438 detector_id: &str,
439 ) -> Option<(keyhog_core::VerificationResult, HashMap<String, String>)>;
440 fn get_with_companions(
441 &self,
442 credential: &str,
443 detector_id: &str,
444 companions: &HashMap<String, String>,
445 ) -> Option<(keyhog_core::VerificationResult, HashMap<String, String>)>;
446 fn put(
447 &self,
448 credential: &str,
449 detector_id: &str,
450 result: keyhog_core::VerificationResult,
451 metadata: HashMap<String, String>,
452 );
453 fn put_with_companions(
454 &self,
455 credential: &str,
456 detector_id: &str,
457 companions: &HashMap<String, String>,
458 result: keyhog_core::VerificationResult,
459 metadata: HashMap<String, String>,
460 );
461 fn len(&self) -> usize;
462 fn queue_len(&self) -> usize;
463 fn is_empty(&self) -> bool;
464 fn evict_expired(&self);
465 fn enforce_max_entries_bound(&self);
466 fn clear_eviction_queue_for_test(&self);
467 fn insert_unqueued_for_test(
468 &self,
469 credential: &str,
470 detector_id: &str,
471 result: keyhog_core::VerificationResult,
472 metadata: HashMap<String, String>,
473 );
474 }
475
476 impl VerifierTestCache for TestVerificationCache {
477 fn new(ttl: Duration) -> Self {
478 Self(crate::cache::VerificationCache::new(ttl))
479 }
480
481 fn with_max_entries(ttl: Duration, max_entries: usize) -> Self {
482 Self(crate::cache::VerificationCache::with_max_entries(
483 ttl,
484 max_entries,
485 ))
486 }
487
488 fn default_ttl() -> Self {
489 Self(crate::cache::VerificationCache::default_ttl())
490 }
491
492 fn get(
493 &self,
494 credential: &str,
495 detector_id: &str,
496 ) -> Option<(keyhog_core::VerificationResult, HashMap<String, String>)> {
497 self.0.get(credential, detector_id)
498 }
499
500 fn get_with_companions(
501 &self,
502 credential: &str,
503 detector_id: &str,
504 companions: &HashMap<String, String>,
505 ) -> Option<(keyhog_core::VerificationResult, HashMap<String, String>)> {
506 self.0
507 .get_with_companions(credential, detector_id, companions)
508 }
509
510 fn put(
511 &self,
512 credential: &str,
513 detector_id: &str,
514 result: keyhog_core::VerificationResult,
515 metadata: HashMap<String, String>,
516 ) {
517 self.0.put(credential, detector_id, result, metadata);
518 }
519
520 fn put_with_companions(
521 &self,
522 credential: &str,
523 detector_id: &str,
524 companions: &HashMap<String, String>,
525 result: keyhog_core::VerificationResult,
526 metadata: HashMap<String, String>,
527 ) {
528 self.0
529 .put_with_companions(credential, detector_id, companions, result, metadata);
530 }
531
532 fn len(&self) -> usize {
533 self.0.len()
534 }
535
536 fn queue_len(&self) -> usize {
537 self.0.queue_len()
538 }
539
540 fn is_empty(&self) -> bool {
541 self.0.is_empty()
542 }
543
544 fn evict_expired(&self) {
545 self.0.evict_expired();
546 }
547
548 fn enforce_max_entries_bound(&self) {
549 self.0.enforce_max_entries_bound();
550 }
551
552 fn clear_eviction_queue_for_test(&self) {
553 self.0.clear_eviction_queue_for_test();
554 }
555
556 fn insert_unqueued_for_test(
557 &self,
558 credential: &str,
559 detector_id: &str,
560 result: keyhog_core::VerificationResult,
561 metadata: HashMap<String, String>,
562 ) {
563 self.0
564 .insert_unqueued_for_test(credential, detector_id, result, metadata);
565 }
566 }
567
568 pub trait VerifierTestApi {
569 const OOB_COMPANION_URL: &'static str;
570 const OOB_COMPANION_HOST: &'static str;
571 const OOB_COMPANION_ID: &'static str;
572
573 fn ip_addr_is_bogon(&self, ip: std::net::IpAddr) -> bool;
574 fn resolve_field(
575 &self,
576 field: &str,
577 credential: &str,
578 companions: &HashMap<String, String>,
579 ) -> String;
580 fn sanitize_oob_value(&self, s: &str) -> String;
581 fn sanitize_raw_value(&self, s: &str) -> String;
582 fn interpolate(
583 &self,
584 template: &str,
585 credential: &str,
586 companions: &HashMap<String, String>,
587 ) -> String;
588 fn interpolate_url(
589 &self,
590 template: &str,
591 credential: &str,
592 companions: &HashMap<String, String>,
593 ) -> String;
594 fn interpolate_http_value(
595 &self,
596 template: &str,
597 credential: &str,
598 companions: &HashMap<String, String>,
599 ) -> String;
600 fn companions_with_oob(
601 &self,
602 base: &HashMap<String, String>,
603 minted_host: &str,
604 minted_url: &str,
605 minted_id: &str,
606 ) -> HashMap<String, String>;
607 fn builtin_service_domains(
608 &self,
609 ) -> &'static HashMap<&'static str, &'static [&'static str]>;
610 fn effective_allowlist(&self, spec: &keyhog_core::VerifySpec) -> Option<Vec<String>>;
611 fn host_is_allowed(&self, host: &str, allowlist: &[String]) -> bool;
612 fn check_url_against_spec(
613 &self,
614 raw_url: &str,
615 spec: &keyhog_core::VerifySpec,
616 ) -> Result<(), String>;
617 fn engine_detector_verify_service(
618 &self,
619 engine: &crate::VerificationEngine,
620 detector_id: &str,
621 ) -> Option<String>;
622 fn engine_inflight_count(&self, engine: &crate::VerificationEngine) -> usize;
623 fn format_sigv4_timestamps(&self, unix_secs: u64) -> (String, String);
624 fn parse_aws_sts_success_metadata(
625 &self,
626 body: &str,
627 ) -> Result<HashMap<String, String>, String>;
628 fn classify_aws_sts_failure(
629 &self,
630 status: u16,
631 body: &str,
632 ) -> (keyhog_core::VerificationResult, bool);
633 fn valid_aws_format_for_test(&self, access_key: &str, secret_key: &str) -> bool;
634 fn validate_aws_region_for_test(
635 &self,
636 region: &str,
637 ) -> Result<(), keyhog_core::VerificationResult>;
638 fn build_aws_probe_final_for_test(
639 &self,
640 access_key: &str,
641 secret_key: &str,
642 region: &str,
643 ) -> impl std::future::Future<
644 Output = (
645 keyhog_core::VerificationResult,
646 HashMap<String, String>,
647 bool,
648 ),
649 > + Send;
650 fn rate_limit_feedback_sequence(&self) -> (usize, usize, usize, usize, usize);
651 fn retry_loop_records_rate_limit_feedback(
652 &self,
653 ) -> impl std::future::Future<Output = usize> + Send;
654 fn interactsh_client_for_test(
655 &self,
656 server: &str,
657 ) -> Result<crate::oob::InteractshClient, crate::oob::InteractshError>;
658 fn interactsh_client_correlation_id<'a>(
659 &self,
660 client: &'a crate::oob::InteractshClient,
661 ) -> &'a str;
662 fn interactsh_client_mint_url(
663 &self,
664 client: &crate::oob::InteractshClient,
665 ) -> TestMintedUrl;
666 fn oob_session_for_test(
667 &self,
668 client: Arc<crate::oob::InteractshClient>,
669 config: crate::oob::OobConfig,
670 ) -> Arc<crate::oob::OobSession>;
671 fn engine_set_oob_session_for_test(
672 &self,
673 engine: &mut crate::VerificationEngine,
674 session: Arc<crate::oob::OobSession>,
675 );
676 fn oob_session_mint(&self, session: &crate::oob::OobSession) -> TestMintedUrl;
677 fn oob_session_default_timeout(&self, session: &crate::oob::OobSession) -> Duration;
678 fn oob_session_set_degraded_for_test(
683 &self,
684 session: &crate::oob::OobSession,
685 degraded: bool,
686 );
687 fn oob_session_store_and_notify(
688 &self,
689 session: &crate::oob::OobSession,
690 interaction: crate::oob::Interaction,
691 );
692 fn oob_session_waiter_count(&self, session: &crate::oob::OobSession) -> usize;
693 fn oob_session_active_waiter_count(&self, session: &crate::oob::OobSession) -> usize;
694 fn oob_session_abort_poller_for_drop(&self, session: &crate::oob::OobSession);
695 fn decrypt_entry_for_test(
696 &self,
697 aes_key: &[u8],
698 b64: &str,
699 ) -> Result<Option<crate::oob::Interaction>, crate::oob::InteractshError>;
700 fn oob_collector_ssrf_check_dns_result(
701 &self,
702 server: &str,
703 resolved: std::io::Result<Vec<std::net::SocketAddr>>,
704 ) -> Result<(), crate::oob::InteractshError>;
705 fn oob_collector_reuses_proxy_client(
709 &self,
710 server: &str,
711 proxy_in_use: bool,
712 resolved: std::io::Result<Vec<std::net::SocketAddr>>,
713 ) -> Result<bool, crate::oob::InteractshError>;
714 fn rate_limiter_initial_last_request(
717 &self,
718 now: std::time::Instant,
719 interval: Duration,
720 ) -> std::time::Instant;
721 fn retry_loop_preserves_metadata_on_exhaustion(
722 &self,
723 ) -> impl std::future::Future<
724 Output = (keyhog_core::VerificationResult, HashMap<String, String>),
725 > + Send;
726 fn retry_delay_bounds_for_attempt(&self, attempt: usize, base_delay_ms: u64) -> (u64, u64);
727 fn multi_step_rate_limit_service_name<'a>(
728 &self,
729 spec: &'a keyhog_core::VerifySpec,
730 auth: &'a keyhog_core::AuthSpec,
731 ) -> &'a str;
732 fn evaluate_success_for_test(
733 &self,
734 spec: &keyhog_core::SuccessSpec,
735 status: u16,
736 body: &str,
737 ) -> bool;
738 fn evaluate_success_result_for_test(
739 &self,
740 spec: &keyhog_core::SuccessSpec,
741 status: u16,
742 body: &str,
743 ) -> Result<bool, String>;
744 fn body_indicates_error_for_test(&self, body: &str) -> bool;
745 fn extract_metadata_for_test(
746 &self,
747 specs: &[keyhog_core::MetadataSpec],
748 body: &str,
749 ) -> Result<HashMap<String, String>, String>;
750 fn retryable_http_status_for_test(&self, status: u16) -> bool;
751 fn success_spec_is_explicit_for_test(&self, spec: &keyhog_core::SuccessSpec) -> bool;
752 fn resolve_live_verdict_for_test(
753 &self,
754 is_live: bool,
755 success_is_explicit: bool,
756 body: &str,
757 ) -> bool;
758 fn record_inflight_cap_bypass_for_test(&self, max_inflight_keys: usize) -> usize;
759 fn verification_result_is_cacheable_for_test(
760 &self,
761 result: &keyhog_core::VerificationResult,
762 ) -> bool;
763 fn ssrf_check_url_with_resolved_addrs_for_test(
764 &self,
765 raw_url: &str,
766 addrs: &[std::net::SocketAddr],
767 allow_private_ips: bool,
768 ) -> Result<(), keyhog_core::VerificationResult>;
769 fn proxied_request_target_for_test(
770 &self,
771 raw_url: &str,
772 allow_private_ips: bool,
773 allow_http: bool,
774 ) -> impl std::future::Future<Output = Result<(), keyhog_core::VerificationResult>> + Send;
775 fn clear_pinned_request_client_cache(&self);
776 fn pinned_request_client_cache_len(&self) -> usize;
777 fn pinned_request_client_cache_len_for_host(&self, host: &str) -> usize;
778 fn pinned_request_client_for_test(
779 &self,
780 host: &str,
781 addrs: &[std::net::SocketAddr],
782 timeout: Duration,
783 insecure_tls: bool,
784 ) -> Result<(), keyhog_core::VerificationResult>;
785 fn build_finding(
786 &self,
787 group: keyhog_core::DedupedMatch,
788 verification: keyhog_core::VerificationResult,
789 metadata: HashMap<String, String>,
790 ) -> keyhog_core::VerifiedFinding;
791 fn built_request_header_body_for_test(
801 &self,
802 header_templates: &[(&str, &str)],
803 body_template: Option<&str>,
804 credential: &str,
805 companions: &HashMap<String, String>,
806 ) -> (Vec<(String, String)>, Option<String>);
807 }
808
809 impl VerifierTestApi for TestApi {
810 const OOB_COMPANION_URL: &'static str = crate::interpolate::OOB_COMPANION_URL;
811 const OOB_COMPANION_HOST: &'static str = crate::interpolate::OOB_COMPANION_HOST;
812 const OOB_COMPANION_ID: &'static str = crate::interpolate::OOB_COMPANION_ID;
813
814 fn ip_addr_is_bogon(&self, ip: std::net::IpAddr) -> bool {
815 crate::bogon::ip_addr_is_bogon(ip)
816 }
817
818 fn resolve_field(
819 &self,
820 field: &str,
821 credential: &str,
822 companions: &HashMap<String, String>,
823 ) -> String {
824 crate::interpolate::resolve_field(field, credential, companions)
825 }
826
827 fn sanitize_oob_value(&self, s: &str) -> String {
828 crate::interpolate::sanitize_oob_value(s)
829 }
830
831 fn sanitize_raw_value(&self, s: &str) -> String {
832 crate::interpolate::sanitize_raw_value(s)
833 }
834
835 fn interpolate(
836 &self,
837 template: &str,
838 credential: &str,
839 companions: &HashMap<String, String>,
840 ) -> String {
841 crate::interpolate::interpolate(template, credential, companions)
842 }
843
844 fn interpolate_url(
845 &self,
846 template: &str,
847 credential: &str,
848 companions: &HashMap<String, String>,
849 ) -> String {
850 crate::interpolate::interpolate_url(template, credential, companions)
851 }
852
853 fn interpolate_http_value(
854 &self,
855 template: &str,
856 credential: &str,
857 companions: &HashMap<String, String>,
858 ) -> String {
859 crate::interpolate::interpolate_http_value(template, credential, companions)
860 }
861
862 fn companions_with_oob(
863 &self,
864 base: &HashMap<String, String>,
865 minted_host: &str,
866 minted_url: &str,
867 minted_id: &str,
868 ) -> HashMap<String, String> {
869 crate::interpolate::companions_with_oob(base, minted_host, minted_url, minted_id)
870 }
871
872 fn built_request_header_body_for_test(
873 &self,
874 header_templates: &[(&str, &str)],
875 body_template: Option<&str>,
876 credential: &str,
877 companions: &HashMap<String, String>,
878 ) -> (Vec<(String, String)>, Option<String>) {
879 let client = reqwest::Client::new();
880 let builder = client.post("https://verify.example.invalid/probe");
884 let specs: Vec<keyhog_core::HeaderSpec> = header_templates
885 .iter()
886 .map(|(name, value)| keyhog_core::HeaderSpec {
887 name: (*name).to_string(),
888 value: (*value).to_string(),
889 })
890 .collect();
891 let builder = crate::verify::request::apply_header_body_templates(
892 builder,
893 &specs,
894 body_template,
895 credential,
896 companions,
897 );
898 let request = builder
899 .build()
900 .expect("a sanitized verification request must always build");
901 let headers = request
902 .headers()
903 .iter()
904 .map(|(name, value)| {
905 (
906 name.as_str().to_string(),
907 String::from_utf8_lossy(value.as_bytes()).into_owned(),
908 )
909 })
910 .collect();
911 let body = request
912 .body()
913 .and_then(reqwest::Body::as_bytes)
914 .map(|bytes| String::from_utf8_lossy(bytes).into_owned());
915 (headers, body)
916 }
917
918 fn builtin_service_domains(
919 &self,
920 ) -> &'static HashMap<&'static str, &'static [&'static str]> {
921 crate::domain_allowlist::builtin_service_domains()
922 }
923
924 fn effective_allowlist(&self, spec: &keyhog_core::VerifySpec) -> Option<Vec<String>> {
925 crate::domain_allowlist::effective_allowlist(spec)
926 }
927
928 fn host_is_allowed(&self, host: &str, allowlist: &[String]) -> bool {
929 crate::domain_allowlist::host_is_allowed(host, allowlist)
930 }
931
932 fn check_url_against_spec(
933 &self,
934 raw_url: &str,
935 spec: &keyhog_core::VerifySpec,
936 ) -> Result<(), String> {
937 crate::domain_allowlist::check_url_against_spec(raw_url, spec)
938 }
939
940 fn engine_detector_verify_service(
941 &self,
942 engine: &crate::VerificationEngine,
943 detector_id: &str,
944 ) -> Option<String> {
945 engine
946 .detectors
947 .get(detector_id)
948 .and_then(|detector| detector.verify.as_ref())
949 .map(|verify| verify.service.clone())
950 }
951
952 fn engine_inflight_count(&self, engine: &crate::VerificationEngine) -> usize {
953 engine
954 .inflight_count
955 .load(std::sync::atomic::Ordering::Acquire)
956 }
957
958 fn format_sigv4_timestamps(&self, unix_secs: u64) -> (String, String) {
959 crate::sigv4::format_sigv4_timestamps(unix_secs)
960 }
961
962 fn parse_aws_sts_success_metadata(
963 &self,
964 body: &str,
965 ) -> Result<HashMap<String, String>, String> {
966 crate::verify::parse_aws_sts_success_metadata(body)
967 }
968
969 fn classify_aws_sts_failure(
970 &self,
971 status: u16,
972 body: &str,
973 ) -> (keyhog_core::VerificationResult, bool) {
974 crate::verify::classify_aws_sts_failure(status, body)
975 }
976
977 fn valid_aws_format_for_test(&self, access_key: &str, secret_key: &str) -> bool {
978 crate::verify::valid_aws_format(access_key, secret_key)
979 }
980
981 fn validate_aws_region_for_test(
982 &self,
983 region: &str,
984 ) -> Result<(), keyhog_core::VerificationResult> {
985 crate::verify::validate_aws_region(region)
986 }
987
988 async fn build_aws_probe_final_for_test(
989 &self,
990 access_key: &str,
991 secret_key: &str,
992 region: &str,
993 ) -> (
994 keyhog_core::VerificationResult,
995 HashMap<String, String>,
996 bool,
997 ) {
998 let client = reqwest::Client::builder()
999 .no_proxy()
1000 .build()
1001 .expect("test verifier client builds");
1002 let companions = HashMap::new();
1003 match crate::verify::build_aws_probe(
1004 access_key,
1005 secret_key,
1006 &None,
1007 region,
1008 access_key,
1009 &companions,
1010 Duration::from_millis(10),
1011 &client,
1012 false,
1013 false,
1014 false,
1015 false,
1016 )
1017 .await
1018 {
1019 crate::verify::RequestBuildResult::Final {
1020 result,
1021 metadata,
1022 transient,
1023 } => (result, metadata, transient),
1024 crate::verify::RequestBuildResult::Ready(_) => {
1025 panic!("AWS probe preflight unexpectedly reached network-ready request")
1026 }
1027 }
1028 }
1029
1030 fn rate_limit_feedback_sequence(&self) -> (usize, usize, usize, usize, usize) {
1031 crate::verify::rate_limit_feedback_sequence_for_test()
1032 }
1033
1034 async fn retry_loop_records_rate_limit_feedback(&self) -> usize {
1035 crate::verify::retry_loop_records_rate_limit_feedback_for_test().await
1036 }
1037
1038 fn interactsh_client_for_test(
1039 &self,
1040 server: &str,
1041 ) -> Result<crate::oob::InteractshClient, crate::oob::InteractshError> {
1042 crate::oob::InteractshClient::for_test(server)
1043 }
1044
1045 fn interactsh_client_correlation_id<'a>(
1046 &self,
1047 client: &'a crate::oob::InteractshClient,
1048 ) -> &'a str {
1049 client.correlation_id()
1050 }
1051
1052 fn interactsh_client_mint_url(
1053 &self,
1054 client: &crate::oob::InteractshClient,
1055 ) -> TestMintedUrl {
1056 test_minted_url(client.mint_url())
1057 }
1058
1059 fn oob_session_for_test(
1060 &self,
1061 client: Arc<crate::oob::InteractshClient>,
1062 config: crate::oob::OobConfig,
1063 ) -> Arc<crate::oob::OobSession> {
1064 crate::oob::OobSession::for_test(client, config)
1065 }
1066
1067 fn engine_set_oob_session_for_test(
1068 &self,
1069 engine: &mut crate::VerificationEngine,
1070 session: Arc<crate::oob::OobSession>,
1071 ) {
1072 engine.oob_session = Some(session);
1073 }
1074
1075 fn oob_session_mint(&self, session: &crate::oob::OobSession) -> TestMintedUrl {
1076 test_minted_url(session.mint())
1077 }
1078
1079 fn oob_session_default_timeout(&self, session: &crate::oob::OobSession) -> Duration {
1080 session.config_default_timeout()
1081 }
1082
1083 fn oob_session_set_degraded_for_test(
1084 &self,
1085 session: &crate::oob::OobSession,
1086 degraded: bool,
1087 ) {
1088 session.set_degraded_for_test(degraded);
1089 }
1090
1091 fn oob_session_store_and_notify(
1092 &self,
1093 session: &crate::oob::OobSession,
1094 interaction: crate::oob::Interaction,
1095 ) {
1096 session.store_and_notify_for_test(interaction);
1097 }
1098
1099 fn oob_session_waiter_count(&self, session: &crate::oob::OobSession) -> usize {
1100 session.waiter_count_for_test()
1101 }
1102
1103 fn oob_session_active_waiter_count(&self, session: &crate::oob::OobSession) -> usize {
1104 session.active_waiter_count_for_test()
1105 }
1106
1107 fn oob_session_abort_poller_for_drop(&self, session: &crate::oob::OobSession) {
1108 session.abort_poller_for_drop();
1109 }
1110
1111 fn decrypt_entry_for_test(
1112 &self,
1113 aes_key: &[u8],
1114 b64: &str,
1115 ) -> Result<Option<crate::oob::Interaction>, crate::oob::InteractshError> {
1116 crate::oob::decrypt_entry_for_test(aes_key, b64)
1117 }
1118
1119 fn oob_collector_ssrf_check_dns_result(
1120 &self,
1121 server: &str,
1122 resolved: std::io::Result<Vec<std::net::SocketAddr>>,
1123 ) -> Result<(), crate::oob::InteractshError> {
1124 crate::oob::ssrf_check_collector_dns_result_for_test(server, resolved)
1125 }
1126
1127 fn oob_collector_reuses_proxy_client(
1128 &self,
1129 server: &str,
1130 proxy_in_use: bool,
1131 resolved: std::io::Result<Vec<std::net::SocketAddr>>,
1132 ) -> Result<bool, crate::oob::InteractshError> {
1133 crate::oob::collector_reuses_proxy_client_for_test(server, proxy_in_use, resolved)
1134 }
1135
1136 fn rate_limiter_initial_last_request(
1137 &self,
1138 now: std::time::Instant,
1139 interval: Duration,
1140 ) -> std::time::Instant {
1141 crate::rate_limit::initial_last_request(now, interval)
1142 }
1143
1144 async fn retry_loop_preserves_metadata_on_exhaustion(
1145 &self,
1146 ) -> (keyhog_core::VerificationResult, HashMap<String, String>) {
1147 crate::verify::retry_loop_preserves_metadata_on_exhaustion_for_test().await
1148 }
1149
1150 fn retry_delay_bounds_for_attempt(&self, attempt: usize, base_delay_ms: u64) -> (u64, u64) {
1151 crate::verify::retry_delay_bounds_for_attempt(attempt, base_delay_ms)
1152 }
1153
1154 fn multi_step_rate_limit_service_name<'a>(
1155 &self,
1156 spec: &'a keyhog_core::VerifySpec,
1157 auth: &'a keyhog_core::AuthSpec,
1158 ) -> &'a str {
1159 crate::verify::multi_step_rate_limit_service_name(spec, auth)
1160 }
1161
1162 fn evaluate_success_for_test(
1163 &self,
1164 spec: &keyhog_core::SuccessSpec,
1165 status: u16,
1166 body: &str,
1167 ) -> bool {
1168 crate::verify::evaluate_success(spec, status, body)
1169 .expect("success contract should evaluate cleanly")
1170 }
1171
1172 fn evaluate_success_result_for_test(
1173 &self,
1174 spec: &keyhog_core::SuccessSpec,
1175 status: u16,
1176 body: &str,
1177 ) -> Result<bool, String> {
1178 crate::verify::evaluate_success(spec, status, body).map_err(|error| error.to_string())
1179 }
1180
1181 fn body_indicates_error_for_test(&self, body: &str) -> bool {
1182 crate::verify::body_indicates_error(body)
1183 }
1184
1185 fn extract_metadata_for_test(
1186 &self,
1187 specs: &[keyhog_core::MetadataSpec],
1188 body: &str,
1189 ) -> Result<HashMap<String, String>, String> {
1190 crate::verify::extract_provider_evidence(specs, body).map_err(|error| error.to_string())
1191 }
1192
1193 fn retryable_http_status_for_test(&self, status: u16) -> bool {
1194 crate::verify::retryable_http_status(status)
1195 }
1196
1197 fn success_spec_is_explicit_for_test(&self, spec: &keyhog_core::SuccessSpec) -> bool {
1198 crate::verify::success_spec_is_explicit(spec)
1199 }
1200
1201 fn resolve_live_verdict_for_test(
1202 &self,
1203 is_live: bool,
1204 success_is_explicit: bool,
1205 body: &str,
1206 ) -> bool {
1207 crate::verify::resolve_live_verdict(is_live, success_is_explicit, body)
1208 }
1209
1210 fn record_inflight_cap_bypass_for_test(&self, max_inflight_keys: usize) -> usize {
1211 crate::verify::note_inflight_cap_bypass(max_inflight_keys)
1212 }
1213
1214 fn verification_result_is_cacheable_for_test(
1215 &self,
1216 result: &keyhog_core::VerificationResult,
1217 ) -> bool {
1218 crate::verify::verification_result_is_cacheable(result)
1219 }
1220
1221 fn ssrf_check_url_with_resolved_addrs_for_test(
1222 &self,
1223 raw_url: &str,
1224 addrs: &[std::net::SocketAddr],
1225 allow_private_ips: bool,
1226 ) -> Result<(), keyhog_core::VerificationResult> {
1227 crate::verify::ssrf_check_url_with_resolved_addrs_for_test(
1228 raw_url,
1229 addrs,
1230 allow_private_ips,
1231 )
1232 }
1233
1234 async fn proxied_request_target_for_test(
1235 &self,
1236 raw_url: &str,
1237 allow_private_ips: bool,
1238 allow_http: bool,
1239 ) -> Result<(), keyhog_core::VerificationResult> {
1240 let client = reqwest::Client::builder()
1241 .no_proxy()
1242 .build()
1243 .expect("test verifier client builds");
1244 crate::verify::resolved_client_for_url(
1245 &client,
1246 raw_url,
1247 Duration::from_millis(10),
1248 allow_private_ips,
1249 allow_http,
1250 true,
1251 false,
1252 )
1253 .await
1254 .map(|_| ())
1255 }
1256
1257 fn clear_pinned_request_client_cache(&self) {
1258 crate::verify::clear_pinned_client_cache_for_test();
1259 }
1260
1261 fn pinned_request_client_cache_len(&self) -> usize {
1262 crate::verify::pinned_client_cache_len_for_test()
1263 }
1264
1265 fn pinned_request_client_cache_len_for_host(&self, host: &str) -> usize {
1266 crate::verify::pinned_client_cache_len_for_host_for_test(host)
1267 }
1268
1269 fn pinned_request_client_for_test(
1270 &self,
1271 host: &str,
1272 addrs: &[std::net::SocketAddr],
1273 timeout: Duration,
1274 insecure_tls: bool,
1275 ) -> Result<(), keyhog_core::VerificationResult> {
1276 crate::verify::pinned_client_for_test(host, addrs, timeout, insecure_tls)
1277 }
1278
1279 fn build_finding(
1280 &self,
1281 group: keyhog_core::DedupedMatch,
1282 verification: keyhog_core::VerificationResult,
1283 metadata: HashMap<String, String>,
1284 ) -> keyhog_core::VerifiedFinding {
1285 crate::into_finding(group, verification, metadata)
1286 }
1287 }
1288}