Пример #1
0
    def test_duplicate_server(self, mock_scan_commands):
        # Given a server to scan
        server_info = ServerConnectivityInfoFactory.create()

        # When trying to queue two scans for this server
        server_scan1 = ServerScanRequest(server_info=server_info, scan_commands={ScanCommandForTests.MOCK_COMMAND_1})
        server_scan2 = ServerScanRequest(server_info=server_info, scan_commands={ScanCommandForTests.MOCK_COMMAND_2})
        scanner = Scanner()
        scanner.queue_scan(server_scan1)

        # It fails
        with pytest.raises(ValueError):
            scanner.queue_scan(server_scan2)
Пример #2
0
    def test_error_bug_in_sslyze_when_processing_job_results(
            self, mock_scan_commands):
        # Given a server to scan with some scan commands
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={
                ScanCommandForTests.MOCK_COMMAND_1,
                ScanCommandForTests.MOCK_COMMAND_2
            },
        )

        # And the first scan command will trigger an error when processing the completed scan jobs
        with mock.patch.object(MockPlugin1Implementation,
                               "_scan_job_work_function",
                               side_effect=RuntimeError):
            # When running the scan
            scanner = Scanner()
            scanner.start_scans([server_scan])

            # It succeeds
            all_results = []
            for result in scanner.get_results():
                all_results.append(result)
            assert len(all_results) == 1

            # And the exception was properly caught and returned
            result = all_results[0]
            assert len(result.scan_commands_errors) == 1
            error = result.scan_commands_errors[
                ScanCommandForTests.MOCK_COMMAND_1]
            assert ScanCommandErrorReasonEnum.BUG_IN_SSLYZE == error.reason
            assert error.exception_trace
Пример #3
0
    def run(self) -> TLSProfilerResult:
        if self.server_info is None:
            return

        # run all scans together
        server_scan_req = ServerScanRequest(
            server_info=self.server_info,
            scan_commands=self.ALL_SCAN_COMMANDS,
            scan_commands_extra_arguments=self.scan_commands_extra_args,
        )
        self.scanner.queue_scan(server_scan_req)

        # We take the first result because only one server was queued
        self.server_scan_result = next(
            self.scanner.get_results())  # type: ServerScanResult

        (
            validation_errors,
            cert_profile_error,
            cert_warnings,
            pub_key_type,
        ) = self._check_certificate()
        hsts_errors = self._check_hsts_age()
        self._preprocess_ciphers_and_protocols()
        profile_errors = self._check_server_matches_profile(pub_key_type)
        vulnerability_errors = self._check_vulnerabilities()

        return TLSProfilerResult(
            validation_errors,
            cert_warnings,
            profile_errors + hsts_errors + cert_profile_error,
            vulnerability_errors,
        )
Пример #4
0
    def test(self, mock_scan_commands):
        # Given a lot of servers to scan
        total_server_scans_count = 100
        server_scans = [
            ServerScanRequest(
                server_info=ServerConnectivityInfoFactory.create(),
                scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2},
            )
            for _ in range(total_server_scans_count)
        ]

        # And a scanner with specifically chosen network settings
        per_server_concurrent_connections_limit = 4
        concurrent_server_scans_limit = 20
        scanner = Scanner(per_server_concurrent_connections_limit, concurrent_server_scans_limit)

        # When queuing the scans, it succeeds
        for scan in server_scans:
            scanner.queue_scan(scan)

        # And the right number of scans was performed
        assert total_server_scans_count == len(scanner._queued_server_scans)

        # And the chosen network settings were used
        assert concurrent_server_scans_limit == len(scanner._thread_pools)
        for pool in scanner._thread_pools:
            assert per_server_concurrent_connections_limit == pool._max_workers

        # And the server scans were evenly distributed among the thread pools to maximize performance
        expected_server_scans_per_pool = int(total_server_scans_count / concurrent_server_scans_limit)
        thread_pools_used = [server_scan.queued_on_thread_pool_at_index for server_scan in scanner._queued_server_scans]
        server_scans_per_pool_count = Counter(thread_pools_used)
        for pool_count in server_scans_per_pool_count.values():
            assert expected_server_scans_per_pool == pool_count
