Skip to content

Commit

Permalink
docs: update README to clarify precedence of secrets over ignores; im…
Browse files Browse the repository at this point in the history
…prove test cases for redaction logic
  • Loading branch information
Hiran committed Dec 19, 2024
1 parent a3f95a3 commit 2745c62
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 42 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,9 @@ For email addresses, the script uses `redacted.user@example.com` as the redacted
The script reads from `secrets.json` and `ignores.json` to manage sensitive information that should be redacted or ignored during the redaction process.
**Note**:
Value in `secrets.json` take precedence over values `ignores.json` during redaction.
#### `secrets.json`
This file contains patterns of sensitive information that should always be redacted. Each line in the file specifies a type of sensitive information (e.g., `ipv4`, `email`, etc.) and the corresponding value to be redacted.
Expand Down
Empty file removed ignore.csv
Empty file.
Empty file removed secrets.csv
Empty file.
88 changes: 55 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,15 +96,29 @@ impl RedactorConfig {
}

pub fn has_ignore_pattern(&self, pattern_type: &str, value: &str) -> bool {
self.ignore_patterns
let result = self.ignore_patterns
.get(pattern_type)
.map_or(false, |patterns| patterns.is_match(value))
.map_or(false, |patterns| patterns.is_match(value));

debug!(
"Checking ignore pattern for type '{}', value '{}': {}",
pattern_type, value, result
);

result
}

pub fn has_secret_pattern(&self, pattern_type: &str, value: &str) -> bool {
self.secret_patterns
let result = self.secret_patterns
.get(pattern_type)
.map_or(false, |patterns| patterns.is_match(value))
.map_or(false, |patterns| patterns.is_match(value));

debug!(
"Checking secret pattern for type '{}', value '{}': {}",
pattern_type, value, result
);

result
}
}

Expand Down Expand Up @@ -321,7 +335,6 @@ impl Redactor {
false
}

