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:
    print("=> Starting the scans")
    date_scans_started = datetime.utcnow()

    # 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
    all_server_scan_results = []
    for server_scan_result in scanner.get_results():
        all_server_scan_results.append(server_scan_result)
        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})"
                )

        # etc... Other scan command results to process are in server_scan_result.scan_result

    # Lastly, save the all the results to a JSON file
    json_file_out = Path("api_sample_results.json")
    print(f"\n\n=> Saving scan results to {json_file_out}")
    example_json_result_output(json_file_out, all_server_scan_results, date_scans_started, datetime.utcnow())

    # And ensure we are able to parse them
    print(f"\n\n=> Parsing scan results from {json_file_out}")
    example_json_result_parsing(json_file_out)

Classes for Starting a Scan

class sslyze.ServerScanRequest(server_location, network_configuration=None, scan_commands=<factory>, scan_commands_extra_arguments=<factory>, uuid=<factory>)

A request to scan a specific server.

Parameters:
  • server_location (ServerNetworkLocation) – The server to scan.

  • network_configuration (ServerNetworkConfiguration) – An optional network configuration. If not supplied, a default configuration will be used.

  • scan_commands (Set[ScanCommand]) – An optional list of scan commands to run against the server. If not supplied, all available scan commands will be run.

  • scan_commands_extra_arguments (ScanCommandsExtraArguments) – An optional list of extra arguments specific to some scan commands. If not supplied, no extra arguments will be set.

  • uuid (UUID) –

class sslyze.ServerNetworkLocation(hostname, port=443, ip_address=None, http_proxy_settings=None)

All the information needed to connect to a server.

hostname

The server’s hostname.

port

The server’s TLS port number.

connection_type

How sslyze should connect to the server: either directly, or via an HTTP proxy.

ip_address

The server’s IP address; only set if sslyze is connecting directly to the server. If no IP address is supplied and connection_type is set to DIRECT, sslyze will automatically lookup one IP address for the supplied hostname.

http_proxy_settings

The HTTP proxy configuration to use in order to tunnel the scans through a proxy; only set if sslyze is connecting to the server via an HTTP proxy. The proxy will be responsible for looking up the server’s IP address and connecting to it.

Parameters:
  • hostname (str) –

  • port (int) –

  • ip_address (Optional[str]) –

  • http_proxy_settings (Optional[HttpProxySettings]) –

class sslyze.Scanner(per_server_concurrent_connections_limit=None, concurrent_server_scans_limit=None, observers=None)
Parameters:
  • per_server_concurrent_connections_limit (Optional[int]) –

  • concurrent_server_scans_limit (Optional[int]) –

  • observers (Optional[Sequence[ScannerObserver]]) –

Additional settings: StartTLS, SNI, etc.

class sslyze.ServerNetworkConfiguration(tls_server_name_indication, tls_opportunistic_encryption=None, tls_client_auth_credentials=None, xmpp_to_hostname=None, network_timeout=5, network_max_retries=3)

Additional network settings to provide fine-grained control on how to connect to a specific server.

tls_server_name_indication

The hostname to set within the Server Name Indication TLS extension.

tls_wrapped_protocol

The protocol wrapped in TLS that the server expects. It allows SSLyze to figure out how to establish a (Start)TLS connection to the server and what kind of “hello” message (SMTP, XMPP, etc.) to send to the server after the handshake was completed. If not supplied, standard TLS will be used.

tls_client_auth_credentials

The client certificate and private key needed to perform mutual authentication with the server. If not supplied, SSLyze will attempt to connect to the server without performing client authentication.

xmpp_to_hostname

The hostname to set within the to attribute of the XMPP stream. If not supplied, the server’s hostname will be used. Should only be set if the supplied tls_wrapped_protocol is an XMPP protocol.

network_timeout

The timeout (in seconds) to be used when attempting to establish a connection to the server.

network_max_retries

The number of retries SSLyze will perform when attempting to establish a connection to the server.

Parameters:
class sslyze.ProtocolWithOpportunisticTlsEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

The list of plaintext protocols supported by SSLyze for opportunistic TLS upgrade (such as STARTTLS).

This allows SSLyze to figure out how to complete an SSL/TLS handshake with the server.

SMTP = 'SMTP'
XMPP = 'XMPP'
XMPP_SERVER = 'XMPP_SERVER'
FTP = 'FTP'
POP3 = 'POP3'
LDAP = 'LDAP'
IMAP = 'IMAP'
RDP = 'RDP'
POSTGRES = 'POSTGRES'
classmethod from_default_port(port)

Given a port number, return the protocol that uses this port number by default.

Parameters:

port (int) –

Return type:

Optional[ProtocolWithOpportunisticTlsEnum]

Enabling SSL/TLS client authentication

