|
|
@@ -0,0 +1,624 @@
|
|
|
+#!/usr/bin/env python3
|
|
|
+"""
|
|
|
+CipherScanner - NS Configuration Cipher Suite Compliance Checker
|
|
|
+Reads cipher suites from scan output and checks against NS configuration file
|
|
|
+"""
|
|
|
+
|
|
|
+import re
|
|
|
+import json
|
|
|
+import argparse
|
|
|
+import sys
|
|
|
+from datetime import datetime
|
|
|
+from collections import defaultdict
|
|
|
+
|
|
|
+class CipherScanner:
|
|
|
+ """Main CipherScanner class for analyzing cipher suite compliance"""
|
|
|
+
|
|
|
+ def __init__(self, ns_conf_file, cipher_file):
|
|
|
+ """Initialize CipherScanner with configuration files"""
|
|
|
+ self.ns_conf_file = ns_conf_file
|
|
|
+ self.cipher_file = cipher_file
|
|
|
+ self.iana_ciphers = []
|
|
|
+ self.ns_config = {}
|
|
|
+ self.analysis_results = {}
|
|
|
+
|
|
|
+ def run_scan(self):
|
|
|
+ """Run the complete cipher scan and analysis"""
|
|
|
+ print(f"""
|
|
|
+╔{'═'*78}╗
|
|
|
+║{'CIPHERSCANNER':^78}║
|
|
|
+╠{'═'*78}╣
|
|
|
+║ NS Configuration: {self.ns_conf_file:<56}║
|
|
|
+║ Cipher File: {self.cipher_file:<58}║
|
|
|
+╚{'═'*78}╝
|
|
|
+ """)
|
|
|
+
|
|
|
+ # Parse cipher suites
|
|
|
+ self.iana_ciphers, _ = self.parse_ciphers_from_text(self.cipher_file)
|
|
|
+ if not self.iana_ciphers:
|
|
|
+ print("❌ No cipher suites parsed. Scan aborted.")
|
|
|
+ return False
|
|
|
+
|
|
|
+ # Parse NS configuration
|
|
|
+ self.ns_config = self.parse_ns_config(self.ns_conf_file)
|
|
|
+ if not self.ns_config:
|
|
|
+ print("❌ No NS configuration parsed. Scan aborted.")
|
|
|
+ return False
|
|
|
+
|
|
|
+ # Analyze compliance
|
|
|
+ self.analysis_results = self.analyze_cipher_compliance()
|
|
|
+
|
|
|
+ # Generate report
|
|
|
+ self.generate_report()
|
|
|
+
|
|
|
+ return True
|
|
|
+
|
|
|
+ def parse_ciphers_from_text(self, text_file):
|
|
|
+ """
|
|
|
+ Parse cipher suites from text file with format:
|
|
|
+ Cipher Suite: TLS_RSA_WITH_AES_256_CBC_SHA (0x0035)
|
|
|
+ """
|
|
|
+ ciphers = []
|
|
|
+ hex_to_name = {}
|
|
|
+
|
|
|
+ try:
|
|
|
+ with open(text_file, 'r') as f:
|
|
|
+ lines = f.readlines()
|
|
|
+
|
|
|
+ cipher_pattern = re.compile(
|
|
|
+ r'Cipher Suite:\s*([\w_]+)\s+\(([^)]+)\)',
|
|
|
+ re.IGNORECASE
|
|
|
+ )
|
|
|
+
|
|
|
+ for line in lines:
|
|
|
+ match = cipher_pattern.search(line.strip())
|
|
|
+ if match:
|
|
|
+ cipher_name = match.group(1).strip()
|
|
|
+ hex_value = match.group(2).strip()
|
|
|
+ ciphers.append(cipher_name)
|
|
|
+ hex_to_name[hex_value] = cipher_name
|
|
|
+
|
|
|
+ print(f"✅ CipherScanner parsed {len(ciphers)} cipher suites from {text_file}")
|
|
|
+ return ciphers, hex_to_name
|
|
|
+
|
|
|
+ except FileNotFoundError:
|
|
|
+ print(f"❌ CipherScanner: File not found: {text_file}")
|
|
|
+ return [], {}
|
|
|
+ except Exception as e:
|
|
|
+ print(f"❌ CipherScanner error parsing cipher file: {e}")
|
|
|
+ return [], {}
|
|
|
+
|
|
|
+ def parse_ns_config(self, ns_conf_file):
|
|
|
+ """
|
|
|
+ Parse NS configuration file for SSL/TLS related configurations
|
|
|
+ """
|
|
|
+ ssl_configs = {
|
|
|
+ 'cipher_groups': [],
|
|
|
+ 'ssl_profiles': [],
|
|
|
+ 'ssl_vservers': [],
|
|
|
+ 'ssl_parameters': {}
|
|
|
+ }
|
|
|
+
|
|
|
+ try:
|
|
|
+ with open(ns_conf_file, 'r') as f:
|
|
|
+ content = f.read()
|
|
|
+
|
|
|
+ # Remove comments and blank lines for cleaner parsing
|
|
|
+ lines = []
|
|
|
+ for line in content.split('\n'):
|
|
|
+ line = line.strip()
|
|
|
+ # Remove comments (starting with # or //)
|
|
|
+ line = re.sub(r'(#|//).*$', '', line).strip()
|
|
|
+ if line:
|
|
|
+ lines.append(line)
|
|
|
+
|
|
|
+ # Join lines for multi-line command parsing
|
|
|
+ content_clean = '\n'.join(lines)
|
|
|
+
|
|
|
+ print(f"\n🔍 CipherScanner parsing NS configuration from {ns_conf_file}")
|
|
|
+
|
|
|
+ # Find cipher groups - improved regex pattern
|
|
|
+ cipher_group_pattern = re.compile(
|
|
|
+ r'add\s+ssl\s+cipher\s+([^\s]+)\s+(.*?)(?=\n\s*(?:add|set|bind|\Z))',
|
|
|
+ re.IGNORECASE | re.DOTALL | re.MULTILINE
|
|
|
+ )
|
|
|
+
|
|
|
+ matches = cipher_group_pattern.findall(content_clean)
|
|
|
+ for match in matches:
|
|
|
+ group_name = match[0]
|
|
|
+ config = match[1].strip()
|
|
|
+
|
|
|
+ # Extract ciphers from group configuration
|
|
|
+ cipher_list = []
|
|
|
+
|
|
|
+ # Try different patterns to find cipher names
|
|
|
+ cipher_matches = re.findall(r'["\']?([A-Z0-9_\-]+)["\']?', config)
|
|
|
+ for cipher_candidate in cipher_matches:
|
|
|
+ # Filter out common non-cipher patterns
|
|
|
+ if (len(cipher_candidate) > 5 and
|
|
|
+ '-' in cipher_candidate and
|
|
|
+ not cipher_candidate.startswith(('PRIORITY', 'DEFAULT'))):
|
|
|
+ cipher_list.append(cipher_candidate)
|
|
|
+
|
|
|
+ # Alternative: Look for cipherName parameter
|
|
|
+ cipher_name_match = re.search(r'cipherName\s*[:=]\s*["\']([^"\']+)["\']', config, re.IGNORECASE)
|
|
|
+ if cipher_name_match:
|
|
|
+ cipher_string = cipher_name_match.group(1)
|
|
|
+ cipher_list = [c.strip() for c in cipher_string.split('-') if c.strip()]
|
|
|
+
|
|
|
+ ssl_configs['cipher_groups'].append({
|
|
|
+ 'name': group_name,
|
|
|
+ 'ciphers': cipher_list,
|
|
|
+ 'raw_config': config[:500] + "..." if len(config) > 500 else config
|
|
|
+ })
|
|
|
+
|
|
|
+ # Find SSL vservers
|
|
|
+ vserver_pattern = re.compile(
|
|
|
+ r'add\s+lb\s+vserver\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+).*?(?=\n\s*(?:add|set|bind|\Z))',
|
|
|
+ re.IGNORECASE | re.DOTALL | re.MULTILINE
|
|
|
+ )
|
|
|
+
|
|
|
+ vserver_matches = vserver_pattern.findall(content_clean)
|
|
|
+ for match in vserver_matches:
|
|
|
+ vserver_name = match[0]
|
|
|
+ protocol = match[1]
|
|
|
+ ip = match[2]
|
|
|
+ port = match[3]
|
|
|
+
|
|
|
+ # Look for SSL bindings for this vserver
|
|
|
+ binding_pattern = re.compile(
|
|
|
+ r'bind\s+ssl\s+vserver\s+' + re.escape(vserver_name) + r'\s+(.*?)(?=\n\s*(?:bind|add|set|\Z))',
|
|
|
+ re.IGNORECASE | re.DOTALL | re.MULTILINE
|
|
|
+ )
|
|
|
+
|
|
|
+ binding_match = binding_pattern.search(content_clean)
|
|
|
+ ssl_config = {
|
|
|
+ 'name': vserver_name,
|
|
|
+ 'protocol': protocol,
|
|
|
+ 'ip': ip,
|
|
|
+ 'port': port,
|
|
|
+ 'ssl_profile': None,
|
|
|
+ 'certificate': None,
|
|
|
+ 'cipher_group': None
|
|
|
+ }
|
|
|
+
|
|
|
+ if binding_match:
|
|
|
+ binding_config = binding_match.group(1)
|
|
|
+
|
|
|
+ # Extract cipher group
|
|
|
+ cipher_group_match = re.search(r'-cipherName\s+(\S+)', binding_config, re.IGNORECASE)
|
|
|
+ if cipher_group_match:
|
|
|
+ ssl_config['cipher_group'] = cipher_group_match.group(1)
|
|
|
+
|
|
|
+ # Extract SSL profile
|
|
|
+ profile_match = re.search(r'-sslProfile\s+(\S+)', binding_config, re.IGNORECASE)
|
|
|
+ if profile_match:
|
|
|
+ ssl_config['ssl_profile'] = profile_match.group(1)
|
|
|
+
|
|
|
+ # Extract certificate
|
|
|
+ cert_match = re.search(r'-certkeyName\s+(\S+)', binding_config, re.IGNORECASE)
|
|
|
+ if cert_match:
|
|
|
+ ssl_config['certificate'] = cert_match.group(1)
|
|
|
+
|
|
|
+ # Only include SSL/TLS vservers
|
|
|
+ if (protocol.upper() in ['SSL', 'SSL_TCP', 'SSL_BRIDGE', 'TCP'] and
|
|
|
+ port in ['443', '8443', '9443', '10443']):
|
|
|
+ ssl_configs['ssl_vservers'].append(ssl_config)
|
|
|
+
|
|
|
+ # Find SSL profiles
|
|
|
+ profile_pattern = re.compile(
|
|
|
+ r'add\s+ssl\s+profile\s+(\S+)\s+(.*?)(?=\n\s*(?:add|set|bind|\Z))',
|
|
|
+ re.IGNORECASE | re.DOTALL | re.MULTILINE
|
|
|
+ )
|
|
|
+
|
|
|
+ profile_matches = profile_pattern.findall(content_clean)
|
|
|
+ for match in profile_matches:
|
|
|
+ profile_name = match[0]
|
|
|
+ config = match[1]
|
|
|
+
|
|
|
+ # Extract cipher settings from profile
|
|
|
+ cipher_match = re.search(r'-cipherName\s+(\S+)', config, re.IGNORECASE)
|
|
|
+ cipher_group = cipher_match.group(1) if cipher_match else None
|
|
|
+
|
|
|
+ # Extract other SSL settings
|
|
|
+ tls_settings = {
|
|
|
+ 'tls11_enabled': 'NOT_FOUND',
|
|
|
+ 'tls12_enabled': 'NOT_FOUND',
|
|
|
+ 'tls13_enabled': 'NOT_FOUND'
|
|
|
+ }
|
|
|
+
|
|
|
+ for tls_ver in ['tls11', 'tls12', 'tls13']:
|
|
|
+ tls_match = re.search(rf'-{tls_ver}\s+(\S+)', config, re.IGNORECASE)
|
|
|
+ if tls_match:
|
|
|
+ tls_settings[f'{tls_ver}_enabled'] = tls_match.group(1)
|
|
|
+
|
|
|
+ ssl_configs['ssl_profiles'].append({
|
|
|
+ 'name': profile_name,
|
|
|
+ 'cipher_group': cipher_group,
|
|
|
+ **tls_settings,
|
|
|
+ 'raw_config': config[:300] + "..." if len(config) > 300 else config
|
|
|
+ })
|
|
|
+
|
|
|
+ # Find SSL parameter settings
|
|
|
+ param_patterns = [
|
|
|
+ (r'set\s+ssl\s+parameter\s+-ssl3\s+(\S+)', 'ssl3_enabled'),
|
|
|
+ (r'set\s+ssl\s+parameter\s+-tls1\s+(\S+)', 'tls1_enabled'),
|
|
|
+ (r'set\s+ssl\s+parameter\s+-tls11\s+(\S+)', 'tls11_enabled'),
|
|
|
+ (r'set\s+ssl\s+parameter\s+-tls12\s+(\S+)', 'tls12_enabled'),
|
|
|
+ (r'set\s+ssl\s+parameter\s+-tls13\s+(\S+)', 'tls13_enabled'),
|
|
|
+ (r'set\s+ssl\s+parameter\s+-denySSLReneg\s+(\S+)', 'deny_ssl_reneg'),
|
|
|
+ ]
|
|
|
+
|
|
|
+ for pattern, key in param_patterns:
|
|
|
+ match = re.search(pattern, content_clean, re.IGNORECASE)
|
|
|
+ if match:
|
|
|
+ ssl_configs['ssl_parameters'][key] = match.group(1)
|
|
|
+
|
|
|
+ print(f"✅ CipherScanner found {len(ssl_configs['cipher_groups'])} cipher groups")
|
|
|
+ print(f"✅ CipherScanner found {len(ssl_configs['ssl_vservers'])} SSL vservers")
|
|
|
+ print(f"✅ CipherScanner found {len(ssl_configs['ssl_profiles'])} SSL profiles")
|
|
|
+
|
|
|
+ return ssl_configs
|
|
|
+
|
|
|
+ except FileNotFoundError:
|
|
|
+ print(f"❌ CipherScanner: File not found: {ns_conf_file}")
|
|
|
+ return {}
|
|
|
+ except Exception as e:
|
|
|
+ print(f"❌ CipherScanner error parsing NS configuration: {e}")
|
|
|
+ import traceback
|
|
|
+ traceback.print_exc()
|
|
|
+ return {}
|
|
|
+
|
|
|
+ def map_ns_cipher_to_iana(self, ns_cipher):
|
|
|
+ """
|
|
|
+ Map NetScaler cipher names to IANA/RFC names
|
|
|
+ """
|
|
|
+ # Clean the cipher name
|
|
|
+ ns_cipher = ns_cipher.strip().upper()
|
|
|
+
|
|
|
+ cipher_mappings = {
|
|
|
+ # AES CBC ciphers
|
|
|
+ 'SSL3-RSA-AES-256-CBC-SHA': 'TLS_RSA_WITH_AES_256_CBC_SHA',
|
|
|
+ 'SSL3-RSA-AES-128-CBC-SHA': 'TLS_RSA_WITH_AES_128_CBC_SHA',
|
|
|
+ 'TLS1-RSA-AES-256-CBC-SHA': 'TLS_RSA_WITH_AES_256_CBC_SHA',
|
|
|
+ 'TLS1-RSA-AES-128-CBC-SHA': 'TLS_RSA_WITH_AES_128_CBC_SHA',
|
|
|
+ 'TLS1.2-RSA-AES-256-CBC-SHA': 'TLS_RSA_WITH_AES_256_CBC_SHA',
|
|
|
+ 'TLS1.2-RSA-AES-128-CBC-SHA': 'TLS_RSA_WITH_AES_128_CBC_SHA',
|
|
|
+ 'TLS1.2-RSA-AES-256-CBC-SHA256': 'TLS_RSA_WITH_AES_256_CBC_SHA256',
|
|
|
+ 'TLS1.2-RSA-AES-128-CBC-SHA256': 'TLS_RSA_WITH_AES_128_CBC_SHA256',
|
|
|
+
|
|
|
+ # AES GCM ciphers
|
|
|
+ 'TLS1.2-RSA-AES-256-GCM-SHA384': 'TLS_RSA_WITH_AES_256_GCM_SHA384',
|
|
|
+ 'TLS1.2-RSA-AES-128-GCM-SHA256': 'TLS_RSA_WITH_AES_128_GCM_SHA256',
|
|
|
+
|
|
|
+ # ECDHE RSA ciphers
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES-256-CBC-SHA': 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES-128-CBC-SHA': 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES-256-CBC-SHA384': 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES-128-CBC-SHA256': 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES-256-GCM-SHA384': 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES-128-GCM-SHA256': 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
|
|
|
+
|
|
|
+ # ECDHE ECDSA ciphers
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES-256-CBC-SHA': 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES-128-CBC-SHA': 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES-256-CBC-SHA384': 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384',
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES-128-CBC-SHA256': 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256',
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES-256-GCM-SHA384': 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES-128-GCM-SHA256': 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
|
|
+
|
|
|
+ # DHE RSA ciphers
|
|
|
+ 'TLS1.2-DHE-RSA-AES-256-CBC-SHA': 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA',
|
|
|
+ 'TLS1.2-DHE-RSA-AES-128-CBC-SHA': 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA',
|
|
|
+ 'TLS1.2-DHE-RSA-AES-256-CBC-SHA256': 'TLS_DHE_RSA_WITH_AES_256_CBC_SHA256',
|
|
|
+ 'TLS1.2-DHE-RSA-AES-128-CBC-SHA256': 'TLS_DHE_RSA_WITH_AES_128_CBC_SHA256',
|
|
|
+ 'TLS1.2-DHE-RSA-AES-256-GCM-SHA384': 'TLS_DHE_RSA_WITH_AES_256_GCM_SHA384',
|
|
|
+ 'TLS1.2-DHE-RSA-AES-128-GCM-SHA256': 'TLS_DHE_RSA_WITH_AES_128_GCM_SHA256',
|
|
|
+
|
|
|
+ # TLS 1.3 ciphers (NetScaler format)
|
|
|
+ 'TLS1.3-AES256-GCM-SHA384': 'TLS_AES_256_GCM_SHA384',
|
|
|
+ 'TLS1.3-AES128-GCM-SHA256': 'TLS_AES_128_GCM_SHA256',
|
|
|
+ 'TLS1.3-CHACHA20-POLY1305-SHA256': 'TLS_CHACHA20_POLY1305_SHA256',
|
|
|
+
|
|
|
+ # Weak/Deprecated ciphers
|
|
|
+ 'SSL3-RSA-RC4-MD5': 'TLS_RSA_WITH_RC4_128_MD5',
|
|
|
+ 'SSL3-RSA-RC4-SHA': 'TLS_RSA_WITH_RC4_128_SHA',
|
|
|
+ 'SSL3-RSA-DES-CBC3-SHA': 'TLS_RSA_WITH_3DES_EDE_CBC_SHA',
|
|
|
+ 'TLS1-RSA-RC4-MD5': 'TLS_RSA_WITH_RC4_128_MD5',
|
|
|
+ 'TLS1-RSA-RC4-SHA': 'TLS_RSA_WITH_RC4_128_SHA',
|
|
|
+
|
|
|
+ # Common NetScaler cipher patterns (simplified)
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES256-SHA384': 'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384',
|
|
|
+ 'TLS1.2-ECDHE-ECDSA-AES128-SHA256': 'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES256-SHA384': 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384',
|
|
|
+ 'TLS1.2-ECDHE-RSA-AES128-SHA256': 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256',
|
|
|
+ }
|
|
|
+
|
|
|
+ # Direct match
|
|
|
+ if ns_cipher in cipher_mappings:
|
|
|
+ return cipher_mappings[ns_cipher]
|
|
|
+
|
|
|
+ # Try to match patterns
|
|
|
+ # Remove version prefixes for pattern matching
|
|
|
+ cipher_without_version = ns_cipher
|
|
|
+ version_prefixes = ['SSL3-', 'TLS1-', 'TLS1.1-', 'TLS1.2-', 'TLS1.3-']
|
|
|
+ for prefix in version_prefixes:
|
|
|
+ if ns_cipher.startswith(prefix):
|
|
|
+ cipher_without_version = ns_cipher[len(prefix):]
|
|
|
+ break
|
|
|
+
|
|
|
+ # Pattern-based mapping
|
|
|
+ if 'AES256-GCM-SHA384' in cipher_without_version and 'ECDHE' not in cipher_without_version and 'DHE' not in cipher_without_version:
|
|
|
+ return 'TLS_RSA_WITH_AES_256_GCM_SHA384'
|
|
|
+ elif 'AES128-GCM-SHA256' in cipher_without_version and 'ECDHE' not in cipher_without_version and 'DHE' not in cipher_without_version:
|
|
|
+ return 'TLS_RSA_WITH_AES_128_GCM_SHA256'
|
|
|
+ elif 'AES256-CBC-SHA256' in cipher_without_version and 'ECDHE' not in cipher_without_version and 'DHE' not in cipher_without_version:
|
|
|
+ return 'TLS_RSA_WITH_AES_256_CBC_SHA256'
|
|
|
+ elif 'AES128-CBC-SHA256' in cipher_without_version and 'ECDHE' not in cipher_without_version and 'DHE' not in cipher_without_version:
|
|
|
+ return 'TLS_RSA_WITH_AES_128_CBC_SHA256'
|
|
|
+ elif 'AES256-CBC-SHA' in cipher_without_version and 'ECDHE' not in cipher_without_version and 'DHE' not in cipher_without_version:
|
|
|
+ return 'TLS_RSA_WITH_AES_256_CBC_SHA'
|
|
|
+ elif 'AES128-CBC-SHA' in cipher_without_version and 'ECDHE' not in cipher_without_version and 'DHE' not in cipher_without_version:
|
|
|
+ return 'TLS_RSA_WITH_AES_128_CBC_SHA'
|
|
|
+ elif 'AES256-GCM-SHA384' in cipher_without_version and 'ECDHE-RSA' in cipher_without_version:
|
|
|
+ return 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384'
|
|
|
+ elif 'AES128-GCM-SHA256' in cipher_without_version and 'ECDHE-RSA' in cipher_without_version:
|
|
|
+ return 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'
|
|
|
+ elif 'AES256-GCM-SHA384' in cipher_without_version and 'ECDHE-ECDSA' in cipher_without_version:
|
|
|
+ return 'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384'
|
|
|
+ elif 'AES128-GCM-SHA256' in cipher_without_version and 'ECDHE-ECDSA' in cipher_without_version:
|
|
|
+ return 'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256'
|
|
|
+
|
|
|
+ return ns_cipher # Return as-is if no mapping found
|
|
|
+
|
|
|
+ def analyze_cipher_compliance(self):
|
|
|
+ """
|
|
|
+ Analyze cipher compliance between IANA ciphers and NS configuration
|
|
|
+ """
|
|
|
+ results = {
|
|
|
+ 'configured_ciphers': set(),
|
|
|
+ 'configured_in_ns': [],
|
|
|
+ 'not_configured': [],
|
|
|
+ 'weak_ciphers_found': [],
|
|
|
+ 'tls_versions': {},
|
|
|
+ 'cipher_group_analysis': [],
|
|
|
+ 'ns_cipher_mappings': {}
|
|
|
+ }
|
|
|
+
|
|
|
+ # Extract all ciphers configured in NS
|
|
|
+ for group in self.ns_config.get('cipher_groups', []):
|
|
|
+ group_ciphers = []
|
|
|
+ for ns_cipher in group['ciphers']:
|
|
|
+ iana_cipher = self.map_ns_cipher_to_iana(ns_cipher)
|
|
|
+ results['configured_ciphers'].add(iana_cipher)
|
|
|
+ results['ns_cipher_mappings'][ns_cipher] = iana_cipher
|
|
|
+ group_ciphers.append({
|
|
|
+ 'ns_name': ns_cipher,
|
|
|
+ 'iana_name': iana_cipher
|
|
|
+ })
|
|
|
+
|
|
|
+ results['cipher_group_analysis'].append({
|
|
|
+ 'group_name': group['name'],
|
|
|
+ 'ciphers': group_ciphers,
|
|
|
+ 'count': len(group_ciphers)
|
|
|
+ })
|
|
|
+
|
|
|
+ # Check which IANA ciphers are configured
|
|
|
+ for iana_cipher in self.iana_ciphers:
|
|
|
+ if iana_cipher in results['configured_ciphers']:
|
|
|
+ results['configured_in_ns'].append(iana_cipher)
|
|
|
+ else:
|
|
|
+ results['not_configured'].append(iana_cipher)
|
|
|
+
|
|
|
+ # Check for weak ciphers
|
|
|
+ weak_patterns = [
|
|
|
+ 'RC4', 'DES', '_3DES_', 'MD5', 'NULL', 'EXPORT',
|
|
|
+ 'ANON', 'ADH', 'IDEA', 'SEED', 'CAMELLIA'
|
|
|
+ ]
|
|
|
+
|
|
|
+ for cipher in results['configured_ciphers']:
|
|
|
+ if any(pattern in cipher.upper() for pattern in weak_patterns):
|
|
|
+ results['weak_ciphers_found'].append(cipher)
|
|
|
+
|
|
|
+ # Check TLS version settings
|
|
|
+ params = self.ns_config.get('ssl_parameters', {})
|
|
|
+ results['tls_versions'] = {
|
|
|
+ 'SSL3': params.get('ssl3_enabled', 'NOT_CONFIGURED'),
|
|
|
+ 'TLS1.0': params.get('tls1_enabled', 'NOT_CONFIGURED'),
|
|
|
+ 'TLS1.1': params.get('tls11_enabled', 'NOT_CONFIGURED'),
|
|
|
+ 'TLS1.2': params.get('tls12_enabled', 'NOT_CONFIGURED'),
|
|
|
+ 'TLS1.3': params.get('tls13_enabled', 'NOT_CONFIGURED')
|
|
|
+ }
|
|
|
+
|
|
|
+ return results
|
|
|
+
|
|
|
+ def generate_report(self):
|
|
|
+ """
|
|
|
+ Generate detailed compliance report
|
|
|
+ """
|
|
|
+ print(f"\n{'='*80}")
|
|
|
+ print(f"CIPHERSCANNER REPORT")
|
|
|
+ print(f"{'='*80}")
|
|
|
+
|
|
|
+ print(f"\n📊 CipherScanner Summary:")
|
|
|
+ print(f" IANA Ciphers Found: {len(self.iana_ciphers)}")
|
|
|
+ print(f" Ciphers Configured in NS: {len(self.analysis_results['configured_ciphers'])}")
|
|
|
+ print(f" Ciphers Matched: {len(self.analysis_results['configured_in_ns'])}")
|
|
|
+ print(f" Ciphers Not Configured: {len(self.analysis_results['not_configured'])}")
|
|
|
+ print(f" Weak Ciphers Found: {len(self.analysis_results['weak_ciphers_found'])}")
|
|
|
+
|
|
|
+ print(f"\n🔒 TLS Version Configuration:")
|
|
|
+ for version, status in self.analysis_results['tls_versions'].items():
|
|
|
+ status_icon = '✅' if status == 'DISABLED' and version in ['SSL3', 'TLS1.0', 'TLS1.1'] else \
|
|
|
+ '✅' if status == 'ENABLED' and version in ['TLS1.2', 'TLS1.3'] else \
|
|
|
+ '⚠️ ' if status == 'ENABLED' and version in ['SSL3', 'TLS1.0', 'TLS1.1'] else \
|
|
|
+ '❓'
|
|
|
+ print(f" {status_icon} {version}: {status}")
|
|
|
+
|
|
|
+ if self.analysis_results['cipher_group_analysis']:
|
|
|
+ print(f"\n📁 Cipher Group Analysis:")
|
|
|
+ for group in self.analysis_results['cipher_group_analysis']:
|
|
|
+ print(f" • {group['group_name']}: {group['count']} ciphers")
|
|
|
+
|
|
|
+ if self.analysis_results['configured_in_ns']:
|
|
|
+ print(f"\n✅ Ciphers Configured in NS ({len(self.analysis_results['configured_in_ns'])}):")
|
|
|
+ for cipher in sorted(self.analysis_results['configured_in_ns'])[:20]:
|
|
|
+ print(f" ✓ {cipher}")
|
|
|
+ if len(self.analysis_results['configured_in_ns']) > 20:
|
|
|
+ print(f" ... and {len(self.analysis_results['configured_in_ns']) - 20} more")
|
|
|
+
|
|
|
+ if self.analysis_results['not_configured']:
|
|
|
+ print(f"\n❌ Ciphers NOT Configured in NS ({len(self.analysis_results['not_configured'])}):")
|
|
|
+ for cipher in sorted(self.analysis_results['not_configured'])[:20]:
|
|
|
+ print(f" ✗ {cipher}")
|
|
|
+ if len(self.analysis_results['not_configured']) > 20:
|
|
|
+ print(f" ... and {len(self.analysis_results['not_configured']) - 20} more")
|
|
|
+
|
|
|
+ if self.analysis_results['weak_ciphers_found']:
|
|
|
+ print(f"\n⚠️ WEAK Ciphers Found in NS Configuration ({len(self.analysis_results['weak_ciphers_found'])}):")
|
|
|
+ for cipher in sorted(self.analysis_results['weak_ciphers_found']):
|
|
|
+ print(f" ⚠️ {cipher}")
|
|
|
+ print("\n CipherScanner RECOMMENDATION: Remove weak ciphers from configuration")
|
|
|
+
|
|
|
+ # SSL VServer Summary
|
|
|
+ if self.ns_config.get('ssl_vservers'):
|
|
|
+ print(f"\n🌐 SSL Virtual Servers ({len(self.ns_config['ssl_vservers'])}):")
|
|
|
+ for vs in self.ns_config['ssl_vservers']:
|
|
|
+ cipher_status = "✅" if vs.get('cipher_group') else "❌"
|
|
|
+ print(f" {cipher_status} {vs['name']} ({vs['ip']}:{vs['port']})")
|
|
|
+ if vs.get('cipher_group'):
|
|
|
+ print(f" Cipher Group: {vs['cipher_group']}")
|
|
|
+ if vs.get('ssl_profile'):
|
|
|
+ print(f" SSL Profile: {vs['ssl_profile']}")
|
|
|
+
|
|
|
+ # Security Recommendations
|
|
|
+ print(f"\n📋 CIPHERSCANNER SECURITY RECOMMENDATIONS:")
|
|
|
+ recommendations = []
|
|
|
+
|
|
|
+ # Check for weak protocols
|
|
|
+ if self.analysis_results['tls_versions'].get('SSL3') == 'ENABLED':
|
|
|
+ recommendations.append("❌ DISABLE SSL 3.0 (Vulnerable to POODLE attack)")
|
|
|
+ if self.analysis_results['tls_versions'].get('TLS1.0') == 'ENABLED':
|
|
|
+ recommendations.append("⚠️ DISABLE TLS 1.0 (Considered weak)")
|
|
|
+ if self.analysis_results['tls_versions'].get('TLS1.1') == 'ENABLED':
|
|
|
+ recommendations.append("⚠️ CONSIDER DISABLING TLS 1.1")
|
|
|
+
|
|
|
+ if self.analysis_results['weak_ciphers_found']:
|
|
|
+ recommendations.append("❌ REMOVE weak ciphers (RC4, DES, 3DES, MD5)")
|
|
|
+
|
|
|
+ if len(self.analysis_results['not_configured']) > 0:
|
|
|
+ recommendations.append(f"⚠️ Configure missing {len(self.analysis_results['not_configured'])} cipher(s)")
|
|
|
+
|
|
|
+ if not recommendations:
|
|
|
+ recommendations.append("✅ CipherScanner: Configuration looks secure!")
|
|
|
+
|
|
|
+ for i, rec in enumerate(recommendations, 1):
|
|
|
+ print(f" {i}. {rec}")
|
|
|
+
|
|
|
+ print(f"\n{'='*80}")
|
|
|
+ print(f"CipherScanner report generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
+ print(f"{'='*80}")
|
|
|
+
|
|
|
+ def save_json_report(self, output_file):
|
|
|
+ """Save comprehensive report as JSON"""
|
|
|
+ report = {
|
|
|
+ 'metadata': {
|
|
|
+ 'generated_at': datetime.now().isoformat(),
|
|
|
+ 'tool': 'CipherScanner',
|
|
|
+ 'version': '1.0.0',
|
|
|
+ 'iana_cipher_count': len(self.iana_ciphers),
|
|
|
+ 'ns_config_file': self.ns_conf_file,
|
|
|
+ 'cipher_file': self.cipher_file
|
|
|
+ },
|
|
|
+ 'iana_ciphers': self.iana_ciphers,
|
|
|
+ 'ns_configuration_summary': {
|
|
|
+ 'cipher_groups_count': len(self.ns_config.get('cipher_groups', [])),
|
|
|
+ 'ssl_vservers_count': len(self.ns_config.get('ssl_vservers', [])),
|
|
|
+ 'ssl_profiles_count': len(self.ns_config.get('ssl_profiles', [])),
|
|
|
+ 'tls_versions': self.ns_config.get('ssl_parameters', {})
|
|
|
+ },
|
|
|
+ 'compliance_analysis': {
|
|
|
+ 'configured_ciphers': list(self.analysis_results['configured_ciphers']),
|
|
|
+ 'configured_in_ns': self.analysis_results['configured_in_ns'],
|
|
|
+ 'not_configured': self.analysis_results['not_configured'],
|
|
|
+ 'weak_ciphers_found': self.analysis_results['weak_ciphers_found'],
|
|
|
+ 'tls_versions': self.analysis_results['tls_versions'],
|
|
|
+ 'ns_cipher_mappings': self.analysis_results['ns_cipher_mappings']
|
|
|
+ },
|
|
|
+ 'security_assessment': {
|
|
|
+ 'has_weak_ciphers': len(self.analysis_results['weak_ciphers_found']) > 0,
|
|
|
+ 'weak_cipher_count': len(self.analysis_results['weak_ciphers_found']),
|
|
|
+ 'tls1_2_enabled': self.analysis_results['tls_versions'].get('TLS1.2') == 'ENABLED',
|
|
|
+ 'tls1_3_enabled': self.analysis_results['tls_versions'].get('TLS1.3') == 'ENABLED',
|
|
|
+ 'ssl3_disabled': self.analysis_results['tls_versions'].get('SSL3') == 'DISABLED',
|
|
|
+ 'overall_grade': 'A' if len(self.analysis_results['weak_ciphers_found']) == 0 else 'C'
|
|
|
+ },
|
|
|
+ 'cipher_scanner_info': {
|
|
|
+ 'scan_completed': True,
|
|
|
+ 'scan_timestamp': datetime.now().isoformat(),
|
|
|
+ 'exit_code': self.get_exit_code()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ with open(output_file, 'w') as f:
|
|
|
+ json.dump(report, f, indent=2)
|
|
|
+
|
|
|
+ print(f"\n💾 CipherScanner JSON report saved to: {output_file}")
|
|
|
+ return report
|
|
|
+
|
|
|
+ def get_exit_code(self):
|
|
|
+ """Determine exit code based on scan results"""
|
|
|
+ if self.analysis_results['weak_ciphers_found']:
|
|
|
+ return 2 # Failure: weak ciphers detected
|
|
|
+ elif len(self.analysis_results['not_configured']) > len(self.iana_ciphers) * 0.5:
|
|
|
+ return 1 # Warning: less than 50% of ciphers configured
|
|
|
+ else:
|
|
|
+ return 0 # Success
|
|
|
+
|
|
|
+def main():
|
|
|
+ parser = argparse.ArgumentParser(
|
|
|
+ description='CipherScanner - NS Configuration Cipher Suite Compliance Checker',
|
|
|
+ formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
+ epilog="""
|
|
|
+Examples:
|
|
|
+ %(prog)s --ns-conf ns.conf --ciphers ciphers.txt
|
|
|
+ %(prog)s --ns-conf /path/to/ns.conf --ciphers scan_results.txt --output report.json
|
|
|
+ %(prog)s --ns-conf ns.conf --ciphers ciphers.txt --verbose
|
|
|
+
|
|
|
+CipherScanner helps security teams audit NS load balancer cipher configurations
|
|
|
+against discovered cipher suites from security scans.
|
|
|
+ """
|
|
|
+ )
|
|
|
+
|
|
|
+ parser.add_argument('--nsconf', required=True, help='NS configuration file (ns.conf)')
|
|
|
+ parser.add_argument('--cipher', required=True, help='Cipher suites text file')
|
|
|
+ parser.add_argument('--output', help='Output JSON report file')
|
|
|
+ parser.add_argument('--verbose', action='store_true', help='Show detailed parsing information')
|
|
|
+
|
|
|
+ args = parser.parse_args()
|
|
|
+
|
|
|
+ # Initialize and run CipherScanner
|
|
|
+ scanner = CipherScanner(args.nsconf, args.cipher)
|
|
|
+
|
|
|
+ if scanner.run_scan():
|
|
|
+ # Save JSON report if requested
|
|
|
+ if args.output:
|
|
|
+ scanner.save_json_report(args.output)
|
|
|
+
|
|
|
+ # Exit with appropriate code
|
|
|
+ exit_code = scanner.get_exit_code()
|
|
|
+
|
|
|
+ if exit_code == 0:
|
|
|
+ print(f"\n✅ CipherScanner: Compliance PASS - Configuration looks good!")
|
|
|
+ elif exit_code == 1:
|
|
|
+ print(f"\n⚠️ CipherScanner: Warning - Less than 50% of scanned ciphers are configured")
|
|
|
+ else:
|
|
|
+ print(f"\n❌ CipherScanner: Compliance FAILURE - Weak ciphers detected!")
|
|
|
+
|
|
|
+ sys.exit(exit_code)
|
|
|
+ else:
|
|
|
+ print(f"\n❌ CipherScanner: Scan failed due to errors")
|
|
|
+ sys.exit(3)
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ main()
|