create-qa-architect 5.0.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.
Files changed (67) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/CLAUDE_MD_AUTOMATION.md +248 -0
  3. package/.github/PROGRESSIVE_QUALITY_IMPLEMENTATION.md +408 -0
  4. package/.github/PROGRESSIVE_QUALITY_PROPOSAL.md +443 -0
  5. package/.github/RELEASE_CHECKLIST.md +100 -0
  6. package/.github/dependabot.yml +50 -0
  7. package/.github/git-sync.sh +48 -0
  8. package/.github/workflows/claude-md-validation.yml +82 -0
  9. package/.github/workflows/nightly-gitleaks-verification.yml +176 -0
  10. package/.github/workflows/pnpm-ci.yml.example +53 -0
  11. package/.github/workflows/python-ci.yml.example +69 -0
  12. package/.github/workflows/quality-legacy.yml.backup +165 -0
  13. package/.github/workflows/quality-progressive.yml.example +291 -0
  14. package/.github/workflows/quality.yml +436 -0
  15. package/.github/workflows/release.yml +53 -0
  16. package/.nvmrc +1 -0
  17. package/.prettierignore +14 -0
  18. package/.prettierrc +9 -0
  19. package/.stylelintrc.json +5 -0
  20. package/README.md +212 -0
  21. package/config/.lighthouserc.js +45 -0
  22. package/config/.pre-commit-config.yaml +66 -0
  23. package/config/constants.js +128 -0
  24. package/config/defaults.js +124 -0
  25. package/config/pyproject.toml +124 -0
  26. package/config/quality-config.schema.json +97 -0
  27. package/config/quality-python.yml +89 -0
  28. package/config/requirements-dev.txt +15 -0
  29. package/create-saas-monetization.js +1465 -0
  30. package/eslint.config.cjs +117 -0
  31. package/eslint.config.ts.cjs +99 -0
  32. package/legal/README.md +106 -0
  33. package/legal/copyright.md +76 -0
  34. package/legal/disclaimer.md +146 -0
  35. package/legal/privacy-policy.html +324 -0
  36. package/legal/privacy-policy.md +196 -0
  37. package/legal/terms-of-service.md +224 -0
  38. package/lib/billing-dashboard.html +645 -0
  39. package/lib/config-validator.js +163 -0
  40. package/lib/dependency-monitoring-basic.js +185 -0
  41. package/lib/dependency-monitoring-premium.js +1490 -0
  42. package/lib/error-reporter.js +444 -0
  43. package/lib/interactive/prompt.js +128 -0
  44. package/lib/interactive/questions.js +146 -0
  45. package/lib/license-validator.js +403 -0
  46. package/lib/licensing.js +989 -0
  47. package/lib/package-utils.js +187 -0
  48. package/lib/project-maturity.js +516 -0
  49. package/lib/security-enhancements.js +340 -0
  50. package/lib/setup-enhancements.js +317 -0
  51. package/lib/smart-strategy-generator.js +344 -0
  52. package/lib/telemetry.js +323 -0
  53. package/lib/template-loader.js +252 -0
  54. package/lib/typescript-config-generator.js +210 -0
  55. package/lib/ui-helpers.js +74 -0
  56. package/lib/validation/base-validator.js +174 -0
  57. package/lib/validation/cache-manager.js +158 -0
  58. package/lib/validation/config-security.js +741 -0
  59. package/lib/validation/documentation.js +326 -0
  60. package/lib/validation/index.js +186 -0
  61. package/lib/validation/validation-factory.js +153 -0
  62. package/lib/validation/workflow-validation.js +172 -0
  63. package/lib/yaml-utils.js +120 -0
  64. package/marketing/beta-user-email-campaign.md +372 -0
  65. package/marketing/landing-page.html +721 -0
  66. package/package.json +165 -0
  67. package/setup.js +2076 -0
