header-grader 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -0
- package/dist/chunk-GQYXZFYW.js +456 -0
- package/dist/chunk-IW5F2ZYM.js +111 -0
- package/dist/chunk-O3B2LYGP.js +45 -0
- package/dist/cli.cjs +652 -0
- package/dist/cli.js +116 -0
- package/dist/index.cjs +637 -0
- package/dist/index.d.cts +64 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +38 -0
- package/dist/middleware-BsJGVvzp.d.cts +64 -0
- package/dist/middleware-BsJGVvzp.d.ts +64 -0
- package/dist/middleware.cjs +483 -0
- package/dist/middleware.d.cts +2 -0
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.js +9 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# header-grader
|
|
2
|
+
|
|
3
|
+
> Security header grader for **local dev** — grade your dev server's HTTP security headers and generate the exact Express/Nginx config that fixes them.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/z1zzles/header-grader/actions/workflows/ci.yml)
|
|
6
|
+
[](#requirements)
|
|
7
|
+
[](#design-goals)
|
|
8
|
+
[](#license)
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Grade: F (13/100) http://localhost:3000/
|
|
12
|
+
|
|
13
|
+
✗ Content-Security-Policy
|
|
14
|
+
Missing. CSP is your strongest defense against XSS.
|
|
15
|
+
✗ X-Content-Type-Options
|
|
16
|
+
Missing. Prevents MIME-type sniffing attacks.
|
|
17
|
+
✗ X-Frame-Options
|
|
18
|
+
Missing (and no CSP frame-ancestors). Your site can be framed for clickjacking.
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
Generate the fix: header-grader http://localhost:3000 --fix express
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why this exists
|
|
25
|
+
|
|
26
|
+
Security headers are one of the highest-leverage, lowest-effort defenses in web development: a handful of response headers protect against XSS, clickjacking, MIME sniffing, protocol downgrade attacks, and cross-origin data leaks. Yet most projects ship without them — not because they're hard, but because nothing in the **development workflow** ever mentions them.
|
|
27
|
+
|
|
28
|
+
The existing tools all live at the wrong end of the pipeline:
|
|
29
|
+
|
|
30
|
+
- **[SecurityHeaders.com](https://securityheaders.com)** is excellent, but it scans public production URLs. It can't reach `localhost`, so you only find out after you deploy.
|
|
31
|
+
- **[Lighthouse](https://developer.chrome.com/docs/lighthouse)** proves the model this tool follows — automated, actionable audits against your local dev server — but security headers are a sidebar there. Its best-practices category includes a CSP-effectiveness check and a couple of related audits, and stops short of the full suite: no HSTS `max-age` grading, no Referrer-Policy, Permissions-Policy, or CORP checks, no flagging of `X-Powered-By`/`Server` version leaks — and no generated fix config. Lighthouse is to performance and accessibility what this tool aims to be for security headers.
|
|
32
|
+
- **Browser devtools** show you headers, but don't evaluate them or tell you what's missing.
|
|
33
|
+
- **Helmet's docs** tell you what to configure, but not what your app is *actually sending* after all your middleware runs.
|
|
34
|
+
|
|
35
|
+
By the time a production scanner flags the problem, the fix means a config change, a review, and a redeploy. The cheapest moment to fix a missing header is while the dev server is still running on your desk.
|
|
36
|
+
|
|
37
|
+
`header-grader` closes that gap with three ideas:
|
|
38
|
+
|
|
39
|
+
1. **Grade the dev server, not prod.** Point it at `localhost` while you're building.
|
|
40
|
+
2. **Don't just diagnose — generate the fix.** Every failing check maps to a concrete config snippet for Express (helmet) or Nginx. The snippet is a *minimal diff*: it only includes headers that are actually failing.
|
|
41
|
+
3. **Be dev-aware.** A production scanner would fail you for missing HSTS on `http://localhost:3000` — but browsers ignore HSTS over plain HTTP anyway. This tool knows the difference between "wrong" and "expected in dev, don't forget it in prod."
|
|
42
|
+
|
|
43
|
+
The project grew out of web development coursework: the same philosophy as accessibility auditing (Lighthouse, axe) applied to security headers — automated, actionable feedback inside the dev loop instead of after deployment.
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Node.js ≥ 18 (uses the built-in `fetch`)
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
No install needed for one-off checks:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
npx header-grader localhost:3000
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
For the middleware or repeated use, add it as a dev dependency:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npm install --save-dev header-grader
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or globally for a system-wide CLI:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
npm install -g header-grader
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> **Note:** until the package is published to npm, install from a local clone:
|
|
70
|
+
> ```sh
|
|
71
|
+
> git clone <repo-url> && cd header-grader
|
|
72
|
+
> npm install && npm run build && npm link # makes `header-grader` available globally
|
|
73
|
+
> ```
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
### 1. CLI — grade a running server
|
|
78
|
+
|
|
79
|
+
Start your dev server, then:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
header-grader localhost:3000
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
You get a letter grade (A+ through F), a 0–100 score, and a per-header breakdown explaining what each missing header protects against.
|
|
86
|
+
|
|
87
|
+
**Generate the fix** for whatever failed:
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
header-grader localhost:3000 --fix express
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
// npm install helmet
|
|
95
|
+
import helmet from "helmet";
|
|
96
|
+
|
|
97
|
+
app.use(
|
|
98
|
+
helmet({
|
|
99
|
+
contentSecurityPolicy: {
|
|
100
|
+
directives: {
|
|
101
|
+
defaultSrc: ["'self'"],
|
|
102
|
+
scriptSrc: ["'self'"], // add CDN origins here as needed
|
|
103
|
+
objectSrc: ["'none'"],
|
|
104
|
+
baseUri: ["'self'"],
|
|
105
|
+
frameAncestors: ["'self'"],
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
...
|
|
109
|
+
})
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Stop advertising Express:
|
|
113
|
+
app.disable("x-powered-by");
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Or for an Nginx reverse proxy:
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
header-grader localhost:3000 --fix nginx
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```nginx
|
|
123
|
+
# Add inside your server {} block
|
|
124
|
+
add_header Content-Security-Policy "default-src 'self'; ..." always;
|
|
125
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
126
|
+
server_tokens off;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Apply the snippet, re-run the command, and watch the grade climb.
|
|
130
|
+
|
|
131
|
+
**Understand the stakes** — `--explain` adds a concrete attack scenario under every failing header:
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
header-grader localhost:3000 --explain
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
✗ X-Frame-Options
|
|
139
|
+
Missing (and no CSP frame-ancestors). Your site can be framed for clickjacking.
|
|
140
|
+
If exploited:
|
|
141
|
+
Clickjacking: an attacker's page loads your site in an invisible
|
|
142
|
+
full-screen iframe and positions a fake 'Play video' button exactly
|
|
143
|
+
over your real 'Delete account' or 'Transfer funds' button. The victim
|
|
144
|
+
clicks their page but presses yours — with their logged-in session.
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
This is the teaching half of the tool: not just *what's* missing, but what an attacker does with the gap — session theft via injected scripts (CSP), sslstrip downgrades on public Wi-Fi (HSTS), stored XSS through file uploads (nosniff), reset-token leaks through the Referer header (Referrer-Policy), and so on.
|
|
148
|
+
|
|
149
|
+
**All CLI options:**
|
|
150
|
+
|
|
151
|
+
| Option | Description |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `--explain` | Show how each missing header could be exploited |
|
|
154
|
+
| `--fix <express\|nginx>` | Print a config snippet that fixes the failing headers |
|
|
155
|
+
| `--json` | Output the full report as JSON |
|
|
156
|
+
| `--min-grade <grade>` | Exit with code 1 if the grade is below this — for CI |
|
|
157
|
+
| `-h, --help` | Show help |
|
|
158
|
+
|
|
159
|
+
### 2. Express middleware — grade yourself on every boot
|
|
160
|
+
|
|
161
|
+
Instead of remembering to run a command, let your app grade itself in development:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
import helmet from "helmet";
|
|
165
|
+
import { headerGrader } from "header-grader/middleware";
|
|
166
|
+
|
|
167
|
+
app.use(helmet());
|
|
168
|
+
|
|
169
|
+
// Mount AFTER helmet/header middleware so it sees the final headers:
|
|
170
|
+
if (app.get("env") === "development") {
|
|
171
|
+
app.use(headerGrader());
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The first time your app serves an HTML response, the report prints to the console — then it stays quiet. It grades the headers your app *actually sends*, after all middleware has run, which catches misconfigurations that reading your helmet config never would.
|
|
176
|
+
|
|
177
|
+
Works with any Connect-compatible framework (Express, plain `node:http` handlers, etc.).
|
|
178
|
+
|
|
179
|
+
**Middleware options:**
|
|
180
|
+
|
|
181
|
+
| Option | Default | Description |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `watch` | `false` | Keep grading; reprint whenever the grade changes |
|
|
184
|
+
| `explain` | `false` | Include the attack scenario for each failing header |
|
|
185
|
+
| `onReport` | — | `(report) => void` — receive the report object instead of console output |
|
|
186
|
+
| `isLocalHttp` | `true` | Relax HSTS scoring (browsers ignore HSTS over plain HTTP) |
|
|
187
|
+
|
|
188
|
+
### 3. CI — enforce a minimum grade
|
|
189
|
+
|
|
190
|
+
Fail the build if headers regress:
|
|
191
|
+
|
|
192
|
+
```yaml
|
|
193
|
+
# .github/workflows/ci.yml (excerpt)
|
|
194
|
+
- run: npm start &
|
|
195
|
+
- run: npx wait-on http://localhost:3000
|
|
196
|
+
- run: npx header-grader http://localhost:3000 --min-grade B
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`--json` gives you a machine-readable report if you want custom tooling:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
header-grader localhost:3000 --json | jq .score
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 4. Programmatic API
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
import {
|
|
209
|
+
scan, // fetch a URL and grade it
|
|
210
|
+
gradeHeaders, // grade headers you already have (no network)
|
|
211
|
+
generateExpress,
|
|
212
|
+
generateNginx,
|
|
213
|
+
} from "header-grader";
|
|
214
|
+
|
|
215
|
+
const report = await scan("http://localhost:3000");
|
|
216
|
+
report.grade; // "F"
|
|
217
|
+
report.score; // 13
|
|
218
|
+
report.results; // per-header CheckResult[]: status, message, recommended value,
|
|
219
|
+
// and — for anything not passing — an `exploit` field with the
|
|
220
|
+
// concrete attack scenario (also present in --json output)
|
|
221
|
+
|
|
222
|
+
console.log(generateNginx(report));
|
|
223
|
+
|
|
224
|
+
// No network needed — useful in tests:
|
|
225
|
+
const r = gradeHeaders({ "x-content-type-options": "nosniff" }, { isLocalHttp: true });
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Full types (`Report`, `CheckResult`, `Grade`, `Rule`, …) are exported.
|
|
229
|
+
|
|
230
|
+
## What it checks
|
|
231
|
+
|
|
232
|
+
Weighted checks (contribute to the score):
|
|
233
|
+
|
|
234
|
+
| Header | Weight | What passes |
|
|
235
|
+
| --- | ---: | --- |
|
|
236
|
+
| `Content-Security-Policy` | 25 | Present, without `unsafe-inline`/`unsafe-eval` in scripts or wildcard sources |
|
|
237
|
+
| `Strict-Transport-Security` | 20 | `max-age` ≥ 180 days; `includeSubDomains` recommended. Relaxed on plain-HTTP localhost |
|
|
238
|
+
| `X-Content-Type-Options` | 10 | Exactly `nosniff` |
|
|
239
|
+
| `X-Frame-Options` | 10 | `DENY`/`SAMEORIGIN` — or CSP `frame-ancestors`, which supersedes it |
|
|
240
|
+
| `Referrer-Policy` | 10 | `strict-origin-when-cross-origin` or stricter |
|
|
241
|
+
| `Permissions-Policy` | 10 | Present (disable features you don't use) |
|
|
242
|
+
| `Cross-Origin-Opener-Policy` | 5 | Present (window isolation, Spectre-class protection) |
|
|
243
|
+
| `Cross-Origin-Resource-Policy` | 5 | Present (controls who may embed your resources) |
|
|
244
|
+
|
|
245
|
+
Hygiene penalties (subtract points):
|
|
246
|
+
|
|
247
|
+
| Header | Penalty | Why |
|
|
248
|
+
| --- | ---: | --- |
|
|
249
|
+
| `X-Powered-By` | −3 | Advertises your framework to attackers |
|
|
250
|
+
| `Server` with a version number | −3 | Advertises exact software versions |
|
|
251
|
+
| `X-XSS-Protection` (non-zero) | −2 | Deprecated; can *introduce* vulnerabilities. Use CSP instead |
|
|
252
|
+
|
|
253
|
+
**Grade scale:** A+ ≥ 95 · A ≥ 88 · B ≥ 75 · C ≥ 60 · D ≥ 45 · F below.
|
|
254
|
+
|
|
255
|
+
Notable grading behaviors:
|
|
256
|
+
|
|
257
|
+
- `unsafe-inline` is only flagged in `script-src` (or an inherited `default-src`) — inline *styles* are a much smaller risk and common in dev.
|
|
258
|
+
- CSP `frame-ancestors` satisfies the clickjacking check even without `X-Frame-Options`, matching modern browser behavior.
|
|
259
|
+
- Missing HSTS on `http://localhost` is a soft warning, not a failure — browsers ignore HSTS over HTTP, so punishing dev servers for it is noise.
|
|
260
|
+
|
|
261
|
+
## Design goals
|
|
262
|
+
|
|
263
|
+
- **Zero runtime dependencies.** The published package depends on nothing; `npx` startup stays fast and the supply-chain surface stays at zero.
|
|
264
|
+
- **One ruleset, three surfaces.** The CLI, the middleware, and the API all run the same rules, and the report and the generated snippets are derived from the same recommended values — they can never disagree.
|
|
265
|
+
- **Minimal-diff fixes.** Generated config only covers what's failing, so it composes with whatever you already have.
|
|
266
|
+
|
|
267
|
+
## Project structure
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
src/
|
|
271
|
+
├── types.ts # Report, CheckResult, Grade, Rule
|
|
272
|
+
├── rules.ts # one weighted rule per header + recommended values + exploit scenarios
|
|
273
|
+
├── grade.ts # weighted score → letter grade
|
|
274
|
+
├── scan.ts # fetch URL → headers → report
|
|
275
|
+
├── report.ts # ANSI terminal formatting
|
|
276
|
+
├── generators/
|
|
277
|
+
│ ├── express.ts # helmet config snippet
|
|
278
|
+
│ └── nginx.ts # add_header block
|
|
279
|
+
├── cli.ts # argument parsing + exit codes
|
|
280
|
+
├── middleware.ts # Express/Connect middleware
|
|
281
|
+
└── index.ts # public API
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Development
|
|
285
|
+
|
|
286
|
+
```sh
|
|
287
|
+
npm install
|
|
288
|
+
npm test # vitest — rules, generators, middleware (real HTTP server)
|
|
289
|
+
npm run typecheck # tsc --noEmit
|
|
290
|
+
npm run build # tsup → dist/ (ESM + CJS + .d.ts)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Try it against a deliberately bad server:
|
|
294
|
+
|
|
295
|
+
```sh
|
|
296
|
+
node -e 'require("http").createServer((q,s)=>{s.setHeader("Content-Type","text/html");s.end("hi")}).listen(3456)' &
|
|
297
|
+
node dist/cli.js localhost:3456 --fix express
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Roadmap
|
|
301
|
+
|
|
302
|
+
- [ ] Caddy and Apache config generators
|
|
303
|
+
- [ ] `--watch` CLI mode — re-grade automatically as you edit config
|
|
304
|
+
- [ ] CSP builder: crawl the page's actual script/style origins and propose a tailored policy
|
|
305
|
+
- [ ] `Report-Only` CSP suggestion mode for safe rollout
|
|
306
|
+
|
|
307
|
+
## License
|
|
308
|
+
|
|
309
|
+
MIT
|