|
8 | 8 |
|
9 | 9 | /** |
10 | 10 | * @typedef {'critical'|'high'|'medium'|'low'} Severity |
11 | | - * @typedef {{ id:string, title:string, severity:Severity, pattern:RegExp, secretGroup?:number, entropy?:number }} Rule |
| 11 | + * @typedef {{ id:string, title:string, severity:Severity, pattern:RegExp, secretGroup?:number, entropy?:number, deny?:RegExp }} Rule |
| 12 | + * |
| 13 | + * `deny` (optional): if the captured secret matches this RegExp in full, the |
| 14 | + * match is discarded. Used to drop well-known placeholder values that clear the |
| 15 | + * entropy gate (e.g. `your-password`). |
12 | 16 | */ |
13 | 17 |
|
14 | 18 | /** |
@@ -235,6 +239,32 @@ export const RULES = [ |
235 | 239 | secretGroup: 1, |
236 | 240 | }, |
237 | 241 |
|
| 242 | + // ── Labeled password in prose (scoped, low floor) ───────────────────────── |
| 243 | + // Pasting a credential as "Password: <value>" is the single most common human |
| 244 | + // leak, and it slips under the generic rule's 16-char floor. Here the label |
| 245 | + // itself is the signal, so we run a lower length floor (8) with only a modest |
| 246 | + // entropy gate (3.0) — enough to drop fixed-char placeholders (xxxxxxxx → 0 |
| 247 | + // bits) but not to require token-grade randomness. |
| 248 | + // |
| 249 | + // `pwd` is deliberately NOT a keyword: transcripts routinely contain |
| 250 | + // `printenv`/`env` output, and `PWD=/home/...` (a path that clears the entropy |
| 251 | + // gate) would be a systematic false positive. `password|passwd|passphrase` |
| 252 | + // only. |
| 253 | + // |
| 254 | + // Genuinely weak human passwords (changeme ≈ 2.75, hunter2 ≈ 2.81) are left |
| 255 | + // uncaught on purpose: they sit in the same entropy band as — and below — |
| 256 | + // common placeholders (your-password ≈ 3.24), so no threshold separates them. |
| 257 | + // The `deny` list removes the placeholder strings that do clear the gate. |
| 258 | + { |
| 259 | + id: 'labeled-password', |
| 260 | + title: 'Labeled password or passphrase', |
| 261 | + severity: 'medium', |
| 262 | + pattern: /\b(?:password|passwd|passphrase)\s*[:=]\s*["']?([A-Za-z0-9+/=_\-!@#$%^&*]{8,128})["']?/gid, |
| 263 | + secretGroup: 1, |
| 264 | + entropy: 3, |
| 265 | + deny: /^(?:your[-_]?password(?:[-_]?here)?|password\d*|examplepassword|example|placeholder|changeme\d*|redacted|<.*>|\*+|x+)$/i, |
| 266 | + }, |
| 267 | + |
238 | 268 | // ── Generic secret assignment (entropy-gated, runs last) ───────────────── |
239 | 269 | // Catches unknown key formats assigned to recognisably secret-named variables. |
240 | 270 | // Only fires when the value's Shannon entropy ≥ 3.5 bits/char, which eliminates |
|
0 commit comments