@lhi/tdd-audit 1.11.0 → 1.14.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 +33 -7
- package/docs/rest-api.md +185 -35
- package/docs/scanner.md +13 -9
- package/docs/vulnerability-patterns.md +137 -1
- package/index.js +5 -0
- package/lib/badge.js +94 -0
- package/lib/jobs.js +53 -0
- package/lib/plugin.js +308 -0
- package/lib/remediator.js +8 -3
- package/lib/scanner.js +96 -3
- package/lib/server.js +57 -100
- package/package.json +12 -1
- package/prompts/auto-audit.md +76 -0
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# @lhi/tdd-audit
|
|
2
|
+
[](https://www.npmjs.com/package/@lhi/tdd-audit) <!-- tdd-audit-badge -->
|
|
2
3
|
|
|
3
|
-
> **v1.
|
|
4
|
+
> **v1.14.0** — Security skill installer for **Claude Code, Gemini CLI, Cursor, Codex, and OpenCode**. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol — prove the hole exists, apply the fix, prove it's closed.
|
|
4
5
|
|
|
5
6
|
## Install
|
|
6
7
|
|
|
@@ -10,7 +11,7 @@ npx @lhi/tdd-audit
|
|
|
10
11
|
|
|
11
12
|
On first run the installer:
|
|
12
13
|
|
|
13
|
-
1. Scans your codebase for **
|
|
14
|
+
1. Scans your codebase for **57 vulnerability patterns** across 6 scanner modules and prints a severity-ranked report
|
|
14
15
|
2. Scaffolds `__tests__/security/` with a framework-matched exploit test boilerplate
|
|
15
16
|
3. Adds `test:security` to `package.json`
|
|
16
17
|
4. Creates `.github/workflows/security-tests.yml` with SHA-pinned actions and `npm audit`
|
|
@@ -80,7 +81,7 @@ npx @lhi/tdd-audit serve --config ~/configs/prod-audit.json
|
|
|
80
81
|
## REST API + AI remediation
|
|
81
82
|
|
|
82
83
|
```bash
|
|
83
|
-
# Start the API server
|
|
84
|
+
# Start the API server (now powered by Fastify)
|
|
84
85
|
npx @lhi/tdd-audit serve --port 3000 --api-key YOUR_SECRET
|
|
85
86
|
|
|
86
87
|
# Scan any path → JSON
|
|
@@ -88,6 +89,19 @@ curl -X POST http://localhost:3000/scan \
|
|
|
88
89
|
-H "Authorization: Bearer YOUR_SECRET" \
|
|
89
90
|
-d '{"path": "."}' | jq '.summary'
|
|
90
91
|
|
|
92
|
+
# Full automated pipeline: scan + remediate in one shot
|
|
93
|
+
curl -X POST http://localhost:3000/audit \
|
|
94
|
+
-H "Authorization: Bearer YOUR_SECRET" \
|
|
95
|
+
-H "Content-Type: application/json" \
|
|
96
|
+
-d '{"path": ".", "provider": "anthropic", "apiKey": "sk-ant-..."}' \
|
|
97
|
+
| jq '.jobId'
|
|
98
|
+
|
|
99
|
+
# Poll job status
|
|
100
|
+
curl http://localhost:3000/jobs/<jobId>
|
|
101
|
+
|
|
102
|
+
# Or stream real-time updates via SSE
|
|
103
|
+
curl -N http://localhost:3000/jobs/<jobId>/stream
|
|
104
|
+
|
|
91
105
|
# Use any OpenAI-compatible service (Groq, OpenRouter, Together AI, etc.)
|
|
92
106
|
npx @lhi/tdd-audit serve \
|
|
93
107
|
--provider openai \
|
|
@@ -98,6 +112,17 @@ npx @lhi/tdd-audit serve \
|
|
|
98
112
|
|
|
99
113
|
Supported providers: `anthropic` · `openai` · `gemini` · `ollama` (local) · **any OpenAI-compatible endpoint via `--base-url`**
|
|
100
114
|
|
|
115
|
+
### Endpoints
|
|
116
|
+
|
|
117
|
+
| Method | Path | Auth | Description |
|
|
118
|
+
|---|---|---|---|
|
|
119
|
+
| `GET` | `/health` | No | Version + liveness check |
|
|
120
|
+
| `POST` | `/scan` | Yes | Scan a path, return findings |
|
|
121
|
+
| `POST` | `/remediate` | Yes | AI-fix a findings list; returns `jobId` |
|
|
122
|
+
| `POST` | `/audit` | Yes | Full scan+remediate pipeline; returns `jobId` |
|
|
123
|
+
| `GET` | `/jobs/:id` | Yes | Poll job status |
|
|
124
|
+
| `GET` | `/jobs/:id/stream` | Yes | SSE stream — real-time job progress |
|
|
125
|
+
|
|
101
126
|
## Output formats
|
|
102
127
|
|
|
103
128
|
```bash
|
|
@@ -108,15 +133,16 @@ npx @lhi/tdd-audit --scan # human-readable text (default)
|
|
|
108
133
|
|
|
109
134
|
## Testing
|
|
110
135
|
|
|
111
|
-
|
|
136
|
+
586 tests across unit, E2E, and security suites:
|
|
112
137
|
|
|
113
138
|
```bash
|
|
114
139
|
npm test # full suite
|
|
115
|
-
npm run test:unit # unit tests with coverage
|
|
140
|
+
npm run test:unit # unit tests with coverage (96.6% branch coverage)
|
|
116
141
|
npm run test:security # security regression tests only
|
|
142
|
+
npm run test:e2e # end-to-end REST API tests
|
|
117
143
|
```
|
|
118
144
|
|
|
119
|
-
Security tests cover prompt injection, path traversal, rate limiting, timing-safe auth, job store bounds, SARIF schema, and more. See [
|
|
145
|
+
Security tests cover prompt injection, path traversal, rate limiting, timing-safe auth, job store bounds, SARIF schema, and more. See [__tests__/security/](__tests__/security/) for all 22 regression tests.
|
|
120
146
|
|
|
121
147
|
## Documentation
|
|
122
148
|
|
|
@@ -125,7 +151,7 @@ Security tests cover prompt injection, path traversal, rate limiting, timing-saf
|
|
|
125
151
|
| [REST API](docs/rest-api.md) | Endpoints, auth, rate limiting, trust-proxy, request/response schema |
|
|
126
152
|
| [AI Remediation](docs/ai-remediation.md) | Provider setup, `--base-url` for compatible APIs, config file |
|
|
127
153
|
| [Scanner](docs/scanner.md) | Architecture, detection logic, false-positive handling |
|
|
128
|
-
| [Vulnerability Patterns](docs/vulnerability-patterns.md) | All
|
|
154
|
+
| [Vulnerability Patterns](docs/vulnerability-patterns.md) | All 57 patterns — descriptions, grep signatures, fix pointers |
|
|
129
155
|
| [TDD Protocol](docs/tdd-protocol.md) | Red-Green-Refactor in full, with framework templates for all 6 stacks |
|
|
130
156
|
| [Agentic AI Security](docs/agentic-ai-security.md) | ASI01–ASI10 — prompt injection, MCP supply chain, Actions injection |
|
|
131
157
|
| [Hardening](docs/hardening.md) | Phase 4 controls — Helmet, CSP, CSRF, rate limiting, gitleaks, SRI |
|
package/docs/rest-api.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# REST API
|
|
2
2
|
|
|
3
|
-
`tdd-audit serve` turns the scanner into an authenticated HTTP API
|
|
3
|
+
`tdd-audit serve` turns the scanner into an authenticated HTTP API built on **Fastify**. Use it to integrate vulnerability scanning and AI remediation into dashboards, CI pipelines, bots, or any tooling that speaks JSON.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -29,7 +29,7 @@ npx @lhi/tdd-audit serve --config ~/configs/prod.json
|
|
|
29
29
|
}
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
If `--api-key` / `serverApiKey` is omitted the server starts unauthenticated with a warning. Always set one in production.
|
|
32
|
+
If `--api-key` / `serverApiKey` is omitted the server starts unauthenticated with a stderr warning. Always set one in production.
|
|
33
33
|
|
|
34
34
|
---
|
|
35
35
|
|
|
@@ -45,13 +45,13 @@ Authorization: Bearer YOUR_SECRET
|
|
|
45
45
|
|
|
46
46
|
Missing or wrong key → `401 Unauthorized`.
|
|
47
47
|
|
|
48
|
-
Tokens are compared using **HMAC + `crypto.timingSafeEqual`** to prevent timing-oracle attacks.
|
|
48
|
+
Tokens are compared using **HMAC + `crypto.timingSafeEqual`** to prevent timing-oracle attacks (both values are HMAC-normalised before comparison so lengths are always equal).
|
|
49
49
|
|
|
50
50
|
### Rate limiting
|
|
51
51
|
|
|
52
|
-
All endpoints are rate-limited to **60 requests / IP / minute
|
|
52
|
+
All endpoints are rate-limited to **60 requests / IP / minute**. Exceeding the limit returns `429 Too Many Requests`.
|
|
53
53
|
|
|
54
|
-
By default the rate limiter keys on the **socket IP**, not `X-Forwarded-For`, to prevent header-spoofing bypasses. Enable proxy-forwarded IPs only
|
|
54
|
+
By default the rate limiter keys on the **socket IP**, not `X-Forwarded-For`, to prevent header-spoofing bypasses. Enable proxy-forwarded IPs only when you are behind a trusted reverse proxy:
|
|
55
55
|
|
|
56
56
|
```json
|
|
57
57
|
{ "trustProxy": true }
|
|
@@ -59,15 +59,16 @@ By default the rate limiter keys on the **socket IP**, not `X-Forwarded-For`, to
|
|
|
59
59
|
|
|
60
60
|
### Path validation
|
|
61
61
|
|
|
62
|
-
`POST /scan`
|
|
62
|
+
`POST /scan` and `POST /audit` validate that the requested path is inside the server's working directory. The check is normalised with a trailing path separator to prevent sibling-directory prefix bypasses (e.g. `/app-evil` cannot escape via `/app`). Paths outside cwd return `400`.
|
|
63
63
|
|
|
64
64
|
### Security headers
|
|
65
65
|
|
|
66
66
|
Every response includes:
|
|
67
67
|
|
|
68
68
|
```
|
|
69
|
-
|
|
70
|
-
X-
|
|
69
|
+
Content-Security-Policy: default-src 'none'
|
|
70
|
+
X-Content-Type-Options: nosniff
|
|
71
|
+
X-Frame-Options: DENY
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
---
|
|
@@ -79,14 +80,14 @@ X-Frame-Options: DENY
|
|
|
79
80
|
No auth required. Returns server status and version.
|
|
80
81
|
|
|
81
82
|
```json
|
|
82
|
-
{ "status": "ok", "version": "1.
|
|
83
|
+
{ "status": "ok", "version": "1.13.0" }
|
|
83
84
|
```
|
|
84
85
|
|
|
85
86
|
---
|
|
86
87
|
|
|
87
88
|
### `POST /scan`
|
|
88
89
|
|
|
89
|
-
Scan a local path and return structured findings.
|
|
90
|
+
Scan a local path and return structured findings synchronously.
|
|
90
91
|
|
|
91
92
|
**Request**
|
|
92
93
|
```json
|
|
@@ -104,13 +105,13 @@ Scan a local path and return structured findings.
|
|
|
104
105
|
**Response — JSON**
|
|
105
106
|
```json
|
|
106
107
|
{
|
|
107
|
-
"version":
|
|
108
|
-
"summary":
|
|
109
|
-
"findings":
|
|
108
|
+
"version": "1.13.0",
|
|
109
|
+
"summary": { "CRITICAL": 1, "HIGH": 3, "MEDIUM": 1, "LOW": 0 },
|
|
110
|
+
"findings": [ ... ],
|
|
110
111
|
"likelyFalsePositives": [ ... ],
|
|
111
|
-
"exempted":
|
|
112
|
-
"scannedAt":
|
|
113
|
-
"duration":
|
|
112
|
+
"exempted": [],
|
|
113
|
+
"scannedAt": "2026-03-25T12:00:00.000Z",
|
|
114
|
+
"duration": 42
|
|
114
115
|
}
|
|
115
116
|
```
|
|
116
117
|
|
|
@@ -122,7 +123,7 @@ Returns a SARIF 2.1.0 object ready to upload to GitHub code scanning.
|
|
|
122
123
|
|
|
123
124
|
| Status | Reason |
|
|
124
125
|
|---|---|
|
|
125
|
-
| 400 | Path traversal attempt,
|
|
126
|
+
| 400 | Path traversal attempt, oversized body (> 512 KB), or invalid JSON |
|
|
126
127
|
| 401 | Missing or invalid API key |
|
|
127
128
|
| 429 | Rate limit exceeded |
|
|
128
129
|
|
|
@@ -130,18 +131,18 @@ Returns a SARIF 2.1.0 object ready to upload to GitHub code scanning.
|
|
|
130
131
|
|
|
131
132
|
### `POST /remediate`
|
|
132
133
|
|
|
133
|
-
Queue an AI-powered remediation job
|
|
134
|
+
Queue an AI-powered remediation job for a **provided findings list**. Returns immediately with a `jobId`; poll `GET /jobs/:id` (or stream `GET /jobs/:id/stream`) for results.
|
|
134
135
|
|
|
135
|
-
|
|
136
|
+
Use `POST /audit` instead if you want the server to run the scan itself.
|
|
136
137
|
|
|
137
138
|
**Request**
|
|
138
139
|
```json
|
|
139
140
|
{
|
|
140
141
|
"findings": [ ... ],
|
|
141
|
-
"provider": "
|
|
142
|
-
"apiKey": "sk-...",
|
|
143
|
-
"model": "
|
|
144
|
-
"baseUrl":
|
|
142
|
+
"provider": "anthropic",
|
|
143
|
+
"apiKey": "sk-ant-...",
|
|
144
|
+
"model": "claude-opus-4-6",
|
|
145
|
+
"baseUrl": null,
|
|
145
146
|
"severity": "HIGH"
|
|
146
147
|
}
|
|
147
148
|
```
|
|
@@ -155,23 +156,69 @@ The server stores up to **1 000 jobs** in memory (TTL: 1 hour). Oldest jobs are
|
|
|
155
156
|
| `baseUrl` | no | Override base URL for any OpenAI-compatible service |
|
|
156
157
|
| `severity` | no | Minimum severity to fix. Default: `LOW` (fix all) |
|
|
157
158
|
|
|
158
|
-
**Response**
|
|
159
|
+
**Response — 202 Accepted**
|
|
159
160
|
```json
|
|
160
161
|
{ "jobId": "job_1_1711363200000" }
|
|
161
162
|
```
|
|
162
163
|
|
|
164
|
+
Job lifecycle: `pending → running → done | error`
|
|
165
|
+
|
|
163
166
|
---
|
|
164
167
|
|
|
165
|
-
### `
|
|
168
|
+
### `POST /audit`
|
|
169
|
+
|
|
170
|
+
Full automated pipeline: **scan + AI remediation in one shot**. No interaction needed. Returns immediately with a `jobId`.
|
|
166
171
|
|
|
167
|
-
|
|
172
|
+
If no `provider`/`apiKey` are supplied, the server runs the scan only (no remediation) and the job transitions to `done` with just the `findings` array.
|
|
168
173
|
|
|
169
|
-
**
|
|
174
|
+
**Request**
|
|
170
175
|
```json
|
|
171
|
-
{
|
|
176
|
+
{
|
|
177
|
+
"path": ".",
|
|
178
|
+
"provider": "anthropic",
|
|
179
|
+
"apiKey": "sk-ant-...",
|
|
180
|
+
"model": "claude-opus-4-6",
|
|
181
|
+
"baseUrl": null,
|
|
182
|
+
"webhook": "https://your-server.example.com/webhook"
|
|
183
|
+
}
|
|
172
184
|
```
|
|
173
185
|
|
|
174
|
-
|
|
186
|
+
| Field | Required | Description |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `path` | no | Path to scan. Defaults to cwd. Must be inside server cwd. |
|
|
189
|
+
| `provider` | no | If supplied with `apiKey`, AI remediation runs after the scan |
|
|
190
|
+
| `apiKey` | no | Provider API key |
|
|
191
|
+
| `model` | no | Defaults per provider |
|
|
192
|
+
| `baseUrl` | no | Override base URL for OpenAI-compatible providers |
|
|
193
|
+
| `webhook` | no | URL to POST the final job payload to when complete (fire-and-forget) |
|
|
194
|
+
|
|
195
|
+
**Response — 202 Accepted**
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
HTTP/1.1 202 Accepted
|
|
199
|
+
Location: /jobs/job_1_1711363200000
|
|
200
|
+
Retry-After: 2
|
|
201
|
+
```
|
|
202
|
+
```json
|
|
203
|
+
{ "jobId": "job_1_1711363200000" }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Job lifecycle: `pending → scanning → scanned → remediating → done | error`
|
|
207
|
+
|
|
208
|
+
Poll `GET /jobs/:id` or stream `GET /jobs/:id/stream` for progress.
|
|
209
|
+
|
|
210
|
+
**Job object during remediation**
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"id": "job_1_...",
|
|
214
|
+
"status": "remediating",
|
|
215
|
+
"total": 8,
|
|
216
|
+
"completed": 3,
|
|
217
|
+
"current": "SQL Injection"
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Job object when done**
|
|
175
222
|
```json
|
|
176
223
|
{
|
|
177
224
|
"id": "job_1_...",
|
|
@@ -179,6 +226,7 @@ Poll for remediation job status.
|
|
|
179
226
|
"createdAt": "...",
|
|
180
227
|
"startedAt": "...",
|
|
181
228
|
"completedAt": "...",
|
|
229
|
+
"findings": [ ... ],
|
|
182
230
|
"results": [
|
|
183
231
|
{
|
|
184
232
|
"finding": { ... },
|
|
@@ -193,28 +241,130 @@ Poll for remediation job status.
|
|
|
193
241
|
|
|
194
242
|
---
|
|
195
243
|
|
|
196
|
-
|
|
244
|
+
### `GET /jobs/:id`
|
|
245
|
+
|
|
246
|
+
Poll for job status. Works for jobs created by both `POST /remediate` and `POST /audit`.
|
|
247
|
+
|
|
248
|
+
**Response — pending / scanning**
|
|
249
|
+
```json
|
|
250
|
+
{ "id": "job_1_...", "status": "scanning", "createdAt": "..." }
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
**Response — remediating (with progress)**
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"id": "job_1_...",
|
|
257
|
+
"status": "remediating",
|
|
258
|
+
"total": 8,
|
|
259
|
+
"completed": 3,
|
|
260
|
+
"current": "SQL Injection"
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Response — done**
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"id": "job_1_...",
|
|
268
|
+
"status": "done",
|
|
269
|
+
"createdAt": "...",
|
|
270
|
+
"startedAt": "...",
|
|
271
|
+
"completedAt": "...",
|
|
272
|
+
"results": [ ... ]
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Response — error**
|
|
277
|
+
```json
|
|
278
|
+
{ "id": "job_1_...", "status": "error", "error": "Provider returned 401: ..." }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
The job store keeps up to **1 000 jobs** in memory (TTL: 1 hour). Oldest jobs are evicted when the cap is reached.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
### `GET /jobs/:id/stream`
|
|
197
286
|
|
|
198
|
-
|
|
287
|
+
Real-time job progress via **Server-Sent Events (SSE)**. The server pushes an event each time the job state changes, and closes the connection when the job reaches `done` or `error`.
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
curl -N http://localhost:3000/jobs/job_1_.../stream \
|
|
291
|
+
-H "Authorization: Bearer YOUR_SECRET"
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Event format**
|
|
295
|
+
```
|
|
296
|
+
data: {"id":"job_1_...","status":"scanning","createdAt":"..."}
|
|
297
|
+
|
|
298
|
+
data: {"id":"job_1_...","status":"scanned","findings":[...]}
|
|
299
|
+
|
|
300
|
+
data: {"id":"job_1_...","status":"remediating","total":8,"completed":1,"current":"SQL Injection"}
|
|
301
|
+
|
|
302
|
+
data: {"id":"job_1_...","status":"done","completedAt":"...","results":[...]}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The connection is closed automatically after the terminal state (`done` / `error`). If you connect to an already-completed job, the server pushes the current state and closes immediately.
|
|
306
|
+
|
|
307
|
+
**Node.js example using EventSource**
|
|
308
|
+
```javascript
|
|
309
|
+
const es = new EventSource(
|
|
310
|
+
'http://localhost:3000/jobs/job_1_.../stream',
|
|
311
|
+
{ headers: { Authorization: 'Bearer YOUR_SECRET' } }
|
|
312
|
+
);
|
|
313
|
+
es.onmessage = (e) => {
|
|
314
|
+
const job = JSON.parse(e.data);
|
|
315
|
+
if (job.status === 'done') { console.log(job.results); es.close(); }
|
|
316
|
+
if (job.status === 'error') { console.error(job.error); es.close(); }
|
|
317
|
+
};
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Full workflow examples
|
|
323
|
+
|
|
324
|
+
### curl — scan only
|
|
199
325
|
|
|
200
326
|
```bash
|
|
201
|
-
# Start server
|
|
202
327
|
npx @lhi/tdd-audit serve --port 3000 --api-key mysecret &
|
|
203
328
|
|
|
204
|
-
# Scan current directory
|
|
205
329
|
curl -s -X POST http://localhost:3000/scan \
|
|
206
330
|
-H "Authorization: Bearer mysecret" \
|
|
207
331
|
-H "Content-Type: application/json" \
|
|
208
332
|
-d '{"path": "."}' | jq '.summary'
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### curl — full pipeline with polling
|
|
209
336
|
|
|
210
|
-
|
|
337
|
+
```bash
|
|
338
|
+
# Kick off audit
|
|
339
|
+
JOB=$(curl -s -X POST http://localhost:3000/audit \
|
|
340
|
+
-H "Authorization: Bearer mysecret" \
|
|
341
|
+
-H "Content-Type: application/json" \
|
|
342
|
+
-d '{
|
|
343
|
+
"path": ".",
|
|
344
|
+
"provider": "anthropic",
|
|
345
|
+
"apiKey": "sk-ant-..."
|
|
346
|
+
}' | jq -r '.jobId')
|
|
347
|
+
|
|
348
|
+
# Poll until done
|
|
349
|
+
while true; do
|
|
350
|
+
STATUS=$(curl -s http://localhost:3000/jobs/$JOB \
|
|
351
|
+
-H "Authorization: Bearer mysecret" | jq -r '.status')
|
|
352
|
+
echo "Status: $STATUS"
|
|
353
|
+
[ "$STATUS" = "done" ] || [ "$STATUS" = "error" ] && break
|
|
354
|
+
sleep 2
|
|
355
|
+
done
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### curl — SARIF output for GitHub code scanning
|
|
359
|
+
|
|
360
|
+
```bash
|
|
211
361
|
curl -s -X POST http://localhost:3000/scan \
|
|
212
362
|
-H "Authorization: Bearer mysecret" \
|
|
213
363
|
-H "Content-Type: application/json" \
|
|
214
364
|
-d '{"path": ".", "format": "sarif"}' > results.sarif
|
|
215
365
|
```
|
|
216
366
|
|
|
217
|
-
### Node.js
|
|
367
|
+
### Node.js — scan
|
|
218
368
|
|
|
219
369
|
```javascript
|
|
220
370
|
const res = await fetch('http://localhost:3000/scan', {
|
package/docs/scanner.md
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
|
|
9
9
|
| Export | Purpose |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `quickScan(projectDir)` | Walk all source files and return a findings array |
|
|
11
|
+
| `quickScan(projectDir)` | Walk all source files and return a merged findings array |
|
|
12
12
|
| `scanPromptFiles(projectDir)` | Walk all `.md` prompt/skill files and check for prompt-specific patterns |
|
|
13
13
|
| `scanAppConfig(projectDir)` | Check `app.json` / `app.config.*` for embedded secrets |
|
|
14
14
|
| `scanAndroidManifest(projectDir)` | Check `AndroidManifest.xml` for `android:debuggable="true"` |
|
|
15
|
+
| `scanPackageJson(projectDir)` | Check `package.json` lifecycle scripts for supply-chain exfiltration (postinstall curl/wget) |
|
|
16
|
+
| `scanEnvFiles(projectDir)` | Check `.env*` files for `NEXT_PUBLIC_*SECRET/KEY/TOKEN` leaking secrets to the browser |
|
|
15
17
|
| `printFindings(findings, exempted)` | Format and print a findings report to stdout |
|
|
16
18
|
| `detectFramework(dir)` | Detect the test framework (`jest`, `vitest`, `mocha`, `pytest`, `go`, `flutter`) |
|
|
17
19
|
| `detectAppFramework(dir)` | Detect the UI framework (`nextjs`, `expo`, `react-native`, `react`, `flutter`) |
|
|
@@ -23,7 +25,7 @@
|
|
|
23
25
|
|
|
24
26
|
```
|
|
25
27
|
projectDir
|
|
26
|
-
└─ walkFiles()
|
|
28
|
+
└─ walkFiles() — yields source files (see Scanned extensions below)
|
|
27
29
|
└─ for each file:
|
|
28
30
|
1. Read file content (read-first, check length after — no TOCTOU)
|
|
29
31
|
2. Skip if content.length > 512 KB
|
|
@@ -32,12 +34,14 @@ projectDir
|
|
|
32
34
|
– If pattern matches, push finding with severity / name / file / line / snippet
|
|
33
35
|
– inTestFile: true if path is under a test directory
|
|
34
36
|
– likelyFalsePositive: true if inTestFile && pattern.skipInTests
|
|
35
|
-
└─ scanAppConfig()
|
|
36
|
-
└─ scanAndroidManifest() — checks android:debuggable
|
|
37
|
-
└─ scanPromptFiles()
|
|
37
|
+
└─ scanAppConfig() — checks app.json / app.config.* for embedded secret patterns
|
|
38
|
+
└─ scanAndroidManifest() — checks android:debuggable="true"
|
|
39
|
+
└─ scanPromptFiles() — walks .md files in agent config directories for prompt-specific patterns
|
|
40
|
+
└─ scanPackageJson() — checks postinstall/preinstall lifecycle scripts for curl/wget exfiltration
|
|
41
|
+
└─ scanEnvFiles() — checks .env* files for NEXT_PUBLIC_* keys with secret-sounding names
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
All
|
|
44
|
+
All six result sets are merged into one array and returned to the caller.
|
|
41
45
|
|
|
42
46
|
---
|
|
43
47
|
|
|
@@ -47,7 +51,7 @@ All four result sets are merged into one array and returned to the caller.
|
|
|
47
51
|
|
|
48
52
|
Yields scannable source files (`SCAN_EXTENSIONS`). Skips:
|
|
49
53
|
|
|
50
|
-
- **`SKIP_DIRS`**: `node_modules`, `.git`, `dist`, `build`, `.next`, `out`, `__pycache__`, `venv`, `.venv`, `vendor`, `.expo`, `.dart_tool`, `.pub-cache`
|
|
54
|
+
- **`SKIP_DIRS`**: `node_modules`, `.git`, `dist`, `build`, `coverage`, `.next`, `out`, `__pycache__`, `venv`, `.venv`, `vendor`, `.expo`, `.dart_tool`, `.pub-cache`
|
|
51
55
|
- **Symlinks** — never followed, preventing escape from the project root on shared/M-series filesystems
|
|
52
56
|
|
|
53
57
|
### `walkMdFiles(dir)`
|
|
@@ -58,9 +62,9 @@ Same skip rules, yields `.md` files only. Used by `scanPromptFiles`.
|
|
|
58
62
|
|
|
59
63
|
## Scanned extensions
|
|
60
64
|
|
|
61
|
-
`.js` `.ts` `.jsx` `.tsx` `.mjs` `.py` `.go` `.dart`
|
|
65
|
+
`.js` `.ts` `.jsx` `.tsx` `.mjs` `.py` `.go` `.dart` `.yml` `.yaml`
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
JSON and XML files are not walked by the code scanner. `package.json` is handled by `scanPackageJson()` and `.env*` files by `scanEnvFiles()` — both run as separate targeted checks. CI workflow files (`.yml`/`.yaml`) **are** now scanned by `walkFiles()` for GitHub Actions expression injection and similar patterns.
|
|
64
68
|
|
|
65
69
|
---
|
|
66
70
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vulnerability Patterns Reference
|
|
2
2
|
|
|
3
|
-
All
|
|
3
|
+
All 57 patterns detected by `@lhi/tdd-audit` across 6 scanner modules. Source patterns are checked against `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.py`, `.go`, `.dart`, `.yml`, and `.yaml` files line-by-line. Prompt/skill patterns are checked separately against `.md` files in agent configuration directories. Supply-chain patterns check `package.json`. NEXT_PUBLIC secret patterns also check `.env*` files.
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -198,3 +198,139 @@ These patterns are checked against `.md` files in `prompts/`, `skills/`, `.claud
|
|
|
198
198
|
**Grep signature:** `android:debuggable="true"`
|
|
199
199
|
**Why it matters:** Debug builds expose the app to `adb` inspection and arbitrary code injection on the device.
|
|
200
200
|
**Fix:** Remove `android:debuggable` from `AndroidManifest.xml` (the build system sets it correctly per variant).
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## AI / LLM Security (CRITICAL)
|
|
205
|
+
|
|
206
|
+
### LLM Prompt Injection (CRITICAL)
|
|
207
|
+
**Grep signature:** `{ role: "user", content: req.body... }`, `messages.push(req.body|query|params)`
|
|
208
|
+
**Why it matters:** Untrusted user input injected directly into LLM messages enables attackers to hijack model behavior, exfiltrate data, or bypass safety controls.
|
|
209
|
+
**Fix:** Sanitize and validate user input before insertion. Use a system-prompt separation layer. Never concatenate raw request data into the messages array.
|
|
210
|
+
|
|
211
|
+
### LLM Output Execution (CRITICAL)
|
|
212
|
+
**Grep signature:** `eval(response)`, `eval(result)`, `eval(output)`, `eval(completion)`
|
|
213
|
+
**Why it matters:** Executing model-generated code gives an attacker who controls the model's context (or poisons its training) arbitrary code execution on your server.
|
|
214
|
+
**Fix:** Never `eval()` LLM output. Use a sandboxed interpreter (Pyodide, isolated subprocess) or structured output schemas.
|
|
215
|
+
|
|
216
|
+
### LangChain ShellTool (CRITICAL)
|
|
217
|
+
**Grep signature:** `ShellTool()`, `LLMMathChain.from_llm(`, `PALChain.from_llm(`
|
|
218
|
+
**Why it matters:** These tools execute shell commands or `eval()` LLM-generated Python on the host system. A malicious prompt achieves full RCE (CVE-2023-29374, CVSS 9.8).
|
|
219
|
+
**Fix:** Remove `ShellTool` from agent toolkits. Replace `LLMMathChain`/`PALChain` with `numexpr` or sandboxed math evaluators.
|
|
220
|
+
|
|
221
|
+
### Dynamic Require (CRITICAL)
|
|
222
|
+
**Grep signature:** `require(req.query.`, `require(req.body.`, `require(req.params.`
|
|
223
|
+
**Why it matters:** Attacker controls which Node.js module is loaded — can load `child_process`, `fs`, or custom malicious modules.
|
|
224
|
+
**Fix:** Use a static map of allowed modules. Never pass user input to `require()`.
|
|
225
|
+
|
|
226
|
+
### VM Code Injection (CRITICAL)
|
|
227
|
+
**Grep signature:** `vm.runInNewContext(req.body`, `vm.runInContext(req.query`
|
|
228
|
+
**Why it matters:** Node.js's `vm` module is not a security boundary — sandbox escape is possible with crafted prototypes. Attacker achieves full RCE.
|
|
229
|
+
**Fix:** Use a proper sandbox (isolated-vm, Deno subprocess) for user-supplied code execution.
|
|
230
|
+
|
|
231
|
+
### node-serialize RCE (CRITICAL)
|
|
232
|
+
**Grep signature:** `require('node-serialize')`
|
|
233
|
+
**Why it matters:** `node-serialize` is known to be vulnerable to remote code execution via IIFE injection in serialized strings. Any use is an immediate CRITICAL risk.
|
|
234
|
+
**Fix:** Uninstall `node-serialize`. Use `JSON.parse()` / `JSON.stringify()` with schema validation.
|
|
235
|
+
|
|
236
|
+
### Hardcoded OpenAI Key (CRITICAL)
|
|
237
|
+
**Grep signature:** `'sk-proj-...'`, `'sk-...T3BlbkFJ...'` (≥60 chars)
|
|
238
|
+
**Note:** `skipInTests: true`
|
|
239
|
+
**Why it matters:** A committed API key gives anyone with repo access unlimited access to your OpenAI account and budget.
|
|
240
|
+
**Fix:** Use `process.env.OPENAI_API_KEY`. Rotate the key immediately. Run `gitleaks` on git history.
|
|
241
|
+
|
|
242
|
+
### Hardcoded Anthropic Key (CRITICAL)
|
|
243
|
+
**Grep signature:** `'sk-ant-api03-...'`
|
|
244
|
+
**Note:** `skipInTests: true`
|
|
245
|
+
**Why it matters:** Committed Anthropic API key leaks billing access and all Claude API capabilities.
|
|
246
|
+
**Fix:** Use environment variables. Rotate immediately via the Anthropic console.
|
|
247
|
+
|
|
248
|
+
### GitHub Actions Injection (CRITICAL)
|
|
249
|
+
**Files checked:** `.yml` and `.yaml` workflow files
|
|
250
|
+
**Grep signature:** `${{ github.event.pull_request.title }}`, `${{ github.head_ref }}`, `${{ github.event.issue.body }}`
|
|
251
|
+
**Why it matters:** These GitHub context values are attacker-controlled (PR title, branch name, issue body). Interpolating them into a `run:` step enables arbitrary command execution in your CI pipeline.
|
|
252
|
+
**Fix:** Use an intermediate environment variable: `env: TITLE: ${{ github.event.pull_request.title }}` then reference `$TITLE` in the shell script.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Electron / Desktop Security
|
|
257
|
+
|
|
258
|
+
### Electron nodeIntegration (CRITICAL)
|
|
259
|
+
**Grep signature:** `nodeIntegration: true`
|
|
260
|
+
**Why it matters:** Enables Node.js APIs in the renderer process. Any XSS in a web page loaded by the app achieves full system compromise.
|
|
261
|
+
**Fix:** Set `nodeIntegration: false` (default). Use `contextBridge` to expose specific APIs.
|
|
262
|
+
|
|
263
|
+
### Electron webSecurity Off (CRITICAL)
|
|
264
|
+
**Grep signature:** `webSecurity: false`
|
|
265
|
+
**Why it matters:** Disables the same-origin policy in the renderer, allowing cross-origin reads and mixed-content loads.
|
|
266
|
+
**Fix:** Never disable `webSecurity`. Fix CORS configuration on the server instead.
|
|
267
|
+
|
|
268
|
+
### Electron contextIsolation Off (HIGH)
|
|
269
|
+
**Grep signature:** `contextIsolation: false`
|
|
270
|
+
**Why it matters:** When context isolation is off, the renderer's JavaScript shares a prototype chain with the preload script, enabling prototype pollution attacks from web content.
|
|
271
|
+
**Fix:** Set `contextIsolation: true` (default since Electron 12). Use `contextBridge.exposeInMainWorld` for IPC.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## AI / LLM Security (HIGH)
|
|
276
|
+
|
|
277
|
+
### Header Injection (HIGH)
|
|
278
|
+
**Grep signature:** `res.setHeader(x, req.body|query|params)`, `res.set(x, req.body|query|params)`
|
|
279
|
+
**Why it matters:** Attacker can inject HTTP response headers, enabling cache poisoning, CORS bypass, or CSP override.
|
|
280
|
+
**Fix:** Validate and allowlist header values before passing to `res.setHeader()`.
|
|
281
|
+
|
|
282
|
+
### XPath Injection (HIGH)
|
|
283
|
+
**Grep signature:** `xpath.select(req.query|body|params`, `xpath.evaluate(req.`
|
|
284
|
+
**Why it matters:** Attacker can manipulate XPath queries to bypass authentication or extract arbitrary XML data.
|
|
285
|
+
**Fix:** Never concatenate user input into XPath expressions. Use parameterized XPath if your library supports it.
|
|
286
|
+
|
|
287
|
+
### Insecure Cookie (HIGH)
|
|
288
|
+
**Grep signature:** `httpOnly: false`
|
|
289
|
+
**Why it matters:** Cookies without `httpOnly` are readable via JavaScript, enabling session theft through XSS.
|
|
290
|
+
**Fix:** Set `httpOnly: true` on all session and auth cookies. Use `secure: true` in production.
|
|
291
|
+
|
|
292
|
+
### Credentials in AI Prompt (HIGH)
|
|
293
|
+
**Grep signature:** `system_prompt = "...mongodb://user:pass@..."`, `prompt += "...postgresql://...@..."`
|
|
294
|
+
**Why it matters:** Database connection strings with embedded passwords sent to external AI APIs expose credentials to the provider and any prompt logs.
|
|
295
|
+
**Fix:** Never include connection strings or credentials in prompts. Pass only sanitized, context-free data.
|
|
296
|
+
|
|
297
|
+
### LangChain Experimental (HIGH)
|
|
298
|
+
**Grep signature:** `from langchain_experimental`, `from 'langchain/experimental'`
|
|
299
|
+
**Why it matters:** The `langchain_experimental` package contains agents and chains with known RCE risk (PALChain, SQLDatabaseChain without sandboxing). It explicitly carries an "experimental" security disclaimer.
|
|
300
|
+
**Fix:** Audit each class imported from `langchain_experimental`. Replace PALChain/LLMMathChain with safe alternatives.
|
|
301
|
+
|
|
302
|
+
### Hardcoded HuggingFace Token (HIGH)
|
|
303
|
+
**Grep signature:** `'hf_...'` (≥30 chars)
|
|
304
|
+
**Note:** `skipInTests: true`
|
|
305
|
+
**Why it matters:** A committed HuggingFace token grants write access to model repos and private datasets.
|
|
306
|
+
**Fix:** Use `process.env.HF_TOKEN`. Rotate via huggingface.co/settings/tokens.
|
|
307
|
+
|
|
308
|
+
### NEXT_PUBLIC Secret (HIGH)
|
|
309
|
+
**Grep signature:** `NEXT_PUBLIC_SECRET_KEY`, `NEXT_PUBLIC_API_KEY`, `NEXT_PUBLIC_TOKEN`, etc. in code and `.env*` files
|
|
310
|
+
**Note:** `skipInTests: true`; also checked in `.env`, `.env.local`, `.env.production`, `.env.development`
|
|
311
|
+
**Why it matters:** Variables prefixed with `NEXT_PUBLIC_` are inlined into the client-side JavaScript bundle at build time. Any secret with this prefix is shipped to every browser.
|
|
312
|
+
**Fix:** Remove `NEXT_PUBLIC_` prefix from secret variables. Access them only server-side via `getServerSideProps` or API routes.
|
|
313
|
+
|
|
314
|
+
### Trojan Source (HIGH)
|
|
315
|
+
**Grep signature:** Unicode bidi control characters (U+202A–U+202E, U+2066–U+2069) in source code
|
|
316
|
+
**Why it matters:** CVE-2021-42574 — bidi control characters cause the code displayed in editors/diffs to differ from what the compiler executes, hiding malicious logic in plain sight.
|
|
317
|
+
**Fix:** Configure your editor and linter to detect and reject bidi characters in source files.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Prompt / Skill / Agent Patterns (expanded)
|
|
322
|
+
|
|
323
|
+
### MCP Tool Poisoning (HIGH)
|
|
324
|
+
**Grep signature:** `"description": "ignore previous instructions..."`, `"description": "override instructions..."`
|
|
325
|
+
**Why it matters:** Malicious MCP servers embed instructions in tool description fields. When an AI agent reads the tool list, it executes the injected instructions — redirecting actions, exfiltrating data, or bypassing safety checks.
|
|
326
|
+
**Fix:** Audit all MCP server tool descriptions. Use only servers from trusted sources. Pin MCP servers to verified commits.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Supply Chain / Package Patterns
|
|
331
|
+
|
|
332
|
+
### Supply Chain Exfiltration (CRITICAL)
|
|
333
|
+
**Files checked:** `package.json` `scripts.postinstall` / `scripts.preinstall`
|
|
334
|
+
**Grep signature:** `"postinstall": "curl https://..."`, `"preinstall": "wget http://..."`
|
|
335
|
+
**Why it matters:** A postinstall script that shells out to `curl`/`wget` can silently exfiltrate environment variables, `.env` files, or SSH keys to an attacker's server the moment anyone installs your package or its parent.
|
|
336
|
+
**Fix:** Remove network calls from lifecycle scripts. If data collection is needed, make it explicit and user-consented, never automatic on install.
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
} = require('./lib/scanner');
|
|
14
14
|
const { toJson, toSarif, toText } = require('./lib/reporter');
|
|
15
15
|
const { writeInitConfig } = require('./lib/config');
|
|
16
|
+
const { badgeLine, injectBadge } = require('./lib/badge');
|
|
16
17
|
|
|
17
18
|
const args = process.argv.slice(2);
|
|
18
19
|
const isLocal = args.includes('--local');
|
|
@@ -87,6 +88,7 @@ if (scanOnly) {
|
|
|
87
88
|
process.stdout.write('\n');
|
|
88
89
|
printFindings(findings, exempted);
|
|
89
90
|
}
|
|
91
|
+
injectBadge(projectDir, badgeLine(findings));
|
|
90
92
|
process.exit(0);
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -243,6 +245,9 @@ if (!skipScan) {
|
|
|
243
245
|
const findings = quickScan(projectDir);
|
|
244
246
|
process.stdout.write('\n');
|
|
245
247
|
printFindings(findings);
|
|
248
|
+
const badge = badgeLine(findings);
|
|
249
|
+
injectBadge(projectDir, badge);
|
|
250
|
+
console.log('✅ README badge updated');
|
|
246
251
|
}
|
|
247
252
|
|
|
248
253
|
console.log(`\nSkill installed to ${path.relative(os.homedir(), targetSkillDir)}`);
|