pnpm-audit-hook 1.0.1 → 1.0.4

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.
Files changed (2) hide show
  1. package/README.md +369 -124
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # pnpm-audit-hook
2
2
 
3
- A pnpm hook that audits dependencies for vulnerabilities **before packages are downloaded**. It queries the GitHub Advisory Database for vulnerabilities and optionally enriches severity data from NVD, blocking installs when critical or high severity issues are found.
3
+ A pnpm hook that audits dependencies for vulnerabilities **before packages are downloaded**. It queries the GitHub Advisory Database and uses a bundled static vulnerability database, blocking installs when critical or high severity issues are found.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -10,6 +10,204 @@ pnpm add -D pnpm-audit-hook && pnpm exec pnpm-audit-setup
10
10
 
11
11
  Done! Every `pnpm install` will now audit packages before downloading.
12
12
 
13
+ ## How It Works
14
+
15
+ ### Overview
16
+
17
+ ```mermaid
18
+ flowchart LR
19
+ A[pnpm install] --> B[Resolve Dependencies]
20
+ B --> C[.pnpmfile.cjs Hook]
21
+ C --> D{Audit Packages}
22
+ D -->|Safe| E[Download & Install]
23
+ D -->|Vulnerable| F[Block Install]
24
+ ```
25
+
26
+ When you run `pnpm install`, the hook intercepts the process **after dependency resolution but before downloading**. This means vulnerable packages are blocked without ever being downloaded to your machine.
27
+
28
+ ### Detailed Flow
29
+
30
+ ```mermaid
31
+ flowchart TD
32
+ subgraph PNPM["pnpm install"]
33
+ A[Start] --> B[Resolve dependency graph]
34
+ B --> C[Generate lockfile]
35
+ end
36
+
37
+ subgraph HOOK["pnpm-audit-hook"]
38
+ C --> D[".pnpmfile.cjs<br/>afterAllResolved()"]
39
+ D --> E[Extract packages from lockfile]
40
+ E --> F[Load config from .pnpm-audit.yaml]
41
+ F --> G{Check cache}
42
+ G -->|Cache hit| H[Use cached results]
43
+ G -->|Cache miss| I[Query vulnerability sources]
44
+
45
+ subgraph SOURCES["Vulnerability Sources"]
46
+ I --> J[Static DB<br/>Historical vulns]
47
+ I --> K[GitHub Advisory API<br/>Recent vulns]
48
+ J --> L[Merge & deduplicate]
49
+ K --> L
50
+ L --> M{Unknown severity?}
51
+ M -->|Yes| N[Enrich from NVD]
52
+ M -->|No| O[Continue]
53
+ N --> O
54
+ end
55
+
56
+ H --> P[Apply policy rules]
57
+ O --> P
58
+ P --> Q{Check allowlist}
59
+ Q -->|Allowed| R[Skip]
60
+ Q -->|Not allowed| S{Severity check}
61
+ S -->|critical/high| T[BLOCK]
62
+ S -->|medium/low| U[WARN]
63
+ S -->|unknown| U
64
+ end
65
+
66
+ subgraph RESULT["Result"]
67
+ T --> V[Throw error<br/>Abort install]
68
+ U --> W[Log warnings]
69
+ R --> W
70
+ W --> X[Continue install]
71
+ X --> Y[Download packages]
72
+ end
73
+ ```
74
+
75
+ ### Installation Changes
76
+
77
+ When you run `pnpm exec pnpm-audit-setup`, these files are created in your project:
78
+
79
+ | File | Purpose |
80
+ |------|---------|
81
+ | `.pnpmfile.cjs` | pnpm hook entry point - intercepts `pnpm install` |
82
+ | `.pnpm-audit.yaml` | Optional configuration file (created if missing) |
83
+ | `.pnpm-audit-cache/` | Cache directory (created automatically at runtime) |
84
+
85
+ ### File Structure After Installation
86
+
87
+ ```
88
+ your-project/
89
+ ├── .pnpmfile.cjs # Hook that pnpm loads automatically
90
+ ├── .pnpm-audit.yaml # Your security policy config (optional)
91
+ ├── .pnpm-audit-cache/ # Cached vulnerability data (auto-created)
92
+ ├── node_modules/
93
+ │ └── pnpm-audit-hook/ # The installed package
94
+ │ ├── dist/ # Compiled audit logic
95
+ │ └── .pnpmfile.cjs # Template hook file
96
+ ├── package.json
97
+ └── pnpm-lock.yaml
98
+ ```
99
+
100
+ ## Vulnerability Sources
101
+
102
+ ```mermaid
103
+ flowchart TD
104
+ subgraph PRIMARY["Primary Source"]
105
+ A[Static Database] --> C[Merged Results]
106
+ B[GitHub Advisory API] --> C
107
+ end
108
+
109
+ subgraph ENRICHMENT["Severity Enrichment"]
110
+ C --> D{Severity = unknown?}
111
+ D -->|Yes| E[Query NVD API]
112
+ D -->|No| F[Final Results]
113
+ E --> F
114
+ end
115
+
116
+ style A fill:#90EE90
117
+ style B fill:#87CEEB
118
+ style E fill:#FFE4B5
119
+ ```
120
+
121
+ | Source | Type | Description | Rate Limits |
122
+ |--------|------|-------------|-------------|
123
+ | **Static DB** | Bundled | Historical vulnerabilities (2020-2025), works offline | None |
124
+ | **GitHub Advisory** | API | Real-time vulnerability data from GHSA | 60/hr (no token), 5000/hr (with token) |
125
+ | **NVD** | API | Severity enrichment only (CVSS scores) | 5/30s (no key), 50/30s (with key) |
126
+
127
+ ### Query Strategy
128
+
129
+ ```mermaid
130
+ sequenceDiagram
131
+ participant H as Hook
132
+ participant C as Cache
133
+ participant S as Static DB
134
+ participant G as GitHub API
135
+ participant N as NVD API
136
+
137
+ H->>C: Check cache for package@version
138
+ alt Cache hit (not expired)
139
+ C-->>H: Return cached vulnerabilities
140
+ else Cache miss
141
+ H->>S: Query historical vulns (before cutoff)
142
+ S-->>H: Historical findings
143
+ H->>G: Query recent vulns (after cutoff)
144
+ G-->>H: Recent findings
145
+ H->>H: Merge & deduplicate
146
+
147
+ opt Has unknown severity
148
+ H->>N: Enrich severity data
149
+ N-->>H: CVSS scores
150
+ end
151
+
152
+ H->>C: Cache results (TTL based on severity)
153
+ end
154
+ ```
155
+
156
+ ## Blocking Policy
157
+
158
+ ### Default Policy
159
+
160
+ ```yaml
161
+ policy:
162
+ block: # Abort install if found
163
+ - critical
164
+ - high
165
+ warn: # Log warning but continue
166
+ - medium
167
+ - low
168
+ - unknown
169
+ ```
170
+
171
+ ### Policy Decision Flow
172
+
173
+ ```mermaid
174
+ flowchart TD
175
+ A[Vulnerability Found] --> B{In allowlist?}
176
+ B -->|Yes, not expired| C[ALLOW - Skip]
177
+ B -->|No or expired| D{Severity level?}
178
+
179
+ D -->|critical| E[BLOCK]
180
+ D -->|high| E
181
+ D -->|medium| F[WARN]
182
+ D -->|low| F
183
+ D -->|unknown| F
184
+
185
+ E --> G[Collect all blocks]
186
+ F --> H[Collect all warnings]
187
+ C --> I[Continue]
188
+
189
+ G --> J{Any blocks?}
190
+ J -->|Yes| K[Throw Error<br/>Abort pnpm install]
191
+ J -->|No| L[Log warnings]
192
+ H --> L
193
+ L --> M[Continue install]
194
+
195
+ style E fill:#FF6B6B
196
+ style F fill:#FFE66D
197
+ style C fill:#90EE90
198
+ style K fill:#FF6B6B
199
+ ```
200
+
201
+ ### Severity Levels
202
+
203
+ | Severity | CVSS Score | Default Action | Example |
204
+ |----------|------------|----------------|---------|
205
+ | **critical** | 9.0 - 10.0 | Block | Remote code execution |
206
+ | **high** | 7.0 - 8.9 | Block | Authentication bypass |
207
+ | **medium** | 4.0 - 6.9 | Warn | Information disclosure |
208
+ | **low** | 0.1 - 3.9 | Warn | Minor information leak |
209
+ | **unknown** | N/A | Warn | Severity not determined |
210
+
13
211
  ## Installation