Пример #5
0
    def test_emergency_shutdown(self, mock_scan_commands):
        # Given a lot of servers to scan
        total_server_scans_count = 100
        server_scans = [
            ServerScanRequest(
                server_info=ServerConnectivityInfoFactory.create(),
                scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2},
            )
            for _ in range(total_server_scans_count)
        ]

        # And the scans get queued
        scanner = Scanner()
        for scan in server_scans:
            scanner.queue_scan(scan)

        # When trying to quickly shutdown the scanner, it succeeds
        scanner.emergency_shutdown()

        # And all the queued jobs were done or cancelled
        all_queued_futures = []
        for server_scan in scanner._queued_server_scans:
            all_queued_futures.extend(server_scan.all_queued_scan_jobs)
        for completed_future in as_completed(all_queued_futures):
            assert completed_future.done()
Пример #6
0
    def test_error_client_certificate_needed(self):
        # Given a server that requires client authentication
        with LegacyOpenSslServer(client_auth_config=ClientAuthConfigEnum.REQUIRED) as server:
            # And sslyze does NOT provide a client certificate
            server_location = ServerNetworkLocationViaDirectConnection(
                hostname=server.hostname, ip_address=server.ip_address, port=server.port
            )
            server_info = ServerConnectivityTester().perform(server_location)

            server_scan = ServerScanRequest(
                server_info=server_info,
                scan_commands={
                    # And a scan command that cannot be completed without a client certificate
                    ScanCommand.HTTP_HEADERS,
                },
            )

            # When queuing the scan
            scanner = Scanner()
            scanner.queue_scan(server_scan)

            # It succeeds
            all_results = []
            for result in scanner.get_results():
                all_results.append(result)

            assert len(all_results) == 1

            # And the error was properly returned
            error = all_results[0].scan_commands_errors[ScanCommand.HTTP_HEADERS]
            assert error.reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED
Пример #7
0
    def test_error_server_connectivity_issue_handshake_timeout(self, mock_scan_commands):
        # Given a server to scan with some commands
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2},
        )

        # And the first scan command will trigger a handshake timeout with the server
        with mock.patch.object(
            MockPlugin1Implementation,
            "_scan_job_work_function",
            side_effect=TlsHandshakeTimedOut(
                server_location=server_scan.server_info.server_location,
                network_configuration=server_scan.server_info.network_configuration,
                error_message="error",
            ),
        ):
            # When queuing the scan
            scanner = Scanner()
            scanner.queue_scan(server_scan)

        # It succeeds
        for result in scanner.get_results():
            # And the error was properly caught and returned
            assert len(result.scan_commands_errors) == 1
            error = result.scan_commands_errors[ScanCommandForTests.MOCK_COMMAND_1]
            assert ScanCommandErrorReasonEnum.CONNECTIVITY_ISSUE == error.reason
            assert error.exception_trace
Пример #8
0
    def test(self, mock_scan_commands):
        # Given a server to scan
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={
                ScanCommandForTests.MOCK_COMMAND_1,
                ScanCommandForTests.MOCK_COMMAND_2
            },
        )

        # When running the scan
        scanner = Scanner()
        scanner.start_scans([server_scan])

        # It succeeds
        all_results = []
        for result in scanner.get_results():
            all_results.append(result)
        assert len(all_results) == 1

        # And the right result is returned
        result = all_results[0]
        assert result.server_info == server_scan.server_info
        assert result.scan_commands == server_scan.scan_commands
        assert result.scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments
        assert len(result.scan_commands_results) == 2

        assert type(result.scan_commands_results[
            ScanCommandForTests.MOCK_COMMAND_1]) == MockPlugin1ScanResult
        assert type(result.scan_commands_results[
            ScanCommandForTests.MOCK_COMMAND_2]) == MockPlugin2ScanResult

        # And the Scanner instance is all done and cleaned up
        assert not scanner._are_server_scans_ongoing
Пример #9
0
    def test_with_extra_arguments(self, mock_scan_commands):
        # Given a server to scan with a scan command
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={ScanCommandForTests.MOCK_COMMAND_1},
            # And the command takes an extra argument
            scan_commands_extra_arguments={
                ScanCommandForTests.MOCK_COMMAND_1:
                MockPlugin1ExtraArguments(extra_field="test")
            },
        )

        # When running the scan
        scanner = Scanner()
        scanner.start_scans([server_scan])

        # It succeeds
        all_results = []
        for result in scanner.get_results():
            all_results.append(result)
        assert len(all_results) == 1

        # And the extra argument was taken into account
        assert all_results[
            0].scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments
