header-sentinel
Fail your CI when deployed URLs are missing or misconfigure HTTP security headers — with a specific fix (example header + Nginx/Express/Vercel/Netlify hints) for every problem. Mozilla HTTP Observatory-style letter grades (A+ through F), zero runtime dependencies, Node ≥ 18, JSON and SARIF (GitHub code scanning) output.
Install
npm i -D header-sentinel # or run ad-hoc:
npx header-sentinel https://example.comUsage
header-sentinel <url...> [options]
-c, --config <file> JSON config (default: .headerguardrc.json / .headersentinelrc.json if present)
--fail-on <level> error | warn | none (default: error)
--json Print JSON results (includes observatory score/tests)
--sarif [file] Print SARIF 2.1.0 (or write to file)
--sitemap <url> Load URLs from sitemap.xml (repeatable)
--baseline <file> Only fail on findings not in baseline
--update-baseline Write current fingerprints to --baseline
--no-http-upgrade Skip HTTP→HTTPS redirect analysis
--no-preload-check Skip HSTS preload list / eligibility lookups
-t, --timeout <ms> Request timeout (default: 10000)
-H, --header "K: V" Extra request header (repeatable)
--no-follow-redirects Check the first response instead of the final one (default follows)
Exit codes: 0 pass, 1 findings at/above --fail-on, 2 usage error. Colours are disabled when NO_COLOR is set or stdout is not a TTY (FORCE_COLOR=1 forces them).
Scoring (Mozilla Observatory)
Sites start at 100. Penalties apply first; bonuses (for example strong CSP, Referrer-Policy, frame-ancestors, HSTS preload) apply only if the pre-bonus score is ≥ 90. Grades use Observatory buckets: A+ (100+), A, A-, B+ … F (0–24). Each scan also reports a likelihood indicator (LOW/MEDIUM) derived from the grade.
Detailed header lint findings (severity error/warn) are separate from Observatory test keys; CI failure is still driven by --fail-on and findings, while --json exposes score, grade, and observatory.tests[].
Sample output
$ npx header-sentinel https://github.com --no-http-upgrade --no-preload-check
https://github.com/ (HTTP 200) Grade B+ score 80/100 · MEDIUM risk
⚠ warn CSP style-src allows 'unsafe-inline' [csp-unsafe-inline]
...
Baseline (CI: fail only on new issues)
# First run on main — record accepted findings npx header-sentinel https://example.com --baseline .header-baseline.json --update-baseline # PR / nightly — exit 1 only if something new appears npx header-sentinel https://example.com --baseline .header-baseline.json --fail-on error
Sitemap mode
npx header-sentinel --sitemap https://example.com/sitemap.xml --baseline .header-baseline.json
Checks
| id | severity | what |
|---|---|---|
hsts |
error | Strict-Transport-Security on HTTPS with max-age ≥ 31536000 |
hsts-subdomains |
warn | HSTS includes includeSubDomains |
hsts-preload-eligible |
warn | Header meets preload submission requirements (not yet on preload list) |
redirection |
error/warn | HTTP→HTTPS upgrade / redirect chain (Observatory-aligned) |
csp |
error | Content-Security-Policy present (live header lint only; use csp-forge to build policies) |
csp-unsafe-inline / csp-unsafe-eval |
warn | unsafe keywords in *-src |
csp-wildcard |
warn | overly broad sources |
csp-missing-* |
warn | important directives missing |
frame-options |
error | X-Frame-Options or CSP frame-ancestors |
content-type-options |
error | X-Content-Type-Options: nosniff |
referrer-policy |
warn/error | safe Referrer-Policy |
permissions-policy |
warn | Permissions-Policy present |
coop |
warn | Cross-Origin-Opener-Policy |
info-leak-* |
warn | Server / X-Powered-By leaks |
cookies |
warn | Set-Cookie presence only — run cookie-sentinel for full cookie audits |
x-xss-protection |
warn | deprecated header enabled |
fetch |
error | URL could not be fetched |
No TLS certificate or DNS checks (see tls-sentinel / dns-sentinel).
Config
.headerguardrc.json (or .headersentinelrc.json, or --config path):
{
"urls": ["https://example.com"],
"sitemap": "https://example.com/sitemap.xml",
"baseline": ".header-baseline.json",
"failOn": "error",
"timeout": 10000,
"checkHttpRedirection": true,
"checkPreload": true,
"headers": { "User-Agent": "my-ci" },
"ignore": ["coop", "info-leak-server"],
"severity": { "permissions-policy": "error", "csp-unsafe-inline": "off" }
}CI
GitHub Actions (action)
name: Security headers on: deployment_status: schedule: [{ cron: '0 6 * * 1' }] workflow_dispatch: permissions: { contents: read, security-events: write } jobs: headers: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: MrLouisWright/header-sentinel@v1 with: urls: https://example.com https://example.com/login fail-on: error sarif: header-sentinel.sarif - uses: github/codeql-action/upload-sarif@v3 if: always() with: { sarif_file: header-sentinel.sarif }
GitHub Actions (npx)
- uses: actions/setup-node@v4 with: { node-version: 20 } - run: npx header-sentinel@1 https://staging.example.com --json --fail-on error
API
ESM
import { scan, scanAll, analyze, shouldFail, toSarif, computeObservatoryScore } from 'header-sentinel';
CommonJS
const { scan, shouldFail } = require('header-sentinel');
const results = await scanAll(['https://example.com'], { timeout: 5000, config: { ignore: ['coop'] }, checkHttpRedirection: true, }); console.log(results[0].grade, results[0].observatory.tests); if (shouldFail(results, 'error')) process.exit(1);
Types are generated in dist/index.d.ts (import / require typings via dist/index.d.cts).
Support
If this project is useful to you, you can support its development with a donation: Buy me a coffee. It helps keep these tools maintained, documented and free for everyone.
License
MIT © Louis Wright (Louis Wright)