def _get_plugin_scan_commands() -> List[OptParseCliOption]: """Retrieve the list of command line options implemented by the plugins currently available.""" scan_commands_options = [] for scan_command in ScanCommandsRepository.get_all_scan_commands(): cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls scan_commands_options.extend(cli_connector_cls.get_cli_options()) return scan_commands_options
def __init__(self, file_to: TextIO) -> None: super().__init__(file_to) self._server_connectivity_errors: List[ _ServerConnectivityErrorAsJson] = [] self._server_scan_results: List[ServerScanResult] = [] # Register all JSON serializer functions defined in plugins for scan_command in ScanCommandsRepository.get_all_scan_commands(): ScanCommandsRepository.get_implementation_cls( scan_command ).cli_connector_cls.register_json_serializer_functions()
def _add_plugin_scan_commands(self) -> None: """Recovers the list of command line options implemented by the available plugins and adds them to the command line parser. """ scan_commands_group = OptionGroup(self._parser, "Scan commands", "") for scan_command in ScanCommandsRepository.get_all_scan_commands(): cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls for option in cli_connector_cls.get_cli_options(): scan_commands_group.add_option(f"--{option.option}", help=option.help, action=option.action) self._parser.add_option_group(scan_commands_group)
def __init__( # type: ignore self, *, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None, ): super().__init__( skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular, allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, separators=separators, default=default, ) self._default_json_encoder = json.JSONEncoder() # Using singledispatch allows plugins that return custom objects to extend the JSON serializing logic @singledispatch def object_to_json(obj: Any) -> JsonType: # Assume a default Python type if this function gets called instead of all the registered functions return self._default_json_encoder.encode(obj) self._json_dispatch_function = object_to_json # Register all JSON serializer functions for basic types self._json_dispatch_function.register(_enum_to_json) self._json_dispatch_function.register(_set_to_json) self._json_dispatch_function.register(_path_to_json) self._json_dispatch_function.register(_traceback_to_json) self._json_dispatch_function.register(_datetime_to_json) self._json_dispatch_function.register(_bytearray_to_json) # Register all JSON serializer functions defined in plugins for scan_command in ScanCommandsRepository.get_all_scan_commands(): cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls for json_serializer_function in cli_connector_cls.get_json_serializer_functions( ): self._json_dispatch_function.register(json_serializer_function)
def _generate_scan_jobs_for_server_scan( server_scan_request: ServerScanRequest, ) -> Tuple[Dict[ScanCommandType, List[ScanJob]], ScanCommandErrorsDict]: all_scan_jobs_per_scan_cmd: Dict[ScanCommandType, List[ScanJob]] = {} scan_command_errors_during_queuing = {} for scan_cmd in server_scan_request.scan_commands: implementation_cls = ScanCommandsRepository.get_implementation_cls(scan_cmd) scan_cmd_extra_args = server_scan_request.scan_commands_extra_arguments.get(scan_cmd) # type: ignore try: jobs_for_scan_cmd = implementation_cls.scan_jobs_for_scan_command( server_info=server_scan_request.server_info, extra_arguments=scan_cmd_extra_args ) all_scan_jobs_per_scan_cmd[scan_cmd] = jobs_for_scan_cmd # Process exceptions and instantly "complete" the scan command if the call to create the jobs failed except ScanCommandWrongUsageError as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.WRONG_USAGE, exception_trace=TracebackException.from_exception(e) ) scan_command_errors_during_queuing[scan_cmd] = error except Exception as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, exception_trace=TracebackException.from_exception(e), ) scan_command_errors_during_queuing[scan_cmd] = error return all_scan_jobs_per_scan_cmd, scan_command_errors_during_queuing
def scan_command_error_as_console_output( server_location: ServerNetworkLocation, scan_command: ScanCommand, scan_command_attempt: ScanCommandAttempt) -> str: if not scan_command_attempt.error_trace: raise ValueError("Should never happen") target_result_str = "\n" cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls if scan_command_attempt.error_reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: target_result_str += cli_connector_cls._format_title( f"Client certificated required for --{cli_connector_cls._cli_option}" ) target_result_str += " use --cert and --key to provide one.\n" elif scan_command_attempt.error_reason == ScanCommandErrorReasonEnum.CONNECTIVITY_ISSUE: target_result_str += cli_connector_cls._format_title( f"Connection timed out for --{cli_connector_cls._cli_option}") target_result_str += " try using --slow_connection to reduce the impact on the server.\n" elif scan_command_attempt.error_reason == ScanCommandErrorReasonEnum.WRONG_USAGE: target_result_str += cli_connector_cls._format_title( f"Wrong usage for --{cli_connector_cls._cli_option}") # Extract the last line which contains the reason last_line = None for line in scan_command_attempt.error_trace.format(chain=False): last_line = line if last_line: exception_cls_in_trace = f"{ScanCommandWrongUsageError.__name__}:" if exception_cls_in_trace in last_line: details_text = last_line.split( exception_cls_in_trace)[1].strip() target_result_str += f" {details_text}" else: target_result_str += f" {last_line}" elif scan_command_attempt.error_reason == ScanCommandErrorReasonEnum.BUG_IN_SSLYZE: target_result_str += cli_connector_cls._format_title( f"Error when running --{cli_connector_cls._cli_option}") target_result_str += "\n" target_result_str += ( " You can open an issue at https://github.com/nabla-c0d3/sslyze/issues" " with the following information:\n\n") target_result_str += f" * SSLyze version: {__version__.__version__}\n" target_result_str += f" * Server: {server_location.display_string}" target_result_str += f" - {_server_location_to_network_route(server_location)}\n" target_result_str += f" * Scan command: {scan_command}\n\n" for line in scan_command_attempt.error_trace.format(chain=False): target_result_str += f" {line}" else: raise ValueError("Should never happen") return target_result_str
def queue_scan(self, server_scan: ServerScanRequest) -> None: """Queue a server scan. """ # Only one scan per server can be submitted if server_scan.server_info in self._pending_server_scan_results: raise ValueError( f"Already submitted a scan for server {server_scan.server_info.server_location}" ) self._queued_server_scans.append(server_scan) self._pending_server_scan_results[server_scan.server_info] = {} self._pending_server_scan_errors[server_scan.server_info] = {} # Assign the server to scan to a thread pool server_scans_count = len(self._queued_server_scans) thread_pools_count = len(self._all_thread_pools) thread_pool_index_to_pick = server_scans_count % thread_pools_count thread_pool_for_server = self._all_thread_pools[ thread_pool_index_to_pick] self._server_to_thread_pool[ server_scan.server_info] = thread_pool_for_server # Convert each scan command within the server scan request into jobs for scan_cmd in server_scan.scan_commands: implementation_cls = ScanCommandsRepository.get_implementation_cls( scan_cmd) scan_cmd_extra_args = server_scan.scan_commands_extra_arguments.get( scan_cmd) # type: ignore jobs_to_run = [] try: jobs_to_run = implementation_cls.scan_jobs_for_scan_command( server_info=server_scan.server_info, extra_arguments=scan_cmd_extra_args) # Process exceptions and instantly "complete" the scan command if the call to create the jobs failed except ScanCommandWrongUsageError as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.WRONG_USAGE, exception_trace=TracebackException.from_exception(e)) self._pending_server_scan_errors[ server_scan.server_info][scan_cmd] = error except Exception as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, exception_trace=TracebackException.from_exception(e), ) self._pending_server_scan_errors[ server_scan.server_info][scan_cmd] = error # Schedule the jobs for job in jobs_to_run: future = thread_pool_for_server.submit(job.function_to_call, *job.function_arguments) self._queued_future_to_server_and_scan_cmd[future] = ( server_scan.server_info, scan_cmd)
def queue_scan(self, server_scan: ServerScanRequest) -> None: """Queue a server scan. """ already_queued_server_info = { queued_scan.server_scan_request.server_info for queued_scan in self._queued_server_scans } # Only one scan per server can be submitted if server_scan.server_info in already_queued_server_info: raise ValueError(f"Already submitted a scan for server {server_scan.server_info.server_location}") # Assign the server to scan to a thread pool assigned_thread_pool_index = self._get_assigned_thread_pool_index() assigned_thread_pool = self._thread_pools[assigned_thread_pool_index] # Convert each scan command within the server scan request into jobs queued_futures_per_scan_command: Dict[ScanCommandType, Set[Future]] = {} scan_command_errors_during_queuing = {} for scan_cmd in server_scan.scan_commands: implementation_cls = ScanCommandsRepository.get_implementation_cls(scan_cmd) scan_cmd_extra_args = server_scan.scan_commands_extra_arguments.get(scan_cmd) # type: ignore jobs_to_run = [] try: jobs_to_run = implementation_cls.scan_jobs_for_scan_command( server_info=server_scan.server_info, extra_arguments=scan_cmd_extra_args ) # Process exceptions and instantly "complete" the scan command if the call to create the jobs failed except ScanCommandWrongUsageError as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.WRONG_USAGE, exception_trace=TracebackException.from_exception(e) ) scan_command_errors_during_queuing[scan_cmd] = error except Exception as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, exception_trace=TracebackException.from_exception(e), ) scan_command_errors_during_queuing[scan_cmd] = error # Schedule the jobs queued_futures_per_scan_command[scan_cmd] = set() for job in jobs_to_run: future = assigned_thread_pool.submit(job.function_to_call, *job.function_arguments) queued_futures_per_scan_command[scan_cmd].add(future) # Save everything as a queued scan self._queued_server_scans.append( _QueuedServerScan( server_scan_request=server_scan, queued_scan_jobs_per_scan_command=queued_futures_per_scan_command, queued_on_thread_pool_at_index=assigned_thread_pool_index, scan_command_errors_during_queuing=scan_command_errors_during_queuing, ) )
def _generate_result_for_completed_server_scan(completed_scan: _QueuedServerScan) -> ServerScanResult: server_scan_results: ScanCommandResultsDict = {} server_scan_errors: ScanCommandErrorsDict = {} # Group all the completed jobs per scan command scan_cmd_to_completed_jobs: Dict[ScanCommandType, List[CompletedScanJob]] = { scan_cmd: [] for scan_cmd in completed_scan.server_scan_request.scan_commands } for completed_job in completed_scan.completed_scan_jobs: scan_cmd_to_completed_jobs[completed_job.for_scan_command].append(completed_job) for scan_cmd, completed_scan_jobs in scan_cmd_to_completed_jobs.items(): # Pass the completed scan jobs to the corresponding plugin implementation to generate a result scan_job_results_for_plugin = [ ScanJobResult(_return_value=job.return_value, _exception=job.exception) for job in completed_scan_jobs ] server_info = completed_scan.server_scan_request.server_info plugin_implementation_cls = ScanCommandsRepository.get_implementation_cls(scan_cmd) try: result = plugin_implementation_cls.result_for_completed_scan_jobs(server_info, scan_job_results_for_plugin) server_scan_results[scan_cmd] = result # Process exceptions that may have been raised while the jobs were being completed except ClientCertificateRequested as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED, exception_trace=TracebackException.from_exception(e), ) server_scan_errors[scan_cmd] = error except (ConnectionToServerTimedOut, TlsHandshakeTimedOut) as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.CONNECTIVITY_ISSUE, exception_trace=TracebackException.from_exception(e), ) server_scan_errors[scan_cmd] = error except Exception as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, exception_trace=TracebackException.from_exception(e), ) server_scan_errors[scan_cmd] = error # Lastly, return the fully completed server scan server_scan_errors.update(completed_scan.scan_command_errors_during_queuing) server_scan_result = ServerScanResult( scan_commands_results=server_scan_results, scan_commands_errors=server_scan_errors, server_info=completed_scan.server_scan_request.server_info, scan_commands=completed_scan.server_scan_request.scan_commands, scan_commands_extra_arguments=completed_scan.server_scan_request.scan_commands_extra_arguments, ) return server_scan_result
def _generate_scan_jobs_for_server_scan( server_scan_request: ServerScanRequest, server_connectivity_result: ServerTlsProbingResult, ) -> Tuple[Dict[ScanCommand, List[ScanJob]], Dict[ScanCommand, ScanCommandAttempt]]: all_scan_jobs_per_scan_cmd: Dict[ScanCommand, List[ScanJob]] = {} scan_command_errors_during_queuing: Dict[ScanCommand, ScanCommandAttempt] = {} for scan_cmd in server_scan_request.scan_commands: implementation_cls = ScanCommandsRepository.get_implementation_cls( scan_cmd) scan_cmd_extra_args = getattr( server_scan_request.scan_commands_extra_arguments, scan_cmd, None) try: jobs_for_scan_cmd = implementation_cls.scan_jobs_for_scan_command( server_info=ServerConnectivityInfo( server_location=server_scan_request.server_location, network_configuration=server_scan_request. network_configuration, tls_probing_result=server_connectivity_result, ), extra_arguments=scan_cmd_extra_args, ) all_scan_jobs_per_scan_cmd[scan_cmd] = jobs_for_scan_cmd # Process exceptions and instantly "complete" the scan command if the call to create the jobs failed except ScanCommandWrongUsageError as e: scan_command_attempt_cls = get_scan_command_attempt_cls(scan_cmd) errored_attempt = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.ERROR, error_reason=ScanCommandErrorReasonEnum.WRONG_USAGE, error_trace=TracebackException.from_exception(e), result=None, ) scan_command_errors_during_queuing[scan_cmd] = errored_attempt except Exception as e: scan_command_attempt_cls = get_scan_command_attempt_cls(scan_cmd) errored_attempt = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.ERROR, error_reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, error_trace=TracebackException.from_exception(e), result=None, ) scan_command_errors_during_queuing[scan_cmd] = errored_attempt return all_scan_jobs_per_scan_cmd, scan_command_errors_during_queuing
def server_scan_completed(self, server_scan_result: ServerScanResult) -> None: if server_scan_result.scan_status != ServerScanStatusEnum.COMPLETED: # Nothing to print here if the scan was not completed return # Generate the console output for each scan command scan_command_results_str = "" for result_field in fields(server_scan_result.scan_result): scan_command = ScanCommand(result_field.name) scan_command_attempt = getattr(server_scan_result.scan_result, scan_command, None) if scan_command_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED: scan_command_results_str += "\n" cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls for line in cli_connector_cls.result_to_console_output( scan_command_attempt.result): scan_command_results_str += line + "\n" elif scan_command_attempt.status == ScanCommandAttemptStatusEnum.ERROR: scan_command_results_str += scan_command_error_as_console_output( server_scan_result.server_location, scan_command, scan_command_attempt) elif scan_command_attempt.status == ScanCommandAttemptStatusEnum.NOT_SCHEDULED: pass else: raise ValueError("Should never happen") # Also display the server that was scanned server_location = server_scan_result.server_location network_route = _server_location_to_network_route(server_location) scan_txt = f"Scan Results For {server_location.display_string} - {network_route}" self._file_to.write("\n" + self._format_title(scan_txt) + scan_command_results_str + "\n")
def get_results(self) -> Iterable[ServerScanResult]: """Return completed server scans. """ server_and_scan_cmd_to_completed_futures: Dict[Tuple[ ServerConnectivityInfo, ScanCommandType], List[Future]] = { server_and_scan_cmd: [] for server_and_scan_cmd in self._queued_future_to_server_and_scan_cmd.values() } jobs_completed_count = 0 jobs_total_count = len(self._queued_future_to_server_and_scan_cmd) while jobs_completed_count < jobs_total_count: # Every 1 seconds, process all the results try: for completed_future in as_completed( self._queued_future_to_server_and_scan_cmd.keys(), timeout=1): jobs_completed_count += 1 # Move the future from "queued" to "completed" server_and_scan_cmd = self._queued_future_to_server_and_scan_cmd[ completed_future] del self._queued_future_to_server_and_scan_cmd[ completed_future] server_and_scan_cmd_to_completed_futures[ server_and_scan_cmd].append(completed_future) except TimeoutError: pass # Have all the jobs of a given scan command completed? scan_cmds_completed = [] for server_and_scan_cmd in server_and_scan_cmd_to_completed_futures: if server_and_scan_cmd not in self._queued_future_to_server_and_scan_cmd.values( ): # Yes - store the result server_info, scan_cmd = server_and_scan_cmd implementation_cls = ScanCommandsRepository.get_implementation_cls( scan_cmd) try: result = implementation_cls.result_for_completed_scan_jobs( server_info, server_and_scan_cmd_to_completed_futures[ server_and_scan_cmd]) self._pending_server_scan_results[server_info][ scan_cmd] = result # Process exceptions that may have been raised while the jobs were being completed except ClientCertificateRequested as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum. CLIENT_CERTIFICATE_NEEDED, exception_trace=TracebackException.from_exception( e), ) self._pending_server_scan_errors[server_info][ scan_cmd] = error except ConnectionToServerTimedOut as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum. CONNECTIVITY_ISSUE, exception_trace=TracebackException.from_exception( e), ) self._pending_server_scan_errors[server_info][ scan_cmd] = error except Exception as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, exception_trace=TracebackException.from_exception( e), ) self._pending_server_scan_errors[server_info][ scan_cmd] = error finally: scan_cmds_completed.append(server_and_scan_cmd) for server_and_scan_cmd in scan_cmds_completed: del server_and_scan_cmd_to_completed_futures[ server_and_scan_cmd] # Lastly, have all the scan commands for a given server completed? completed_server_scan_indexes: List[int] = [] for index, server_scan in enumerate(self._queued_server_scans): scan_commands_processed_count = len( self._pending_server_scan_results[server_scan.server_info] ) + len( self._pending_server_scan_errors[server_scan.server_info]) if len(server_scan.scan_commands ) == scan_commands_processed_count: # Yes - return the fully completed server scan yield ServerScanResult( scan_commands_results=self. _pending_server_scan_results[server_scan.server_info], scan_commands_errors=self._pending_server_scan_errors[ server_scan.server_info], server_info=server_scan.server_info, scan_commands=server_scan.scan_commands, scan_commands_extra_arguments=server_scan. scan_commands_extra_arguments, ) del self._pending_server_scan_results[ server_scan.server_info] del self._pending_server_scan_errors[ server_scan.server_info] completed_server_scan_indexes.append(index) # Remove the completed server scans - highest to lowest indexes as otherwise indexes to delete would no # longer be valid while the loop is running for index in reversed(completed_server_scan_indexes): del self._queued_server_scans[index] self._shutdown_thread_pools()
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 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 parse_command_line(self) -> ParsedCommandLine: """Parses the command line used to launch SSLyze. """ (args_command_list, args_target_list) = self._parser.parse_args() if args_command_list.update_trust_stores: # Just update the trust stores and do nothing TrustStoresRepository.update_default() raise TrustStoresUpdateCompleted() # Handle the --targets_in command line and fill args_target_list if args_command_list.targets_in: if args_target_list: raise CommandLineParsingError( "Cannot use --targets_list and specify targets within the command line." ) try: # Read targets from a file with open(args_command_list.targets_in) as f: for target in f.readlines(): if target.strip(): # Ignore empty lines if not target.startswith( "#"): # Ignore comment lines args_target_list.append(target.strip()) except IOError: raise CommandLineParsingError( "Can't read targets from input file '{}.".format( args_command_list.targets_in)) if not args_target_list: raise CommandLineParsingError("No targets to scan.") # Handle the --regular command line parameter as a shortcut if self._parser.has_option("--regular"): if getattr(args_command_list, "regular"): setattr(args_command_list, "regular", False) for cmd in self.REGULAR_CMD: setattr(args_command_list, cmd, True) # Handle JSON settings should_print_json_to_console = False json_path_out: Optional[Path] = None if args_command_list.json_file: if args_command_list.json_file == "-": if args_command_list.quiet: raise CommandLineParsingError( "Cannot use --quiet with --json_out -.") should_print_json_to_console = True else: json_path_out = Path(args_command_list.json_file).absolute() # Sanity checks on the client cert options client_auth_creds = None if bool(args_command_list.cert) ^ bool(args_command_list.key): raise CommandLineParsingError( "No private key or certificate file were given. See --cert and --key." ) elif args_command_list.cert: # Private key formats if args_command_list.keyform == "DER": key_type = OpenSslFileTypeEnum.ASN1 elif args_command_list.keyform == "PEM": key_type = OpenSslFileTypeEnum.PEM else: raise CommandLineParsingError( "--keyform should be DER or PEM.") # Let's try to open the cert and key files try: client_auth_creds = ClientAuthenticationCredentials( certificate_chain_path=Path(args_command_list.cert), key_path=Path(args_command_list.key), key_password=args_command_list.keypass, key_type=key_type, ) except ValueError as e: raise CommandLineParsingError( "Invalid client authentication settings: {}.".format( e.args[0])) # HTTP CONNECT proxy http_proxy_settings = None if args_command_list.https_tunnel: try: http_proxy_settings = HttpProxySettings.from_url( args_command_list.https_tunnel) except ValueError as e: raise CommandLineParsingError( "Invalid proxy URL for --https_tunnel: {}.".format( e.args[0])) # Create the server location objects for each specified servers good_servers: List[Tuple[ServerNetworkLocation, ServerNetworkConfiguration]] = [] invalid_server_strings: List[InvalidServerStringError] = [] for server_string in args_target_list: try: # Parse the string supplied via the CLI for this server hostname, ip_address, port = CommandLineServerStringParser.parse_server_string( server_string) except InvalidServerStringError as e: # The server string is malformed invalid_server_strings.append(e) continue # If not port number was supplied, assume 443 final_port = port if port else 443 # Figure out how we're going to connect to the server server_location: ServerNetworkLocation if http_proxy_settings: # Connect to the server via an HTTP proxy # A limitation when using the CLI is that only one http_proxy_settings can be specified for all servers server_location = ServerNetworkLocationViaHttpProxy( hostname=hostname, port=final_port, http_proxy_settings=http_proxy_settings) else: # Connect to the server directly if ip_address: server_location = ServerNetworkLocationViaDirectConnection( hostname=hostname, port=final_port, ip_address=ip_address) else: # No IP address supplied - do a DNS lookup try: server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup( hostname=hostname, port=final_port) except ServerHostnameCouldNotBeResolved: invalid_server_strings.append( InvalidServerStringError( server_string=f"{hostname}:{final_port}", error_message= f"Could not resolve hostname {hostname}", )) continue # Figure out extra network config for this server # Opportunistic TLS opportunistic_tls: Optional[ ProtocolWithOpportunisticTlsEnum] = None if args_command_list.starttls: if args_command_list.starttls == "auto": # Special value to auto-derive the protocol from the port number opportunistic_tls = ProtocolWithOpportunisticTlsEnum.from_default_port( final_port) elif args_command_list.starttls in _STARTTLS_PROTOCOL_DICT: opportunistic_tls = _STARTTLS_PROTOCOL_DICT[ args_command_list.starttls] else: raise CommandLineParsingError(self.START_TLS_USAGE) try: sni_hostname = args_command_list.sni if args_command_list.sni else hostname network_config = ServerNetworkConfiguration( tls_opportunistic_encryption=opportunistic_tls, tls_server_name_indication=sni_hostname, tls_client_auth_credentials=client_auth_creds, xmpp_to_hostname=args_command_list.xmpp_to, ) good_servers.append((server_location, network_config)) except InvalidServerNetworkConfigurationError as e: raise CommandLineParsingError(e.args[0]) # Figure out global network settings concurrent_server_scans_limit = None per_server_concurrent_connections_limit = None if args_command_list.https_tunnel: # All the connections will go through a single proxy; only scan one server at a time to not DOS the proxy concurrent_server_scans_limit = 1 if args_command_list.slow_connection: # Go easy on the servers; only open 2 concurrent connections against each server per_server_concurrent_connections_limit = 2 # Figure out the scan commands that are enabled scan_commands: Set[ScanCommandType] = set() scan_commands_extra_arguments: ScanCommandExtraArgumentsDict = {} for scan_command in ScanCommandsRepository.get_all_scan_commands(): cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls is_scan_cmd_enabled, extra_args = cli_connector_cls.find_cli_options_in_command_line( args_command_list.__dict__) if is_scan_cmd_enabled: scan_commands.add(scan_command) if extra_args: scan_commands_extra_arguments[ scan_command] = extra_args # type: ignore return ParsedCommandLine( invalid_servers=invalid_server_strings, servers_to_scans=good_servers, scan_commands=scan_commands, scan_commands_extra_arguments=scan_commands_extra_arguments, should_print_json_to_console=should_print_json_to_console, json_path_out=json_path_out, should_disable_console_output=args_command_list.quiet or args_command_list.json_file == "-", concurrent_server_scans_limit=concurrent_server_scans_limit, per_server_concurrent_connections_limit= per_server_concurrent_connections_limit, )
def server_scan_completed(self, server_scan_result: ServerScanResult) -> None: target_result_str = "" # Display the server that was scanned server_location = server_scan_result.server_info.server_location network_route = _server_location_to_network_route(server_location) # Display result for scan commands that were run successfully for scan_command, scan_command_result in server_scan_result.scan_commands_results.items( ): typed_scan_command = cast(ScanCommandType, scan_command) target_result_str += "\n" cli_connector_cls = ScanCommandsRepository.get_implementation_cls( typed_scan_command).cli_connector_cls for line in cli_connector_cls.result_to_console_output( scan_command_result): target_result_str += line + "\n" # Display scan commands that failed for scan_command, scan_command_error in server_scan_result.scan_commands_errors.items( ): target_result_str += "\n" cli_connector_cls = ScanCommandsRepository.get_implementation_cls( scan_command).cli_connector_cls if scan_command_error.reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED: target_result_str += cli_connector_cls._format_title( f"Client certificated required for --{cli_connector_cls._cli_option}" ) target_result_str += " use --cert and --key to provide one.\n" elif scan_command_error.reason == ScanCommandErrorReasonEnum.CONNECTIVITY_ISSUE: target_result_str += cli_connector_cls._format_title( f"Connection timed out for --{cli_connector_cls._cli_option}" ) target_result_str += " try using --slow_connection to reduce the impact on the server.\n" elif scan_command_error.reason == ScanCommandErrorReasonEnum.WRONG_USAGE: target_result_str += cli_connector_cls._format_title( f"Wrong usage for --{cli_connector_cls._cli_option}") # Extract the last line which contains the reason last_line = None for line in scan_command_error.exception_trace.format( chain=False): last_line = line if last_line: exception_cls_in_trace = f"{ScanCommandWrongUsageError.__name__}:" if exception_cls_in_trace in last_line: details_text = last_line.split( exception_cls_in_trace)[1].strip() target_result_str += f" {details_text}" else: target_result_str += f" {last_line}" elif scan_command_error.reason in [ ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, ]: target_result_str += cli_connector_cls._format_title( f"Error when running --{cli_connector_cls._cli_option}") target_result_str += "\n" target_result_str += ( " You can open an issue at https://github.com/nabla-c0d3/sslyze/issues" " with the following information:\n\n") target_result_str += f" * SSLyze version: {__version__.__version__}\n" target_result_str += ( f" * Server: {server_location.hostname}:{server_location.port} - {network_route}\n" ) target_result_str += f" * Scan command: {scan_command}\n\n" for line in scan_command_error.exception_trace.format( chain=False): target_result_str += f" {line}" else: raise ValueError("Should never happen") scan_txt = f"Scan Results For {server_location.hostname}:{server_location.port} - {network_route}" self._file_to.write( self._format_title(scan_txt) + target_result_str + "\n\n")
def _generate_result_for_completed_server_scan( completed_scan: _OngoingServerScan) -> ServerScanResult: all_scan_command_attempts: Dict[ScanCommand, ScanCommandAttempt] = {} # Group all the completed jobs per scan command scan_cmd_to_completed_jobs: Dict[ScanCommand, List[CompletedScanJob]] = { scan_cmd: [] for scan_cmd in completed_scan.server_scan_request.scan_commands } for completed_job in completed_scan.completed_scan_jobs: scan_cmd_to_completed_jobs[completed_job.for_scan_command].append( completed_job) for scan_cmd, completed_scan_jobs in scan_cmd_to_completed_jobs.items(): scan_command_attempt_cls = get_scan_command_attempt_cls(scan_cmd) # Pass the completed scan jobs to the corresponding plugin implementation to generate a result scan_job_results_for_plugin = [ ScanJobResult(_return_value=job.return_value, _exception=job.exception) for job in completed_scan_jobs ] plugin_implementation_cls = ScanCommandsRepository.get_implementation_cls( scan_cmd) try: scan_cmd_result = plugin_implementation_cls.result_for_completed_scan_jobs( server_info=ServerConnectivityInfo( server_location=completed_scan.server_scan_request. server_location, network_configuration=completed_scan.server_scan_request. network_configuration, tls_probing_result=completed_scan. server_connectivity_result, ), scan_job_results=scan_job_results_for_plugin, ) scan_cmd_attempt = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.COMPLETED, error_reason=None, error_trace=None, result=scan_cmd_result, ) # Process exceptions that may have been raised while the jobs were being completed except ClientCertificateRequested as e: scan_cmd_attempt = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.ERROR, error_reason=ScanCommandErrorReasonEnum. CLIENT_CERTIFICATE_NEEDED, error_trace=TracebackException.from_exception(e), result=None, ) except (ConnectionToServerTimedOut, TlsHandshakeTimedOut) as e: scan_cmd_attempt = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.ERROR, error_reason=ScanCommandErrorReasonEnum.CONNECTIVITY_ISSUE, error_trace=TracebackException.from_exception(e), result=None, ) except Exception as e: scan_cmd_attempt = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.ERROR, error_reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, error_trace=TracebackException.from_exception(e), result=None, ) all_scan_command_attempts[scan_cmd] = scan_cmd_attempt # Add scan command attempts that failed when queuing them all_scan_command_attempts.update( completed_scan.scan_command_errors_during_queuing) # Add remaining scan commands as NOT_SCHEDULED for cls_field in fields(AllScanCommandsAttempts): if cls_field.name not in all_scan_command_attempts: scan_cmd = ScanCommand(cls_field.name) scan_command_attempt_cls = get_scan_command_attempt_cls(scan_cmd) all_scan_command_attempts[scan_cmd] = scan_command_attempt_cls( status=ScanCommandAttemptStatusEnum.NOT_SCHEDULED, error_reason=None, error_trace=None, result=None, ) # Generate the final scan_result object scan_cmd_str_to_scan_cmd_result = { scan_cmd.value: scan_cmd_result for scan_cmd, scan_cmd_result in all_scan_command_attempts.items() } scan_result = AllScanCommandsAttempts( **scan_cmd_str_to_scan_cmd_result) # type: ignore # Lastly, return the fully completed server scan server_scan_result = ServerScanResult( uuid=completed_scan.server_scan_request.uuid, server_location=completed_scan.server_scan_request.server_location, network_configuration=completed_scan.server_scan_request. network_configuration, connectivity_status=ServerConnectivityStatusEnum.COMPLETED, connectivity_error_trace=None, connectivity_result=completed_scan.server_connectivity_result, scan_status=ServerScanStatusEnum.COMPLETED, scan_result=scan_result, ) return server_scan_result
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 get_results(self) -> Iterable[ServerScanResult]: """Return completed server scans. """ ongoing_scan_jobs = set() for queued_server_scan in self._queued_server_scans: ongoing_scan_jobs.update(queued_server_scan.all_queued_scan_jobs) while ongoing_scan_jobs: # Every 0.3 seconds, check for completed jobs all_completed_scan_jobs, _ = wait(ongoing_scan_jobs, timeout=0.3) # Check if a server scan has been fully completed for queued_server_scan in self._queued_server_scans: if not queued_server_scan.all_queued_scan_jobs.issubset( all_completed_scan_jobs): # This server scan still has jobs ongoing; check the next one continue # If we get here, all the jobs for a specific server scan have been completed # Generate the result for each scan command server_scan_results: ScanCommandResultsDict = {} server_scan_errors: ScanCommandErrorsDict = {} for scan_cmd, completed_scan_jobs in queued_server_scan.queued_scan_jobs_per_scan_command.items( ): server_info = queued_server_scan.server_scan_request.server_info implementation_cls = ScanCommandsRepository.get_implementation_cls( scan_cmd) try: result = implementation_cls.result_for_completed_scan_jobs( server_info, list(completed_scan_jobs)) server_scan_results[scan_cmd] = result # Process exceptions that may have been raised while the jobs were being completed except ClientCertificateRequested as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum. CLIENT_CERTIFICATE_NEEDED, exception_trace=TracebackException.from_exception( e), ) server_scan_errors[scan_cmd] = error except (ConnectionToServerTimedOut, TlsHandshakeTimedOut) as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum. CONNECTIVITY_ISSUE, exception_trace=TracebackException.from_exception( e), ) server_scan_errors[scan_cmd] = error except Exception as e: error = ScanCommandError( reason=ScanCommandErrorReasonEnum.BUG_IN_SSLYZE, exception_trace=TracebackException.from_exception( e), ) server_scan_errors[scan_cmd] = error # Discard the corresponding jobs ongoing_scan_jobs.difference_update( queued_server_scan.all_queued_scan_jobs) # Lastly, return the fully completed server scan server_scan_errors.update( queued_server_scan.scan_command_errors_during_queuing) server_scan_result = ServerScanResult( scan_commands_results=server_scan_results, scan_commands_errors=server_scan_errors, server_info=queued_server_scan.server_scan_request. server_info, scan_commands=queued_server_scan.server_scan_request. scan_commands, scan_commands_extra_arguments=queued_server_scan. server_scan_request.scan_commands_extra_arguments, ) yield server_scan_result self._shutdown_thread_pools()
def test_all_commands_are_implemented(self): for scan_command in ScanCommandsRepository.get_all_scan_commands(): assert ScanCommandsRepository.get_implementation_cls(scan_command)