@lhi/tdd-audit 1.10.0 → 1.11.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/index.js CHANGED
@@ -48,10 +48,12 @@ const targetTestDir = path.join(projectDir, testBaseDir, 'security');
48
48
  // ─── Init mode early exit ────────────────────────────────────────────────────
49
49
 
50
50
  if (args[0] === 'init') {
51
- const destArg = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
52
- const force = args.includes('--force');
51
+ const destArg = args[1] && !args[1].startsWith('-') ? args[1] : undefined;
52
+ const force = args.includes('--force');
53
+ const providerIdx = args.indexOf('--provider');
54
+ const provider = providerIdx !== -1 ? args[providerIdx + 1] : 'openai';
53
55
  try {
54
- const written = writeInitConfig(destArg, force);
56
+ const written = writeInitConfig(destArg, force, provider);
55
57
  console.log(`✅ Created ${path.relative(process.cwd(), written)}`);
56
58
  console.log(' Edit it, then run: node index.js serve or node index.js --scan');
57
59
  } catch (e) {
package/lib/config.js CHANGED
@@ -19,12 +19,36 @@ const DEFAULTS = {
19
19
  trustProxy: false, // trust X-Forwarded-For for rate limiting
20
20
  };
21
21
 
22
+ // Provider-specific defaults for `tdd-audit init --provider <name>`
23
+ const PROVIDER_TEMPLATES = {
24
+ openai: {
25
+ provider: 'openai',
26
+ model: 'gpt-4o',
27
+ apiKeyEnv: 'OPENAI_API_KEY',
28
+ baseUrl: null,
29
+ },
30
+ anthropic: {
31
+ provider: 'anthropic',
32
+ model: 'claude-opus-4-6',
33
+ apiKeyEnv: 'ANTHROPIC_API_KEY',
34
+ baseUrl: null,
35
+ },
36
+ gemini: {
37
+ provider: 'gemini',
38
+ model: 'gemini-2.0-flash',
39
+ apiKeyEnv: 'GEMINI_API_KEY',
40
+ baseUrl: null,
41
+ },
42
+ ollama: {
43
+ provider: 'ollama',
44
+ model: 'llama3',
45
+ apiKeyEnv: null,
46
+ baseUrl: 'http://localhost:11434',
47
+ },
48
+ };
49
+
22
50
  // Template written by `tdd-audit init`
23
51
  const INIT_TEMPLATE = {
24
- provider: 'openai',
25
- model: 'gpt-4o',
26
- apiKeyEnv: 'OPENAI_API_KEY',
27
- baseUrl: null,
28
52
  output: 'text',
29
53
  severityThreshold: 'LOW',
30
54
  port: 3000,
@@ -102,15 +126,23 @@ function parseCliOverrides(args) {
102
126
  *
103
127
  * @param {string} [destPath]
104
128
  * @param {boolean} [force=false]
129
+ * @param {string} [provider='openai']
105
130
  * @returns {string}
106
131
  */
107
- function writeInitConfig(destPath, force = false) {
132
+ function writeInitConfig(destPath, force = false, provider = 'openai') {
133
+ const providerDefaults = PROVIDER_TEMPLATES[provider];
134
+ if (!providerDefaults) {
135
+ throw new Error(
136
+ `Unknown provider "${provider}". Valid options: ${Object.keys(PROVIDER_TEMPLATES).join(', ')}`
137
+ );
138
+ }
108
139
  const target = destPath || path.join(process.cwd(), CONFIG_FILE);
109
140
  if (fs.existsSync(target) && !force) {
110
141
  throw new Error(`${target} already exists. Pass --force to overwrite.`);
111
142
  }
112
- fs.writeFileSync(target, JSON.stringify(INIT_TEMPLATE, null, 2) + '\n', 'utf8');
143
+ const template = { ...providerDefaults, ...INIT_TEMPLATE };
144
+ fs.writeFileSync(target, JSON.stringify(template, null, 2) + '\n', 'utf8');
113
145
  return target;
114
146
  }
115
147
 
116
- module.exports = { loadConfig, parseCliOverrides, writeInitConfig, DEFAULTS, INIT_TEMPLATE, CONFIG_FILE };
148
+ module.exports = { loadConfig, parseCliOverrides, writeInitConfig, DEFAULTS, INIT_TEMPLATE, PROVIDER_TEMPLATES, CONFIG_FILE };
package/lib/remediator.js CHANGED
@@ -68,15 +68,32 @@ function sanitizeSnippet(raw) {
68
68
  .trim();
69
69
  }
70
70
 
71
+ /**
72
+ * Sanitize a scalar finding field (name, file, line, severity) before
73
+ * embedding it in an AI prompt. Strips null bytes and collapses newlines
74
+ * so that attacker-controlled metadata cannot inject top-level instructions.
75
+ */
76
+ function sanitizeField(raw) {
77
+ const s = typeof raw === 'string' ? raw : String(raw ?? '');
78
+ return s
79
+ .replace(/\x00/g, '')
80
+ .replace(/[\r\n]+/g, ' ')
81
+ .trim();
82
+ }
83
+
71
84
  function buildRemediationPrompt(finding) {
72
- const snippet = sanitizeSnippet(finding.snippet);
85
+ const snippet = sanitizeSnippet(finding.snippet);
86
+ const name = sanitizeField(finding.name);
87
+ const severity = sanitizeField(finding.severity);
88
+ const file = sanitizeField(finding.file);
89
+ const line = sanitizeField(finding.line);
73
90
  return `You are a security engineer applying the Red-Green-Refactor TDD remediation protocol.
74
91
 
75
92
  VULNERABILITY FINDING:
76
- - Type: ${finding.name}
77
- - Severity: ${finding.severity}
78
- - File: ${finding.file}
79
- - Line: ${finding.line}
93
+ - Type: ${name}
94
+ - Severity: ${severity}
95
+ - File: ${file}
96
+ - Line: ${line}
80
97
  - Code snippet: <snippet>${snippet}</snippet>
81
98
 
82
99
  TASK:
@@ -118,6 +135,18 @@ async function callProvider(provider, apiKey, model, prompt, baseUrl) {
118
135
 
119
136
  let url = typeof p.url === 'function' ? p.url(apiKey) : p.url;
120
137
  if (baseUrl && p.openaiCompat) {
138
+ // Validate baseUrl to prevent SSRF: must be HTTPS or a localhost origin.
139
+ let parsed;
140
+ try { parsed = new URL(baseUrl); } catch {
141
+ throw new Error(`Invalid baseUrl "${baseUrl}" — must be a valid URL`);
142
+ }
143
+ const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
144
+ if (parsed.protocol !== 'https:' && !isLocalhost) {
145
+ throw new Error(
146
+ `baseUrl must use HTTPS for non-localhost hosts (got "${parsed.protocol}//${parsed.hostname}"). ` +
147
+ 'Plain HTTP is only allowed for localhost.'
148
+ );
149
+ }
121
150
  // Any OpenAI-compatible service: strip trailing slash and append path
122
151
  url = baseUrl.replace(/\/+$/, '') + '/chat/completions';
123
152
  }
@@ -126,8 +155,11 @@ async function callProvider(provider, apiKey, model, prompt, baseUrl) {
126
155
 
127
156
  const res = await fetch(url, { method: 'POST', headers, body });
128
157
  if (!res.ok) {
129
- const text = await res.text().catch(() => '');
130
- throw new Error(`Provider ${provider} returned ${res.status}: ${text.slice(0, 200)}`);
158
+ const raw = await res.text().catch(() => '');
159
+ // Redact the apiKey from provider error bodies before surfacing the message —
160
+ // some providers echo the submitted key in 401/403 responses.
161
+ const safe = apiKey ? raw.split(apiKey).join('[REDACTED]') : raw;
162
+ throw new Error(`Provider ${provider} returned ${res.status}: ${safe.slice(0, 200)}`);
131
163
  }
132
164
  const data = await res.json();
133
165
  return p.extract(data);
package/lib/server.js CHANGED
@@ -65,10 +65,11 @@ const rateLimiter = {
65
65
  function json(res, status, body) {
66
66
  const payload = JSON.stringify(body);
67
67
  res.writeHead(status, {
68
- 'Content-Type': 'application/json',
69
- 'Content-Length': Buffer.byteLength(payload),
70
- 'X-Content-Type-Options': 'nosniff',
71
- 'X-Frame-Options': 'DENY',
68
+ 'Content-Type': 'application/json',
69
+ 'Content-Length': Buffer.byteLength(payload),
70
+ 'X-Content-Type-Options': 'nosniff',
71
+ 'X-Frame-Options': 'DENY',
72
+ 'Content-Security-Policy': "default-src 'none'",
72
73
  });
73
74
  res.end(payload);
74
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lhi/tdd-audit",
3
- "version": "1.10.0",
3
+ "version": "1.11.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": {
@@ -21,6 +21,7 @@
21
21
  "test": "jest --forceExit",
22
22
  "test:unit": "jest --testPathPatterns=__tests__/unit --forceExit --coverage",
23
23
  "test:security": "jest --testPathPatterns=__tests__/security --forceExit",
24
+ "test:e2e": "jest --testPathPatterns=__tests__/e2e --forceExit",
24
25
  "test:smoke": "node index.js --local --skip-scan && echo 'Smoke test passed'"
25
26
  },
26
27
  "keywords": [