14
212
 
15
213
  ### Per-Project (Recommended)
@@ -108,30 +306,27 @@ performance:
108
306
 
109
307
  cache:
110
308
  ttlSeconds: 3600
111
- ```
112
-
113
- All fields are optional. Set any source to `false` to disable it.
114
309
 
115
- ### Configuration Constraints
116
-
117
- The following validation rules are applied to configuration values:
118
-
119
- | Setting | Constraint | Default |
120
- |---------|------------|---------|
121
- | `performance.timeoutMs` | 1 to 300,000 ms (5 minutes max) | 15,000 |
122
- | `cache.ttlSeconds` | 1 to 86,400 seconds (24 hours max) | 3,600 |
123
- | `staticBaseline.cutoffDate` | Valid ISO date format, must not be in the future | 2025-12-31 |
124
-
125
- Invalid values are silently replaced with defaults to ensure safe operation.
310
+ staticBaseline:
311
+ enabled: true
312
+ cutoffDate: "2025-12-31"
313
+ ```
126
314
 
127
- ## Vulnerability Sources
315
+ All fields are optional. Defaults are applied for missing values.
128
316
 
129
- | Source | Description | Auth |
130
- |--------|-------------|------|
131
- | **GitHub Advisory** | Primary source - GitHub Security Advisory database (GHSA) | Optional |
132
- | **NVD** | Severity enrichment only - NIST National Vulnerability Database | Optional |
317
+ ### Configuration Options
133
318
 
