create-qa-architect 5.13.5 → 5.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/.github/workflows/claude-md-validation.yml +0 -3
- package/.semgrep/defensive-patterns.yaml +356 -0
- package/.semgrep/vibe-audit-rules.yaml +332 -0
- package/LICENSE +192 -39
- package/README.md +98 -46
- package/config/requirements-dev.txt +2 -2
- package/docs/CI-COST-ANALYSIS.md +10 -10
- package/docs/POLAR-DEPLOYMENT.md +157 -0
- package/docs/{STRIPE-LIVE-MODE-DEPLOYMENT.md → _archive/STRIPE-LIVE-MODE-DEPLOYMENT.md} +24 -21
- package/docs/plans/PLAN-vibe-code-auditor.md +130 -0
- package/docs/plans/POLAR-MIGRATION.md +111 -0
- package/docs/plans/pro-features-2026-05.md +159 -0
- package/eslint.config.cjs +1 -0
- package/lib/commands/analyze-ci.js +20 -11
- package/lib/commands/audit.js +668 -0
- package/lib/commands/ci-doctor.js +341 -0
- package/lib/commands/history-scan.js +342 -0
- package/lib/commands/index.js +8 -0
- package/lib/commands/pr-check.js +484 -0
- package/lib/commands/prelaunch-setup.js +4 -0
- package/lib/commands/ship-check.js +570 -0
- package/lib/license-validator.js +200 -6
- package/lib/licensing.js +99 -11
- package/package.json +7 -6
- package/scripts/deploy-consumers.sh +10 -5
- package/scripts/risk-policy-gate.js +410 -0
- package/setup.js +132 -4
- /package/lib/{billing-dashboard.html → _archive/billing-dashboard.html} +0 -0
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibe-Code Audit — security scan for AI-generated codebases
|
|
3
|
+
*
|
|
4
|
+
* Runs semgrep (SAST) + npm audit (CVEs) + hallucination check (Pro)
|
|
5
|
+
* and produces a structured Critical/High/Medium/Low report.
|
|
6
|
+
*
|
|
7
|
+
* Free: semgrep with both rule files + npm audit
|
|
8
|
+
* Pro: + hallucinated package detection (npm registry check)
|
|
9
|
+
* + --fix flag generates Claude Code prompts per finding
|
|
10
|
+
*
|
|
11
|
+
* All external process invocations use spawnSync with argument arrays (no shell).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict'
|
|
15
|
+
|
|
16
|
+
const fs = require('fs')
|
|
17
|
+
const path = require('path')
|
|
18
|
+
const https = require('https')
|
|
19
|
+
const { spawnSync } = require('child_process')
|
|
20
|
+
const {
|
|
21
|
+
hasFeature,
|
|
22
|
+
showUpgradeMessage,
|
|
23
|
+
ensureLicenseFresh,
|
|
24
|
+
} = require('../licensing')
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Constants
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
const SEVERITY = {
|
|
31
|
+
CRITICAL: 'critical',
|
|
32
|
+
HIGH: 'high',
|
|
33
|
+
MEDIUM: 'medium',
|
|
34
|
+
LOW: 'low',
|
|
35
|
+
INFO: 'info',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SEVERITY_ORDER = [
|
|
39
|
+
SEVERITY.CRITICAL,
|
|
40
|
+
SEVERITY.HIGH,
|
|
41
|
+
SEVERITY.MEDIUM,
|
|
42
|
+
SEVERITY.LOW,
|
|
43
|
+
SEVERITY.INFO,
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
const SEMGREP_TO_SEVERITY = {
|
|
47
|
+
ERROR: SEVERITY.HIGH,
|
|
48
|
+
WARNING: SEVERITY.MEDIUM,
|
|
49
|
+
INFO: SEVERITY.LOW,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// OWASP categories that map to Critical (escalate from ERROR)
|
|
53
|
+
const CRITICAL_CWE = new Set([
|
|
54
|
+
'CWE-89', // SQL injection
|
|
55
|
+
'CWE-78', // Command injection
|
|
56
|
+
'CWE-798', // Hardcoded credentials
|
|
57
|
+
'CWE-639', // IDOR
|
|
58
|
+
'CWE-95', // Eval injection
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
const SEVERITY_ICON = {
|
|
62
|
+
[SEVERITY.CRITICAL]: '🚨',
|
|
63
|
+
[SEVERITY.HIGH]: '❌',
|
|
64
|
+
[SEVERITY.MEDIUM]: '⚠️ ',
|
|
65
|
+
[SEVERITY.LOW]: '💡',
|
|
66
|
+
[SEVERITY.INFO]: 'ℹ️ ',
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Semgrep detection
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function detectSemgrep() {
|
|
74
|
+
const result = spawnSync('semgrep', ['--version'], {
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
timeout: 10_000,
|
|
77
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
78
|
+
shell: false,
|
|
79
|
+
})
|
|
80
|
+
if (result.error || result.status !== 0) return null
|
|
81
|
+
const version = (result.stdout || '').trim().split('\n')[0]
|
|
82
|
+
return version || 'installed'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function semgrepInstallHint() {
|
|
86
|
+
return [
|
|
87
|
+
'',
|
|
88
|
+
' semgrep is not installed. Install it to enable code-pattern scanning:',
|
|
89
|
+
'',
|
|
90
|
+
' pip install semgrep # Python (recommended)',
|
|
91
|
+
' brew install semgrep # macOS Homebrew',
|
|
92
|
+
' npm install -g @semgrep/semgrep # npm (slower)',
|
|
93
|
+
'',
|
|
94
|
+
' Then re-run: npx create-qa-architect@latest --audit',
|
|
95
|
+
'',
|
|
96
|
+
].join('\n')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Semgrep runner
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function runSemgrep(projectPath, ruleFiles) {
|
|
104
|
+
const args = ['--json', '--quiet', '--no-git-ignore']
|
|
105
|
+
for (const f of ruleFiles) {
|
|
106
|
+
args.push('--config', f)
|
|
107
|
+
}
|
|
108
|
+
args.push('.')
|
|
109
|
+
|
|
110
|
+
const result = spawnSync('semgrep', args, {
|
|
111
|
+
cwd: projectPath,
|
|
112
|
+
encoding: 'utf8',
|
|
113
|
+
timeout: 120_000,
|
|
114
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
115
|
+
shell: false,
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (result.error) {
|
|
119
|
+
if (result.error.code === 'ENOENT') return { error: 'not_installed' }
|
|
120
|
+
if (result.error.code === 'ETIMEDOUT') return { error: 'timeout' }
|
|
121
|
+
return { error: result.error.message }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// semgrep exits 1 when findings exist — that's normal
|
|
125
|
+
if (!result.stdout) return { findings: [] }
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const parsed = JSON.parse(result.stdout)
|
|
129
|
+
return { findings: parsed.results || [] }
|
|
130
|
+
} catch {
|
|
131
|
+
return { error: 'parse_error', raw: result.stdout.slice(0, 500) }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Semgrep finding → structured finding
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
function mapSemgrepFinding(raw) {
|
|
140
|
+
const cwe = raw.extra?.metadata?.cwe || ''
|
|
141
|
+
const baseSeverity =
|
|
142
|
+
SEMGREP_TO_SEVERITY[raw.extra?.severity?.toUpperCase()] || SEVERITY.MEDIUM
|
|
143
|
+
|
|
144
|
+
// Escalate to CRITICAL for high-impact CWEs
|
|
145
|
+
const severity = CRITICAL_CWE.has(cwe) ? SEVERITY.CRITICAL : baseSeverity
|
|
146
|
+
|
|
147
|
+
const fix = raw.extra?.metadata?.fix || null
|
|
148
|
+
const note = raw.extra?.metadata?.note || null
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
id: raw.check_id,
|
|
152
|
+
severity,
|
|
153
|
+
file: raw.path,
|
|
154
|
+
line: raw.start?.line ?? 0,
|
|
155
|
+
endLine: raw.end?.line ?? 0,
|
|
156
|
+
message: (raw.extra?.message || raw.message || '').trim(),
|
|
157
|
+
fix,
|
|
158
|
+
note,
|
|
159
|
+
cwe,
|
|
160
|
+
owasp: raw.extra?.metadata?.owasp || '',
|
|
161
|
+
source: 'semgrep',
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// npm audit runner
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
function runNpmAudit(projectPath) {
|
|
170
|
+
const pkgPath = path.join(projectPath, 'package.json')
|
|
171
|
+
if (!fs.existsSync(pkgPath)) return []
|
|
172
|
+
|
|
173
|
+
const result = spawnSync(
|
|
174
|
+
'npm',
|
|
175
|
+
['audit', '--json', '--audit-level', 'none'],
|
|
176
|
+
{
|
|
177
|
+
cwd: projectPath,
|
|
178
|
+
encoding: 'utf8',
|
|
179
|
+
timeout: 60_000,
|
|
180
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
181
|
+
shell: false,
|
|
182
|
+
}
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if (result.error || !result.stdout) return []
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const data = JSON.parse(result.stdout)
|
|
189
|
+
const findings = []
|
|
190
|
+
|
|
191
|
+
// npm v7+ audit JSON format
|
|
192
|
+
const vulns = data.vulnerabilities || {}
|
|
193
|
+
for (const [pkgName, vuln] of Object.entries(vulns)) {
|
|
194
|
+
const severity = mapNpmSeverity(vuln.severity)
|
|
195
|
+
const via = Array.isArray(vuln.via)
|
|
196
|
+
? vuln.via
|
|
197
|
+
.filter(v => typeof v === 'object')
|
|
198
|
+
.map(v => v.title || v.url || '')
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
: []
|
|
201
|
+
findings.push({
|
|
202
|
+
id: `npm-audit-${pkgName}`,
|
|
203
|
+
severity,
|
|
204
|
+
file: 'package.json',
|
|
205
|
+
line: 0,
|
|
206
|
+
message: `${pkgName}@${vuln.range || 'unknown'}: ${via[0] || vuln.severity + ' severity vulnerability'}`,
|
|
207
|
+
fix: vuln.fixAvailable
|
|
208
|
+
? typeof vuln.fixAvailable === 'object'
|
|
209
|
+
? `npm install ${vuln.fixAvailable.name}@${vuln.fixAvailable.version}`
|
|
210
|
+
: 'npm audit fix'
|
|
211
|
+
: 'No automatic fix available — check for alternative package',
|
|
212
|
+
cwe: '',
|
|
213
|
+
owasp: '',
|
|
214
|
+
source: 'npm-audit',
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
return findings
|
|
218
|
+
} catch {
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function mapNpmSeverity(severity) {
|
|
224
|
+
const map = {
|
|
225
|
+
critical: SEVERITY.CRITICAL,
|
|
226
|
+
high: SEVERITY.HIGH,
|
|
227
|
+
moderate: SEVERITY.MEDIUM,
|
|
228
|
+
low: SEVERITY.LOW,
|
|
229
|
+
info: SEVERITY.INFO,
|
|
230
|
+
}
|
|
231
|
+
return map[severity?.toLowerCase()] || SEVERITY.MEDIUM
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// Hallucinated package check (Pro)
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
|
|
238
|
+
function checkHallucinatedPackages(projectPath) {
|
|
239
|
+
const pkgPath = path.join(projectPath, 'package.json')
|
|
240
|
+
if (!fs.existsSync(pkgPath)) return Promise.resolve([])
|
|
241
|
+
|
|
242
|
+
let pkg
|
|
243
|
+
try {
|
|
244
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
|
|
245
|
+
} catch {
|
|
246
|
+
return Promise.resolve([])
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const allDeps = {
|
|
250
|
+
...pkg.dependencies,
|
|
251
|
+
...pkg.devDependencies,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const packageNames = Object.keys(allDeps)
|
|
255
|
+
if (packageNames.length === 0) return Promise.resolve([])
|
|
256
|
+
|
|
257
|
+
// Check up to 50 packages to avoid rate limits
|
|
258
|
+
const toCheck = packageNames.slice(0, 50)
|
|
259
|
+
|
|
260
|
+
const checks = toCheck.map(name => checkNpmRegistry(name))
|
|
261
|
+
|
|
262
|
+
return Promise.all(checks).then(results => {
|
|
263
|
+
const findings = []
|
|
264
|
+
results.forEach((exists, i) => {
|
|
265
|
+
if (!exists) {
|
|
266
|
+
findings.push({
|
|
267
|
+
id: `hallucinated-package-${toCheck[i]}`,
|
|
268
|
+
severity: SEVERITY.CRITICAL,
|
|
269
|
+
file: 'package.json',
|
|
270
|
+
line: 0,
|
|
271
|
+
message: `"${toCheck[i]}" does not exist on npm registry — possible hallucinated package (slopsquatting risk)`,
|
|
272
|
+
fix: `Remove "${toCheck[i]}" from dependencies and find a verified replacement`,
|
|
273
|
+
cwe: 'CWE-1104',
|
|
274
|
+
owasp: 'A06:2021',
|
|
275
|
+
source: 'hallucination-check',
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
return findings
|
|
280
|
+
})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function checkNpmRegistry(packageName) {
|
|
284
|
+
return new Promise(resolve => {
|
|
285
|
+
// Scoped packages: @org/name → encode for URL
|
|
286
|
+
const encoded = encodeURIComponent(packageName)
|
|
287
|
+
.replace('%40', '@')
|
|
288
|
+
.replace('%2F', '%2F')
|
|
289
|
+
const url = `https://registry.npmjs.org/${encoded}`
|
|
290
|
+
|
|
291
|
+
const req = https.get(
|
|
292
|
+
url,
|
|
293
|
+
{
|
|
294
|
+
headers: { Accept: 'application/json' },
|
|
295
|
+
timeout: 8000,
|
|
296
|
+
},
|
|
297
|
+
res => {
|
|
298
|
+
resolve(res.statusCode !== 404)
|
|
299
|
+
res.resume()
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
req.on('error', () => resolve(true)) // Network error = assume exists (avoid false positives)
|
|
303
|
+
req.on('timeout', () => {
|
|
304
|
+
req.destroy()
|
|
305
|
+
resolve(true)
|
|
306
|
+
})
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ---------------------------------------------------------------------------
|
|
311
|
+
// Report formatting
|
|
312
|
+
// ---------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
function groupBySeverity(findings) {
|
|
315
|
+
const grouped = {}
|
|
316
|
+
for (const sev of SEVERITY_ORDER) {
|
|
317
|
+
grouped[sev] = []
|
|
318
|
+
}
|
|
319
|
+
for (const f of findings) {
|
|
320
|
+
const sev = SEVERITY_ORDER.includes(f.severity) ? f.severity : SEVERITY.LOW
|
|
321
|
+
grouped[sev].push(f)
|
|
322
|
+
}
|
|
323
|
+
return grouped
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function buildHumanReport(findings, options = {}) {
|
|
327
|
+
const grouped = groupBySeverity(findings)
|
|
328
|
+
const totalCritical = grouped[SEVERITY.CRITICAL].length
|
|
329
|
+
const totalHigh = grouped[SEVERITY.HIGH].length
|
|
330
|
+
const total = findings.length
|
|
331
|
+
|
|
332
|
+
const lines = []
|
|
333
|
+
|
|
334
|
+
lines.push('')
|
|
335
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
336
|
+
lines.push(' QA Architect — Vibe-Code Security Audit')
|
|
337
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
338
|
+
lines.push('')
|
|
339
|
+
|
|
340
|
+
if (total === 0) {
|
|
341
|
+
lines.push(' ✅ No security issues found.')
|
|
342
|
+
lines.push('')
|
|
343
|
+
lines.push(' Run periodically as your codebase grows.')
|
|
344
|
+
lines.push('')
|
|
345
|
+
return lines.join('\n')
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Summary line
|
|
349
|
+
const verdict =
|
|
350
|
+
totalCritical > 0
|
|
351
|
+
? '🚨 NOT SAFE TO SHIP'
|
|
352
|
+
: totalHigh > 0
|
|
353
|
+
? '⚠️ REVIEW BEFORE SHIPPING'
|
|
354
|
+
: '💛 MINOR ISSUES'
|
|
355
|
+
lines.push(` ${verdict}`)
|
|
356
|
+
lines.push('')
|
|
357
|
+
lines.push(` Total findings: ${total}`)
|
|
358
|
+
if (grouped[SEVERITY.CRITICAL].length)
|
|
359
|
+
lines.push(` 🚨 Critical: ${grouped[SEVERITY.CRITICAL].length}`)
|
|
360
|
+
if (grouped[SEVERITY.HIGH].length)
|
|
361
|
+
lines.push(` ❌ High: ${grouped[SEVERITY.HIGH].length}`)
|
|
362
|
+
if (grouped[SEVERITY.MEDIUM].length)
|
|
363
|
+
lines.push(` ⚠️ Medium: ${grouped[SEVERITY.MEDIUM].length}`)
|
|
364
|
+
if (grouped[SEVERITY.LOW].length)
|
|
365
|
+
lines.push(` 💡 Low: ${grouped[SEVERITY.LOW].length}`)
|
|
366
|
+
lines.push('')
|
|
367
|
+
|
|
368
|
+
for (const sev of SEVERITY_ORDER) {
|
|
369
|
+
const sevFindings = grouped[sev]
|
|
370
|
+
if (sevFindings.length === 0) continue
|
|
371
|
+
|
|
372
|
+
const label = sev.toUpperCase()
|
|
373
|
+
lines.push(` ${SEVERITY_ICON[sev]} ${label} (${sevFindings.length})`)
|
|
374
|
+
lines.push(' ' + '─'.repeat(55))
|
|
375
|
+
|
|
376
|
+
for (const f of sevFindings) {
|
|
377
|
+
const loc = f.line > 0 ? `${f.file}:${f.line}` : f.file
|
|
378
|
+
lines.push(` ${loc}`)
|
|
379
|
+
lines.push(` ${f.message}`)
|
|
380
|
+
if (f.fix) lines.push(` → Fix: ${f.fix}`)
|
|
381
|
+
if (f.note) lines.push(` ℹ️ ${f.note}`)
|
|
382
|
+
if (options.showIds && f.id) lines.push(` [${f.id}]`)
|
|
383
|
+
lines.push('')
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (options.fix && findings.length > 0) {
|
|
388
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
389
|
+
lines.push(' CLAUDE CODE FIX PROMPTS')
|
|
390
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
391
|
+
lines.push('')
|
|
392
|
+
|
|
393
|
+
const toFix = findings
|
|
394
|
+
.filter(f => [SEVERITY.CRITICAL, SEVERITY.HIGH].includes(f.severity))
|
|
395
|
+
.slice(0, 10)
|
|
396
|
+
|
|
397
|
+
for (const f of toFix) {
|
|
398
|
+
lines.push(` ── ${f.file}${f.line > 0 ? ':' + f.line : ''} ──`)
|
|
399
|
+
lines.push(' Copy this prompt into Claude Code:')
|
|
400
|
+
lines.push('')
|
|
401
|
+
lines.push(' """')
|
|
402
|
+
lines.push(buildClaudePrompt(f))
|
|
403
|
+
lines.push(' """')
|
|
404
|
+
lines.push('')
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━')
|
|
409
|
+
lines.push('')
|
|
410
|
+
|
|
411
|
+
return lines.join('\n')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function buildMarkdownReport(findings, options = {}) {
|
|
415
|
+
const grouped = groupBySeverity(findings)
|
|
416
|
+
const total = findings.length
|
|
417
|
+
const totalCritical = grouped[SEVERITY.CRITICAL].length
|
|
418
|
+
const totalHigh = grouped[SEVERITY.HIGH].length
|
|
419
|
+
|
|
420
|
+
const verdict =
|
|
421
|
+
totalCritical > 0
|
|
422
|
+
? '🚨 **NOT SAFE TO SHIP**'
|
|
423
|
+
: totalHigh > 0
|
|
424
|
+
? '⚠️ **REVIEW BEFORE SHIPPING**'
|
|
425
|
+
: total > 0
|
|
426
|
+
? '💛 **MINOR ISSUES**'
|
|
427
|
+
: '✅ **SAFE TO SHIP**'
|
|
428
|
+
|
|
429
|
+
const lines = []
|
|
430
|
+
|
|
431
|
+
lines.push('## QA Architect — Vibe-Code Security Audit')
|
|
432
|
+
lines.push('')
|
|
433
|
+
lines.push(`**Verdict:** ${verdict}`)
|
|
434
|
+
lines.push('')
|
|
435
|
+
lines.push('| Severity | Count |')
|
|
436
|
+
lines.push('|---|---|')
|
|
437
|
+
if (grouped[SEVERITY.CRITICAL].length)
|
|
438
|
+
lines.push(`| 🚨 Critical | ${grouped[SEVERITY.CRITICAL].length} |`)
|
|
439
|
+
if (grouped[SEVERITY.HIGH].length)
|
|
440
|
+
lines.push(`| ❌ High | ${grouped[SEVERITY.HIGH].length} |`)
|
|
441
|
+
if (grouped[SEVERITY.MEDIUM].length)
|
|
442
|
+
lines.push(`| ⚠️ Medium | ${grouped[SEVERITY.MEDIUM].length} |`)
|
|
443
|
+
if (grouped[SEVERITY.LOW].length)
|
|
444
|
+
lines.push(`| 💡 Low | ${grouped[SEVERITY.LOW].length} |`)
|
|
445
|
+
if (total === 0) lines.push('| ✅ None | 0 |')
|
|
446
|
+
lines.push('')
|
|
447
|
+
|
|
448
|
+
for (const sev of SEVERITY_ORDER) {
|
|
449
|
+
const sevFindings = grouped[sev]
|
|
450
|
+
if (sevFindings.length === 0) continue
|
|
451
|
+
|
|
452
|
+
lines.push(
|
|
453
|
+
`### ${SEVERITY_ICON[sev]} ${sev.charAt(0).toUpperCase() + sev.slice(1)}`
|
|
454
|
+
)
|
|
455
|
+
lines.push('')
|
|
456
|
+
|
|
457
|
+
for (const f of sevFindings) {
|
|
458
|
+
const loc = f.line > 0 ? `\`${f.file}:${f.line}\`` : `\`${f.file}\``
|
|
459
|
+
lines.push(`**${loc}**`)
|
|
460
|
+
lines.push('')
|
|
461
|
+
lines.push(f.message)
|
|
462
|
+
if (f.fix) lines.push('')
|
|
463
|
+
if (f.fix) lines.push(`**Fix:** ${f.fix}`)
|
|
464
|
+
if (f.note) lines.push(`> ${f.note}`)
|
|
465
|
+
if (f.cwe) lines.push(`_${f.cwe}_${f.owasp ? ` · ${f.owasp}` : ''}`)
|
|
466
|
+
lines.push('')
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (options.fix && findings.length > 0) {
|
|
471
|
+
lines.push('---')
|
|
472
|
+
lines.push('## Claude Code Fix Prompts')
|
|
473
|
+
lines.push('')
|
|
474
|
+
const toFix = findings
|
|
475
|
+
.filter(f => [SEVERITY.CRITICAL, SEVERITY.HIGH].includes(f.severity))
|
|
476
|
+
.slice(0, 10)
|
|
477
|
+
|
|
478
|
+
for (const f of toFix) {
|
|
479
|
+
lines.push(`### ${f.file}${f.line > 0 ? ':' + f.line : ''}`)
|
|
480
|
+
lines.push('')
|
|
481
|
+
lines.push('```')
|
|
482
|
+
lines.push(buildClaudePrompt(f))
|
|
483
|
+
lines.push('```')
|
|
484
|
+
lines.push('')
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return lines.join('\n')
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function buildClaudePrompt(finding) {
|
|
492
|
+
const lines = [
|
|
493
|
+
`Fix a security issue in ${finding.file}${finding.line > 0 ? ' at line ' + finding.line : ''}.`,
|
|
494
|
+
'',
|
|
495
|
+
`Issue: ${finding.message}`,
|
|
496
|
+
]
|
|
497
|
+
if (finding.cwe)
|
|
498
|
+
lines.push(
|
|
499
|
+
`Category: ${finding.cwe}${finding.owasp ? ' (' + finding.owasp + ')' : ''}`
|
|
500
|
+
)
|
|
501
|
+
if (finding.fix) {
|
|
502
|
+
lines.push('')
|
|
503
|
+
lines.push(`Recommended fix: ${finding.fix}`)
|
|
504
|
+
}
|
|
505
|
+
lines.push('')
|
|
506
|
+
lines.push('Please fix this issue while preserving existing functionality.')
|
|
507
|
+
return lines.join('\n')
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ---------------------------------------------------------------------------
|
|
511
|
+
// Main audit orchestrator
|
|
512
|
+
// ---------------------------------------------------------------------------
|
|
513
|
+
|
|
514
|
+
async function runAudit(projectPath, options = {}) {
|
|
515
|
+
const semgrepVersion = detectSemgrep()
|
|
516
|
+
|
|
517
|
+
if (!semgrepVersion) {
|
|
518
|
+
return {
|
|
519
|
+
error: 'semgrep_not_installed',
|
|
520
|
+
hint: semgrepInstallHint(),
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Rule files — relative to this file's location
|
|
525
|
+
const semgrepDir = path.resolve(__dirname, '../../.semgrep')
|
|
526
|
+
const ruleFiles = [
|
|
527
|
+
path.join(semgrepDir, 'defensive-patterns.yaml'),
|
|
528
|
+
path.join(semgrepDir, 'vibe-audit-rules.yaml'),
|
|
529
|
+
].filter(f => fs.existsSync(f))
|
|
530
|
+
|
|
531
|
+
if (ruleFiles.length === 0) {
|
|
532
|
+
return {
|
|
533
|
+
error: 'no_rules',
|
|
534
|
+
hint: 'Semgrep rule files not found. Reinstall create-qa-architect.',
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Run semgrep
|
|
539
|
+
const semgrepResult = runSemgrep(projectPath, ruleFiles)
|
|
540
|
+
if (semgrepResult.error) {
|
|
541
|
+
return { error: semgrepResult.error }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const findings = semgrepResult.findings.map(mapSemgrepFinding)
|
|
545
|
+
|
|
546
|
+
// Run npm audit
|
|
547
|
+
const npmFindings = runNpmAudit(projectPath)
|
|
548
|
+
findings.push(...npmFindings)
|
|
549
|
+
|
|
550
|
+
// Hallucination check (Pro only)
|
|
551
|
+
if (options.pro) {
|
|
552
|
+
const hallucinated = await checkHallucinatedPackages(projectPath)
|
|
553
|
+
findings.push(...hallucinated)
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Sort: critical first, then by file path
|
|
557
|
+
findings.sort((a, b) => {
|
|
558
|
+
const ai = SEVERITY_ORDER.indexOf(a.severity)
|
|
559
|
+
const bi = SEVERITY_ORDER.indexOf(b.severity)
|
|
560
|
+
if (ai !== bi) return ai - bi
|
|
561
|
+
return a.file.localeCompare(b.file)
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
return { findings, semgrepVersion }
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
// Entry point
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
|
|
571
|
+
async function handleAudit(options = {}) {
|
|
572
|
+
const projectPath = options.projectPath || process.cwd()
|
|
573
|
+
const isJson = options.json || false
|
|
574
|
+
const outPath = options.outPath || null
|
|
575
|
+
const wantFix = options.fix || false
|
|
576
|
+
|
|
577
|
+
// Basic audit is free; only the Pro --fix path needs a license re-check.
|
|
578
|
+
if (wantFix) {
|
|
579
|
+
await ensureLicenseFresh()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Feature gate: audit is free for basic, Pro for hallucination check
|
|
583
|
+
const isPro = hasFeature('auditPro')
|
|
584
|
+
const auditOptions = { pro: isPro, fix: wantFix }
|
|
585
|
+
|
|
586
|
+
if (wantFix && !isPro) {
|
|
587
|
+
showUpgradeMessage('Audit --fix (Claude Code prompt generation)')
|
|
588
|
+
// Continue without fix prompts rather than blocking
|
|
589
|
+
auditOptions.fix = false
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const result = await runAudit(projectPath, auditOptions)
|
|
593
|
+
|
|
594
|
+
if (result.error === 'semgrep_not_installed') {
|
|
595
|
+
console.error('❌ semgrep is not installed.')
|
|
596
|
+
console.error(result.hint)
|
|
597
|
+
process.exit(1)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
if (result.error) {
|
|
601
|
+
console.error(`❌ Audit error: ${result.error}`)
|
|
602
|
+
process.exit(1)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const { findings } = result
|
|
606
|
+
|
|
607
|
+
if (isJson) {
|
|
608
|
+
const output = JSON.stringify(
|
|
609
|
+
{
|
|
610
|
+
summary: {
|
|
611
|
+
total: findings.length,
|
|
612
|
+
critical: findings.filter(f => f.severity === SEVERITY.CRITICAL)
|
|
613
|
+
.length,
|
|
614
|
+
high: findings.filter(f => f.severity === SEVERITY.HIGH).length,
|
|
615
|
+
medium: findings.filter(f => f.severity === SEVERITY.MEDIUM).length,
|
|
616
|
+
low: findings.filter(f => f.severity === SEVERITY.LOW).length,
|
|
617
|
+
},
|
|
618
|
+
findings,
|
|
619
|
+
},
|
|
620
|
+
null,
|
|
621
|
+
2
|
|
622
|
+
)
|
|
623
|
+
if (outPath) {
|
|
624
|
+
fs.writeFileSync(outPath, output, 'utf8')
|
|
625
|
+
console.log(`✅ Audit report written to ${outPath}`)
|
|
626
|
+
} else {
|
|
627
|
+
console.log(output)
|
|
628
|
+
}
|
|
629
|
+
} else {
|
|
630
|
+
const report = outPath
|
|
631
|
+
? buildMarkdownReport(findings, auditOptions)
|
|
632
|
+
: buildHumanReport(findings, auditOptions)
|
|
633
|
+
|
|
634
|
+
if (outPath) {
|
|
635
|
+
fs.writeFileSync(outPath, report, 'utf8')
|
|
636
|
+
console.log(`✅ Audit report written to ${outPath}`)
|
|
637
|
+
// Also print summary to stdout
|
|
638
|
+
const total = findings.length
|
|
639
|
+
const critical = findings.filter(
|
|
640
|
+
f => f.severity === SEVERITY.CRITICAL
|
|
641
|
+
).length
|
|
642
|
+
const high = findings.filter(f => f.severity === SEVERITY.HIGH).length
|
|
643
|
+
console.log(` ${total} finding(s): ${critical} critical, ${high} high`)
|
|
644
|
+
} else {
|
|
645
|
+
console.log(report)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const hasCritical = findings.some(f => f.severity === SEVERITY.CRITICAL)
|
|
650
|
+
const hasHigh = findings.some(f => f.severity === SEVERITY.HIGH)
|
|
651
|
+
|
|
652
|
+
if (options.noFail) {
|
|
653
|
+
process.exit(0)
|
|
654
|
+
} else if (hasCritical || hasHigh) {
|
|
655
|
+
process.exit(1)
|
|
656
|
+
} else {
|
|
657
|
+
process.exit(0)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
module.exports = {
|
|
662
|
+
handleAudit,
|
|
663
|
+
runAudit,
|
|
664
|
+
mapSemgrepFinding,
|
|
665
|
+
groupBySeverity,
|
|
666
|
+
buildMarkdownReport,
|
|
667
|
+
buildHumanReport,
|
|
668
|
+
}
|