Пример #10
0
    def test_exception_when_processing_jobs(self):
        # Given a server to scan
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={
                ScanCommandForTests.MOCK_COMMAND_1,
                # And one of the scan commands will trigger an exception when processing the completed scan jobs
                ScanCommandForTests.
                MOCK_COMMAND_EXCEPTION_WHEN_PROCESSING_JOBS,
            },
        )

        # When queuing the scan
        scanner = Scanner()
        scanner.queue_scan(server_scan)

        # It succeeds
        all_results = []
        for result in scanner.get_results():
            all_results.append(result)

            assert result.server_info == server_scan.server_info
            assert result.scan_commands == server_scan.scan_commands
            assert result.scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments
            assert len(result.scan_commands_results) == 1

            # And the exception was properly caught and returned
            assert len(result.scan_commands_errors) == 1
            error = result.scan_commands_errors[
                ScanCommandForTests.
                MOCK_COMMAND_EXCEPTION_WHEN_PROCESSING_JOBS]
            assert ScanCommandErrorReasonEnum.BUG_IN_SSLYZE == error.reason
            assert error.exception_trace

        assert len(all_results) == 1
Пример #11
0
    def test(self, mock_scan_commands):
        # Given a server to scan
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2},
        )

        # When queuing the scan
        scanner = Scanner()
        scanner.queue_scan(server_scan)

        # It succeeds
        all_results = []
        for result in scanner.get_results():
            all_results.append(result)

            # And the right result is returned
            assert result.server_info == server_scan.server_info
            assert result.scan_commands == server_scan.scan_commands
            assert result.scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments
            assert len(result.scan_commands_results) == 2

            assert type(result.scan_commands_results[ScanCommandForTests.MOCK_COMMAND_1]) == MockPlugin1ScanResult
            assert type(result.scan_commands_results[ScanCommandForTests.MOCK_COMMAND_2]) == MockPlugin2ScanResult

        assert len(all_results) == 1
Пример #12
0
    def test_with_extra_arguments(self):
        # Given a server to scan
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={ScanCommandForTests.MOCK_COMMAND_1},
            # With an extra argument for one command
            scan_commands_extra_arguments={
                ScanCommandForTests.MOCK_COMMAND_1:
                MockPlugin1ExtraArguments(extra_field="test")
            },
        )

        # When queuing the scan
        scanner = Scanner()
        scanner.queue_scan(server_scan)

        # It succeeds
        all_results = []
        for result in scanner.get_results():
            all_results.append(result)

            # And the extra argument was taken into account
            assert result.scan_commands_extra_arguments == server_scan.scan_commands_extra_arguments

        assert len(all_results) == 1
Пример #13
0
def main() -> None:
    global global_scanner

    # For py2exe builds
    freeze_support()

    # Handle SIGINT to terminate processes
    signal.signal(signal.SIGINT, sigint_handler)
    start_time = time()

    # Create the command line parser and the list of available options
    sslyze_parser = CommandLineParser(__version__)
    try:
        parsed_command_line = sslyze_parser.parse_command_line()
    except CommandLineParsingError as e:
        print(e.get_error_msg())
        return

    output_hub = OutputHub()
    output_hub.command_line_parsed(parsed_command_line)

    global_scanner = Scanner(
        per_server_concurrent_connections_limit=parsed_command_line.per_server_concurrent_connections_limit,
        concurrent_server_scans_limit=parsed_command_line.concurrent_server_scans_limit,
    )

    # Figure out which hosts are up and fill the task queue with work to do
    connectivity_tester = ServerConnectivityTester()
    with ThreadPoolExecutor(max_workers=10) as thread_pool:
        futures = [
            thread_pool.submit(connectivity_tester.perform, server_location, network_config)
            for server_location, network_config in parsed_command_line.servers_to_scans
        ]
        for completed_future in as_completed(futures):
            try:
                server_connectivity_info = completed_future.result()
                output_hub.server_connectivity_test_succeeded(server_connectivity_info)

                # Send scan commands for this server to the scanner
                scan_request = ServerScanRequest(
                    server_info=server_connectivity_info,
                    scan_commands=parsed_command_line.scan_commands,
                    scan_commands_extra_arguments=parsed_command_line.scan_commands_extra_arguments,
                )
                global_scanner.queue_scan(scan_request)

            except ConnectionToServerFailed as e:
                output_hub.server_connectivity_test_failed(e)

    output_hub.scans_started()

    # Process the results as they come
    for scan_result in global_scanner.get_results():
        output_hub.server_scan_completed(scan_result)

    # All done
    exec_time = time() - start_time
    output_hub.scans_completed(exec_time)
