Skip to content

Commit 78a6a2d

Browse files
author
Saiprashanth Pulisetti
committed
feat(cyber_security): add password_strength_checker CLI
1 parent 4d8db35 commit 78a6a2d

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env python3
2+
"""Password strength checker (entropy-based) with actionable feedback.
3+
4+
Usage:
5+
python -m cyber_security.password_strength_checker "P@ssw0rd!"
6+
python -m cyber_security.password_strength_checker --help
7+
"""
8+
from __future__ import annotations
9+
10+
import argparse
11+
import math
12+
import re
13+
import sys
14+
from dataclasses import dataclass
15+
16+
17+
@dataclass
18+
class StrengthResult:
19+
bits_of_entropy: float
20+
estimated_search_space: int
21+
category: str
22+
feedback: list[str]
23+
24+
25+
CHARSETS = {
26+
"lower": "abcdefghijklmnopqrstuvwxyz",
27+
"upper": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
28+
"digits": "0123456789",
29+
"symbols": "!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?`~",
30+
"space": " ",
31+
}
32+
33+
34+
def estimate_entropy(password: str) -> StrengthResult:
35+
if not password:
36+
return StrengthResult(0.0, 1, "empty", ["Password is empty."])
37+
38+
used = 0
39+
charspace = 0
40+
41+
def uses(charset: str) -> bool:
42+
return any(c in charset for c in password)
43+
44+
if uses(CHARSETS["lower"]):
45+
charspace += 26
46+
used += 1
47+
if uses(CHARSETS["upper"]):
48+
charspace += 26
49+
used += 1
50+
if uses(CHARSETS["digits"]):
51+
charspace += 10
52+
used += 1
53+
if uses(CHARSETS["symbols"]):
54+
charspace += len(CHARSETS["symbols"])
55+
used += 1
56+
if uses(CHARSETS["space"]):
57+
charspace += 1
58+
used += 1
59+
60+
# Fallback if classifier failed for some unusual unicode input
61+
if charspace == 0:
62+
unique_chars = len(set(password))
63+
charspace = max(unique_chars, 1)
64+
65+
length = len(password)
66+
# Shannon-style estimate: log2(charspace^length) = length * log2(charspace)
67+
entropy = length * math.log2(max(charspace, 1))
68+
69+
# Heuristics reduce entropy for common patterns
70+
penalties = 0.0
71+
common_subs = [("password", 10), ("1234", 8), ("qwerty", 8)]
72+
lower_pw = password.lower()
73+
for patt, pen in common_subs:
74+
if patt in lower_pw:
75+
penalties += pen
76+
if re.search(r"^(?:[A-Za-z]+\d+|\d+[A-Za-z]+)$", password):
77+
penalties += 5
78+
entropy = max(0.0, entropy - penalties)
79+
80+
# Category mapping
81+
if entropy < 28:
82+
category = "very weak"
83+
elif entropy < 36:
84+
category = "weak"
85+
elif entropy < 60:
86+
category = "moderate"
87+
elif entropy < 80:
88+
category = "strong"
89+
else:
90+
category = "very strong"
91+
92+
feedback: list[str] = []
93+
if length < 12:
94+
feedback.append("Use at least 12-16 characters.")
95+
if used < 3:
96+
feedback.append("Mix upper/lowercase, digits, and symbols.")
97+
if re.search(r"(.)\\1{2,}", password):
98+
feedback.append("Avoid repeated characters.")
99+
if re.search(r"(\d){4,}", password):
100+
feedback.append("Avoid long digit sequences.")
101+
102+
estimated_space = int(2 ** entropy) if entropy < 60 else int(10 ** (entropy / math.log2(10)))
103+
return StrengthResult(entropy, estimated_space, category, feedback)
104+
105+
106+
def parse_args(argv: list[str]) -> argparse.Namespace:
107+
parser = argparse.ArgumentParser(description="Estimate password strength and provide feedback.")
108+
parser.add_argument("password", nargs="?", help="Password string to evaluate. If omitted, read from stdin.")
109+
parser.add_argument("-q", "--quiet", action="store_true", help="Print only the entropy and category.")
110+
return parser.parse_args(argv)
111+
112+
113+
def main(argv: list[str] | None = None) -> int:
114+
args = parse_args(sys.argv[1:] if argv is None else argv)
115+
pwd = args.password
116+
if pwd is None:
117+
pwd = sys.stdin.read().rstrip("\n")
118+
119+
result = estimate_entropy(pwd)
120+
121+
if args.quiet:
122+
print(f"{result.bits_of_entropy:.2f} bits\t{result.category}")
123+
return 0
124+
125+
print(f"Entropy: {result.bits_of_entropy:.2f} bits")
126+
print(f"Estimated search space: ~{result.estimated_search_space}")
127+
print(f"Category: {result.category}")
128+
if result.feedback:
129+
print("Feedback:")
130+
for item in result.feedback:
131+
print(f" - {item}")
132+
return 0
133+
134+
135+
if __name__ == "__main__": # pragma: no cover
136+
raise SystemExit(main())

0 commit comments

Comments
 (0)