@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 +5 -3
- package/lib/config.js +39 -7
- package/lib/remediator.js +39 -7
- package/lib/server.js +5 -4
- package/package.json +2 -1
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
|
|
52
|
-
const 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
|
-
|
|
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
|
|
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: ${
|
|
77
|
-
- Severity: ${
|
|
78
|
-
- File: ${
|
|
79
|
-
- 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
|
|
130
|
-
|
|
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':
|
|
69
|
-
'Content-Length':
|
|
70
|
-
'X-Content-Type-Options':
|
|
71
|
-
'X-Frame-Options':
|
|
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.
|
|
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": [
|