Пример #14
0
def main() -> None:
    # First validate that we can connect to the servers we want to scan
    servers_to_scan = []
    for hostname in ["cloudflare.com", "google.com"]:
        server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443)
        try:
            server_info = ServerConnectivityTester().perform(server_location)
            servers_to_scan.append(server_info)
        except ConnectionToServerFailed as e:
            print(f"Error connecting to {server_location.hostname}:{server_location.port}: {e.error_message}")
            return

    scanner = Scanner()

    # Then queue some scan commands for each server
    for server_info in servers_to_scan:
        server_scan_req = ServerScanRequest(
            server_info=server_info,
            scan_commands={
                ScanCommandEnum.TLS_1_0_CIPHER_SUITES,
                ScanCommandEnum.TLS_1_1_CIPHER_SUITES,
                ScanCommandEnum.TLS_1_2_CIPHER_SUITES,
                ScanCommandEnum.CERTIFICATE_INFO,
                ScanCommandEnum.TLS_COMPRESSION,
            },
        )
        scanner.queue_scan(server_scan_req)

    # Then retrieve the result of the scan commands for each server
    for server_scan_result in scanner.get_results():
        print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:")

        # Scan commands that were run with no errors
        for scan_command, result in server_scan_result.scan_commands_results.items():
            if scan_command in [
                ScanCommandEnum.TLS_1_0_CIPHER_SUITES,
                ScanCommandEnum.TLS_1_1_CIPHER_SUITES,
                ScanCommandEnum.TLS_1_2_CIPHER_SUITES,
            ]:
                typed_result = cast(CipherSuitesScanResult, result)
                print(f"\nAccepted cipher suites for {scan_command.name}:")
                for accepted_cipher_suite in typed_result.accepted_cipher_suites:
                    print(f"* {accepted_cipher_suite.cipher_suite.name}")

            elif scan_command == ScanCommandEnum.CERTIFICATE_INFO:
                typed_result = cast(CertificateInfoScanResult, result)
                print("\nCertificate info:")
                for cert_deployment in typed_result.certificate_deployments:
                    print(f"Leaf certificate: \n{cert_deployment.verified_certificate_chain_as_pem[0]}")

            elif scan_command == ScanCommandEnum.TLS_COMPRESSION:
                typed_result = cast(CompressionScanResult, result)
                print(f"\nCompression / CRIME: {typed_result.supports_compression}")

        # Scan commands that were run with errors
        for scan_command, error in server_scan_result.scan_commands_errors.items():
            print(f"\nError when running {scan_command}:\n{error.exception_trace}")