@@ -0,0 +1,741 @@
1
+ 'use strict'
2
+
3
+ const fs = require('fs')
4
+ const path = require('path')
5
+ const os = require('os')
6
+ const crypto = require('crypto')
7
+ const https = require('https')
8
+ const { execSync } = require('child_process')
9
+ const { showProgress } = require('../ui-helpers')
10
+
11
+ // Pinned gitleaks version for reproducible security scanning
12
+ const GITLEAKS_VERSION = '8.28.0'
13
+ // Real SHA256 checksums from https://github.com/gitleaks/gitleaks/releases/tag/v8.28.0
14
+ const GITLEAKS_CHECKSUMS = {
15
+ 'linux-x64':
16
+ 'a65b5253807a68ac0cafa4414031fd740aeb55f54fb7e55f386acb52e6a840eb',
17
+ 'darwin-x64':
18
+ 'edf5a507008b0d2ef4959575772772770586409c1f6f74dabf19cbe7ec341ced',
19
+ 'darwin-arm64':
20
+ '5588b5d942dffa048720f7e6e1d274283219fb5722a2c7564d22e83ba39087d7',
21
+ 'win32-x64':
22
+ 'da6458e8864af553807de1c46a7a8eac0880bd6b99ba56288e87e86a45af884f',
23
+ }
24
+
25
+ /**
26
+ * Configuration Security Scanner
27
+ * Uses mature security tools instead of custom regex heuristics
28
+ */
29
+ class ConfigSecurityScanner {
30
+ constructor(options = {}) {
31
+ this.issues = []
32
+ this.options = options
33
+
34
+ // checksumMap dependency injection - FOR TESTING ONLY
35
+ // WARNING: Do not use in production CLI - this bypasses security verification!
36
+ if (options.checksumMap) {
37
+ if (process.env.NODE_ENV === 'production') {
38
+ throw new Error(
39
+ 'checksumMap override not allowed in production environment'
40
+ )
41
+ }
42
+ if (!options.quiet) {
43
+ console.warn(
44
+ '⚠️ WARNING: Using custom checksum map - FOR TESTING ONLY!'
45
+ )
46
+ }
47
+ this.checksumMap = options.checksumMap
48
+ } else {
49
+ this.checksumMap = GITLEAKS_CHECKSUMS
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Resolve gitleaks binary with fallback chain for reproducible security
55
+ * 1. Process.env.GITLEAKS_PATH (user override)
56
+ * 2. Global gitleaks binary (brew, choco, etc.)
57
+ * 3. Cached pinned version in ~/.cache/create-qa-architect/
58
+ * 4. npx fallback with loud warning
59
+ */
60
+ async resolveGitleaksBinary() {
61
+ // 1. Check environment override
62
+ if (process.env.GITLEAKS_PATH) {
63
+ if (fs.existsSync(process.env.GITLEAKS_PATH)) {
64
+ return process.env.GITLEAKS_PATH
65
+ }
66
+ console.warn(
67
+ `⚠️ GITLEAKS_PATH set but binary not found: ${process.env.GITLEAKS_PATH}`
68
+ )
69
+ }
70
+
71
+ // 2. Check global installation
72
+ try {
73
+ const globalPath = execSync('which gitleaks', {
74
+ encoding: 'utf8',
75
+ stdio: 'pipe',
76
+ }).trim()
77
+ if (globalPath && fs.existsSync(globalPath)) {
78
+ return globalPath
79
+ }
80
+ } catch {
81
+ // which command failed, global binary not available
82
+ }
83
+
84
+ // 3. Use cached pinned version or download if missing
85
+ const cacheDir = path.join(os.homedir(), '.cache', 'create-qa-architect')
86
+ const binaryName =
87
+ process.platform === 'win32' ? 'gitleaks.exe' : 'gitleaks'
88
+ const cachedBinary = path.join(
89
+ cacheDir,
90
+ 'gitleaks',
91
+ GITLEAKS_VERSION,
92
+ binaryName
93
+ )
94
+
95
+ if (fs.existsSync(cachedBinary)) {
96
+ // Verify cached binary integrity
97
+ if (await this.verifyBinaryChecksum(cachedBinary)) {
98
+ return cachedBinary
99
+ } else {
100
+ console.warn(
101
+ '⚠️ Cached gitleaks binary failed checksum verification, re-downloading...'
102
+ )
103
+ fs.rmSync(path.dirname(cachedBinary), { recursive: true, force: true })
104
+ }
105
+ }
106
+
107
+ // Download and cache pinned version
108
+ try {
109
+ await this.downloadGitleaksBinary(cachedBinary)
110
+ return cachedBinary
111
+ } catch (error) {
112
+ if (!this.options.quiet) {
113
+ console.error(
114
+ `❌ Failed to download gitleaks v${GITLEAKS_VERSION}: ${error.message}`
115
+ )
116
+ }
117
+
118
+ // Check if fallback to unpinned gitleaks is explicitly allowed
119
+ if (this.options.allowLatestGitleaks) {
120
+ console.warn(
121
+ '🚨 WARNING: Using npx gitleaks (supply chain risk - downloads latest version)'
122
+ )
123
+ console.warn(
124
+ '📌 Consider: brew install gitleaks (macOS) or choco install gitleaks (Windows)'
125
+ )
126
+ return 'npx gitleaks'
127
+ }
128
+
129
+ // Security-first: fail hard instead of silent fallback
130
+ throw new Error(
131
+ `Cannot resolve secure gitleaks binary. Options:\n` +
132
+ `1. Install globally: brew install gitleaks (macOS) or choco install gitleaks (Windows)\n` +
133
+ `2. Set GITLEAKS_PATH to your preferred binary\n` +
134
+ `3. Use --allow-latest-gitleaks flag (NOT RECOMMENDED - supply chain risk)`
135
+ )
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Download and verify gitleaks binary for current platform
141
+ */
142
+ async downloadGitleaksBinary(targetPath) {
143
+ const platform = this.detectPlatform()
144
+ if (!platform) {
145
+ throw new Error(
146
+ `Unsupported platform: ${process.platform}-${process.arch}`
147
+ )
148
+ }
149
+
150
+ const tarballName = `gitleaks_${GITLEAKS_VERSION}_${platform}.tar.gz`
151
+ const downloadUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${tarballName}`
152
+
153
+ console.log(
154
+ `📥 Downloading gitleaks v${GITLEAKS_VERSION} for ${platform}...`
155
+ )
156
+
157
+ // Ensure cache directory exists
158
+ const cacheDir = path.dirname(targetPath)
159
+ fs.mkdirSync(cacheDir, { recursive: true })
160
+
161
+ // Download and extract
162
+ const tarballPath = path.join(cacheDir, tarballName)
163
+ await this.downloadFile(downloadUrl, tarballPath)
164
+
165
+ // Extract binary (tar.gz contains just the gitleaks executable)
166
+ const tar = require('tar')
167
+ await tar.extract({
168
+ file: tarballPath,
169
+ cwd: cacheDir,
170
+ })
171
+
172
+ // Move extracted binary to final location and make executable
173
+ const extractedBinary = path.join(
174
+ cacheDir,
175
+ process.platform === 'win32' ? 'gitleaks.exe' : 'gitleaks'
176
+ )
177
+ fs.renameSync(extractedBinary, targetPath)
178
+ if (process.platform !== 'win32') {
179
+ fs.chmodSync(targetPath, 0o755)
180
+ }
181
+
182
+ // Verify checksum
183
+ if (!(await this.verifyBinaryChecksum(targetPath))) {
184
+ fs.rmSync(targetPath, { force: true })
185
+ throw new Error('Downloaded binary failed checksum verification')
186
+ }
187
+
188
+ // Cleanup tarball
189
+ fs.rmSync(tarballPath, { force: true })
190
+
191
+ console.log(`✅ gitleaks v${GITLEAKS_VERSION} cached and verified`)
192
+ }
193
+
194
+ /**
195
+ * Detect current platform for gitleaks release naming
196
+ */
197
+ detectPlatform() {
198
+ const platforms = {
199
+ 'darwin-x64': 'darwin_x64',
200
+ 'darwin-arm64': 'darwin_arm64',
201
+ 'linux-x64': 'linux_x64',
202
+ 'win32-x64': 'windows_x64',
203
+ }
204
+
205
+ const key = `${process.platform}-${process.arch}`
206
+ return platforms[key] || null
207
+ }
208
+
209
+ /**
210
+ * Download file from URL to target path
211
+ */
212
+ downloadFile(url, targetPath) {
213
+ return new Promise((resolve, reject) => {
214
+ const file = fs.createWriteStream(targetPath)
215
+
216
+ https
217
+ .get(url, response => {
218
+ if (response.statusCode === 302 || response.statusCode === 301) {
219
+ // Follow redirect
220
+ return this.downloadFile(response.headers.location, targetPath)
221
+ .then(resolve)
222
+ .catch(reject)
223
+ }
224
+
225
+ if (response.statusCode !== 200) {
226
+ reject(
227
+ new Error(
228
+ `HTTP ${response.statusCode}: ${response.statusMessage}`
229
+ )
230
+ )
231
+ return
232
+ }
233
+
234
+ response.pipe(file)
235
+
236
+ file.on('finish', () => {
237
+ file.close()
238
+ resolve()
239
+ })
240
+
241
+ file.on('error', err => {
242
+ fs.unlink(targetPath, () => {}) // Delete partial file
243
+ reject(err)
244
+ })
245
+ })
246
+ .on('error', reject)
247
+ })
248
+ }
249
+
250
+ /**
251
+ * Verify binary checksum against known good hash
252
+ * CRITICAL SECURITY: Fails hard on missing checksums - no silent bypass
253
+ */
254
+ async verifyBinaryChecksum(binaryPath) {
255
+ const platformKey = `${process.platform}-${process.arch}`
256
+ const expectedChecksum = this.checksumMap[platformKey]
257
+
258
+ if (!expectedChecksum) {
259
+ throw new Error(
260
+ `No checksum available for platform ${platformKey} - refusing to execute unverified binary`
261
+ )
262
+ }
263
+
264
+ try {
265
+ const binaryData = fs.readFileSync(binaryPath)
266
+ const actualChecksum = crypto
267
+ .createHash('sha256')
268
+ .update(binaryData)
269
+ .digest('hex')
270
+
271
+ if (actualChecksum !== expectedChecksum) {
272
+ throw new Error(
273
+ `Checksum mismatch for ${binaryPath}: expected ${expectedChecksum}, got ${actualChecksum}`
274
+ )
275
+ }
276
+
277
+ return true
278
+ } catch (error) {
279
+ if (
280
+ error.message.includes('Checksum mismatch') ||
281
+ error.message.includes('No checksum available')
282
+ ) {
283
+ throw error // Re-throw security-critical errors
284
+ }
285
+ throw new Error(`Checksum verification failed: ${error.message}`)
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Scan all configuration files for security issues
291
+ */
292
+ async scanAll() {
293
+ console.log('🔍 Running security scans with mature tools...')
294
+
295
+ this.issues = []
296
+
297
+ if (!this.options.disableNpmAudit) {
298
+ await this.runNpmAudit()
299
+ }
300
+
301
+ if (!this.options.disableEslintSecurity) {
302
+ await this.runESLintSecurity()
303
+ }
304
+
305
+ if (!this.options.disableGitleaks) {
306
+ await this.runGitleaks()
307
+ }
308
+
309
+ await this.scanClientSideSecrets()
310
+ await this.scanDockerSecrets()
311
+ await this.scanEnvironmentFiles()
312
+ await this.checkGitignore()
313
+
314
+ if (this.issues.length > 0) {
315
+ console.error(`❌ Found ${this.issues.length} security issue(s):`)
316
+ this.issues.forEach(issue => console.error(` ${issue}`))
317
+ throw new Error('Security violations detected')
318
+ }
319
+
320
+ console.log('✅ Security checks passed')
321
+ return { issues: this.issues, passed: this.issues.length === 0 }
322
+ }
323
+
324
+ /**
325
+ * Run package manager audit for dependency vulnerabilities
326
+ * Only checks production dependencies (dev vulnerabilities are acceptable)
327
+ */
328
+ async runNpmAudit() {
329
+ if (!fs.existsSync('package.json')) return
330
+
331
+ const spinner = showProgress(
332
+ 'Running npm audit for dependency vulnerabilities...'
333
+ )
334
+
335
+ // Detect package manager
336
+ const {
337
+ detectPackageManager,
338
+ getAuditCommand,
339
+ } = require('../package-utils')
340
+ const packageManager = detectPackageManager(process.cwd())
341
+ const baseAuditCmd = getAuditCommand(packageManager)
342
+
343
+ try {
344
+ // Run audit and capture high/critical vulnerabilities
345
+ // Use --omit=dev to only check production dependencies
346
+ const auditCmd = `${baseAuditCmd} --audit-level high --omit=dev --json`
347
+ execSync(auditCmd, {
348
+ stdio: 'pipe',
349
+ timeout: 60000, // 60 second timeout for audit operations
350
+ encoding: 'utf8',
351
+ })
352
+ } catch (error) {
353
+ if (error.signal === 'SIGTERM') {
354
+ // Timeout occurred
355
+ this.issues.push(
356
+ `${packageManager} audit: Scan timed out after 60 seconds. Check for network issues or consider running audit manually.`
357
+ )
358
+ return
359
+ }
360
+ // Audit exits with code 1 when vulnerabilities are found
361
+ if (error.stdout) {
362
+ try {
363
+ const auditResult = JSON.parse(error.stdout.toString())
364
+ if (auditResult.metadata && auditResult.metadata.vulnerabilities) {
365
+ const vulns = auditResult.metadata.vulnerabilities
366
+ const total = vulns.high + vulns.critical + vulns.moderate
367
+ if (total > 0) {
368
+ spinner.fail(`npm audit found ${total} vulnerabilities`)
369
+ this.issues.push(
370
+ `${packageManager} audit: ${total} vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
371
+ )
372
+ } else {
373
+ spinner.succeed(
374
+ 'npm audit completed - no high/critical vulnerabilities'
375
+ )
376
+ }
377
+ } else {
378
+ spinner.succeed('npm audit completed')
379
+ }
380
+ } catch {
381
+ spinner.warn(`Could not parse ${packageManager} audit output`)
382
+ console.warn(`Could not parse ${packageManager} audit output`)
383
+ }
384
+ } else {
385
+ spinner.succeed('npm audit completed')
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Run gitleaks for comprehensive secret scanning with timeout and redaction
392
+ * Uses pinned binary for reproducible security scanning
393
+ */
394
+ async runGitleaks() {
395
+ const spinner = showProgress('Scanning for secrets with gitleaks...')
396
+
397
+ try {
398
+ // Resolve gitleaks binary with security-focused fallback chain
399
+ const gitleaksBinary = await this.resolveGitleaksBinary()
400
+
401
+ // Build command - handle npx vs direct binary execution
402
+ const isNpxCommand = gitleaksBinary.startsWith('npx ')
403
+ const command = isNpxCommand
404
+ ? `${gitleaksBinary} detect --source . --redact`
405
+ : `"${gitleaksBinary}" detect --source . --redact`
406
+
407
+ // Run gitleaks with --redact to prevent secret exposure and timeout for safety
408
+ execSync(command, {
409
+ stdio: 'pipe',
410
+ timeout: 30000, // 30 second timeout to prevent hangs
411
+ encoding: 'utf8',
412
+ })
413
+ spinner.succeed('gitleaks scan completed - no secrets detected')
414
+ } catch (error) {
415
+ if (error.signal === 'SIGTERM') {
416
+ // Timeout occurred
417
+ this.issues.push(
418
+ 'gitleaks: Scan timed out after 30 seconds. Repository may be too large for comprehensive scanning.'
419
+ )
420
+ return
421
+ }
422
+
423
+ if (error.status === 1) {
424
+ // Gitleaks found secrets (exit code 1)
425
+ const output = error.stdout
426
+ ? error.stdout.toString()
427
+ : error.stderr
428
+ ? error.stderr.toString()
429
+ : ''
430
+ if (output.includes('leaks found') || output.includes('Finding:')) {
431
+ // Extract just the count, not the actual findings
432
+ const leakMatches = output.match(/(\d+)\s+leaks?\s+found/i)
433
+ const leakCount = leakMatches ? leakMatches[1] : 'some'
434
+ spinner.fail('gitleaks found potential secrets')
435
+ this.issues.push(
436
+ `gitleaks: ${leakCount} potential secret(s) detected in repository. Run gitleaks with --redact for details.`
437
+ )
438
+ } else {
439
+ spinner.succeed('gitleaks scan completed')
440
+ }
441
+ } else {
442
+ // Other errors (missing binary, permission issues, etc.)
443
+ const stderr = error.stderr ? error.stderr.toString() : ''
444
+ const stdout = error.stdout ? error.stdout.toString() : ''
445
+ const output = stderr || stdout || error.message
446
+
447
+ if (
448
+ output.includes('not found') ||
449
+ output.includes('command not found') ||
450
+ output.includes('ENOENT') ||
451
+ error?.code === 'ENOENT'
452
+ ) {
453
+ // Missing gitleaks should block security validation, not silently pass
454
+ spinner.fail('gitleaks tool not found')
455
+ this.issues.push(
456
+ `gitleaks: Tool not found. Install gitleaks v${GITLEAKS_VERSION}+ for comprehensive secret scanning or use --no-gitleaks to skip.`
457
+ )
458
+ } else {
459
+ // Log the actual error so users know gitleaks failed to run (redact any potential sensitive info)
460
+ const sanitizedError = output
461
+ .split('\n')[0]
462
+ .replace(/[A-Za-z0-9+/=]{20,}/g, '[REDACTED]')
463
+ spinner.fail('gitleaks failed to run')
464
+ console.warn(`⚠️ gitleaks failed to run: ${sanitizedError}`)
465
+ this.issues.push(
466
+ `gitleaks: Failed to run - ${sanitizedError}. Install gitleaks for secret scanning.`
467
+ )
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Run ESLint with security rules using programmatic API
475
+ */
476
+ async runESLintSecurity() {
477
+ // Detect which ESLint config file exists (check all variants)
478
+ let eslintConfigPath = null
479
+ const configVariants = [
480
+ 'eslint.config.cjs',
481
+ 'eslint.config.js',
482
+ 'eslint.config.mjs',
483
+ 'eslint.config.ts.cjs', // TypeScript projects
484
+ ]
485
+
486
+ for (const configFile of configVariants) {
487
+ if (fs.existsSync(configFile)) {
488
+ eslintConfigPath = path.resolve(configFile)
489
+ break
490
+ }
491
+ }
492
+
493
+ if (!eslintConfigPath) {
494
+ return // No ESLint config found
495
+ }
496
+
497
+ const spinner = showProgress('Running ESLint security checks...')
498
+
499
+ // Step 1: Load ESLint module
500
+ let ESLint
501
+ try {
502
+ const eslintModule = require('eslint')
503
+ ESLint = eslintModule.ESLint
504
+ } catch (error) {
505
+ if (
506
+ error?.code === 'MODULE_NOT_FOUND' ||
507
+ error?.message?.includes('Cannot find module')
508
+ ) {
509
+ spinner.fail('ESLint not found')
510
+ this.issues.push(
511
+ 'ESLint Security: ESLint is not installed or cannot be loaded. ' +
512
+ 'Install eslint and eslint-plugin-security to enable security validation, or use --no-eslint-security to skip.'
513
+ )
514
+ return
515
+ }
516
+ throw error // Re-throw unexpected errors
517
+ }
518
+
519
+ // Step 2: Create ESLint instance with detected config
520
+ let eslint
521
+ try {
522
+ eslint = new ESLint({
523
+ overrideConfigFile: eslintConfigPath,
524
+ })
525
+ } catch (error) {
526
+ if (error.name === 'SyntaxError' || error.message?.includes('parse')) {
527
+ spinner.fail('ESLint configuration error')
528
+ this.issues.push(
529
+ `ESLint Security: ESLint configuration file has errors: ${error.message}. ` +
530
+ 'Fix the configuration or use --no-eslint-security to skip.'
531
+ )
532
+ return
533
+ }
534
+ spinner.fail('ESLint initialization failed')
535
+ this.issues.push(
536
+ `ESLint Security: Failed to initialize ESLint: ${error.message}. ` +
537
+ 'Review the error and fix the issue, or use --no-eslint-security to skip.'
538
+ )
539
+ return
540
+ }
541
+
542
+ // Step 3: Lint files in current directory
543
+ let results
544
+ try {
545
+ results = await eslint.lintFiles(['.'])
546
+ } catch (error) {
547
+ spinner.fail('ESLint linting failed')
548
+ this.issues.push(
549
+ `ESLint Security: Linting failed: ${error.message}. ` +
550
+ 'Review the error and fix the issue, or use --no-eslint-security to skip.'
551
+ )
552
+ return
553
+ }
554
+
555
+ // Step 4: Process results (no try-catch needed - safe operations)
556
+ const securityIssues = []
557
+ for (const result of results) {
558
+ for (const message of result.messages) {
559
+ if (message.ruleId && message.ruleId.startsWith('security/')) {
560
+ securityIssues.push({
561
+ file: result.filePath,
562
+ line: message.line,
563
+ column: message.column,
564
+ rule: message.ruleId,
565
+ message: message.message,
566
+ severity: message.severity,
567
+ })
568
+ }
569
+ }
570
+ }
571
+
572
+ // Report security issues
573
+ if (securityIssues.length > 0) {
574
+ spinner.fail(`ESLint security found ${securityIssues.length} issue(s)`)
575
+ securityIssues.forEach(issue => {
576
+ const relativePath = path.relative(process.cwd(), issue.file)
577
+ this.issues.push(
578
+ `ESLint Security: ${relativePath}:${issue.line}:${issue.column} - ${issue.message} (${issue.rule})`
579
+ )
580
+ })
581
+ } else {
582
+ spinner.succeed('ESLint security checks passed')
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Scan for client-side secret exposure in Next.js and Vite
588
+ */
589
+ async scanClientSideSecrets() {
590
+ await this.scanNextjsConfig()
591
+ await this.scanViteConfig()
592
+ }
593
+
594
+ /**
595
+ * Scan Next.js configuration for client-side secret exposure
596
+ */
597
+ async scanNextjsConfig() {
598
+ const configFiles = ['next.config.js', 'next.config.mjs', 'next.config.ts']
599
+
600
+ for (const configFile of configFiles) {
601
+ if (fs.existsSync(configFile)) {
602
+ const content = fs.readFileSync(configFile, 'utf8')
603
+
604
+ // Check for secrets in env block (client-side exposure risk)
605
+ const envBlockRegex = /env:\s*\{([^}]+)\}/gi
606
+ let match
607
+
608
+ while ((match = envBlockRegex.exec(content)) !== null) {
609
+ const envBlock = match[1]
610
+
611
+ const secretPatterns = [
612
+ { pattern: /\b\w*SECRET\w*\b/gi, type: 'SECRET' },
613
+ { pattern: /\b\w*PASSWORD\w*\b/gi, type: 'PASSWORD' },
614
+ { pattern: /\b\w*PRIVATE\w*\b/gi, type: 'PRIVATE' },
615
+ { pattern: /\b\w*API_KEY\w*\b/gi, type: 'API_KEY' },
616
+ { pattern: /\b\w*_KEY\b/gi, type: 'KEY' },
617
+ { pattern: /\b\w*TOKEN\w*\b/gi, type: 'TOKEN' },
618
+ { pattern: /\b\w*WEBHOOK\w*\b/gi, type: 'WEBHOOK' },
619
+ ]
620
+
621
+ for (const { pattern, type } of secretPatterns) {
622
+ if (pattern.test(envBlock)) {
623
+ this.issues.push(
624
+ `${configFile}: Potential ${type} exposure in env block. ` +
625
+ `Variables in 'env' are sent to client bundle. ` +
626
+ `Use process.env.${type} server-side instead.`
627
+ )
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Scan Vite configuration for client-side secret exposure
637
+ */
638
+ async scanViteConfig() {
639
+ const configFiles = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
640
+
641
+ for (const configFile of configFiles) {
642
+ if (fs.existsSync(configFile)) {
643
+ const content = fs.readFileSync(configFile, 'utf8')
644
+
645
+ // VITE_ prefixed variables are automatically exposed to client
646
+ const viteSecretPattern =
647
+ /VITE_[^=]*(?:SECRET|PASSWORD|PRIVATE|KEY|TOKEN)/gi
648
+ const matches = content.match(viteSecretPattern)
649
+
650
+ if (matches && matches.length > 0) {
651
+ this.issues.push(
652
+ `${configFile}: VITE_ prefixed secrets detected: ${matches.join(', ')}. ` +
653
+ `All VITE_ variables are exposed to client bundle!`
654
+ )
655
+ }
656
+ }
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Scan Dockerfile for hardcoded secrets
662
+ */
663
+ async scanDockerSecrets() {
664
+ if (fs.existsSync('Dockerfile')) {
665
+ const content = fs.readFileSync('Dockerfile', 'utf8')
666
+
667
+ // Check for hardcoded secrets in ENV statements
668
+ const envStatements = content.match(/^ENV\s+.+$/gim) || []
669
+
670
+ for (const envStatement of envStatements) {
671
+ const secretPattern =
672
+ /(?:SECRET|PASSWORD|KEY|TOKEN)\s*=\s*["']?[^"\s']+/gi
673
+ if (secretPattern.test(envStatement)) {
674
+ this.issues.push(
675
+ `Dockerfile: Hardcoded secret in ENV statement: ${envStatement.trim()}`
676
+ )
677
+ }
678
+ }
679
+ }
680
+ }
681
+
682
+ /**
683
+ * Check .gitignore for security-sensitive files
684
+ */
685
+ async checkGitignore() {
686
+ if (!fs.existsSync('.gitignore')) {
687
+ this.issues.push(
688
+ 'No .gitignore found. Create one to prevent committing sensitive files.'
689
+ )
690
+ return
691
+ }
692
+
693
+ const gitignore = fs.readFileSync('.gitignore', 'utf8')
694
+ const requiredIgnores = ['.env*', 'node_modules', '*.log']
695
+
696
+ for (const pattern of requiredIgnores) {
697
+ if (!gitignore.includes(pattern)) {
698
+ this.issues.push(`Missing '${pattern}' in .gitignore`)
699
+ }
700
+ }
701
+ }
702
+
703
+ /**
704
+ * Scan environment files for common issues
705
+ */
706
+ async scanEnvironmentFiles() {
707
+ // Check that .env files are properly ignored
708
+ const envFiles = [
709
+ '.env',
710
+ '.env.local',
711
+ '.env.production',
712
+ '.env.development',
713
+ ]
714
+
715
+ const existingEnvFiles = envFiles.filter(file => fs.existsSync(file))
716
+
717
+ if (existingEnvFiles.length > 0) {
718
+ if (!fs.existsSync('.gitignore')) {
719
+ this.issues.push('Environment files found but no .gitignore exists')
720
+ } else {
721
+ const gitignore = fs.readFileSync('.gitignore', 'utf8')
722
+ for (const envFile of existingEnvFiles) {
723
+ if (!gitignore.includes(envFile) && !gitignore.includes('.env*')) {
724
+ this.issues.push(
725
+ `${envFile} exists but not in .gitignore. Add it to prevent secret exposure.`
726
+ )
727
+ }
728
+ }
729
+ }
730
+ }
731
+
732
+ // Check for .env.example without corresponding documentation
733
+ if (fs.existsSync('.env.example') && !fs.existsSync('README.md')) {
734
+ this.issues.push(
735
+ '.env.example exists but no README.md to document required variables'
736
+ )
737
+ }
738
+ }
739
+ }
740
+
741
+ module.exports = { ConfigSecurityScanner }