From da06d3d962d1ad028c6bace654a87326c271c18f Mon Sep 17 00:00:00 2001 From: emadasefi Date: Fri, 26 Dec 2025 23:36:08 +0330 Subject: [PATCH] add algorithm scores --- sshscan.py | 490 +++++++++++++++++++---------------------------------- 1 file changed, 170 insertions(+), 320 deletions(-) diff --git a/sshscan.py b/sshscan.py index e9f13dd..f3b2345 100755 --- a/sshscan.py +++ b/sshscan.py @@ -4,6 +4,7 @@ # Copyright (c) 2020-2025 Babak Farrokhi # # Algorithm classifications based on algorithm_guidance.json +# v3.3.2 - Original code + COMPLETE FIXED scoring import argparse import os @@ -12,63 +13,31 @@ import sys from typing import Any, Optional, Tuple, List, Dict +__version__ = "1.0.2" # Terminal Colors class TerminalColors: - """ANSI color codes for terminal output.""" RED = '\033[91m' GREEN = '\033[92m' + YELLOW = '\033[93m' RESET = '\033[0m' - def supports_color() -> bool: - """ - Detect if the terminal supports color output. - - Checks for: - - NO_COLOR environment variable (https://no-color.org/) - - FORCE_COLOR environment variable - - Output is to a TTY (not redirected) - - TERM environment variable is set and not 'dumb' - """ - if os.environ.get('NO_COLOR'): - return False - - if os.environ.get('FORCE_COLOR'): - return True - - if not hasattr(sys.stdout, 'isatty') or not sys.stdout.isatty(): - return False + if os.environ.get('NO_COLOR'): return False + if os.environ.get('FORCE_COLOR'): return True + if not sys.stdout.isatty(): return False + return os.environ.get('TERM', '') != 'dumb' - term = os.environ.get('TERM', '') - if term == 'dumb': - return False - - return True - - -# Global flag to control color output USE_COLOR = supports_color() - def colorize(text: str, color: str) -> str: - """Apply color to text if colors are enabled.""" - if USE_COLOR: - return f"{color}{text}{TerminalColors.RESET}" - return text + return f"{color}{text}{TerminalColors.RESET}" if USE_COLOR else text +def red(text: str) -> str: return colorize(text, TerminalColors.RED) +def green(text: str) -> str: return colorize(text, TerminalColors.GREEN) +def yellow(text: str) -> str: return colorize(text, TerminalColors.YELLOW) -def red(text: str) -> str: - """Return text in red (for not recommended or insecure algorithms).""" - return colorize(text, TerminalColors.RED) - - -def green(text: str) -> str: - """Return text in green (for strong/secure algorithms).""" - return colorize(text, TerminalColors.GREEN) - - -# SSH Protocol Constants +# SSH Constants SSH_MSG_KEXINIT = 20 MAX_PACKET_LENGTH = 1024 * 1024 SSH_HEADER_LENGTH = 5 @@ -77,301 +46,203 @@ def green(text: str) -> str: MIN_PADDING_LENGTH = 4 MAX_PADDING_LENGTH = 255 -# Strong algorithm lists based on algorithm_guidance.json (canonical source) -# See algorithm_guidance.json for detailed rationale and references +# Strong lists (original) STRONG_CIPHERS = [ - 'chacha20-poly1305@openssh.com', - 'aes256-gcm@openssh.com', - 'aes128-gcm@openssh.com', - 'aes256-ctr', - 'aes192-ctr', - 'aes128-ctr' + 'chacha20-poly1305@openssh.com','aes256-gcm@openssh.com','aes128-gcm@openssh.com', + 'aes256-ctr','aes192-ctr','aes128-ctr' ] STRONG_MACS = [ - 'hmac-sha2-512-etm@openssh.com', - 'hmac-sha2-256-etm@openssh.com', - 'umac-128', - 'umac-128-etm@openssh.com', - 'hmac-sha2-512', - 'hmac-sha2-256', - 'umac-128@openssh.com' + 'hmac-sha2-512-etm@openssh.com','hmac-sha2-256-etm@openssh.com','umac-128', + 'umac-128-etm@openssh.com','hmac-sha2-512','hmac-sha2-256','umac-128@openssh.com' ] STRONG_KEX = [ - 'curve25519-sha256', - 'curve25519-sha256@libssh.org', - 'diffie-hellman-group-exchange-sha256', - 'diffie-hellman-group14-sha256', - 'diffie-hellman-group16-sha512', - 'diffie-hellman-group18-sha512', - 'sntrup761x25519-sha512@openssh.com', - 'sntrup761x25519-sha512', - 'mlkem768x25519-sha256', - 'kex-strict-s-v00@openssh.com', - 'ext-info-s' + 'curve25519-sha256','curve25519-sha256@libssh.org','diffie-hellman-group-exchange-sha256', + 'diffie-hellman-group14-sha256','diffie-hellman-group16-sha512','diffie-hellman-group18-sha512', + 'sntrup761x25519-sha512@openssh.com','sntrup761x25519-sha512','mlkem768x25519-sha256', + 'kex-strict-s-v00@openssh.com','ext-info-s' ] STRONG_HOST_KEY_ALGORITHMS = [ - 'ssh-ed25519', - 'ssh-ed25519-cert-v01@openssh.com', - 'rsa-sha2-256', - 'rsa-sha2-512', - 'ssh-rsa-cert-v01@openssh.com' + 'ssh-ed25519','ssh-ed25519-cert-v01@openssh.com','rsa-sha2-256', + 'rsa-sha2-512','ssh-rsa-cert-v01@openssh.com' ] - +# COMPLETE Scoring System +ALGORITHM_SCORES = { + # Ciphers + 'chacha20-poly1305@openssh.com':100,'aes256-gcm@openssh.com':98,'aes128-gcm@openssh.com':95, + 'aes256-ctr':92,'aes192-ctr':90,'aes128-ctr':88,'aes256-cbc':70,'aes192-cbc':68, + 'aes128-cbc':65,'3des-cbc':20,'blowfish-cbc':15,'cast128-cbc':10,'arcfour':5, + 'arcfour128':5,'arcfour256':5,'rijndael-cbc@lysator.liu.se':60, + + # MACs + 'hmac-sha2-512-etm@openssh.com':100,'hmac-sha2-256-etm@openssh.com':98, + 'umac-128-etm@openssh.com':95,'umac-128@openssh.com':92,'hmac-sha2-512':90, + 'hmac-sha2-256':88,'umac-128':85,'hmac-sha2-256-etm':88, + 'hmac-sha1-etm@openssh.com':60,'hmac-sha1':50,'hmac-ripemd160-etm@openssh.com':25, + 'hmac-ripemd160':15,'hmac-md5-etm@openssh.com':20,'hmac-md5':10,'hmac-md5-96':5, + + # KEX + 'curve25519-sha256':100,'curve25519-sha256@libssh.org':100, + 'sntrup761x25519-sha512@openssh.com':98,'mlkem768x25519-sha256':97, + 'diffie-hellman-group16-sha512':95,'diffie-hellman-group18-sha512':94, + 'diffie-hellman-group14-sha256':92,'diffie-hellman-group15-sha512':93, + 'diffie-hellman-group-exchange-sha256':90,'ecdh-sha2-nistp384':87, + 'ecdh-sha2-nistp521':88,'ecdh-sha2-nistp256':85,'kex-strict-s-v00@openssh.com':95, + 'diffie-hellman-group-exchange-sha1':40,'diffie-hellman-group14-sha1':30, + 'diffie-hellman-group1-sha1':10,'gss-gex-sha1-':20,'gss-group14-sha1-':15, + 'ext-info-s':90, + + # HostKey + 'ssh-ed25519':100,'ssh-ed25519-cert-v01@openssh.com':100,'sk-ssh-ed25519@openssh.com':99, + 'rsa-sha2-512':95,'rsa-sha2-256':92,'ecdsa-sha2-nistp256':90, + 'ecdsa-sha2-nistp384':92,'ecdsa-sha2-nistp521':94,'ssh-rsa':40, + 'ssh-rsa-cert-v01@openssh.com':45,'ssh-dss':10 +} + +def get_score(algo: str) -> int: return ALGORITHM_SCORES.get(algo, 0) + +def calculate_final_score(ciphers: List[str], kex: List[str], macs: List[str], hka: List[str]) -> float: + cipher_avg = sum(get_score(c) for c in ciphers) / len(ciphers) if ciphers else 0 + kex_avg = sum(get_score(k) for k in kex) / len(kex) if kex else 0 + mac_avg = sum(get_score(m) for m in macs) / len(macs) if macs else 0 + hka_avg = sum(get_score(h) for h in hka) / len(hka) if hka else 0 + return cipher_avg * 0.35 + kex_avg * 0.30 + mac_avg * 0.25 + hka_avg * 0.10 + +def get_status(score: float) -> str: + s = int(score) + if s >= 90: return "EXCELLENT ✅" + elif s >= 80: return "GOOD 👍" + elif s >= 70: return "FAIR ⚠️" + elif s >= 60: return "POOR 😐" + return "DANGER ❌" + +# Parsing functions def parse_uint32(data: bytes, offset: int) -> Tuple[int, int]: - """Parse a 4-byte big-endian unsigned integer. Returns (value, new_offset).""" - if offset + 4 > len(data): - raise ValueError("Insufficient data to parse uint32") - value = struct.unpack('>I', data[offset:offset + 4])[0] - return value, offset + 4 - + if offset + 4 > len(data): raise ValueError("uint32") + return struct.unpack('>I', data[offset:offset+4])[0], offset + 4 def parse_byte(data: bytes, offset: int) -> Tuple[int, int]: - """Parse a single byte. Returns (value, new_offset).""" - if offset >= len(data): - raise ValueError("Insufficient data to parse byte") + if offset >= len(data): raise ValueError("byte") return data[offset], offset + 1 - def parse_string(data: bytes, offset: int) -> Tuple[bytes, int]: - """Parse a length-prefixed string. Returns (string_bytes, new_offset).""" length, offset = parse_uint32(data, offset) - if offset + length > len(data): - raise ValueError(f"Insufficient data to parse string of length {length}") - string_data = data[offset:offset + length] - return string_data, offset + length - + if offset + length > len(data): raise ValueError("string") + return data[offset:offset+length], offset + length def parse_name_list(data: bytes, offset: int) -> Tuple[List[str], int]: - """Parse a name-list (comma-separated algorithm names). Returns (list, new_offset).""" name_list_bytes, offset = parse_string(data, offset) - if len(name_list_bytes) == 0: - return [], offset - try: - name_list_str = name_list_bytes.decode('ascii') - except UnicodeDecodeError as e: - raise ValueError(f"Invalid ASCII data in name-list: {e}") - names = name_list_str.split(',') - return names, offset - + if not name_list_bytes: return [], offset + return name_list_bytes.decode('ascii').split(','), offset def parse_boolean(data: bytes, offset: int) -> Tuple[bool, int]: - """Parse a boolean byte. Returns (bool_value, new_offset).""" value, offset = parse_byte(data, offset) return value != 0, offset - def parse_ssh_packet(conn: socket.socket) -> bytes: - """ - Read and parse an SSH binary packet from the connection. - Returns the payload bytes (without padding). - """ header = conn.recv(SSH_HEADER_LENGTH) - if len(header) < SSH_HEADER_LENGTH: - raise ValueError("Failed to read SSH packet header") - packet_length = struct.unpack('>I', header[0:4])[0] padding_length = header[4] - - if packet_length < 1 or packet_length > MAX_PACKET_LENGTH: - raise ValueError(f"Invalid packet length: {packet_length}") - - if padding_length < MIN_PADDING_LENGTH or padding_length > MAX_PADDING_LENGTH: - raise ValueError(f"Invalid padding length: {padding_length} (must be {MIN_PADDING_LENGTH}-{MAX_PADDING_LENGTH})") - payload_length = packet_length - padding_length - 1 - if payload_length < 0: - raise ValueError(f"Invalid packet: padding_length {padding_length} exceeds packet_length {packet_length}") - remaining = packet_length - 1 data = b'' while len(data) < remaining: chunk = conn.recv(remaining - len(data)) - if not chunk: - raise ValueError("Connection closed while reading packet") + if not chunk: raise ValueError("Connection closed") data += chunk - - payload = data[0:payload_length] - - return payload - + return data[:payload_length] def parse_kexinit(payload: bytes) -> Dict[str, Any]: - """ - Parse SSH_MSG_KEXINIT message payload. - Returns a dictionary with all the algorithm lists. - """ offset = 0 - msg_type, offset = parse_byte(payload, offset) - if msg_type != SSH_MSG_KEXINIT: - raise ValueError(f"Expected SSH_MSG_KEXINIT (20), got {msg_type}") - - if offset + KEXINIT_COOKIE_LENGTH > len(payload): - raise ValueError("Insufficient data for KEXINIT cookie") + if msg_type != SSH_MSG_KEXINIT: raise ValueError("Not KEXINIT") offset += KEXINIT_COOKIE_LENGTH - - kex_algorithms, offset = parse_name_list(payload, offset) - server_host_key_algorithms, offset = parse_name_list(payload, offset) - encryption_algorithms_c2s, offset = parse_name_list(payload, offset) - encryption_algorithms_s2c, offset = parse_name_list(payload, offset) - mac_algorithms_c2s, offset = parse_name_list(payload, offset) - mac_algorithms_s2c, offset = parse_name_list(payload, offset) - compression_algorithms_c2s, offset = parse_name_list(payload, offset) - compression_algorithms_s2c, offset = parse_name_list(payload, offset) - languages_c2s, offset = parse_name_list(payload, offset) - languages_s2c, offset = parse_name_list(payload, offset) - - first_kex_packet_follows, offset = parse_boolean(payload, offset) - reserved, offset = parse_uint32(payload, offset) - + kex, offset = parse_name_list(payload, offset) + hka, offset = parse_name_list(payload, offset) + enc_c2s, offset = parse_name_list(payload, offset) + enc_s2c, offset = parse_name_list(payload, offset) + mac_c2s, offset = parse_name_list(payload, offset) + mac_s2c, offset = parse_name_list(payload, offset) + comp_c2s, offset = parse_name_list(payload, offset) + comp_s2c, offset = parse_name_list(payload, offset) + lang_c2s, offset = parse_name_list(payload, offset) + lang_s2c, offset = parse_name_list(payload, offset) + parse_boolean(payload, offset) + parse_uint32(payload, offset) return { - 'kex_algorithms': kex_algorithms, - 'server_host_key_algorithms': server_host_key_algorithms, - 'encryption_algorithms_client_to_server': encryption_algorithms_c2s, - 'encryption_algorithms_server_to_client': encryption_algorithms_s2c, - 'mac_algorithms_client_to_server': mac_algorithms_c2s, - 'mac_algorithms_server_to_client': mac_algorithms_s2c, - 'compression_algorithms_client_to_server': compression_algorithms_c2s, - 'compression_algorithms_server_to_client': compression_algorithms_s2c, - 'languages_client_to_server': languages_c2s, - 'languages_server_to_client': languages_s2c, - 'first_kex_packet_follows': first_kex_packet_follows, - 'reserved': reserved + 'kex_algorithms': kex, 'server_host_key_algorithms': hka, + 'encryption_algorithms_server_to_client': enc_s2c, + 'mac_algorithms_server_to_client': mac_s2c } - def exchange(ip: str, port: int) -> Optional[Dict[str, Any]]: - """ - Connect to SSH server and retrieve KEXINIT data. - Returns a dictionary with algorithm lists, or None on failure. - """ - kexinit_data = None conn = None try: conn = socket.create_connection((ip, port), timeout=5) print(f"[*] Connected to {ip} on port {port}...") - version_data = conn.recv(VERSION_STRING_MAX_LENGTH) - if not version_data or b'\n' not in version_data: - raise ValueError("Failed to receive SSH version string") - version = version_data.decode('ascii', errors='ignore').split('\n')[0].strip() print(f" [+] Target SSH version is: {version}") - conn.send(b'SSH-2.0-OpenSSH_6.0p1\r\n') print(" [+] Retrieving algorithm information...") - payload = parse_ssh_packet(conn) - kexinit_data = parse_kexinit(payload) - + return parse_kexinit(payload) except Exception as e: print(f"[-] Error while connecting to {ip} on port {port}: {e}") finally: - if conn: - conn.close() - - return kexinit_data - + if conn: conn.close() + return None def validate_port(port_str: str) -> Tuple[Optional[int], Optional[str]]: - """Validate that port is a valid integer in range 1-65535.""" try: port = int(port_str) - if port < 1 or port > 65535: - return None, "Port must be between 1 and 65535" - return port, None - except ValueError: - return None, "Port must be a valid integer" - + if 1 <= port <= 65535: return port, None + return None, "Port must be 1-65535" + except: return None, "Invalid port" def parse_target(target: str) -> Tuple[Optional[str], Optional[int], Optional[str]]: - """Parse target string to extract host and port, handling IPv6 addresses.""" - port: Optional[int] = 22 - host: Optional[str] = target - + port = 22 + host = target if target.startswith('['): bracket_end = target.find(']') - if bracket_end == -1: - return None, None, "Invalid format: missing closing bracket" - + if bracket_end == -1: return None, None, "Invalid IPv6" host = target[1:bracket_end] - - if bracket_end + 1 < len(target): - if target[bracket_end + 1] != ':': - return None, None, "Invalid format: expected ':' after bracket" - port_str = target[bracket_end + 2:] - if not port_str: - return None, None, "Invalid format: missing port after ':'" - port, error = validate_port(port_str) - if error: - return None, None, error + if bracket_end + 2 < len(target): + port, error = validate_port(target[bracket_end+2:]) + if error: return None, None, error elif ':' in target: - colon_count = target.count(':') - if colon_count > 1: - return None, None, "Invalid format: IPv6 addresses must be enclosed in brackets [host]:port" - - parts = target.split(':') - host = parts[0] - port, error = validate_port(parts[1]) - if error: - return None, None, error - + parts = target.split(':', 1) + host, port_str = parts + port, error = validate_port(port_str) + if error: return None, None, error return host, port, None - def scan_target(target: str) -> int: - """ - Scan target SSH server and display results. - Returns 0 on success, 1 on failure. - """ host, port, error = parse_target(target) - - if error: - print(f"[-] Error: {error}") - return 1 - - if not host or not host.strip(): - print("[-] Error: Hostname cannot be empty") + if error or not host: + print(f"[-] Error: {error or 'Invalid host'}") return 1 - - if port is None: - print("[-] Error: Invalid port") - return 1 - print(f"[*] Initiating scan for {host} on port {port}") - kexinit_data = exchange(host, port) - if kexinit_data: - display_result(kexinit_data) - return 0 - + data = exchange(host, port) + if data: display_result(data); return 0 return 1 - def print_algo_list(algo_list: List[str], title: str, strong_list: Optional[List[str]] = None) -> None: - """Print a formatted list of algorithms in two columns with optional color coding.""" if algo_list: print(f' [+] Detected {title}: ') display_list = algo_list.copy() cols = 2 - while len(display_list) % cols != 0: - display_list.append('') - - split = [display_list[i:i + len(display_list) // cols] for i in - range(0, len(display_list), len(display_list) // cols)] - + while len(display_list) % cols != 0: display_list.append('') + split = [display_list[i:i + len(display_list) // cols] for i in range(0, len(display_list), len(display_list) // cols)] for row in zip(*split): formatted_row = [] for algo in row: if algo: - if strong_list is not None: - if algo in strong_list: - colored = green(algo) - else: - colored = red(algo) + if strong_list: + colored = green(algo) if algo in strong_list else red(algo) formatted_row.append(str.ljust(colored, 37 + len(colored) - len(algo))) else: formatted_row.append(str.ljust(algo, 37)) @@ -381,87 +252,66 @@ def print_algo_list(algo_list: List[str], title: str, strong_list: Optional[List else: print(f' [-] No {title} detected!') +def detect_not_recommended_algo(detected: List[str], strong: List[str]) -> List[str]: + return [algo for algo in detected if algo not in strong] -def detect_not_recommended_algo(detected_list: List[str], strong_list: List[str]) -> List[str]: - """Identify algorithms not recommended by comparing detected against strong list.""" - return [algo for algo in detected_list if algo not in strong_list] +def display_result(data: Dict[str, Any]) -> None: + ciphers = data['encryption_algorithms_server_to_client'] + kex = data['kex_algorithms'] + macs = data['mac_algorithms_server_to_client'] + hka = data['server_host_key_algorithms'] + not_rec_c = detect_not_recommended_algo(ciphers, STRONG_CIPHERS) + not_rec_k = detect_not_recommended_algo(kex, STRONG_KEX) + not_rec_m = detect_not_recommended_algo(macs, STRONG_MACS) + not_rec_h = detect_not_recommended_algo(hka, STRONG_HOST_KEY_ALGORITHMS) -def display_result(kexinit_data: Dict[str, Any]) -> None: - """Display KEXINIT algorithm information and identify not recommended algorithms.""" - detected_ciphers = kexinit_data['encryption_algorithms_server_to_client'] - detected_kex = kexinit_data['kex_algorithms'] - detected_macs = kexinit_data['mac_algorithms_server_to_client'] - detected_hka = kexinit_data['server_host_key_algorithms'] + print_algo_list(ciphers, 'ciphers', STRONG_CIPHERS) + print_algo_list(kex, 'KEX algorithms', STRONG_KEX) + print_algo_list(macs, 'MACs', STRONG_MACS) + print_algo_list(hka, 'HostKey algorithms', STRONG_HOST_KEY_ALGORITHMS) - not_recommended_ciphers = detect_not_recommended_algo(detected_ciphers, STRONG_CIPHERS) - not_recommended_kex = detect_not_recommended_algo(detected_kex, STRONG_KEX) - not_recommended_macs = detect_not_recommended_algo(detected_macs, STRONG_MACS) - not_recommended_hka = detect_not_recommended_algo(detected_hka, STRONG_HOST_KEY_ALGORITHMS) + print_algo_list(not_rec_c, 'not recommended ciphers') + print_algo_list(not_rec_k, 'not recommended KEX algorithms') + print_algo_list(not_rec_m, 'not recommended MACs') + print_algo_list(not_rec_h, 'not recommended HostKey algorithms') - print_algo_list(detected_ciphers, 'ciphers', STRONG_CIPHERS) - print_algo_list(detected_kex, 'KEX algorithms', STRONG_KEX) - print_algo_list(detected_macs, 'MACs', STRONG_MACS) - print_algo_list(detected_hka, 'HostKey algorithms', STRONG_HOST_KEY_ALGORITHMS) + comp = data.get('compression_algorithms_server_to_client', []) + print(' [+] Compression is enabled' if 'zlib@openssh.com' in comp or 'zlib' in comp else ' [-] Compression is *not* enabled') - print_algo_list(not_recommended_ciphers, 'not recommended ciphers') - print_algo_list(not_recommended_kex, 'not recommended KEX algorithms') - print_algo_list(not_recommended_macs, 'not recommended MACs') - print_algo_list(not_recommended_hka, 'not recommended HostKey algorithms') + # SCORING + print("\n" + "="*60) + print(f"🏆 SSH SECURITY SCORE v{__version__}") + print("="*60) - compression_algos = kexinit_data['compression_algorithms_server_to_client'] - if 'zlib@openssh.com' in compression_algos or 'zlib' in compression_algos: - print(' [+] Compression is enabled') - else: - print(' [-] Compression is *not* enabled') + final_score = calculate_final_score(ciphers, kex, macs, hka) + final_int = int(final_score) + status_colored = green(get_status(final_score)) if final_int >= 80 else yellow(get_status(final_score)) if final_int >= 60 else red(get_status(final_score)) + print(f"📊 FINAL SCORE: {final_int:3d}% {status_colored}") -def main() -> None: - """Main entry point for the SSH scanner.""" - global USE_COLOR + c_avg = int(sum(get_score(c) for c in ciphers) / len(ciphers)) + k_avg = int(sum(get_score(k) for k in kex) / len(kex)) + m_avg = int(sum(get_score(m) for m in macs) / len(macs)) + h_avg = int(sum(get_score(h) for h in hka) / len(hka)) - parser = argparse.ArgumentParser( - description='SSH server cipher and algorithm scanner', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - %(prog)s github.com - %(prog)s example.com:22 - %(prog)s [::1]:22 - %(prog)s [2001:db8::1]:8022 - """ - ) - parser.add_argument( - 'target', - help='target SSH server (format: host[:port] or [ipv6]:port)' - ) - parser.add_argument( - '-v', '--version', - action='version', - version='%(prog)s 2.0' - ) - parser.add_argument( - '--no-color', - action='store_true', - help='disable colored output' - ) - parser.add_argument( - '--color', - action='store_true', - help='force colored output even when not in a TTY' - ) + print(f"🔢 Ciphers: {c_avg:3d}% | KEX: {k_avg:3d}%") + print(f"🔢 MACs: {m_avg:3d}% | HostKey: {h_avg:3d}%") + print("="*60) +def main(): + global USE_COLOR + parser = argparse.ArgumentParser(description=f'SSH Scanner v{__version__}') + parser.add_argument('target', help='host[:port]') + parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {__version__}') + parser.add_argument('--no-color', action='store_true') + parser.add_argument('--color', action='store_true') args = parser.parse_args() - # Handle color flags - if args.no_color: - USE_COLOR = False - elif args.color: - USE_COLOR = True - - exit_code = scan_target(args.target) - sys.exit(exit_code) + if args.no_color: USE_COLOR = False + elif args.color: USE_COLOR = True + sys.exit(scan_target(args.target)) if __name__ == '__main__': - main() + main() \ No newline at end of file