| 102 | |
| 103 | def _find_patterns(password: str) -> list[str]: |
| 104 | found: list[str] = [] |
| 105 | pw_lower = password.lower() |
| 106 | |
| 107 | # Repeated characters (aaa, 111) |
| 108 | if re.search(r"(.)\1{2,}", password): |
| 109 | found.append("repeated characters (e.g. 'aaa')") |
| 110 | |
| 111 | # Sequential letters/numbers |
| 112 | for i in range(len(pw_lower) - 2): |
| 113 | chunk = pw_lower[i : i + 3] |
| 114 | if chunk in _SEQUENCES or chunk[::-1] in _SEQUENCES: |
| 115 | found.append("sequential characters") |
| 116 | break |
| 117 | |
| 118 | # Keyboard runs |
| 119 | for run in _KEYBOARD_RUNS: |
| 120 | if run in pw_lower: |
| 121 | found.append(f"keyboard pattern ('{run}')") |
| 122 | break |
| 123 | |
| 124 | # All digits |
| 125 | if password.isdigit(): |
| 126 | found.append("all numeric") |
| 127 | |
| 128 | return found |
| 129 | |
| 130 | |
| 131 | # ── Scorer ───────────────────────────────────────────────────────────────────── |
| 132 | |