134
- GitHub Advisory is the primary vulnerability source. NVD provides additional severity metadata but does not add new vulnerability entries.
319
+ | Option | Description | Default |
320
+ |--------|-------------|---------|
321
+ | `policy.block` | Severities that abort install | `["critical", "high"]` |
322
+ | `policy.warn` | Severities that log warnings | `["medium", "low", "unknown"]` |
323
+ | `policy.allowlist` | Exceptions to skip | `[]` |
324
+ | `sources.github` | Enable GitHub Advisory | `true` |
325
+ | `sources.nvd` | Enable NVD enrichment | `true` |
326
+ | `performance.timeoutMs` | API timeout (1-300,000) | `15000` |
327
+ | `cache.ttlSeconds` | Cache duration (1-86,400) | `3600` |
328
+ | `staticBaseline.enabled` | Use bundled vuln database | `true` |
329
+ | `staticBaseline.cutoffDate` | Static DB coverage date | `2025-12-31` |
135
330
 
136
331
  ## Allowlist
137
332
 
@@ -140,42 +335,78 @@ Suppress specific vulnerabilities or packages:
140
335
  ```yaml
141
336
  policy:
142
337
  allowlist:
338
+ # By CVE/GHSA ID
143
339
  - id: CVE-2024-12345
144
340
  reason: "False positive for our use case"
341
+
342
+ # By package name
145
343
  - package: legacy-lib
146
344
  reason: "Accepted risk"
147
345
  expires: "2025-06-01"
346
+
347
+ # Scoped: specific CVE in specific package
348
+ - id: CVE-2024-12345
349
+ package: affected-pkg
350
+ version: ">=1.0.0 <2.0.0" # Optional version constraint
351
+ reason: "Only affects unused feature"
148
352
  ```
149
353
 
