def basic_example() -> None: # Define the server that you want to scan server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.google.com", 443) # Do connectivity testing to ensure SSLyze is able to connect try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: # Could not connect to the server; abort print(f"Error connecting to {server_location}: {e.error_message}") return # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest( server_info=server_info, scan_commands={ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES}, ) scanner.start_scans([server_scan_req]) # Then retrieve the results for server_scan_result in scanner.get_results(): print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:") # SSL 2.0 results ssl2_result = server_scan_result.scan_commands_results[ScanCommand.SSL_2_0_CIPHER_SUITES] 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}") # Certificate info results certinfo_result = server_scan_result.scan_commands_results[ScanCommand.CERTIFICATE_INFO] print("\nCertificate info:") for cert_deployment in certinfo_result.certificate_deployments: print(f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}")
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={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES }, ) 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 try: ssl2_result = server_scan_result.scan_commands_results[ ScanCommand.SSL_2_0_CIPHER_SUITES] print(f"\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}") except KeyError: pass try: certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] print("\nCertificate info:") for cert_deployment in certinfo_result.certificate_deployments: print( f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}" ) except KeyError: pass # 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 create( server_location: Optional[ServerNetworkLocation] = None, scan_commands: Optional[Set[ScanCommand]] = None, scan_commands_extra_arguments: Optional[ ScanCommandsExtraArguments] = None, ) -> ServerScanRequest: final_server_location: Optional[ServerNetworkLocation] if server_location is None: final_server_location = ServerNetworkLocationViaDirectConnectionFactory.create( ) else: final_server_location = server_location if scan_commands is None: final_scan_commands = { ScanCommand.CERTIFICATE_INFO, ScanCommand.ROBOT } else: final_scan_commands = scan_commands if scan_commands_extra_arguments is None: final_extra_args = ScanCommandsExtraArguments() else: final_extra_args = scan_commands_extra_arguments return ServerScanRequest( server_location=final_server_location, scan_commands=final_scan_commands, scan_commands_extra_arguments=final_extra_args, )
def test_error_client_certificate_needed(self): # Given a server that requires client authentication with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server: # And a scan request for it that does NOT provide a client certificate scan_request = ServerScanRequest( server_location=ServerNetworkLocation( hostname=server.hostname, ip_address=server.ip_address, port=server.port ), scan_commands={ # And the request has a scan command that cannot be completed without a client certificate ScanCommand.HTTP_HEADERS, }, ) # When running the scan scanner = Scanner() scanner.queue_scans([scan_request]) # It succeeds all_results = [] for result in scanner.get_results(): all_results.append(result) # And the right result was returned assert len(all_results) == 1 # And the fact that a client certificate is needed was properly returned http_headers_result = all_results[0].scan_result.http_headers assert http_headers_result.status == ScanCommandAttemptStatusEnum.ERROR assert http_headers_result.error_reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED assert http_headers_result.error_trace assert http_headers_result.result is None
def initiateScan(self, commands): self.scanner = Scanner() serverScanReq = ServerScanRequest( server_info=self.serverInfo, scan_commands=commands, ) self.scanner.queue_scan(serverScanReq) return self.scanner.get_results()
def test_default_values(self): # Given just a server location server_location = ServerNetworkLocation(hostname="www.google.com", port=443, ip_address="1.1.1.1") # When creating a scan request with the minimum set of arguments, it succeeds scan_request = ServerScanRequest(server_location=server_location) # And default values were generated assert scan_request.uuid assert scan_request.network_configuration assert len(scan_request.scan_commands) > 5
def addScanRequests(scanner, servers_to_scan, commands): """ Queue scan requests for each open server connections :param scanner: Scanner object which holds connections open for each IP :param servers_to_scan: list of open connections :param commands: set of string scan commands (like 'certificate_info' and 'tls_1_0_cipher_suites' :return: None """ print('Queueing TLS scans (this might take a little while...)') for server_info in servers_to_scan: server_scan_req = ServerScanRequest(server_info=server_info, scan_commands=commands) scanner.queue_scan(server_scan_req)
def test_extra_arguments_but_no_corresponding_scan_command(self): # Given a server location server_location = ServerNetworkLocation(hostname="www.google.com", port=443, ip_address="1.1.1.1") # When trying to queue a scan for a server with pytest.raises(ValueError): ServerScanRequest( server_location=server_location, # With an extra argument for one command scan_commands_extra_arguments=ScanCommandsExtraArguments( certificate_info=CertificateInfoExtraArgument( custom_ca_file=Path(__file__))), # But that specific scan command was not queued scan_commands={ScanCommand.ROBOT}, )
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 scan_runner(seq,host): hostname=host.decode("utf-8") servers_to_scan = [] server_location = None try: if r.hget(seq,"ipaddr"): server_location = ServerNetworkLocationViaDirectConnection(hostname, 443, r.hget(seq,"ipaddr").decode("utf-8")) else: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443) r.hset(seq,"ipaddr",server_location.ip_address) #Initialize with hostname, port int and ip address str #print(server_location) except Exception as e: return try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: 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={ScanCommand.TLS_1_3_EARLY_DATA}, ) 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(): try: if server_scan_result.scan_commands_results[ScanCommand.TLS_1_3_EARLY_DATA].supports_early_data: r.hset(seq,"early","TRUE") except KeyError: return
def search_subject_alt_name(self, target): print("Searching for Subject Alt Names") try: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( target, 443) # Do connectivity testing to ensure SSLyze is able to connect try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: # Could not connect to the server; abort print(f"Error connecting to {server_location}: {e.error_message}") return # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest(server_info=server_info, scan_commands={ ScanCommand.CERTIFICATE_INFO}, ) scanner.queue_scan(server_scan_req) # Then retrieve the results for server_scan_result in scanner.get_results(): # Certificate info results certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] # Direct object reference is pretty bad, but then again so is the crypto.x509 object implementation, so... cert_deployment = certinfo_result.certificate_deployments[0] chain = cert_deployment.received_certificate_chain[0] ext = chain.extensions.get_extension_for_oid( ExtensionOID.SUBJECT_ALTERNATIVE_NAME) for entry in ext.value.get_values_for_type(x509.DNSName): if entry.strip() not in self.domains: self.domains.append(entry.strip()) except Exception as e: self.handle_exception(e)
def main() -> None: # Ensure the server is accessible on localhost server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("localhost", 443) server_info = ServerConnectivityTester().perform(server_location) if server_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.REQUIRED: raise RuntimeError( f"SSLyze did not detect that client authentication was required by the server:" f" {server_info.tls_probing_result.client_auth_requirement}." ) # Queue all scan commands print("Starting scan.") scanner = Scanner() server_scan_req = ServerScanRequest( server_info=server_info, scan_commands=ScanCommandsRepository.get_all_scan_commands(), ) scanner.queue_scan(server_scan_req) # Retrieve the result for server_scan_result in scanner.get_results(): successful_cmds_count = len(server_scan_result.scan_commands_results) errored_cmds_count = len(server_scan_result.scan_commands_errors) print(f"Finished scan with {successful_cmds_count} results and {errored_cmds_count} errors.") # Crash if any scan commands triggered an error that's not due to client authentication being required triggered_unexpected_error = False for scan_command, error in server_scan_result.scan_commands_errors.items(): if error.reason != ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: triggered_unexpected_error = True print(f"\nError when running {scan_command}: {error.reason.name}.") if error.exception_trace: exc_trace = "" for line in error.exception_trace.format(chain=False): exc_trace += f" {line}" print(exc_trace) print("\n") 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 expected_scan_command_results = { 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, } if server_scan_result.scan_commands_results.keys() != expected_scan_command_results: raise RuntimeError("SSLyze did not complete all the expected scan commands.") else: print("OK: Completed all the expected scan commands.") # Ensure TLS 1.2 and 1.3 were detected by SSLyze to be enabled # https://github.com/nabla-c0d3/sslyze/issues/472 for ciphers_scan_cmd in [ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES]: scan_cmd_result = server_scan_result.scan_commands_results[ciphers_scan_cmd] # type: ignore 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.")
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 test(self): # Given a bunch of servers to scan # Including two reachable servers reachable_request1 = ServerScanRequest( server_location=ServerNetworkLocation(hostname="www.google.com", port=443)) reachable_request2 = ServerScanRequest( server_location=ServerNetworkLocation( hostname="www.cloudflare.com", port=443)) # And two servers that are not reachable non_reachable_request1 = ServerScanRequest( server_location=ServerNetworkLocation(hostname="localhost", port=12345)) non_reachable_request2 = ServerScanRequest( server_location=ServerNetworkLocation(hostname="localhost", port=54321)) # When using the MassConnectivityTester to test connectivity completed_request_uuids = set() def server_connectivity_test_completed_callback( server_scan_request: ServerScanRequest, tls_probing_result: ServerTlsProbingResult) -> None: completed_request_uuids.add(server_scan_request.uuid) error_request_uuids = set() def server_connectivity_test_error_callback( server_scan_request: ServerScanRequest, connectivity_error: ConnectionToServerFailed) -> None: error_request_uuids.add(server_scan_request.uuid) tester = MassConnectivityTester(concurrent_server_scans_count=3) tester.start_work([ reachable_request1, reachable_request2, non_reachable_request1, non_reachable_request2 ]) assert tester.has_started_work # It succeeds tester.wait_until_all_work_was_processed( server_connectivity_test_completed_callback= server_connectivity_test_completed_callback, server_connectivity_test_error_callback= server_connectivity_test_error_callback, ) # And the reachable servers were returned assert len(completed_request_uuids) == 2 assert {reachable_request1.uuid, reachable_request2.uuid} == completed_request_uuids # And the non-reachable server was returned assert len(error_request_uuids) == 2 assert {non_reachable_request1.uuid, non_reachable_request2.uuid} == error_request_uuids # And the tester was shutdown cleanly tester._scan_requests_queue.join() tester._results_queue.join() for thread in tester._all_worker_threads: assert not thread.is_alive()
def scan(target, ip, port, view, suite): """ Five inputs: web site name, ip, port split-dns view, and cipher suite """ server_location = ServerNetworkLocationViaDirectConnection( target, port, ip) # This line checks to see if the host is online try: server_info = ServerConnectivityTester().perform(server_location) except errors.ConnectionToServerTimedOut: raise ConnectionError("Connection Timeout", ERROR_MSG_CONNECTION_TIMEOUT(target, port)) except errors.ConnectionToServerFailed: raise ConnectionError("Unknown Connection Error", ERROR_MSG_UNKNOWN_CONNECTION(target, port)) # Create a new results dictionary scan_output = results.new_result_set() # I hash the combination of hostname and ip for tracking key = md5((target + ip).encode("utf-8")).hexdigest() results.set_result(scan_output, "MD5", key) results.set_result(scan_output, "Target", f"{target}:{port}") results.set_result(scan_output, "IP", f"{ip}:{port}") results.set_result(scan_output, "Scan", suite) results.set_result(scan_output, "View", view) scanner = Scanner() server_scan_req = ServerScanRequest(server_info=server_info, scan_commands=CIPHER_SUITES.get(suite)) scanner.queue_scan(server_scan_req) for result in scanner.get_results(): for cipher_suite in CIPHER_SUITES.get(suite): scan_result = result.scan_commands_results[cipher_suite] for accepted_cipher_suite in scan_result.accepted_cipher_suites: if suite == "policy" and scan_result.tls_version_used.name == "TLS_1_2": if (accepted_cipher_suite.cipher_suite.name not in ALLOWED_TLS12_CIPHERS): results.set_ciphers( scan_output, { "Version": f"{scan_result.tls_version_used.name}", "Cipher": f"{accepted_cipher_suite.cipher_suite.name}", }, ) else: results.set_ciphers( scan_output, { "Version": f"{scan_result.tls_version_used.name}", "Cipher": f"{accepted_cipher_suite.cipher_suite.name}", }, ) if len(scan_output["Results"]) == 0: results.set_result(scan_output, "Results", "No Policy Violations") return scan_output
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.")
def main(server_software_running_on_localhost: WebServerSoftwareEnum) -> None: # Ensure the server is accessible on localhost server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( "localhost", 443) server_info = ServerConnectivityTester().perform(server_location) 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_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.REQUIRED: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Apache2:" f" {server_info.tls_probing_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_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.OPTIONAL: raise RuntimeError( f"SSLyze did not detect that client authentication was required by Nginx:" f" {server_info.tls_probing_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_info.tls_probing_result.client_auth_requirement != ClientAuthRequirementEnum.DISABLED: raise RuntimeError( f"SSLyze detected that client authentication was enabled by IIS:" f" {server_info.tls_probing_result.client_auth_requirement}.") else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") # Queue all scan commands print("Starting scan.") scanner = Scanner() server_scan_req = ServerScanRequest( server_info=server_info, scan_commands=ScanCommandsRepository.get_all_scan_commands(), ) scanner.queue_scan(server_scan_req) # Retrieve the result for server_scan_result in scanner.get_results(): successful_cmds_count = len(server_scan_result.scan_commands_results) errored_cmds_count = len(server_scan_result.scan_commands_errors) print( f"Finished scan with {successful_cmds_count} results and {errored_cmds_count} errors." ) # Crash if any scan commands triggered an error that's not due to client authentication being required triggered_unexpected_error = False for scan_command, error in server_scan_result.scan_commands_errors.items( ): if error.reason != ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: triggered_unexpected_error = True print( f"\nError when running {scan_command}: {error.reason.name}." ) if error.exception_trace: exc_trace = "" for line in error.exception_trace.format(chain=False): exc_trace += f" {line}" print(exc_trace) print("\n") 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_command_results = { 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_command_results = { 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_RESUMPTION_RATE, 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_command_results = ScanCommandsRepository.get_all_scan_commands( ) # type: ignore else: raise ValueError( f"Unexpected value: {server_software_running_on_localhost}") completed_scan_command_results = server_scan_result.scan_commands_results.keys( ) if completed_scan_command_results != expected_scan_command_results: raise RuntimeError( f"SSLyze did not complete all the expected scan commands: {completed_scan_command_results}" ) else: 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_result = server_scan_result.scan_commands_results[ ciphers_scan_cmd] # type: ignore 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 json_output = _SslyzeOutputAsJson( server_scan_results=[server_scan_result], server_connectivity_errors=[], total_scan_time=3, ) json_output_as_dict = asdict(json_output) json.dumps(json_output_as_dict, cls=JsonEncoder, sort_keys=True, indent=4, ensure_ascii=True) print("OK: Was able to generate JSON output.")
def ssl_scan(self, target): print("Running SSL Scan") # Define the server that you want to scan server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( target, 443) try: # Do connectivity testing to ensure SSLyze is able to connect try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: # Could not connect to the server; abort print(f"Error connecting to {server_location}: {e.error_message}") return # Then queue some scan commands for the server scanner = Scanner() server_scan_req = ServerScanRequest(server_info=server_info, scan_commands={ ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_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.HEARTBLEED, ScanCommand.HTTP_HEADERS}, ) scanner.queue_scan(server_scan_req) # Then retrieve the results for server_scan_result in scanner.get_results(): print( f"\nResults for {server_scan_result.server_info.server_location.hostname}:") heartbleed_vuln = server_scan_result.scan_commands_results[ ScanCommand.HEARTBLEED].is_vulnerable_to_heartbleed print(f"\nIs vulnerable to heartbleed? {heartbleed_vuln}") print("\nAccepted cipher suites for TLS 1.0:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_0_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") print("\nAccepted cipher suites for TLS 1.1:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_1_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") print("\nAccepted cipher suites for TLS 1.2:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") print("\nAccepted cipher suites for TLS 1.3:") for accepted_cipher_suite in server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES].accepted_cipher_suites: print(f"* {accepted_cipher_suite.cipher_suite.name}") # SSL 2.0 results ssl2_result = server_scan_result.scan_commands_results[ ScanCommand.SSL_2_0_CIPHER_SUITES] 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}") # Certificate info results certinfo_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] print("\nCertificate info:") for cert_deployment in certinfo_result.certificate_deployments: print( f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}") except Exception as e: self.handle_exception(e, "Error running SSL scan") pass
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 run(url): scan_result = {"name": __plugin__, "sequence": SEQUENCE, "result": []} error_result = {"name": __plugin__, "sequence": SEQUENCE, "result": []} error_result["result"] = [{ "name": "Error", "result": [{ "name": f"{__plugin__} can't scan this website" }] }] result_map = { "https": { "name": "Enabled HTTPS", "sequence": 0, "result": [] }, "effective": { "name": "Effective", "sequence": 1, "result": [] }, "subject": { "name": "Subject", "sequence": 2, "result": [] }, "issuer": { "name": "Issuer", "sequence": 3, "result": [] }, "public": { "name": "Public key algorithm", "sequence": 4, "result": [], }, "signature": { "name": "Signature hash algorithm", "sequence": 5, "result": [], }, "before": { "name": "Not valid before (UTC)", "sequence": 6, "result": [], }, "after": { "name": "Not valid after (UTC)", "sequence": 7, "result": [], }, "tls1_2": { "name": "Accepted TLS1.2 cipher suites", "sequence": 8, "result": [] }, "tls1_3": { "name": "Accepted TLS1.3 cipher suites", "sequence": 9, "result": [] }, "pfs": { "name": "Perfect Forward Secrecy (PFS)", "sequence": 10, "result": [], }, "ats": { "name": "App Transport Security (ATS)", "sequence": 11, "result": [], }, "tls1_3_early_data": { "name": "Support TLS1.3 early data", "sequence": 12, "result": [] }, "match": { "name": "Leaf certificate subject matches hostname", "sequence": 13, "result": [], }, "ocsp": { "name": "OCSP Must-Staple", "sequence": 14, "result": [], }, "fallback": { "name": "The TLS_FALLBACK_SCSV mechanism", "sequence": 15, "result": [] }, "ccs": { "name": "The OpenSSL CCS Injection vulnerability", "sequence": 16, "result": [] }, "heartbleed": { "name": "The Heartbleed vulnerability", "sequence": 17, "result": [] }, "crime": { "name": "The CRIME vulnerability", "sequence": 18, "result": [] }, "robot": { "name": "The ROBOT vulnerability", "sequence": 19, "result": [] }, } server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( url.netloc, 443) try: server_info = ServerConnectivityTester().perform(server_location) except ConnectionToServerFailed as e: return error_result scanner = Scanner() server_scan_req = ServerScanRequest( server_info, { ScanCommand.CERTIFICATE_INFO, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_3_EARLY_DATA, ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.OPENSSL_CCS_INJECTION, ScanCommand.HEARTBLEED, ScanCommand.TLS_COMPRESSION, ScanCommand.ROBOT, }, ) scanner.queue_scan(server_scan_req) for server_scan_result in scanner.get_results(): certificate_result = server_scan_result.scan_commands_results[ ScanCommand.CERTIFICATE_INFO] tls_1_2_cipher_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES] tls_1_3_cipher_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES] tls_1_3_early_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_EARLY_DATA] tls_fallback_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_FALLBACK_SCSV] ccs_result = server_scan_result.scan_commands_results[ ScanCommand.OPENSSL_CCS_INJECTION] heartbleed_result = server_scan_result.scan_commands_results[ ScanCommand.HEARTBLEED] crime_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_COMPRESSION] robot_result = server_scan_result.scan_commands_results[ ScanCommand.ROBOT] for certificate_deployment in certificate_result.certificate_deployments: for certificate_info in certificate_deployment.received_certificate_chain: result_map["subject"]["result"] = [{ "name": certificate_info.subject.rfc4514_string() }] result_map["issuer"]["result"] = [{ "name": certificate_info.issuer.rfc4514_string() }] public_key = certificate_info.public_key() public_key_name = type(public_key).__name__[1:][:-9] if "key_size" in dir(public_key): result_map["public"]["result"] = [{ "name": f"{public_key_name}{certificate_info.public_key().key_size}" }] else: result_map["public"]["result"] = [{"name": public_key_name}] result_map["signature"]["result"] = [{ "name": certificate_info.signature_hash_algorithm.name.upper() }] if datetime.now( ) > certificate_info.not_valid_before and datetime.now( ) < certificate_info.not_valid_after: result_map["effective"]["result"] = True result_map["before"]["result"] = [{ "name": datetime.strftime(certificate_info.not_valid_before, "%Y-%m-%d %H:%M:%S") }] result_map["after"]["result"] = [{ "name": datetime.strftime(certificate_info.not_valid_after, "%Y-%m-%d %H:%M:%S") }] break result_map["https"]["result"] = True result_map["match"][ "result"] = certificate_deployment.leaf_certificate_subject_matches_hostname result_map["ocsp"][ "result"] = certificate_deployment.leaf_certificate_has_must_staple_extension break tls_1_2_cipher_list = [ accepted_cipher_suite.cipher_suite.name for accepted_cipher_suite in tls_1_2_cipher_result.accepted_cipher_suites ] tls_1_3_cipher_list = [ accepted_cipher_suite.cipher_suite.name for accepted_cipher_suite in tls_1_3_cipher_result.accepted_cipher_suites ] result_map["tls1_2"]["result"] = [{ "name": tls_1_2_cipher } for tls_1_2_cipher in tls_1_2_cipher_list ] if tls_1_2_cipher_list else False result_map["tls1_3"]["result"] = [{ "name": tls_1_3_cipher } for tls_1_3_cipher in tls_1_3_cipher_list ] if tls_1_3_cipher_list else False cipher_list = tls_1_2_cipher_list + tls_1_3_cipher_list for cipher in cipher_list: if "DHE" in cipher: result_map["pfs"]["result"] = True if set(cipher_list).intersection(ATS_CIPHER_SET): result_map["ats"]["result"] = True result_map["tls1_3_early_data"][ "result"] = tls_1_3_early_result.supports_early_data result_map["fallback"][ "result"] = tls_fallback_result.supports_fallback_scsv result_map["ccs"]["result"] = not ccs_result.is_vulnerable_to_ccs_injection result_map["heartbleed"][ "result"] = not heartbleed_result.is_vulnerable_to_heartbleed result_map["crime"]["result"] = not crime_result.supports_compression result_map["robot"]["result"] = [{"name": robot_result.robot_result.name}] scan_result["result"] = sorted([item for item in result_map.values()], key=lambda x: x.get("sequence", 0)) return scan_result
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 scan_runner(seq, host): hostname = host.decode("utf-8") servers_to_scan = [] server_location = None try: if r.hget(seq, "ipaddr"): server_location = ServerNetworkLocationViaDirectConnection( hostname, 443, r.hget(seq, "ipaddr").decode("utf-8")) else: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname, 443) r.hset(seq, "ipaddr", server_location.ip_address) #Initialize with hostname, port int and ip address str #print(server_location) except Exception as e: print(e) r.hset(seq, "STATUS", 2) try: server_info = ServerConnectivityTester().perform(server_location) servers_to_scan.append(server_info) except ConnectionToServerFailed as e: if 'Probing failed' in str(e): r.hset(seq, "STATUS", 31) else: r.hset(seq, "STATUS", 32) 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={ ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.TLS_1_2_CIPHER_SUITES, ScanCommand.TLS_1_1_CIPHER_SUITES, ScanCommand.TLS_1_0_CIPHER_SUITES }, ) 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(): try: tls1_3_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_3_CIPHER_SUITES] cipherstr = "" if tls1_3_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_3_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_3", cipherstr) tls1_2_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_2_CIPHER_SUITES] cipherstr = "" if tls1_2_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_2_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_2", cipherstr) tls1_1_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_1_CIPHER_SUITES] cipherstr = "" if tls1_1_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_1_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_1", cipherstr) tls1_0_result = server_scan_result.scan_commands_results[ ScanCommand.TLS_1_0_CIPHER_SUITES] cipherstr = "" if tls1_0_result.accepted_cipher_suites: for accepted_cipher_suite in tls1_0_result.accepted_cipher_suites: cipherstr = cipherstr + str( accepted_cipher_suite.cipher_suite.name) + " " r.hset(seq, "TLS1_0", cipherstr) r.hset(seq, "STATUS", 1) except KeyError: r.hset(seq, "STATUS", 4) # Scan commands that were run with errors for scan_command, error in server_scan_result.scan_commands_errors.items( ): r.hset(seq, "STATUS", 5)