Exemplo n.º 1
0
 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
Exemplo n.º 2
0
    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()
Exemplo n.º 3
0
    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)
Exemplo n.º 4
0
    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)
Exemplo n.º 5
0
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
Exemplo n.º 6
0
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
Exemplo n.º 7
0
    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)
Exemplo n.º 8
0
    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,
            )
        )
Exemplo n.º 9
0
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
Exemplo n.º 10
0
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
Exemplo n.º 11
0
    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")
Exemplo n.º 12
0
    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()
Exemplo n.º 13
0
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.")
Exemplo n.º 14
0
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.")
Exemplo n.º 15
0
    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,
        )
Exemplo n.º 16
0
    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")
Exemplo n.º 17
0
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
Exemplo n.º 18
0
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.")
Exemplo n.º 19
0
    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()
Exemplo n.º 20
0
 def test_all_commands_are_implemented(self):
     for scan_command in ScanCommandsRepository.get_all_scan_commands():
         assert ScanCommandsRepository.get_implementation_cls(scan_command)