150
- - `id` - CVE or GHSA identifier to ignore (case-insensitive)
151
- - `package` - Package name to ignore entirely (case-insensitive)
152
- - If both `id` and `package` are set, **both must match** (scoped allowlist)
153
- - `reason` - Why it's allowed (for audit trail)
154
- - `expires` - ISO date when the allowlist entry expires
354
+ | Field | Required | Description |
355
+ |-------|----------|-------------|
356
+ | `id` | One of id/package | CVE or GHSA identifier (case-insensitive) |
357
+ | `package` | One of id/package | Package name to ignore (case-insensitive) |
358
+ | `version` | No | Semver range constraint |
359
+ | `reason` | No | Audit trail documentation |
360
+ | `expires` | No | ISO date when entry expires |
155
361
 
156
362
  ## Environment Variables
157
363
 
158
364
  | Variable | Description |
159
365
  |----------|-------------|
160
- | `PNPM_AUDIT_CONFIG_PATH` | Override config file location |
366
+ | `GITHUB_TOKEN` / `GH_TOKEN` | GitHub API token (higher rate limits) |
367
+ | `NVD_API_KEY` / `NIST_NVD_API_KEY` | NVD API key (higher rate limits) |
368
+ | `PNPM_AUDIT_CONFIG_PATH` | Custom config file location |
161
369
  | `PNPM_AUDIT_DISABLE_GITHUB` | Disable GitHub Advisory source |
162
- | `GITHUB_TOKEN` | GitHub API token (optional) |
163
- | `GH_TOKEN` | Alternative to GITHUB_TOKEN |
164
- | `NVD_API_KEY` | NVD API key (optional) |
165
- | `NIST_NVD_API_KEY` | Alternative to NVD_API_KEY |
166
- | `PNPM_AUDIT_QUIET` | Suppress info/warn output (`true` to enable) |
167
- | `PNPM_AUDIT_DEBUG` | Enable debug logging (`true` to enable) |
168
- | `PNPM_AUDIT_JSON` | Enable JSON output format (`true` to enable) |
370
+ | `PNPM_AUDIT_QUIET` | Suppress info/warn output |
371
+ | `PNPM_AUDIT_DEBUG` | Enable debug logging |
372
+ | `PNPM_AUDIT_JSON` | JSON output format |
373
+
374
+ ## Caching
375
+
376
+ ```mermaid
377
+ flowchart LR
378
+ A[Package Query] --> B{Cache exists?}
379
+ B -->|Yes| C{Expired?}
380
+ C -->|No| D[Return cached]
381
+ C -->|Yes| E[Query APIs]
382
+ B -->|No| E
383
+ E --> F[Cache with TTL]
384
+ F --> G[Return results]
385
+
386
+ style D fill:#90EE90
387
+ ```
169
388
 
170
- ## How It Works
389
+ ### Cache Location
390
+
391
+ ```
392
+ .pnpm-audit-cache/
393
+ ├── ab/
394
+ │ └── ab1234...def.json # Cached by SHA256 hash
395
+ ├── cd/
396
+ │ └── cd5678...ghi.json
397
+ └── ...
398
+ ```
171
399
 
172
- 1. pnpm resolves the full dependency graph
173
- 2. `.pnpmfile.cjs` hook runs `afterAllResolved()` before downloads
174
- 3. The hook queries GitHub Advisory (and optionally NVD for severity enrichment)
175
- 4. Findings are deduplicated and checked against the severity policy
176
- 5. If any blocking vulnerabilities exist, pnpm aborts the install
400
+ ### Dynamic TTL
177
401
 
178
- **Note:** The `.pnpmfile.cjs` file must be in your workspace root directory.
402
+ Cache duration varies by severity to balance freshness and performance:
403
+
404
+ | Severity | TTL | Reason |
405
+ |----------|-----|--------|
406
+ | Critical | 15 min | Need fast response for active threats |
407
+ | High | 30 min | Important but less urgent |
408
+ | Medium | 1 hour | Standard caching |
409
+ | Low/Unknown | Config TTL | Use configured default |
179
410
 
180
411
  ## CI/CD Integration
181
412
 
@@ -207,61 +438,109 @@ The hook runs automatically during `pnpm install` and will fail the job if block
207
438
 
208
439
  The hook includes a bundled database of historical vulnerabilities (2020-2025) that enables faster audits and reduced API calls.
