-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
139 lines (116 loc) · 5.78 KB
/
cli.py
File metadata and controls
139 lines (116 loc) · 5.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
"""
CLI for the password auditing tool.
"""
from __future__ import annotations
import argparse
import json
import getpass
import sys
import unicodedata
from audit import analyze_password, load_wordlist
MAX_PASSWORD_LENGTH = 512
def _has_illegal_control_characters(value: str) -> bool:
"""Return True if value contains non-printable/control characters."""
return any((ord(ch) < 32 and ch != " ") or not ch.isprintable() for ch in value)
try:
from termcolor import colored
HAS_COLOR = True
except ImportError:
HAS_COLOR = False
def colored(text, color=None, on_color=None, attrs=None):
return text
def main():
parser = argparse.ArgumentParser(description="Password auditing tool")
group = parser.add_mutually_exclusive_group(required=False)
group.add_argument("-p", "--password", help="Single password to analyze (optional - will prompt if not provided)")
group.add_argument("-f", "--file", help="File with one password per line")
parser.add_argument("-g", "--guesses", type=float, default=1e9,
help="Guesses per second attacker can try (default: 1e9)")
parser.add_argument("--json", action="store_true", help="Output JSON")
parser.add_argument(
"-w",
"--wordlist",
default="Wordlists/100k-most-used-passwords-NCSC.txt",
help="Path to password wordlist (default: Wordlists/100k-most-used-passwords-NCSC.txt)",
)
args = parser.parse_args()
results = []
wordlist_set = load_wordlist(args.wordlist)
# Handle file input
if args.file:
with open(args.file, "r", encoding="utf-8") as fh:
for line in fh:
pw = line.strip()
if not pw:
continue
if len(pw) > MAX_PASSWORD_LENGTH:
print(
f"Error: Input exceeds the maximum allowed length of "
f"{MAX_PASSWORD_LENGTH} characters."
)
sys.exit(1)
if _has_illegal_control_characters(pw):
print("Error: Illegal control characters detected in input.")
sys.exit(1)
pw = unicodedata.normalize("NFKC", pw)
results.append(analyze_password(pw, args.guesses, wordlist_set))
# Handle single password input
else:
# If password not provided via command line, prompt securely
if args.password:
password = args.password
else:
# Use getpass for secure, masked input (bypasses shell interpretation)
password = getpass.getpass("Enter password to audit (input hidden): ")
password = password.strip()
if len(password) > MAX_PASSWORD_LENGTH:
print(
f"Error: Input exceeds the maximum allowed length of "
f"{MAX_PASSWORD_LENGTH} characters."
)
sys.exit(1)
if _has_illegal_control_characters(password):
print("Error: Illegal control characters detected in input.")
sys.exit(1)
password = unicodedata.normalize("NFKC", password)
if not password:
print("Error: Password cannot be empty")
return
results.append(analyze_password(password, args.guesses, wordlist_set))
if args.json:
print(json.dumps(results, indent=2, ensure_ascii=False))
else:
for r in results:
sec = r.get("security_level", {})
emoji = sec.get("emoji", "❓")
level = sec.get("level", "UNKNOWN")
print(colored("╔════════════════════════════════════════╗", "cyan"))
print(colored(f"║ {emoji} Password Analysis - {level:<20} ║", "cyan"))
print(colored("╚════════════════════════════════════════╝", "cyan"))
print(colored("Basic Information:", "cyan"))
print(f" Password: {colored(r['password'], 'yellow')}")
print(f" Length: {colored(str(r['length']), 'green')}")
print(colored("\nEntropy Analysis:", "cyan"))
entropy_str = f"{r['entropy_bits']:.2f} bits"
shannon_str = f"{r['shannon_bits']:.2f} bits"
print(f" Pool Entropy: {colored(entropy_str, 'yellow')}")
print(f" Shannon Entropy: {colored(shannon_str, 'yellow')}")
print(colored("\nSecurity Level:", "cyan"))
print(f" {emoji} {colored(level, 'yellow')} - {sec.get('description', '')}")
print(f" Recommendation: {sec.get('recommendation', '')}")
print(colored("\nCrack Time (Brute Force):", "cyan"))
print(f" Standard CPU: {colored(r['crack_scenarios']['cpu_multi']['max_time_human'], 'yellow')} (max)")
print(f" Single GPU: {colored(r['crack_scenarios']['gpu_single']['max_time_human'], 'yellow')} (max)")
print(f" Large Botnet: {colored(r['crack_scenarios']['distributed']['max_time_human'], 'yellow')} (max)")
print(colored("\nWith Hash Protection:", "cyan"))
print(f" bcrypt: {colored(r['with_hash_protection']['bcrypt']['crack_time_human'], 'green')}")
print(f" Argon2: {colored(r['with_hash_protection']['argon2']['crack_time_human'], 'green')}")
if r.get("issues"):
print(colored("\n⚠️ Issues Found:", "red"))
for issue in r['issues']:
print(f" • {colored(issue, 'red')}")
else:
print(colored("\n✅ No issues detected!", "green"))
print()
if __name__ == "__main__":
main()