use crate::config::schema::Config;
use crate::engine::deterministic;
use crate::engine::diff_parser::parse_diff;
use crate::engine::index_bridge::IndexBridge;
use crate::engine::postprocess::{Source, postprocess};
type Row = (&'static str, &'static str, bool);
fn flagged(ext: &str, line: &str) -> bool {
let diff = format!(
"diff --git a/src/app.{ext} b/src/app.{ext}\n--- a/src/app.{ext}\n+++ b/src/app.{ext}\n@@ -0,0 +1 @@\n+{line}\n"
);
let chunks = parse_diff(&diff);
let report = deterministic::run(&chunks, &Config::default(), &IndexBridge::unavailable());
let issues = report.merge_into(Vec::new());
postprocess(issues, &Source::Diff(&chunks), &Config::default())
.iter()
.any(|i| {
let id = i.rule_id.as_deref().unwrap_or("");
id.contains("hardcoded-secret") || id.starts_with("secrets/")
})
}
fn mismatches(rows: &[Row]) -> Vec<String> {
rows.iter()
.filter(|(ext, line, expect)| flagged(ext, line) != *expect)
.map(|(ext, line, expect)| {
format!(
" ({ext:?}, {line:?}): expected flagged={expect}, got {}",
!expect
)
})
.collect()
}
const CASES: &[Row] = &[
("js", r#"const password = "hunter2hunter2";"#, true),
("js", "const apiKey = 'abcd1234efgh5678';", true),
("js", "const token = `abcd1234efgh5678`;", true),
("ts", r#"const password: string = "hunter2hunter2";"#, true),
(
"ts",
r#" readonly secret: string = "hunter2hunter2";"#,
true,
),
("ts", " secret: string;", false),
(
"js",
r#"const password = "hunter2hunter2"; // rotate later"#,
true,
),
(
"js",
r#"const password = "hunter2hunter2"; // note: rotate later"#,
true,
),
(
"ts",
r#"const password: string = "hunter2hunter2"; // TODO: move to vault"#,
true,
),
(
"js",
r#"const password = "hunter2hunter2"; // cora-ignore: crypto/hardcoded-secret"#,
false,
),
(
"js",
r#"const password = "hunter2hunter2"; // cora-ignore: sec-hardcoded-secret"#,
false,
),
(
"py",
r#"password = "hunter2hunter2" # cora-ignore: crypto/hardcoded-secret, sec-hardcoded-secret"#,
false,
),
(
"js",
r#"const password = "hunter2hunter2"; // cora-ignore"#,
true,
),
("ts", " { app_secret: formAppSecret }", false),
("ts", "const body = { password: formPassword };", false),
("js", r#"let password = "";"#, false),
("js", "let password = '';", false),
("ts", r#"let password = $state('');"#, false),
(
"svelte",
"<input type=\"password\" bind:value={password} />",
false,
),
("ts", "let secret = $state(initialSecretValue);", false),
("js", r#"const password = "changeme-please-123";"#, true),
("py", r#"password = "hunter2hunter2""#, true),
("py", "api_key = 'abcd1234efgh5678'", true),
("py", r#"password: str = "hunter2hunter2""#, true),
("py", r#"password = "hunter2hunter2" # note: temp"#, true),
("py", r#"password = """#, false),
("py", "password = ''", false),
("rs", r#"let password = "hunter2hunter2";"#, true),
("rs", r#"let password: &str = "hunter2hunter2";"#, true),
("rs", r#"const API_KEY: &str = "abcd1234efgh5678";"#, true),
(
"rs",
r#"let password = "hunter2hunter2"; // note: test only"#,
true,
),
(
"rs",
r#"let password = "hunter2hunter2"; // cora-ignore: crypto/hardcoded-secret"#,
false,
),
("rs", r#"let password = "";"#, false),
("rs", " pub password: String,", false),
("rs", " api_key: extract_api_key.clone(),", false),
("go", r#"var password = "hunter2hunter2""#, true),
("go", "const token = `abcd1234efgh5678`", true),
("go", r#"password := os.Getenv("DB_PASSWORD")"#, false),
("go", r#"password := """#, false),
("go", r#"password := "hunter2hunter2""#, true),
("go", r#"var apiKey string = "abcd1234efgh5678""#, true),
("go", r#"password := "hunter2hunter2" // note: temp"#, true),
(
"go",
r#"password := "hunter2hunter2" // cora-ignore: crypto/hardcoded-secret"#,
false,
),
("go", r#"var apiKey string = """#, false),
("go", r#"var apiKey string = os.Getenv("API_KEY")"#, false),
(
"java",
r#"private static final String PASSWORD = "hunter2hunter2";"#,
true,
),
("java", r#"String apiKey = "abcd1234efgh5678";"#, true),
("java", r#"String password = "";"#, false),
(
"java",
r#"String password = "hunter2hunter2"; // cora-ignore: crypto/hardcoded-secret"#,
false,
),
("kt", r#"val password = "hunter2hunter2""#, true),
("kt", r#"val apiKey: String = "abcd1234efgh5678""#, true),
("kt", r#"val password = """#, false),
("yaml", r#"password: "hunter2hunter2""#, true),
("yaml", "api_key: 'abcd1234efgh5678'", true),
("yaml", "password: ${DB_PASSWORD}", false),
("yaml", "password: \"\"", false),
("yaml", "password: hunter2hunter2", true),
("yaml", " db_password: hunter2hunter2", true),
("yaml", "- token: abcd1234efgh5678", true),
("yaml", "password: hunter2hunter2 # prod db", true),
("yaml", "password: !secret db", false),
("yaml", "password: !vault db_password_1", false),
("yaml", "password: null", false),
("yaml", "password: ~", false),
("yaml", "password: required", false),
("yaml", "password: *dbpass1", false),
("yaml", "password: &dbpass1 hunter2", false),
("yaml", "password: $DB_PASSWORD_1", false),
("yaml", "password: \"${DB_PASSWORD}\"", false),
("yaml", "password_file: /run/secrets/db_password", false),
("yaml", "password: /run/secrets/db_password", false),
("yaml", "secretName: db-credentials-secret", false),
("ts", " secret: Secret1Type", true),
("json", r#" "password": "hunter2hunter2","#, true),
("json", r#" "apiKey": "abcd1234efgh5678""#, true),
("json", r#" "password": "hunter2hunter2" "#, true),
("json", r#" "apiKey": "${API_KEY}""#, false),
("json", r#" "password": "$DB_PASSWORD","#, false),
("json", r#" "password": "{{ db_password }}","#, false),
("json", r#" "password": null,"#, false),
("json", r#" "password": "","#, false),
("json", r#" "password": "${DB_PASSWORD}","#, false),
("json", r#"{"password": "${X}"}"#, false),
("json", r#"{"password": "${X}", "apiKey": ""}"#, false),
("json", r#"{"password": "", "token": "${T}"}"#, false),
(
"json",
r#"{"password": "${X}", "token": "hunter2hunter2xx"}"#,
true,
),
("json", r#"{"password": "hunter2hunter2xx"}"#, true),
("env", "DB_PASSWORD=hunter2hunter2", true),
("env", "API_KEY=abcd1234efgh5678", true),
("env", "DB_PASSWORD=", false),
("properties", "db.password=hunter2hunter2", true),
("properties", "db.password=", false),
("sh", r#"export DB_PASSWORD="hunter2hunter2""#, true),
("sh", "API_KEY='abcd1234efgh5678'", true),
("sh", r#"password="""#, false),
(
"sh",
r#"export DB_PASSWORD="hunter2hunter2" # note: dev"#,
true,
),
(
"sql",
"ALTER ROLE app SET password = 'hunter2hunter2';",
true,
),
("sql", "ALTER ROLE app SET password = '';", false),
(
"sql",
"CREATE USER app WITH PASSWORD 'hunter2hunter2';",
true,
),
("sql", "ALTER USER app PASSWORD 'hunter2hunter2';", true),
(
"sql",
"CREATE USER app IDENTIFIED BY 'hunter2hunter2';",
true,
),
(
"sql",
"CREATE USER app WITH PASSWORD 'hunter2hunter2'; -- note: temp",
true,
),
("sql", "CREATE USER app WITH PASSWORD '';", false),
("sql", "CREATE USER app WITH PASSWORD '%s';", false),
("sql", "CREATE USER app WITH PASSWORD '$1';", false),
("sql", "CREATE USER app WITH PASSWORD ':pw';", false),
("sql", "CREATE USER app WITH PASSWORD '{password}';", false),
("sql", "CREATE USER app IDENTIFIED BY '?';", false),
("sql", "CREATE USER app WITH PASSWORD NULL;", false),
("go", r#"q := "CREATE USER app WITH PASSWORD '%s'""#, false),
(
"py",
r#"q = f"CREATE USER app WITH PASSWORD '{pw}'""#,
false,
),
];
const KNOWN_GAPS: &[Row] = &[
("js", "const token = process.env.API_TOKEN;", true),
(
"js",
"const secret = process.env.SESSION_SECRET || fallbackValue;",
true,
),
("py", r#"password = os.environ["DB_PASSWORD"]"#, true),
("py", r#"password = os.getenv("DB_PASSWORD")"#, true),
("py", "password = get_password_from_vault()", true),
("rs", r#"let token = std::env::var("API_TOKEN")?;"#, true),
(
"rs",
r#"let token = std::env::var("API_TOKEN").unwrap_or_default();"#,
true,
),
("rs", r#"let password = String::new();"#, true),
(
"java",
r#"String password = System.getenv("DB_PASSWORD");"#,
true,
),
("kt", r#"val password = System.getenv("DB_PASSWORD")"#, true),
("env", "DB_PASSWORD=${DB_PASSWORD_FROM_VAULT}", true),
("properties", "db.password=${DB_PASSWORD}", true),
("sh", r#"export DB_PASSWORD="$VAULT_DB_PASSWORD""#, true),
];
#[test]
fn hardcoded_secret_table() {
let bad = mismatches(CASES);
assert!(
bad.is_empty(),
"{} of {} rows mismatched:\n{}",
bad.len(),
CASES.len(),
bad.join("\n")
);
}
#[test]
fn hardcoded_secret_known_gaps_keep_current_behaviour() {
let bad = mismatches(KNOWN_GAPS);
assert!(
bad.is_empty(),
"a KNOWN_GAPS row changed behaviour (fixed? move it into CASES):\n{}",
bad.join("\n")
);
}