209
440
 
210
- ### How It Works
211
-
212
- - **Historical vulnerabilities** (before the cutoff date) are served from the bundled static database
213
- - **New vulnerabilities** (after the cutoff date) are fetched from live APIs
214
- - This hybrid approach provides offline capability for historical data while ensuring fresh data for recent disclosures
215
-
216
441
  ### Benefits
217
442
 
218
443
  - **Faster audits**: No API calls needed for known historical vulnerabilities
219
- - **Reduced API calls**: Only new vulnerabilities require network requests
220
- - **Offline capability**: Historical vulnerability checks work without internet access
221
- - **Rate limit friendly**: Minimizes API usage against GitHub and NVD
222
-
223
- ### Configuration
224
-
225
- Enable or disable the static baseline in `.pnpm-audit.yaml`:
226
-
227
- ```yaml
228
- staticBaseline:
229
- enabled: true
230
- cutoffDate: "2025-12-31"
231
- dataPath: "node_modules/pnpm-audit-hook/dist/static-db/data" # optional custom path
232
- ```
233
-
234
- - `enabled` - Whether to use the static database (default: `true`)
235
- - `cutoffDate` - Vulnerabilities published before this date use the static database (must be valid ISO format, not in future)
236
- - `dataPath` - Optional custom path to static data directory (default: bundled data)
444
+ - **Offline capability**: Historical vulnerability checks work without internet
445
+ - **Rate limit friendly**: Minimizes API usage
446
+ - **Reliable**: Not affected by API outages for historical data
237
447
 
238
448
  ### Updating the Database
239
449
 
240
- Update the bundled vulnerability database monthly to capture new disclosures:
241
-
242
450
  ```bash
243
- # Full rebuild of the vulnerability database
244
- pnpm run update-vuln-db
245
-
246
- # Incremental update (faster, adds only new vulnerabilities)
451
+ # Incremental update (recommended)
247
452
  pnpm run update-vuln-db:incremental
248
- ```
249
453
 
250
- After updating, rebuild and commit the changes:
454
+ # Full rebuild
455
+ pnpm run update-vuln-db
251
456
 
252
- ```bash
457
+ # Rebuild and commit
253
458
  pnpm run build
254
459
  git add src/static-db/data/ dist/static-db/data/
255
460
  git commit -m "chore: update vulnerability database"
256
461
  ```
257
462
 
258
- ### Update Workflow
463
+ ## Architecture
464
+
465
+ ```mermaid
466
+ classDiagram
467
+ class PnpmHook {
468
+ +afterAllResolved(lockfile, context)
469
+ }
470
+
471
+ class AuditEngine {
472
+ +runAudit(lockfile, runtime)
473
+ -extractPackages(lockfile)
474
+ -aggregateVulnerabilities(packages)
475
+ -evaluatePolicies(findings)
476
+ }
477
+
478
+ class VulnerabilitySource {
479
+ <<interface>>
480
+ +query(packageName, version)
481
+ }
482
+
483
+ class StaticDatabase {
484
+ +query(packageName, version)
485
+ -loadShard(packageName)
486
+ -bloomFilter
487
+ }
488
+
489
+ class GitHubAdvisory {
490
+ +query(packageName, version)
491
+ -fetchFromAPI()
492
+ -rateLimiter
493
+ }
494
+
495
+ class NVDEnricher {
496
+ +enrichSeverity(findings)
497
+ -fetchCVSS(cveId)
498
+ }
499
+
500
+ class PolicyEngine {
501
+ +evaluate(findings, config)
502
+ -checkAllowlist(finding)
503
+ -checkSeverity(finding)
504
+ }
505
+
506
+ class FileCache {
507
+ +get(key)
508
+ +set(key, value, ttl)
509
+ +prune()
510
+ }
511
+
512
+ PnpmHook --> AuditEngine
513
+ AuditEngine --> VulnerabilitySource
514
+ AuditEngine --> PolicyEngine
515
+ AuditEngine --> FileCache
516
+ VulnerabilitySource <|.. StaticDatabase
517
+ VulnerabilitySource <|.. GitHubAdvisory
518
+ GitHubAdvisory --> NVDEnricher
519
+ ```
520
+
521
+ ## Security Model
522
+
523
+ ### Fail-Closed Design
259
524
 
