def test_badssl_compliant_with_modern(self):
        # Given the scan results for a server that is compliant with the "modern" Mozilla config
        scanner = Scanner()
        scanner.queue_scans(
            [ServerScanRequest(server_location=ServerNetworkLocation(hostname="mozilla-modern.badssl.com"))]
        )
        server_scan_result = next(scanner.get_results())

        # When checking if the server is compliant with the Mozilla "modern" TLS config
        # It succeeds and the server is returned as compliant
        checker = MozillaTlsConfigurationChecker.get_default()
        checker.check_server(
            against_config=MozillaTlsConfigurationEnum.MODERN,
            server_scan_result=server_scan_result,
        )

        # And the server is returned as NOT compliant for the other Mozilla configs
        for mozilla_config in [MozillaTlsConfigurationEnum.OLD, MozillaTlsConfigurationEnum.INTERMEDIATE]:
            with pytest.raises(ServerNotCompliantWithMozillaTlsConfiguration):
                checker.check_server(against_config=mozilla_config, server_scan_result=server_scan_result)
def server_scan_result_for_google():
    scanner = Scanner()
    scanner.queue_scans([ServerScanRequest(server_location=ServerNetworkLocation(hostname="google.com"))])
    for server_scan_result in scanner.get_results():
        yield server_scan_result
Ejemplo n.º 3
0
def main() -> None:
    # First create the scan requests for each server that we want to scan
    try:
        all_scan_requests = [
            ServerScanRequest(server_location=ServerNetworkLocation(hostname="cloudflare.com")),
            ServerScanRequest(server_location=ServerNetworkLocation(hostname="google.com")),
        ]
    except ServerHostnameCouldNotBeResolved:
        # Handle bad input ie. invalid hostnames
        print("Error resolving the supplied hostnames")
        return

    # Then queue all the scans
    scanner = Scanner()
    scanner.queue_scans(all_scan_requests)

    # And retrieve and process the results for each server
    for server_scan_result in scanner.get_results():
        print(f"\n\n****Results for {server_scan_result.server_location.hostname}****")

        # Were we able to connect to the server and run the scan?
        if server_scan_result.scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY:
            # No we weren't
            print(
                f"\nError: Could not connect to {server_scan_result.server_location.hostname}:"
                f" {server_scan_result.connectivity_error_trace}"
            )
            continue

        # Since we were able to run the scan, scan_result is populated
        assert server_scan_result.scan_result

        # Process the result of the SSL 2.0 scan command
        ssl2_attempt = server_scan_result.scan_result.ssl_2_0_cipher_suites
        if ssl2_attempt.status == ScanCommandAttemptStatusEnum.ERROR:
            # An error happened when this scan command was run
            _print_failed_scan_command_attempt(ssl2_attempt)
        elif ssl2_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED:
            # This scan command was run successfully
            ssl2_result = ssl2_attempt.result
            assert ssl2_result
            print("\nAccepted cipher suites for SSL 2.0:")
            for accepted_cipher_suite in ssl2_result.accepted_cipher_suites:
                print(f"* {accepted_cipher_suite.cipher_suite.name}")

        # Process the result of the TLS 1.3 scan command
        tls1_3_attempt = server_scan_result.scan_result.tls_1_3_cipher_suites
        if tls1_3_attempt.status == ScanCommandAttemptStatusEnum.ERROR:
            _print_failed_scan_command_attempt(ssl2_attempt)
        elif tls1_3_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED:
            tls1_3_result = tls1_3_attempt.result
            assert tls1_3_result
            print("\nAccepted cipher suites for TLS 1.3:")
            for accepted_cipher_suite in tls1_3_result.accepted_cipher_suites:
                print(f"* {accepted_cipher_suite.cipher_suite.name}")

        # Process the result of the certificate info scan command
        certinfo_attempt = server_scan_result.scan_result.certificate_info
        if certinfo_attempt.status == ScanCommandAttemptStatusEnum.ERROR:
            _print_failed_scan_command_attempt(certinfo_attempt)
        elif certinfo_attempt.status == ScanCommandAttemptStatusEnum.COMPLETED:
            certinfo_result = certinfo_attempt.result
            assert certinfo_result
            print("\nLeaf certificates deployed:")
            for cert_deployment in certinfo_result.certificate_deployments:
                leaf_cert = cert_deployment.received_certificate_chain[0]
                print(
                    f"{leaf_cert.public_key().__class__.__name__}: {leaf_cert.subject.rfc4514_string()}"
                    f" (Serial: {leaf_cert.serial_number})"
                )
