# SQL `IDENTIFIED BY '<password>'` credential, the plaintext login password in
# a `CREATE USER` / `ALTER USER` / `GRANT` statement (MySQL/MariaDB and the same
# syntax echoed by Postgres roles and many ORMs/migration scripts).
#
# Why this detector:
# On the CredData benchmark the "SQL Password" class is ~39 labeled positives
# whose value is, every time, the quoted string after `IDENTIFIED BY`
# (`IDENTIFIED BY 'argriyjqr'`, `IDENTIFIED WITH mysql_native_password BY
# 'bjy-jbpuyowe'`). keyhog caught 0 of them: the value is NOT a `keyword=value`
# assignment, so the generic bridge never produced a candidate, and there was
# no SQL-shaped detector. `IDENTIFIED BY` is an unambiguous credential context
#, you do not write it except to set a login password, so the quoted token
# immediately after it is a precise, recall-safe anchor. As a strong-anchor
# member of the structural-password-slot family (NOT weak_anchor), the Tier-B
# randomness floor is skipped so short low-alpha passwords surface; placeholder
# values (`'password'`, `'secret'`, `'changeme'`) are dropped by the
# `dictionary_word_placeholder` gate (suppression/api.rs) and the `{6,128}`
# value floor.
#
# The captured group is the plaintext password (the credential). The `AS '<hash>'`
# form (a pre-hashed `mysql_native_password` digest) is deliberately NOT matched
#, it is a hash, not a recoverable secret, and `*`-prefixed digests would dilute
# precision.
#
# Verify: no verification endpoint (the database host/port are unknown).
[detector]
id = "sql-password"
name = "SQL IDENTIFIED BY Password"
service = "generic"
severity = "high"
ml = { match_mode = "lift", entropy_mode = "disabled", weight = 1.0, context_radius_lines = 5 }
match_confidence = { literal_prefix_weight = 0.35, context_anchor_weight = 0.20, entropy_weight = 0.20, high_entropy_partial_weight = 0.12, moderate_entropy_threshold = 3.0, moderate_entropy_weight = 0.05, low_entropy_penalty_floor = 2.0, low_entropy_min_match_length = 10, low_entropy_penalty_multiplier = 0.60, keyword_nearby_weight = 0.10, sensitive_file_weight = 0.10, companion_weight = 0.05, very_high_entropy_margin = 1.2999999999999998, named_anchor_floor = 0.55, assignment_context_multiplier = 1.0, string_literal_context_multiplier = 0.9, unknown_context_multiplier = 0.8, documentation_context_multiplier = 0.3, comment_context_multiplier = 0.4, test_context_multiplier = 0.3, encrypted_context_multiplier = 0.05, soft_context_suppression_threshold = 0.5, encrypted_context_suppression_threshold = 0.8, post_match = { placeholder_multiplier = 0.05, minimum_byte_diversity = 0.1, low_diversity_multiplier = 0.1, maximum_repeat_ratio = 0.8, degenerate_run_min_length = 10, degenerate_repeat_multiplier = 0.1, fixture_path_multiplier = 0.5, ml_context_reapply_below = 0.95 } }
keywords = ["IDENTIFIED", "identified", "Identified"]
# Strong-anchor free-form password slot: apply the placeholder-word gate, skip
# the Tier-B randomness floor (family declared per-detector, see DetectorSpec).
structural_password_slot = true
[[detector.patterns]]
# `IDENTIFIED [WITH <plugin>] BY '<password>'`: single-quoted. The optional
# `WITH <plugin>` clause (`mysql_native_password`, `caching_sha2_password`, …)
# may itself be quoted. `[^']` lets an internal double-quote live inside the
# value; the 4-char floor keeps empty/one-char noise out.
regex = '''(?i)identified\s+(?:with\s+'?[a-z0-9_]+'?\s+)?by\s+'([^']{6,128})\''''
description = "plaintext password after a single-quoted SQL IDENTIFIED BY clause"
group = 1
[[detector.patterns]]
# Same clause, double-quoted value (`IDENTIFIED BY "5q'jK3d7ca"`). `[^"]` lets an
# internal single-quote live inside the value.
regex = '''(?i)identified\s+(?:with\s+'?[a-z0-9_]+'?\s+)?by\s+"([^"]{6,128})"'''
description = "plaintext password after a double-quoted SQL IDENTIFIED BY clause"
group = 1
# No verify block (generic detector, database endpoint unknown).
[[detector.tests]]
test_positive = "CREATE USER 'svc'@'localhost' IDENTIFIED BY 'Xy9KmPq2LvWnB7tR';"
test_negative = "CREATE USER 'x'@'localhost' IDENTIFIED BY 'password';"