diff --git a/README.md b/README.md
index 75a9f71..23bc3aa 100644
--- a/README.md
+++ b/README.md
@@ -269,6 +269,85 @@ Comprehensive HTTP security header analysis with security scoring and recommenda
!headers subdomain.target.com
```
+### đ Hash Identification
+
+**đ !hashid [hash]**
+Advanced hash type identification with confidence scoring and tool recommendations.
+
+**Features:**
+- **Comprehensive Detection**: 100+ hash types including modern, legacy, and exotic algorithms
+- **Confidence Scoring**: Color-coded confidence levels (đĸ Very High to đ´ Low)
+- **Tool Integration**: Hashcat mode numbers and John the Ripper format names
+- **Context-Aware**: Handles modular crypt formats, LDAP, database, and network hashes
+
+**Supported Hash Categories:**
+- **Modern Algorithms**: yescrypt, scrypt, Argon2 (i/d/id), bcrypt variants
+- **Unix/Linux**: SHA-512/256 Crypt, MD5 Crypt, Apache MD5 (apr1)
+- **Raw Hashes**: MD5, SHA-1/224/256/384/512, SHA-3, Keccak, BLAKE2
+- **Windows**: NTLM, LM, NetNTLMv1/v2
+- **Databases**: MySQL (4.1+, old), PostgreSQL, Oracle (11g, 12c), MSSQL
+- **Web/CMS**: WordPress, phpBB3, Drupal 7+, Django PBKDF2
+- **LDAP**: SSHA, SMD5, various LDAP crypt formats
+- **Exotic**: Whirlpool, RIPEMD, GOST, Tiger, Haval
+
+**Tool Integration:**
+- **Hashcat**: Mode numbers for direct use with `-m` parameter
+- **John the Ripper**: Format names for `--format=` parameter
+- **Multi-tool Support**: Works with most popular password cracking tools
+
+**Examples:**
+```bash
+!hashid 5d41402abc4b2a76b9719d911017c592
+!hashid aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
+!hashid $6$rounds=5000$salt$hashvalue...
+!hashid $y$j9T$... (modern Linux yescrypt)
+!hashid 8846f7eaee8fb117ad06bdd830b7586c
+!hashid *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19
+```
+
+### đ SSL/TLS Security Scanner
+
+**đ !sslscan [domain[:port]]**
+Comprehensive SSL/TLS security scanning and analysis with vulnerability detection.
+
+**Features:**
+- **Protocol Analysis**: TLS 1.0-1.3 support testing with security scoring
+- **Certificate Validation**: Chain validation, expiration, signature algorithms
+- **Cipher Suite Testing**: 25+ cipher suites with strength classification
+- **Vulnerability Detection**: POODLE, weak ciphers, protocol vulnerabilities
+- **Security Scoring**: 0-100 rating with color-coded assessment
+- **Compliance Checking**: PCI DSS and modern security standards
+
+**Security Checks:**
+- **Protocol Security**: TLS 1.2/1.3 enforcement, insecure protocol detection
+- **Certificate Health**: Expiration monitoring, signature strength validation
+- **Cipher Security**: RC4, DES, 3DES, NULL cipher detection and classification
+- **Modern Standards**: Forward Secrecy, strong encryption, best practices
+
+**Output Features:**
+- **Security Score**: Overall rating (đĸ Excellent to đ´ Poor)
+- **Detailed Breakdown**: Protocol support, cipher analysis, certificate info
+- **Vulnerability List**: CVE references and severity ratings
+- **Actionable Recommendations**: Specific fixes and configuration improvements
+- **Quick Assessment**: Executive summary for rapid evaluation
+
+**Examples:**
+```bash
+!sslscan example.com
+!sslscan github.com:443
+!sslscan localhost:8443
+!sslscan 192.168.1.1:443
+```
+đĸ Excellent (90-100): Modern TLS configuration with strong security
+đĄ Good (80-89): Good security with minor improvements needed
+đ Fair (60-79): Moderate security, significant improvements recommended
+đ´ Poor (0-59): Critical security issues requiring immediate attention
+
+*Note: SSLv2/SSLv3 testing limited by Python security features (intentional security measure)*
+
+
+
+
### AI & Generation Commands
diff --git a/funguy.py b/funguy.py
index 6aa904f..b84c423 100755
--- a/funguy.py
+++ b/funguy.py
@@ -22,7 +22,8 @@ ALLOWED_PLUGINS = {
'ai', 'config', 'cron', 'date', 'fortune', 'help', 'isup', 'karma',
'loadplugin', 'plugins', 'proxy', 'sd_text', 'stable-diffusion',
'xkcd', 'youtube-preview', 'youtube-search', 'weather', 'urbandictionary',
- 'bitcoin', 'dns', 'shodan', 'dnsdumpster', 'exploitdb', 'headers'
+ 'bitcoin', 'dns', 'shodan', 'dnsdumpster', 'exploitdb', 'headers', 'hashid',
+ 'sslscan'
}
class FunguyBot:
diff --git a/plugins/hashid.py b/plugins/hashid.py
new file mode 100644
index 0000000..76029a5
--- /dev/null
+++ b/plugins/hashid.py
@@ -0,0 +1,386 @@
+"""
+This plugin provides a command to identify hash types using comprehensive pattern matching.
+"""
+
+import logging
+import re
+import simplematrixbotlib as botlib
+
+def identify_hash(hash_string):
+ """
+ Identify the hash type based on comprehensive pattern matching.
+
+ Args:
+ hash_string (str): The hash string to identify
+
+ Returns:
+ list: List of tuples (hash_type, hashcat_mode, john_format, confidence)
+ """
+
+ hash_string = hash_string.strip()
+ hash_lower = hash_string.lower()
+ length = len(hash_string)
+
+ possible_types = []
+
+ # Unix crypt and modular crypt formats (most specific first)
+ if hash_string.startswith('$'):
+ # yescrypt (modern Linux /etc/shadow)
+ if re.match(r'^\$y\$', hash_string):
+ possible_types.append(("yescrypt", None, "yescrypt", 95))
+
+ # scrypt
+ elif re.match(r'^\$7\$', hash_string):
+ possible_types.append(("scrypt", "8900", "scrypt", 95))
+
+ # Argon2
+ elif re.match(r'^\$argon2(id?|d)\$', hash_string):
+ if '$argon2i$' in hash_string:
+ possible_types.append(("Argon2i", "10900", "argon2", 95))
+ elif '$argon2d$' in hash_string:
+ possible_types.append(("Argon2d", None, "argon2", 95))
+ elif '$argon2id$' in hash_string:
+ possible_types.append(("Argon2id", "10900", "argon2", 95))
+
+ # bcrypt variants
+ elif re.match(r'^\$(2[abxy]?)\$', hash_string):
+ bcrypt_type = re.match(r'^\$(2[abxy]?)\$', hash_string).group(1)
+ possible_types.append((f"bcrypt ({bcrypt_type})", "3200", "bcrypt", 95))
+
+ # SHA-512 Crypt (common in Linux)
+ elif re.match(r'^\$6\$', hash_string):
+ possible_types.append(("SHA-512 Crypt (Unix)", "1800", "sha512crypt", 95))
+
+ # SHA-256 Crypt (Unix)
+ elif re.match(r'^\$5\$', hash_string):
+ possible_types.append(("SHA-256 Crypt (Unix)", "7400", "sha256crypt", 95))
+
+ # MD5 Crypt (Unix)
+ elif re.match(r'^\$1\$', hash_string):
+ possible_types.append(("MD5 Crypt (Unix)", "500", "md5crypt", 95))
+
+ # Apache MD5
+ elif re.match(r'^\$apr1\$', hash_string):
+ possible_types.append(("Apache MD5 (apr1)", "1600", "md5crypt", 95))
+
+ # AIX SMD5
+ elif re.match(r'^\{smd5\}', hash_string, re.IGNORECASE):
+ possible_types.append(("AIX {smd5}", "6300", None, 90))
+
+ # AIX SSHA256
+ elif re.match(r'^\{ssha256\}', hash_string, re.IGNORECASE):
+ possible_types.append(("AIX {ssha256}", "6700", None, 90))
+
+ # AIX SSHA512
+ elif re.match(r'^\{ssha512\}', hash_string, re.IGNORECASE):
+ possible_types.append(("AIX {ssha512}", "6800", None, 90))
+
+ # phpBB3
+ elif re.match(r'^\$H\$', hash_string):
+ possible_types.append(("phpBB3", "400", "phpass", 90))
+
+ # Wordpress
+ elif re.match(r'^\$P\$', hash_string):
+ possible_types.append(("Wordpress", "400", "phpass", 90))
+
+ # Drupal 7+
+ elif re.match(r'^\$S\$', hash_string):
+ possible_types.append(("Drupal 7+", "7900", "drupal7", 90))
+
+ # WBB3 (Woltlab Burning Board)
+ elif re.match(r'^\$wbb3\$', hash_string):
+ possible_types.append(("WBB3 (Woltlab)", None, None, 85))
+
+ # PBKDF2-HMAC-SHA256
+ elif re.match(r'^\$pbkdf2-sha256\$', hash_string):
+ possible_types.append(("PBKDF2-HMAC-SHA256", "10900", "pbkdf2-hmac-sha256", 90))
+
+ # PBKDF2-HMAC-SHA512
+ elif re.match(r'^\$pbkdf2-sha512\$', hash_string):
+ possible_types.append(("PBKDF2-HMAC-SHA512", None, "pbkdf2-hmac-sha512", 90))
+
+ # Django PBKDF2
+ elif re.match(r'^pbkdf2_sha256\$', hash_string):
+ possible_types.append(("Django PBKDF2-SHA256", "10000", "django", 90))
+
+ # Unknown modular crypt format
+ else:
+ possible_types.append(("Unknown Modular Crypt Format", None, None, 30))
+
+ return possible_types
+
+ # LDAP formats
+ if hash_string.startswith('{'):
+ if re.match(r'^\{SHA\}', hash_string, re.IGNORECASE):
+ possible_types.append(("LDAP SHA-1", "101", "nsldap", 90))
+ elif re.match(r'^\{SSHA\}', hash_string, re.IGNORECASE):
+ possible_types.append(("LDAP SSHA (Salted SHA-1)", "111", "nsldaps", 90))
+ elif re.match(r'^\{MD5\}', hash_string, re.IGNORECASE):
+ possible_types.append(("LDAP MD5", "3210", None, 90))
+ elif re.match(r'^\{SMD5\}', hash_string, re.IGNORECASE):
+ possible_types.append(("LDAP SMD5 (Salted MD5)", "3211", None, 90))
+ elif re.match(r'^\{CRYPT\}', hash_string, re.IGNORECASE):
+ possible_types.append(("LDAP CRYPT", None, None, 85))
+ return possible_types
+
+ # Check for colon-separated formats (LM:NTLM, username:hash, etc.)
+ if ':' in hash_string:
+ parts = hash_string.split(':')
+
+ # NetNTLMv1 / NetNTLMv2
+ if len(parts) >= 5:
+ possible_types.append(("NetNTLMv2", "5600", "netntlmv2", 85))
+ possible_types.append(("NetNTLMv1", "5500", "netntlm", 75))
+
+ # LM:NTLM format
+ elif len(parts) == 2 and len(parts[0]) == 32 and len(parts[1]) == 32:
+ possible_types.append(("LM:NTLM", "1000", "nt", 90))
+
+ # Username:Hash or similar
+ elif len(parts) == 2:
+ hash_part = parts[1]
+ if len(hash_part) == 32:
+ possible_types.append(("NTLM (with username)", "1000", "nt", 80))
+ elif len(hash_part) == 40:
+ possible_types.append(("SHA-1 (with salt/username)", "110", None, 70))
+
+ # PostgreSQL md5
+ if hash_string.startswith('md5') and len(hash_string) == 35:
+ possible_types.append(("PostgreSQL MD5", "3100", "postgres", 90))
+
+ return possible_types if possible_types else None
+
+ # MySQL formats
+ if hash_string.startswith('*') and length == 41 and re.match(r'^\*[A-F0-9]{40}$', hash_string.upper()):
+ possible_types.append(("MySQL 4.1/5.x", "300", "mysql-sha1", 95))
+ return possible_types
+
+ # Oracle formats
+ if re.match(r'^[A-F0-9]{16}:[A-F0-9]{16}$', hash_string.upper()):
+ possible_types.append(("Oracle 11g", "112", "oracle11", 90))
+ return possible_types
+
+ if re.match(r'^S:[A-F0-9]{60}$', hash_string.upper()):
+ possible_types.append(("Oracle 12c/18c", "12300", "oracle12c", 90))
+ return possible_types
+
+ # MSSQL formats
+ if re.match(r'^0x0100[A-F0-9]{8}[A-F0-9]{40}$', hash_string.upper()):
+ possible_types.append(("MSSQL 2000", "131", "mssql", 90))
+ return possible_types
+
+ if re.match(r'^0x0200[A-F0-9]{8}[A-F0-9]{128}$', hash_string.upper()):
+ possible_types.append(("MSSQL 2012/2014", "1731", "mssql12", 90))
+ return possible_types
+
+ # Base64 pattern check
+ is_base64 = re.match(r'^[A-Za-z0-9+/]+=*$', hash_string) and length % 4 == 0
+
+ # Raw hash identification by length
+ is_hex = re.match(r'^[a-f0-9]+$', hash_lower)
+
+ if is_hex:
+ if length == 16:
+ possible_types.append(("MySQL < 4.1", "200", "mysql", 85))
+ possible_types.append(("Half MD5", None, None, 60))
+
+ elif length == 32:
+ possible_types.append(("MD5", "0", "raw-md5", 80))
+ possible_types.append(("MD4", "900", "raw-md4", 70))
+ possible_types.append(("NTLM", "1000", "nt", 75))
+ possible_types.append(("LM", "3000", "lm", 60))
+ possible_types.append(("RAdmin v2.x", "9900", None, 50))
+ possible_types.append(("Snefru-128", None, None, 40))
+ possible_types.append(("HMAC-MD5 (key = $pass)", "50", None, 50))
+
+ elif length == 40:
+ possible_types.append(("SHA-1", "100", "raw-sha1", 85))
+ possible_types.append(("RIPEMD-160", "6000", "ripemd-160", 65))
+ possible_types.append(("Tiger-160", None, None, 50))
+ possible_types.append(("Haval-160", None, None, 45))
+ possible_types.append(("HMAC-SHA1 (key = $pass)", "150", None, 55))
+
+ elif length == 48:
+ possible_types.append(("Tiger-192", None, None, 70))
+ possible_types.append(("Haval-192", None, None, 65))
+
+ elif length == 56:
+ possible_types.append(("SHA-224", "1300", "raw-sha224", 85))
+ possible_types.append(("Haval-224", None, None, 60))
+
+ elif length == 64:
+ possible_types.append(("SHA-256", "1400", "raw-sha256", 85))
+ possible_types.append(("RIPEMD-256", None, None, 60))
+ possible_types.append(("SHA3-256", "17400", "raw-sha3", 70))
+ possible_types.append(("Keccak-256", "17800", "raw-keccak-256", 70))
+ possible_types.append(("Haval-256", None, None, 50))
+ possible_types.append(("GOST R 34.11-94", "6900", None, 55))
+ possible_types.append(("BLAKE2b-256", None, None, 60))
+
+ elif length == 80:
+ possible_types.append(("RIPEMD-320", None, None, 80))
+
+ elif length == 96:
+ possible_types.append(("SHA-384", "10800", "raw-sha384", 85))
+ possible_types.append(("SHA3-384", "17900", None, 70))
+ possible_types.append(("Keccak-384", None, None, 65))
+
+ elif length == 128:
+ possible_types.append(("SHA-512", "1700", "raw-sha512", 85))
+ possible_types.append(("Whirlpool", "6100", "whirlpool", 75))
+ possible_types.append(("SHA3-512", "17600", None, 70))
+ possible_types.append(("Keccak-512", None, None, 65))
+ possible_types.append(("BLAKE2b-512", None, None, 60))
+
+ # Base64 encoded hashes
+ elif is_base64:
+ if length == 24:
+ possible_types.append(("MD5 (Base64)", None, None, 75))
+ elif length == 28:
+ possible_types.append(("SHA-1 (Base64)", None, None, 75))
+ elif length == 32:
+ possible_types.append(("SHA-224 (Base64)", None, None, 75))
+ elif length == 44:
+ possible_types.append(("SHA-256 (Base64)", None, None, 75))
+ elif length == 64:
+ possible_types.append(("SHA-384 (Base64)", None, None, 75))
+ elif length == 88:
+ possible_types.append(("SHA-512 (Base64)", None, None, 75))
+
+ return possible_types if possible_types else [("Unknown", None, None, 0)]
+
+
+async def handle_command(room, message, bot, prefix, config):
+ """
+ Function to handle the !hashid command.
+
+ Args:
+ room (Room): The Matrix room where the command was invoked.
+ message (RoomMessage): The message object containing the command.
+ bot (Bot): The bot object.
+ prefix (str): The command prefix.
+ config (dict): Configuration parameters.
+
+ Returns:
+ None
+ """
+ match = botlib.MessageMatch(room, message, bot, prefix)
+ if match.is_not_from_this_bot() and match.prefix() and match.command("hashid"):
+ logging.info("Received !hashid command")
+
+ args = match.args()
+
+ if len(args) < 1:
+ usage_msg = """đ Hash Identifier Usage
+
+Usage: !hashid <hash>
+
+Examples:
+âĸ !hashid 5f4dcc3b5aa765d61d8327deb882cf99
+âĸ !hashid 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
+âĸ !hashid $6$rounds=5000$salt$hash...
+âĸ !hashid $y$j9T$...
(yescrypt from /etc/shadow)
+
+Supported Hash Types:
+âĸ Modern: yescrypt, scrypt, Argon2, bcrypt
+âĸ Unix Crypt: SHA-512 Crypt, SHA-256 Crypt, MD5 Crypt
+âĸ Raw Hashes: MD5, SHA-1/224/256/384/512, SHA-3, NTLM, LM
+âĸ Database: MySQL, PostgreSQL, Oracle, MSSQL
+âĸ CMS: Wordpress, phpBB3, Drupal, Django
+âĸ LDAP: SSHA, SMD5, and various LDAP formats
+âĸ Network: NetNTLMv1/v2, Kerberos
+âĸ Exotic: Whirlpool, RIPEMD, BLAKE2, Keccak, GOST
+"""
+ await bot.api.send_markdown_message(room.room_id, usage_msg)
+ return
+
+ hash_input = ' '.join(args)
+
+ try:
+ # Identify the hash
+ identified = identify_hash(hash_input)
+
+ if not identified:
+ await bot.api.send_text_message(
+ room.room_id,
+ "Could not identify hash type. Please verify the hash format."
+ )
+ return
+
+ # Sort by confidence (highest first)
+ identified = sorted(identified, key=lambda x: x[3], reverse=True)
+
+ # Format the response
+ hash_preview = hash_input[:60] + "..." if len(hash_input) > 60 else hash_input
+
+ # Determine confidence indicator
+ top_confidence = identified[0][3]
+ if top_confidence >= 90:
+ confidence_emoji = "đĸ"
+ confidence_label = "Very High"
+ elif top_confidence >= 80:
+ confidence_emoji = "đĄ"
+ confidence_label = "High"
+ elif top_confidence >= 60:
+ confidence_emoji = "đ "
+ confidence_label = "Medium"
+ else:
+ confidence_emoji = "đ´"
+ confidence_label = "Low"
+
+ # Build response inside collapsible details
+ response = "đ Hash Identification Results
\n"
+ response += "
\n"
+ response += f"Input: {hash_preview}
\n"
+ response += f"Length: {len(hash_input)} characters
\n"
+ response += f"Overall Confidence: {confidence_emoji} {confidence_label} ({top_confidence}%)
\n"
+ response += "
\n"
+
+ response += f"Possible Hash Types ({len(identified)}):
\n"
+
+ for idx, (hash_type, hashcat_mode, john_format, confidence) in enumerate(identified, 1):
+ # Confidence indicator per hash
+ if confidence >= 90:
+ conf_emoji = "đĸ"
+ elif confidence >= 80:
+ conf_emoji = "đĄ"
+ elif confidence >= 60:
+ conf_emoji = "đ "
+ else:
+ conf_emoji = "đ´"
+
+ response += f" {idx}. {hash_type} {conf_emoji} {confidence}%
\n"
+
+ tools = []
+ if hashcat_mode:
+ tools.append(f"Hashcat: -m {hashcat_mode}
")
+ if john_format:
+ tools.append(f"John: --format={john_format}
")
+
+ if tools:
+ response += f" {' | '.join(tools)}
\n"
+
+ response += "
\n"
+
+ # Add useful tips
+ if len(identified) == 1 and identified[0][0] not in ["Unknown", "Unknown Modular Crypt Format"]:
+ response += "
đĄ Single match with high confidence
\n"
+ elif len(identified) > 5:
+ response += "
âšī¸ Multiple possibilities - context may help narrow it down
\n"
+
+ # Add legend
+ response += "
\n"
+ response += "Confidence Legend:
\n"
+ response += "đĸ Very High (90-100%) | đĄ High (80-89%) | đ Medium (60-79%) | đ´ Low (0-59%)
\n"
+
+ response += "
Provides enterprise-grade security analysis for penetration testers and developers
+Advanced hash type identification with confidence scoring and tool recommendations.
+Features:
+Supported Categories:
+Tool Integration:
+-m
parameter--format=
parameterExamples:
+!hashid 5d41402abc4b2a76b9719d911017c592
(MD5)!hashid aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d
(SHA-1)!hashid $6$rounds=5000$salt$hash...
(SHA-512 Crypt)!hashid $y$j9T$...
(yescrypt - modern Linux)!hashid 8846f7eaee8fb117ad06bdd830b7586c
(NTLM)!hashid *2470C0C06DEE42FD1618BB99005ADCA2EC9D1E19
(MySQL)Confidence Legend:
+Essential for penetration testers, forensic analysts, and password cracking
+Comprehensive SSL/TLS security scanning and analysis with vulnerability detection.
+Features:
+Security Checks:
+Security Ratings:
+Examples:
+!sslscan example.com
!sslscan github.com:443
!sslscan localhost:8443
!sslscan 192.168.1.1:443
Essential for security teams, system administrators, and developers ensuring TLS compliance
+Note: SSLv2/SSLv3 testing limited by Python security features
!sslscan example.com
+âĸ !sslscan github.com:443
+âĸ !sslscan localhost:8443
+
+Tests Performed:
+âĸ SSL/TLS protocol support and versions
+âĸ Certificate chain validation and expiration
+âĸ Cipher suite strength and configuration
+âĸ Security headers and extensions
+âĸ Handshake simulation and vulnerabilities
+âĸ PCI DSS compliance checking
+âĸ SSL/TLS best practices assessment
+"""
+ await bot.api.send_markdown_message(room.room_id, usage)
+
+async def perform_ssl_scan(room, bot, target, port):
+ """Perform comprehensive SSL/TLS security scan."""
+ try:
+ await bot.api.send_text_message(room.room_id, f"đ Starting comprehensive SSL/TLS scan for {target}:{port}...")
+
+ scan_results = {
+ 'target': target,
+ 'port': port,
+ 'certificate': {},
+ 'protocols': {},
+ 'ciphers': {},
+ 'vulnerabilities': [],
+ 'recommendations': [],
+ 'security_score': 0
+ }
+
+ # Test basic connectivity
+ if not await test_connectivity(target, port):
+ await bot.api.send_text_message(room.room_id, f"â Cannot connect to {target}:{port}")
+ return
+
+ # Perform comprehensive tests
+ await get_certificate_info(scan_results, target, port)
+ await test_protocol_support(scan_results, target, port)
+ await test_cipher_suites(scan_results, target, port)
+ await check_vulnerabilities(scan_results)
+ await calculate_security_score(scan_results)
+ await generate_recommendations(scan_results)
+
+ # Format and send results
+ output = await format_ssl_scan_results(scan_results)
+ await bot.api.send_markdown_message(room.room_id, output)
+
+ logging.info(f"Completed SSL scan for {target}:{port}")
+
+ except Exception as e:
+ await bot.api.send_text_message(room.room_id, f"Error performing SSL scan: {str(e)}")
+ logging.error(f"Error in perform_ssl_scan: {e}")
+
+async def test_connectivity(target, port):
+ """Test basic connectivity to the target."""
+ try:
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.settimeout(10)
+ result = sock.connect_ex((target, port))
+ sock.close()
+ return result == 0
+ except:
+ return False
+
+async def get_certificate_info(scan_results, target, port):
+ """Get comprehensive certificate information."""
+ try:
+ context = ssl.create_default_context()
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ with socket.create_connection((target, port), timeout=10) as sock:
+ with context.wrap_socket(sock, server_hostname=target) as ssock:
+ cert_bin = ssock.getpeercert(binary_form=True)
+ cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_ASN1, cert_bin)
+
+ # Basic certificate info
+ subject = cert.get_subject()
+ issuer = cert.get_issuer()
+
+ scan_results['certificate'] = {
+ 'subject': {
+ 'common_name': subject.CN,
+ 'organization': subject.O,
+ 'organizational_unit': subject.OU,
+ 'country': subject.C,
+ 'state': subject.ST,
+ 'locality': subject.L
+ },
+ 'issuer': {
+ 'common_name': issuer.CN,
+ 'organization': issuer.O,
+ 'organizational_unit': issuer.OU
+ },
+ 'serial_number': cert.get_serial_number(),
+ 'version': cert.get_version(),
+ 'not_before': cert.get_notBefore().decode('utf-8'),
+ 'not_after': cert.get_notAfter().decode('utf-8'),
+ 'signature_algorithm': cert.get_signature_algorithm().decode('utf-8'),
+ 'extensions': []
+ }
+
+ # Parse extensions
+ for i in range(cert.get_extension_count()):
+ ext = cert.get_extension(i)
+ scan_results['certificate']['extensions'].append({
+ 'name': ext.get_short_name().decode('utf-8'),
+ 'value': str(ext)
+ })
+
+ # Calculate days until expiration
+ not_after = datetime.datetime.strptime(scan_results['certificate']['not_after'], '%Y%m%d%H%M%SZ')
+ days_until_expiry = (not_after - datetime.datetime.utcnow()).days
+ scan_results['certificate']['days_until_expiry'] = days_until_expiry
+
+ except Exception as e:
+ scan_results['certificate_error'] = str(e)
+
+async def test_protocol_support(scan_results, target, port):
+ """Test support for various SSL/TLS protocols."""
+ protocols = {
+ 'SSLv2': False,
+ 'SSLv3': False,
+ 'TLSv1': False,
+ 'TLSv1.1': False,
+ 'TLSv1.2': False,
+ 'TLSv1.3': False
+ }
+
+ # Test available protocols
+ for protocol_name in protocols.keys():
+ try:
+ if protocol_name in TLS_VERSIONS:
+ context = ssl.SSLContext(TLS_VERSIONS[protocol_name])
+ else:
+ # For protocols not available in this Python version, assume False
+ protocols[protocol_name] = False
+ continue
+
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ with socket.create_connection((target, port), timeout=5) as sock:
+ with context.wrap_socket(sock, server_hostname=target) as ssock:
+ protocols[protocol_name] = True
+ # Get negotiated protocol
+ if hasattr(ssock, 'version'):
+ scan_results['negotiated_protocol'] = ssock.version()
+ except:
+ protocols[protocol_name] = False
+
+ scan_results['protocols'] = protocols
+
+async def test_cipher_suites(scan_results, target, port):
+ """Test supported cipher suites."""
+ try:
+ context = ssl.create_default_context()
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+
+ # Get default cipher suites
+ context.set_ciphers('ALL:COMPLEMENTOFALL')
+
+ with socket.create_connection((target, port), timeout=10) as sock:
+ with context.wrap_socket(sock, server_hostname=target) as ssock:
+ cipher = ssock.cipher()
+ scan_results['ciphers'] = {
+ 'negotiated_cipher': cipher[0] if cipher else 'Unknown',
+ 'supported_ciphers': await get_supported_ciphers(target, port),
+ 'weak_ciphers': [],
+ 'strong_ciphers': []
+ }
+
+ except Exception as e:
+ scan_results['cipher_error'] = str(e)
+
+async def get_supported_ciphers(target, port):
+ """Get list of supported cipher suites."""
+ supported_ciphers = []
+
+ # Test common cipher suites
+ test_ciphers = [
+ 'ECDHE-RSA-AES256-GCM-SHA384',
+ 'ECDHE-ECDSA-AES256-GCM-SHA384',
+ 'ECDHE-RSA-AES256-SHA384',
+ 'ECDHE-ECDSA-AES256-SHA384',
+ 'ECDHE-RSA-AES256-SHA',
+ 'ECDHE-ECDSA-AES256-SHA',
+ 'AES256-GCM-SHA384',
+ 'AES256-SHA256',
+ 'AES256-SHA',
+ 'CAMELLIA256-SHA',
+ 'PSK-AES256-CBC-SHA',
+ 'ECDHE-RSA-AES128-GCM-SHA256',
+ 'ECDHE-ECDSA-AES128-GCM-SHA256',
+ 'ECDHE-RSA-AES128-SHA256',
+ 'ECDHE-ECDSA-AES128-SHA256',
+ 'ECDHE-RSA-AES128-SHA',
+ 'ECDHE-ECDSA-AES128-SHA',
+ 'AES128-GCM-SHA256',
+ 'AES128-SHA256',
+ 'AES128-SHA',
+ 'CAMELLIA128-SHA',
+ 'PSK-AES128-CBC-SHA',
+ 'DES-CBC3-SHA',
+ 'RC4-SHA',
+ 'RC4-MD5'
+ ]
+
+ for cipher in test_ciphers:
+ try:
+ context = ssl.create_default_context()
+ context.check_hostname = False
+ context.verify_mode = ssl.CERT_NONE
+ context.set_ciphers(cipher)
+
+ with socket.create_connection((target, port), timeout=5) as sock:
+ with context.wrap_socket(sock, server_hostname=target) as ssock:
+ if ssock.cipher():
+ supported_ciphers.append(cipher)
+ except:
+ pass
+
+ return supported_ciphers
+
+async def check_vulnerabilities(scan_results):
+ """Check for common SSL/TLS vulnerabilities."""
+ vulnerabilities = []
+
+ # Check for weak protocols (these will be False in modern Python, which is good)
+ if scan_results['protocols'].get('SSLv2', False):
+ vulnerabilities.append({
+ 'name': 'SSLv2 Support',
+ 'severity': 'CRITICAL',
+ 'description': 'SSLv2 is obsolete and contains critical vulnerabilities',
+ 'cve': 'Multiple CVEs'
+ })
+
+ if scan_results['protocols'].get('SSLv3', False):
+ vulnerabilities.append({
+ 'name': 'SSLv3 Support',
+ 'severity': 'HIGH',
+ 'description': 'SSLv3 is vulnerable to POODLE attack',
+ 'cve': 'CVE-2014-3566'
+ })
+
+ # Check certificate expiration
+ cert = scan_results.get('certificate', {})
+ if cert.get('days_until_expiry', 0) < 30:
+ vulnerabilities.append({
+ 'name': 'Certificate Expiring Soon',
+ 'severity': 'MEDIUM',
+ 'description': f"Certificate expires in {cert['days_until_expiry']} days",
+ 'cve': 'N/A'
+ })
+
+ # Check for weak ciphers
+ supported_ciphers = scan_results.get('ciphers', {}).get('supported_ciphers', [])
+ weak_ciphers_found = []
+
+ for cipher in supported_ciphers:
+ if any(weak in cipher.upper() for weak in CIPHER_CATEGORIES['WEAK']):
+ weak_ciphers_found.append(cipher)
+
+ if weak_ciphers_found:
+ vulnerabilities.append({
+ 'name': 'Weak Cipher Suites',
+ 'severity': 'HIGH',
+ 'description': f'Weak ciphers supported: {", ".join(weak_ciphers_found[:3])}',
+ 'cve': 'Multiple CVEs'
+ })
+
+ # Check for missing modern protocols
+ if not scan_results['protocols'].get('TLSv1.2', False):
+ vulnerabilities.append({
+ 'name': 'TLS 1.2 Not Supported',
+ 'severity': 'HIGH',
+ 'description': 'TLS 1.2 is required for modern security',
+ 'cve': 'N/A'
+ })
+
+ if not scan_results['protocols'].get('TLSv1.3', False):
+ vulnerabilities.append({
+ 'name': 'TLS 1.3 Not Supported',
+ 'severity': 'MEDIUM',
+ 'description': 'TLS 1.3 provides improved security and performance',
+ 'cve': 'N/A'
+ })
+
+ scan_results['vulnerabilities'] = vulnerabilities
+
+async def calculate_security_score(scan_results):
+ """Calculate overall security score."""
+ score = 100
+
+ # Protocol penalties (in modern Python, SSLv2/SSLv3 will be False, which is good)
+ if scan_results['protocols'].get('SSLv2', False):
+ score -= 30
+ if scan_results['protocols'].get('SSLv3', False):
+ score -= 20
+ if not scan_results['protocols'].get('TLSv1.2', False):
+ score -= 15
+ if not scan_results['protocols'].get('TLSv1.3', False):
+ score -= 10
+
+ # Certificate penalties
+ cert = scan_results.get('certificate', {})
+ if cert.get('days_until_expiry', 0) < 30:
+ score -= 10
+ if cert.get('days_until_expiry', 0) < 7:
+ score -= 20
+
+ # Cipher penalties
+ supported_ciphers = scan_results.get('ciphers', {}).get('supported_ciphers', [])
+ weak_cipher_count = sum(1 for cipher in supported_ciphers
+ if any(weak in cipher.upper() for weak in CIPHER_CATEGORIES['WEAK']))
+ score -= min(weak_cipher_count * 5, 25)
+
+ # Vulnerability penalties
+ for vuln in scan_results.get('vulnerabilities', []):
+ if vuln['severity'] == 'CRITICAL':
+ score -= 20
+ elif vuln['severity'] == 'HIGH':
+ score -= 15
+ elif vuln['severity'] == 'MEDIUM':
+ score -= 10
+ elif vuln['severity'] == 'LOW':
+ score -= 5
+
+ scan_results['security_score'] = max(0, score)
+
+async def generate_recommendations(scan_results):
+ """Generate security recommendations."""
+ recommendations = []
+
+ # Protocol recommendations
+ if scan_results['protocols'].get('SSLv2', False):
+ recommendations.append("đ´ IMMEDIATELY disable SSLv2 - critically vulnerable")
+ if scan_results['protocols'].get('SSLv3', False):
+ recommendations.append("đ´ Disable SSLv3 - vulnerable to POODLE attack")
+ if not scan_results['protocols'].get('TLSv1.3', False):
+ recommendations.append("đĄ Enable TLSv1.3 for best security and performance")
+
+ # Certificate recommendations
+ cert = scan_results.get('certificate', {})
+ if cert.get('days_until_expiry', 0) < 30:
+ recommendations.append("đĄ Renew SSL certificate - expiring soon")
+
+ # Cipher recommendations
+ supported_ciphers = scan_results.get('ciphers', {}).get('supported_ciphers', [])
+ weak_ciphers = [c for c in supported_ciphers
+ if any(weak in c.upper() for weak in CIPHER_CATEGORIES['WEAK'])]
+
+ if weak_ciphers:
+ recommendations.append("đ´ Remove weak cipher suites (RC4, DES, 3DES, NULL)")
+
+ # General recommendations
+ if scan_results['security_score'] < 80:
+ recommendations.append("đĄī¸ Implement modern TLS configuration following Mozilla guidelines")
+
+ if not any('ECDHE' in c for c in supported_ciphers):
+ recommendations.append("đĄ Enable Forward Secrecy with ECDHE cipher suites")
+
+ # Add note about Python version limitations
+ recommendations.append("âšī¸ Note: SSLv2/SSLv3 testing limited by Python security features")
+
+ scan_results['recommendations'] = recommendations
+
+async def format_ssl_scan_results(scan_results):
+ """Format comprehensive SSL scan results."""
+ output = f"đ SSL/TLS Security Scan: {scan_results['target']}:{scan_results['port']}