HTTP security headers audit tool. Scans a list of sites against a configurable ruleset and posts results to Discord — via a bot with slash commands and a weekly scheduled report.
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]" # installs hardhat + pytest for testsVerify:
hardhat --helpEdit sites.yaml:
sites:
- url: https://yourdomain.com
tags: [prod]
min_grade: A
- url: https://blog.yourdomain.com
tags: [prod]
min_grade: Bmin_grade is optional. If set, the bot marks the result with
# Scan all sites in sites.yaml (posts to Discord if webhook is configured)
hardhat scan
# One-off scan of a single URL
hardhat scan --url https://example.com
# Start the Discord bot (slash commands + weekly reports)
hardhat bot
# Fetch OWASP OSHP header list and diff against local ruleset
hardhat syncThe bot provides two slash commands and a weekly automated report.
- Create an application at discord.com/developers/applications
- Under Bot → create a bot and copy the token
- Under OAuth2 → URL Generator → scopes:
bot+applications.commands, permissions: Send Messages + Embed Links → invite to your server - Enable Developer Mode in Discord settings, right-click your report channel → Copy Channel ID
- Add to
.env(see below)
| Command | Description |
|---|---|
/hardhat check <url> |
Scan a single URL on demand |
/hardhat scan |
Scan all sites in sites.yaml |
Every Monday at 08:00 UTC the bot posts a full scan of all sites in sites.yaml to your configured channel.
Create a .env file in the repo root (it is gitignored and never committed):
HARDHAT_WEBHOOK_URL=https://discord.com/api/webhooks/<id>/<token>
DISCORD_BOT_TOKEN=your-bot-token-here
DISCORD_REPORT_CHANNEL_ID=your-channel-id-here
HARDHAT_WEBHOOK_URL is used by hardhat scan (CLI). The bot reads DISCORD_BOT_TOKEN and DISCORD_REPORT_CHANNEL_ID.
Edit scripts/hardhat.service with your username and path, then:
sudo cp scripts/hardhat.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable hardhat
sudo systemctl start hardhatCheck status:
sudo systemctl status hardhat
sudo journalctl -u hardhat -fThe service restarts automatically on crash or reboot.
All rules live in rulesets/current.yaml. Adding a new header check only requires editing YAML — no code changes:
rules:
cross-origin-opener-policy:
required: false
points: 5
must_contain_one_of: ["same-origin", "same-origin-allow-popups"]
advice: "Set COOP to same-origin to isolate browsing context."Available rule fields:
| Field | Type | Meaning |
|---|---|---|
required |
bool | Missing header = rule failure |
should_be_absent |
bool | Header must NOT be present |
points |
int | Points awarded for passing |
must_contain |
list[str] | All tokens must appear in value |
must_contain_one_of |
list[str] | At least one token must appear |
must_not_contain |
list[str] | None of these tokens may appear* |
min_max_age_seconds |
int | max-age must be >= this value |
advice |
str | Shown in Discord embed when rule fails |
* unsafe-inline is permitted when a nonce or hash is also present in the value (matches securityheaders.com behaviour).
Score = sum of points from passing rules. Grade is assigned by percentage of max possible score:
| Grade | Threshold |
|---|---|
| A+ | ≥ 95 % |
| A | ≥ 85 % |
| B | ≥ 75 % |
| C | ≥ 65 % |
| D | ≥ 55 % |
| E | ≥ 45 % |
| F | < 45 % |
hardhat syncDownloads the OWASP Secure Headers Project add/remove lists, diffs them against rulesets/current.yaml, and writes a snapshot to rulesets/owasp-snapshot.json. Never modifies current.yaml automatically — the diff is for human review.
pytest- TLS/certificate expiry scanning
- Web dashboard
- Multi-user support with auth
- Automatic ruleset updates from OWASP (current
syncis review-only by design)