fix(security): expand Password PII patterns; add regression tests
All checks were successful
Test / rust-fmt-check (pull_request) Successful in 1m20s
Test / frontend-typecheck (pull_request) Successful in 1m37s
Test / frontend-tests (pull_request) Successful in 1m35s
Test / rust-clippy (pull_request) Successful in 3m11s
PR Review Automation / review (pull_request) Successful in 4m22s
Test / rust-tests (pull_request) Successful in 4m28s

Two credential patterns were missing from the PiiDetector, confirmed
by live audit log showing was_pii_redacted: false with plaintext creds:

1. Abbreviated key form (pass: abc123!!): the pattern only matched
   password|passwd|pwd. Added pass, passphrase, secret with a word
   boundary to prevent substring false positives (bypass:, compass:).

2. Natural language form (Is the password password123 good): added a
   second Password sub-pattern for keyword-adjacent values without a
   key separator. Value must contain a digit or special char to avoid
   flagging plain words (password strength, password policy).

5 new regression tests added. 233/233 Rust tests pass.
This commit is contained in:
Shaun Arman 2026-05-31 20:47:59 -05:00
parent 249d20bf85
commit fbd6aab7fe
2 changed files with 99 additions and 2 deletions

View File

@ -86,6 +86,91 @@ mod tests {
assert!(spans.iter().any(|s| s.pii_type == "Bearer")); assert!(spans.iter().any(|s| s.pii_type == "Bearer"));
} }
#[test]
fn test_detect_password_keyword() {
let detector = PiiDetector::new();
// Full keyword forms
assert!(detector
.detect("password: hunter2")
.iter()
.any(|s| s.pii_type == "Password"));
assert!(detector
.detect("passwd=hunter2")
.iter()
.any(|s| s.pii_type == "Password"));
assert!(detector
.detect("pwd: hunter2")
.iter()
.any(|s| s.pii_type == "Password"));
}
#[test]
fn test_detect_pass_abbreviation() {
let detector = PiiDetector::new();
// Abbreviated form used in credential files (was the failing case)
let text = "user: alpha\npass: abc123!!";
let spans = detector.detect(text);
assert!(
spans.iter().any(|s| s.pii_type == "Password"),
"Expected Password span for 'pass: abc123!!' — got: {spans:?}"
);
}
#[test]
fn test_detect_secret_keyword() {
let detector = PiiDetector::new();
assert!(detector
.detect("secret: mysecretvalue")
.iter()
.any(|s| s.pii_type == "Password"));
assert!(detector
.detect("passphrase: correct horse battery staple")
.iter()
.any(|s| s.pii_type == "Password"));
}
#[test]
fn test_detect_password_natural_language() {
let detector = PiiDetector::new();
// Direct juxtaposition: "password <value>" (was the second failing case)
let spans = detector.detect("Is the password password123 good");
assert!(
spans.iter().any(|s| s.pii_type == "Password"),
"Expected Password span for natural-language 'password password123' — got: {spans:?}"
);
// "password is X"
assert!(detector
.detect("my password is hunter2")
.iter()
.any(|s| s.pii_type == "Password"));
// Value must have digit or special — plain words should not trigger
assert!(
!detector
.detect("password strength")
.iter()
.any(|s| s.pii_type == "Password"),
"False positive: 'password strength' should not match"
);
assert!(
!detector
.detect("password policy")
.iter()
.any(|s| s.pii_type == "Password"),
"False positive: 'password policy' should not match"
);
}
#[test]
fn test_password_no_false_positive_bypass() {
let detector = PiiDetector::new();
// "bypass" contains "pass" as a substring — must NOT match
let spans = detector.detect("bypass: enabled");
assert!(
!spans.iter().any(|s| s.pii_type == "Password"),
"False positive: 'bypass:' should not match Password pattern"
);
}
#[test] #[test]
fn test_no_overlap() { fn test_no_overlap() {
let detector = PiiDetector::new(); let detector = PiiDetector::new();

View File

@ -22,10 +22,22 @@ pub fn get_patterns() -> Vec<(PiiType, Regex)> {
) )
.unwrap(), .unwrap(),
), ),
// Password // Password (key=value / config file form) — word-boundary anchored
( (
PiiType::Password, PiiType::Password,
Regex::new(r"(?i)(?:password|passwd|pwd)\s*[=:]\s*\S+").unwrap(), Regex::new(
r"(?i)\b(?:password|passwd|passphrase|pass|pwd|secret)\s*[=:]\s*\S+",
)
.unwrap(),
),
// Password (natural language form): "password is X", "password X"
// Value must contain at least one digit or special char to avoid flagging plain words.
(
PiiType::Password,
Regex::new(
r"(?i)\b(?:password|passwd|passphrase)\s+(?:is\s+|was\s+)?[A-Za-z0-9!@#$%^&*_\-+=@#,.]*[0-9!@#$%^&*_\-+=@#][A-Za-z0-9!@#$%^&*_\-+=@#,.]*",
)
.unwrap(),
), ),
// SSN (check before phone to avoid partial matches) // SSN (check before phone to avoid partial matches)
( (