What you need
Python 3 with the requests package (pip install requests). The MDN HTTP Observatory API, the SSL Labs API, crt.sh certificate search and the Wayback Machine Availability API need no key. The Google PageSpeed Insights API needs a free Google Cloud API key in practice, because the shared quota without a key is often used up.
Run this from a script or a server, not from a web page: SSL Labs refuses calls from browsers. Only check sites you run or have permission to test.
The checks
The HTTP Observatory scans the security headers, such as Content-Security-Policy and Strict-Transport-Security, and grades them from A+ to F. SSL Labs grades the HTTPS set-up the same way; a fresh test takes a few minutes, so the script asks for a result of the last 24 hours first (fromCache=on&maxAge=24) and otherwise checks back every 15 seconds.
The certificate's expiry date comes from the server itself, through Python's ssl module: the Certificate Transparency logs behind crt.sh can lag weeks behind, so they are used for something else, the list of host names that have certificates, which often shows a forgotten subdomain.
The script
Save it as check_site.py and run python3 check_site.py example.org. Put your own e-mail address in the User-Agent, so the services know who to contact about heavy use. Each check that fails says so and the others carry on.
Python
import os
import socket
import ssl
import sys
import time
import requests
HEADERS = {"User-Agent": "site-check script (you@example.com)"}
def observatory(host):
# Mozilla's HTTP Observatory: security headers, graded A+ to F
response = requests.post(
"https://observatory-api.mdn.mozilla.net/api/v2/scan",
params={"host": host}, headers=HEADERS, timeout=60)
data = response.json()
return f"{data['grade']} ({data['score']}/100, {data['tests_passed']} of {data['tests_quantity']} tests passed)"
def ssl_labs(host):
# Qualys SSL Labs: the HTTPS set-up, graded A+ to F; a new test takes a few minutes
url = "https://api.ssllabs.com/api/v3/analyze"
params = {"host": host, "fromCache": "on", "maxAge": 24}
for attempt in range(20):
data = requests.get(url, params=params, headers=HEADERS, timeout=60).json()
if data["status"] == "READY":
return ", ".join(sorted({e.get("grade", "no grade") for e in data["endpoints"]}))
if data["status"] == "ERROR":
return "error: " + data.get("statusMessage", "unknown")
time.sleep(15)
return "not ready yet, try again in a few minutes"
def certificate_expiry(host):
# the certificate the server sends now, read with Python's own ssl module
context = ssl.create_default_context()
with socket.create_connection((host, 443), timeout=30) as connection:
with context.wrap_socket(connection, server_hostname=host) as tls:
expires = ssl.cert_time_to_seconds(tls.getpeercert()["notAfter"])
days = int((expires - time.time()) // 86400)
return f"valid for {days} more days"
def host_names(host):
# crt.sh: host names in the public Certificate Transparency logs, which
# often shows subdomains you forgot; the logs can lag behind by weeks
response = requests.get("https://crt.sh/", params={"q": "%." + host, "output": "json"},
headers=HEADERS, timeout=120)
names = {name.lower() for cert in response.json() for name in cert["name_value"].split("\n")}
return f"{len(names)}: " + ", ".join(sorted(names)[:10]) + (" ..." if len(names) > 10 else "")
def wayback(host):
# Wayback Machine: the most recent saved copy of the home page
data = requests.get("https://archive.org/wayback/available", params={"url": host},
headers=HEADERS, timeout=60).json()
snapshot = data.get("archived_snapshots", {}).get("closest")
return f"last saved {snapshot['timestamp'][:8]}" if snapshot else "no saved copy"
def pagespeed(host):
# PageSpeed Insights: Lighthouse scores; needs your own free key
key = os.environ.get("PAGESPEED_API_KEY")
if not key:
return "skipped (set PAGESPEED_API_KEY)"
data = requests.get("https://www.googleapis.com/pagespeedonline/v5/runPagespeed",
params={"url": f"https://{host}/", "strategy": "mobile", "key": key},
headers=HEADERS, timeout=120).json()
return f"{round(data['lighthouseResult']['categories']['performance']['score'] * 100)}/100 on mobile"
if __name__ == "__main__":
host = sys.argv[1] if len(sys.argv) > 1 else "example.com"
for name, check in [("Security headers", observatory), ("HTTPS", ssl_labs),
("Certificate", certificate_expiry), ("Host names", host_names), ("Wayback Machine", wayback),
("Speed", pagespeed)]:
try:
print(f"{name}: {check(host)}")
except (requests.RequestException, OSError, KeyError, ValueError) as error:
print(f"{name}: failed ({error})")
Example output
For this website, on the day this guide was written:
Output
Security headers: A+ (110/100, 12 of 12 tests passed)
HTTPS: A+
Certificate: valid for 60 more days
Host names: 1: free-web-services.com
Wayback Machine: last saved 20260309
Speed: skipped (set PAGESPEED_API_KEY)
Going further
Run it weekly with cron and send yourself the output only when a grade drops or the certificate has less than 14 days left.
PageSpeed Insights runs a lab test; for the speed real visitors get, the Chrome UX Report (CrUX) API gives 28-day averages of the Core Web Vitals for sites with enough Chrome visitors.