Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions Python/password_strength_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import re
import math
import getpass
from typing import Tuple, List

class PasswordStrengthChecker:
def __init__(self):
# Expanded common passwords list
self.common_passwords = {
'password', '123456', '12345678', '1234', 'qwerty', '12345',
'dragon', 'baseball', 'football', 'letmein', 'monkey', 'abc123',
'shadow', 'master', 'jennifer', '111111','2000', 'superman',
'1234567', 'freedom','batman', 'trustno1', 'admin', 'welcome',
'password1', '123123'
}

# Strength thresholds
self.strength_levels = {
0: ("Very Weak", "🔴"),
1: ("Weak", "🟠"),
2: ("Fair", "🟡"),
3: ("Good", "🟢"),
4: ("Strong", "🟢"),
5: ("Very Strong", "🟢")
}

def calculate_entropy(self, password: str) -> float:
"""Calculate password entropy to measure randomness"""
if not password:
return 0

# Character pool size estimation
char_pool = 0
if re.search(r'[a-z]', password):
char_pool += 26
if re.search(r'[A-Z]', password):
char_pool += 26
if re.search(r'[0-9]', password):
char_pool += 10
if re.search(r'[^A-Za-z0-9]', password):
char_pool += 32 # Common special characters

if char_pool == 0:
return 0

# Entropy formula: log2(pool_size^length)
entropy = len(password) * math.log2(char_pool)
return entropy

def check_common_patterns(self, password: str) -> List[str]:
"""Check for common patterns and sequences"""
patterns = []

# Repeated characters
if re.search(r'(.)\1{2,}', password):
patterns.append("Avoid repeated characters (e.g., 'aaa')")

# Sequential characters
sequences = ['abc', '123', 'qwe', 'asd', 'zxc']
password_lower = password.lower()
for seq in sequences:
if seq in password_lower or seq[::-1] in password_lower:
patterns.append(f"Avoid sequential patterns like '{seq}'")
break

