This commit is contained in:
Mick Grove 2026-02-09 12:11:35 -08:00
commit 2866367c2e
18 changed files with 49 additions and 24 deletions

View file

@ -2,6 +2,9 @@
All notable changes to this project will be documented in this file.
## [v1.80.0]
- Added `--full-validation-response` flag to include complete validation response bodies without truncation. By default, validation responses are still truncated to 512 characters for readability. When enabled, users can parse and present full validation responses as needed (e.g., for GitHub token validation responses that include user metadata beyond the first 512 characters).
## [v1.79.0]
- Added revocation support for SendGrid, Tailscale, MongoDB Atlas, Twilio, and NPM using multi-step (lookup ID then delete) pattern.
- Added new Sumo Logic rule with direct revocation support.

View file

@ -19,7 +19,7 @@ publish = false
[package]
name = "kingfisher"
version = "1.79.0"
version = "1.80.0"
description = "MongoDB's blazingly fast and accurate secret scanning and validation tool"
edition.workspace = true
rust-version.workspace = true

View file

@ -6,7 +6,7 @@ rules:
\b
(
(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)
[2-7A-Z]{16}
[A-Z0-9]{16}
)
\b
pattern_requirements:

View file

@ -102,6 +102,10 @@ pub struct ScanArgs {
)]
pub validation_retries: u32,
/// Include full validation response bodies without truncation
#[arg(global = true, long, default_value_t = false)]
pub full_validation_response: bool,
/// Map validated cloud credentials to their effective identities; use only when
/// authorized for the target account because this triggers additional network
/// requests to determine granted access

View file

