Running a Scan in Python

Overview

SSLyze’s Python API can be used to run scans and process results in an automated fashion.

Every SSLyze class has typing annotations, which allows IDEs such as VS Code and PyCharms to auto-import modules and auto-complete field names. Make sure to leverage this typing information as it will make it significantly easier to use SSLyze’s Python API.

To run a scan against a server, the scan can be described via the ServerScanRequest class, which contains information about the server to scan(hostname, port, etc.):

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

More details can optionally be supplied to the ServerScanRequest, including:

  • Server settings via the server_location argument, for example to use an HTTP proxy, or scan a specific IP address.

  • Network settings via the network_configuration argument, for example to configure a client certificate, or scan a non-HTTP server.

  • A specific of specific TLS checks to run (Heartbleed, cipher suites, etc.), via the scan_commands argument. By default, all the checks will be enabled.

Every type of TLS check that SSLyze can run against a server (supported cipher suites, Heartbleed, etc.) is represented by a ScanCommand. Once a ScanCommand is run against a server, it returns a “result” object with attributes containing the results of the scan command.

All the available ScanCommands and corresponding results are described in Appendix: Scan Commands.

Then, to start the scan, pass the list of ServerScanRequest to Scanner.queue_scans():

scanner = Scanner()
scanner.queue_scans(all_scan_requests)

The Scanner class, uses a pool of workers to run the scans concurrently, but without DOS-ing the servers.

Lastly, the results can be retrieved using the Scanner.get_results() method, which returns an iterable of ServerScanResult. Each result is returned as soon as the server scan was completed:

for server_scan_result in scanner.get_results():
    print(f"\n\n****Results for {server_scan_result.server_location.hostname}****")

Full Example

A full example of running a scan on a couple servers follow:

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})"
                )

Classes for Starting a Scan

Additional settings: StartTLS, SNI, etc.

Enabling SSL/TLS client authentication

Classes for Processing Scan Results