Пример #15
0
def main() -> None:
    start_time = time()

    # Create the command line parser and the list of available options
    sslyze_parser = CommandLineParser(__version__)
    try:
        # Parse the supplied command line
        parsed_command_line = sslyze_parser.parse_command_line()
    except CommandLineParsingError as e:
        print(e.get_error_msg())
        return

    output_hub = OutputHub()
    output_hub.command_line_parsed(parsed_command_line)

    # Figure out which servers are reachable
    connectivity_tester = ServerConnectivityTester()
    all_server_scan_requests = []
    with ThreadPoolExecutor(max_workers=10) as thread_pool:
        futures = [
            thread_pool.submit(connectivity_tester.perform, server_location, network_config)
            for server_location, network_config in parsed_command_line.servers_to_scans
        ]
        for completed_future in as_completed(futures):
            try:
                server_connectivity_info = completed_future.result()
                output_hub.server_connectivity_test_succeeded(server_connectivity_info)

                # Server is only; add it to the list of servers to scan
                scan_request = ServerScanRequest(
                    server_info=server_connectivity_info,
                    scan_commands=parsed_command_line.scan_commands,
                    scan_commands_extra_arguments=parsed_command_line.scan_commands_extra_arguments,
                )
                all_server_scan_requests.append(scan_request)

            except ConnectionToServerFailed as e:
                output_hub.server_connectivity_test_failed(e)

    # For the servers that are reachable, start the scans
    output_hub.scans_started()
    if all_server_scan_requests:
        sslyze_scanner = Scanner(
            per_server_concurrent_connections_limit=parsed_command_line.per_server_concurrent_connections_limit,
            concurrent_server_scans_limit=parsed_command_line.concurrent_server_scans_limit,
        )
        sslyze_scanner.start_scans(all_server_scan_requests)

        # Process the results as they come
        for scan_result in sslyze_scanner.get_results():
            output_hub.server_scan_completed(scan_result)

    # All done
    exec_time = time() - start_time
    output_hub.scans_completed(exec_time)
Пример #16
0
 def test_with_extra_arguments_but_no_corresponding_scan_command(self):
     # When trying to queue a scan for a server
     with pytest.raises(ValueError):
         ServerScanRequest(
             server_info=ServerConnectivityInfoFactory.create(),
             # With an extra argument for one command
             scan_commands_extra_arguments={
                 ScanCommandForTests.MOCK_COMMAND_1: MockPlugin1ExtraArguments(extra_field="test")
             },
             # But that specific scan command was not queued
             scan_commands={ScanCommandForTests.MOCK_COMMAND_2},
         )
Пример #17
0
    def scan_server(self) -> None:
        """
        This method scan the server's TLS settings and preprocesses
        the results to later compare them to a specific profile.
        """
        if self._server_info is None:
            return

        # run all scans together
        server_scan_req = ServerScanRequest(
            server_info=self._server_info,
            scan_commands=self._ALL_SCAN_COMMANDS,
            scan_commands_extra_arguments=self._scan_commands_extra_args,
        )
        self._scanner.queue_scan(server_scan_req)

        # We take the first result because only one server was queued
        self._server_scan_result = next(
            self._scanner.get_results())  # type: ServerScanResult

        # preprocess scan results
        (
            supported_ciphers,
            supported_protocols,
            supported_key_exchange,
            server_preferred_order,
        ) = self._preprocess_ciphers_and_protocols()
        supported_ecdh_curves = self._preprocess_ecdh_curves()
        certificate_obj = self._preprocess_certificate()
        hsts_header = self._preprocess_hsts_header()

        # Initialize the comparator class to compare
        # the TLS settings to a specific profile later.
        self._comparator = Comparator(
            supported_ciphers,
            supported_protocols,
            supported_key_exchange,
            supported_ecdh_curves,
            server_preferred_order,
            certificate_obj,
            self._cert_expire_warning,
            hsts_header,
        )

        self._validation_errors = self._validate_certificate(certificate_obj)
        self._vulnerability_errors = self._check_vulnerabilities()
Пример #18
0
    def test_error_bug_in_sslyze_when_scheduling_jobs(self, mock_scan_commands):
        # Given a server to scan with some scan commands
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={ScanCommandForTests.MOCK_COMMAND_1, ScanCommandForTests.MOCK_COMMAND_2},
        )

        # And the first scan command will trigger an error when generating scan jobs
        with mock.patch.object(MockPlugin1Implementation, "scan_jobs_for_scan_command", side_effect=RuntimeError):
            # When queuing the scan
            scanner = Scanner()
            scanner.queue_scan(server_scan)

        # It succeeds
        for result in scanner.get_results():
            # And the exception was properly caught and returned
            assert len(result.scan_commands_errors) == 1
            error = result.scan_commands_errors[ScanCommandForTests.MOCK_COMMAND_1]
            assert ScanCommandErrorReasonEnum.BUG_IN_SSLYZE == error.reason
            assert error.exception_trace
