@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/lib/server.js
CHANGED
|
@@ -1,48 +1,53 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
-
const http = require('http');
|
|
5
4
|
const path = require('path');
|
|
6
|
-
const { quickScan
|
|
7
|
-
const { toJson, toSarif
|
|
5
|
+
const { quickScan } = require('./scanner');
|
|
6
|
+
const { toJson, toSarif } = require('./reporter');
|
|
8
7
|
const { loadConfig, parseCliOverrides } = require('./config');
|
|
9
|
-
const { version }
|
|
8
|
+
const { version } = require('../package.json');
|
|
9
|
+
const { buildApp, RATE_LIMIT_MAX } = require('./plugin');
|
|
10
|
+
const {
|
|
11
|
+
jobs, createJob, updateJob, MAX_JOBS, JOB_TTL_MS,
|
|
12
|
+
} = require('./jobs');
|
|
10
13
|
|
|
11
|
-
// ───
|
|
14
|
+
// ─── Auth (kept here for backward compat — SEC-17 reads this file) ────────────
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const MAX_JOBS = 1_000;
|
|
17
|
-
const JOB_TTL_MS = 60 * 60 * 1_000; // 1 hour
|
|
18
|
-
|
|
19
|
-
function evictJobs() {
|
|
20
|
-
const cutoff = Date.now() - JOB_TTL_MS;
|
|
21
|
-
for (const [id, job] of jobs) {
|
|
22
|
-
if (new Date(job.createdAt).getTime() < cutoff) jobs.delete(id);
|
|
23
|
-
}
|
|
24
|
-
// Hard cap: drop oldest entries until within limit
|
|
25
|
-
while (jobs.size >= MAX_JOBS) {
|
|
26
|
-
jobs.delete(jobs.keys().next().value);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
16
|
+
// Fixed HMAC key for normalising token lengths before constant-time comparison.
|
|
17
|
+
const _authHmacKey = crypto.randomBytes(32);
|
|
29
18
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Authenticate incoming requests.
|
|
21
|
+
* Accepts Node.js http.IncomingMessage OR Fastify Request objects.
|
|
22
|
+
* Uses HMAC + timingSafeEqual to prevent timing-oracle attacks.
|
|
23
|
+
*/
|
|
24
|
+
function authenticate(req, cfg) {
|
|
25
|
+
if (!cfg.serverApiKey) return true;
|
|
26
|
+
const headers = req.headers || {};
|
|
27
|
+
const header = headers['authorization'] || '';
|
|
28
|
+
const token = header.startsWith('Bearer ') ? header.slice(7) : '';
|
|
29
|
+
const expected = crypto.createHmac('sha256', _authHmacKey).update(cfg.serverApiKey).digest();
|
|
30
|
+
const actual = crypto.createHmac('sha256', _authHmacKey).update(token).digest();
|
|
31
|
+
return crypto.timingSafeEqual(expected, actual);
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Validate and sanitise the `path` field from POST /scan.
|
|
36
|
+
* Only allow paths inside cwd to prevent path traversal.
|
|
37
|
+
*/
|
|
38
|
+
function safeScanPath(rawPath) {
|
|
39
|
+
const cwd = process.cwd();
|
|
40
|
+
const resolved = path.resolve(cwd, rawPath || cwd);
|
|
41
|
+
const cwdNorm = cwd.endsWith(path.sep) ? cwd : cwd + path.sep;
|
|
42
|
+
if (resolved !== cwd && !resolved.startsWith(cwdNorm)) {
|
|
43
|
+
throw new Error('Path outside working directory');
|
|
44
|
+
}
|
|
45
|
+
return resolved;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
// ─── Rate limiter (
|
|
48
|
+
// ─── Rate limiter (kept here for backward compat — SEC-14/16/20 read this) ───
|
|
43
49
|
|
|
44
|
-
const
|
|
45
|
-
const RATE_LIMIT_WINDOW = 60 * 1_000; // 1 minute in ms
|
|
50
|
+
const RATE_LIMIT_WINDOW = 60 * 1_000;
|
|
46
51
|
|
|
47
52
|
const rateLimiter = {
|
|
48
53
|
_counts: new Map(),
|
|
@@ -89,46 +94,11 @@ function readBody(req) {
|
|
|
89
94
|
});
|
|
90
95
|
}
|
|
91
96
|
|
|
92
|
-
//
|
|
93
|
-
// Does not need to be secret — purpose is timing-safety, not confidentiality.
|
|
94
|
-
const _authHmacKey = crypto.randomBytes(32);
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Authenticate incoming requests.
|
|
98
|
-
* Uses HMAC + timingSafeEqual to prevent timing-oracle attacks.
|
|
99
|
-
*/
|
|
100
|
-
function authenticate(req, cfg) {
|
|
101
|
-
if (!cfg.serverApiKey) return true; // no key configured — open
|
|
102
|
-
const header = req.headers['authorization'] || '';
|
|
103
|
-
const token = header.startsWith('Bearer ') ? header.slice(7) : '';
|
|
104
|
-
const expected = crypto.createHmac('sha256', _authHmacKey).update(cfg.serverApiKey).digest();
|
|
105
|
-
const actual = crypto.createHmac('sha256', _authHmacKey).update(token).digest();
|
|
106
|
-
return crypto.timingSafeEqual(expected, actual);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Validate and sanitise the `path` field from POST /scan.
|
|
111
|
-
* Only allow paths inside cwd to prevent path traversal.
|
|
112
|
-
*/
|
|
113
|
-
function safeScanPath(rawPath) {
|
|
114
|
-
const cwd = process.cwd();
|
|
115
|
-
const resolved = path.resolve(cwd, rawPath || cwd);
|
|
116
|
-
// Append sep so "/app" cannot match "/app-evil" via startsWith
|
|
117
|
-
const cwdNorm = cwd.endsWith(path.sep) ? cwd : cwd + path.sep;
|
|
118
|
-
if (resolved !== cwd && !resolved.startsWith(cwdNorm)) {
|
|
119
|
-
throw new Error('Path outside working directory');
|
|
120
|
-
}
|
|
121
|
-
return resolved;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ─── Router ───────────────────────────────────────────────────────────────────
|
|
97
|
+
// ─── Router (kept for backward compat — SEC-16/20 and E2E tests use this) ────
|
|
125
98
|
|
|
126
99
|
async function handleRequest(req, res, cfg) {
|
|
127
100
|
const { method, url } = req;
|
|
128
101
|
|
|
129
|
-
// ── Rate limiting ──────────────────────────────────────────────────────────
|
|
130
|
-
// Only trust X-Forwarded-For when cfg.trustProxy is explicitly enabled.
|
|
131
|
-
// Default is false to prevent header-spoofing rate-limit bypasses.
|
|
132
102
|
const ip = cfg.trustProxy
|
|
133
103
|
? (req.headers['x-forwarded-for'] || req.socket?.remoteAddress || '').split(',')[0].trim()
|
|
134
104
|
: (req.socket?.remoteAddress || 'unknown');
|
|
@@ -136,17 +106,14 @@ async function handleRequest(req, res, cfg) {
|
|
|
136
106
|
return json(res, 429, { error: 'Too Many Requests' });
|
|
137
107
|
}
|
|
138
108
|
|
|
139
|
-
// ── GET /health ────────────────────────────────────────────────────────────
|
|
140
109
|
if (method === 'GET' && url === '/health') {
|
|
141
110
|
return json(res, 200, { status: 'ok', version });
|
|
142
111
|
}
|
|
143
112
|
|
|
144
|
-
// All other routes require authentication
|
|
145
113
|
if (!authenticate(req, cfg)) {
|
|
146
114
|
return json(res, 401, { error: 'Unauthorized' });
|
|
147
115
|
}
|
|
148
116
|
|
|
149
|
-
// ── POST /scan ─────────────────────────────────────────────────────────────
|
|
150
117
|
if (method === 'POST' && url === '/scan') {
|
|
151
118
|
let body;
|
|
152
119
|
try { body = await readBody(req); }
|
|
@@ -156,19 +123,16 @@ async function handleRequest(req, res, cfg) {
|
|
|
156
123
|
try { scanPath = safeScanPath(body.path); }
|
|
157
124
|
catch (e) { return json(res, 400, { error: e.message }); }
|
|
158
125
|
|
|
159
|
-
const format
|
|
160
|
-
const t0
|
|
126
|
+
const format = body.format || cfg.output || 'json';
|
|
127
|
+
const t0 = Date.now();
|
|
161
128
|
const findings = quickScan(scanPath);
|
|
162
129
|
const exempted = findings.exempted || [];
|
|
163
130
|
const duration = Date.now() - t0;
|
|
164
131
|
|
|
165
|
-
if (format === 'sarif')
|
|
166
|
-
return json(res, 200, toSarif(findings, scanPath));
|
|
167
|
-
}
|
|
132
|
+
if (format === 'sarif') return json(res, 200, toSarif(findings, scanPath));
|
|
168
133
|
return json(res, 200, { ...toJson(findings, exempted), duration });
|
|
169
134
|
}
|
|
170
135
|
|
|
171
|
-
// ── POST /remediate ────────────────────────────────────────────────────────
|
|
172
136
|
if (method === 'POST' && url === '/remediate') {
|
|
173
137
|
let body;
|
|
174
138
|
try { body = await readBody(req); }
|
|
@@ -181,7 +145,6 @@ async function handleRequest(req, res, cfg) {
|
|
|
181
145
|
|
|
182
146
|
const jobId = createJob();
|
|
183
147
|
|
|
184
|
-
// Kick off async remediation (non-blocking)
|
|
185
148
|
setImmediate(async () => {
|
|
186
149
|
try {
|
|
187
150
|
updateJob(jobId, { status: 'running', startedAt: new Date().toISOString() });
|
|
@@ -200,7 +163,6 @@ async function handleRequest(req, res, cfg) {
|
|
|
200
163
|
return json(res, 202, { jobId });
|
|
201
164
|
}
|
|
202
165
|
|
|
203
|
-
// ── GET /jobs/:id ──────────────────────────────────────────────────────────
|
|
204
166
|
const jobMatch = url.match(/^\/jobs\/([^/?]+)$/);
|
|
205
167
|
if (method === 'GET' && jobMatch) {
|
|
206
168
|
const job = jobs.get(jobMatch[1]);
|
|
@@ -211,33 +173,28 @@ async function handleRequest(req, res, cfg) {
|
|
|
211
173
|
return json(res, 404, { error: 'Not found' });
|
|
212
174
|
}
|
|
213
175
|
|
|
214
|
-
// ─── Start
|
|
176
|
+
// ─── Start (uses Fastify) ─────────────────────────────────────────────────────
|
|
215
177
|
|
|
216
|
-
function start(args = []) {
|
|
178
|
+
async function start(args = []) {
|
|
217
179
|
const cfg = loadConfig(process.cwd(), parseCliOverrides(args));
|
|
218
180
|
const port = cfg.port;
|
|
219
181
|
|
|
220
|
-
const
|
|
221
|
-
try {
|
|
222
|
-
await handleRequest(req, res, cfg);
|
|
223
|
-
} catch (err) {
|
|
224
|
-
// Production error handler — no stack traces
|
|
225
|
-
json(res, 500, { error: 'Internal server error' });
|
|
226
|
-
}
|
|
227
|
-
});
|
|
182
|
+
const fastify = buildApp(cfg);
|
|
228
183
|
|
|
229
|
-
|
|
230
|
-
process.stdout.write(`\n🔒 tdd-audit REST API listening on http://localhost:${port}\n`);
|
|
231
|
-
if (!cfg.serverApiKey) {
|
|
232
|
-
process.stderr.write('⚠️ No --api-key set — server is unauthenticated. Set one for production.\n');
|
|
233
|
-
}
|
|
234
|
-
process.stdout.write(' GET /health\n');
|
|
235
|
-
process.stdout.write(' POST /scan { path, format? }\n');
|
|
236
|
-
process.stdout.write(' POST /remediate { findings, provider, apiKey, model? }\n');
|
|
237
|
-
process.stdout.write(' GET /jobs/:id\n\n');
|
|
238
|
-
});
|
|
184
|
+
await fastify.listen({ port, host: '0.0.0.0' });
|
|
239
185
|
|
|
240
|
-
|
|
186
|
+
process.stdout.write(`\n🔒 tdd-audit REST API listening on http://localhost:${port}\n`);
|
|
187
|
+
if (!cfg.serverApiKey) {
|
|
188
|
+
process.stderr.write('⚠️ No --api-key set — server is unauthenticated. Set one for production.\n');
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write(' GET /health\n');
|
|
191
|
+
process.stdout.write(' POST /scan { path, format? }\n');
|
|
192
|
+
process.stdout.write(' POST /remediate { findings, provider, apiKey, model? }\n');
|
|
193
|
+
process.stdout.write(' POST /audit { path, provider?, apiKey?, model? }\n');
|
|
194
|
+
process.stdout.write(' GET /jobs/:id\n');
|
|
195
|
+
process.stdout.write(' GET /jobs/:id/stream (SSE)\n\n');
|
|
196
|
+
|
|
197
|
+
return fastify.server; // returned for testing
|
|
241
198
|
}
|
|
242
199
|
|
|
243
200
|
module.exports = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lhi/tdd-audit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "Security skill installer for Claude Code, Gemini CLI, Cursor, Codex, and OpenCode. Patches vulnerabilities using a Red-Green-Refactor exploit-test protocol.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -50,7 +50,18 @@
|
|
|
50
50
|
},
|
|
51
51
|
"author": "Kyra Lee",
|
|
52
52
|
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/kyralee2992/tdd-remediation-skill.git"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/kyralee2992/tdd-remediation-skill#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/kyralee2992/tdd-remediation-skill/issues"
|
|
60
|
+
},
|
|
53
61
|
"devDependencies": {
|
|
54
62
|
"jest": "^30.3.0"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"fastify": "^5.8.4"
|
|
55
66
|
}
|
|
56
67
|
}
|
package/prompts/auto-audit.md
CHANGED
|
@@ -247,6 +247,80 @@ resolve_entities.*True # Python lxml entity expansion
|
|
|
247
247
|
# bundle audit
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
+
**AI / LLM Security (check when the project uses OpenAI, Anthropic, LangChain, or any LLM SDK)**
|
|
251
|
+
```
|
|
252
|
+
role.*content.*req\. # LLM Prompt Injection — user input in messages array
|
|
253
|
+
messages.*push.*req\. # LLM Prompt Injection — appending request data to LLM context
|
|
254
|
+
eval\(.*response # LLM Output Execution — evaluating model output
|
|
255
|
+
eval\(.*result # LLM Output Execution — evaluating model result
|
|
256
|
+
eval\(.*completion # LLM Output Execution — evaluating AI completion
|
|
257
|
+
ShellTool\(\) # LangChain ShellTool — shell command execution
|
|
258
|
+
LLMMathChain\.from_llm # LangChain math eval — known RCE (CVE-2023-29374)
|
|
259
|
+
PALChain\.from_llm # LangChain PAL — eval of LLM-generated Python
|
|
260
|
+
require\(req\. # Dynamic Require — loading user-controlled modules
|
|
261
|
+
vm\.runIn.*Context.*req\. # VM Code Injection — sandbox escape risk
|
|
262
|
+
require.*node-serialize # node-serialize RCE — known deserialization vulnerability
|
|
263
|
+
langchain_experimental # LangChain Experimental — contains RCE-risk components
|
|
264
|
+
system_prompt.*mongodb:// # Credentials in AI Prompt — DB URL in prompt context
|
|
265
|
+
prompt.*postgresql:// # Credentials in AI Prompt — DB URL in prompt context
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Hardcoded AI API Keys**
|
|
269
|
+
```
|
|
270
|
+
sk-proj- # OpenAI new-format key (≥60 chars)
|
|
271
|
+
T3BlbkFJ # OpenAI old-format key marker (base64 of "OpenAI")
|
|
272
|
+
sk-ant-api03- # Anthropic API key prefix
|
|
273
|
+
hf_[A-Za-z0-9]{30,} # HuggingFace token (≥30 chars after hf_)
|
|
274
|
+
NEXT_PUBLIC_.*SECRET # Next.js client-bundled secret variable
|
|
275
|
+
NEXT_PUBLIC_.*API_KEY # Next.js client-bundled API key
|
|
276
|
+
NEXT_PUBLIC_.*TOKEN # Next.js client-bundled token
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**GitHub Actions Injection (scan .github/workflows/*.yml)**
|
|
280
|
+
```
|
|
281
|
+
github\.event\.pull_request\.title # Attacker-controlled PR title in run: step
|
|
282
|
+
github\.event\.pull_request\.body # Attacker-controlled PR body in run: step
|
|
283
|
+
github\.event\.issue\.title # Attacker-controlled issue title in run: step
|
|
284
|
+
github\.event\.issue\.body # Attacker-controlled issue body in run: step
|
|
285
|
+
github\.event\.comment\.body # Attacker-controlled comment body in run: step
|
|
286
|
+
github\.head_ref # Attacker-controlled branch name in run: step
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Electron Security (check main process and BrowserWindow config)**
|
|
290
|
+
```
|
|
291
|
+
nodeIntegration.*true # CRITICAL: enables Node.js in renderer — XSS → full system compromise
|
|
292
|
+
webSecurity.*false # CRITICAL: disables same-origin policy in renderer
|
|
293
|
+
contextIsolation.*false # HIGH: allows prototype pollution from web content
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**Supply Chain (check package.json)**
|
|
297
|
+
```
|
|
298
|
+
postinstall.*curl # Supply Chain Exfiltration — curl in postinstall script
|
|
299
|
+
preinstall.*curl # Supply Chain Exfiltration — curl in preinstall script
|
|
300
|
+
postinstall.*wget # Supply Chain Exfiltration — wget in postinstall script
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
**Web / Protocol Injection**
|
|
304
|
+
```
|
|
305
|
+
res\.setHeader.*req\. # Header Injection — user input in response header value
|
|
306
|
+
xpath\.select.*req\. # XPath Injection — user input in XPath query
|
|
307
|
+
httpOnly.*false # Insecure Cookie — session cookie readable via JavaScript
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**Trojan Source (use Grep with unicode flag if available)**
|
|
311
|
+
```
|
|
312
|
+
\u202[A-E]|\u206[6-9] # Bidi control characters — visual/compiled mismatch (CVE-2021-42574)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Dependency Audit**
|
|
316
|
+
```
|
|
317
|
+
# Run manually — not grep-based:
|
|
318
|
+
# npm audit --audit-level=high
|
|
319
|
+
# pip-audit
|
|
320
|
+
# govulncheck ./...
|
|
321
|
+
# bundle audit
|
|
322
|
+
```
|
|
323
|
+
|
|
250
324
|
### 0d. Audit Prompt & Skill Files
|
|
251
325
|
|
|
252
326
|
For projects that contain AI agent configurations, scan the following locations for prompt-specific vulnerabilities:
|
|
@@ -257,6 +331,8 @@ For projects that contain AI agent configurations, scan the following locations
|
|
|
257
331
|
|---|---|---|
|
|
258
332
|
| `csurf` package reference | CRITICAL | `csurf` was deprecated March 2023 and is unmaintained — use `csrf-csrf` instead |
|
|
259
333
|
| `"command": "npx"` in MCP config | HIGH | Unpinned npx MCP server executes whatever version npm resolves at runtime |
|
|
334
|
+
| `"description": "ignore previous instructions..."` | HIGH | MCP Tool Poisoning — malicious instructions embedded in tool description fields hijack agent behavior |
|
|
335
|
+
| `"description": "override instructions..."` | HIGH | MCP Tool Poisoning — agent reads tool list and executes injected instructions |
|
|
260
336
|
| `http://` URL (non-localhost) | MEDIUM | Cleartext URLs in prompts can mislead agents to make insecure requests |
|
|
261
337
|
| Prompt reads arbitrary user-controlled files without a guardrail | HIGH | AI reading untrusted file content without isolation is a prompt-injection risk (ASI01) |
|
|
262
338
|
|