@@ -33,6 +33,8 @@ pub struct RedactorConfig {
33
33
ignores : Option < HashMap < String , Vec < String > > > ,
34
34
secret_patterns : HashMap < String , Vec < Regex > > ,
35
35
ignore_patterns : HashMap < String , Vec < Regex > > ,
36
+ secret_exact_values : HashMap < String , Vec < String > > , // New field for exact secrets
37
+ ignore_exact_values : HashMap < String , Vec < String > > , // New field for exact ignores
36
38
}
37
39
38
40
impl RedactorConfig {
@@ -58,45 +60,110 @@ impl RedactorConfig {
58
60
ignores,
59
61
secret_patterns : HashMap :: new ( ) ,
60
62
ignore_patterns : HashMap :: new ( ) ,
63
+ secret_exact_values : HashMap :: new ( ) , // Initialize new field
64
+ ignore_exact_values : HashMap :: new ( ) , // Initialize new field
61
65
} ;
62
66
63
- // Compile regex patterns for secrets
67
+ // Compile regex patterns and collect exact values for secrets
64
68
if let Some ( ref secrets) = config. secrets {
65
69
for ( key, patterns) in secrets {
66
70
let mut regex_patterns = Vec :: new ( ) ;
71
+ let mut exact_values = Vec :: new ( ) ;
67
72
for pattern in patterns {
68
- if let Ok ( regex) = Self :: compile_pattern ( pattern) {
69
- regex_patterns. push ( regex) ;
73
+ if Self :: is_wildcard_pattern ( pattern) {
74
+ if let Ok ( regex) = Self :: compile_pattern ( pattern) {
75
+ regex_patterns. push ( regex) ;
76
+ }
77
+ } else {
78
+ exact_values. push ( pattern. clone ( ) ) ;
70
79
}
71
80
}
72
81
config. secret_patterns . insert ( key. clone ( ) , regex_patterns) ;
82
+ config. secret_exact_values . insert ( key. clone ( ) , exact_values) ;
73
83
}
74
84
}
75
85
76
- // Compile regex patterns for ignores
86
+ // Compile regex patterns and collect exact values for ignores
77
87
if let Some ( ref ignores) = config. ignores {
78
88
for ( key, patterns) in ignores {
79
89
let mut regex_patterns = Vec :: new ( ) ;
90
+ let mut exact_values = Vec :: new ( ) ;
80
91
for pattern in patterns {
81
- if let Ok ( regex) = Self :: compile_pattern ( pattern) {
82
- regex_patterns. push ( regex) ;
92
+ if Self :: is_wildcard_pattern ( pattern) {
93
+ if let Ok ( regex) = Self :: compile_pattern ( pattern) {
94
+ regex_patterns. push ( regex) ;
95
+ }
96
+ } else {
97
+ exact_values. push ( pattern. clone ( ) ) ;
83
98
}
84
99
}
85
100
config. ignore_patterns . insert ( key. clone ( ) , regex_patterns) ;
101
+ config. ignore_exact_values . insert ( key. clone ( ) , exact_values) ;
86
102
}
87
103
}
88
104
89
105
Ok ( config)
90
106
}
91
107
108
+ // Helper function to determine if a pattern is a wildcard pattern
109
+ fn is_wildcard_pattern ( pattern : & str ) -> bool {
110
+ pattern. contains ( '*' ) || pattern. contains ( '?' ) || pattern. contains ( '[' )
111
+ }
112
+
92
113
fn compile_pattern ( pattern : & str ) -> Result < Regex , regex:: Error > {
93
- let escaped = regex:: escape ( pattern) . replace ( "\\ *" , ".*" ) . replace ( "\\ ?" , "." ) ;
114
+ let mut escaped = String :: new ( ) ;
115
+ let mut chars = pattern. chars ( ) . peekable ( ) ;
116
+
117
+ while let Some ( ch) = chars. next ( ) {
118
+ match ch {
119
+ '*' => {
120
+ if pattern. contains ( '-' ) || pattern. contains ( '.' ) || pattern. contains ( ' ' ) {
121
+ // For phone numbers with separators, match remaining digits
122
+ escaped. push_str ( r"\d+" ) ;
123
+ } else {
124
+ escaped. push_str ( ".*" ) ;
125
+ }
126
+ }
127
+ '?' => escaped. push ( '.' ) ,
128
+ '[' => {
129
+ escaped. push ( '[' ) ;
130
+ while let Some ( & next_ch) = chars. peek ( ) {
131
+ escaped. push ( next_ch) ;
132
+ chars. next ( ) ;
133
+ if next_ch == ']' {
134
+ break ;
135
+ }
136
+ }
137
+ }
138
+ '\\' => {
139
+ escaped. push ( '\\' ) ;
140
+ if let Some ( & next_ch) = chars. peek ( ) {
141
+ escaped. push ( next_ch) ;
142
+ chars. next ( ) ;
143
+ }
144
+ }
145
+ // Escape special regex characters in phone numbers
146
+ '.' | '-' | '(' | ')' | ' ' => {
147
+ escaped. push ( '\\' ) ;
148
+ escaped. push ( ch) ;
149
+ }
150
+ _ => escaped. push_str ( & regex:: escape ( & ch. to_string ( ) ) ) ,
151
+ }
152
+ }
153
+
94
154
RegexBuilder :: new ( & format ! ( "^{}$" , escaped) )
95
155
. case_insensitive ( true )
96
156
. build ( )
97
157
}
98
158
99
159
pub fn has_ignore_pattern ( & self , pattern_type : & str , value : & str ) -> bool {
160
+ // Check exact matches first
161
+ if let Some ( values) = self . ignore_exact_values . get ( pattern_type) {
162
+ if values. contains ( & value. to_string ( ) ) {
163
+ return true ;
164
+ }
165
+ }
166
+ // Then check regex patterns
100
167
if let Some ( patterns) = self . ignore_patterns . get ( pattern_type) {
101
168
patterns. iter ( ) . any ( |regex| regex. is_match ( value) )
102
169
} else {
@@ -105,6 +172,13 @@ impl RedactorConfig {
105
172
}
106
173
107
174
pub fn has_secret_pattern ( & self , pattern_type : & str , value : & str ) -> bool {
175
+ // Check exact matches first
176
+ if let Some ( values) = self . secret_exact_values . get ( pattern_type) {
177
+ if values. contains ( & value. to_string ( ) ) {
178
+ return true ;
179
+ }
180
+ }
181
+ // Then check regex patterns
108
182
if let Some ( patterns) = self . secret_patterns . get ( pattern_type) {
109
183
patterns. iter ( ) . any ( |regex| regex. is_match ( value) )
110
184
} else {
@@ -326,20 +400,40 @@ impl Redactor {
326
400
false
327
401
}
328
402
403
+ #[ allow( dead_code) ]
329
404
fn should_redact_value ( & self , value : & str , pattern_type : & str ) -> bool {
330
- // Check if both secret and ignore patterns exist
331
405
let is_secret = self . config . has_secret_pattern ( pattern_type, value) ;
332
406
let is_ignored = self . config . has_ignore_pattern ( pattern_type, value) ;
333
407
408
+ // First check if value matches both patterns
334
409
if is_secret && is_ignored {
335
410
warn ! (
336
- "Precedence conflict: Value '{}' matches both secret and ignore patterns for type '{}'. Treating as secret." ,
411
+ "Precedence conflict: Value '{}' matches both secret and ignore patterns for type '{}'. Using secret pattern." ,
412
+ value, pattern_type
413
+ ) ;
414
+ return true ; // Secret takes precedence
415
+ }
416
+
417
+ // Then check ignore patterns
418
+ if is_ignored {
419
+ debug ! (
420
+ "Value '{}' matches ignore pattern for type '{}'" ,
337
421
value, pattern_type
338
422
) ;
423
+ return false ; // Don't redact ignored values
339
424
}
340
425
341
- // Secrets take precedence over ignores
342
- is_secret && !is_ignored
426
+ // Finally check secret patterns
427
+ if is_secret {
428
+ debug ! (
429
+ "Value '{}' matches secret pattern for type '{}'" ,
430
+ value, pattern_type
431
+ ) ;
432
+ return true ; // Redact secret values
433
+ }
434
+
435
+ // No pattern matches
436
+ false
343
437
}
344
438
345
439
fn redact_pattern ( & mut self , line : & str , pattern_type : & str ) -> String {
@@ -348,7 +442,8 @@ impl Redactor {
348
442
return line. to_string ( ) ;
349
443
}
350
444
351
- debug ! ( "Redacting pattern type: {} for line: {}" , pattern_type, line) ;
445
+ println ! ( "Redacting pattern type: {} for line: {}" , pattern_type, line) ;
446
+
352
447
let pattern = & self . patterns [ pattern_type] ;
353
448
let captures: Vec < _ > = pattern. captures_iter ( line) . collect ( ) ;
354
449
@@ -359,8 +454,6 @@ impl Redactor {
359
454
) ;
360
455
361
456
let validator_fn = self . validators [ pattern_type] ;
362
- let interactive = self . interactive ;
363
-
364
457
let mut redacted_line = line. to_string ( ) ;
365
458
366
459
for cap in captures {
@@ -373,48 +466,48 @@ impl Redactor {
373
466
374
467
debug ! ( "Processing match: {} of type: {}" , value, key_type) ;
375
468
376
- // Skip if value should be ignored based on format
469
+ // 1. Skip if value should be ignored based on format
377
470
if self . should_ignore_value ( value, pattern_type) {
378
471
debug ! ( "Ignoring value due to format: {}" , value) ;
379
472
continue ;
380
473
}
381
474
382
- // Check if the value should be redacted based on patterns
383
- let should_redact = self . should_redact_value ( value, pattern_type) ;
384
- debug ! (
385
- "Should redact '{}' based on patterns? {}" ,
386
- value, should_redact
387
- ) ;
475
+ // 2. First check core validation rules
476
+ if !validator_fn ( value) {
477
+ debug ! ( "Value '{}' failed validation" , value) ;
478
+ continue ;
479
+ }
388
480
389
- if should_redact {
481
+ // 3. Check for secrets and ignore patterns, including exact matches
482
+ let is_secret = self . config . has_secret_pattern ( pattern_type, value) ;
483
+ let is_ignored = self . config . has_ignore_pattern ( pattern_type, value) ;
484
+
485
+ // 4. Secret takes precedence if found in both
486
+ if is_secret && is_ignored {
487
+ debug ! ( "Value '{}' found in both secrets and ignore lists, using secret pattern" , value) ;
390
488
let replacement = self . generate_unique_mapping ( value, key_type) ;
391
- debug ! ( "Replacing '{}' with '{}'" , value, replacement) ;
392
489
redacted_line = redacted_line. replace ( value, & replacement) ;
393
490
continue ;
394
491
}
395
492
396
- // For hostnames, implement additional validation
397
- if pattern_type == "hostname" {
398
- let should_process = should_process_hostname ( value) ;
399
- debug ! (
400
- "Should process hostname '{}'? {}" ,
401
- value, should_process
402
- ) ;
403
- if !should_process {
404
- continue ;
405
- }
493
+ // 5. Check secrets after ignore
494
+ if is_secret {
495
+ debug ! ( "Value '{}' matches secret pattern" , value) ;
496
+ let replacement = self . generate_unique_mapping ( value, key_type) ;
497
+ redacted_line = redacted_line. replace ( value, & replacement) ;
498
+ continue ;
406
499
}
407
500
408
- // Validate and check interactive mode
409
- if validator_fn ( value ) {
410
- debug ! ( "Value '{}' passed validation " , value) ;
411
- if !interactive || self . ask_user ( value , key_type ) {
412
- let replacement = self . generate_unique_mapping ( value , key_type ) ;
413
- debug ! ( "Replacing '{}' with '{}'" , value , replacement ) ;
414
- redacted_line = redacted_line . replace ( value , & replacement ) ;
415
- }
416
- } else {
417
- debug ! ( "Value '{}' failed validation" , value ) ;
501
+ // 6. Skip if explicitly ignored
502
+ if is_ignored {
503
+ debug ! ( "Value '{}' matches ignore pattern, skipping " , value) ;
504
+ continue ;
505
+ }
506
+
507
+ // 7. If interactive mode is enabled, ask user
508
+ if self . interactive && self . ask_user ( value , key_type ) {
509
+ let replacement = self . generate_unique_mapping ( value , key_type ) ;
510
+ redacted_line = redacted_line . replace ( value , & replacement ) ;
418
511
}
419
512
}
420
513
@@ -423,6 +516,7 @@ impl Redactor {
423
516
}
424
517
425
518
pub fn redact ( & mut self , lines : Vec < String > ) -> Vec < String > {
519
+ let _ = env_logger:: builder ( ) . is_test ( true ) . try_init ( ) ;
426
520
let pattern_keys: Vec < String > = self . patterns . keys ( ) . cloned ( ) . collect ( ) ;
427
521
lines
428
522
. into_iter ( )
0 commit comments