Fixed validation caching for HTTP validators to include rendered headers so inactive secrets no longer appear active, in some cases

This commit is contained in:
Mick Grove 2025-08-01 09:15:24 -07:00
commit f48eeb79e2
5 changed files with 28 additions and 118 deletions

View file

@ -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`

View file

@ -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

View file

@ -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

View file

@ -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(())
// }
}

View file

@ -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 {
},
];
// 2Simulate the real Slack response you posted
// 2Simulate 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"));
// 3Call validate_response with html_allowed = false
// 3Call validate_response with html_allowed = false
let ok = validate_response(&matchers, body, &status, &headers, false);
// 4It *should* be valid (true) because all matcher conditions hold
// 4It *should* be valid (true) because all matcher conditions hold
assert!(ok, "Slack webhook response should be considered ACTIVE");
}
}