mod support;
use support::contracts::{make_chunk, scanner};
use keyhog_core::Chunk;
use keyhog_scanner::CompiledScanner;
fn matches(s: &CompiledScanner, chunk: &Chunk) -> Vec<(String, String)> {
s.clear_fragment_cache();
s.scan(chunk)
.expect("scanner call should succeed")
.into_iter()
.map(|m| (m.detector_id.to_string(), m.credential.as_str().to_string()))
.collect()
}
fn surfaces_under(text: &str, detector_id: &str, credential: &str) -> bool {
let s = scanner();
let chunk = make_chunk(text, "source", "probe.sql");
matches(&s, &chunk)
.iter()
.any(|(id, cred)| id == detector_id && cred == credential)
}
fn nothing_surfaces(text: &str, credential: &str) -> bool {
let s = scanner();
let chunk = make_chunk(text, "source", "probe.sql");
!matches(&s, &chunk)
.iter()
.any(|(_, cred)| cred == credential)
}
fn no_password_slot_match(text: &str) -> bool {
let s = scanner();
let chunk = make_chunk(text, "source", "probe.sql");
!matches(&s, &chunk)
.iter()
.any(|(id, _)| id == "sql-password" || id == "cli-password-flag")
}
#[test]
fn sql_random_lowercase_value_recovered_by_strong_anchor() {
let text = "mysql -e \"CREATE USER 'cactiuser'@'localhost' IDENTIFIED BY 'argriyjqr';\"";
assert!(
surfaces_under(text, "sql-password", "argriyjqr"),
"a random lowercase IDENTIFIED BY value must surface under the strong anchor"
);
}
#[test]
fn sql_short_value_below_regex_floor_no_match() {
assert!(
no_password_slot_match("CREATE USER 'svc' IDENTIFIED BY 'k7v2x';"),
"a 5-char IDENTIFIED BY value is below the regex floor and must not match"
);
}
#[test]
fn sql_identified_by_secret_dictionary_suppressed() {
assert!(
nothing_surfaces("ALTER USER app IDENTIFIED BY 'secret';", "secret"),
"`secret` is a confident dictionary word, the dictionary gate must drop it"
);
}
#[test]
fn sql_alter_user_with_plugin_by_surfaces() {
let text =
"ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bjy-jbpuyowe';";
assert!(
surfaces_under(text, "sql-password", "bjy-jbpuyowe"),
"CredData ALTER USER ... WITH <plugin> BY '<random>' must surface"
);
}
#[test]
fn sql_truly_random_lowercase_surfaces() {
let text = "CREATE USER 'svc'@'localhost' IDENTIFIED BY 'pxqzjwkv';";
assert!(
surfaces_under(text, "sql-password", "pxqzjwkv"),
"a truly-random lowercase IDENTIFIED BY value must surface under the strong anchor"
);
}
#[test]
fn sql_double_quoted_value_with_inner_quote_surfaces() {
let text = "CREATE USER root@'hostname' IDENTIFIED BY \"5q'jK3d7ca\";";
assert!(
surfaces_under(text, "sql-password", "5q'jK3d7ca"),
"double-quoted IDENTIFIED BY value (inner ' allowed) must surface"
);
}
#[test]
fn cli_powershell_password_flag_surfaces() {
let text = "Invoke-MSOLSpray -UserList .\\userlist.txt -Password Rcuhxw1486";
assert!(
surfaces_under(text, "cli-password-flag", "Rcuhxw1486"),
"CredData PowerShell -Password <random> must surface"
);
}
#[test]
fn cli_low_alpha_short_password_surfaces() {
let text = "nats server raft step-down --user admin --password i8cr1w!";
assert!(
surfaces_under(text, "cli-password-flag", "i8cr1w!"),
"a ≥6-char low-alpha --password value must surface under the strong anchor"
);
}
#[test]
fn cli_passwd_alias_random_surfaces() {
let text = "mysqldump --user=root --passwd=Qx7Kp2Vn9Rm4Lt8w db";
assert!(
surfaces_under(text, "cli-password-flag", "Qx7Kp2Vn9Rm4Lt8w"),
"the --passwd alias with a random value must surface"
);
}
#[test]
fn cli_short_value_below_regex_floor_no_match() {
assert!(
no_password_slot_match("svc --password k7v2x"),
"a 5-char --password value is below the regex floor and must not match"
);
}
#[test]
fn sql_identified_by_placeholder_stays_suppressed() {
assert!(
nothing_surfaces("CREATE USER 'x' IDENTIFIED BY 'password';", "password"),
"an `IDENTIFIED BY 'password'` placeholder must STAY suppressed"
);
}
#[test]
fn sql_identified_by_your_password_here_suppressed() {
assert!(
nothing_surfaces(
"CREATE USER 'svc' IDENTIFIED BY 'your_password_here';",
"your_password_here"
),
"the instructional `your_password_here` placeholder must STAY suppressed"
);
}
#[test]
fn cli_password_dictionary_value_stays_suppressed() {
assert!(
nothing_surfaces("deploy --password changeme", "changeme"),
"a `--password changeme` placeholder must STAY suppressed"
);
}
#[test]
fn cli_password_welcome_dictionary_suppressed() {
assert!(
nothing_surfaces("login-tool --password welcome", "welcome"),
"`welcome` is a confident dictionary word, the dictionary gate must drop it"
);
}
#[test]
fn cli_password_shell_variable_stays_suppressed() {
assert!(
nothing_surfaces("run --host db --password $DB_PASSWORD", "$DB_PASSWORD"),
"a `--password $VAR` shell reference must STAY suppressed"
);
}
#[test]
fn cli_password_angle_template_suppressed() {
assert!(
nothing_surfaces("deploy --password <YOUR_PASSWORD>", "<YOUR_PASSWORD>"),
"a `--password <YOUR_PASSWORD>` angle template must STAY suppressed"
);
}
#[test]
fn sql_prose_identified_by_no_quotes_no_match() {
assert!(
no_password_slot_match("The bug was identified by the security team during the Q3 audit."),
"prose `identified by` with no quoted value must not match"
);
}
#[test]
fn cli_prose_without_flag_no_match() {
assert!(
no_password_slot_match("See the docs to set the password value before deploying."),
"prose mentioning `password` with no `--password` flag must not match"
);
}
#[test]
fn sql_capture_is_password_only() {
let s = scanner();
let chunk = make_chunk(
"CREATE USER 'svc'@'localhost' IDENTIFIED BY 'Zx9KmPq2LvWnB7tR';",
"source",
"probe.sql",
);
let caps: Vec<String> = matches(&s, &chunk)
.into_iter()
.filter(|(id, _)| id == "sql-password")
.map(|(_, cred)| cred)
.collect();
assert_eq!(
caps,
vec!["Zx9KmPq2LvWnB7tR".to_string()],
"sql-password must capture ONLY the quoted password, not the IDENTIFIED BY clause"
);
}
#[test]
fn cli_capture_is_password_only() {
let s = scanner();
let chunk = make_chunk(
"deploy --user admin --password Zx9KmPq2LvWnB7tR --verbose",
"source",
"probe.sh",
);
let caps: Vec<String> = matches(&s, &chunk)
.into_iter()
.filter(|(id, _)| id == "cli-password-flag")
.map(|(_, cred)| cred)
.collect();
assert_eq!(
caps,
vec!["Zx9KmPq2LvWnB7tR".to_string()],
"cli-password-flag must capture ONLY the flag value, not the surrounding args"
);
}