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.now(timezone.utc)
# 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.now(timezone.utc))
# 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)