Ejemplo n.º 4
0
def main() -> None:
    # Parse the supplied command line
    date_scans_started = datetime.utcnow()
    sslyze_parser = CommandLineParser(__version__)
    try:
        parsed_command_line = sslyze_parser.parse_command_line()
    except CommandLineParsingError as e:
        print(e.get_error_msg())
        return

    # Setup the observer to print to the console, if needed
    scanner_observers = []
    if not parsed_command_line.should_disable_console_output:
        observer_for_console_output = ObserverToGenerateConsoleOutput(
            file_to=sys.stdout, json_path_out=parsed_command_line.json_path_out
        )
        observer_for_console_output.command_line_parsed(parsed_command_line=parsed_command_line)

        scanner_observers.append(observer_for_console_output)

    # Setup the scanner
    sslyze_scanner = Scanner(
        per_server_concurrent_connections_limit=parsed_command_line.per_server_concurrent_connections_limit,
        concurrent_server_scans_limit=parsed_command_line.concurrent_server_scans_limit,
        observers=scanner_observers,
    )

    # Queue the scans
    all_server_scan_requests = []
    for server_location, network_config in parsed_command_line.servers_to_scans:
        scan_request = ServerScanRequest(
            server_location=server_location,
            network_configuration=network_config,
            scan_commands=parsed_command_line.scan_commands,
            scan_commands_extra_arguments=parsed_command_line.scan_commands_extra_arguments,
        )
        all_server_scan_requests.append(scan_request)

    # If there are servers that we were able to resolve, scan them
    all_server_scan_results = []
    if all_server_scan_requests:
        sslyze_scanner.queue_scans(all_server_scan_requests)
        for result in sslyze_scanner.get_results():
            # Results are actually displayed by the observer; here we just store them
            all_server_scan_results.append(result)

    # Write results to a JSON file if needed
    json_file_out: Optional[TextIO] = None
    if parsed_command_line.should_print_json_to_console:
        json_file_out = sys.stdout
    elif parsed_command_line.json_path_out:
        json_file_out = parsed_command_line.json_path_out.open("wt", encoding="utf-8")

    if json_file_out:
        json_output = SslyzeOutputAsJson(
            server_scan_results=[ServerScanResultAsJson.from_orm(result) for result in all_server_scan_results],
            invalid_server_strings=[
                InvalidServerStringAsJson.from_orm(bad_server) for bad_server in parsed_command_line.invalid_servers
            ],
            date_scans_started=date_scans_started,
            date_scans_completed=datetime.utcnow(),
        )
        json_output_as_str = json_output.json(sort_keys=True, indent=4, ensure_ascii=True)
        json_file_out.write(json_output_as_str)

    # If we printed the JSON results to the console, don't run the Mozilla compliance check so we return valid JSON
    if parsed_command_line.should_print_json_to_console:
        sys.exit(0)

    if not all_server_scan_results:
        # There are no results to present: all supplied server strings were invalid?
        sys.exit(0)

    # Check the results against the Mozilla config if needed
    are_all_servers_compliant = True
    # TODO(AD): Expose format_title method
    title = ObserverToGenerateConsoleOutput._format_title("Compliance against Mozilla TLS configuration")
    print()
    print(title)
    if not parsed_command_line.check_against_mozilla_config:
        print("    Disabled; use --mozilla_config={old, intermediate, modern}.\n")
    else:

        print(
            f'    Checking results against Mozilla\'s "{parsed_command_line.check_against_mozilla_config}"'
            f" configuration. See https://ssl-config.mozilla.org/ for more details.\n"
        )
        mozilla_checker = MozillaTlsConfigurationChecker.get_default()
        for server_scan_result in all_server_scan_results:
            try:
                mozilla_checker.check_server(
                    against_config=parsed_command_line.check_against_mozilla_config,
                    server_scan_result=server_scan_result,
                )
                print(f"    {server_scan_result.server_location.display_string}: OK - Compliant.\n")

            except ServerNotCompliantWithMozillaTlsConfiguration as e:
                are_all_servers_compliant = False
                print(f"    {server_scan_result.server_location.display_string}: FAILED - Not compliant.")
                for criteria, error_description in e.issues.items():
                    print(f"        * {criteria}: {error_description}")
                print()

            except ServerScanResultIncomplete:
                are_all_servers_compliant = False
                print(
                    f"    {server_scan_result.server_location.display_string}: ERROR - Scan did not run successfully;"
                    f" review the scan logs above."
                )

    if not are_all_servers_compliant:
        # Return a non-zero error code to signal failure (for example to fail a CI/CD pipeline)
        sys.exit(1)
