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 queuing the scan scanner = Scanner() scanner.queue_scan(server_scan) # It succeeds for result in scanner.get_results(): # And the error was properly caught and returned 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 test_exception_when_processing_jobs(self): # Given a server to scan server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ ScanCommandForTests.MOCK_COMMAND_1, # And one of the scan commands will trigger an exception when processing the completed scan jobs ScanCommandForTests. MOCK_COMMAND_EXCEPTION_WHEN_PROCESSING_JOBS, }, ) # When queuing the scan scanner = Scanner() scanner.queue_scan(server_scan) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) 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) == 1 # And the exception was properly caught and returned assert len(result.scan_commands_errors) == 1 error = result.scan_commands_errors[ ScanCommandForTests. MOCK_COMMAND_EXCEPTION_WHEN_PROCESSING_JOBS] assert ScanCommandErrorReasonEnum.BUG_IN_SSLYZE == error.reason assert error.exception_trace assert len(all_results) == 1
def test_with_extra_arguments(self): # Given a server to scan server_scan = ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ScanCommandForTests.MOCK_COMMAND_1}, # With an extra argument for one command scan_commands_extra_arguments={ ScanCommandForTests.MOCK_COMMAND_1: MockPlugin1ExtraArguments(extra_field="test") }, ) # When queuing the scan scanner = Scanner() scanner.queue_scan(server_scan) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) # And the extra argument was taken into account assert result.scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments assert len(all_results) == 1
def test(self, mock_scan_commands): # Given a lot of servers to scan total_server_scans_count = 100 server_scans = [ ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2}, ) for _ in range(total_server_scans_count) ] # And a scanner with specifically chosen network settings per_server_concurrent_connections_limit = 4 concurrent_server_scans_limit = 20 scanner = Scanner(per_server_concurrent_connections_limit, concurrent_server_scans_limit) # When queuing the scans, it succeeds for scan in server_scans: scanner.queue_scan(scan) # And the right number of scans was performed assert total_server_scans_count == len(scanner._queued_server_scans) # And the chosen network settings were used assert concurrent_server_scans_limit == len(scanner._thread_pools) for pool in scanner._thread_pools: assert per_server_concurrent_connections_limit == pool._max_workers # And the server scans were evenly distributed among the thread pools to maximize performance expected_server_scans_per_pool = int(total_server_scans_count / concurrent_server_scans_limit) thread_pools_used = [server_scan.queued_on_thread_pool_at_index for server_scan in scanner._queued_server_scans] server_scans_per_pool_count = Counter(thread_pools_used) for pool_count in server_scans_per_pool_count.values(): assert expected_server_scans_per_pool == pool_count
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_emergency_shutdown(self, mock_scan_commands): # Given a lot of servers to scan total_server_scans_count = 100 server_scans = [ ServerScanRequest( server_info=ServerConnectivityInfoFactory.create(), scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2}, ) for _ in range(total_server_scans_count) ] # And the scans get queued scanner = Scanner() for scan in server_scans: scanner.queue_scan(scan) # When trying to quickly shutdown the scanner, it succeeds scanner.emergency_shutdown() # And all the queued jobs were done or cancelled all_queued_futures = [] for server_scan in scanner._queued_server_scans: all_queued_futures.extend(server_scan.all_queued_scan_jobs) for completed_future in as_completed(all_queued_futures): assert completed_future.done()
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(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 queuing the scan scanner = Scanner() scanner.queue_scan(server_scan) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) # And the right result is returned 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 assert len(all_results) == 1
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 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 queuing the scan scanner = Scanner() scanner.queue_scan(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 main() -> None: global global_scanner # For py2exe builds freeze_support() # Handle SIGINT to terminate processes signal.signal(signal.SIGINT, sigint_handler) start_time = time() # Create the command line parser and the list of available options sslyze_parser = CommandLineParser(__version__) try: 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) global_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, ) # Figure out which hosts are up and fill the task queue with work to do connectivity_tester = ServerConnectivityTester() 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) # Send scan commands for this server to the scanner 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, ) global_scanner.queue_scan(scan_request) except ConnectionToServerFailed as e: output_hub.server_connectivity_test_failed(e) output_hub.scans_started() # Process the results as they come for scan_result in global_scanner.get_results(): output_hub.server_scan_completed(scan_result) # All done exec_time = time() - start_time output_hub.scans_completed(exec_time)
def main() -> None: # First validate that we can connect to the servers we want to scan servers_to_scan = [] for hostname in ["cloudflare.com", "google.com"]: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443) try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: print(f"Error connecting to {server_location.hostname}:{server_location.port}: {e.error_message}") return scanner = Scanner() # Then queue some scan commands for each server for server_info in servers_to_scan: server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ ScanCommandEnum.TLS_1_0_CIPHER_SUITES, ScanCommandEnum.TLS_1_1_CIPHER_SUITES, ScanCommandEnum.TLS_1_2_CIPHER_SUITES, ScanCommandEnum.CERTIFICATE_INFO, ScanCommandEnum.TLS_COMPRESSION, }, ) scanner.queue_scan(server_scan_req) # Then retrieve the result of the scan commands for each server for server_scan_result in scanner.get_results(): print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:") # Scan commands that were run with no errors for scan_command, result in server_scan_result.scan_commands_results.items(): if scan_command in [ ScanCommandEnum.TLS_1_0_CIPHER_SUITES, ScanCommandEnum.TLS_1_1_CIPHER_SUITES, ScanCommandEnum.TLS_1_2_CIPHER_SUITES, ]: typed_result = cast(CipherSuitesScanResult, result) print(f"\nAccepted cipher suites for {scan_command.name}:") for accepted_cipher_suite in typed_result.accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") elif scan_command == ScanCommandEnum.CERTIFICATE_INFO: typed_result = cast(CertificateInfoScanResult, result) print("\nCertificate info:") for cert_deployment in typed_result.certificate_deployments: print(f"Leaf certificate: \n{cert_deployment.verified_certificate_chain_as_pem[0]}") elif scan_command == ScanCommandEnum.TLS_COMPRESSION: typed_result = cast(CompressionScanResult, result) print(f"\nCompression / CRIME: {typed_result.supports_compression}") # Scan commands that were run with errors for scan_command, error in server_scan_result.scan_commands_errors.items(): print(f"\nError when running {scan_command}:\n{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 __init__( self, domain: str, target_profile: str, ca_file: Optional[str] = None, cert_expire_warning: int = 15, ) -> None: """ :param domain: :param target_profile: One of [old|intermediate|modern] :param ca_file: Path to a trusted custom root certificates in PEM format. :param cert_expire_warning: A warning is issued if the certificate expires in less days than specified. """ self.scan_commands_extra_args = {} if ca_file: ca_path = Path(ca_file) self.scan_commands_extra_args[ ScanCommand.CERTIFICATE_INFO] = CertificateInfoExtraArguments( ca_path) self.cert_expire_warning = cert_expire_warning if TLSProfiler.PROFILES is None: TLSProfiler.PROFILES = requests.get(self.PROFILES_URL).json() log.info( f"Loaded version {TLSProfiler.PROFILES['version']} of the Mozilla TLS configuration recommendations." ) self.target_profile = TLSProfiler.PROFILES["configurations"][ target_profile] self.target_profile["tls_curves"] = self._get_equivalent_curves( self.target_profile["tls_curves"]) self.target_profile[ "certificate_curves_preprocessed"] = self._get_equivalent_curves( self.target_profile["certificate_curves"]) server_location = ( ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( domain, 443)) self.scanner = Scanner() try: log.info( f"Testing connectivity with {server_location.hostname}:{server_location.port}..." ) self.server_info = ServerConnectivityTester().perform( server_location) self.server_error = None except ConnectionToServerFailed as e: # Could not establish an SSL connection to the server log.warning( f"Could not connect to {e.server_location.hostname}: {e.error_message}" ) self.server_error = e.error_message self.server_info = None
def test_duplicate_server(self, mock_scan_commands): # Given a server to scan server_info = ServerConnectivityInfoFactory.create() # When trying to queue two scans for this server server_scan1 = ServerScanRequest(server_info=server_info, scan_commands={ScanCommandForTests.MOCK_COMMAND_1}) server_scan2 = ServerScanRequest(server_info=server_info, scan_commands={ScanCommandForTests.MOCK_COMMAND_2}) scanner = Scanner() scanner.queue_scan(server_scan1) # It fails with pytest.raises(ValueError): scanner.queue_scan(server_scan2)
def __init__( self, domain: str, ca_file: Optional[str] = None, cert_expire_warning: int = 15, ) -> None: """ :param domain: the domain name of the target server :param ca_file: Path to a trusted custom root certificates in PEM format. :param cert_expire_warning: A warning is issued if the certificate expires in less days than specified. """ self._comparator = None self._validation_errors = None self._vulnerability_errors = None self._server_scan_result = None self._scan_commands_extra_args = {} if ca_file: ca_path = Path(ca_file) self._scan_commands_extra_args[ ScanCommand.CERTIFICATE_INFO] = CertificateInfoExtraArguments( ca_path) self._cert_expire_warning = cert_expire_warning server_location = ( ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( domain, 443)) self._scanner = Scanner() try: log.info( f"Testing connectivity with {server_location.hostname}:{server_location.port}..." ) self._server_info = ServerConnectivityTester().perform( server_location) self.server_error = None except ConnectionToServerFailed as e: # Could not establish an SSL connection to the server log.warning( f"Could not connect to {e.server_location.hostname}: {e.error_message}" ) self.server_error = e.error_message self._server_info = None
def test_error_bug_in_sslyze_when_scheduling_jobs(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 generating scan jobs with mock.patch.object(MockPlugin1Implementation, "scan_jobs_for_scan_command", side_effect=RuntimeError): # When queuing the scan scanner = Scanner() scanner.queue_scan(server_scan) # It succeeds for result in scanner.get_results(): # And the exception was properly caught and returned 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 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 https_check(endpoint): """ Uses sslyze to figure out the reason the endpoint wouldn't verify. """ # remove the https:// from prefix for sslyze try: hostname = endpoint.url[8:] server_location = ( ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname, 443)) server_tester = ServerConnectivityTester() server_info = server_tester.perform(server_location) endpoint.live = True ip = server_location.ip_address if endpoint.ip is None: endpoint.ip = ip else: if endpoint.ip != ip: logging.debug( "{}: Endpoint IP is already {}, but requests IP is {}.". format(endpoint.url, endpoint.ip, ip)) if server_info.tls_probing_result.client_auth_requirement.name == "REQUIRED": endpoint.https_client_auth_required = True logging.debug("{}: Client Authentication REQUIRED".format( endpoint.url)) except ConnectionToServerFailed as err: endpoint.live = False endpoint.https_valid = False logging.debug("{}: Error in sslyze server connectivity check".format( endpoint.url)) return except Exception as err: endpoint.unknown_error = True logging.debug( "{}: Unknown exception in sslyze server connectivity check.". format(endpoint.url)) return try: cert_plugin_result = None command = ScanCommand.CERTIFICATE_INFO scanner = Scanner() scan_request = ServerScanRequest(server_info=server_info, scan_commands=[command]) scanner.queue_scan(scan_request) # Retrieve results from generator object scan_result = [x for x in scanner.get_results()][0] cert_plugin_result = scan_result.scan_commands_results.get( "certificate_info", None) except Exception as err: try: if "timed out" in str(err): logging.debug( "{}: Retrying sslyze scanner certificate plugin.".format( endpoint.url)) scanner.queue_scan(scan_request) # Retrieve results from generator object scan_result = [x for x in scanner.get_results()][0] cert_plugin_result = scan_result.scan_commands_results.get( "certificate_info", None) else: logging.debug( "{}: Unknown exception in sslyze scanner certificate plugin." .format(endpoint.url)) endpoint.unknown_error = True # We could make this False, but there was an error so # we don't know endpoint.https_valid = None return except Exception: logging.debug( "{}: Unknown exception in sslyze scanner certificate plugin.". format(endpoint.url)) endpoint.unknown_error = True # We could make this False, but there was an error so we # don't know endpoint.https_valid = None return try: public_trust = True custom_trust = True public_not_trusted_string = "" if cert_plugin_result is not None: validation_results = cert_plugin_result.certificate_deployments[ 0].path_validation_results else: validation_results = [] for result in validation_results: if result.was_validation_successful: # We're assuming that it is trusted to start with pass else: if "Custom" in result.trust_store.name: custom_trust = False else: public_trust = False if len(public_not_trusted_string) > 0: public_not_trusted_string += ", " public_not_trusted_string += result.trust_store.name if public_trust: logging.debug( "{}: Publicly trusted by common trust stores.".format( endpoint.url)) else: logging.debug( "{}: Not publicly trusted - not trusted by {}.".format( endpoint.url, public_not_trusted_string)) custom_trust = None endpoint.https_public_trusted = public_trust endpoint.https_custom_trusted = custom_trust except Exception as err: # Ignore exception logging.debug("{}: Unknown exception examining trust.".format( endpoint.url)) # Default endpoint assessments to False until proven True. endpoint.https_expired_cert = False endpoint.https_self_signed_cert = False endpoint.https_bad_chain = False endpoint.https_bad_hostname = False endpoint.https_cert_revoked = False cert_chain = cert_plugin_result.certificate_deployments[ 0].received_certificate_chain # Check for missing SAN (Leaf certificate) leaf_cert = cert_chain[0] # Extract Subject Alternative Names san_list = extract_dns_subject_alternative_names(leaf_cert) # If an empty list was return, SAN(s) are missing. Bad hostname if isinstance(san_list, list) and len(san_list) == 0: endpoint.https_bad_hostname = True # If leaf certificate subject does NOT match hostname, bad hostname if not cert_plugin_result.certificate_deployments[ 0].leaf_certificate_subject_matches_hostname: endpoint.https_bad_hostname = True try: endpoint.https_cert_revoked = query_crlite( leaf_cert.public_bytes(Encoding.PEM)) except ValueError as e: logging.debug( f"Error while checking revocation status for {endpoint.url}: {str(e)}" ) endpoint.https_cert_revoked = None # Check for leaf certificate expiration/self-signature. if leaf_cert.not_valid_after < datetime.datetime.now(): endpoint.https_expired_cert = True # Check to see if the cert is self-signed if leaf_cert.issuer is leaf_cert.subject: endpoint.https_self_signed_cert = True # Check certificate chain for cert in cert_chain[1:]: # Check for certificate expiration if cert.not_valid_after < datetime.datetime.now(): endpoint.https_bad_chain = True # Check to see if the cert is self-signed if cert.issuer is (cert.subject or None): endpoint.https_bad_chain = True try: endpoint.https_cert_chain_len = len( cert_plugin_result.certificate_deployments[0]. received_certificate_chain) if endpoint.https_self_signed_cert is False and (len( cert_plugin_result.certificate_deployments[0]. received_certificate_chain) < 2): # *** TODO check that it is not a bad hostname and that the root cert is trusted before suggesting that it is an intermediate cert issue. endpoint.https_missing_intermediate_cert = True if cert_plugin_result.verified_certificate_chain is None: logging.debug( "{}: Untrusted certificate chain, probably due to missing intermediate certificate." .format(endpoint.url)) logging.debug( "{}: Only {} certificates in certificate chain received.". format( endpoint.url, cert_plugin_result.received_certificate_chain.__len__( ), )) else: endpoint.https_missing_intermediate_cert = False except Exception: logging.debug("Error while determining length of certificate chain") # If anything is wrong then https is not valid if (endpoint.https_expired_cert or endpoint.https_self_signed_cert or endpoint.https_bad_chain or endpoint.https_bad_hostname): endpoint.https_valid = False
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