예제 #1
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
예제 #2
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
예제 #3
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 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 error was properly returned
            error = all_results[0].scan_commands_errors[
                ScanCommand.HTTP_HEADERS]
            assert error.reason == ScanCommandErrorReasonEnum.CLIENT_CERTIFICATE_NEEDED
예제 #4
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
예제 #5
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)
예제 #6
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()
예제 #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 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 error 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.CONNECTIVITY_ISSUE == error.reason
            assert error.exception_trace
예제 #8
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