def test_badssl_compliant_with_modern(self): # Given the scan results for a server that is compliant with the "modern" Mozilla config scanner = Scanner() scanner.queue_scans( [ServerScanRequest(server_location=ServerNetworkLocation(hostname="mozilla-modern.badssl.com"))] ) server_scan_result = next(scanner.get_results()) # When checking if the server is compliant with the Mozilla "modern" TLS config # It succeeds and the server is returned as compliant checker = MozillaTlsConfigurationChecker.get_default() checker.check_server( against_config=MozillaTlsConfigurationEnum.MODERN, server_scan_result=server_scan_result, ) # And the server is returned as NOT compliant for the other Mozilla configs for mozilla_config in [MozillaTlsConfigurationEnum.OLD, MozillaTlsConfigurationEnum.INTERMEDIATE]: with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration): checker.check_server(against_config=mozilla_config, server_scan_result=server_scan_result)
def server_scan_result_for_google(): scanner = Scanner() scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="google.com"))]) for server_scan_result in scanner.get_results(): yield server_scan_result
def main() -> None: # First create the scan requests for each server that we want to scan try: all_scan_requests = [ ServerScanRequest(server_location=ServerNetworkLocation(hostname="cloudflare.com")), ServerScanRequest(server_location=ServerNetworkLocation(hostname="google.com")), ] except ServerHostnameCouldNotBeResolved: # Handle bad input ie. invalid hostnames print("Error resolving the supplied hostnames") return # Then queue all the scans scanner = Scanner() scanner.queue_scans(all_scan_requests) # And retrieve and process the results for each server for server_scan_result in scanner.get_results(): print(f"\n\n****Results for {server_scan_result.server_location.hostname}****") # Were we able to connect to the server and run the scan? if server_scan_result.scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY: # No we weren't print( f"\nError: Could not connect to {server_scan_result.server_location.hostname}:" f" {server_scan_result.connectivity_error_trace}" ) continue # Since we were able to run the scan, scan_result is populated assert server_scan_result.scan_result # Process the result of the SSL 2.0 scan command ssl2_attempt = server_scan_result.scan_result.ssl_2_0_cipher_suites if ssl2_attempt.status == ScanCommandAttemptStatusEnum.ERROR: # An error happened when this scan command was run _print_failed_scan_command_attempt(ssl2_attempt) elif ssl2_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: # This scan command was run successfully ssl2_result = ssl2_attempt.result assert ssl2_result print("\nAccepted cipher suites for SSL 2.0:") for accepted_cipher_suite in ssl2_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # Process the result of the TLS 1.3 scan command tls1_3_attempt = server_scan_result.scan_result.tls_1_3_cipher_suites if tls1_3_attempt.status == ScanCommandAttemptStatusEnum.ERROR: _print_failed_scan_command_attempt(ssl2_attempt) elif tls1_3_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: tls1_3_result = tls1_3_attempt.result assert tls1_3_result print("\nAccepted cipher suites for TLS 1.3:") for accepted_cipher_suite in tls1_3_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # Process the result of the certificate info scan command certinfo_attempt = server_scan_result.scan_result.certificate_info if certinfo_attempt.status == ScanCommandAttemptStatusEnum.ERROR: _print_failed_scan_command_attempt(certinfo_attempt) elif certinfo_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: certinfo_result = certinfo_attempt.result assert certinfo_result print("\nLeaf certificates deployed:") for cert_deployment in certinfo_result.certificate_deployments: leaf_cert = cert_deployment.received_certificate_chain[0] print( f"{leaf_cert.public_key().__class__.__name__}: {leaf_cert.subject.rfc4514_string()}" f" (Serial: {leaf_cert.serial_number})" )
def main() -> None: # Parse the supplied command line date_scans_started = datetime.utcnow() sslyze_parser = CommandLineParser(__version__) try: parsed_command_line = sslyze_parser.parse_command_line() except CommandLineParsingError as e: print(e.get_error_msg()) return # Setup the observer to print to the console, if needed scanner_observers = [] if not parsed_command_line.should_disable_console_output: observer_for_console_output = ObserverToGenerateConsoleOutput( file_to=sys.stdout, json_path_out=parsed_command_line.json_path_out ) observer_for_console_output.command_line_parsed(parsed_command_line=parsed_command_line) scanner_observers.append(observer_for_console_output) # Setup the scanner sslyze_scanner = Scanner( per_server_concurrent_connections_limit=parsed_command_line.per_server_concurrent_connections_limit, concurrent_server_scans_limit=parsed_command_line.concurrent_server_scans_limit, observers=scanner_observers, ) # Queue the scans all_server_scan_requests = [] for server_location, network_config in parsed_command_line.servers_to_scans: scan_request = ServerScanRequest( server_location=server_location, network_configuration=network_config, scan_commands=parsed_command_line.scan_commands, scan_commands_extra_arguments=parsed_command_line.scan_commands_extra_arguments, ) all_server_scan_requests.append(scan_request) # If there are servers that we were able to resolve, scan them all_server_scan_results = [] if all_server_scan_requests: sslyze_scanner.queue_scans(all_server_scan_requests) for result in sslyze_scanner.get_results(): # Results are actually displayed by the observer; here we just store them all_server_scan_results.append(result) # Write results to a JSON file if needed json_file_out: Optional[TextIO] = None if parsed_command_line.should_print_json_to_console: json_file_out = sys.stdout elif parsed_command_line.json_path_out: json_file_out = parsed_command_line.json_path_out.open("wt", encoding="utf-8") if json_file_out: json_output = SslyzeOutputAsJson( server_scan_results=[ServerScanResultAsJson.from_orm(result) for result in all_server_scan_results], invalid_server_strings=[ InvalidServerStringAsJson.from_orm(bad_server) for bad_server in parsed_command_line.invalid_servers ], date_scans_started=date_scans_started, date_scans_completed=datetime.utcnow(), ) json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True) json_file_out.write(json_output_as_str) # If we printed the JSON results to the console, don't run the Mozilla compliance check so we return valid JSON if parsed_command_line.should_print_json_to_console: sys.exit(0) if not all_server_scan_results: # There are no results to present: all supplied server strings were invalid? sys.exit(0) # Check the results against the Mozilla config if needed are_all_servers_compliant = True # TODO(AD): Expose format_title method title = ObserverToGenerateConsoleOutput._format_title("Compliance against Mozilla TLS configuration") print() print(title) if not parsed_command_line.check_against_mozilla_config: print(" Disabled; use --mozilla_config={old, intermediate, modern}.\n") else: print( f' Checking results against Mozilla\'s "{parsed_command_line.check_against_mozilla_config}"' f" configuration. See https://ssl-config.mozilla.org/ for more details.\n" ) mozilla_checker = MozillaTlsConfigurationChecker.get_default() for server_scan_result in all_server_scan_results: try: mozilla_checker.check_server( against_config=parsed_command_line.check_against_mozilla_config, server_scan_result=server_scan_result, ) print(f" {server_scan_result.server_location.display_string}: OK - Compliant.\n") except ServerNotCompliantWithMozillaTlsConfiguration as e: are_all_servers_compliant = False print(f" {server_scan_result.server_location.display_string}: FAILED - Not compliant.") for criteria, error_description in e.issues.items(): print(f" * {criteria}: {error_description}") print() except ServerScanResultIncomplete: are_all_servers_compliant = False print( f" {server_scan_result.server_location.display_string}: ERROR - Scan did not run successfully;" f" review the scan logs above." ) if not are_all_servers_compliant: # Return a non-zero error code to signal failure (for example to fail a CI/CD pipeline) sys.exit(1)
def analyze(hostname: str, port: int) -> List[Tuple[int, str]]: results = [] # Define the server that you want to scan try: server_location = ServerNetworkLocation(hostname, port) except ServerHostnameCouldNotBeResolved: log_red(_("Could not resolve {0}"), hostname) return results # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest( server_location=server_location, scan_commands={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.ROBOT, ScanCommand.HEARTBLEED, ScanCommand.TLS_COMPRESSION, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.SESSION_RENEGOTIATION, ScanCommand.HTTP_HEADERS }, network_configuration=ServerNetworkConfiguration( tls_server_name_indication=server_location.hostname, network_timeout=5, network_max_retries=2)) scanner.queue_scans([server_scan_req]) # TLS 1.2 / 1.3 results good_protocols = { ScanCommand.TLS_1_2_CIPHER_SUITES: "TLS v1.2", ScanCommand.TLS_1_3_CIPHER_SUITES: "TLS v1.3" } # https://blog.mozilla.org/security/2014/10/14/the-poodle-attack-and-the-end-of-ssl-3-0/ # https://blog.qualys.com/product-tech/2018/11/19/grade-change-for-tls-1-0-and-tls-1-1-protocols bad_protocols = { ScanCommand.SSL_2_0_CIPHER_SUITES: "SSL v2", ScanCommand.SSL_3_0_CIPHER_SUITES: "SSL v3", ScanCommand.TLS_1_0_CIPHER_SUITES: "TLS v1.0", ScanCommand.TLS_1_1_CIPHER_SUITES: "TLS v1.1" } # Then retrieve the results for result in scanner.get_results(): log_blue("\n" + _("Results for") + f" {result.server_location.hostname}:") deprecated_protocols = [] if result.connectivity_error_trace: # Stuff like connection timeout log_red(result.connectivity_error_trace) continue for scan_command in result.scan_result.__annotations__: scan_results = getattr(result.scan_result, scan_command) if scan_results.error_reason: log_red(scan_results.error_reason) continue if scan_results.status != ScanCommandAttemptStatusEnum.COMPLETED: continue if scan_command == ScanCommand.CERTIFICATE_INFO: for level, message in process_certificate_info( scan_results.result): results.append((level, message)) elif scan_command in bad_protocols: if scan_results.result.accepted_cipher_suites: deprecated_protocols.append(bad_protocols[scan_command]) elif scan_command == ScanCommand.ROBOT: if scan_results.result.robot_result in ( RobotScanResultEnum.VULNERABLE_WEAK_ORACLE, RobotScanResultEnum.VULNERABLE_STRONG_ORACLE): message = _("Server is vulnerable to ROBOT attack") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.HEARTBLEED: if scan_results.result.is_vulnerable_to_heartbleed: message = _("Server is vulnerable to Heartbleed attack") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_COMPRESSION: if scan_results.result.supports_compression: message = _( "Server is vulnerable to CRIME attack (compression is supported)" ) log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_FALLBACK_SCSV: if not scan_results.result.supports_fallback_scsv: message = _( "Server is vulnerable to downgrade attacks (support for TLS_FALLBACK_SCSV is missing)" ) log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.TLS_1_3_EARLY_DATA: # https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/ if scan_results.result.supports_early_data: message = _( "TLS 1.3 Early Data (0RTT) is vulnerable to replay attacks" ) log_orange(message) results.append((MEDIUM_LEVEL, message)) elif scan_command == ScanCommand.OPENSSL_CCS_INJECTION: if scan_results.result.is_vulnerable_to_ccs_injection: message = _( "Server is vulnerable to OpenSSL CCS (CVE-2014-0224)") log_red(message) results.append((CRITICAL_LEVEL, message)) elif scan_command == ScanCommand.SESSION_RENEGOTIATION: if scan_results.result.is_vulnerable_to_client_renegotiation_dos: message = _( "Server honors client-initiated renegotiations (vulnerable to DoS attacks)" ) log_red(message) results.append((HIGH_LEVEL, message)) if not scan_results.result.supports_secure_renegotiation: message = _("Server doesn't support secure renegotiations") log_orange(message) results.append((MEDIUM_LEVEL, message)) elif scan_command == ScanCommand.HTTP_HEADERS: if scan_results.result.strict_transport_security_header is None: message = _("Strict Transport Security (HSTS) is not set") log_red(message) results.append((HIGH_LEVEL, message)) elif scan_command in good_protocols: for level, message in process_cipher_suites( scan_results.result, good_protocols[scan_command]): results.append((level, message)) if deprecated_protocols: message = _("The following protocols are deprecated and/or insecure and should be deactivated:") + \ " " + ", ".join(deprecated_protocols) log_red(message) results.append((CRITICAL_LEVEL, message)) return results
def main(server_software_running_on_localhost: WebServerSoftwareEnum) -> None: # Queue all scan commands against a server running on localhost print("Starting scan.") date_scans_started = datetime.utcnow() scanner = Scanner() scanner.queue_scans([ ServerScanRequest( server_location=ServerNetworkLocation("localhost", 443)) ]) # Retrieve the result for server_scan_result in scanner.get_results(): # First validate the connectivity testing assert server_scan_result.connectivity_status == ServerConnectivityStatusEnum.COMPLETED assert server_scan_result.connectivity_result if server_software_running_on_localhost == WebServerSoftwareEnum.APACHE2: # Apache2 is configured to require a client cert, and returns an error at the TLS layer if it is missing if server_scan_result.connectivity_result.client_auth_requirement != ClientAuthRequirementEnum.REQUIRED: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Apache2:" f" {server_scan_result.connectivity_result.client_auth_requirement}." ) elif server_software_running_on_localhost == WebServerSoftwareEnum.NGINX: # Nginx is configured to require a client cert but implements this by returning an error at the HTTP layer, # if the client cert is missing. This gets translated in SSLyze as "optionally" requiring a client cert if server_scan_result.connectivity_result.client_auth_requirement != ClientAuthRequirementEnum.OPTIONAL: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Nginx:" f" {server_scan_result.connectivity_result.client_auth_requirement}." ) elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # IIS is not configured to require a client cert for now because I don't know how to enable this if server_scan_result.connectivity_result.client_auth_requirement != ClientAuthRequirementEnum.DISABLED: raise RuntimeError( f"SSLyze detected that client authentication was enabled by IIS:" f" {server_scan_result.connectivity_result.client_auth_requirement}." ) else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") successful_cmds = set() triggered_unexpected_error = False for scan_command in ScanCommandsRepository.get_all_scan_commands(): scan_cmd_attempt = getattr(server_scan_result.scan_result, scan_command.value) if scan_cmd_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: successful_cmds.add(scan_command) elif scan_cmd_attempt.status == ScanCommandAttemptStatusEnum.ERROR: # Crash if any scan commands triggered an error that's not due to client authentication being required if scan_cmd_attempt.error_reason != ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: triggered_unexpected_error = True print( f"\nError when running {scan_command}: {scan_cmd_attempt.error_reason}." ) if scan_cmd_attempt.error_trace: exc_trace = "" for line in scan_cmd_attempt.error_trace.format( chain=False): exc_trace += f" {line}" print(exc_trace) print(f"Finished scan with {len(successful_cmds)} results.") if triggered_unexpected_error: raise RuntimeError("The scan triggered unexpected errors") else: # The CLIENT_CERTIFICATE_NEEDED errors are expected, because of how Apache2 is configured print("OK: Triggered CLIENT_CERTIFICATE_NEEDED errors only.") # Crash if SSLyze didn't complete the scan commands that are supposed to work even when we don't provide a # client certificate if server_software_running_on_localhost == WebServerSoftwareEnum.APACHE2: expected_scan_cmds_to_succeed = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, } elif server_software_running_on_localhost == WebServerSoftwareEnum.NGINX: # With nginx, when configured to require client authentication, more scan commands work because unlike # Apache2, it does complete a full TLS handshake even when a client cert was not provided. It then returns # an error page at the HTTP layer. expected_scan_cmds_to_succeed = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, ScanCommand.SSL_3_0_CIPHER_SUITES, ScanCommand.SSL_2_0_CIPHER_SUITES, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.ELLIPTIC_CURVES, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_COMPRESSION, ScanCommand.SESSION_RESUMPTION, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.HTTP_HEADERS, ScanCommand.SESSION_RENEGOTIATION, } elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # With IIS, client authentication is not enabled so all scan commands should succeed expected_scan_cmds_to_succeed = ScanCommandsRepository.get_all_scan_commands( ) # type: ignore else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") missing_scan_cmds = expected_scan_cmds_to_succeed.difference( successful_cmds) if missing_scan_cmds: raise RuntimeError( f"SSLyze did not complete all the expected scan commands: {missing_scan_cmds}" ) print("OK: Completed all the expected scan commands.") # Ensure the right TLS versions were detected by SSLyze as enabled # https://github.com/nabla-c0d3/sslyze/issues/472 if server_software_running_on_localhost in [ WebServerSoftwareEnum.APACHE2, WebServerSoftwareEnum.NGINX ]: # Apache and nginx are configured to only enable TLS 1.2 and TLS 1.3 expected_enabled_tls_scan_commands = { ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, } elif server_software_running_on_localhost == WebServerSoftwareEnum.IIS: # TLS 1.3 is not supported by IIS expected_enabled_tls_scan_commands = { ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES, } else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") for ciphers_scan_cmd in expected_enabled_tls_scan_commands: scan_cmd_attempt = getattr(server_scan_result.scan_result, ciphers_scan_cmd, None) scan_cmd_result = scan_cmd_attempt.result if not scan_cmd_result.accepted_cipher_suites: raise RuntimeError( f"SSLyze did not detect {scan_cmd_result.tls_version_used.name} to be enabled on the server." ) else: print( f"OK: Scan command {ciphers_scan_cmd} detected cipher suites." ) # Ensure a JSON output can be generated from the results final_json_output = SslyzeOutputAsJson( server_scan_results=[ ServerScanResultAsJson.from_orm(server_scan_result) ], date_scans_started=date_scans_started, date_scans_completed=datetime.utcnow(), ) final_json_output.json(sort_keys=True, indent=4, ensure_ascii=True) print("OK: Was able to generate JSON output.")