Command-line tool grading a site's HTTP security headers: one request, six headers, a 0-100 score and an A-F grade. Follows redirects to grade the final URL, tells a missing header from a present-but-inert one, and returns a CI-usable exit code - same engine exposed as a FastAPI web app on Render.
A command-line tool that grades the HTTP security headers of a website. One request, six headers evaluated, a score from 0 to 100 and a letter grade A–F. The same engine is exposed as a CLI, a JSON API and a web page.
It grades the final URL. Redirects are followed, and the verdict applies to the page the browser actually lands on — not to the address that was typed. A site redirecting HTTP to HTTPS would otherwise be scored on headers nobody ever receives.
A header being present is not a header being effective. The status is a three-value verdict — ok, weak, missing — so a Strict-Transport-Security: max-age=0, which disables HSTS while looking like a correct configuration, is reported as weak and scores half the weight.
| Header | Weight | ok when |
|---|---:|---|
| Strict-Transport-Security | 30 | max-age ≥ 6 months |
| Content-Security-Policy | 30 | policy present and restrictive |
| X-Frame-Options | 15 | DENY or SAMEORIGIN |
| X-Content-Type-Options | 15 | nosniff |
| Referrer-Policy | 5 | strict policy |
| Permissions-Policy | 5 | present and non-empty |
Weights total 100. Grades: ≥ 90 A · ≥ 80 B · ≥ 70 C · ≥ 60 D · otherwise F
The exit code carries the verdict — 0 for an A or B, 1 for a C or D, 2 for an F, a network error or an invalid URL:
shhc https://my-site.com || echo "insufficient headers"
Output comes in three shapes: a colored Rich table, --quiet for a single line, --json for machines.
A single Finding dataclass travels between layers — rules.py produces a list of them, scoring.py consumes it, render.py displays it. No layer knows the others.
cli.py / api.py ... orchestration, exit codes, HTTP routes
|
fetch.py ...... 1 request -> (final URL, lowercased headers)
|
rules.py ...... 6 rules -> list[Finding]
|
scoring.py .... sum of points -> score + grade
|
render.py ..... table + grade panel + recommendations
Pure modules first. rules.py and scoring.py do neither network nor display: a function takes a dictionary and returns an object. They are 311 of the package's 548 lines, and they are tested without mocking anything — hence 80 tests running in seconds.
Normalization in a single place. HTTP headers being case-insensitive, fetch.py lowercases every key on the way out of the request, so no rule downstream has to test several spellings.
api.py reuses fetch, rules and scoring exactly like the CLI — the business logic is not duplicated. FastAPI serves the form page, GET /api/check?url=… for the JSON report, /api/health as a liveness probe, and generates the OpenAPI documentation at /docs. Deployed on Render.com through a render.yaml blueprint.
A service that fetches a visitor-supplied URL is a door into the host's internal network. Two guardrails:
http://169.254.169.254/, the cloud metadata endpoint that often holds credentials.Known limitation, documented rather than hidden: the guard resolves the name, then httpx resolves it again — a window for DNS rebinding. Closing it requires resolving once and connecting to the validated IP.
httpx over requests. Redirect following must be a visible choice, not an implicit behavior — it is the core of the "grade the final URL" rule. httpx requires follow_redirects=True explicitly and enforces an explicit timeout, avoiding the classic omission that leaves a program hanging forever.
respx for the tests. It replaces the httpx transport layer and serves responses declared in advance: the whole HTTP code path really runs, redirects included, only the socket is short-circuited. An unplanned network call fails the test instead of reaching the internet — so the suite is neither slow nor flaky.
ASCII-only rendering. The Windows console commonly runs code page 850, and CI logs are rarely UTF-8; em dashes and bullets would render as ?. Colors, on the other hand, work everywhere.
Stack: Python 3.11, httpx, rich, typer, FastAPI, uvicorn, pytest, respx.