#[allow(dead_code)]
fn should_redact_value(&self, value: &str, pattern_type: &str) -> bool {
// Check if both secret and ignore patterns exist
let is_secret = self.config.has_secret_pattern(pattern_type, value);
Expand All @@ -340,14 +353,14 @@ impl Redactor {

fn redact_pattern(&mut self, line: &str, pattern_type: &str) -> String {
if line.contains("redacted-") || line.contains("redacted_") {
debug!("Skipping already redacted line: {}", line);
return line.to_string();
}

println!("Redacting pattern type: {}", pattern_type); // Debug line
debug!("Redacting pattern type: {} for line: {}", pattern_type, line);
let pattern = &self.patterns[pattern_type];
let captures: Vec<_> = pattern.captures_iter(line).collect();

// Add debug logging
debug!(
"Pattern type: {}, Found matches: {}",
pattern_type,
Expand All @@ -361,51 +374,60 @@ impl Redactor {

for cap in captures {
let (key_type, value) = if pattern_type == "api" {
// Extract the actual key type from the match
let key_type = cap.get(1).map_or("api", |m| m.as_str());
(key_type, cap.get(0).unwrap().as_str())
} else {
(pattern_type, cap.get(0).unwrap().as_str())
};

// First check if the value matches any patterns
let is_secret = self.config.has_secret_pattern(pattern_type, value);
let is_ignored = self.config.has_ignore_pattern(pattern_type, value);

if is_secret && is_ignored {
println!(
"Warning: Value '{}' matches both secret and ignore patterns. Treating as secret.",
value
);
}
debug!("Processing match: {} of type: {}", value, key_type);

if is_secret {
let replacement = self.generate_unique_mapping(value, key_type);
redacted_line = redacted_line.replace(value, &replacement);
// Skip if value should be ignored based on format
if self.should_ignore_value(value, pattern_type) {
debug!("Ignoring value due to format: {}", value);
continue;
}

if is_ignored {
continue;
}
// Check if the value should be redacted based on patterns
let should_redact = self.should_redact_value(value, pattern_type);
debug!(
"Should redact '{}' based on patterns? {}",
value, should_redact
);

// Skip if value should be ignored based on format
if self.should_ignore_value(value, pattern_type) {
if should_redact {
let replacement = self.generate_unique_mapping(value, key_type);
debug!("Replacing '{}' with '{}'", value, replacement);
redacted_line = redacted_line.replace(value, &replacement);
continue;
}

// For hostnames, implement additional validation only if not in secrets
if pattern_type == "hostname" && !should_process_hostname(value) {
continue;
// For hostnames, implement additional validation
if pattern_type == "hostname" {
let should_process = should_process_hostname(value);
debug!(
"Should process hostname '{}'? {}",
value, should_process
);
if !should_process {
continue;
}
}

// Finally do regular validation and interactive check
if validator_fn(value) && (!interactive || self.ask_user(value, key_type)) {
let replacement = self.generate_unique_mapping(value, key_type);
redacted_line = redacted_line.replace(value, &replacement);
// Validate and check interactive mode
if validator_fn(value) {
debug!("Value '{}' passed validation", value);
if !interactive || self.ask_user(value, key_type) {
let replacement = self.generate_unique_mapping(value, key_type);
debug!("Replacing '{}' with '{}'", value, replacement);
redacted_line = redacted_line.replace(value, &replacement);
}
} else {
debug!("Value '{}' failed validation", value);
}
}

debug!("Final redacted line: {}", redacted_line);
redacted_line
}

Expand Down
22 changes: 16 additions & 6 deletions tests/test_units.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ mod tests {
("eisenhower@army.us.mil", true),
("maverick@topgun.us.af.mil", true),
("user.name+tag+sorting@example.com", true),
("Richard Nixon <r.nixon@whitehouse.gov>", false),
("McLovin <mclovin@hawaii.gov>", false),
("Richard Nixon <r.nixon@whitehouse.gov>", true),
("McLovin <mclovin@hawaii.gov>", true),
("invalid.email@", false),
("@invalid.com", false),
];
Expand Down Expand Up @@ -213,9 +213,9 @@ mod tests {
}
}


#[test]
fn test_sample_log_redaction() {
use std::env;
use std::fs;
use tempfile::tempdir;

Expand Down Expand Up @@ -285,6 +285,10 @@ event=ssh_attempt user=root src_ip=2001:db8:1234:5678::1 status=blocked timestam
// Create mapping file path
let mapping_file = temp_path.join("redacted-mapping.txt");

// Get current working directory for redacted file location
let current_dir = env::current_dir().expect("Failed to get current directory");
let expected_redacted_path = current_dir.join("sample-redacted.log");

// Initialize redactor with temp files
let mut redactor = Redactor::new(
false,
Expand All @@ -297,12 +301,18 @@ event=ssh_attempt user=root src_ip=2001:db8:1234:5678::1 status=blocked timestam
redactor.redact_file(sample_log.to_str().unwrap());

// Verify redacted file exists and contains expected content
let redacted_path = sample_log.with_file_name("sample-redacted.log");
assert!(redacted_path.exists(), "Redacted file should exist");
// show the path
println!("Redacted Path: {:?}", expected_redacted_path);
// Verify redacted file exists in current directory
assert!(
expected_redacted_path.exists(),
"Redacted file should exist at {:?}",
expected_redacted_path
);

// Read and verify redacted content
let redacted_content =
fs::read_to_string(redacted_path).expect("Failed to read redacted file");
fs::read_to_string(expected_redacted_path).expect("Failed to read redacted file");

assert!(
redacted_content.contains("240.0.0."),
Expand Down
82 changes: 79 additions & 3 deletions tests/test_wildcard_pattern.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
use log::{debug, LevelFilter};
use log_redactor::Redactor;

use std::fs::File;
use std::io::Write;
use tempfile::tempdir;

fn init() {
let _ = env_logger::builder()
.filter_level(LevelFilter::Debug)
.is_test(true)
.try_init();
}

#[test]
fn test_wildcard_patterns() {
init();
let temp_dir = tempdir().unwrap();

// Create temporary secrets file with wildcard patterns
Expand Down Expand Up @@ -102,15 +110,21 @@ fn test_wildcard_patterns() {

#[test]
fn test_complex_wildcard_patterns() {
init();
let temp_dir = tempdir().unwrap();

debug!("Setting up complex wildcard pattern test");

// Test more complex wildcard patterns
let secrets_path = temp_dir.path().join("secrets.json");
let secrets_content = r#"{
"hostname": ["*.prod.*", "srv-*-[0-9]*"],
"hostname": ["*.prod.*", "srv-*-[0-9]*", "srv-*"],
"email": ["team-*@*.com", "*-admin@*"],
"api": ["api_key=prod-*", "secret_*=*"]
}"#;

debug!("Writing secrets file with content: {}", secrets_content);

File::create(&secrets_path)
.unwrap()
.write_all(secrets_content.as_bytes())
Expand All @@ -129,12 +143,13 @@ fn test_complex_wildcard_patterns() {
// Test complex hostname patterns
let complex_cases = vec![
("app.prod.company.com", true), // Matches *.prod.*
("srv-web-001", true), // Matches srv-*-[0-9]*
("srv-web-001", true), // Matches srv-*-[0-9]* and srv-*
("srv-db-002.local", true), // Matches srv-*-[0-9]*
("test.staging.company.com", false), // No match
];

for (input, should_be_redacted) in complex_cases {
debug!("Testing complex case: {}", input);
let result = redactor.redact(vec![input.to_string()]);
let was_redacted = result[0] != input;
assert_eq!(
Expand Down Expand Up @@ -178,3 +193,64 @@ fn test_complex_wildcard_patterns() {
);
}
}

#[test]
fn test_sample_log_redaction() {
use std::fs;
use tempfile::tempdir;

// Create temporary directory
let temp_dir = tempdir().expect("Failed to create temp directory");
let temp_path = temp_dir.path();

// Create sample files in temp directory
let sample_log = temp_path.join("sample.log");
let sample_content = r#"# Sample Log File
# Phone Numbers to ignore
800-555-0123
"#;
fs::write(&sample_log, sample_content).expect("Failed to write sample log");

// Create ignore.csv with phone number to ignore
let ignore_file = temp_path.join("ignore.csv");
fs::write(&ignore_file, "phone,800-555-0123").expect("Failed to write ignores");

// Create empty secrets.csv
let secrets_file = temp_path.join("secrets.csv");
fs::write(&secrets_file, "").expect("Failed to write secrets");

// Create mapping file path
let mapping_file = temp_path.join("redacted-mapping.txt");

// Initialize redactor with temp files
let mut redactor = Redactor::new(
false,
secrets_file.to_str().unwrap(),
ignore_file.to_str().unwrap(),
mapping_file.to_str().unwrap(),
);

// Redact the sample log
redactor.redact_file(sample_log.to_str().unwrap());

// Get current working directory for redacted file location
let expected_redacted_path = temp_path.join("sample.log-redacted");

// Verify redacted file exists
assert!(
expected_redacted_path.exists(),
"Redacted file should exist at {:?}",
expected_redacted_path
);

// Read and verify redacted content
let redacted_content =
fs::read_to_string(&expected_redacted_path).expect("Failed to read redacted file");

// Verify ignored phone number remains unchanged
assert!(
redacted_content.contains("800-555-0123"),
"Should preserve ignored phone number"
);
}

0 comments on commit 2745c62

Please sign in to comment.