Пример #19
0
    def test_enforces_per_server_concurrent_connections_limit(
            self, mock_scan_commands):
        # Given a server to scan with a scan command that requires multiple connections/jobs to the server
        server_scan = ServerScanRequest(
            server_info=ServerConnectivityInfoFactory.create(),
            scan_commands={ScanCommandForTests.MOCK_COMMAND_1},
        )

        # And a scanner configured to only perform one concurrent connection per server scan
        scanner = Scanner(per_server_concurrent_connections_limit=1)

        # And the scan command will notify us when more than one connection is being performed concurrently
        # Test internals: setup plumbing to detect when more than one thread are running at the same time
        # We use a Barrier that waits for 2 concurrent threads, and puts True in a queue if that ever happens
        queue = Queue()

        def flag_concurrent_threads_running():
            # Only called when two threads are running at the same time
            queue.put(True)

        barrier = threading.Barrier(parties=2,
                                    action=flag_concurrent_threads_running,
                                    timeout=1)

        def scan_job_work_function(arg1: str, arg2: int):
            barrier.wait()

        with mock.patch.object(MockPlugin1Implementation,
                               "_scan_job_work_function",
                               scan_job_work_function):
            # When running the scan
            scanner.start_scans([server_scan])

            # It succeeds
            all_results = []
            for result in scanner.get_results():
                all_results.append(result)
            assert len(all_results) == 1

            # And there never was more than one thread (=1 job/connection) running at the same time
            assert queue.empty()
