| Layer | Technology | Version |
|---|---|---|
| Language | Python | ≥ 3.11 |
| CLI framework | Typer | ≥ 0.12 |
| Terminal output | Rich | ≥ 13.7 |
| HTTP client | requests | ≥ 2.31 |
| Testing | pytest + pytest-cov | ≥ 8 / ≥ 5 |
pip install -e . # install in editable mode
headersvalidator check https://example.com
headersvalidator info rules
python -m pytest # run tests with coverage
python -m pytest --tb=short -q # quick runheadersvalidator/
cli.py → Typer entry point; calls assessor + reporter
assessor.py → Public API: assess(url) → HeadersReport (only I/O here)
checker.py → Pure validation logic — HeadersChecker.check_all()
constants.py → HEADER_RULES table + per-header check functions
models.py → Status enum, HeaderResult, HeadersReport dataclasses
reporter.py → Rich terminal renderers (no I/O of its own)
http_utils.py → fetch_headers(), normalise_url(), extract_headers()
tests/
conftest.py → make_response() factory + SECURE/MINIMAL/EMPTY fixtures
test_*.py → pytest, AAA pattern, class-per-feature grouping
Request lifecycle:
- CLI (
cli.py) callsassess(url)fromassessor.py assessor.pycallsfetch_headers()(HEAD with GET fallback on 405)extract_headers()normalises headers to{lowercase: value}HeadersChecker.check_all()applies every rule inHEADER_RULESHeadersReportis returned;reporter.pyrenders it with Rich
Single I/O boundary: all network calls go through http_utils.fetch_headers.
Mock only this function in tests.
- Add a
_check_fn(value) → tuple[Status, str]function inconstants.py - Append an entry to
HEADER_RULES(name, iana_status, source, required, check, recommended, description) - Write tests in
tests/test_checker.pyandtests/test_constants.py
- Mock boundary:
headersvalidator.http_utils.fetch_headersviamonkeypatch - Use
conftest.make_response()to build fake responses - Fixtures:
secure_response,minimal_response,empty_response,mock_fetch - Test class naming:
TestCheckAll,TestAssess, etc. (class-per-feature) - Coverage configured in
pyproject.toml; target ≥ 80%
PASS = 2 pts, WARN = 1 pt, FAIL/DEPRECATED = 0 pts (INFO not graded).
Score = round(earned / (graded_count * 2) * 100).
- 0 — all required headers pass
- 1 — one or more FAIL (or WARN with
--strict) - 2 — network / connection error
from __future__ import annotationsat the top of every module- Snake_case for all files, functions, and variables
- Sphinx-style docstrings:
:param name:,:returns:,:rtype:(no:type:— type annotations on signatures are sufficient) - Conventional commits:
fix:,feat:,fix(scope):,refactor:,test:,docs: - Input validation lives in
cli.py(URL normalisation delegated tonormalise_url()inhttp_utils.py) fetch_headers()fromhttp_utilsis the single I/O abstraction; patch it in tests viamonkeypatch- No CI config currently present
Run these checks and update these files as needed — do not skip any step:
# 1. Verify tests pass and coverage is still 100%
pytestIf the test count changed, update both occurrences in README.md:
- Badge line (near top):
 - Running Tests section: "The test suite has NNN tests…" sentence
Also update the count in this file (CLAUDE.md) under "Current State".
# 2. Check for lint issues
ruff check headersvalidator/Fix any F401 (unused import) or other errors before committing.
Before pushing, update CHANGELOG.md: add your changes under ## [Unreleased]
using the standard sections (### Added, ### Changed, ### Fixed, ### Removed).
When bumping the version, move unreleased items to a new ## [x.y.z] — YYYY-MM-DD
section and update the comparison links at the bottom of CHANGELOG.md.
When committing a set of changes, bump the version using semver:
- patch (
0.1.x) — bug fixes, RFC compliance fixes, lint/refactor, docs - minor (
0.x.0) — new checks, new CLI commands, new features - major (
x.0.0) — breaking API changes
Two files must always be updated together:
pyproject.toml→version = "x.y.z"headersvalidator/__init__.py→ fallback__version__ = "x.y.z"(theexceptbranch)
Every version bump must be followed by a GitHub release. Do not leave a version tag without a release.
After bumping the version, committing, and pushing:
# Tag the version commit and push
git tag vX.Y.Z
git push origin vX.Y.Z
# Create the GitHub release
gh release create vX.Y.Z \
--title "vX.Y.Z" \
--notes "$(cat <<'EOF'
## What's changed
<Copy the ### Added / ### Changed / ### Fixed / ### Removed blocks verbatim
from the [X.Y.Z] section in CHANGELOG.md>
## Impact
<1–3 sentences: what this means for users — what improves, what breaks,
whether the upgrade is urgent (e.g. new header rule, scoring change,
OWASP/RFC alignment fix, etc.)>
## Migration
<Only for minor/major bumps: list any CLI flags, `assess()` parameters,
or `HEADER_RULES` entries removed/renamed that require user action.
Omit for patch releases.>
---
**Full changelog:** https://github.com/NC3-TestingPlatform/headersvalidator/blob/master/CHANGELOG.md
EOF
)"Release body checklist:
- Changelog entries for this version copied verbatim
- Impact note written (even one sentence is enough)
- Migration note present if CLI flags,
assess()signature, or rule names changed - Full changelog link at the bottom
Conventions:
- Tag and title:
vX.Y.Z— semver,v-prefixed, must matchpyproject.tomlversion - Do not mark as draft or pre-release for normal semver releases