class sslyze.ClientAuthenticationCredentials(certificate_chain_path, key_path, key_password='', key_type=OpenSslFileTypeEnum.PEM)

Everything needed by a client to perform SSL/TLS client authentication with the server.

certificate_chain_path

Path to the file containing the client’s certificate.

key_path

Path to the file containing the client’s private key.

key_password

The password to decrypt the private key.

key_type

The format of the key file.

Parameters:
  • certificate_chain_path (Path) –

  • key_path (Path) –

  • key_password (str) –

  • key_type (OpenSslFileTypeEnum) –

class sslyze.OpenSslFileTypeEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)

Certificate and private key format constants which map to the SSL_FILETYPE_XXX OpenSSL constants.

PEM = 1
ASN1 = 2

Classes for Processing Scan Results

class sslyze.ServerScanResult(uuid, server_location, network_configuration, connectivity_status, connectivity_error_trace, connectivity_result, scan_status, scan_result)

The result of scanning a server.

uuid
server_location
network_configuration
connectivity_status

Whether SSLyze was able to connect to the server, or not.

connectivity_error_trace

The connectivity error; only set if SSLyze was NOT able to connect to the server.

connectivity_result

The result of connectivity testing; only set if SSLyze was able to connect to the server.

scan_status

Whether SSLyze was able to complete the scan, or not.

scan_result

The result of the scan; only set if SSLyze was able to complete the scan.

Parameters:
class sslyze.ServerConnectivityStatusEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
class sslyze.ServerScanStatusEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
class sslyze.ServerTlsProbingResult(highest_tls_version_supported, cipher_suite_supported, client_auth_requirement, supports_ecdh_key_exchange)

Additional details about the server, detected via connectivity testing.

Parameters:
  • highest_tls_version_supported (TlsVersionEnum) –

  • cipher_suite_supported (str) –

  • client_auth_requirement (ClientAuthRequirementEnum) –

  • supports_ecdh_key_exchange (bool) –

class sslyze.AllScanCommandsAttempts(certificate_info, ssl_2_0_cipher_suites, ssl_3_0_cipher_suites, tls_1_0_cipher_suites, tls_1_1_cipher_suites, tls_1_2_cipher_suites, tls_1_3_cipher_suites, tls_compression, tls_1_3_early_data, openssl_ccs_injection, tls_fallback_scsv, heartbleed, robot, session_renegotiation, session_resumption, elliptic_curves, http_headers)

The result of every scan command supported by SSLyze.

Parameters:
  • certificate_info (CertificateInfoScanAttempt) –

  • ssl_2_0_cipher_suites (CipherSuitesScanAttempt) –

  • ssl_3_0_cipher_suites (CipherSuitesScanAttempt) –

  • tls_1_0_cipher_suites (CipherSuitesScanAttempt) –

  • tls_1_1_cipher_suites (CipherSuitesScanAttempt) –

  • tls_1_2_cipher_suites (CipherSuitesScanAttempt) –

  • tls_1_3_cipher_suites (CipherSuitesScanAttempt) –

  • tls_compression (CompressionScanAttempt) –

  • tls_1_3_early_data (EarlyDataScanAttempt) –

  • openssl_ccs_injection (OpenSslCcsInjectionScanAttempt) –

  • tls_fallback_scsv (FallbackScsvScanAttempt) –

  • heartbleed (HeartbleedScanAttempt) –

  • robot (RobotScanAttempt) –

  • session_renegotiation (SessionRenegotiationScanAttempt) –

  • session_resumption (SessionResumptionSupportScanAttempt) –

  • elliptic_curves (SupportedEllipticCurvesScanAttempt) –

  • http_headers (HttpHeadersScanAttempt) –

class sslyze.ScanCommandAttempt(status, error_reason, error_trace, result)

The result of a single scan command.

status

Whether this specific scan command was ran successfully.

error_reason

The reason why the scan command failed; None if the scan command succeeded.

error_trace

The exception trace of when the scan command failed; None if the scan command succeeded.

result

The actual result of the scan command; None if the scan command failed. The type of this attribute is the “ScanResult” object corresponding to the scan command.

Parameters:
  • status (ScanCommandAttemptStatusEnum) –

  • error_reason (Optional[ScanCommandErrorReasonEnum]) –

  • error_trace (Optional[TracebackException]) –

  • result (Optional[TypeVar(_ScanCommandResultTypeVar)]) –

class sslyze.ScanCommandErrorReasonEnum(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
BUG_IN_SSLYZE = 'BUG_IN_SSLYZE'
CLIENT_CERTIFICATE_NEEDED = 'CLIENT_CERTIFICATE_NEEDED'
CONNECTIVITY_ISSUE = 'CONNECTIVITY_ISSUE'
WRONG_USAGE = 'WRONG_USAGE'