Пример #20
0
def https_check(endpoint):
    """
    Uses sslyze to figure out the reason the endpoint wouldn't verify.
    """

    # remove the https:// from prefix for sslyze
    try:
        hostname = endpoint.url[8:]
        server_location = (
            ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(
                hostname, 443))
        server_tester = ServerConnectivityTester()
        server_info = server_tester.perform(server_location)
        endpoint.live = True
        ip = server_location.ip_address
        if endpoint.ip is None:
            endpoint.ip = ip
        else:
            if endpoint.ip != ip:
                logging.debug(
                    "{}: Endpoint IP is already {}, but requests IP is {}.".
                    format(endpoint.url, endpoint.ip, ip))
        if server_info.tls_probing_result.client_auth_requirement.name == "REQUIRED":
            endpoint.https_client_auth_required = True
            logging.debug("{}: Client Authentication REQUIRED".format(
                endpoint.url))
    except ConnectionToServerFailed as err:
        endpoint.live = False
        endpoint.https_valid = False
        logging.debug("{}: Error in sslyze server connectivity check".format(
            endpoint.url))
        return
    except Exception as err:
        endpoint.unknown_error = True
        logging.debug(
            "{}: Unknown exception in sslyze server connectivity check.".
            format(endpoint.url))
        return

    try:
        cert_plugin_result = None
        command = ScanCommand.CERTIFICATE_INFO
        scanner = Scanner()
        scan_request = ServerScanRequest(server_info=server_info,
                                         scan_commands=[command])
        scanner.queue_scan(scan_request)
        # Retrieve results from generator object
        scan_result = [x for x in scanner.get_results()][0]
        cert_plugin_result = scan_result.scan_commands_results.get(
            "certificate_info", None)
    except Exception as err:
        try:
            if "timed out" in str(err):
                logging.debug(
                    "{}: Retrying sslyze scanner certificate plugin.".format(
                        endpoint.url))
                scanner.queue_scan(scan_request)
                # Retrieve results from generator object
                scan_result = [x for x in scanner.get_results()][0]
                cert_plugin_result = scan_result.scan_commands_results.get(
                    "certificate_info", None)
            else:
                logging.debug(
                    "{}: Unknown exception in sslyze scanner certificate plugin."
                    .format(endpoint.url))
                endpoint.unknown_error = True
                # We could make this False, but there was an error so
                # we don't know
                endpoint.https_valid = None
                return
        except Exception:
            logging.debug(
                "{}: Unknown exception in sslyze scanner certificate plugin.".
                format(endpoint.url))
            endpoint.unknown_error = True
            # We could make this False, but there was an error so we
            # don't know
            endpoint.https_valid = None
            return

    try:
        public_trust = True
        custom_trust = True
        public_not_trusted_string = ""
        if cert_plugin_result is not None:
            validation_results = cert_plugin_result.certificate_deployments[
                0].path_validation_results
        else:
            validation_results = []
        for result in validation_results:
            if result.was_validation_successful:
                # We're assuming that it is trusted to start with
                pass
            else:
                if "Custom" in result.trust_store.name:
                    custom_trust = False
                else:
                    public_trust = False
                    if len(public_not_trusted_string) > 0:
                        public_not_trusted_string += ", "
                    public_not_trusted_string += result.trust_store.name
        if public_trust:
            logging.debug(
                "{}: Publicly trusted by common trust stores.".format(
                    endpoint.url))
        else:
            logging.debug(
                "{}: Not publicly trusted - not trusted by {}.".format(
                    endpoint.url, public_not_trusted_string))
        custom_trust = None
        endpoint.https_public_trusted = public_trust
        endpoint.https_custom_trusted = custom_trust
    except Exception as err:
        # Ignore exception
        logging.debug("{}: Unknown exception examining trust.".format(
            endpoint.url))

    # Default endpoint assessments to False until proven True.
    endpoint.https_expired_cert = False
    endpoint.https_self_signed_cert = False
    endpoint.https_bad_chain = False
    endpoint.https_bad_hostname = False
    endpoint.https_cert_revoked = False

    cert_chain = cert_plugin_result.certificate_deployments[
        0].received_certificate_chain

    # Check for missing SAN (Leaf certificate)
    leaf_cert = cert_chain[0]
    # Extract Subject Alternative Names
    san_list = extract_dns_subject_alternative_names(leaf_cert)
    # If an empty list was return, SAN(s) are missing. Bad hostname
    if isinstance(san_list, list) and len(san_list) == 0:
        endpoint.https_bad_hostname = True

    # If leaf certificate subject does NOT match hostname, bad hostname
    if not cert_plugin_result.certificate_deployments[
            0].leaf_certificate_subject_matches_hostname:
        endpoint.https_bad_hostname = True

    try:
        endpoint.https_cert_revoked = query_crlite(
            leaf_cert.public_bytes(Encoding.PEM))
    except ValueError as e:
        logging.debug(
            f"Error while checking revocation status for {endpoint.url}: {str(e)}"
        )
        endpoint.https_cert_revoked = None

    # Check for leaf certificate expiration/self-signature.
    if leaf_cert.not_valid_after < datetime.datetime.now():
        endpoint.https_expired_cert = True

    # Check to see if the cert is self-signed
    if leaf_cert.issuer is leaf_cert.subject:
        endpoint.https_self_signed_cert = True

    # Check certificate chain
    for cert in cert_chain[1:]:
        # Check for certificate expiration
        if cert.not_valid_after < datetime.datetime.now():
            endpoint.https_bad_chain = True

        # Check to see if the cert is self-signed
        if cert.issuer is (cert.subject or None):
            endpoint.https_bad_chain = True

    try:
        endpoint.https_cert_chain_len = len(
            cert_plugin_result.certificate_deployments[0].
            received_certificate_chain)
        if endpoint.https_self_signed_cert is False and (len(
                cert_plugin_result.certificate_deployments[0].
                received_certificate_chain) < 2):
            # *** TODO check that it is not a bad hostname and that the root cert is trusted before suggesting that it is an intermediate cert issue.
            endpoint.https_missing_intermediate_cert = True
            if cert_plugin_result.verified_certificate_chain is None:
                logging.debug(
                    "{}: Untrusted certificate chain, probably due to missing intermediate certificate."
                    .format(endpoint.url))
                logging.debug(
                    "{}: Only {} certificates in certificate chain received.".
                    format(
                        endpoint.url,
                        cert_plugin_result.received_certificate_chain.__len__(
                        ),
                    ))
        else:
            endpoint.https_missing_intermediate_cert = False
    except Exception:
        logging.debug("Error while determining length of certificate chain")

    # If anything is wrong then https is not valid
    if (endpoint.https_expired_cert or endpoint.https_self_signed_cert
            or endpoint.https_bad_chain or endpoint.https_bad_hostname):
        endpoint.https_valid = False
