@lhi/tdd-audit 1.3.0 → 1.4.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
|
@@ -4,6 +4,14 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
+
const {
|
|
8
|
+
detectFramework,
|
|
9
|
+
detectAppFramework,
|
|
10
|
+
detectTestBaseDir,
|
|
11
|
+
quickScan,
|
|
12
|
+
printFindings,
|
|
13
|
+
} = require('./lib/scanner');
|
|
14
|
+
|
|
7
15
|
const args = process.argv.slice(2);
|
|
8
16
|
const isLocal = args.includes('--local');
|
|
9
17
|
const isClaude = args.includes('--claude');
|
|
@@ -20,245 +28,22 @@ const targetWorkflowDir = isClaude
|
|
|
20
28
|
? path.join(agentBaseDir, agentDirName, 'commands')
|
|
21
29
|
: path.join(agentBaseDir, agentDirName, 'workflows');
|
|
22
30
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Flutter / Dart — check before package.json since a Flutter project may have both
|
|
27
|
-
if (fs.existsSync(path.join(projectDir, 'pubspec.yaml'))) return 'flutter';
|
|
28
|
-
|
|
29
|
-
const pkgPath = path.join(projectDir, 'package.json');
|
|
30
|
-
if (fs.existsSync(pkgPath)) {
|
|
31
|
-
try {
|
|
32
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
33
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
34
|
-
if (deps.vitest) return 'vitest';
|
|
35
|
-
if (deps.jest || deps.supertest) return 'jest';
|
|
36
|
-
if (deps.mocha) return 'mocha';
|
|
37
|
-
} catch {}
|
|
38
|
-
}
|
|
39
|
-
if (
|
|
40
|
-
fs.existsSync(path.join(projectDir, 'pytest.ini')) ||
|
|
41
|
-
fs.existsSync(path.join(projectDir, 'pyproject.toml')) ||
|
|
42
|
-
fs.existsSync(path.join(projectDir, 'setup.py')) ||
|
|
43
|
-
fs.existsSync(path.join(projectDir, 'requirements.txt'))
|
|
44
|
-
) return 'pytest';
|
|
45
|
-
if (fs.existsSync(path.join(projectDir, 'go.mod'))) return 'go';
|
|
46
|
-
return 'jest';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Detect the UI framework for richer scan context (React, Next.js, RN, Expo, Flutter)
|
|
50
|
-
function detectAppFramework() {
|
|
51
|
-
if (fs.existsSync(path.join(projectDir, 'pubspec.yaml'))) return 'flutter';
|
|
52
|
-
const pkgPath = path.join(projectDir, 'package.json');
|
|
53
|
-
if (fs.existsSync(pkgPath)) {
|
|
54
|
-
try {
|
|
55
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
56
|
-
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
57
|
-
if (deps.expo) return 'expo';
|
|
58
|
-
if (deps['react-native']) return 'react-native';
|
|
59
|
-
if (deps.next) return 'nextjs';
|
|
60
|
-
if (deps.react) return 'react';
|
|
61
|
-
} catch {}
|
|
62
|
-
}
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const appFramework = detectAppFramework();
|
|
67
|
-
|
|
68
|
-
const framework = detectFramework();
|
|
69
|
-
|
|
70
|
-
// ─── 2. Test Directory Detection ─────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
function detectTestBaseDir() {
|
|
73
|
-
// Respect an existing convention before inventing one
|
|
74
|
-
const candidates = ['__tests__', 'tests', 'test', 'spec'];
|
|
75
|
-
for (const dir of candidates) {
|
|
76
|
-
if (fs.existsSync(path.join(projectDir, dir))) return dir;
|
|
77
|
-
}
|
|
78
|
-
// Framework-informed defaults when no directory exists yet
|
|
79
|
-
if (framework === 'pytest') return 'tests';
|
|
80
|
-
if (framework === 'go') return 'test';
|
|
81
|
-
return '__tests__';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const testBaseDir = detectTestBaseDir();
|
|
31
|
+
const appFramework = detectAppFramework(projectDir);
|
|
32
|
+
const framework = detectFramework(projectDir);
|
|
33
|
+
const testBaseDir = detectTestBaseDir(projectDir, framework);
|
|
85
34
|
const targetTestDir = path.join(projectDir, testBaseDir, 'security');
|
|
86
35
|
|
|
87
|
-
// ───
|
|
88
|
-
|
|
89
|
-
const VULN_PATTERNS = [
|
|
90
|
-
{ name: 'SQL Injection', severity: 'CRITICAL', pattern: /(`SELECT[^`]*\$\{|"SELECT[^"]*"\s*\+|execute\(f"|cursor\.execute\(.*%s|\.query\(`[^`]*\$\{)/i },
|
|
91
|
-
{ name: 'Command Injection', severity: 'CRITICAL', pattern: /\bexec(Sync)?\s*\(.*req\.(params|body|query)|subprocess\.(run|Popen|call)\([^)]*shell\s*=\s*True/i },
|
|
92
|
-
{ name: 'IDOR', severity: 'HIGH', pattern: /findById\s*\(\s*req\.(params|body|query)\.|findOne\s*\(\s*\{[^}]*id\s*:\s*req\.(params|body|query)/i },
|
|
93
|
-
{ name: 'XSS', severity: 'HIGH', pattern: /[^/]innerHTML\s*=(?!=)|dangerouslySetInnerHTML\s*=\s*\{\{|document\.write\s*\(|res\.send\s*\(`[^`]*\$\{req\./i },
|
|
94
|
-
{ name: 'Path Traversal', severity: 'HIGH', pattern: /(readFile|sendFile|createReadStream|open)\s*\(.*req\.(params|body|query)|path\.join\s*\([^)]*req\.(params|body|query)/i },
|
|
95
|
-
{ name: 'Broken Auth', severity: 'HIGH', pattern: /jwt\.decode\s*\((?![^;]*\.verify)|verify\s*:\s*false|secret\s*=\s*['"][a-z0-9]{1,20}['"]/i },
|
|
96
|
-
// Vibecoding / mobile stacks
|
|
97
|
-
{ name: 'Sensitive Storage', severity: 'HIGH', pattern: /(localStorage|AsyncStorage)\.setItem\s*\(\s*['"](token|password|secret|auth|jwt|api.?key)['"]/i },
|
|
98
|
-
{ name: 'TLS Bypass', severity: 'CRITICAL', pattern: /badCertificateCallback[^;]*=\s*true|rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0/i },
|
|
99
|
-
{ name: 'Hardcoded Secret', severity: 'CRITICAL', skipInTests: true, pattern: /(?:const|final|var|let|static)\s+(?:API_KEY|PRIVATE_KEY|SECRET_KEY|ACCESS_TOKEN|CLIENT_SECRET)\s*=\s*['"][A-Za-z0-9+/=_\-]{20,}['"]/i },
|
|
100
|
-
{ name: 'eval() Injection', severity: 'HIGH', pattern: /\beval\s*\([^)]*(?:route\.params|searchParams\.get|req\.(query|body)|params\[)/i },
|
|
101
|
-
// Common vibecoding anti-patterns
|
|
102
|
-
{ name: 'Insecure Random', severity: 'HIGH', pattern: /(?:token|sessionId|nonce|secret|csrf)\w*\s*=.*Math\.random\(\)|Math\.random\(\).*(?:token|session|nonce|secret)/i },
|
|
103
|
-
{ name: 'Sensitive Log', severity: 'MEDIUM', skipInTests: true, pattern: /console\.(log|info|debug)\([^)]*(?:token|password|secret|jwt|authorization|apiKey|api_key)/i },
|
|
104
|
-
{ name: 'Secret Fallback', severity: 'HIGH', pattern: /process\.env\.\w+\s*\|\|\s*['"][A-Za-z0-9+/=_\-]{10,}['"]/i },
|
|
105
|
-
// SSRF, redirects, injection
|
|
106
|
-
{ name: 'SSRF', severity: 'CRITICAL', pattern: /\b(?:fetch|axios\.(?:get|post|put|patch|delete|request)|got|https?\.get)\s*\(\s*req\.(?:query|body|params)\./i },
|
|
107
|
-
{ name: 'Open Redirect', severity: 'HIGH', pattern: /res\.redirect\s*\(\s*req\.(?:query|body|params)\.|window\.location(?:\.href)?\s*=\s*(?:params\.|route\.params\.|searchParams\.get)/i },
|
|
108
|
-
{ name: 'NoSQL Injection', severity: 'HIGH', pattern: /\.(?:find|findOne|findById|updateOne|deleteOne)\s*\(\s*req\.(?:body|query|params)\b|\$where\s*:\s*['"`]/i },
|
|
109
|
-
{ name: 'Template Injection', severity: 'HIGH', pattern: /res\.render\s*\(\s*req\.(?:params|body|query)\.|(?:ejs|pug|nunjucks|handlebars)\.render(?:File)?\s*\([^)]*req\.(?:body|params|query)/i },
|
|
110
|
-
{ name: 'Insecure Deserialization',severity: 'CRITICAL', pattern: /\.unserialize\s*\(.*req\.|__proto__\s*[=:][^=]|Object\.setPrototypeOf\s*\([^,]+,\s*req\./i },
|
|
111
|
-
// Assignment / pollution
|
|
112
|
-
{ name: 'Mass Assignment', severity: 'HIGH', pattern: /new\s+\w+\s*\(\s*req\.body\b|\.create\s*\(\s*req\.body\b|\.update(?:One)?\s*\(\s*\{[^}]*\},\s*req\.body\b/i },
|
|
113
|
-
{ name: 'Prototype Pollution', severity: 'HIGH', pattern: /(?:_\.merge|lodash\.merge|deepmerge|hoek\.merge)\s*\([^)]*req\.(?:body|query|params)/i },
|
|
114
|
-
// Crypto / config
|
|
115
|
-
{ name: 'Weak Crypto', severity: 'HIGH', pattern: /createHash\s*\(\s*['"](?:md5|sha1)['"]\)|(?:md5|sha1)\s*\(\s*(?:password|passwd|pwd|secret)/i },
|
|
116
|
-
{ name: 'CORS Wildcard', severity: 'MEDIUM', pattern: /cors\s*\(\s*\{\s*origin\s*:\s*['"]?\*['"]?|'Access-Control-Allow-Origin',\s*['"]?\*/i },
|
|
117
|
-
{ name: 'Cleartext Traffic', severity: 'MEDIUM', skipInTests: true, pattern: /(?:baseURL|apiUrl|API_URL|endpoint|baseUrl)\s*[:=]\s*['"]http:\/\/(?!localhost|127\.0\.0\.1)/i },
|
|
118
|
-
{ name: 'XXE', severity: 'HIGH', pattern: /noent\s*:\s*true|expand_entities\s*=\s*True|resolve_entities\s*=\s*True/i },
|
|
119
|
-
// Mobile / WebView
|
|
120
|
-
{ name: 'WebView JS Bridge', severity: 'HIGH', pattern: /addJavascriptInterface\s*\(|javaScriptEnabled\s*:\s*true|allowFileAccess\s*:\s*true|allowUniversalAccessFromFileURLs\s*:\s*true/i },
|
|
121
|
-
{ name: 'Deep Link Injection', severity: 'MEDIUM', pattern: /Linking\.getInitialURL\s*\(\)|Linking\.addEventListener\s*\(\s*['"]url['"]/i },
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
const SCAN_EXTENSIONS = new Set(['.js', '.ts', '.jsx', '.tsx', '.mjs', '.py', '.go', '.dart']);
|
|
125
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', '__pycache__', 'venv', '.venv', 'vendor', '.expo', '.dart_tool', '.pub-cache']);
|
|
126
|
-
|
|
127
|
-
function* walkFiles(dir) {
|
|
128
|
-
let entries;
|
|
129
|
-
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
130
|
-
for (const entry of entries) {
|
|
131
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
132
|
-
const fullPath = path.join(dir, entry.name);
|
|
133
|
-
if (entry.isDirectory()) yield* walkFiles(fullPath);
|
|
134
|
-
else if (SCAN_EXTENSIONS.has(path.extname(entry.name))) yield fullPath;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Returns true for test/spec files — used to down-weight false-positive-prone patterns
|
|
139
|
-
function isTestFile(filePath) {
|
|
140
|
-
const rel = path.relative(projectDir, filePath).replace(/\\/g, '/');
|
|
141
|
-
return /[._-]test\.[a-z]+$|[._-]spec\.[a-z]+$|_test\.dart$|\/tests?\/|\/spec\/|\/test_/.test(rel);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Scan app.json / app.config.* for embedded secrets (common Expo vibecoding issue)
|
|
145
|
-
function scanAppConfig() {
|
|
146
|
-
const findings = [];
|
|
147
|
-
const configCandidates = ['app.json', 'app.config.js', 'app.config.ts'];
|
|
148
|
-
const secretPattern = /['"]?(?:apiKey|api_key|secret|privateKey|accessToken|clientSecret)['"]?\s*[:=]\s*['"][A-Za-z0-9+/=_\-]{20,}['"]/i;
|
|
149
|
-
|
|
150
|
-
for (const name of configCandidates) {
|
|
151
|
-
const filePath = path.join(projectDir, name);
|
|
152
|
-
if (!fs.existsSync(filePath)) continue;
|
|
153
|
-
let lines;
|
|
154
|
-
try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
|
|
155
|
-
for (let i = 0; i < lines.length; i++) {
|
|
156
|
-
if (secretPattern.test(lines[i])) {
|
|
157
|
-
findings.push({
|
|
158
|
-
severity: 'CRITICAL',
|
|
159
|
-
name: 'Config Secret',
|
|
160
|
-
file: name,
|
|
161
|
-
line: i + 1,
|
|
162
|
-
snippet: lines[i].trim().slice(0, 80),
|
|
163
|
-
inTestFile: false,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return findings;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function scanAndroidManifest() {
|
|
172
|
-
const findings = [];
|
|
173
|
-
const manifestPath = path.join(projectDir, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
174
|
-
if (!fs.existsSync(manifestPath)) return findings;
|
|
175
|
-
let lines;
|
|
176
|
-
try { lines = fs.readFileSync(manifestPath, 'utf8').split('\n'); } catch { return findings; }
|
|
177
|
-
for (let i = 0; i < lines.length; i++) {
|
|
178
|
-
if (/android:debuggable\s*=\s*["']true["']/i.test(lines[i])) {
|
|
179
|
-
findings.push({
|
|
180
|
-
severity: 'HIGH',
|
|
181
|
-
name: 'Android Debuggable',
|
|
182
|
-
file: 'android/app/src/main/AndroidManifest.xml',
|
|
183
|
-
line: i + 1,
|
|
184
|
-
snippet: lines[i].trim().slice(0, 80),
|
|
185
|
-
inTestFile: false,
|
|
186
|
-
likelyFalsePositive: false,
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
return findings;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function quickScan() {
|
|
194
|
-
const findings = [];
|
|
195
|
-
for (const filePath of walkFiles(projectDir)) {
|
|
196
|
-
const inTest = isTestFile(filePath);
|
|
197
|
-
let lines;
|
|
198
|
-
try { lines = fs.readFileSync(filePath, 'utf8').split('\n'); } catch { continue; }
|
|
199
|
-
for (let i = 0; i < lines.length; i++) {
|
|
200
|
-
for (const vuln of VULN_PATTERNS) {
|
|
201
|
-
if (vuln.pattern.test(lines[i])) {
|
|
202
|
-
findings.push({
|
|
203
|
-
severity: vuln.severity,
|
|
204
|
-
name: vuln.name,
|
|
205
|
-
file: path.relative(projectDir, filePath),
|
|
206
|
-
line: i + 1,
|
|
207
|
-
snippet: lines[i].trim().slice(0, 80),
|
|
208
|
-
inTestFile: inTest,
|
|
209
|
-
likelyFalsePositive: inTest && !!vuln.skipInTests,
|
|
210
|
-
});
|
|
211
|
-
break; // one finding per line
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return [...findings, ...scanAppConfig(), ...scanAndroidManifest()];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function printFindings(findings) {
|
|
220
|
-
if (findings.length === 0) {
|
|
221
|
-
console.log(' ✅ No obvious vulnerability patterns detected.\n');
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
const real = findings.filter(f => !f.likelyFalsePositive);
|
|
225
|
-
const noisy = findings.filter(f => f.likelyFalsePositive);
|
|
226
|
-
|
|
227
|
-
const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
|
|
228
|
-
for (const f of real) (bySeverity[f.severity] || bySeverity.LOW).push(f);
|
|
229
|
-
const icons = { CRITICAL: '🔴', HIGH: '🟠', MEDIUM: '🟡', LOW: '🔵' };
|
|
230
|
-
|
|
231
|
-
console.log(`\n Found ${real.length} potential issue(s)${noisy.length ? ` (+${noisy.length} in test files — see below)` : ''}:\n`);
|
|
232
|
-
for (const [sev, list] of Object.entries(bySeverity)) {
|
|
233
|
-
if (!list.length) continue;
|
|
234
|
-
for (const f of list) {
|
|
235
|
-
const testBadge = f.inTestFile ? ' [test file]' : '';
|
|
236
|
-
console.log(` ${icons[sev]} [${sev}] ${f.name} — ${f.file}:${f.line}${testBadge}`);
|
|
237
|
-
console.log(` ${f.snippet}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (noisy.length) {
|
|
242
|
-
console.log('\n ⚪ Likely intentional (in test files — verify manually):');
|
|
243
|
-
for (const f of noisy) {
|
|
244
|
-
console.log(` ${f.name} — ${f.file}:${f.line}`);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
console.log('\n Run /tdd-audit in your agent to remediate.\n');
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// ─── 4. Scan-only early exit ──────────────────────────────────────────────────
|
|
36
|
+
// ─── Scan-only early exit ─────────────────────────────────────────────────────
|
|
252
37
|
|
|
253
38
|
if (scanOnly) {
|
|
254
39
|
process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
|
|
255
|
-
const findings = quickScan();
|
|
40
|
+
const findings = quickScan(projectDir);
|
|
256
41
|
process.stdout.write('\n');
|
|
257
42
|
printFindings(findings);
|
|
258
43
|
process.exit(0);
|
|
259
44
|
}
|
|
260
45
|
|
|
261
|
-
// ───
|
|
46
|
+
// ─── Install Skill Files ──────────────────────────────────────────────────────
|
|
262
47
|
|
|
263
48
|
const appLabel = appFramework ? `, app: ${appFramework}` : '';
|
|
264
49
|
console.log(`\nInstalling TDD Remediation Skill (${isLocal ? 'local' : 'global'}, framework: ${framework}${appLabel}, test dir: ${testBaseDir}/)...\n`);
|
|
@@ -271,7 +56,7 @@ for (const item of ['SKILL.md', 'prompts', 'templates']) {
|
|
|
271
56
|
if (fs.existsSync(src)) fs.cpSync(src, dest, { recursive: true });
|
|
272
57
|
}
|
|
273
58
|
|
|
274
|
-
// ───
|
|
59
|
+
// ─── Scaffold Security Test Boilerplate ───────────────────────────────────────
|
|
275
60
|
|
|
276
61
|
if (!fs.existsSync(targetTestDir)) {
|
|
277
62
|
fs.mkdirSync(targetTestDir, { recursive: true });
|
|
@@ -296,7 +81,7 @@ if (!fs.existsSync(destTest) && fs.existsSync(srcTest)) {
|
|
|
296
81
|
console.log(`✅ Scaffolded ${path.relative(projectDir, destTest)}`);
|
|
297
82
|
}
|
|
298
83
|
|
|
299
|
-
// ───
|
|
84
|
+
// ─── Install Workflow Shortcode ───────────────────────────────────────────────
|
|
300
85
|
|
|
301
86
|
if (!fs.existsSync(targetWorkflowDir)) fs.mkdirSync(targetWorkflowDir, { recursive: true });
|
|
302
87
|
const srcWorkflow = path.join(__dirname, 'workflows', 'tdd-audit.md');
|
|
@@ -306,7 +91,7 @@ if (fs.existsSync(srcWorkflow)) {
|
|
|
306
91
|
console.log(`✅ Installed /tdd-audit workflow shortcode`);
|
|
307
92
|
}
|
|
308
93
|
|
|
309
|
-
// ───
|
|
94
|
+
// ─── Inject test:security into package.json ───────────────────────────────────
|
|
310
95
|
|
|
311
96
|
const pkgPath = path.join(projectDir, 'package.json');
|
|
312
97
|
if (framework !== 'pytest' && framework !== 'go' && fs.existsSync(pkgPath)) {
|
|
@@ -316,10 +101,10 @@ if (framework !== 'pytest' && framework !== 'go' && fs.existsSync(pkgPath)) {
|
|
|
316
101
|
pkg.scripts = pkg.scripts || {};
|
|
317
102
|
const secDir = `${testBaseDir}/security`;
|
|
318
103
|
pkg.scripts['test:security'] = {
|
|
319
|
-
jest: `jest --
|
|
104
|
+
jest: `jest --testPathPatterns=${secDir} --forceExit`,
|
|
320
105
|
vitest: `vitest run ${secDir}`,
|
|
321
106
|
mocha: `mocha '${secDir}/**/*.spec.js'`,
|
|
322
|
-
}[framework] || `jest --
|
|
107
|
+
}[framework] || `jest --testPathPatterns=${secDir} --forceExit`;
|
|
323
108
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
324
109
|
console.log(`✅ Added "test:security" script to package.json`);
|
|
325
110
|
} else {
|
|
@@ -330,31 +115,43 @@ if (framework !== 'pytest' && framework !== 'go' && fs.existsSync(pkgPath)) {
|
|
|
330
115
|
}
|
|
331
116
|
}
|
|
332
117
|
|
|
333
|
-
// ───
|
|
118
|
+
// ─── Scaffold CI Workflows ────────────────────────────────────────────────────
|
|
334
119
|
|
|
335
120
|
const ciWorkflowDir = path.join(projectDir, '.github', 'workflows');
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
121
|
+
fs.mkdirSync(ciWorkflowDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
const ciWorkflows = [
|
|
124
|
+
{
|
|
125
|
+
destName: 'security-tests.yml',
|
|
126
|
+
templateMap: {
|
|
127
|
+
jest: 'security-tests.node.yml', vitest: 'security-tests.node.yml',
|
|
128
|
+
mocha: 'security-tests.node.yml', pytest: 'security-tests.python.yml',
|
|
129
|
+
go: 'security-tests.go.yml', flutter: 'security-tests.flutter.yml',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
destName: 'ci.yml',
|
|
134
|
+
templateMap: {
|
|
135
|
+
jest: 'ci.node.yml', vitest: 'ci.node.yml', mocha: 'ci.node.yml',
|
|
136
|
+
pytest: 'ci.python.yml', go: 'ci.go.yml', flutter: 'ci.flutter.yml',
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const { destName, templateMap } of ciWorkflows) {
|
|
142
|
+
const destPath = path.join(ciWorkflowDir, destName);
|
|
143
|
+
if (!fs.existsSync(destPath)) {
|
|
144
|
+
const srcPath = path.join(__dirname, 'templates', 'workflows', templateMap[framework]);
|
|
145
|
+
if (fs.existsSync(srcPath)) {
|
|
146
|
+
fs.copyFileSync(srcPath, destPath);
|
|
147
|
+
console.log(`✅ Scaffolded .github/workflows/${destName}`);
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
console.log(` .github/workflows/${destName} already exists — skipped`);
|
|
352
151
|
}
|
|
353
|
-
} else {
|
|
354
|
-
console.log(` .github/workflows/security-tests.yml already exists — skipped`);
|
|
355
152
|
}
|
|
356
153
|
|
|
357
|
-
// ───
|
|
154
|
+
// ─── Pre-commit Hook (opt-in) ─────────────────────────────────────────────────
|
|
358
155
|
|
|
359
156
|
if (withHooks) {
|
|
360
157
|
const gitDir = path.join(projectDir, '.git');
|
|
@@ -392,11 +189,11 @@ if (withHooks) {
|
|
|
392
189
|
}
|
|
393
190
|
}
|
|
394
191
|
|
|
395
|
-
// ───
|
|
192
|
+
// ─── Quick Scan ───────────────────────────────────────────────────────────────
|
|
396
193
|
|
|
397
194
|
if (!skipScan) {
|
|
398
195
|
process.stdout.write('\n🔍 Scanning for vulnerability patterns...');
|
|
399
|
-
const findings = quickScan();
|
|
196
|
+
const findings = quickScan(projectDir);
|
|
400
197
|
process.stdout.write('\n');
|
|
401
198
|
printFindings(findings);
|
|
402
199
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lhi/tdd-audit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Anti-Gravity Skill for TDD Remediation. Patches security vulnerabilities using a Red-Green-Refactor protocol with automated exploit tests.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"test": "
|
|
20
|
-
"test:
|
|
19
|
+
"test": "jest --forceExit",
|
|
20
|
+
"test:unit": "jest --testPathPatterns=__tests__/unit --forceExit --coverage",
|
|
21
|
+
"test:security": "jest --testPathPatterns=__tests__/security --forceExit",
|
|
22
|
+
"test:smoke": "node index.js --local --skip-scan && echo 'Smoke test passed'"
|
|
21
23
|
},
|
|
22
24
|
"keywords": [
|
|
23
25
|
"security",
|
|
@@ -44,5 +46,8 @@
|
|
|
44
46
|
"node": ">=16.7.0"
|
|
45
47
|
},
|
|
46
48
|
"author": "Kyra Lee",
|
|
47
|
-
"license": "MIT"
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"jest": "^30.3.0"
|
|
52
|
+
}
|
|
48
53
|
}
|
package/prompts/red-phase.md
CHANGED
|
@@ -99,8 +99,12 @@ const request = require('supertest');
|
|
|
99
99
|
const app = require('../../app');
|
|
100
100
|
|
|
101
101
|
describe('[VulnType] - Red Phase', () => {
|
|
102
|
+
let server;
|
|
103
|
+
beforeAll(() => { server = app.listen(0); });
|
|
104
|
+
afterAll(() => server.close());
|
|
105
|
+
|
|
102
106
|
it('SHOULD block [exploit description]', async () => {
|
|
103
|
-
const res = await request(
|
|
107
|
+
const res = await request(server)
|
|
104
108
|
.post('/api/vulnerable-endpoint')
|
|
105
109
|
.send({ input: '<exploit payload>' });
|
|
106
110
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test & Analyze
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Flutter
|
|
18
|
+
uses: subosito/flutter-action@v2
|
|
19
|
+
with:
|
|
20
|
+
flutter-version: stable
|
|
21
|
+
cache: true
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: flutter pub get
|
|
25
|
+
|
|
26
|
+
- name: Analyze (dart analyze)
|
|
27
|
+
run: dart analyze --fatal-infos
|
|
28
|
+
|
|
29
|
+
- name: Format check
|
|
30
|
+
run: dart format --output=none --set-exit-if-changed .
|
|
31
|
+
|
|
32
|
+
- name: Unit tests with coverage
|
|
33
|
+
run: flutter test --coverage
|
|
34
|
+
|
|
35
|
+
- name: Security tests
|
|
36
|
+
run: flutter test test/security/
|
|
37
|
+
|
|
38
|
+
- name: Upload coverage
|
|
39
|
+
uses: actions/upload-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: coverage
|
|
42
|
+
path: coverage/lcov.info
|
|
43
|
+
retention-days: 7
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test & Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
go-version: ["1.21", "1.22", "1.23"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Go ${{ matrix.go-version }}
|
|
22
|
+
uses: actions/setup-go@v5
|
|
23
|
+
with:
|
|
24
|
+
go-version: ${{ matrix.go-version }}
|
|
25
|
+
cache: true
|
|
26
|
+
|
|
27
|
+
- name: Lint (staticcheck)
|
|
28
|
+
uses: dominikh/staticcheck-action@v1
|
|
29
|
+
with:
|
|
30
|
+
version: latest
|
|
31
|
+
install-go: false
|
|
32
|
+
|
|
33
|
+
- name: Run tests with coverage
|
|
34
|
+
run: go test ./... -coverprofile=coverage.out -covermode=atomic -v
|
|
35
|
+
|
|
36
|
+
- name: Security tests
|
|
37
|
+
run: go test ./security/... -v
|
|
38
|
+
|
|
39
|
+
- name: Upload coverage
|
|
40
|
+
if: matrix.go-version == '1.22'
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: coverage
|
|
44
|
+
path: coverage.out
|
|
45
|
+
retention-days: 7
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test & Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
node-version: [18.x, 20.x, 22.x]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: ${{ matrix.node-version }}
|
|
25
|
+
cache: npm
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: Lint
|
|
31
|
+
run: npm run lint --if-present
|
|
32
|
+
|
|
33
|
+
- name: Unit tests
|
|
34
|
+
run: npm test
|
|
35
|
+
|
|
36
|
+
- name: Security tests
|
|
37
|
+
run: npm run test:security --if-present
|
|
38
|
+
|
|
39
|
+
- name: Upload coverage
|
|
40
|
+
if: matrix.node-version == '20.x'
|
|
41
|
+
uses: actions/upload-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: coverage
|
|
44
|
+
path: coverage/
|
|
45
|
+
retention-days: 7
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main, master, develop]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main, master, develop]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Test & Lint
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
cache: pip
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
pip install pytest pytest-cov ruff
|
|
32
|
+
|
|
33
|
+
- name: Lint (ruff)
|
|
34
|
+
run: ruff check .
|
|
35
|
+
|
|
36
|
+
- name: Unit tests with coverage
|
|
37
|
+
run: pytest --cov=. --cov-report=xml -v
|
|
38
|
+
|
|
39
|
+
- name: Security tests
|
|
40
|
+
run: pytest tests/security/ -v
|
|
41
|
+
|
|
42
|
+
- name: Upload coverage
|
|
43
|
+
if: matrix.python-version == '3.11'
|
|
44
|
+
uses: actions/upload-artifact@v4
|
|
45
|
+
with:
|
|
46
|
+
name: coverage
|
|
47
|
+
path: coverage.xml
|
|
48
|
+
retention-days: 7
|