260
- 1. Run `pnpm run update-vuln-db:incremental` monthly
261
- 2. Optionally extend `cutoffDate` in your config to include newer static data
262
- 3. Commit the updated `data/` directory to your repository
525
+ The hook uses a **fail-closed** security model:
263
526
 
264
- ## Local Development with pnpm
527
+ | Condition | Behavior |
528
+ |-----------|----------|
529
+ | API failure | Block install (configurable) |
530
+ | Invalid allowlist entry | Entry ignored (treated as not allowed) |
531
+ | Expired allowlist | Entry ignored |
532
+ | Unknown severity | Treated as "warn" (configurable) |
533
+ | Invalid semver in vuln data | Treated as potentially affected |
534
+
535
+ ### Security Features
536
+
537
+ - **Pre-download blocking**: Vulnerable code never reaches your machine
538
+ - **No credential storage**: API keys only from environment variables
539
+ - **Path traversal protection**: Validates all file paths
540
+ - **Symlink attack prevention**: Detects symlinks in cache
541
+ - **Atomic cache writes**: Prevents partial/corrupted cache files
542
+
543
+ ## Local Development
265
544
 
266
545
  ### Setup
267
546
 
@@ -272,29 +551,7 @@ pnpm install
272
551
  pnpm run build
273
552
  ```
274
553
 
275
- ### Test in Another Project (pnpm link)
276
-
277
- ```bash
278
- # In pnpm-audit-hook directory
279
- pnpm link --global
280
-
281
- # In your target project
282
- pnpm link --global pnpm-audit-hook
283
-
284
- # Copy the hook file to your project root
285
- cp node_modules/pnpm-audit-hook/.pnpmfile.cjs .
286
-
287
- # Edit .pnpmfile.cjs to point to linked package
288
- # Change: path.join(__dirname, 'dist', 'index.js')
289
- # To: path.join(__dirname, 'node_modules', 'pnpm-audit-hook', 'dist', 'index.js')
290
-
291
- # Test it
292
- pnpm add lodash
293
- ```
294
-
295
- ### Test Directly in This Repo
296
-
297
- The `.pnpmfile.cjs` already points to `./dist`, so you can test directly:
554
+ ### Test Directly
298
555
 
299
556
  ```bash
300
557
  pnpm run build
@@ -302,33 +559,21 @@ pnpm add lodash # Safe package
302
559
  pnpm add event-stream@3.3.6 # Vulnerable - should be blocked
303
560
  ```
304
561
 
305
- ### Development Workflow
562
+ ### Run Tests
306
563
 
307
564
  ```bash
308
- # Make changes to src/
309
- pnpm run build
310
-
311
- # Run tests
312
565
  pnpm test
313
-
314
- # Test the hook manually
315
- pnpm add some-package
316
566
  ```
317
567
 
318
- ### Unlink After Testing
319
-
320
- ```bash
321
- # In your target project
322
- pnpm unlink pnpm-audit-hook
323
- rm .pnpmfile.cjs
568
+ ## Exit Codes
324
569
 
325
- # In pnpm-audit-hook directory
326
- pnpm unlink --global
327
- ```
570
+ | Code | Meaning |
571
+ |------|---------|
572
+ | 0 | Success - no blocking vulnerabilities |
573
+ | 1 | Blocked - critical/high vulnerabilities found |
574
+ | 2 | Warnings - medium/low vulnerabilities found |
575
+ | 3 | Source error - API failure (fail-closed) |
328
576
 
329
- ## Build
577
+ ## License
330
578
 
331
- ```bash
332
- pnpm install
333
- pnpm run build
334
- ```
579
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pnpm-audit-hook",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "pnpm hook that blocks vulnerable packages before download. Uses GitHub Advisory Database with offline static DB fallback.",
5
5
  "license": "MIT",
6
6
  "author": "asx8678",