Пример #21
0
    def run(self):
        try:
            server_info = self.get_server_info()

            highest_tls_supported = str(
                server_info.tls_probing_result.highest_tls_version_supported
            ).split(".")[1]

            tls_supported = self.get_supported_tls(highest_tls_supported)
        except ConnectionToServerFailed as e:
            logging.error(f"Failed to connect to {self.domain}: {e}")
            return {}
        except ServerHostnameCouldNotBeResolved as e:
            logging.error(f"{self.domain} could not be resolved: {e}")
            return {}
        except gaierror as e:
            logging.error(
                f"Could not retrieve address info for {self.domain} {e}")
            return {}

        scanner = Scanner()

        designated_scans = set()

        # Scan for common vulnerabilities, certificate info, elliptic curves
        designated_scans.add(ScanCommand.OPENSSL_CCS_INJECTION)
        designated_scans.add(ScanCommand.HEARTBLEED)
        designated_scans.add(ScanCommand.CERTIFICATE_INFO)
        designated_scans.add(ScanCommand.ELLIPTIC_CURVES)

        # Test supported SSL/TLS
        if "SSL_2_0" in tls_supported:
            designated_scans.add(ScanCommand.SSL_2_0_CIPHER_SUITES)
        elif "SSL_3_0" in tls_supported:
            designated_scans.add(ScanCommand.SSL_3_0_CIPHER_SUITES)
        elif "TLS_1_0" in tls_supported:
            designated_scans.add(ScanCommand.TLS_1_0_CIPHER_SUITES)
        elif "TLS_1_1" in tls_supported:
            designated_scans.add(ScanCommand.TLS_1_1_CIPHER_SUITES)
        elif "TLS_1_2" in tls_supported:
            designated_scans.add(ScanCommand.TLS_1_2_CIPHER_SUITES)
        elif "TLS_1_3" in tls_supported:
            designated_scans.add(ScanCommand.TLS_1_3_CIPHER_SUITES)

        scan_request = ServerScanRequest(server_info=server_info,
                                         scan_commands=designated_scans)

        scanner.start_scans([scan_request])

        # Wait for asynchronous scans to complete
        # get_results() returns a generator with a single "ServerScanResult". We only want that object
        scan_results = [x for x in scanner.get_results()][0]
        logging.info("Scan results retrieved from generator")

        res = {
            "TLS": {
                "supported": tls_supported,
                "accepted_cipher_list": [],
                "rejected_cipher_list": [],
            }
        }

        # Parse scan results for required info
        for name, result in scan_results.scan_commands_results.items():

            # If CipherSuitesScanResults
            if name.endswith("suites"):
                logging.info("Parsing Cipher Suite Scan results...")

                for c in result.accepted_cipher_suites:
                    res["TLS"]["accepted_cipher_list"].append(
                        c.cipher_suite.name)

                for c in result.rejected_cipher_suites:
                    res["TLS"]["rejected_cipher_list"].append(
                        c.cipher_suite.name)

            elif name == "openssl_ccs_injection":
                logging.info(
                    "Parsing OpenSSL CCS Injection Vulnerability Scan results..."
                )
                res["is_vulnerable_to_ccs_injection"] = result.is_vulnerable_to_ccs_injection

            elif name == "heartbleed":
                logging.info(
                    "Parsing Heartbleed Vulnerability Scan results...")
                res["is_vulnerable_to_heartbleed"] = result.is_vulnerable_to_heartbleed

            elif name == "certificate_info":
                logging.info("Parsing Certificate Info Scan results...")
                try:
                    res["signature_algorithm"] = (
                        result.certificate_deployments[0].
                        verified_certificate_chain[0].signature_hash_algorithm.
                        __class__.__name__)
                except TypeError:
                    res["signature_algorithm"] = None

            else:
                logging.info("Parsing Elliptic Curve Scan results...")
                res["supports_ecdh_key_exchange"] = result.supports_ecdh_key_exchange
                res["supported_curves"] = []
                if result.supported_curves is not None:
                    for curve in result.supported_curves:
                        # sslyze returns ANSI curve names occaisionally
                        # In at least these two cases we can simply convert to
                        # using the equivalent SECG name, so that this aligns
                        # with CCCS guidance:
                        # https://datatracker.ietf.org/doc/html/rfc4492#appendix-A
                        if curve.name == "prime192v1":
                            res["supported_curves"].append("secp192r1")
                        elif curve.name == "prime256v1":
                            res["supported_curves"].append("secp256r1")
                        else:
                            res["supported_curves"].append(curve.name)

        return res