@ -222,9 +222,7 @@ fn render_extractor(
let template = parser
.parse(template_str)
.map_err(|e| anyhow!("Failed to parse extractor template: {}", e))?;
template
.render(globals)
.map_err(|e| anyhow!("Failed to render extractor template: {}", e))
template.render(globals).map_err(|e| anyhow!("Failed to render extractor template: {}", e))
};
match extractor {
@ -234,9 +232,7 @@ fn render_extractor(
ResponseExtractor::Regex { pattern } => {
Ok(ResponseExtractor::Regex { pattern: render(pattern)? })
}
ResponseExtractor::Header { name } => {
Ok(ResponseExtractor::Header { name: render(name)? })
}
ResponseExtractor::Header { name } => Ok(ResponseExtractor::Header { name: render(name)? }),
// Body and StatusCode have no string fields to render
other => Ok(other.clone()),
}
@ -417,8 +413,8 @@ async fn execute_revocation_step(
for (var_name, extractor) in extractors {
// Render any Liquid templates in the extractor (e.g., {{ TOKEN | prefix: 8 }})
let rendered_extractor = render_extractor(extractor, parser, globals)
.with_context(|| {
let rendered_extractor =
render_extractor(extractor, parser, globals).with_context(|| {
format!(
"Failed to render extractor template for '{}' in step {}",
var_name, step_number
@ -1252,7 +1248,10 @@ mod tests {
.unwrap();
let mut globals = Object::new();
// kingfisher:ignore (test fixture, not a real token)
globals.insert("TOKEN".into(), Value::scalar("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".to_string()));
globals.insert(
"TOKEN".into(),
Value::scalar("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".to_string()),
);
let extractor = ResponseExtractor::Regex {
pattern: r#""key":"([^"]+)","token":"{{ TOKEN | prefix: 8 }}"#.to_string(),
@ -1274,7 +1273,10 @@ mod tests {
.unwrap();
let mut globals = Object::new();
// kingfisher:ignore (test fixture, not a real token)
globals.insert("TOKEN".into(), Value::scalar("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".to_string()));
globals.insert(
"TOKEN".into(),
Value::scalar("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".to_string()),
);
let extractor = ResponseExtractor::Regex {
pattern: r#""key":"([^"]+)","token":"{{ TOKEN | prefix: 8 }}"#.to_string(),
@ -1284,13 +1286,9 @@ mod tests {
// Simulated npm API response with multiple tokens
let body = r#"{"objects":[{"key":"e089a40c-800b-4ec0-95b1-c17a63305887","token":"npm_yJcQ...rEf1"},{"key":"43c14e2d-8b5d-4f8b-91cd-280a7afead0c","token":"npm_rmll...aQIU"},{"key":"1ced5278-29a9-4266-bf8e-03223bc9c30c","token":"npm_ahWC...2pw1"}]}"#;
let result = extract_value_from_response(
&rendered,
body,
&HeaderMap::new(),
&StatusCode::OK,
)
.unwrap();
let result =
extract_value_from_response(&rendered, body, &HeaderMap::new(), &StatusCode::OK)
.unwrap();
// Should extract the key for the token matching prefix "npm_rmll", NOT the first one
assert_eq!(result, "43c14e2d-8b5d-4f8b-91cd-280a7afead0c");

View file

@ -866,6 +866,7 @@ pub(crate) fn create_minimal_scan_args() -> crate::cli::commands::scan::ScanArgs
no_ignore_if_contains: false,
validation_timeout: 10,
validation_retries: 1,
full_validation_response: false,
}
}

View file

@ -547,6 +547,7 @@ fn create_default_scan_args() -> cli::commands::scan::ScanArgs {
no_ignore_if_contains: false,
validation_timeout: 10,
validation_retries: 1,
full_validation_response: false,
}
}
/// Run the rules check command

View file

@ -662,12 +662,16 @@ impl DetailsReporter {
"Inactive Credential".to_string()
};
const MAX_RESPONSE_LENGTH: usize = 512;
let validation_body_str = validation_body::as_str(&rm.validation_response_body);
let truncated_body: String =
validation_body_str.chars().take(MAX_RESPONSE_LENGTH).collect();
let ellipsis = if validation_body_str.len() > MAX_RESPONSE_LENGTH { "..." } else { "" };
let response_body = format!("{}{}", truncated_body, ellipsis);
let response_body = if args.full_validation_response {
validation_body_str.to_string()
} else {
const MAX_RESPONSE_LENGTH: usize = 512;
let truncated_body: String =
validation_body_str.chars().take(MAX_RESPONSE_LENGTH).collect();
let ellipsis = if validation_body_str.len() > MAX_RESPONSE_LENGTH { "..." } else { "" };
format!("{}{}", truncated_body, ellipsis)
};
let git_metadata_val = rm
.origin
@ -1237,6 +1241,7 @@ mod tests {
no_ignore_if_contains: false,
validation_timeout: 10,
validation_retries: 1,
full_validation_response: false,
}
}

View file

@ -197,6 +197,7 @@ mod tests {
no_ignore_if_contains: false,
validation_timeout: 10,
validation_retries: 1,
full_validation_response: false,
}
}

View file

@ -159,6 +159,7 @@ fn run_skiplist(skip_regex: Vec<String>, skip_skipword: Vec<String>) -> Result<u
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {

View file

@ -159,6 +159,7 @@ fn test_bitbucket_remote_scan() -> Result<()> {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {

View file

@ -179,6 +179,7 @@ rules:
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {

View file

@ -166,6 +166,7 @@ fn test_github_remote_scan() -> Result<()> {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
// Create global arguments
let global_args = GlobalArgs {

View file

@ -164,6 +164,7 @@ fn test_gitlab_remote_scan() -> Result<()> {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {
@ -328,6 +329,7 @@ fn test_gitlab_remote_scan_no_history() -> Result<()> {
view_report: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {

View file

@ -142,6 +142,7 @@ async fn test_redact_hashes_finding_values() -> Result<()> {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {

View file

@ -147,6 +147,7 @@ impl TestContext {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules).load(&scan_args)?;
@ -297,6 +298,7 @@ async fn test_scan_slack_messages() -> Result<()> {
view_report: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {

View file

@ -222,6 +222,7 @@ async fn test_validation_cache_and_depvars() -> Result<()> {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
/* --------------------------------------------------------- *

View file

@ -165,6 +165,7 @@ impl TestContext {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let loaded = RuleLoader::from_rule_specifiers(&scan_args.rules)
@ -302,6 +303,7 @@ impl TestContext {
no_ignore_if_contains: false,
validation_retries: 1,
validation_timeout: 10,
full_validation_response: false,
};
let global_args = GlobalArgs {