diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b67a7..f230483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/Cargo.toml b/Cargo.toml index 133cf6e..8f2487c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/crates/kingfisher-rules/data/rules/aws.yml b/crates/kingfisher-rules/data/rules/aws.yml index 03d51b9..50c56f7 100644 --- a/crates/kingfisher-rules/data/rules/aws.yml +++ b/crates/kingfisher-rules/data/rules/aws.yml @@ -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: diff --git a/src/cli/commands/scan.rs b/src/cli/commands/scan.rs index 4cc6612..9cacc22 100644 --- a/src/cli/commands/scan.rs +++ b/src/cli/commands/scan.rs @@ -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 diff --git a/src/direct_revoke.rs b/src/direct_revoke.rs index 441ae54..4af8b78 100644 --- a/src/direct_revoke.rs +++ b/src/direct_revoke.rs @@ -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"); diff --git a/src/direct_validate.rs b/src/direct_validate.rs index 41c68e6..f32bbfb 100644 --- a/src/direct_validate.rs +++ b/src/direct_validate.rs @@ -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, } } diff --git a/src/main.rs b/src/main.rs index 8ed2a5e..04c1dd3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 diff --git a/src/reporter.rs b/src/reporter.rs index 9352774..bf1cd75 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -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, } } diff --git a/src/reporter/json_format.rs b/src/reporter/json_format.rs index c31f0dd..00b94c6 100644 --- a/src/reporter/json_format.rs +++ b/src/reporter/json_format.rs @@ -197,6 +197,7 @@ mod tests { no_ignore_if_contains: false, validation_timeout: 10, validation_retries: 1, + full_validation_response: false, } } diff --git a/tests/int_allowlist.rs b/tests/int_allowlist.rs index 256a5a1..76b2239 100644 --- a/tests/int_allowlist.rs +++ b/tests/int_allowlist.rs @@ -159,6 +159,7 @@ fn run_skiplist(skip_regex: Vec, skip_skipword: Vec) -> Result Result<()> { no_ignore_if_contains: false, validation_retries: 1, validation_timeout: 10, + full_validation_response: false, }; let global_args = GlobalArgs { diff --git a/tests/int_dedup.rs b/tests/int_dedup.rs index 3ed4f67..1b2d1c4 100644 --- a/tests/int_dedup.rs +++ b/tests/int_dedup.rs @@ -179,6 +179,7 @@ rules: no_ignore_if_contains: false, validation_retries: 1, validation_timeout: 10, + full_validation_response: false, }; let global_args = GlobalArgs { diff --git a/tests/int_github.rs b/tests/int_github.rs index 1ca4ebc..7aeb386 100644 --- a/tests/int_github.rs +++ b/tests/int_github.rs @@ -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 { diff --git a/tests/int_gitlab.rs b/tests/int_gitlab.rs index 2c821e4..0cbf2cd 100644 --- a/tests/int_gitlab.rs +++ b/tests/int_gitlab.rs @@ -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 { diff --git a/tests/int_redact.rs b/tests/int_redact.rs index c90f8dc..87c3879 100644 --- a/tests/int_redact.rs +++ b/tests/int_redact.rs @@ -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 { diff --git a/tests/int_slack.rs b/tests/int_slack.rs index e765230..2f9afd3 100644 --- a/tests/int_slack.rs +++ b/tests/int_slack.rs @@ -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 { diff --git a/tests/int_validation_cache.rs b/tests/int_validation_cache.rs index 3dcc498..64e1cb5 100644 --- a/tests/int_validation_cache.rs +++ b/tests/int_validation_cache.rs @@ -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, }; /* --------------------------------------------------------- * diff --git a/tests/int_vulnerable_files.rs b/tests/int_vulnerable_files.rs index b2e3ca0..76e984f 100644 --- a/tests/int_vulnerable_files.rs +++ b/tests/int_vulnerable_files.rs @@ -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 {