Skip to content

Commit

Permalink
Merge pull request #528 from nabla-c0d3/dev-5.0.0
Browse files Browse the repository at this point in the history
Dev 5.0.0
  • Loading branch information
nabla-c0d3 committed Nov 26, 2021
2 parents 02ede71 + 4c17544 commit fbfc52d
Show file tree
Hide file tree
Showing 196 changed files with 46,648 additions and 51,216 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,13 @@ jobs:

- name: Install pip
run: |
python -m pip install --upgrade pip setuptools
python -m pip install --upgrade pip setuptools wheel
- name: Install sslyze dependencies
run: python -m pip install -e .

- name: Install dev dependencies
run: python -m pip install -r requirements.txt
run: python -m pip install -r dev-requirements.txt

- name: Run linters
# Only do linting once
Expand Down
89 changes: 69 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,89 @@ SSLyze
[![PyPI version](https://img.shields.io/pypi/v/sslyze.svg)](https://pypi.org/project/sslyze/)
[![Python version](https://img.shields.io/pypi/pyversions/sslyze.svg)](https://pypi.org/project/sslyze/)

SSLyze is a fast and powerful SSL/TLS scanning library.
SSLyze is a fast and powerful SSL/TLS scanning tool and Python library.

It allows you to analyze the SSL/TLS configuration of a server by connecting to it, in order to detect various
issues (bad certificate, weak cipher suites, Heartbleed, ROBOT, TLS 1.3 support, etc.).

SSLyze can either be used as a command line tool or as a Python library.
SSLyze can analyze the SSL/TLS configuration of a server by connecting to it, in order to ensure that it uses strong
encryption settings (certificate, cipher suites, elliptic curves, etc.), and that it is not vulnerable to known TLS
attacks (Heartbleed, ROBOT, OpenSSL CCS injection, etc.).

Key features
------------

* Fully [documented Python API](https://nabla-c0d3.github.io/sslyze/documentation/), in order to run scans and process
the results directly from Python.
* Support for TLS 1.3 and early data (0-RTT) testing.
* Scans are automatically dispatched among multiple workers, making them very fast.
* Performance testing: session resumption and TLS tickets support.
* Security testing: weak cipher suites, supported curves, ROBOT, Heartbleed and more.
* Server certificate validation and revocation checking through OCSP stapling.
* Support for StartTLS handshakes on SMTP, XMPP, LDAP, POP, IMAP, RDP, PostGres and FTP.
* Scan results can be written to a JSON file for further processing.
* Focus on speed and reliability: SSLyze is a battle-tested tool that is used to reliably scan **hundreds of thousands**
of servers every day.
* Easy to operationalize: SSLyze can be directly run from CI/CD, in order to continuously check a server against
Mozilla's recommended TLS configuration.
* Fully documented [Python API](https://nabla-c0d3.github.io/sslyze/documentation/) to run scans directly from any
Python application, such as a function deployed to AWS Lambda.
* Support for scanning non-HTTP servers including SMTP, XMPP, LDAP, POP, IMAP, RDP, Postgres and FTP servers.
* Results of a scan can easily be saved to a JSON file for later processing.
* And much more!

Quick start
-----------

SSLyze can be installed directly via pip:

$ pip install --upgrade setuptools pip
$ pip install --upgrade sslyze
$ python -m sslyze www.yahoo.com www.google.com "[2607:f8b0:400a:807::2004]:443"
```
$ pip install --upgrade pip setuptools wheel
$ pip install --upgrade sslyze
$ python -m sslyze www.yahoo.com www.google.com "[2607:f8b0:400a:807::2004]:443"
```

Usage as a CI/CD step
---------------------

By default, SSLyze will check the server's scan results against Mozilla's recommended ["intermediate" TLS
configuration](https://wiki.mozilla.org/Security/Server_Side_TLS), and will return a non-zero exit code if the server
is not compliant.

```
$ python -m sslyze mozilla.com
```
```
Checking results against Mozilla's "intermediate" configuration. See https://ssl-config.mozilla.org/ for more details.
mozilla.com:443: OK - Compliant.
```

The Mozilla configuration to check against can be configured via `--mozilla-config={old, intermediate, modern}`:
```
$ python -m sslyze --mozilla-config=modern mozilla.com
```
```
Checking results against Mozilla's "modern" configuration. See https://ssl-config.mozilla.org/ for more details.
mozilla.com:443: FAILED - Not compliant.
* certificate_types: Deployed certificate types are {'rsa'}, should have at least one of {'ecdsa'}.
* certificate_signatures: Deployed certificate signatures are {'sha256WithRSAEncryption'}, should have at least one of {'ecdsa-with-SHA512', 'ecdsa-with-SHA256', 'ecdsa-with-SHA384'}.
* tls_versions: TLS versions {'TLSv1.2'} are supported, but should be rejected.
* ciphers: Cipher suites {'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256', 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'} are supported, but should be rejected.
```

This can be used to easily run an SSLyze scan as a CI/CD step.

Development environment
-----------------------

To setup a development environment:

```
$ pip install --upgrade pip setuptools wheel
$ pip install -e .
$ pip install -r dev-requirements.txt
```

The tests can then be run using:

```
$ invoke test
```

Documentation
-------------
API Documentation
-----------------

Documentation is [available here][documentation].
Documentation for SSLyze's Python API is [available here][documentation].

License
-------
Expand Down
197 changes: 106 additions & 91 deletions api_sample.py
Original file line number Diff line number Diff line change
@@ -1,111 +1,126 @@
from pathlib import Path

from sslyze import (
ServerNetworkLocationViaDirectConnection,
ServerConnectivityTester,
Scanner,
ServerScanRequest,
ScanCommand,
SslyzeOutputAsJson,
ServerNetworkLocation,
ScanCommandAttemptStatusEnum,
ServerScanStatusEnum,
)
from sslyze.errors import ConnectionToServerFailed
from sslyze.errors import ServerHostnameCouldNotBeResolved
from sslyze.scanner.scan_command_attempt import ScanCommandAttempt


def _print_failed_scan_command_attempt(scan_command_attempt: ScanCommandAttempt) -> None:
print(
f"\nError when running ssl_2_0_cipher_suites: {scan_command_attempt.error_reason}:\n"
f"{scan_command_attempt.error_trace}"
)


def main() -> None:
# First validate that we can connect to the servers we want to scan
servers_to_scan = []
for hostname in ["cloudflare.com", "google.com"]:
server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup(hostname, 443)
try:
server_info = ServerConnectivityTester().perform(server_location)
servers_to_scan.append(server_info)
except ConnectionToServerFailed as e:
print(f"Error connecting to {server_location.hostname}:{server_location.port}: {e.error_message}")
return
# 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)

# Then queue some scan commands for each server
all_server_scans = [
ServerScanRequest(
server_info=server_info, scan_commands={ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES}
)
for server_info in servers_to_scan
]
scanner.start_scans(all_server_scans)

# Then retrieve the result of the scan commands for each server
# And retrieve and process the results for each server
for server_scan_result in scanner.get_results():
print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:")

# Scan commands that were run with no errors
try:
ssl2_result = server_scan_result.scan_commands_results[ScanCommand.SSL_2_0_CIPHER_SUITES]
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}")
except KeyError:
pass

try:
certinfo_result = server_scan_result.scan_commands_results[ScanCommand.CERTIFICATE_INFO]
print("\nCertificate info:")
for cert_deployment in certinfo_result.certificate_deployments:
print(f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}")
except KeyError:
pass
# 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}")

# Scan commands that were run with errors
for scan_command, error in server_scan_result.scan_commands_errors.items():
print(f"\nError when running {scan_command}:\n{error.exception_trace}")
# 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


def example_json_result_parsing() -> None:
# SSLyze scan results serialized to JSON were saved to this file using --json_out
results_as_json_file = Path(__file__).parent / "tests" / "json_tests" / "sslyze_output.json"
results_as_json = results_as_json_file.read_text()

# These results can be parsed
parsed_results = SslyzeOutputAsJson.parse_raw(results_as_json)

# Making it easy to do post-processing and inspection of the results
print("The following servers were scanned:")
for server_scan_result in parsed_results.server_scan_results:
print(f"\n****{server_scan_result.server_location.hostname}:{server_scan_result.server_location.port}****")

if server_scan_result.scan_status == ServerScanStatusEnum.ERROR_NO_CONNECTIVITY:
print(f"That scan failed with the following error:\n{server_scan_result.connectivity_error_trace}")
continue

assert server_scan_result.scan_result
certinfo_attempt = server_scan_result.scan_result.certificate_info
if certinfo_attempt.status == ScanCommandAttemptStatusEnum.ERROR:
_print_failed_scan_command_attempt(certinfo_attempt) # type: ignore
else:
certinfo_result = server_scan_result.scan_result.certificate_info.result
assert certinfo_result
for cert_deployment in certinfo_result.certificate_deployments:
print(f" SHA1 of leaf certificate: {cert_deployment.received_certificate_chain[0].fingerprint_sha1}")
print("")


if __name__ == "__main__":
main()


def basic_example_connectivity_testing() -> None:
# Define the server that you want to scan
server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.google.com", 443)

# Do connectivity testing to ensure SSLyze is able to connect
try:
server_info = ServerConnectivityTester().perform(server_location)
except ConnectionToServerFailed as e:
# Could not connect to the server; abort
print(f"Error connecting to {server_location}: {e.error_message}")
return
print(f"Connectivity testing completed: {server_info}")


def basic_example() -> None:
# Define the server that you want to scan
server_location = ServerNetworkLocationViaDirectConnection.with_ip_address_lookup("www.google.com", 443)

# Do connectivity testing to ensure SSLyze is able to connect
try:
server_info = ServerConnectivityTester().perform(server_location)
except ConnectionToServerFailed as e:
# Could not connect to the server; abort
print(f"Error connecting to {server_location}: {e.error_message}")
return

# Then queue some scan commands for the server
scanner = Scanner()
server_scan_req = ServerScanRequest(
server_info=server_info, scan_commands={ScanCommand.CERTIFICATE_INFO, ScanCommand.SSL_2_0_CIPHER_SUITES},
)
scanner.start_scans([server_scan_req])

# Then retrieve the results
for server_scan_result in scanner.get_results():
print(f"\nResults for {server_scan_result.server_info.server_location.hostname}:")

# SSL 2.0 results
ssl2_result = server_scan_result.scan_commands_results[ScanCommand.SSL_2_0_CIPHER_SUITES]
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}")

# Certificate info results
certinfo_result = server_scan_result.scan_commands_results[ScanCommand.CERTIFICATE_INFO]
print("\nCertificate info:")
for cert_deployment in certinfo_result.certificate_deployments:
print(f"Leaf certificate: \n{cert_deployment.received_certificate_chain_as_pem[0]}")
14 changes: 14 additions & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
mypy
flake8
invoke
pytest<6.0.0
sphinx
sphinx-rtd-theme
twine
sphinx-autodoc-typehints
black==19.10b0
pytest-cov
faker

# For building the Windows executable
cx-freeze; sys.platform == 'win32'

0 comments on commit fbfc52d

Please sign in to comment.