#![cfg(feature = "logging")]
use bashkit::LogConfig;
mod redaction_tests {
use super::*;
#[test]
fn test_common_secret_env_vars_redacted() {
let config = LogConfig::new();
assert!(
config.should_redact_env("PASSWORD"),
"PASSWORD should be redacted"
);
assert!(
config.should_redact_env("DB_PASSWORD"),
"DB_PASSWORD should be redacted"
);
assert!(
config.should_redact_env("MYSQL_ROOT_PASSWORD"),
"MYSQL_ROOT_PASSWORD should be redacted"
);
assert!(
config.should_redact_env("TOKEN"),
"TOKEN should be redacted"
);
assert!(
config.should_redact_env("ACCESS_TOKEN"),
"ACCESS_TOKEN should be redacted"
);
assert!(
config.should_redact_env("REFRESH_TOKEN"),
"REFRESH_TOKEN should be redacted"
);
assert!(
config.should_redact_env("GITHUB_TOKEN"),
"GITHUB_TOKEN should be redacted"
);
assert!(
config.should_redact_env("NPM_TOKEN"),
"NPM_TOKEN should be redacted"
);
assert!(
config.should_redact_env("API_KEY"),
"API_KEY should be redacted"
);
assert!(
config.should_redact_env("SECRET_KEY"),
"SECRET_KEY should be redacted"
);
assert!(
config.should_redact_env("PRIVATE_KEY"),
"PRIVATE_KEY should be redacted"
);
assert!(
config.should_redact_env("ENCRYPTION_KEY"),
"ENCRYPTION_KEY should be redacted"
);
assert!(config.should_redact_env("AUTH"), "AUTH should be redacted");
assert!(
config.should_redact_env("AUTHORIZATION"),
"AUTHORIZATION should be redacted"
);
assert!(
config.should_redact_env("DATABASE_URL"),
"DATABASE_URL should be redacted"
);
assert!(
config.should_redact_env("DB_URL"),
"DB_URL should be redacted"
);
assert!(
config.should_redact_env("AWS_SECRET_ACCESS_KEY"),
"AWS_SECRET_ACCESS_KEY should be redacted"
);
assert!(
config.should_redact_env("SESSION_SECRET"),
"SESSION_SECRET should be redacted"
);
assert!(
config.should_redact_env("COOKIE_SECRET"),
"COOKIE_SECRET should be redacted"
);
assert!(
config.should_redact_env("OPENAI_API_KEY"),
"OPENAI_API_KEY should be redacted"
);
assert!(
config.should_redact_env("ANTHROPIC_API_KEY"),
"ANTHROPIC_API_KEY should be redacted"
);
assert!(
config.should_redact_env("CLAUDE_API_KEY"),
"CLAUDE_API_KEY should be redacted"
);
assert!(
config.should_redact_env("AZURE_OPENAI_KEY"),
"AZURE_OPENAI_KEY should be redacted"
);
assert!(
config.should_redact_env("GOOGLE_AI_API_KEY"),
"GOOGLE_AI_API_KEY should be redacted"
);
assert!(
config.should_redact_env("GEMINI_API_KEY"),
"GEMINI_API_KEY should be redacted"
);
assert!(
config.should_redact_env("COHERE_API_KEY"),
"COHERE_API_KEY should be redacted"
);
assert!(
config.should_redact_env("HUGGINGFACE_TOKEN"),
"HUGGINGFACE_TOKEN should be redacted"
);
assert!(
config.should_redact_env("HUGGING_FACE_TOKEN"),
"HUGGING_FACE_TOKEN should be redacted"
);
assert!(
config.should_redact_env("REPLICATE_API_TOKEN"),
"REPLICATE_API_TOKEN should be redacted"
);
assert!(
config.should_redact_env("MISTRAL_API_KEY"),
"MISTRAL_API_KEY should be redacted"
);
assert!(
config.should_redact_env("GROQ_API_KEY"),
"GROQ_API_KEY should be redacted"
);
assert!(
config.should_redact_env("TOGETHER_API_KEY"),
"TOGETHER_API_KEY should be redacted"
);
assert!(
config.should_redact_env("PERPLEXITY_API_KEY"),
"PERPLEXITY_API_KEY should be redacted"
);
assert!(
config.should_redact_env("FIREWORKS_API_KEY"),
"FIREWORKS_API_KEY should be redacted"
);
assert!(
config.should_redact_env("VERTEX_AI_KEY"),
"VERTEX_AI_KEY should be redacted"
);
assert!(
config.should_redact_env("BEDROCK_ACCESS_KEY"),
"BEDROCK_ACCESS_KEY should be redacted"
);
}
#[test]
fn test_normal_env_vars_not_redacted() {
let config = LogConfig::new();
assert!(
!config.should_redact_env("HOME"),
"HOME should not be redacted"
);
assert!(
!config.should_redact_env("PATH"),
"PATH should not be redacted"
);
assert!(
!config.should_redact_env("USER"),
"USER should not be redacted"
);
assert!(
!config.should_redact_env("SHELL"),
"SHELL should not be redacted"
);
assert!(
!config.should_redact_env("TERM"),
"TERM should not be redacted"
);
assert!(
!config.should_redact_env("PWD"),
"PWD should not be redacted"
);
assert!(
!config.should_redact_env("LANG"),
"LANG should not be redacted"
);
assert!(
!config.should_redact_env("LC_ALL"),
"LC_ALL should not be redacted"
);
assert!(
!config.should_redact_env("EDITOR"),
"EDITOR should not be redacted"
);
assert!(
!config.should_redact_env("DEBUG"),
"DEBUG should not be redacted"
);
}
#[test]
fn test_case_insensitive_redaction() {
let config = LogConfig::new();
assert!(
config.should_redact_env("password"),
"lowercase 'password' should be redacted"
);
assert!(
config.should_redact_env("Password"),
"mixed case 'Password' should be redacted"
);
assert!(
config.should_redact_env("PASSWORD"),
"uppercase 'PASSWORD' should be redacted"
);
assert!(
config.should_redact_env("PaSsWoRd"),
"weird case 'PaSsWoRd' should be redacted"
);
}
#[test]
fn test_custom_redaction_patterns() {
let config = LogConfig::new()
.redact_env("MY_INTERNAL_SECRET")
.redact_env("COMPANY_SPECIFIC");
assert!(
config.should_redact_env("MY_INTERNAL_SECRET"),
"Custom pattern should be redacted"
);
assert!(
config.should_redact_env("COMPANY_SPECIFIC_KEY"),
"Custom pattern should match substrings"
);
}
#[test]
fn test_url_credential_redaction() {
let config = LogConfig::new();
assert_eq!(
config
.redact_url("https://user:password@example.com/path")
.as_ref(),
"https://[REDACTED]@example.com/path"
);
assert_eq!(
config
.redact_url("https://admin:super$ecret123@db.example.com:5432/mydb")
.as_ref(),
"https://[REDACTED]@db.example.com:5432/mydb"
);
assert_eq!(
config
.redact_url("http://user:pass@internal.example.com")
.as_ref(),
"http://[REDACTED]@internal.example.com"
);
}
#[test]
fn test_url_without_credentials_unchanged() {
let config = LogConfig::new();
assert_eq!(
config.redact_url("https://example.com/path").as_ref(),
"https://example.com/path"
);
assert_eq!(
config.redact_url("https://user@example.com/path").as_ref(),
"https://user@example.com/path"
);
assert_eq!(
config
.redact_url("https://api.example.com?key=value")
.as_ref(),
"https://api.example.com?key=value"
);
}
#[test]
fn test_jwt_redaction() {
let config = LogConfig::new();
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
assert_eq!(config.redact_value(jwt).as_ref(), "[REDACTED]");
}
#[test]
fn test_api_key_prefixes_redacted() {
let config = LogConfig::new();
assert_eq!(
config.redact_value("sk-FAKE_TEST_KEY_123456").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("pk-FAKE_TEST_KEY_123456").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("sk_live_FAKE_TEST_KEY_123456").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("sk_test_FAKE_TEST_KEY_123456").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("ghp_FAKE_TEST_TOKEN_12345678").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("gho_FAKE_TEST_TOKEN_12345678").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("xoxb-FAKE-TEST-TOKEN-HERE").as_ref(),
"[REDACTED]"
);
assert_eq!(
config.redact_value("AKIAFAKEKEY12345678").as_ref(),
"[REDACTED]"
);
}
#[test]
fn test_normal_values_not_redacted() {
let config = LogConfig::new();
assert_eq!(config.redact_value("hello world").as_ref(), "hello world");
assert_eq!(config.redact_value("12345").as_ref(), "12345");
assert_eq!(
config.redact_value("/path/to/file").as_ref(),
"/path/to/file"
);
assert_eq!(
config.redact_value("user@example.com").as_ref(),
"user@example.com"
);
}
#[test]
fn test_script_content_not_logged_by_default() {
use bashkit::logging::format_script_for_log;
let config = LogConfig::new();
let script = r#"
export SECRET_API_KEY="sk-supersecretkey123"
curl -H "Authorization: Bearer $SECRET_API_KEY" https://api.example.com
"#;
let formatted = format_script_for_log(script, &config);
assert!(formatted.contains("lines"), "Should show line count");
assert!(formatted.contains("bytes"), "Should show byte count");
assert!(
!formatted.contains("SECRET_API_KEY"),
"Should not contain secret var name"
);
assert!(
!formatted.contains("sk-supersecretkey"),
"Should not contain secret value"
);
assert!(
!formatted.contains("Authorization"),
"Should not contain auth header"
);
}
#[test]
fn test_script_content_with_unsafe_flag() {
use bashkit::logging::format_script_for_log;
unsafe { std::env::set_var("BASHKIT_UNSAFE_LOGGING", "1") };
let config = LogConfig::new().unsafe_log_scripts();
unsafe { std::env::remove_var("BASHKIT_UNSAFE_LOGGING") };
let script = "echo hello";
let formatted = format_script_for_log(script, &config);
assert!(
formatted.contains("echo"),
"Should contain script content with unsafe flag"
);
}
}
mod injection_tests {
use bashkit::logging::sanitize_for_log;
#[test]
fn test_newline_injection_prevented() {
let malicious = "normal\n[ERROR] SECURITY BREACH: Hacked!";
let sanitized = sanitize_for_log(malicious);
assert!(
!sanitized.contains('\n'),
"Should not contain literal newlines"
);
assert!(sanitized.contains("\\n"), "Should contain escaped newlines");
assert!(
sanitized.contains("normal"),
"Should preserve normal content"
);
assert!(
sanitized.contains("[ERROR]"),
"Should preserve injected content (escaped)"
);
}
#[test]
fn test_carriage_return_injection_prevented() {
let malicious = "normal\r[ERROR] Fake entry";
let sanitized = sanitize_for_log(malicious);
assert!(
!sanitized.contains('\r'),
"Should not contain literal carriage returns"
);
assert!(
sanitized.contains("\\r"),
"Should contain escaped carriage returns"
);
}
#[test]
fn test_crlf_injection_prevented() {
let malicious = "normal\r\n[ERROR] Windows-style injection";
let sanitized = sanitize_for_log(malicious);
assert!(!sanitized.contains('\r'), "Should not contain literal CR");
assert!(!sanitized.contains('\n'), "Should not contain literal LF");
assert!(sanitized.contains("\\r\\n"), "Should contain escaped CRLF");
}
#[test]
fn test_tab_escaped() {
let input = "column1\tcolumn2";
let sanitized = sanitize_for_log(input);
assert!(!sanitized.contains('\t'), "Should not contain literal tabs");
assert!(sanitized.contains("\\t"), "Should contain escaped tabs");
}
#[test]
fn test_control_chars_filtered() {
let input = "normal\x00null\x07bell\x1bescapeseq";
let sanitized = sanitize_for_log(input);
assert!(!sanitized.contains('\x00'), "Should not contain null bytes");
assert!(!sanitized.contains('\x07'), "Should not contain bell char");
assert!(
!sanitized.contains('\x1b'),
"Should not contain escape char"
);
assert!(sanitized.contains("normal"), "Should preserve normal text");
assert!(
sanitized.contains("null"),
"Should preserve text after null"
);
}
#[test]
fn test_ansi_escape_sequences_filtered() {
let input = "normal\x1b[31mRED TEXT\x1b[0m";
let sanitized = sanitize_for_log(input);
assert!(
!sanitized.contains("\x1b["),
"Should not contain ANSI sequences"
);
assert!(sanitized.contains("normal"), "Should preserve normal text");
assert!(
sanitized.contains("RED TEXT"),
"Should preserve text content"
);
}
}
mod truncation_tests {
use super::*;
#[test]
fn test_long_values_truncated() {
let config = LogConfig::new().max_value_length(50);
let long_value = "a".repeat(200);
let truncated = config.redact_value(&long_value);
let truncated_str = truncated.as_ref();
assert!(truncated_str.len() < 200, "Should be shorter than original");
assert!(
truncated_str.starts_with("aaaa"),
"Should start with original content"
);
assert!(
truncated_str.contains("[truncated"),
"Should indicate truncation"
);
assert!(
truncated_str.contains("bytes]"),
"Should show truncated byte count"
);
}
#[test]
fn test_short_values_not_truncated() {
let config = LogConfig::new().max_value_length(200);
let short_value = "hello world";
let result = config.redact_value(short_value);
assert_eq!(result.as_ref(), "hello world");
assert!(!result.contains("[truncated"), "Should not be truncated");
}
#[test]
fn test_default_truncation_length() {
let config = LogConfig::new();
let value_199 = "a".repeat(199);
let value_201 = "a".repeat(201);
let result_199 = config.redact_value(&value_199);
let result_201 = config.redact_value(&value_201);
assert!(
!result_199.contains("[truncated"),
"199 chars should not be truncated"
);
assert!(
result_201.contains("[truncated"),
"201 chars should be truncated"
);
}
}
mod disabled_redaction_tests {
use super::*;
#[test]
fn test_disabled_redaction_shows_secrets() {
unsafe { std::env::set_var("BASHKIT_UNSAFE_LOGGING", "1") };
let config = LogConfig::new().unsafe_disable_redaction();
unsafe { std::env::remove_var("BASHKIT_UNSAFE_LOGGING") };
assert!(
!config.should_redact_env("PASSWORD"),
"Should not redact with disabled"
);
assert!(
!config.should_redact_env("SECRET_KEY"),
"Should not redact with disabled"
);
assert_eq!(
config.redact_url("https://user:pass@example.com").as_ref(),
"https://user:pass@example.com"
);
}
#[test]
fn test_default_redaction_enabled() {
let config = LogConfig::new();
assert!(
config.redact_sensitive,
"Redaction should be enabled by default"
);
}
}
mod proptest_redaction {
use bashkit::LogConfig;
use proptest::prelude::*;
proptest! {
#[test]
fn redacted_env_never_returns_value(name in "[A-Z_]{1,50}") {
let config = LogConfig::new();
if config.should_redact_env(&name) {
let upper = name.to_uppercase();
let patterns = [
"PASSWORD", "SECRET", "TOKEN", "KEY", "CREDENTIAL", "AUTH",
"API_KEY", "APIKEY", "PRIVATE", "BEARER", "JWT", "SESSION",
"COOKIE", "ENCRYPTION", "SIGNING", "DATABASE_URL", "DB_URL",
"CONNECTION_STRING", "AWS_SECRET", "AWS_ACCESS", "GITHUB_TOKEN",
"NPM_TOKEN", "STRIPE", "TWILIO", "SENDGRID", "PASSWD",
"OPENAI", "ANTHROPIC", "CLAUDE", "AZURE_OPENAI", "GOOGLE_AI",
"GEMINI", "COHERE", "HUGGINGFACE", "HUGGING_FACE", "REPLICATE",
"MISTRAL", "PERPLEXITY", "GROQ", "TOGETHER", "ANYSCALE",
"FIREWORKS", "DEEPMIND", "VERTEX_AI", "BEDROCK", "SAGEMAKER",
];
let contains_pattern = patterns.iter().any(|p| upper.contains(p));
prop_assert!(contains_pattern, "Redacted var '{}' should contain sensitive pattern", name);
}
}
#[test]
fn url_redaction_preserves_scheme_and_host(
scheme in "https?",
user in "[a-z]{3,10}",
pass in "[a-z]{5,15}", host in "[a-z]{5,10}",
path in "[a-z]{0,10}"
) {
let url = format!("{}://{}:{}@{}.example.com/{}", scheme, user, pass, host, path);
let config = LogConfig::new();
let redacted = config.redact_url(&url);
let redacted_str = redacted.as_ref();
prop_assert!(redacted_str.starts_with(&scheme), "Should preserve scheme in '{}'", redacted_str);
prop_assert!(redacted_str.contains(&format!("{}.example.com", host)), "Should preserve host in '{}'", redacted_str);
prop_assert!(redacted_str.contains("[REDACTED]"), "Should have redaction marker in '{}'", redacted_str);
prop_assert!(!redacted_str.contains(&format!("{}:{}", user, pass)), "Credentials should be redacted");
}
#[test]
fn sanitize_removes_control_chars(input in "[^\x00-\x1f]*") {
use bashkit::logging::sanitize_for_log;
let sanitized = sanitize_for_log(&input);
prop_assert!(
sanitized.len() <= input.len() + 10, "Sanitized length {} should be close to input length {}",
sanitized.len(), input.len()
);
}
#[test]
fn truncation_respects_limit(value in "[a-zA-Z0-9 ]{0,500}", limit in 10usize..300) {
let config = LogConfig::new().max_value_length(limit);
let test_value = format!("test_{}", value);
let result = config.redact_value(&test_value);
let max_expected = limit + 50;
prop_assert!(
result.len() <= max_expected,
"Result length {} exceeds expected max {} for limit {}",
result.len(), max_expected, limit
);
}
}
}