DefendKing
A defensive-only Python security toolkit (~70 functions across 9 sections) covering passwords, phishing/URL safety, brute-force protection, file hygiene, web-app security, network checks, cryptography, monitoring/ reporting, and API/session security.
Every function protects, detects, or reports — none of them attack, exploit, or access systems you don't own/manage.
Author: Barman License: MIT Current version: 3.6.17
Table of contents
- Install
- Quick start
- Error handling (read this first)
- 1. Passwords & authentication
- 2. Phishing & URL/email safety
- 3. Brute-force / abuse protection
- 4. File / malware hygiene
- 5. Web application security
- 6. Network-level defense
- 7. Cryptography & secrets management
- 8. Monitoring, risk scoring & reporting
- 9. API & session security
- Known limitations
- Changelog
Install
pip install defendking # core package, no third-party deps required
pip install "defendking[full]" # adds requests / cryptography / pyjwt for the
# functions that call external APIs or do AES/JWT
pip install "defendking[dev]" # adds pytest for running the test suite
For local development (editable install from a clone):
pip install -e ".[full,dev]"
python -m pytest test/ -v
ruff check .
mypy defendking/
CI/CD
Every push to main runs:
- test — the full test suite on Python 3.9-3.12
- lint —
ruffandmypy
Pushing a version tag (e.g. v3.6.16) additionally triggers publish,
which builds and uploads the package to PyPI via
Trusted Publishing (OIDC) —
no API token is stored anywhere. See .github/workflows/ci.yml.
Quick start
import defendking as dk
result = dk.check_password_strength("Tr0ub4dor&3")
print(result)
# {'score': 64, 'label': 'medium', 'entropy_bits': 65.4, 'suggestions': [...]}
url_check = dk.check_suspicious_url("http://192.168.1.1@paypal-login.tk/verify")
print(url_check["suspicious"], url_check["risk_score"])
# True 85
Everything is also importable by name:
from defendking import check_password_strength, generate_strong_password
Error handling
DefendKing follows one consistent rule everywhere (see defendking.exceptions):
- Programmer errors (wrong argument type, empty required value, value
out of range) raise
InvalidInputErrorimmediately. Catch it like:from defendking import InvalidInputError try: dk.generate_strong_password(length=2) except InvalidInputError as e: print("bad input:", e)
- External-world failures (network down, DNS failed, file missing):
- Functions that already return a dict report the failure inside that
dict —
error/error_type: "ExternalServiceError"— and do not raise.result = dk.check_ssl_certificate("unreachable-host.example") if not result["valid"]: print(result["error"])
- Functions whose normal return is a plain scalar (a hash string, a
bool) raise
ExternalServiceErrorinstead, since silently turning their return type into a dict would break every call site.from defendking import ExternalServiceError try: dk.compute_file_hash("missing.txt") except ExternalServiceError as e: print("could not hash file:", e)
- Functions that already return a dict report the failure inside that
dict —
Every function's docstring says explicitly which behavior applies to it.
1. Passwords & authentication
check_password_strength(password: str) -> dict
Scores a password 0-100 using length, character diversity, estimated entropy (bits), and known weak-pattern detection.
dk.check_password_strength("correct horse battery staple 42!")
# {'score': 100, 'label': 'strong', 'entropy_bits': 112.8, 'suggestions': []}
check_password_breached(password: str, use_api: bool = True) -> dict
Checks a password against Have I Been Pwned (k-anonymity model — only a hash prefix is sent) with a local-list fallback.
dk.check_password_breached("123456")
# {'breached': True, 'times_seen': 37810483, 'source': 'hibp'}
generate_strong_password(length: int = 16, use_symbols: bool = True) -> str
dk.generate_strong_password(20)
# 'xQ2!kP9$mZr7@vLwT4nB'
hash_password(password: str) -> str / verify_password(password, stored_hash) -> bool
PBKDF2-HMAC-SHA256 password hashing with a random salt.
stored = dk.hash_password("my secret password")
dk.verify_password("my secret password", stored) # True
dk.verify_password("wrong guess", stored) # False
detect_common_password_patterns(password: str) -> list[str]
Flags keyboard walks, sequences, repeats, dates, and leetspeak substitutions.
dk.detect_common_password_patterns("Qwerty123")
# ['Contains a keyboard-walk pattern', 'Likely contains a birth year or date']
check_two_factor_status(is_enabled_flag: bool, backup_codes_count: int = 0) -> dict
dk.check_two_factor_status(True, backup_codes_count=1)
# {'enabled': True, 'has_backup_codes': True, 'low_on_backup_codes': True, 'recommendation': 'Good posture'}
check_password_expiry(last_changed: datetime, max_age_days: int = 90) -> dict
from datetime import datetime, timedelta, timezone
dk.check_password_expiry(datetime.now(timezone.utc) - timedelta(days=95))
# {'age_days': 95, 'max_age_days': 90, 'expired': True, ...}
validate_password_policy_compliance(password: str, policy: dict = None) -> dict
dk.validate_password_policy_compliance("alllowercase")
# {'compliant': False, 'violations': ['An uppercase letter is required', ...]}
check_default_credentials(username: str, password: str) -> bool
dk.check_default_credentials("admin", "admin") # True
2. Phishing & URL/email safety
check_suspicious_url(url: str) -> dict
Regex/heuristic phishing signal detection with a weighted risk_score
(0-100): raw IPs, punycode, homograph domains, open-redirect params,
brand impersonation, and more.
dk.check_suspicious_url("http://192.168.1.1@paypal-login.tk/verify")
# {'suspicious': True, 'risk_score': 100, 'reasons': [...]}
check_domain_similarity(domain: str, trusted_domains: list[str]) -> dict
Typosquat detection via Levenshtein distance, homoglyph normalization
(rn→m, 1→l, ...), substring spoofing, and TLD-swap detection.
dk.check_domain_similarity("paypa1.com", ["paypal.com"])
# {'likely_typosquat': True, 'matched_via_homoglyph_normalization': True, ...}
check_ssl_certificate(hostname: str, port: int = 443, timeout: float = 5.0) -> dict
dk.check_ssl_certificate("example.com")
# {'valid': True, 'expires': '...', 'days_until_expiry': 62, 'expiring_soon': False}
scan_email_for_phishing_signs(email_text: str) -> dict
Weighted-score scan for urgency language, credential harvesting, unusual payment requests, BEC/executive-impersonation patterns, and more.
dk.scan_email_for_phishing_signs("Dear customer, act now and click here to verify your account: http://bit.ly/xyz")
# {'suspicious': True, 'risk_score': 60, 'reasons': [...]}
extract_and_check_links(text: str) -> list[dict]
dk.extract_and_check_links("Check this out: http://bit.ly/xyz")
# [{'url': 'http://bit.ly/xyz', 'suspicious': True, ...}]
validate_email_format_and_mx(email: str, check_mx: bool = True) -> dict
dk.validate_email_format_and_mx("test@example.com", check_mx=False)
# {'email': 'test@example.com', 'valid_format': True, 'has_mx_record': None, 'is_disposable_domain': False}
3. Brute-force / abuse protection
RateLimiter(max_attempts=5, window_seconds=300)
limiter = dk.RateLimiter(max_attempts=3, window_seconds=60)
limiter.allow("1.2.3.4") # True, True, True, then False on the 4th call
limiter.reset("1.2.3.4") # clear after a successful login
rate_limiter(key, store, max_attempts=5, window_seconds=300) -> bool
Functional variant for callers managing their own storage dict.
detect_brute_force_attempt(failed_timestamps: list[float], threshold=5, window_seconds=60) -> bool
dk.detect_brute_force_attempt([t, t+1, t+2, t+3, t+4], threshold=5, window_seconds=60)
# True
IPBlocklist()
blocklist = dk.IPBlocklist()
blocklist.block("10.0.0.1", duration_seconds=900)
blocklist.is_blocked("10.0.0.1") # True
blocklist.unblock("10.0.0.1")
implement_captcha_trigger(failed_attempts, threshold=3, window_seconds=None, failed_timestamps=None) -> bool
dk.implement_captcha_trigger(failed_attempts=3, threshold=3) # True
log_failed_login_attempt(username, ip, log_path="failed_logins.csv") -> dict
dk.log_failed_login_attempt("baduser", "1.2.3.4")
# {'success': True, 'path': 'failed_logins.csv'}
distance_km_between(lat1, lon1, lat2, lon2) -> float
Haversine great-circle distance.
dk.distance_km_between(35.6892, 51.3890, 40.7128, -74.0060)
# 9877.5 (Tehran to New York, km)
detect_anomalous_login_location(previous_country, new_country, previous_time, new_time, previous_coords=None, new_coords=None) -> dict
"Impossible travel" detector.
dk.detect_anomalous_login_location(
"IR", "US", t1, t2,
previous_coords=(35.6892, 51.3890), new_coords=(40.7128, -74.0060),
)
# {'anomalous': True, 'high_confidence': True, 'implied_speed_kmh': 118530.0, ...}
detect_privilege_escalation_attempt(role_before, role_after, allowed_transitions: dict) -> bool
dk.detect_privilege_escalation_attempt("viewer", "admin", {"viewer": {"editor"}})
# True (not an allowed transition)
4. File / malware hygiene
compute_file_hash(filepath, algorithm="sha256") -> str
dk.compute_file_hash("report.pdf")
# 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3...'
scan_file_hash_virustotal(filepath, api_key) -> dict
Looks up a file's hash on VirusTotal (does not upload the file).
detect_suspicious_file_extension(filename: str) -> dict
Flags dangerous extensions, double extensions, RTL-override tricks, and padded/hidden extensions.
dk.detect_suspicious_file_extension("invoice.pdf.exe")
# {'flagged': True, 'double_extension_trick': True, ...}
check_file_integrity(filepath, known_good_hash, algorithm="sha256") -> bool
dk.check_file_integrity("backup.zip", "a94a8fe5...")
scan_directory_for_malware_signatures(directory, signatures: dict) -> list[dict]
dk.scan_directory_for_malware_signatures("/downloads", {"a94a8f...": "known_trojan_x"})
quarantine_suspicious_file(filepath, quarantine_dir="./quarantine") -> str
dk.quarantine_suspicious_file("suspicious.exe")
# './quarantine/1735689600_suspicious.exe.quarantined'
verify_backup_integrity(backup_path, expected_hash) -> bool
5. Web application security
sanitize_user_input(user_input: str) -> str
dk.sanitize_user_input("<script>alert(1)</script>")
# '<script>alert(1)</script>'
generate_csrf_token() -> str / check_csrf_token_validity(request_token, session_token) -> bool
validate_cors_policy(allowed_origins: list[str], request_origin: str) -> dict
dk.validate_cors_policy(["*"], "https://example.com")
# {'allowed': True, 'warning': "Using '*' together with credentials is a security risk", ...}
detect_xss_patterns(input_text: str) -> list[str]
Covers <script>, event handlers, javascript:/vbscript:, data:
URIs, meta-refresh, entity/unicode encoding, template-literal injection,
and more.
dk.detect_xss_patterns("<img src=x onerror=alert(1)>")
# ['inline event handler (...)', '<img> tag with onerror handler']
check_secure_cookie_flags(set_cookie_header: str) -> dict
dk.check_secure_cookie_flags("session=abc; SameSite=None")
# {'httponly': False, 'secure': False, 'samesite': True, 'samesite_none_without_secure': True}
validate_input_against_sql_injection(user_input: str) -> dict
Returns a severity classification (none/low/medium/high) in addition to
the boolean.
dk.validate_input_against_sql_injection("' UNION ALL SELECT username, password FROM users--")
# {'suspicious': True, 'matched_patterns': 4, 'severity': 'high'}
check_security_headers(headers: dict) -> dict
dk.check_security_headers({"Content-Security-Policy": "default-src 'self'"})
# {'checklist': {...}, 'missing': [...], 'weak_configurations': [...], 'score': 33}
verify_csp_header(csp_header: str) -> dict
dk.verify_csp_header("default-src 'self'; script-src 'unsafe-inline'")
# {'issues': [...], 'safe': False}
6. Network-level defense
(Run these only against systems you own or are authorized to assess.)
check_tls_version_support(hostname, port=443, timeout=5.0) -> dict
dk.check_tls_version_support("example.com")
# {'tls_version': 'TLSv1.3', 'outdated': False, 'cipher_suite': '...', 'weak_cipher': False}
check_firewall_rules_status(rules: list[dict]) -> dict
dk.check_firewall_rules_status([{"source": "0.0.0.0/0", "port": 22}])
# {'total_rules': 1, 'risky_count': 1, ...}
detect_arp_spoofing(arp_table: dict, known_good_mappings: dict) -> list[dict]
dk.detect_arp_spoofing({"192.168.1.1": "AA:BB:CC:DD:EE:FF"}, {"192.168.1.1": "11:22:33:44:55:66"})
validate_dns_response(hostname, resolved_ip, expected_ips) -> dict
check_vpn_connection_security(public_ip_before, public_ip_after, dns_servers_after, expected_vpn_dns) -> dict
audit_server_config(config: dict) -> list[str]
dk.audit_server_config({"debug": True, "firewall_enabled": False})
# ['Debug mode is on - must not be enabled in production', 'Firewall is disabled']
7. Cryptography & secrets management
generate_secure_token(length_bytes=32) -> str
ApiKeyRecord / rotate_api_key(current, grace_period_seconds=3600) -> tuple
mask_sensitive_data_in_logs(log_line, fields_to_mask=(...)) -> str
dk.mask_sensitive_data_in_logs('login attempt password="hunter2" user=barman')
# 'login attempt password=***REDACTED*** user=barman'
encrypt_sensitive_data(plaintext, key: bytes) -> str / decrypt_sensitive_data(token, key: bytes) -> str
AES-256-GCM. Requires pip install "defendking[full]".
import secrets
key = secrets.token_bytes(32)
token = dk.encrypt_sensitive_data("top secret", key)
dk.decrypt_sensitive_data(token, key) # 'top secret'
validate_jwt_token(token, secret, algorithms=None) -> dict
Also rejects alg: none tokens explicitly.
check_secrets_in_codebase(directory, file_extensions=(...)) -> list[dict]
Detects AWS/GitHub/Slack/Google/Stripe/SendGrid/Twilio/Mailgun keys, JWTs, private keys, and DB connection strings with embedded credentials.
dk.check_secrets_in_codebase("./src")
# [{'file': './src/config.py', 'line': 4, 'type': 'Possible GitHub personal access / OAuth token'}]
8. Monitoring, risk scoring & reporting
SecurityFinding(category, description, severity, timestamp=now)
severity must be one of low/medium/high/critical.
calculate_risk_score(findings: list[SecurityFinding]) -> dict
findings = [dk.SecurityFinding("auth", "weak password policy", "high")]
dk.calculate_risk_score(findings)
# {'score': 14, 'level': 'low', 'finding_count': 1, 'by_severity': {'high': 1}, 'has_critical_finding': False}
generate_security_report(findings, target_name="system") -> str
Markdown report.
export_findings_to_json(findings, output_path) -> dict
dk.export_findings_to_json(findings, "report.json")
# {'success': True, 'path': 'report.json'}
generate_incident_response_report(incident_title, detected_at, affected_systems, actions_taken) -> str
audit_user_permissions(users: list[dict], expected_max_role: dict) -> list[dict]
notify_admin_dashboard(finding, webhook_url) -> dict / send_security_alert(message, telegram_bot_token, chat_id) -> dict
schedule_periodic_scan(interval_seconds, scan_function, max_runs=None) -> None
generate_security_checklist(system_type) -> list[str]
system_type is one of web/server/network/cloud/mobile.
dk.generate_security_checklist("cloud")
9. API & session security
validate_api_request_signature(payload, timestamp, signature, secret, tolerance_seconds=300) -> dict
HMAC-SHA256 request signing/verification (the pattern used by most REST/exchange/trading APIs).
import time, hmac, hashlib
secret = "shared-secret"
ts = str(time.time())
sig = hmac.new(secret.encode(), f"{ts}{payload}".encode(), hashlib.sha256).hexdigest()
dk.validate_api_request_signature(payload, ts, sig, secret)
# {'valid': True, 'reason': None}
detect_replay_attack(nonce, seen_nonces: set, max_stored=100_000) -> bool
seen = set()
dk.detect_replay_attack("req-123", seen) # False (new)
dk.detect_replay_attack("req-123", seen) # True (replay!)
Not thread-safe and has no time-based expiry (only a size cap) - for
those, use ReplayGuard below instead.
ReplayGuard(ttl_seconds=300)
Thread-safe, TTL-based replay detector - each nonce is automatically
forgotten after ttl_seconds instead of only being evicted once a size
cap is hit.
guard = dk.ReplayGuard(ttl_seconds=300)
guard.check("req-123") # False (new)
guard.check("req-123") # True (replay!)
check_session_fixation(pre_login_session_id, post_login_session_id) -> dict
dk.check_session_fixation("sess-abc", "sess-abc")
# {'rotated': False, 'vulnerable_to_fixation': True, ...}
generate_scoped_api_key(scopes: list[str], prefix="sk") -> dict
dk.generate_scoped_api_key(["read", "trade"])
# {'api_key': 'sk_...', 'stored_hash': '...', 'scopes': ['read', 'trade'], ...}
check_api_rate_limit_headers(headers: dict) -> dict
dk.check_api_rate_limit_headers({"X-RateLimit-Remaining": "0", "Retry-After": "30"})
# {'should_back_off': True, 'suggested_backoff_seconds': 30, ...}
Known limitations
- Heuristic detection still isn't a real classifier.
detect_xss_patterns,validate_input_against_sql_injection, andcheck_suspicious_urlare regex-based signals, not a guarantee of safety. A determined attacker can craft a payload that slips past a fixed pattern list. Always pair them with the real defenses: parameterized queries/ORM for SQL, contextual output encoding + a strict CSP for XSS, and a reputation/block-list service for URLs. - The local common-password list is a sample, not a real breach corpus.
check_password_breachedqueries the Have I Been Pwned API by default — the local list is only an offline fallback. detect_anomalous_login_locationgives an accurate result only when you pass realprevious_coords/new_coords(haversine distance viadistance_km_between). Without coordinates it falls back to a conservative fixed-distance estimate.entropy_bitsincheck_password_strengthis a charset-size-based estimate, not a dictionary-aware one (see the function's own docstring for detail) -score/labelcombine it with pattern detection for a more reliable overall verdict.scan_directory_for_malware_signaturesdoes exact-hash matching only, which a single changed byte evades entirely - this is a property of hash-based detection in general (see the function's docstring), not something fixable within the function; pair it with a real AV/EDR engine if you need resilience to modified variants.detect_replay_attack's in-memoryseen_noncesset isn't thread-safe and doesn't expire entries by time, only by count (max_stored) - useReplayGuardinstead if either matters to you.RateLimiter/IPBlocklist/ReplayGuardare in-process only. Their internal locks make them safe to share across threads, but not across separate processes or machines - back them with Redis (or similar) for a multi-process/multi-instance deployment.schedule_periodic_scanis a simple blocking loop for scripts/demos, not a production scheduler — useAPScheduleror a system cron job for real deployments.
Changelog
3.6.17
- Fixed the CI pipeline itself (the previous release's workflow had
two bugs, caught only after the first real run on GitHub):
- The test job ran
pytest tests/but the actual test directory is namedtest/(no "s") - every test job failed immediately with a "no tests collected" error regardless of the code being tested. Fixed by aligning the workflow,pyproject.toml's mypy config, and the README's documented commands to the realtest/directory name. requires-pythonwas set to>=3.9, which made theTest (Python 3.8)matrix job fail at thepip installstep before any test could even run, since this project intentionally supports 3.8. Reverted to>=3.8(verified via AST inspection that the codebase uses no 3.9+-only syntax - no walrus operator, nomatchstatements).
- The test job ran
- No functional changes to
defendking/handler.pyin this release - this is a CI/tooling-only fix.
3.6.16
- Fixed every weakness identified in a self-review of 2.5.1:
- ReDoS mitigation: all regex-heavy scanning functions now cap
input length (
_MAX_SCANNED_TEXT_LENGTH/_MAX_LONG_TEXT_LENGTH) before running pattern matching, bounding worst-case regex cost regardless of which specific pattern might be added later. - Thread-safety:
RateLimiterandIPBlocklistnow use an internal lock and are safe to share across threads. AddedReplayGuard, a thread-safe, TTL-based replacement fordetect_replay_attack's plain-set approach. - Fewer false positives:
mask_sensitive_data_in_logsnow Luhn-validates digit runs before redacting them as "card numbers," instead of flagging any 13-19-digit sequence (phone numbers, order IDs, timestamps included). - Network retry/backoff:
check_ssl_certificateandcheck_tls_version_supportnow retry transient connection failures with a short backoff instead of reporting a single dropped packet as a hard failure. - Honest documentation for two inherent limitations that aren't
fixable within the function itself:
entropy_bits' charset-based (not dictionary-aware) estimate, and exact-hash malware matching's single-byte-change blind spot.
- ReDoS mitigation: all regex-heavy scanning functions now cap
input length (
- Added CI/CD (
.github/workflows/ci.yml): test matrix (Python 3.9-3.12),ruff+mypylint job, and an automated PyPI publish job (via Trusted Publishing/OIDC) triggered by pushing a version tag. - No new user-facing functions were added in this release - the focus was entirely on hardening the ~70 functions that already existed.
2.5.1
- Added
defendking.exceptions(DefendKingError,InvalidInputError,ExternalServiceError) and applied a consistent error-handling convention across every function (see Error handling). - Substantially deepened detection accuracy across every section: password entropy scoring, homoglyph/TLD-swap domain-similarity checks, weighted risk scoring for URLs and phishing emails, SQLi severity classification, more secret-scanning patterns, more security-header/CSP checks, "high confidence" flagging for impossible-travel detection, and more.
- Full README rewrite with per-function documentation and examples.
- No new functions were added in this release — the focus was entirely on robustness and detection depth of the existing ~65 functions plus the five functions the 1.5.0 API-security section already introduced.
1.5.0
- New section: API & session security (
validate_api_request_signature,detect_replay_attack,check_session_fixation,generate_scoped_api_key,check_api_rate_limit_headers). - First pass at sharpening detection across passwords, URLs/phishing, XSS/SQLi, file extensions, secret-scanning, security headers/CSP, and firewall/default-credential checks.
1.0.0
- Initial public release: 8 sections, ~65 functions.
Versioning
Following semantic-ish convention: first digit = structural changes, second = new features, third = bug fixes.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file defendking-3.6.17.tar.gz.
File metadata
- Download URL: defendking-3.6.17.tar.gz
- Upload date:
- Size: 67.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.8.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97ed80c7af1999188d1e5e6ff78cd5ddeab663c67c88925ab51a0723439578f4
|
|
| MD5 |
a6c9eb9b9cb6b0b98aabf5357a11e23b
|
|
| BLAKE2b-256 |
d5b563b4bd8350b8538cb370342922c0c0742dcfb191fca117bfaf11a4e4dd9d
|
File details
Details for the file defendking-3.6.17-py3-none-any.whl.
File metadata
- Download URL: defendking-3.6.17-py3-none-any.whl
- Upload date:
- Size: 52.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.1.0 CPython/3.8.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ada4a76e67bc981f9fdaa9e35e00e3001757f4b6bde1c246ff5be4ffab4e94d
|
|
| MD5 |
8209811aafdc0d2735f51b0fd142a308
|
|
| BLAKE2b-256 |
897a52555c8e1733bc4b41fa3d57e74769a63b554b3ef9c1215373c48b4ec9b0
|