Compute 0–100 score and list of deduction reasons.
(password: str, policy: PolicyMode)
| 132 | |
| 133 | def _score(password: str, policy: PolicyMode) -> tuple[int, list[str]]: |
| 134 | """Compute 0–100 score and list of deduction reasons.""" |
| 135 | pol = _POLICY[policy] |
| 136 | score = 50 |
| 137 | deductions: list[str] = [] |
| 138 | |
| 139 | # Length |
| 140 | min_len = pol["min_length"] |
| 141 | pref_len = pol["preferred_length"] |
| 142 | if len(password) >= pref_len: |
| 143 | score += 25 |
| 144 | elif len(password) >= min_len: |
| 145 | score += 10 |
| 146 | else: |
| 147 | deficit = min_len - len(password) |
| 148 | score -= deficit * 5 |
| 149 | deductions.append(f"too short (minimum {min_len} chars)") |
| 150 | |
| 151 | # Character variety |
| 152 | varieties = sum([ |
| 153 | any(c.isupper() for c in password), |
| 154 | any(c.islower() for c in password), |
| 155 | any(c.isdigit() for c in password), |
| 156 | any(c in string.punctuation for c in password), |
| 157 | ]) |
| 158 | score += (varieties - 1) * 5 |
| 159 | |
| 160 | # Common password |
| 161 | if password.lower() in _COMMON_PASSWORDS: |
| 162 | score -= 40 |
| 163 | deductions.append("matches a known common password") |
| 164 | |
| 165 | # Patterns |
| 166 | patterns = _find_patterns(password) |
| 167 | score -= len(patterns) * 8 |
| 168 | deductions.extend(patterns) |
| 169 | |
| 170 | return max(0, min(100, score)), deductions |
| 171 | |
| 172 | |
| 173 | def _risk_label(score: int) -> str: |
| 174 | if score >= 80: |