1pub mod gitleaks;
2use crate::helper::generate_simple_id;
3pub use gitleaks::initialize_gitleaks_config;
5use gitleaks::{DetectedSecret, detect_secrets};
6use std::collections::HashMap;
7use std::fmt;
8
9#[derive(Debug, Clone)]
11pub struct RedactionResult {
12 pub redacted_string: String,
14 pub redaction_map: HashMap<String, String>,
16}
17
18impl RedactionResult {
19 pub fn new(redacted_string: String, redaction_map: HashMap<String, String>) -> Self {
20 Self {
21 redacted_string,
22 redaction_map,
23 }
24 }
25}
26
27impl fmt::Display for RedactionResult {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(f, "{}", self.redacted_string)
30 }
31}
32
33pub fn redact_secrets(
37 content: &str,
38 path: Option<&str>,
39 old_redaction_map: &HashMap<String, String>,
40 privacy_mode: bool,
41) -> RedactionResult {
42 if content.contains("[REDACTED_SECRET:") {
44 return RedactionResult::new(content.to_string(), HashMap::new());
45 }
46
47 let mut secrets = detect_secrets(content, path, privacy_mode);
48
49 let mut redaction_map = old_redaction_map.clone();
50 let mut reverse_redaction_map: HashMap<String, String> = old_redaction_map
51 .clone()
52 .into_iter()
53 .map(|(k, v)| (v, k))
54 .collect();
55
56 for (original_secret, redaction_key) in &reverse_redaction_map {
57 let key_parts = redaction_key.split(':').collect::<Vec<&str>>();
59 if key_parts.len() == 3 {
60 let rule_id = key_parts[1].to_string();
61 if let Some(start) = content.find(original_secret) {
62 let end = start + original_secret.len();
63 secrets.push(DetectedSecret {
64 rule_id,
65 value: original_secret.clone(),
66 start_pos: start,
67 end_pos: end,
68 });
69 }
70 }
71 }
72
73 if secrets.is_empty() {
74 return RedactionResult::new(content.to_string(), HashMap::new());
75 }
76
77 let mut redacted_string = content.to_string();
78
79 let mut deduplicated_secrets: Vec<DetectedSecret> = Vec::new();
81 let mut sorted_by_start = secrets;
82 sorted_by_start.sort_by(|a, b| a.start_pos.cmp(&b.start_pos));
83
84 for secret in sorted_by_start {
85 let mut should_add = true;
86 let mut to_remove = Vec::new();
87
88 for (i, existing) in deduplicated_secrets.iter().enumerate() {
89 let overlaps =
91 secret.start_pos < existing.end_pos && secret.end_pos > existing.start_pos;
92
93 if overlaps {
94 if secret.value.len() > existing.value.len() {
96 to_remove.push(i);
97 } else {
98 should_add = false;
99 break;
100 }
101 }
102 }
103
104 for &i in to_remove.iter().rev() {
106 deduplicated_secrets.remove(i);
107 }
108
109 if should_add {
110 deduplicated_secrets.push(secret);
111 }
112 }
113
114 deduplicated_secrets.sort_by(|a, b| b.start_pos.cmp(&a.start_pos));
116
117 for secret in deduplicated_secrets {
118 if !content.is_char_boundary(secret.start_pos) || !content.is_char_boundary(secret.end_pos)
120 {
121 continue;
122 }
123
124 if secret.start_pos >= redacted_string.len() || secret.end_pos > redacted_string.len() {
126 continue;
127 }
128
129 let redaction_key = if let Some(existing_key) = reverse_redaction_map.get(&secret.value) {
132 existing_key.clone()
133 } else {
134 let key = generate_redaction_key(&secret.rule_id);
135 redaction_map.insert(key.clone(), secret.value.clone());
137 reverse_redaction_map.insert(secret.value, key.clone());
138 key
139 };
140
141 redacted_string.replace_range(secret.start_pos..secret.end_pos, &redaction_key);
143 }
144
145 RedactionResult::new(redacted_string, redaction_map)
146}
147
148pub fn restore_secrets(redacted_string: &str, redaction_map: &HashMap<String, String>) -> String {
150 let mut restored = redacted_string.to_string();
151
152 for (redaction_key, original_value) in redaction_map {
153 restored = restored.replace(redaction_key, original_value);
154 }
155
156 restored
157}
158
159pub fn redact_password(
161 content: &str,
162 password: &str,
163 old_redaction_map: &HashMap<String, String>,
164) -> RedactionResult {
165 if password.is_empty() {
166 return RedactionResult::new(content.to_string(), HashMap::new());
167 }
168
169 if content.contains("[REDACTED_SECRET:") {
171 return RedactionResult::new(content.to_string(), HashMap::new());
172 }
173
174 let mut redacted_string = content.to_string();
175 let mut redaction_map = old_redaction_map.clone();
176 let mut reverse_redaction_map: HashMap<String, String> = old_redaction_map
177 .clone()
178 .into_iter()
179 .map(|(k, v)| (v, k))
180 .collect();
181
182 let redaction_key = if let Some(existing_key) = reverse_redaction_map.get(password) {
184 existing_key.clone()
185 } else {
186 let key = generate_redaction_key("password");
187 redaction_map.insert(key.clone(), password.to_string());
189 reverse_redaction_map.insert(password.to_string(), key.clone());
190 key
191 };
192
193 redacted_string = redacted_string.replace(password, &redaction_key);
195
196 RedactionResult::new(redacted_string, redaction_map)
197}
198
199fn generate_redaction_key(rule_id: &str) -> String {
201 let id = generate_simple_id(6);
202 format!("[REDACTED_SECRET:{rule_id}:{id}]")
203}
204
205#[cfg(test)]
206mod tests {
207 use regex::Regex;
208
209 use crate::secrets::gitleaks::{
210 GITLEAKS_CONFIG, calculate_entropy, contains_any_keyword, create_simple_api_key_regex,
211 is_allowed_by_rule_allowlist, should_allow_match,
212 };
213
214 use super::*;
215
216 fn fake_aws_access_key() -> String {
217 ["AKIA", "IOSFODNN7EX23PLE"].concat()
218 }
219
220 fn fake_aws_access_key_alt() -> String {
221 ["AKIA", "IOSFODNN7REALKEY"].concat()
222 }
223
224 fn fake_aws_access_key_example() -> String {
225 ["AKIA", "IOSFODNN7EXAMPLE"].concat()
226 }
227
228 fn fake_github_token() -> String {
229 ["ghp", "_1234567890abcdef", "1234567890abcdef", "12345678"].concat()
230 }
231
232 fn fake_github_token_short() -> String {
233 ["ghp", "_1234567890abcdef"].concat()
234 }
235
236 fn fake_api_key_long() -> String {
237 ["abc123def456", "ghi789jkl012", "mno345pqr678"].concat()
238 }
239
240 fn fake_api_key() -> String {
241 ["abc123def456", "ghi789jklmnop"].concat()
242 }
243
244 fn fake_secret_token() -> String {
245 ["Kx9mP2nQ8rT4", "vW7yZ3cF6hJ1", "lN5sA0bD8eF"].concat()
246 }
247
248 fn fake_secret_token_long() -> String {
249 ["Kx9mP2nQ8rT4", "vW7yZ3cF6hJ1", "lN5sA0bD8eF2gH5jK"].concat()
250 }
251
252 fn fake_password_secret() -> String {
253 ["super", "secret", "password", "123456"].concat()
254 }
255
256 #[test]
257 fn test_redaction_key_generation() {
258 let key1 = generate_redaction_key("test");
259 let key2 = generate_redaction_key("my-rule");
260
261 assert_ne!(key1, key2);
263
264 assert!(key1.starts_with("[REDACTED_SECRET:test:"));
266 assert!(key1.ends_with("]"));
267 assert!(key2.starts_with("[REDACTED_SECRET:my-rule:"));
268 assert!(key2.ends_with("]"));
269 }
270
271 #[test]
272 fn test_empty_input() {
273 let result = redact_secrets("", None, &HashMap::new(), false);
274 assert_eq!(result.redacted_string, "");
275 assert!(result.redaction_map.is_empty());
276 }
277
278 #[test]
279 fn test_restore_secrets() {
280 let mut redaction_map = HashMap::new();
281 redaction_map.insert("[REDACTED_abc123]".to_string(), "secret123".to_string());
282 redaction_map.insert("[REDACTED_def456]".to_string(), "api_key_xyz".to_string());
283
284 let redacted = "Password is [REDACTED_abc123] and key is [REDACTED_def456]";
285 let restored = restore_secrets(redacted, &redaction_map);
286
287 assert_eq!(restored, "Password is secret123 and key is api_key_xyz");
288 }
289
290 #[test]
291 fn test_redaction_result_display() {
292 let mut redaction_map = HashMap::new();
293 redaction_map.insert("[REDACTED_test]".to_string(), "secret".to_string());
294
295 let result = RedactionResult::new("Hello [REDACTED_test]".to_string(), redaction_map);
296 assert_eq!(format!("{}", result), "Hello [REDACTED_test]");
297 }
298
299 #[test]
300 fn test_redact_secrets_with_api_key() {
301 let input = format!("export API_KEY={}", fake_api_key_long());
303 let result = redact_secrets(&input, None, &HashMap::new(), false);
304
305 assert!(!result.redaction_map.is_empty());
307 assert!(result.redacted_string.contains("[REDACTED_"));
308 println!("Input: {}", input);
309 println!("Redacted: {}", result.redacted_string);
310 println!("Mapping: {:?}", result.redaction_map);
311 }
312
313 #[test]
314 fn test_redact_secrets_with_aws_key() {
315 let input = format!("AWS_ACCESS_KEY_ID={}", fake_aws_access_key());
316 let result = redact_secrets(&input, None, &HashMap::new(), false);
317
318 assert!(!result.redaction_map.is_empty());
320 println!("Input: {}", input);
321 println!("Redacted: {}", result.redacted_string);
322 println!("Mapping: {:?}", result.redaction_map);
323 }
324
325 #[test]
326 fn test_redaction_identical_secrets() {
327 let aws_key = fake_aws_access_key();
328 let input = format!(
329 "\n export AWS_ACCESS_KEY_ID={aws_key}\n export AWS_ACCESS_KEY_ID_2={aws_key}\n "
330 );
331 let result = redact_secrets(&input, None, &HashMap::new(), false);
332
333 assert_eq!(result.redaction_map.len(), 1);
334 }
335
336 #[test]
337 fn test_redaction_identical_secrets_different_contexts() {
338 let aws_key = fake_aws_access_key();
339 let input_1 = format!("\n export AWS_ACCESS_KEY_ID={aws_key}\n ");
340 let input_2 = format!("\n export SOME_OTHER_SECRET={aws_key}\n ");
341 let result_1 = redact_secrets(&input_1, None, &HashMap::new(), false);
342 let result_2 = redact_secrets(&input_2, None, &result_1.redaction_map, false);
343
344 assert_eq!(result_1.redaction_map, result_2.redaction_map);
345 }
346
347 #[test]
348 fn test_redact_secrets_with_github_token() {
349 let input = format!("GITHUB_TOKEN={}", fake_github_token());
350 let result = redact_secrets(&input, None, &HashMap::new(), false);
351
352 assert!(!result.redaction_map.is_empty());
354 println!("Input: {}", input);
355 println!("Redacted: {}", result.redacted_string);
356 println!("Mapping: {:?}", result.redaction_map);
357 }
358
359 #[test]
360 fn test_no_secrets() {
361 let input = "This is just a normal string with no secrets";
362 let result = redact_secrets(input, None, &HashMap::new(), false);
363
364 assert_eq!(result.redaction_map.len(), 0);
366 assert_eq!(result.redacted_string, input);
367 }
368
369 #[test]
370 fn test_debug_generic_api_key() {
371 let config = &*GITLEAKS_CONFIG;
372
373 let generic_rule = config.rules.iter().find(|r| r.id == "generic-api-key");
375 if let Some(rule) = generic_rule {
376 println!("Generic API Key Rule:");
377 println!(" Regex: {:?}", rule.regex);
378 println!(" Entropy: {:?}", rule.entropy);
379 println!(" Keywords: {:?}", rule.keywords);
380
381 if let Some(regex_pattern) = &rule.regex {
383 if let Ok(regex) = Regex::new(regex_pattern) {
384 let test_input = format!("API_KEY={}", fake_api_key_long());
385 println!("\nTesting regex directly:");
386 println!(" Input: {}", test_input);
387
388 for mat in regex.find_iter(&test_input) {
389 println!(" Raw match: '{}'", mat.as_str());
390 println!(" Match position: {}-{}", mat.start(), mat.end());
391
392 if let Some(captures) = regex.captures(mat.as_str()) {
394 for (i, cap) in captures.iter().enumerate() {
395 if let Some(cap) = cap {
396 println!(" Capture {}: '{}'", i, cap.as_str());
397 if i == 1 {
398 let entropy = calculate_entropy(cap.as_str());
399 println!(" Entropy of capture 1: {:.2}", entropy);
400 }
401 }
402 }
403 }
404 }
405 }
406 } else {
407 println!(" No regex pattern (path-based rule)");
408 }
409
410 let test_inputs = vec![
412 format!("API_KEY={}", fake_api_key_long()),
413 "api_key=RaNd0mH1ghEnTr0pyV4luE567890abcdef".to_string(),
414 format!("access_key={}", fake_secret_token_long()),
415 "secret_token=1234567890abcdef1234567890abcdef".to_string(),
416 "password=9k2L8pMvB3nQ7rX1ZdF5GhJwY4AsPo6C".to_string(),
417 ];
418
419 for input in test_inputs {
420 println!("\nTesting input: {}", input);
421 let result = redact_secrets(&input, None, &HashMap::new(), false);
422 println!(" Detected secrets: {}", result.redaction_map.len());
423 if !result.redaction_map.is_empty() {
424 println!(" Redacted: {}", result.redacted_string);
425 }
426 }
427 } else {
428 println!("Generic API key rule not found!");
429 }
430 }
431
432 #[test]
433 fn test_simple_regex_match() {
434 let input = "key=abcdefghijklmnop";
436 println!("Testing simple input: {}", input);
437
438 let config = &*GITLEAKS_CONFIG;
439 let generic_rule = config
440 .rules
441 .iter()
442 .find(|r| r.id == "generic-api-key")
443 .unwrap();
444
445 if let Some(regex_pattern) = &generic_rule.regex {
446 if let Ok(regex) = Regex::new(regex_pattern) {
447 println!("Regex pattern: {}", regex_pattern);
448
449 if regex.is_match(input) {
450 println!("✓ Regex MATCHES the input!");
451
452 for mat in regex.find_iter(input) {
453 println!("Match found: '{}'", mat.as_str());
454
455 if let Some(captures) = regex.captures(mat.as_str()) {
456 println!("Full capture groups:");
457 for (i, cap) in captures.iter().enumerate() {
458 if let Some(cap) = cap {
459 println!(" Group {}: '{}'", i, cap.as_str());
460 if i == 1 {
461 let entropy = calculate_entropy(cap.as_str());
462 println!(" Entropy: {:.2} (threshold: 3.5)", entropy);
463 }
464 }
465 }
466 }
467 }
468 } else {
469 println!("✗ Regex does NOT match the input");
470 }
471 }
472 } else {
473 println!("Rule has no regex pattern (path-based rule)");
474 }
475
476 let result = redact_secrets(input, None, &HashMap::new(), false);
478 println!(
479 "Full function result: {} secrets detected",
480 result.redaction_map.len()
481 );
482 }
483
484 #[test]
485 fn test_regex_breakdown() {
486 let config = &*GITLEAKS_CONFIG;
487 let generic_rule = config
488 .rules
489 .iter()
490 .find(|r| r.id == "generic-api-key")
491 .unwrap();
492
493 if let Some(regex_pattern) = &generic_rule.regex {
494 println!("Full regex: {}", regex_pattern);
495
496 let test_inputs = vec![
498 "key=abcdefghijklmnop",
499 "api_key=abcdefghijklmnop",
500 "secret=abcdefghijklmnop",
501 "token=abcdefghijklmnop",
502 "password=abcdefghijklmnop",
503 "access_key=abcdefghijklmnop",
504 ];
505
506 for input in test_inputs {
507 println!("\nTesting: '{}'", input);
508
509 if let Ok(regex) = Regex::new(regex_pattern) {
511 let matches: Vec<_> = regex.find_iter(input).collect();
512 println!(" Matches found: {}", matches.len());
513
514 for (i, mat) in matches.iter().enumerate() {
515 println!(" Match {}: '{}'", i, mat.as_str());
516
517 if let Some(captures) = regex.captures(mat.as_str()) {
519 for (j, cap) in captures.iter().enumerate() {
520 if let Some(cap) = cap {
521 println!(" Capture {}: '{}'", j, cap.as_str());
522 if j == 1 {
523 let entropy = calculate_entropy(cap.as_str());
524 println!(" Entropy: {:.2} (threshold: 3.5)", entropy);
525 if entropy >= 3.5 {
526 println!(" ✓ Entropy check PASSED");
527 } else {
528 println!(" ✗ Entropy check FAILED");
529 }
530 }
531 }
532 }
533 }
534 }
535 }
536 }
537 } else {
538 println!("Rule has no regex pattern (path-based rule)");
539 }
540
541 println!("\nTesting AWS pattern that we know works:");
543 let aws_input = format!("AWS_ACCESS_KEY_ID={}", fake_aws_access_key_example());
544 println!("Input: {}", aws_input);
545
546 let aws_rule = config
547 .rules
548 .iter()
549 .find(|r| r.id == "aws-access-token")
550 .unwrap();
551 if let Some(aws_regex_pattern) = &aws_rule.regex {
552 if let Ok(regex) = Regex::new(aws_regex_pattern) {
553 for mat in regex.find_iter(&aws_input) {
554 println!("AWS Match: '{}'", mat.as_str());
555 if let Some(captures) = regex.captures(mat.as_str()) {
556 for (i, cap) in captures.iter().enumerate() {
557 if let Some(cap) = cap {
558 println!(" AWS Capture {}: '{}'", i, cap.as_str());
559 }
560 }
561 }
562 }
563 }
564 } else {
565 println!("AWS rule has no regex pattern");
566 }
567 }
568
569 #[test]
570 fn test_working_api_key_patterns() {
571 let config = &*GITLEAKS_CONFIG;
572 let generic_rule = config
573 .rules
574 .iter()
575 .find(|r| r.id == "generic-api-key")
576 .unwrap();
577
578 let regex = generic_rule
580 .compiled_regex
581 .as_ref()
582 .expect("Regex should be compiled");
583
584 let test_inputs = vec![
586 format!("myapp_api_key = \"{}\"", fake_api_key()),
588 format!("export SECRET_TOKEN={}", fake_secret_token()),
589 "app.auth.password: 9k2L8pMvB3nQ7rX1ZdF5GhJwY4AsPo6C8mN".to_string(),
590 "config.access_key=\"RaNd0mH1ghEnTr0pyV4luE567890abcdef\";".to_string(),
591 "DB_CREDENTIALS=xy9mP2nQ8rT4vW7yZ3cF6hJ1lN5sAdefghij".to_string(),
592 ];
593
594 for input in test_inputs {
595 println!("\nTesting: '{}'", input);
596
597 let matches: Vec<_> = regex.find_iter(&input).collect();
598 println!(" Matches found: {}", matches.len());
599
600 for (i, mat) in matches.iter().enumerate() {
601 println!(" Match {}: '{}'", i, mat.as_str());
602
603 if let Some(captures) = regex.captures(mat.as_str()) {
604 for (j, cap) in captures.iter().enumerate() {
605 if let Some(cap) = cap {
606 println!(" Capture {}: '{}'", j, cap.as_str());
607 if j == 1 {
608 let entropy = calculate_entropy(cap.as_str());
609 println!(" Entropy: {:.2} (threshold: 3.5)", entropy);
610
611 let allowed = should_allow_match(
613 &input,
614 None,
615 mat.as_str(),
616 mat.start(),
617 mat.end(),
618 generic_rule,
619 &config.allowlist,
620 );
621 println!(" Allowed by allowlist: {}", allowed);
622 }
623 }
624 }
625 }
626 }
627
628 let result = redact_secrets(&input, None, &HashMap::new(), false);
630 println!(
631 " Full function detected: {} secrets",
632 result.redaction_map.len()
633 );
634 if !result.redaction_map.is_empty() {
635 println!(" Redacted result: {}", result.redacted_string);
636 }
637 }
638 }
639
640 #[test]
641 fn test_regex_components() {
642 let test_input = format!("export API_KEY={}", fake_secret_token());
644 println!("Testing input: {}", test_input);
645
646 let test_patterns = vec![
648 (r"API_KEY", "Simple keyword match"),
649 (r"(?i)api_key", "Case insensitive keyword"),
650 (r"(?i).*key.*", "Any text with 'key'"),
651 (r"(?i).*key\s*=", "Key with equals"),
652 (r"(?i).*key\s*=\s*\w+", "Key with value"),
653 (
654 r"(?i)[\w.-]*(?:key).*?=.*?(\w{10,})",
655 "Complex pattern with capture",
656 ),
657 ];
658
659 for (pattern, description) in test_patterns {
660 println!("\nTesting pattern: {} ({})", pattern, description);
661
662 match Regex::new(pattern) {
663 Ok(regex) => {
664 if regex.is_match(&test_input) {
665 println!(" ✓ MATCHES");
666 for mat in regex.find_iter(&test_input) {
667 println!(" Full match: '{}'", mat.as_str());
668 }
669 if let Some(captures) = regex.captures(&test_input) {
670 for (i, cap) in captures.iter().enumerate() {
671 if let Some(cap) = cap {
672 println!(" Capture {}: '{}'", i, cap.as_str());
673 }
674 }
675 }
676 } else {
677 println!(" ✗ NO MATCH");
678 }
679 }
680 Err(e) => println!(" Error: {}", e),
681 }
682 }
683
684 let config = &*GITLEAKS_CONFIG;
686 let generic_rule = config
687 .rules
688 .iter()
689 .find(|r| r.id == "generic-api-key")
690 .unwrap();
691
692 println!("\nTesting actual gitleaks regex:");
693 if let Some(regex_pattern) = &generic_rule.regex {
694 match Regex::new(regex_pattern) {
695 Ok(regex) => {
696 println!(" ✓ Regex compiles successfully");
697 println!(" Testing against: {}", test_input);
698 if regex.is_match(&test_input) {
699 println!(" ✓ MATCHES");
700 } else {
701 println!(" ✗ NO MATCH");
702 }
703 }
704 Err(e) => println!(" ✗ Regex compilation error: {}", e),
705 }
706 } else {
707 println!(" Rule has no regex pattern (path-based rule)");
708 }
709 }
710
711 #[test]
712 fn test_comprehensive_secrets_redaction() {
713 let aws_key = fake_aws_access_key_alt();
714 let github_token = fake_github_token();
715 let api_key = fake_api_key();
716 let secret_token = fake_secret_token();
717 let password = fake_password_secret();
718 let input = format!(
719 "\n# Configuration file with various secrets\nexport AWS_ACCESS_KEY_ID={aws_key}\nexport GITHUB_TOKEN={github_token}\nexport API_KEY={api_key}\nexport SECRET_TOKEN={secret_token}\nexport PASSWORD={password}\n\n# Some normal configuration\nexport DEBUG=true\nexport PORT=3000\n"
720 );
721
722 println!("Original input:\n{}", input);
723
724 let result = redact_secrets(&input, None, &HashMap::new(), false);
725
726 println!("Redacted output:\n{}", result.redacted_string);
727 println!("\nDetected {} secrets:", result.redaction_map.len());
728 for (key, value) in &result.redaction_map {
729 println!(" {} -> {}", key, value);
730 }
731
732 assert!(
734 result.redaction_map.len() >= 5,
735 "Should detect at least 5 secrets, found: {}",
736 result.redaction_map.len()
737 );
738
739 assert!(!result.redacted_string.contains(&aws_key));
741 assert!(!result.redacted_string.contains(&github_token));
742 assert!(!result.redacted_string.contains(&api_key));
743
744 assert!(result.redacted_string.contains("DEBUG=true"));
746 assert!(result.redacted_string.contains("PORT=3000"));
747 }
748
749 fn count_rules_that_would_process(input: &str) -> Vec<String> {
751 let config = &*GITLEAKS_CONFIG;
752 let mut rules = Vec::new();
753
754 for rule in &config.rules {
755 if rule.keywords.is_empty() || contains_any_keyword(input, &rule.keywords) {
756 rules.push(rule.id.clone());
757 }
758 }
759
760 rules
761 }
762
763 #[test]
764 fn test_keyword_filtering() {
765 println!("=== TESTING KEYWORD FILTERING ===");
766
767 let config = &*GITLEAKS_CONFIG;
768
769 let generic_rule = config
771 .rules
772 .iter()
773 .find(|r| r.id == "generic-api-key")
774 .unwrap();
775 println!("Generic API Key rule keywords: {:?}", generic_rule.keywords);
776
777 let input_with_keywords = format!("export API_KEY={}", fake_api_key());
779 let result1 = redact_secrets(&input_with_keywords, None, &HashMap::new(), false);
780 println!("\nTest 1 - Input WITH keywords:");
781 println!(" Input: {}", input_with_keywords);
782 println!(
783 " Keywords present: {}",
784 contains_any_keyword(&input_with_keywords, &generic_rule.keywords)
785 );
786 println!(" Secrets detected: {}", result1.redaction_map.len());
787
788 let input_without_keywords = "export DATABASE_URL=postgresql://user:pass@localhost/db";
790 let result2 = redact_secrets(input_without_keywords, None, &HashMap::new(), false);
791 println!("\nTest 2 - Input WITHOUT generic-api-key keywords:");
792 println!(" Input: {}", input_without_keywords);
793 println!(
794 " Keywords present: {}",
795 contains_any_keyword(input_without_keywords, &generic_rule.keywords)
796 );
797 println!(" Secrets detected: {}", result2.redaction_map.len());
798
799 let aws_rule = config
801 .rules
802 .iter()
803 .find(|r| r.id == "aws-access-token")
804 .unwrap();
805 let aws_input = format!("AWS_ACCESS_KEY_ID={}", fake_aws_access_key_example());
806 let result3 = redact_secrets(&aws_input, None, &HashMap::new(), false);
807 println!("\nTest 3 - AWS input:");
808 println!(" Input: {}", aws_input);
809 println!(" AWS rule keywords: {:?}", aws_rule.keywords);
810 println!(
811 " Keywords present: {}",
812 contains_any_keyword(&aws_input, &aws_rule.keywords)
813 );
814 println!(" Secrets detected: {}", result3.redaction_map.len());
815
816 assert!(
818 contains_any_keyword(&input_with_keywords, &generic_rule.keywords),
819 "API_KEY input should contain generic-api-key keywords"
820 );
821 assert!(
822 !contains_any_keyword(input_without_keywords, &generic_rule.keywords),
823 "DATABASE_URL input should NOT contain generic-api-key keywords"
824 );
825 assert!(
826 contains_any_keyword(&aws_input, &aws_rule.keywords),
827 "AWS input should contain AWS rule keywords"
828 );
829 }
830
831 #[test]
832 fn test_keyword_optimization_performance() {
833 println!("=== TESTING KEYWORD OPTIMIZATION PERFORMANCE ===");
834
835 let config = &*GITLEAKS_CONFIG;
836
837 let no_keywords_input = "export DATABASE_CONNECTION=some_long_connection_string_that_has_no_common_secret_keywords";
839 println!("Testing input with no secret keywords:");
840 println!(" Input: {}", no_keywords_input);
841
842 let mut keyword_matches = 0;
843 for rule in &config.rules {
844 if contains_any_keyword(no_keywords_input, &rule.keywords) {
845 keyword_matches += 1;
846 println!(" Rule '{}' keywords match: {:?}", rule.id, rule.keywords);
847 }
848 }
849 println!(
850 " Rules with matching keywords: {} out of {}",
851 keyword_matches,
852 config.rules.len()
853 );
854
855 let result = redact_secrets(no_keywords_input, None, &HashMap::new(), false);
856 println!(" Secrets detected: {}", result.redaction_map.len());
857
858 let specific_keywords_input = format!("export GITHUB_TOKEN={}", fake_github_token_short());
860 println!("\nTesting input with specific keywords (github):");
861 println!(" Input: {}", specific_keywords_input);
862
863 let mut matching_rules = Vec::new();
864 for rule in &config.rules {
865 if contains_any_keyword(&specific_keywords_input, &rule.keywords) {
866 matching_rules.push(&rule.id);
867 }
868 }
869 println!(" Rules that would be processed: {:?}", matching_rules);
870
871 let result = redact_secrets(&specific_keywords_input, None, &HashMap::new(), false);
872 println!(" Secrets detected: {}", result.redaction_map.len());
873
874 let rules_without_keywords: Vec<_> = config
876 .rules
877 .iter()
878 .filter(|rule| rule.keywords.is_empty())
879 .collect();
880 println!(
881 "\nRules without keywords (always processed): {}",
882 rules_without_keywords.len()
883 );
884 for rule in &rules_without_keywords {
885 println!(" - {}", rule.id);
886 }
887
888 assert!(
890 keyword_matches < config.rules.len(),
891 "Input with no keywords should not match all rules"
892 );
893 assert!(
894 !matching_rules.is_empty(),
895 "GitHub token input should match some rules"
896 );
897 assert!(
898 matching_rules.contains(&&"github-pat".to_string())
899 || matching_rules
900 .iter()
901 .any(|rule_id| rule_id.contains("github")),
902 "GitHub token should match GitHub-related rules"
903 );
904 }
905
906 #[test]
907 fn test_keyword_filtering_efficiency() {
908 println!("=== KEYWORD FILTERING EFFICIENCY TEST ===");
909
910 let config = &*GITLEAKS_CONFIG;
911 println!("Total rules in config: {}", config.rules.len());
912
913 let non_secret_input = "export DATABASE_URL=localhost PORT=3000 DEBUG=true TIMEOUT=30";
915 println!("\nTesting non-secret input: {}", non_secret_input);
916
917 let mut rules_skipped = 0;
918 let mut rules_processed = 0;
919
920 for rule in &config.rules {
921 if rule.keywords.is_empty() || contains_any_keyword(non_secret_input, &rule.keywords) {
922 rules_processed += 1;
923 } else {
924 rules_skipped += 1;
925 }
926 }
927
928 println!(
929 " Rules skipped due to keyword filtering: {}",
930 rules_skipped
931 );
932 println!(" Rules that would be processed: {}", rules_processed);
933 println!(
934 " Efficiency gain: {:.1}% of rules skipped",
935 (rules_skipped as f64 / config.rules.len() as f64) * 100.0
936 );
937
938 let result = redact_secrets(non_secret_input, None, &HashMap::new(), false);
940 println!(" Secrets detected: {}", result.redaction_map.len());
941
942 let secret_input = format!(
944 "export API_KEY={} SECRET_TOKEN=xyz789uvw012rst345def678",
945 fake_api_key()
946 );
947 println!("\nTesting input WITH secret keywords:");
948 println!(" Input: {}", secret_input);
949
950 let mut rules_with_keywords = 0;
951 for rule in &config.rules {
952 if contains_any_keyword(&secret_input, &rule.keywords) {
953 rules_with_keywords += 1;
954 }
955 }
956
957 println!(" Rules that match keywords: {}", rules_with_keywords);
958
959 let result = redact_secrets(&secret_input, None, &HashMap::new(), false);
960 println!(" Secrets detected: {}", result.redaction_map.len());
961
962 assert!(
964 rules_skipped > 0,
965 "Should skip at least some rules for non-secret input"
966 );
967 assert!(
968 rules_with_keywords > 0,
969 "Should find matching rules for secret input"
970 );
971 assert!(
972 !result.redaction_map.is_empty(),
973 "Should detect at least one secret"
974 );
975 }
976
977 #[test]
978 fn test_keyword_validation_summary() {
979 println!("=== KEYWORD VALIDATION SUMMARY ===");
980
981 let config = &*GITLEAKS_CONFIG;
982 let total_rules = config.rules.len();
983 println!("Total rules in gitleaks config: {}", total_rules);
984
985 let no_keyword_input = "export DATABASE_URL=localhost PORT=3000";
987 println!("\n--- No keywords - should skip all rules ---");
988 println!("Input: {}", no_keyword_input);
989
990 let no_keyword_rules = count_rules_that_would_process(no_keyword_input);
991 println!(
992 "Rules that would be processed: {} out of {}",
993 no_keyword_rules.len(),
994 total_rules
995 );
996 println!(" Rules: {:?}", no_keyword_rules);
997
998 let no_keyword_secrets = detect_secrets(no_keyword_input, None, false);
999 println!(
1000 "Secrets detected: {} (expected: 0)",
1001 no_keyword_secrets.len()
1002 );
1003 assert_eq!(no_keyword_secrets.len(), 0, "Should not detect any secrets");
1004 println!("✅ Test passed");
1005
1006 let api_input = format!("export API_KEY={}", fake_api_key());
1008 println!("\n--- API keyword - should process generic-api-key rule ---");
1009 println!("Input: {}", api_input);
1010
1011 let api_rules = count_rules_that_would_process(&api_input);
1012 println!(
1013 "Rules that would be processed: {} out of {}",
1014 api_rules.len(),
1015 total_rules
1016 );
1017 println!(" Rules: {:?}", api_rules);
1018
1019 let api_secrets = detect_secrets(&api_input, None, false);
1020 println!("Secrets detected: {} (expected: 1)", api_secrets.len());
1021 assert!(!api_secrets.is_empty(), "Should detect at least 1 secrets");
1022 println!("✅ Test passed");
1023
1024 let aws_input = format!("AWS_ACCESS_KEY_ID={}", fake_aws_access_key_alt());
1027 println!("\n--- AWS keyword - should process aws-access-token rule ---");
1028 println!("Input: {}", aws_input);
1029
1030 let aws_rules = count_rules_that_would_process(&aws_input);
1031 println!(
1032 "Rules that would be processed: {} out of {}",
1033 aws_rules.len(),
1034 total_rules
1035 );
1036 println!(" Rules: {:?}", aws_rules);
1037
1038 let aws_secrets = detect_secrets(&aws_input, None, false);
1039 println!("Secrets detected: {} (expected: 1)", aws_secrets.len());
1040
1041 assert!(!aws_secrets.is_empty(), "Should detect at least 1 secrets");
1043 println!("✅ Test passed");
1044 }
1045
1046 #[test]
1047 fn test_debug_missing_secrets() {
1048 println!("=== DEBUGGING MISSING SECRETS ===");
1049
1050 let test_cases = vec![
1051 format!("SECRET_TOKEN={}", fake_secret_token()),
1052 format!("PASSWORD={}", fake_password_secret()),
1053 ];
1054
1055 for input in test_cases {
1056 println!("\nTesting: {}", input);
1057
1058 let parts: Vec<&str> = input.split('=').collect();
1060 if parts.len() == 2 {
1061 let secret_value = parts[1];
1062 let entropy = calculate_entropy(secret_value);
1063 println!(" Secret value: '{}'", secret_value);
1064 println!(" Entropy: {:.2} (threshold: 3.5)", entropy);
1065
1066 if entropy >= 3.5 {
1067 println!(" ✓ Entropy check PASSED");
1068 } else {
1069 println!(" ✗ Entropy check FAILED - this is why it's not detected");
1070 }
1071 }
1072
1073 if let Ok(regex) = create_simple_api_key_regex() {
1075 println!(" Testing fallback regex:");
1076 if regex.is_match(&input) {
1077 println!(" ✓ Fallback regex MATCHES");
1078 for mat in regex.find_iter(&input) {
1079 println!(" Match: '{}'", mat.as_str());
1080 if let Some(captures) = regex.captures(mat.as_str()) {
1081 for (i, cap) in captures.iter().enumerate() {
1082 if let Some(cap) = cap {
1083 println!(" Capture {}: '{}'", i, cap.as_str());
1084 }
1085 }
1086 }
1087
1088 let config = &*GITLEAKS_CONFIG;
1090 let generic_rule = config
1091 .rules
1092 .iter()
1093 .find(|r| r.id == "generic-api-key")
1094 .unwrap();
1095 let allowed = should_allow_match(
1096 &input,
1097 None,
1098 mat.as_str(),
1099 mat.start(),
1100 mat.end(),
1101 generic_rule,
1102 &config.allowlist,
1103 );
1104 println!(" Allowed by allowlist: {}", allowed);
1105 if allowed {
1106 println!(
1107 " ✗ FILTERED OUT by allowlist - this is why it's not detected"
1108 );
1109 }
1110 }
1111 } else {
1112 println!(" ✗ Fallback regex does NOT match");
1113 }
1114 }
1115
1116 let result = redact_secrets(&input, None, &HashMap::new(), false);
1118 println!(
1119 " Full detection result: {} secrets",
1120 result.redaction_map.len()
1121 );
1122 }
1123 }
1124
1125 #[test]
1126 fn test_debug_allowlist_filtering() {
1127 println!("=== DEBUGGING ALLOWLIST FILTERING ===");
1128
1129 let test_cases = vec![
1130 format!("SECRET_TOKEN={}", fake_secret_token()),
1131 format!("PASSWORD={}", fake_password_secret()),
1132 ];
1133
1134 let config = &*GITLEAKS_CONFIG;
1135 let generic_rule = config
1136 .rules
1137 .iter()
1138 .find(|r| r.id == "generic-api-key")
1139 .unwrap();
1140
1141 for input in test_cases {
1142 println!("\nAnalyzing: {}", input);
1143
1144 if let Ok(regex) = create_simple_api_key_regex() {
1145 for mat in regex.find_iter(&input) {
1146 let match_text = mat.as_str();
1147 println!(" Match: '{}'", match_text);
1148
1149 if let Some(global_allowlist) = &config.allowlist {
1151 println!(" Checking global allowlist:");
1152
1153 if let Some(regexes) = &global_allowlist.regexes {
1155 for (i, pattern) in regexes.iter().enumerate() {
1156 if let Ok(regex) = Regex::new(pattern)
1157 && regex.is_match(match_text)
1158 {
1159 println!(" ✗ FILTERED by global regex {}: '{}'", i, pattern);
1160 }
1161 }
1162 }
1163
1164 if let Some(stopwords) = &global_allowlist.stopwords {
1166 for stopword in stopwords {
1167 if match_text.to_lowercase().contains(&stopword.to_lowercase()) {
1168 println!(" ✗ FILTERED by global stopword: '{}'", stopword);
1169 }
1170 }
1171 }
1172 }
1173
1174 if let Some(rule_allowlists) = &generic_rule.allowlists {
1176 for (rule_idx, allowlist) in rule_allowlists.iter().enumerate() {
1177 println!(" Checking rule allowlist {}:", rule_idx);
1178
1179 if let Some(regexes) = &allowlist.regexes {
1181 for (i, pattern) in regexes.iter().enumerate() {
1182 if let Ok(regex) = Regex::new(pattern)
1183 && regex.is_match(match_text)
1184 {
1185 println!(
1186 " ✗ FILTERED by rule regex {}: '{}'",
1187 i, pattern
1188 );
1189 }
1190 }
1191 }
1192
1193 if let Some(stopwords) = &allowlist.stopwords {
1195 for stopword in stopwords {
1196 if match_text.to_lowercase().contains(&stopword.to_lowercase())
1197 {
1198 println!(" ✗ FILTERED by rule stopword: '{}'", stopword);
1199 }
1200 }
1201 }
1202 }
1203 }
1204 }
1205 }
1206 }
1207 }
1208
1209 #[test]
1210 fn test_debug_new_allowlist_logic() {
1211 println!("=== DEBUGGING NEW ALLOWLIST LOGIC ===");
1212
1213 let test_cases = vec![
1214 format!("SECRET_TOKEN={}", fake_secret_token()),
1215 format!("PASSWORD={}", fake_password_secret()),
1216 "PASSWORD=password123".to_string(), "API_KEY=example_key".to_string(), ];
1219
1220 let config = &*GITLEAKS_CONFIG;
1221 let generic_rule = config
1222 .rules
1223 .iter()
1224 .find(|r| r.id == "generic-api-key")
1225 .unwrap();
1226
1227 for input in test_cases {
1228 println!("\nTesting: {}", input);
1229
1230 if let Ok(regex) = create_simple_api_key_regex() {
1231 for mat in regex.find_iter(&input) {
1232 let match_text = mat.as_str();
1233 println!(" Match: '{}'", match_text);
1234
1235 if let Some((_, value)) = match_text.split_once('=') {
1237 println!(" Value: '{}'", value);
1238
1239 let test_stopwords = ["token", "password", "super", "word"];
1241 for stopword in test_stopwords {
1242 let value_lower = value.to_lowercase();
1243 let stopword_lower = stopword.to_lowercase();
1244
1245 if value_lower == stopword_lower {
1246 println!(" '{}' - Exact match: YES", stopword);
1247 } else if value.len() < 15 && value_lower.contains(&stopword_lower) {
1248 let without_stopword = value_lower.replace(&stopword_lower, "");
1249 let is_simple = without_stopword.chars().all(|c| {
1250 c.is_ascii_digit() || "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c)
1251 });
1252 println!(
1253 " '{}' - Short+contains: len={}, without='{}', simple={}",
1254 stopword,
1255 value.len(),
1256 without_stopword,
1257 is_simple
1258 );
1259 } else {
1260 println!(" '{}' - No filter", stopword);
1261 }
1262 }
1263 }
1264
1265 if let Some(rule_allowlists) = &generic_rule.allowlists {
1267 for (rule_idx, allowlist) in rule_allowlists.iter().enumerate() {
1268 let allowed = is_allowed_by_rule_allowlist(
1269 &input,
1270 None,
1271 match_text,
1272 mat.start(),
1273 mat.end(),
1274 allowlist,
1275 );
1276 println!(" Rule allowlist {}: allowed = {}", rule_idx, allowed);
1277 }
1278 }
1279 }
1280 }
1281 }
1282 }
1283
1284 #[test]
1285 fn test_redact_password_basic() {
1286 let content = "User password is supersecret123 and should be hidden";
1287 let password = "supersecret123";
1288 let result = redact_password(content, password, &HashMap::new());
1289
1290 assert!(!result.redacted_string.contains(password));
1292 assert!(
1293 result
1294 .redacted_string
1295 .contains("[REDACTED_SECRET:password:")
1296 );
1297 assert_eq!(result.redaction_map.len(), 1);
1298
1299 let redacted_password = result.redaction_map.values().next().unwrap();
1301 assert_eq!(redacted_password, password);
1302 }
1303
1304 #[test]
1305 fn test_redact_password_empty() {
1306 let content = "Some content without password";
1307 let password = "";
1308 let result = redact_password(content, password, &HashMap::new());
1309
1310 assert_eq!(result.redacted_string, content);
1312 assert!(result.redaction_map.is_empty());
1313 }
1314
1315 #[test]
1316 fn test_redact_password_multiple_occurrences() {
1317 let content = "Password is mypass123 and again mypass123 appears here";
1318 let password = "mypass123";
1319 let result = redact_password(content, password, &HashMap::new());
1320
1321 assert!(!result.redacted_string.contains(password));
1323 assert_eq!(result.redaction_map.len(), 1);
1324
1325 let redaction_key = result.redaction_map.keys().next().unwrap();
1327 let count = result.redacted_string.matches(redaction_key).count();
1328 assert_eq!(count, 2);
1329 }
1330
1331 #[test]
1332 fn test_redact_password_reuse_existing_key() {
1333 let mut existing_map = HashMap::new();
1335 existing_map.insert(
1336 "[REDACTED_SECRET:password:abc123]".to_string(),
1337 "mypassword".to_string(),
1338 );
1339
1340 let content = "The password mypassword should use existing key";
1341 let password = "mypassword";
1342 let result = redact_password(content, password, &existing_map);
1343
1344 assert_eq!(result.redaction_map.len(), 1);
1346 assert!(
1347 result
1348 .redaction_map
1349 .contains_key("[REDACTED_SECRET:password:abc123]")
1350 );
1351 assert!(
1352 result
1353 .redacted_string
1354 .contains("[REDACTED_SECRET:password:abc123]")
1355 );
1356 }
1357
1358 #[test]
1359 fn test_redact_password_with_existing_different_secrets() {
1360 let mut existing_map = HashMap::new();
1362 existing_map.insert(
1363 "[REDACTED_SECRET:api-key:xyz789]".to_string(),
1364 "some_api_key".to_string(),
1365 );
1366
1367 let content = "API key is some_api_key and password is newpassword123";
1368 let password = "newpassword123";
1369 let result = redact_password(content, password, &existing_map);
1370
1371 assert_eq!(result.redaction_map.len(), 2);
1373 assert!(
1374 result
1375 .redaction_map
1376 .contains_key("[REDACTED_SECRET:api-key:xyz789]")
1377 );
1378 assert!(
1379 result
1380 .redaction_map
1381 .get("[REDACTED_SECRET:api-key:xyz789]")
1382 .unwrap()
1383 == "some_api_key"
1384 );
1385
1386 let new_keys: Vec<_> = result
1388 .redaction_map
1389 .keys()
1390 .filter(|k| k.contains("password"))
1391 .collect();
1392 assert_eq!(new_keys.len(), 1);
1393 let password_key = new_keys[0];
1394 assert_eq!(
1395 result.redaction_map.get(password_key).unwrap(),
1396 "newpassword123"
1397 );
1398 }
1399
1400 #[test]
1401 fn test_redact_password_no_match() {
1402 let content = "This content has no matching password";
1403 let password = "notfound";
1404 let result = redact_password(content, password, &HashMap::new());
1405
1406 assert_eq!(result.redacted_string, content);
1408 assert_eq!(result.redaction_map.len(), 1);
1409 assert_eq!(result.redaction_map.values().next().unwrap(), "notfound");
1410 }
1411
1412 #[test]
1413 fn test_redact_password_integration_with_restore() {
1414 let content = "Login with username admin and password secret456";
1415 let password = "secret456";
1416 let result = redact_password(content, password, &HashMap::new());
1417
1418 assert!(!result.redacted_string.contains(password));
1420 assert!(result.redacted_string.contains("username admin"));
1421
1422 let restored = restore_secrets(&result.redacted_string, &result.redaction_map);
1424 assert_eq!(restored, content);
1425 }
1426
1427 #[test]
1428 fn test_redact_secrets_with_existing_redaction_map() {
1429 let content = "The secret value is mysecretvalue123 and another is anothersecret456";
1431
1432 let result_empty = redact_secrets(content, None, &HashMap::new(), false);
1434
1435 assert!(result_empty.redacted_string.contains("mysecretvalue123"));
1437 let mut existing_redaction_map = HashMap::new();
1439 existing_redaction_map.insert(
1440 "[REDACTED_SECRET:manual:abc123]".to_string(),
1441 "mysecretvalue123".to_string(),
1442 );
1443
1444 let result = redact_secrets(content, None, &existing_redaction_map, false);
1445
1446 assert!(
1448 result
1449 .redacted_string
1450 .contains("[REDACTED_SECRET:manual:abc123]")
1451 );
1452 assert!(!result.redacted_string.contains("mysecretvalue123"));
1453
1454 assert!(
1456 result
1457 .redaction_map
1458 .contains_key("[REDACTED_SECRET:manual:abc123]")
1459 );
1460 assert_eq!(
1461 result
1462 .redaction_map
1463 .get("[REDACTED_SECRET:manual:abc123]")
1464 .unwrap(),
1465 "mysecretvalue123"
1466 );
1467 }
1468
1469 #[test]
1470 fn test_redact_secrets_skip_already_redacted() {
1471 let content = "The password is [REDACTED_SECRET:password:abc123] and API key is [REDACTED_SECRET:api-key:xyz789]";
1473 let result = redact_secrets(content, None, &HashMap::new(), false);
1474
1475 assert_eq!(result.redacted_string, content);
1477 assert!(result.redaction_map.is_empty());
1479 }
1480
1481 #[test]
1482 fn test_redact_password_skip_already_redacted() {
1483 let content = "[REDACTED_SECRET:password:existing123]";
1485 let password = "newpassword";
1486 let result = redact_password(content, password, &HashMap::new());
1487
1488 assert_eq!(result.redacted_string, content);
1490 assert!(result.redaction_map.is_empty());
1492 }
1493
1494 #[test]
1495 fn test_redact_secrets_skip_nested_redaction() {
1496 let original_password = "MySecureP@ssw0rd!";
1498
1499 let first_result = redact_password(original_password, original_password, &HashMap::new());
1501 assert!(
1502 first_result
1503 .redacted_string
1504 .contains("[REDACTED_SECRET:password:")
1505 );
1506
1507 let second_result =
1509 redact_secrets(&first_result.redacted_string, None, &HashMap::new(), false);
1510
1511 assert_eq!(second_result.redacted_string, first_result.redacted_string);
1513 assert!(second_result.redaction_map.is_empty());
1514 }
1515
1516 #[test]
1517 fn test_huawei_cloud_credentials_detection() {
1518 let csv_content = r#"User Name,Access Key Id,Secret Access Key
1521terraform,TESTHUAWEIKEY1234567,TestHuaweiSecretKey1234567890abcdefghij"#;
1522
1523 let result = redact_secrets(csv_content, None, &HashMap::new(), false);
1524
1525 println!("Input: {}", csv_content);
1526 println!("Redacted: {}", result.redacted_string);
1527 println!("Mapping: {:?}", result.redaction_map);
1528
1529 assert!(
1531 !result.redaction_map.is_empty(),
1532 "Should detect Huawei credentials"
1533 );
1534
1535 assert!(
1537 !result.redacted_string.contains("TESTHUAWEIKEY1234567"),
1538 "AK should be redacted"
1539 );
1540
1541 assert!(
1543 !result
1544 .redacted_string
1545 .contains("TestHuaweiSecretKey1234567890abcdefghij"),
1546 "SK should be redacted"
1547 );
1548
1549 assert!(
1551 result.redacted_string.contains("[REDACTED_SECRET:huawei-"),
1552 "Should contain Huawei redaction markers"
1553 );
1554 }
1555
1556 #[test]
1557 fn test_huawei_access_key_id_pattern() {
1558 let input = "Access Key Id: TESTHWCLOUD123456789";
1562 let result = redact_secrets(input, None, &HashMap::new(), false);
1563
1564 println!("Input: {}", input);
1565 println!("Redacted: {}", result.redacted_string);
1566
1567 assert!(
1568 !result.redaction_map.is_empty(),
1569 "Should detect Huawei AK with 'Access Key Id' keyword"
1570 );
1571 assert!(
1572 !result.redacted_string.contains("TESTHWCLOUD123456789"),
1573 "AK should be redacted"
1574 );
1575 }
1576
1577 #[test]
1578 fn test_huawei_secret_access_key_pattern() {
1579 let input = "Secret Access Key: TestHwCloudSecretKey12345678901234567890";
1582 let result = redact_secrets(input, None, &HashMap::new(), false);
1583
1584 println!("Input: {}", input);
1585 println!("Redacted: {}", result.redacted_string);
1586
1587 assert!(
1588 !result.redaction_map.is_empty(),
1589 "Should detect Huawei SK with 'Secret Access Key' keyword"
1590 );
1591 assert!(
1592 !result
1593 .redacted_string
1594 .contains("TestHwCloudSecretKey12345678901234567890"),
1595 "SK should be redacted"
1596 );
1597 }
1598}