mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-20 23:31:31 +02:00
178 lines
4.9 KiB
Python
178 lines
4.9 KiB
Python
import ipaddress
|
|
import re
|
|
|
|
import validators
|
|
from zxcvbn import zxcvbn
|
|
|
|
from app.config import settings
|
|
from shared.enums.target import TargetType
|
|
|
|
# Password policy
|
|
MIN_PASSWORD_SCORE = 3
|
|
MIN_PASSWORD_LENGTH = 10
|
|
|
|
|
|
# Username policy
|
|
MIN_USERNAME_LENGTH = 4
|
|
MAX_USERNAME_LENGTH = 50
|
|
|
|
# Hex color pattern
|
|
HEX_COLOR_LENGTH = 7 # e.g., #RRGGBB
|
|
|
|
|
|
def validate_password_strength(
|
|
password: str,
|
|
user_inputs: list[str] | None = None,
|
|
) -> str:
|
|
"""
|
|
Validate password strength using zxcvbn.
|
|
|
|
Args:
|
|
password: The password to validate
|
|
user_inputs: Optional list of user-specific inputs (email, username) to check against
|
|
|
|
Returns:
|
|
The validated password
|
|
|
|
Raises:
|
|
ValueError: If password doesn't meet requirements
|
|
"""
|
|
# Skip validation in DEBUG mode
|
|
if settings.DEBUG:
|
|
return password
|
|
|
|
if len(password) < MIN_PASSWORD_LENGTH:
|
|
return_error = (
|
|
f"Password must be at least {MIN_PASSWORD_LENGTH} characters long"
|
|
)
|
|
raise ValueError(return_error)
|
|
|
|
result = zxcvbn(password, user_inputs=user_inputs or [])
|
|
|
|
if result["score"] < MIN_PASSWORD_SCORE:
|
|
return_error = (
|
|
"Password is too weak. Consider using a stronger password with a "
|
|
"mix of uppercase, lowercase, numbers, and special characters."
|
|
)
|
|
raise ValueError(return_error)
|
|
|
|
if result["guesses_log10"] < MIN_PASSWORD_SCORE:
|
|
return_error = "Password is too guessable. Choose a less common password."
|
|
raise ValueError(return_error)
|
|
|
|
return password
|
|
|
|
|
|
def validate_username(username: str) -> str:
|
|
"""
|
|
Validate username format and length.
|
|
|
|
Args:
|
|
username: The username to validate
|
|
|
|
Returns:
|
|
The validated username
|
|
|
|
Raises:
|
|
ValueError: If username doesn't meet requirements
|
|
"""
|
|
if len(username) < MIN_USERNAME_LENGTH:
|
|
return_error = (
|
|
f"Username must be at least {MIN_USERNAME_LENGTH} characters long"
|
|
)
|
|
raise ValueError(return_error)
|
|
|
|
if len(username) > MAX_USERNAME_LENGTH:
|
|
return_error = f"Username must be at most {MAX_USERNAME_LENGTH} characters long"
|
|
raise ValueError(return_error)
|
|
|
|
# change the conditions if needed for organization specific but not recommended
|
|
# COndition: Must start with a letter
|
|
if not re.match(r"^[a-zA-Z]", username):
|
|
return_error = "Username must start with a letter"
|
|
raise ValueError(return_error)
|
|
|
|
# Condition: Can only contain letters, numbers, underscores, and dots
|
|
if not re.match(r"^[a-zA-Z][a-zA-Z0-9_.]*$", username):
|
|
return_error = (
|
|
"Username can only contain letters, numbers, underscores, and dots"
|
|
)
|
|
raise ValueError(return_error)
|
|
|
|
return username
|
|
|
|
|
|
def validate_domain(value: str) -> bool:
|
|
"""Validate domain/subdomain format"""
|
|
return validators.domain(value) is True
|
|
|
|
|
|
def validate_ip(value: str) -> bool:
|
|
"""Validate single IP address"""
|
|
try:
|
|
ipaddress.ip_address(value)
|
|
return True
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def validate_ip_range(value: str) -> bool:
|
|
"""Validate IP range in CIDR notation"""
|
|
try:
|
|
ipaddress.ip_network(value, strict=False)
|
|
return "/" in value
|
|
except ValueError:
|
|
return False
|
|
|
|
|
|
def validate_asn(value: str) -> bool:
|
|
"""Validate ASN format (AS followed by numbers)"""
|
|
asn_pattern = r"^AS\d+$"
|
|
return bool(re.match(asn_pattern, value.upper()))
|
|
|
|
|
|
def validate_url(value: str) -> bool:
|
|
"""Validate fully qualified URL"""
|
|
return validators.url(value) is True
|
|
|
|
|
|
def validate_target(target_value: str) -> TargetType | None:
|
|
"""
|
|
Validate and auto-detect target type from value.
|
|
This is our main validator for targets, each target MUST pass this validation before being accepted.
|
|
Also needs to be ported to frontend for immediate feedback to users by either copying the logic or creating an API endpoint for validation.
|
|
|
|
Returns: TargetType if valid, None if invalid
|
|
"""
|
|
validators = [
|
|
(TargetType.IP_RANGE, validate_ip_range),
|
|
(TargetType.IP, validate_ip),
|
|
(TargetType.URL, validate_url),
|
|
(TargetType.ASN, validate_asn),
|
|
(TargetType.DOMAIN, validate_domain),
|
|
]
|
|
|
|
for target_type, validator_func in validators:
|
|
if validator_func(target_value):
|
|
return target_type
|
|
|
|
return None
|
|
|
|
|
|
def validate_hex_color(color: str) -> str:
|
|
"""
|
|
Validate hex color format (#RRGGBB).
|
|
"""
|
|
color = color.strip()
|
|
if not color.startswith("#"):
|
|
msg = "Color must start with #"
|
|
raise ValueError(msg)
|
|
if len(color) != HEX_COLOR_LENGTH:
|
|
msg = f"Color must be in format #RRGGBB ({HEX_COLOR_LENGTH} characters)"
|
|
raise ValueError(msg)
|
|
hex_pattern = r"^#[0-9A-Fa-f]{6}$"
|
|
if not re.match(hex_pattern, color):
|
|
msg = "Color must contain valid hexadecimal characters (0-9, A-F)"
|
|
raise ValueError(msg)
|
|
return color.upper()
|