def test_with_extra_arguments(self, mock_scan_commands): # Given a server to scan with a scan command server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ScanCommandForTests.MOCK_COMMAND_1}, # And the command takes an extra argument scan_commands_extra_arguments={ ScanCommandForTests.MOCK_COMMAND_1: MockPlugin1ExtraArguments(extra_field="test") }, ) # When running the scan scanner = Scanner() scanner.start_scans([server_scan]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And the extra argument was taken into account assert all_results[ 0].scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments
def test(self, mock_scan_commands): # Given a server to scan server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2 }, ) # When running the scan scanner = Scanner() scanner.start_scans([server_scan]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And the right result is returned result = all_results[0] assert result.server_info == server_scan.server_info assert result.scan_commands == server_scan.scan_commands assert result.scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments assert len(result.scan_commands_results) == 2 assert type(result.scan_commands_results[ ScanCommandForTests.MOCK_COMMAND_1]) == MockPlugin1ScanResult assert type(result.scan_commands_results[ ScanCommandForTests.MOCK_COMMAND_2]) == MockPlugin2ScanResult # And the Scanner instance is all done and cleaned up assert not scanner._are_server_scans_ongoing
def test_error_client_certificate_needed(self): # Given a server that requires client authentication with LegacyOpenSslServer( client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: # And sslyze does NOT provide a client certificate server_location = ServerNetworkLocationViaDirectConnection( hostname=server.hostname, ip_address=server.ip_address, port=server.port) server_info = ServerConnectivityTester().perform(server_location) server_scan = ServerScanRequest( server_info=server_info, scan_commands={ # And a scan command that cannot be completed without a client certificate ScanCommand.HTTP_HEADERS, }, ) # When running the scan scanner = Scanner() scanner.start_scans([server_scan]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And the error was properly returned error = all_results[0].scan_commands_errors[ ScanCommand.HTTP_HEADERS] assert error.reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED
def test_error_bug_in_sslyze_when_processing_job_results( self, mock_scan_commands): # Given a server to scan with some scan commands server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2 }, ) # And the first scan command will trigger an error when processing the completed scan jobs with mock.patch.object(MockPlugin1Implementation, "_scan_job_work_function", side_effect=RuntimeError): # When running the scan scanner = Scanner() scanner.start_scans([server_scan]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And the exception was properly caught and returned result = all_results[0] assert len(result.scan_commands_errors) == 1 error = result.scan_commands_errors[ ScanCommandForTests.MOCK_COMMAND_1] assert ScanCommandErrorReasonEnum.BUG_IN_SSLYZE == error.reason assert error.exception_trace
def main() -> None: start_time = time() # Create the command line parser and the list of available options sslyze_parser = CommandLineParser(__version__) try: # Parse the supplied command line parsed_command_line = sslyze_parser.parse_command_line() except CommandLineParsingError as e: print(e.get_error_msg()) return output_hub = OutputHub() output_hub.command_line_parsed(parsed_command_line) # Figure out which servers are reachable connectivity_tester = ServerConnectivityTester() all_server_scan_requests = [] with ThreadPoolExecutor(max_workers=10) as thread_pool: futures = [ thread_pool.submit(connectivity_tester.perform, server_location, network_config) for server_location, network_config in parsed_command_line.servers_to_scans ] for completed_future in as_completed(futures): try: server_connectivity_info = completed_future.result() output_hub.server_connectivity_test_succeeded(server_connectivity_info) # Server is only; add it to the list of servers to scan scan_request = ServerScanRequest( server_info=server_connectivity_info, 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) except ConnectionToServerFailed as e: output_hub.server_connectivity_test_failed(e) # For the servers that are reachable, start the scans output_hub.scans_started() if all_server_scan_requests: 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, ) sslyze_scanner.start_scans(all_server_scan_requests) # Process the results as they come for scan_result in sslyze_scanner.get_results(): output_hub.server_scan_completed(scan_result) # All done exec_time = time() - start_time output_hub.scans_completed(exec_time)
def test_enforces_per_server_concurrent_connections_limit( self, mock_scan_commands): # Given a server to scan with a scan command that requires multiple connections/jobs to the server server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ScanCommandForTests.MOCK_COMMAND_1}, ) # And a scanner configured to only perform one concurrent connection per server scan scanner = Scanner(per_server_concurrent_connections_limit=1) # And the scan command will notify us when more than one connection is being performed concurrently # Test internals: setup plumbing to detect when more than one thread are running at the same time # We use a Barrier that waits for 2 concurrent threads, and puts True in a queue if that ever happens queue = Queue() def flag_concurrent_threads_running(): # Only called when two threads are running at the same time queue.put(True) barrier = threading.Barrier(parties=2, action=flag_concurrent_threads_running, timeout=1) def scan_job_work_function(arg1: str, arg2: int): barrier.wait() with mock.patch.object(MockPlugin1Implementation, "_scan_job_work_function", scan_job_work_function): # When running the scan scanner.start_scans([server_scan]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And there never was more than one thread (=1 job/connection) running at the same time assert queue.empty()
def test_error_server_connectivity_issue_handshake_timeout( self, mock_scan_commands): # Given a server to scan with some commands server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2 }, ) # And the first scan command will trigger a handshake timeout with the server with mock.patch.object( MockPlugin1Implementation, "_scan_job_work_function", side_effect=TlsHandshakeTimedOut( server_location=server_scan.server_info.server_location, network_configuration=server_scan.server_info. network_configuration, error_message="error", ), ): # When running the scan scanner = Scanner() scanner.start_scans([server_scan]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) assert len(all_results) == 1 # And the error was properly caught and returned result = all_results[0] assert len(result.scan_commands_errors) == 1 error = result.scan_commands_errors[ ScanCommandForTests.MOCK_COMMAND_1] assert ScanCommandErrorReasonEnum.CONNECTIVITY_ISSUE == error.reason assert error.exception_trace
def run(self): try: server_info = self.get_server_info() highest_tls_supported = str( server_info.tls_probing_result.highest_tls_version_supported ).split(".")[1] tls_supported = self.get_supported_tls(highest_tls_supported) except ConnectionToServerFailed as e: logging.error(f"Failed to connect to {self.domain}: {e}") return {} except ServerHostnameCouldNotBeResolved as e: logging.error(f"{self.domain} could not be resolved: {e}") return {} except gaierror as e: logging.error( f"Could not retrieve address info for {self.domain} {e}") return {} scanner = Scanner() designated_scans = set() # Scan for common vulnerabilities, certificate info, elliptic curves designated_scans.add(ScanCommand.OPENSSL_CCS_INJECTION) designated_scans.add(ScanCommand.HEARTBLEED) designated_scans.add(ScanCommand.CERTIFICATE_INFO) designated_scans.add(ScanCommand.ELLIPTIC_CURVES) # Test supported SSL/TLS if "SSL_2_0" in tls_supported: designated_scans.add(ScanCommand.SSL_2_0_CIPHER_SUITES) elif "SSL_3_0" in tls_supported: designated_scans.add(ScanCommand.SSL_3_0_CIPHER_SUITES) elif "TLS_1_0" in tls_supported: designated_scans.add(ScanCommand.TLS_1_0_CIPHER_SUITES) elif "TLS_1_1" in tls_supported: designated_scans.add(ScanCommand.TLS_1_1_CIPHER_SUITES) elif "TLS_1_2" in tls_supported: designated_scans.add(ScanCommand.TLS_1_2_CIPHER_SUITES) elif "TLS_1_3" in tls_supported: designated_scans.add(ScanCommand.TLS_1_3_CIPHER_SUITES) scan_request = ServerScanRequest(server_info=server_info, scan_commands=designated_scans) scanner.start_scans([scan_request]) # Wait for asynchronous scans to complete # get_results() returns a generator with a single "ServerScanResult". We only want that object scan_results = [x for x in scanner.get_results()][0] logging.info("Scan results retrieved from generator") res = { "TLS": { "supported": tls_supported, "accepted_cipher_list": [], "rejected_cipher_list": [], } } # Parse scan results for required info for name, result in scan_results.scan_commands_results.items(): # If CipherSuitesScanResults if name.endswith("suites"): logging.info("Parsing Cipher Suite Scan results...") for c in result.accepted_cipher_suites: res["TLS"]["accepted_cipher_list"].append( c.cipher_suite.name) for c in result.rejected_cipher_suites: res["TLS"]["rejected_cipher_list"].append( c.cipher_suite.name) elif name == "openssl_ccs_injection": logging.info( "Parsing OpenSSL CCS Injection Vulnerability Scan results..." ) res["is_vulnerable_to_ccs_injection"] = result.is_vulnerable_to_ccs_injection elif name == "heartbleed": logging.info( "Parsing Heartbleed Vulnerability Scan results...") res["is_vulnerable_to_heartbleed"] = result.is_vulnerable_to_heartbleed elif name == "certificate_info": logging.info("Parsing Certificate Info Scan results...") try: res["signature_algorithm"] = ( result.certificate_deployments[0]. verified_certificate_chain[0].signature_hash_algorithm. __class__.__name__) except TypeError: res["signature_algorithm"] = None else: logging.info("Parsing Elliptic Curve Scan results...") res["supports_ecdh_key_exchange"] = result.supports_ecdh_key_exchange res["supported_curves"] = [] if result.supported_curves is not None: for curve in result.supported_curves: # sslyze returns ANSI curve names occaisionally # In at least these two cases we can simply convert to # using the equivalent SECG name, so that this aligns # with CCCS guidance: # https://datatracker.ietf.org/doc/html/rfc4492#appendix-A if curve.name == "prime192v1": res["supported_curves"].append("secp192r1") elif curve.name == "prime256v1": res["supported_curves"].append("secp256r1") else: res["supported_curves"].append(curve.name) return res