Skip to content

Commit 6e3dc16

Browse files
authored
Support both Qualys SSLLabs API v4 and v3 (#189) (#199)
- Support Qualys SSLLabs API v4 - The tool uses [API v4](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v4.md) if you provide your registered email with Qualys SSLLabs via the `--email` argument. - The tool uses [API v3](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md) if you do not specify the `--email` argument. Note that v3 will be being deprecated in 2024 by Qualys. - Ticket: #189
1 parent 8cb1716 commit 6e3dc16

File tree

8 files changed

+90
-22
lines changed

8 files changed

+90
-22
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
3.0.0 - 2024-04-02
5+
==================
6+
7+
### Changed
8+
- Support Qualys SSLLabs API v4 (#189)
9+
- The tool uses [API v4](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v4.md) if you provide your registered email with Qualys SSLLabs via the `--email` argument.
10+
- The tool uses [API v3](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md) if you do not specify the `--email` argument. Note that v3 will be being deprecated in 2024 by Qualys.
11+
412

513
2.3.0 - 2024-04-01
614
==================

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
[![SecretsScan](https://github.com/kyhau/ssllabs-scan/actions/workflows/secrets-scan.yml/badge.svg)](https://github.com/kyhau/ssllabs-scan/actions/workflows/secrets-scan.yml)
77
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](http://en.wikipedia.org/wiki/MIT_License)
88

9-
This tool calls the SSL Labs [API v3](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md) to do SSL testings on the given hosts, and generates csv and html reports.
9+
This tool calls the SSL Labs API to do SSL testings on the given hosts, and generates csv and html reports.
10+
- The tool uses [API v4](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v4.md) if you provide your registered email with Qualys SSLLabs via the `--email` argument.
11+
- The tool uses [API v3](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md) if you do not specify the `--email` argument. Note that v3 will be being deprecated in 2024 by Qualys.
12+
1013

1114
All notable changes to this project will be documented in [CHANGELOG](./CHANGELOG.md).
1215

@@ -33,6 +36,12 @@ You can change the report template and styles in these files:
3336
- [ssllabsscan/report_template.py](./ssllabsscan/report_template.py)
3437
- [ssllabsscan/styles.css](./ssllabsscan/styles.css)
3538

39+
---
40+
## Important Notes
41+
42+
ℹ️ Please note that from Qualys SSLLabs API v4, you must use a one-time registration with Qualys SSLLabs. For details see [Introduction of API v4 for Qualys SSLLabs and deprecation of API v3](https://notifications.qualys.com/api/2023/09/28/introduction-of-api-v4-for-qualys-ssllabs-and-deprecation-of-api-v3).
43+
> The API v3 API will be available until the end of 2023 (Dec 31st 2023), and starting from 1st January 2024, we will be deprecating the API v3 support for SSL Labs. Request all customers to move to API v4.
44+
3645
ℹ️ Please note that the SSL Labs Assessment API has access rate limits. You can find more details in the sections "Error Response Status Codes" and "Access Rate and Rate Limiting" in the official [SSL Labs API Documentation](https://github.com/ssllabs/ssllabs-scan/blob/master/ssllabs-api-docs-v3.md). Some common status codes are:
3746
- 400 - invocation error (e.g., invalid parameters)
3847
- 429 - client request rate too high or too many new assessments too fast
@@ -49,9 +58,14 @@ You can change the report template and styles in these files:
4958
virtualenv env
5059
. env/bin/activate
5160
52-
# Install and run
61+
# Install
5362
pip install -e .
63+
64+
# Run with v3 (v3, which does not required a registered email, will be being deprecated in 2024)
5465
ssllabs-scan sample/SampleServerList.txt
66+
67+
# Run with v4
68+
ssllabs-scan sample/SampleServerList.txt --email <your registered email with Qualys SSLLabs>
5569
```
5670

5771
### Windows
@@ -60,9 +74,14 @@ ssllabs-scan sample/SampleServerList.txt
6074
virtualenv env
6175
env\Scripts\activate
6276
63-
# Install and run
77+
# Install
6478
pip install -e .
79+
80+
# Run with v3 (v3, which does not required a registered email, will be being deprecated in 2024)
6581
ssllabs-scan sample\SampleServerList.txt
82+
83+
# Run with v4
84+
ssllabs-scan sample\SampleServerList.txt --email <your registered email with Qualys SSLLabs>
6685
```
6786

6887
### Docker

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from setuptools import find_packages, setup
22

33
__title__ = "ssllabsscan"
4-
__version__ = "2.3.0"
4+
__version__ = "3.0.0"
55
__author__ = "Kay Hau"
66
__email__ = "virtualda@gmail.com"
77
__uri__ = "https://github.com/kyhau/ssllabs-scan"

ssllabsscan/main.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ def output_summary_html(input_csv, output_html):
4848

4949
def process(
5050
server_list_file,
51+
email,
5152
check_progress_interval_secs=30,
5253
summary_csv=SUMMARY_CSV,
5354
summary_html=SUMMARY_HTML
@@ -65,7 +66,7 @@ def process(
6566
for server in servers:
6667
try:
6768
print(f"Start analyzing {server}...")
68-
SSLLabsClient(check_progress_interval_secs).analyze(server, summary_csv)
69+
SSLLabsClient(email, check_progress_interval_secs).analyze(server, summary_csv)
6970
except Exception as e:
7071
traceback.print_exc()
7172
ret = 1
@@ -82,6 +83,12 @@ def parse_args():
8283
"inputfile",
8384
help="Input file containing list of servers to scan",
8485
)
86+
parser.add_argument(
87+
"-e",
88+
"--email",
89+
dest="email",
90+
help="Registered-email required for Qualys SSLLabs API v4",
91+
)
8592
parser.add_argument(
8693
"-o",
8794
"--output",
@@ -110,7 +117,13 @@ def main():
110117
Entry point of the app.
111118
"""
112119
args = parse_args()
113-
return process(server_list_file=args.inputfile, check_progress_interval_secs=args.progress, summary_csv=args.summary, summary_html=args.output)
120+
return process(
121+
server_list_file=args.inputfile,
122+
email=args.email,
123+
check_progress_interval_secs=args.progress,
124+
summary_csv=args.summary,
125+
summary_html=args.output,
126+
)
114127

115128

116129
if __name__ == "__main__":

ssllabsscan/ssllabs_client.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
logging.getLogger().setLevel(logging.INFO)
1313

14-
API_URL = "https://api.ssllabs.com/api/v3/analyze"
14+
API_V4_URL = "https://api.ssllabs.com/api/v4/analyze"
15+
API_V3_URL = "https://api.ssllabs.com/api/v3/analyze"
16+
1517

1618
CHAIN_ISSUES = {
1719
"0": "none",
@@ -47,7 +49,8 @@
4749

4850

4951
class SSLLabsClient():
50-
def __init__(self, check_progress_interval_secs=30, max_attempts=100, verify=True):
52+
def __init__(self, email, check_progress_interval_secs=30, max_attempts=100, verify=True):
53+
self.email = email
5154
self._check_progress_interval_secs = check_progress_interval_secs
5255
self._max_attempts = max_attempts
5356
self._verify = verify
@@ -66,7 +69,6 @@ def analyze(self, host, summary_csv_file):
6669
self.append_summary_csv(summary_csv_file, host, data)
6770

6871
def start_new_scan(self, host, publish="off", startNew="off", all="done", ignoreMismatch="on"):
69-
path = API_URL
7072
payload = {
7173
"host": host,
7274
"publish": publish,
@@ -75,7 +77,7 @@ def start_new_scan(self, host, publish="off", startNew="off", all="done", ignore
7577
"ignoreMismatch": ignoreMismatch
7678
}
7779

78-
response = self.request_api(path, payload)
80+
response = self.request_api(payload)
7981
results = response.json()
8082

8183
payload.pop("startNew")
@@ -84,7 +86,7 @@ def start_new_scan(self, host, publish="off", startNew="off", all="done", ignore
8486
while response.status_code == 200 and results["status"] not in ["READY", "ERROR"]:
8587
self.print_msg(response, "WAIT_FOR_COMPLETE")
8688
time.sleep(self._check_progress_interval_secs)
87-
response = self.request_api(path, payload)
89+
response = self.request_api(payload)
8890
results = response.json()
8991

9092
if response.status_code != 200 or results["status"] == "ERROR":
@@ -93,8 +95,8 @@ def start_new_scan(self, host, publish="off", startNew="off", all="done", ignore
9395

9496
return results
9597

96-
def request_api(self, url, payload):
97-
response = self.requests_get(url, payload)
98+
def request_api(self, payload):
99+
response = self.requests_get(payload)
98100
attempts = 0
99101

100102
# Supported error codes
@@ -109,12 +111,18 @@ def request_api(self, url, payload):
109111
self.print_msg(response, "WAIT_FOR_RETRY")
110112
attempts += 1
111113
time.sleep(self._check_progress_interval_secs)
112-
response = self.requests_get(url, payload)
114+
response = self.requests_get(payload)
113115

114116
return response
115117

116-
def requests_get(self, url, payload):
117-
return requests.get(url, params=payload, verify=self._verify)
118+
def requests_get(self, payload):
119+
kargs = {}
120+
url = API_V3_URL
121+
if self.email:
122+
kargs = {"headers": {"email": self.email}}
123+
url = API_V4_URL
124+
125+
return requests.get(url, params=payload, verify=self._verify, **kargs)
118126

119127
@staticmethod
120128
def prepare_datetime(epoch_time):

ssllabsscan/tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,16 @@ def sample_server_list_file_2(unit_tests_tmp_output_dir):
127127
return server_list_file
128128

129129

130+
@pytest.fixture(scope="session")
131+
def email_1():
132+
return None
133+
134+
135+
@pytest.fixture(scope="session")
136+
def email_2():
137+
return "dummy@example.com"
138+
139+
130140
@pytest.fixture(scope="session")
131141
def output_summary_csv_file(unit_tests_tmp_output_dir):
132142
return os.path.join(unit_tests_tmp_output_dir, "test_summary.csv")

ssllabsscan/tests/test_main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ def common_tests(
1313
sample_dns_response,
1414
sample_in_progress_response,
1515
sample_ready_response,
16+
email,
1617
output_summary_csv_file,
1718
output_summary_html_file,
1819
output_server_1_json_file
@@ -27,6 +28,7 @@ def common_tests(
2728

2829
assert 0 == process(
2930
sample_input_file,
31+
email,
3032
check_progress_interval_secs=1,
3133
summary_csv=output_summary_csv_file,
3234
summary_html=output_summary_html_file
@@ -42,6 +44,7 @@ def test_main_process_1(
4244
sample_dns_response,
4345
sample_in_progress_response,
4446
sample_ready_response,
47+
email_1,
4548
output_summary_csv_file,
4649
output_summary_html_file,
4750
output_server_1_json_file
@@ -51,6 +54,7 @@ def test_main_process_1(
5154
sample_dns_response,
5255
sample_in_progress_response,
5356
sample_ready_response,
57+
email_1,
5458
output_summary_csv_file,
5559
output_summary_html_file,
5660
output_server_1_json_file
@@ -62,6 +66,7 @@ def test_main_process_2(
6266
sample_dns_response,
6367
sample_in_progress_response,
6468
sample_ready_response,
69+
email_2,
6570
output_summary_csv_file,
6671
output_summary_html_file,
6772
output_server_1_json_file
@@ -71,6 +76,7 @@ def test_main_process_2(
7176
sample_dns_response,
7277
sample_in_progress_response,
7378
sample_ready_response,
79+
email_2,
7480
output_summary_csv_file,
7581
output_summary_html_file,
7682
output_server_1_json_file

ssllabsscan/tests/test_ssllabs_client.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from mock import Mock
5+
56
from ssllabsscan.ssllabs_client import SSLLabsClient
67

78
from .conftest import MockHttpResponse
@@ -13,6 +14,7 @@ def test_ssl_labs_client_analyze(
1314
sample_dns_response,
1415
sample_in_progress_response,
1516
sample_ready_response,
17+
email_1,
1618
output_summary_csv_file,
1719
output_server_1_json_file
1820
):
@@ -22,7 +24,7 @@ def test_ssl_labs_client_analyze(
2224
MockHttpResponse(200, sample_ready_response)
2325
]
2426

25-
client = SSLLabsClient(check_progress_interval_secs=1)
27+
client = SSLLabsClient(email=email_1, check_progress_interval_secs=1)
2628
client.requests_get = Mock(side_effect=mocked_request_ok_response_sequence)
2729

2830
client.analyze(host=sample_ready_response["host"], summary_csv_file=output_summary_csv_file)
@@ -35,6 +37,7 @@ def test_ssl_labs_client_start_new_scan_valid_url(
3537
sample_dns_response,
3638
sample_in_progress_response,
3739
sample_ready_response,
40+
email_1
3841
):
3942
"""Case 1: valid server url"""
4043

@@ -44,7 +47,7 @@ def test_ssl_labs_client_start_new_scan_valid_url(
4447
MockHttpResponse(200, sample_ready_response)
4548
]
4649

47-
client1 = SSLLabsClient(check_progress_interval_secs=1)
50+
client1 = SSLLabsClient(email=email_1, check_progress_interval_secs=1)
4851
client1.requests_get = Mock(side_effect=mocked_request_ok_response_sequence)
4952

5053
ret = client1.start_new_scan(host=sample_ready_response["host"])
@@ -53,14 +56,14 @@ def test_ssl_labs_client_start_new_scan_valid_url(
5356
assert ret["endpoints"][0]["grade"]
5457

5558

56-
def test_ssl_labs_client_start_new_scan_invalid_url(sample_dns_response):
59+
def test_ssl_labs_client_start_new_scan_invalid_url(sample_dns_response, email_1):
5760
"""Case 2: unable to resolve domain name"""
5861
mocked_request_err_response_sequence = [
5962
MockHttpResponse(200, sample_dns_response),
6063
MockHttpResponse(200, SAMPLE_UNABLE_TO_RESOLVE_DOMAIN_RESPONSE)
6164
]
6265

63-
client2 = SSLLabsClient(check_progress_interval_secs=1)
66+
client2 = SSLLabsClient(email=email_1, check_progress_interval_secs=1)
6467
client2.requests_get = Mock(side_effect=mocked_request_err_response_sequence)
6568

6669
ret = client2.start_new_scan(host="example2.com")
@@ -70,7 +73,8 @@ def test_ssl_labs_client_start_new_scan_invalid_url(sample_dns_response):
7073

7174
def test_ssl_labs_client_start_new_scan_unexpected_error_code(
7275
sample_dns_response,
73-
sample_in_progress_response
76+
sample_in_progress_response,
77+
email_1
7478
):
7579
# Case 3: received error codes other than the supported one
7680
mocked_request_err_response_sequence = [
@@ -79,7 +83,7 @@ def test_ssl_labs_client_start_new_scan_unexpected_error_code(
7983
MockHttpResponse(441, {"status": "ERROR", "statusMessage": "some error"})
8084
]
8185

82-
client3 = SSLLabsClient(check_progress_interval_secs=1)
86+
client3 = SSLLabsClient(email=email_1, check_progress_interval_secs=1)
8387
client3.requests_get = Mock(side_effect=mocked_request_err_response_sequence)
8488

8589
ret = client3.start_new_scan(host="example3.com")

0 commit comments

Comments
 (0)