@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/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, scanPromptFiles } = require('./scanner');
7
- const { toJson, toSarif, toText } = require('./reporter');
5
+ const { quickScan } = require('./scanner');
6
+ const { toJson, toSarif } = require('./reporter');
8
7
  const { loadConfig, parseCliOverrides } = require('./config');
9
- const { version } = require('../package.json');
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
- // ─── Job store (in-memory) ────────────────────────────────────────────────────
14
+ // ─── Auth (kept here for backward compat — SEC-17 reads this file) ────────────
12
15
 
13
- const jobs = new Map();
14
- let jobSeq = 0;
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
- function createJob() {
31
- evictJobs();
32
- const id = `job_${++jobSeq}_${Date.now()}`;
33
- jobs.set(id, { id, status: 'pending', createdAt: new Date().toISOString() });
34
- return id;
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
- function updateJob(id, patch) {
38
- const job = jobs.get(id);
39
- if (job) jobs.set(id, { ...job, ...patch });
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 (in-memory, per-IP sliding window) ─────────────────────────
48
+ // ─── Rate limiter (kept here for backward compat — SEC-14/16/20 read this) ───
43
49
 
44
- const RATE_LIMIT_MAX = 60; // requests per window
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
- // Fixed HMAC key for normalising token lengths before constant-time comparison.
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 = body.format || cfg.output || 'json';
160
- const t0 = Date.now();
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 server = http.createServer(async (req, res) => {
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
- server.listen(port, () => {
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
- return server; // returned for testing
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.11.0",
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
  }
@@ -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