Ejemplo n.º 5
0
def analyze(hostname: str, port: int) -> List[Tuple[int, str]]:
    results = []
    # Define the server that you want to scan
    try:
        server_location = ServerNetworkLocation(hostname, port)
    except ServerHostnameCouldNotBeResolved:
        log_red(_("Could not resolve {0}"), hostname)
        return results

    # Then queue some scan commands for the server
    scanner = Scanner()
    server_scan_req = ServerScanRequest(
        server_location=server_location,
        scan_commands={
            ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES,
            ScanCommand.SSL_3_0_CIPHER_SUITES,
            ScanCommand.TLS_1_0_CIPHER_SUITES,
            ScanCommand.TLS_1_1_CIPHER_SUITES,
            ScanCommand.TLS_1_2_CIPHER_SUITES,
            ScanCommand.TLS_1_3_CIPHER_SUITES, ScanCommand.ROBOT,
            ScanCommand.HEARTBLEED, ScanCommand.TLS_COMPRESSION,
            ScanCommand.TLS_FALLBACK_SCSV, ScanCommand.TLS_1_3_EARLY_DATA,
            ScanCommand.OPENSSL_CCS_INJECTION,
            ScanCommand.SESSION_RENEGOTIATION, ScanCommand.HTTP_HEADERS
        },
        network_configuration=ServerNetworkConfiguration(
            tls_server_name_indication=server_location.hostname,
            network_timeout=5,
            network_max_retries=2))
    scanner.queue_scans([server_scan_req])

    # TLS 1.2 / 1.3 results
    good_protocols = {
        ScanCommand.TLS_1_2_CIPHER_SUITES: "TLS v1.2",
        ScanCommand.TLS_1_3_CIPHER_SUITES: "TLS v1.3"
    }

    # https://blog.mozilla.org/security/2014/10/14/the-poodle-attack-and-the-end-of-ssl-3-0/
    # https://blog.qualys.com/product-tech/2018/11/19/grade-change-for-tls-1-0-and-tls-1-1-protocols
    bad_protocols = {
        ScanCommand.SSL_2_0_CIPHER_SUITES: "SSL v2",
        ScanCommand.SSL_3_0_CIPHER_SUITES: "SSL v3",
        ScanCommand.TLS_1_0_CIPHER_SUITES: "TLS v1.0",
        ScanCommand.TLS_1_1_CIPHER_SUITES: "TLS v1.1"
    }

    # Then retrieve the results
    for result in scanner.get_results():
        log_blue("\n" + _("Results for") +
                 f" {result.server_location.hostname}:")
        deprecated_protocols = []

        if result.connectivity_error_trace:
            # Stuff like connection timeout
            log_red(result.connectivity_error_trace)
            continue

        for scan_command in result.scan_result.__annotations__:
            scan_results = getattr(result.scan_result, scan_command)

            if scan_results.error_reason:
                log_red(scan_results.error_reason)
                continue

            if scan_results.status != ScanCommandAttemptStatusEnum.COMPLETED:
                continue

            if scan_command == ScanCommand.CERTIFICATE_INFO:
                for level, message in process_certificate_info(
                        scan_results.result):
                    results.append((level, message))
            elif scan_command in bad_protocols:
                if scan_results.result.accepted_cipher_suites:
                    deprecated_protocols.append(bad_protocols[scan_command])
            elif scan_command == ScanCommand.ROBOT:
                if scan_results.result.robot_result in (
                        RobotScanResultEnum.VULNERABLE_WEAK_ORACLE,
                        RobotScanResultEnum.VULNERABLE_STRONG_ORACLE):
                    message = _("Server is vulnerable to ROBOT attack")
                    log_red(message)
                    results.append((CRITICAL_LEVEL, message))
            elif scan_command == ScanCommand.HEARTBLEED:
                if scan_results.result.is_vulnerable_to_heartbleed:
                    message = _("Server is vulnerable to Heartbleed attack")
                    log_red(message)
                    results.append((CRITICAL_LEVEL, message))
            elif scan_command == ScanCommand.TLS_COMPRESSION:
                if scan_results.result.supports_compression:
                    message = _(
                        "Server is vulnerable to CRIME attack (compression is supported)"
                    )
                    log_red(message)
                    results.append((CRITICAL_LEVEL, message))
            elif scan_command == ScanCommand.TLS_FALLBACK_SCSV:
                if not scan_results.result.supports_fallback_scsv:
                    message = _(
                        "Server is vulnerable to downgrade attacks (support for TLS_FALLBACK_SCSV is missing)"
                    )
                    log_red(message)
                    results.append((CRITICAL_LEVEL, message))
            elif scan_command == ScanCommand.TLS_1_3_EARLY_DATA:
                # https://blog.trailofbits.com/2019/03/25/what-application-developers-need-to-know-about-tls-early-data-0rtt/
                if scan_results.result.supports_early_data:
                    message = _(
                        "TLS 1.3 Early Data (0RTT) is vulnerable to replay attacks"
                    )
                    log_orange(message)
                    results.append((MEDIUM_LEVEL, message))
            elif scan_command == ScanCommand.OPENSSL_CCS_INJECTION:
                if scan_results.result.is_vulnerable_to_ccs_injection:
                    message = _(
                        "Server is vulnerable to OpenSSL CCS (CVE-2014-0224)")
                    log_red(message)
                    results.append((CRITICAL_LEVEL, message))
            elif scan_command == ScanCommand.SESSION_RENEGOTIATION:
                if scan_results.result.is_vulnerable_to_client_renegotiation_dos:
                    message = _(
                        "Server honors client-initiated renegotiations (vulnerable to DoS attacks)"
                    )
                    log_red(message)
                    results.append((HIGH_LEVEL, message))
                if not scan_results.result.supports_secure_renegotiation:
                    message = _("Server doesn't support secure renegotiations")
                    log_orange(message)
                    results.append((MEDIUM_LEVEL, message))
            elif scan_command == ScanCommand.HTTP_HEADERS:
                if scan_results.result.strict_transport_security_header is None:
                    message = _("Strict Transport Security (HSTS) is not set")
                    log_red(message)
                    results.append((HIGH_LEVEL, message))
            elif scan_command in good_protocols:
                for level, message in process_cipher_suites(
                        scan_results.result, good_protocols[scan_command]):
                    results.append((level, message))

        if deprecated_protocols:
            message = _("The following protocols are deprecated and/or insecure and should be deactivated:") + \
                      " " + ", ".join(deprecated_protocols)
            log_red(message)
            results.append((CRITICAL_LEVEL, message))

    return results
Ejemplo n.º 6
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.")