# Repeated substrings
if len(password) >= 6:
for i in range(3, len(password)//2 + 1):
for j in range(len(password) - i*2 + 1):
substring = password[j:j+i]
if password.count(substring) >= 2:
patterns.append("Avoid repeated character sequences")
return patterns

return patterns

def check_password_strength(self, password: str) -> Tuple[str, str, List[str], int]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider refactoring the password strength checker by splitting large methods into smaller private scorers and moving I/O logic out of the class.

Here are a few focused refactorings that will keep all behavior the same but dramatically reduce nesting and size:

  1. Split your one big check_password_strength into small private scorers:
class PasswordStrengthChecker:
    def __init__(…): …

    def _score_length(self, pwd: str) -> Tuple[int, List[str]]:
        score, fb = 0, []
        if len(pwd) >= 16:
            score += 2; fb.append("✅ Excellent password length")
        elif len(pwd) >= 12:
            score += 1; fb.append("✅ Good password length")
        elif len(pwd) >= 8:
            score += 1; fb.append("⚠️  Consider using 12+ characters")
        else:
            fb.append("❌ Password should be at least 8 characters long")
        return score, fb

    def _score_diversity(self, pwd: str) -> Tuple[int, List[str]]:
        rules = [
            (r'[A-Z]', "uppercase letters"),
            (r'[a-z]', "lowercase letters"),
            (r'\d',    "numbers"),
            (r'[^A-Za-z0-9]', "special characters"),
        ]
        score, fb = 0, []
        for patt, name in rules:
            if re.search(patt, pwd):
                score += 1; fb.append(f"✅ Contains {name}")
            else:
                fb.append(f"❌ Include {name}")
        return score, fb

    def _apply_common_password_penalty(self, pwd: str) -> Tuple[int, List[str]]:
        norm = re.sub(r'[^a-z0-9]', '', pwd.lower())
        if pwd.lower() in self.common_passwords or norm in self.common_passwords:
            return -2, ["❌ This is a commonly used password"]
        return 0, []

    def _score_entropy(self, pwd: str) -> Tuple[int, List[str]]:
        ent = self.calculate_entropy(pwd)
        if ent >= 60:
            return 1, ["✅ High entropy"]
        if ent >= 40:
            return 0, ["⚠️  Moderate entropy"]
        return 0, ["❌ Low entropy"]

Then your check_password_strength becomes:

def check_password_strength(self, pwd: str):
    total, feedback = 0, []
    for scorer in (self._score_length,
                   self._score_diversity,
                   self._apply_common_password_penalty,
                   self._score_entropy):
        sc, fb = scorer(pwd)
        total += sc
        feedback.extend(fb)

    patterns = self.check_common_patterns(pwd)
    if patterns:
        total = max(0, total - 1)
        feedback.extend(patterns)

    total = min(max(total, 0), 5)
    strength, emoji = self.strength_levels[total]
    return strength, emoji, feedback, total
  1. Simplify check_common_patterns with any() and comprehensions:
def check_common_patterns(self, pwd: str) -> List[str]:
    lower = pwd.lower()
    out = []
    if re.search(r'(.)\1{2,}', pwd):
        out.append("Avoid repeated characters")
    seqs = ['abc','123','qwe','asd','zxc']
    if any(s in lower or s[::-1] in lower for s in seqs):
        out.append("Avoid sequential patterns")
    # repeated substrings of length ≥3
    if any(pwd.count(sub) > 1
           for length in range(3, len(pwd)//2+1)
           for sub in {pwd[i:i+length] for i in range(len(pwd)-length+1)}):
        out.append("Avoid repeated substrings")
    return out
  1. Pull all print and getpass logic out of this class into a separate password_cli.py (or similar). Your checker stays pure-logic and returns data; the CLI module handles I/O.

"""Comprehensive password strength analysis"""
score = 0
feedback = []

# Length scoring (more granular)
if len(password) >= 16:
score += 2
feedback.append("✅ Excellent password length")
elif len(password) >= 12:
score += 1
feedback.append("✅ Good password length")
elif len(password) >= 8:
score += 1
feedback.append("⚠️ Consider using 12+ characters for better security")
else:
feedback.append("❌ Password should be at least 8 characters long")

# Character diversity scoring
checks = {
'uppercase': (r'[A-Z]', "Include uppercase letters"),
'lowercase': (r'[a-z]', "Include lowercase letters"),
'numbers': (r'[0-9]', "Include numbers"),
'special': (r'[^A-Za-z0-9]', "Include special characters")
}

for check_type, (pattern, message) in checks.items():
if re.search(pattern, password):
score += 1
feedback.append(f"✅ Contains {check_type} characters")
else:
feedback.append(f"❌ {message}")

# Common password check (improved)
normalized_password = re.sub(r'[^a-z0-9]', '', password.lower())
if (password.lower() in self.common_passwords or
normalized_password in self.common_passwords):
score = max(0, score - 2) # Heavy penalty for common passwords
feedback.append("❌ This is a commonly used password - choose something more unique")

# Entropy check
entropy = self.calculate_entropy(password)
if entropy >= 60:
score += 1
feedback.append("✅ High entropy - excellent randomness")
elif entropy >= 40:
feedback.append("⚠️ Moderate entropy - consider more random characters")
else:
feedback.append("❌ Low entropy - password is too predictable")

# Pattern checks
pattern_feedback = self.check_common_patterns(password)
if pattern_feedback:
score = max(0, score - 1)
feedback.extend(pattern_feedback)

# Cap score at maximum
score = min(score, 5)

# Get strength rating
strength, emoji = self.strength_levels.get(score, ("Very Weak", "🔴"))

return strength, emoji, feedback, score

def get_strength_breakdown(self, password: str):
"""Provide detailed strength analysis"""
strength, emoji, feedback, score = self.check_password_strength(password)

print(f"\n{'='*50}")
print(f"Password Strength: {strength} {emoji} ({score}/5)")
print(f"{'='*50}")

# Prioritize feedback - show most critical first
critical = [f for f in feedback if f.startswith('❌')]
warnings = [f for f in feedback if f.startswith('⚠️')]
positive = [f for f in feedback if f.startswith('✅')]

if critical:
print("\nCritical Issues:")
for item in critical[:3]: # Show top 3 critical issues
print(f" {item}")

if warnings:
print("\nSuggestions:")
for item in warnings[:3]: # Show top 3 suggestions
print(f" {item}")

if positive and not critical: # Only show positives if no critical issues
print("\nStrengths:")
for item in positive[:3]:
print(f" {item}")
Comment on lines +164 to +167
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Positive feedback is suppressed when critical issues exist.

Consider displaying strengths alongside critical issues to provide more balanced feedback.

Suggested change
if positive and not critical: # Only show positives if no critical issues
print("\nStrengths:")
for item in positive[:3]:
print(f" {item}")
if positive:
print("\nStrengths:")
for item in positive[:3]:
print(f" {item}")


def interactive_mode():
"""Interactive password checking with retries"""
checker = PasswordStrengthChecker()

print("🔐 Advanced Password Strength Checker")
print("=" * 40)

while True:
password = getpass.getpass("\nEnter password to check (or 'quit' to exit): ")

if password.lower() == 'quit':
break

if not password:
print("❌ Please enter a password")
continue

checker.get_strength_breakdown(password)

# Offer to check another password
continue_check = input("\nCheck another password? (y/n): ").lower()
if continue_check != 'y':
break

def quick_check_mode(password: str):
"""Quick check mode for programmatic use"""
checker = PasswordStrengthChecker()
strength, emoji, feedback, score = checker.check_password_strength(password)

return {
'strength': strength,
'emoji': emoji,
'score': score,
'feedback': feedback
}

# Test function
def test_passwords():
"""Test various password examples"""
test_cases = [
"password", # Very weak
"123456", # Very weak
"Password1", # Weak
"Password123", # Fair
"SecurePass123!", # Good
"MyV3ryS3cur3P@ssw0rd2024!" # Strong
]

checker = PasswordStrengthChecker()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused variable.

The checker variable is assigned but never used. Line 223 calls quick_check_mode(pwd) which creates its own checker instance internally.

Apply this diff to remove the unused variable:

-    checker = PasswordStrengthChecker()
-    
     print("Testing Password Examples:")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
checker = PasswordStrengthChecker()
print("Testing Password Examples:")
🧰 Tools
🪛 Ruff (0.14.1)

217-217: Local variable checker is assigned to but never used

Remove assignment to unused variable checker

(F841)

🤖 Prompt for AI Agents
In Python/password_strength_checker.py around line 217, the variable `checker`
is created but never used (quick_check_mode(pwd) makes its own checker); remove
the unused assignment `checker = PasswordStrengthChecker()` from that location
so there is no dead variable left in the code and ensure surrounding code still
calls quick_check_mode(pwd) as before.


print("Testing Password Examples:")
print("=" * 50)

for pwd in test_cases:
result = quick_check_mode(pwd)
print(f"\nPassword: {pwd}")
print(f"Strength: {result['strength']} {result['emoji']} ({result['score']}/5)")
Comment on lines +222 to +225
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Avoid loops in tests. (no-loop-in-tests)

ExplanationAvoid complex code, like loops, in test functions.

Google's software engineering guidelines says:
"Clear tests are trivially correct upon inspection"
To reach that avoid complex code in tests:

  • loops
  • conditionals

Some ways to fix this:

  • Use parametrized tests to get rid of the loop.
  • Move the complex logic into helpers.
  • Move the complex part into pytest fixtures.

Complexity is most often introduced in the form of logic. Logic is defined via the imperative parts of programming languages such as operators, loops, and conditionals. When a piece of code contains logic, you need to do a bit of mental computation to determine its result instead of just reading it off of the screen. It doesn't take much logic to make a test more difficult to reason about.

Software Engineering at Google / Don't Put Logic in Tests


if __name__ == "__main__":
# Run in interactive mode by default
interactive_mode()

# Uncomment to run tests
# test_passwords()