diff --git a/CHANGELOG.md b/CHANGELOG.md index 79a3ac4..b43faf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All notable changes to this project will be documented in this file. +## [1.30.0] +- Fixed validation caching for HTTP validators to include rendered headers so inactive secrets no longer appear active. + ## [1.29.0] - Fixed issue when more than 1 named capture group is used in a rule variable - Added a new liquid template filters: `b64dec` diff --git a/Cargo.toml b/Cargo.toml index e4576b5..d6e7199 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ publish = false [package] name = "kingfisher" -version = "1.29.0" +version = "1.30.0" description = "MongoDB's blazingly fast secret scanning and validation tool" edition.workspace = true rust-version.workspace = true diff --git a/install-precommit-hook.sh b/install-precommit-hook.sh index 923fb94..3f852df 100755 --- a/install-precommit-hook.sh +++ b/install-precommit-hook.sh @@ -68,6 +68,10 @@ if [[ $status -eq 200 ]]; then status=0 fi +if [[ $status -eq 205 ]]; then + status=0 +fi + if [[ $status -ne 0 ]]; then echo "Kingfisher detected secrets in staged files. Commit aborted." >&2 exit $status diff --git a/src/validation.rs b/src/validation.rs index 4823c27..7ef8c24 100644 --- a/src/validation.rs +++ b/src/validation.rs @@ -1,6 +1,7 @@ use std::{ fs, hash::{Hash, Hasher}, + collections::BTreeMap, sync::Arc, time::{Duration, Instant}, }; @@ -345,10 +346,24 @@ async fn timed_validate_single_match<'a>( // old per-request cache (optional) if !is_multipart { + let rendered_headers = httpvalidation::process_headers( + &http_validation.request.headers, + parser, + &globals, + &url, + ) + .unwrap_or_default(); + + let mut header_map = BTreeMap::new(); + for (name, value) in rendered_headers.iter() { + if let Ok(v) = value.to_str() { + header_map.insert(name.as_str().to_string(), v.to_string()); + } + } cache_key = httpvalidation::generate_http_cache_key_parts( http_validation.request.method.as_str(), &url, - &http_validation.request.headers, + &header_map, ); if let Some(cached) = cache.get(&cache_key) { let c = cached.value(); @@ -1029,116 +1044,4 @@ rules: Ok(()) } - // // ──────────────────────────────────────────────────────────────── - // // Slack Webhook – end-to-end validation test - // // ──────────────────────────────────────────────────────────────── - // #[tokio::test] - // async fn test_actual_slack_webhook_validation() -> anyhow::Result<()> { - // use std::sync::Arc; - - // use crossbeam_skiplist::SkipMap; - // use http::StatusCode; - // use rustc_hash::FxHashMap; - - // use crate::{ - // blob::BlobId, - // liquid_filters::register_all, - // location::OffsetSpan, - // matcher::{OwnedBlobMatch, SerializableCapture, SerializableCaptures}, - // rules::{ - // rule::{Confidence, Rule}, - // Rules, - // }, - // validation::{validate_single_match, Cache}, - // }; - - // // 1️⃣ YAML snippet with the **exact** Slack rule - // let slack_yaml = r#" - // rules: - // - name: Slack Webhook id: kingfisher.slack.4 pattern: | (?xi) \b ( https://hooks\.slack\.com/services/ - // T[a-z0-9_-]{8,12}/ B[a-z0-9_-]{8,12}/ [a-z0-9_-]{20,30} ) \b min_entropy: 3.3 confidence: - // medium examples: - // - https://hooks.slack.com/services/TY40v9sZ9/BxIqhIXIi/NGUyXK6nK7HMAqd0ASzXluoV - // - https://hooks.slack.com/services/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 - // validation: - // type: Http - // content: - // request: - // headers: - // Content-Type: application/json - // method: POST - // response_matcher: - // - report_response: true - // - type: WordMatch words: - // - invalid_payload - // - type: WordMatch words: - // - "invalid_token" - // negative: true - // url: "{{ TOKEN }}" - // "#; - - // // 2️⃣ Load that YAML into a Rules object - // let data = vec![(std::path::Path::new("slack_test.yaml"), slack_yaml.as_bytes())]; - // let rules = Rules::from_paths_and_contents(data, Confidence::Low)?; - - // // 3️⃣ Pull the rule syntax & wrap into a Rule - // let slack_rule_syntax = rules - // .rules - // .iter() - // .find(|r| r.id == "kingfisher.slack.4") - // .expect("Slack rule not found") - // .clone(); - // let slack_rule = Rule::new(slack_rule_syntax); - - // // 4️⃣ Provide a real-looking webhook URL (use one of the examples) - // let token = "ENTER YOUR SLACK WEBHOOK URL HERE"; - - // // 5️⃣ Build OwnedBlobMatch stub - // let blob_id = BlobId::new(&token.as_bytes()); - // let mut owned_blob_match = OwnedBlobMatch { - // rule: slack_rule.into(), - // blob_id, - // finding_fingerprint: 0, - // matching_input_offset_span: OffsetSpan { start: 0, end: token.len() }, - // captures: SerializableCaptures { - // captures: vec![SerializableCapture { - // name: Some("TOKEN".to_string()), - // match_number: -1, - // start: 0, - // end: token.len(), - // value: token.into(), - // }], - // }, - // validation_response_body: String::new(), - // validation_response_status: StatusCode::OK, - // validation_success: false, - // calculated_entropy: 5.0, - // }; - - // // 6️⃣ Prepare helpers and run validation - // let parser = register_all(liquid::ParserBuilder::with_stdlib()).build()?; - // let client = reqwest::Client::new(); - // let cache: Cache = Arc::new(SkipMap::new()); - // let dependent_vars = FxHashMap::default(); - // let missing_deps = FxHashMap::default(); - - // validate_single_match( - // &mut owned_blob_match, - // &parser, - // &client, - // &dependent_vars, - // &missing_deps, - // &cache, - // ) - // .await; - - // // 7️⃣ Inspect outcome (true ⇒ credential considered ACTIVE) - // assert!( - // owned_blob_match.validation_success, - // "Slack webhook should be reported ACTIVE; body was {:?}", - // owned_blob_match.validation_response_body - // ); - - // Ok(()) - // } } diff --git a/src/validation/httpvalidation.rs b/src/validation/httpvalidation.rs index 598da51..908de67 100644 --- a/src/validation/httpvalidation.rs +++ b/src/validation/httpvalidation.rs @@ -491,7 +491,7 @@ mod tests { } #[test] fn test_validate_response_slack_webhook() { - // 1️⃣ Build matchers equivalent to rule kingfisher.slack.4 + // 1Build matchers equivalent to rule kingfisher.slack.4 let matchers = vec![ ResponseMatcher::WordMatch { r#type: "word-match".to_string(), @@ -507,16 +507,16 @@ mod tests { }, ]; - // 2️⃣ Simulate the real Slack response you posted + // 2️Simulate the real Slack response you posted let body = "invalid_payload"; let status = StatusCode::BAD_REQUEST; // 400 let mut headers = HeaderMap::new(); headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); - // 3️⃣ Call validate_response with html_allowed = false + // 3️Call validate_response with html_allowed = false let ok = validate_response(&matchers, body, &status, &headers, false); - // 4️⃣ It *should* be valid (true) because all matcher conditions hold + // 4️It *should* be valid (true) because all matcher conditions hold assert!(ok, "Slack webhook response should be considered ACTIVE"); } }