create-qa-architect 5.0.7 → 5.4.3

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 (45) hide show
  1. package/.github/workflows/auto-release.yml +49 -0
  2. package/.github/workflows/quality.yml +11 -11
  3. package/.github/workflows/shell-ci.yml.example +82 -0
  4. package/.github/workflows/shell-quality.yml.example +148 -0
  5. package/README.md +165 -12
  6. package/config/shell-ci.yml +82 -0
  7. package/config/shell-quality.yml +148 -0
  8. package/docs/ADOPTION-SUMMARY.md +41 -0
  9. package/docs/ARCHITECTURE-REVIEW.md +67 -0
  10. package/docs/ARCHITECTURE.md +29 -45
  11. package/docs/CI-COST-ANALYSIS.md +323 -0
  12. package/docs/CODE-REVIEW.md +100 -0
  13. package/docs/REQUIREMENTS.md +148 -0
  14. package/docs/SECURITY-AUDIT.md +68 -0
  15. package/docs/test-trace-matrix.md +28 -0
  16. package/eslint.config.cjs +2 -0
  17. package/lib/commands/analyze-ci.js +616 -0
  18. package/lib/commands/deps.js +293 -0
  19. package/lib/commands/index.js +29 -0
  20. package/lib/commands/validate.js +85 -0
  21. package/lib/config-validator.js +28 -45
  22. package/lib/error-reporter.js +14 -2
  23. package/lib/github-api.js +138 -13
  24. package/lib/license-signing.js +125 -0
  25. package/lib/license-validator.js +359 -71
  26. package/lib/licensing.js +434 -106
  27. package/lib/package-utils.js +9 -9
  28. package/lib/prelaunch-validator.js +828 -0
  29. package/lib/project-maturity.js +58 -6
  30. package/lib/quality-tools-generator.js +495 -0
  31. package/lib/result-types.js +112 -0
  32. package/lib/security-enhancements.js +1 -1
  33. package/lib/smart-strategy-generator.js +46 -10
  34. package/lib/telemetry.js +1 -1
  35. package/lib/template-loader.js +52 -19
  36. package/lib/ui-helpers.js +1 -1
  37. package/lib/validation/cache-manager.js +36 -6
  38. package/lib/validation/config-security.js +100 -33
  39. package/lib/validation/index.js +68 -97
  40. package/lib/validation/workflow-validation.js +28 -7
  41. package/package.json +4 -6
  42. package/scripts/check-test-coverage.sh +46 -0
  43. package/scripts/validate-claude-md.js +80 -0
  44. package/setup.js +923 -301
  45. package/create-saas-monetization.js +0 -1513
@@ -0,0 +1,112 @@
1
+ /**
2
+ * DR25 fix: Standard result type helpers for consistent API across modules
3
+ *
4
+ * This module provides standard result builders to ensure consistency across
5
+ * all modules in the codebase. Use these instead of ad-hoc result objects.
6
+ *
7
+ * STANDARD PATTERNS:
8
+ *
9
+ * 1. Success/failure operations (file I/O, validation, etc.):
10
+ * { success: true, data?: any }
11
+ * { success: false, error: string, details?: any }
12
+ *
13
+ * 2. Validation results:
14
+ * { valid: true, data?: any }
15
+ * { valid: false, error: string, details?: any }
16
+ *
17
+ * 3. Query results:
18
+ * - Found: return data directly or { found: true, data }
19
+ * - Not found: return null or { found: false }
20
+ *
21
+ * MIGRATION STATUS:
22
+ * - lib/licensing.js: Uses mixed patterns (being migrated)
23
+ * - lib/license-validator.js: Uses valid/error pattern
24
+ * - webhook-handler.js: Uses success/error pattern
25
+ * - lib/error-reporter.js: Uses success/error pattern
26
+ * - lib/template-loader.js: Uses success/errors array pattern
27
+ *
28
+ * @module result-types
29
+ */
30
+
31
+ 'use strict'
32
+
33
+ /**
34
+ * Create a successful operation result
35
+ * @param {any} data - Optional data to include
36
+ * @returns {{ success: true, data?: any }}
37
+ */
38
+ function success(data = null) {
39
+ const result = { success: true }
40
+ if (data !== null && data !== undefined) {
41
+ result.data = data
42
+ }
43
+ return result
44
+ }
45
+
46
+ /**
47
+ * Create a failed operation result
48
+ * @param {string} error - Error message
49
+ * @param {any} details - Optional error details
50
+ * @returns {{ success: false, error: string, details?: any }}
51
+ */
52
+ function failure(error, details = null) {
53
+ const result = { success: false, error }
54
+ if (details !== null && details !== undefined) {
55
+ result.details = details
56
+ }
57
+ return result
58
+ }
59
+
60
+ /**
61
+ * Create a successful validation result
62
+ * @param {any} data - Optional validated data
63
+ * @returns {{ valid: true, data?: any }}
64
+ */
65
+ function valid(data = null) {
66
+ const result = { valid: true }
67
+ if (data !== null && data !== undefined) {
68
+ result.data = data
69
+ }
70
+ return result
71
+ }
72
+
73
+ /**
74
+ * Create a failed validation result
75
+ * @param {string} error - Validation error message
76
+ * @param {any} details - Optional error details
77
+ * @returns {{ valid: false, error: string, details?: any }}
78
+ */
79
+ function invalid(error, details = null) {
80
+ const result = { valid: false, error }
81
+ if (details !== null && details !== undefined) {
82
+ result.details = details
83
+ }
84
+ return result
85
+ }
86
+
87
+ /**
88
+ * Check if a result indicates success (works with both patterns)
89
+ * @param {object} result - Result object to check
90
+ * @returns {boolean} True if successful or valid
91
+ */
92
+ function isSuccess(result) {
93
+ return result && (result.success === true || result.valid === true)
94
+ }
95
+
96
+ /**
97
+ * Check if a result indicates failure (works with both patterns)
98
+ * @param {object} result - Result object to check
99
+ * @returns {boolean} True if failed or invalid
100
+ */
101
+ function isFailure(result) {
102
+ return result && (result.success === false || result.valid === false)
103
+ }
104
+
105
+ module.exports = {
106
+ success,
107
+ failure,
108
+ valid,
109
+ invalid,
110
+ isSuccess,
111
+ isFailure,
112
+ }
@@ -10,7 +10,7 @@ const path = require('path')
10
10
  * Generate enhanced security configuration
11
11
  * Makes security scanning the default, not optional
12
12
  */
13
- function generateSecurityFirstConfig(_projectPath = '.') {
13
+ function generateSecurityFirstConfig() {
14
14
  const securityConfig = {
15
15
  // Secret scanning configuration
16
16
  gitleaks: {
@@ -154,17 +154,48 @@ const PROJECT_CONFIGS = {
154
154
 
155
155
  /**
156
156
  * Read package.json from project path
157
+ * DR22 fix: Differentiate between missing file vs. read/parse errors
157
158
  */
158
159
  function readPackageJson(projectPath) {
160
+ const pkgPath = path.join(projectPath, 'package.json')
161
+
162
+ // File doesn't exist - expected for non-JS projects
163
+ if (!fs.existsSync(pkgPath)) {
164
+ return null
165
+ }
166
+
159
167
  try {
160
- const pkgPath = path.join(projectPath, 'package.json')
161
- if (fs.existsSync(pkgPath)) {
162
- return JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
168
+ const content = fs.readFileSync(pkgPath, 'utf8')
169
+ return JSON.parse(content)
170
+ } catch (error) {
171
+ // DR22 fix: Provide specific error messages for different failure types
172
+ if (error instanceof SyntaxError) {
173
+ const errorId = 'PKG_JSON_INVALID_SYNTAX'
174
+ console.error(`❌ [${errorId}] package.json has invalid JSON syntax:`)
175
+ console.error(` File: ${pkgPath}`)
176
+ console.error(` Error: ${error.message}`)
177
+ console.error(`\n Recovery steps:`)
178
+ console.error(` 1. Validate JSON: cat ${pkgPath} | jq .`)
179
+ console.error(
180
+ ` 2. Common issues: trailing commas, missing quotes, unclosed brackets`
181
+ )
182
+ console.error(` 3. Use a JSON validator: https://jsonlint.com`)
183
+ console.error(
184
+ `\n ⚠️ Smart Test Strategy will use generic fallback until this is fixed\n`
185
+ )
186
+ } else if (error.code === 'EACCES') {
187
+ console.error(`❌ Permission denied reading package.json: ${pkgPath}`)
188
+ console.error(' Check file permissions and try again')
189
+ } else {
190
+ console.error(
191
+ `❌ Unexpected error reading package.json: ${error.message}`
192
+ )
193
+ if (process.env.DEBUG) {
194
+ console.error(error.stack)
195
+ }
163
196
  }
164
- } catch {
165
- // Ignore errors
197
+ return null
166
198
  }
167
- return null
168
199
  }
169
200
 
170
201
  /**
@@ -214,7 +245,14 @@ function generateSmartStrategy(options = {}) {
214
245
  )
215
246
 
216
247
  if (!fs.existsSync(templatePath)) {
217
- throw new Error(`Smart strategy template not found at ${templatePath}`)
248
+ throw new Error(
249
+ `Smart strategy template not found at ${templatePath}\n` +
250
+ `This indicates a package installation issue.\n\n` +
251
+ `Troubleshooting steps:\n` +
252
+ `1. Reinstall the package: npm install -g create-qa-architect@latest\n` +
253
+ `2. Check file permissions: ls -la ${path.dirname(templatePath)}\n` +
254
+ `3. Report issue if problem persists: https://github.com/vibebuildlab/qa-architect/issues/new`
255
+ )
218
256
  }
219
257
 
220
258
  let template = fs.readFileSync(templatePath, 'utf8')
@@ -321,9 +359,7 @@ fi
321
359
  /**
322
360
  * Add test tier scripts to package.json
323
361
  */
324
- function getTestTierScripts(_projectType) {
325
- // const config = PROJECT_CONFIGS[projectType] || PROJECT_CONFIGS.default
326
-
362
+ function getTestTierScripts() {
327
363
  return {
328
364
  'test:fast': 'vitest run --reporter=basic --coverage=false',
329
365
  'test:medium':
package/lib/telemetry.js CHANGED
@@ -75,7 +75,7 @@ function loadTelemetryData() {
75
75
  const data = fs.readFileSync(file, 'utf8')
76
76
  return JSON.parse(data)
77
77
  }
78
- } catch {
78
+ } catch (_error) {
79
79
  // If corrupted, start fresh
80
80
  console.warn('⚠️ Telemetry data corrupted, starting fresh')
81
81
  }
@@ -34,7 +34,11 @@ class TemplateLoader {
34
34
  try {
35
35
  const stats = fs.statSync(templatePath)
36
36
  return stats.isDirectory()
37
- } catch {
37
+ } catch (error) {
38
+ // Silent failure fix: Log unexpected errors in debug mode
39
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
40
+ console.warn(`⚠️ Template path check failed: ${error.message}`)
41
+ }
38
42
  return false
39
43
  }
40
44
  }
@@ -51,7 +55,13 @@ class TemplateLoader {
51
55
 
52
56
  const stats = fs.statSync(templateDir)
53
57
  return stats.isDirectory()
54
- } catch {
58
+ } catch (error) {
59
+ // Silent failure fix: Log unexpected errors in debug mode
60
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
61
+ console.warn(
62
+ `⚠️ Template structure validation failed: ${error.message}`
63
+ )
64
+ }
55
65
  return false
56
66
  }
57
67
  }
@@ -128,6 +138,7 @@ class TemplateLoader {
128
138
  async loadTemplates(dir, baseDir = dir, isPackageDir = false) {
129
139
  /** @type {Record<string, string>} */
130
140
  const templates = {}
141
+ const errors = []
131
142
 
132
143
  try {
133
144
  const entries = fs.readdirSync(dir, { withFileTypes: true })
@@ -144,29 +155,51 @@ class TemplateLoader {
144
155
  const fullPath = path.join(dir, entry.name)
145
156
  const relativePath = path.relative(baseDir, fullPath)
146
157
 
147
- if (entry.isDirectory()) {
148
- // Recursively load from subdirectories
149
- // After first level, we're no longer in package root
150
- const subTemplates = await this.loadTemplates(
151
- fullPath,
152
- baseDir,
153
- false
154
- )
155
- Object.assign(templates, subTemplates)
156
- } else if (entry.isFile()) {
157
- // Load file content asynchronously for better performance
158
- const content = await fs.promises.readFile(fullPath, 'utf8')
159
- templates[relativePath] = content
158
+ try {
159
+ if (entry.isDirectory()) {
160
+ // Recursively load from subdirectories
161
+ const subTemplates = await this.loadTemplates(
162
+ fullPath,
163
+ baseDir,
164
+ false
165
+ )
166
+ Object.assign(templates, subTemplates)
167
+ } else if (entry.isFile()) {
168
+ // DR6 fix: Handle individual file errors separately
169
+ const content = await fs.promises.readFile(fullPath, 'utf8')
170
+ templates[relativePath] = content
171
+ }
172
+ } catch (fileError) {
173
+ // Track individual file errors
174
+ errors.push({ file: relativePath, error: fileError.message })
175
+
176
+ if (this.strict) {
177
+ throw new Error(
178
+ `Failed to load template ${relativePath}: ${fileError.message}`
179
+ )
180
+ } else if (this.verbose) {
181
+ console.warn(
182
+ `⚠️ Skipping template ${relativePath}: ${fileError.message}`
183
+ )
184
+ }
160
185
  }
161
186
  }
162
187
  } catch (error) {
163
- if (this.verbose) {
164
- console.warn(
165
- `⚠️ Warning: Could not load templates from ${dir}: ${error.message}`
166
- )
188
+ const message = `Could not load templates from ${dir}: ${error.message}`
189
+
190
+ if (this.strict) {
191
+ throw new Error(message)
192
+ } else if (this.verbose) {
193
+ console.warn(`⚠️ ${message}`)
167
194
  }
168
195
  }
169
196
 
197
+ // DR6 fix: Alert if critical templates are missing
198
+ if (errors.length > 0 && this.verbose) {
199
+ console.warn(`⚠️ ${errors.length} template file(s) failed to load`)
200
+ console.warn(' This may result in incomplete configuration')
201
+ }
202
+
170
203
  return templates
171
204
  }
172
205
 
package/lib/ui-helpers.js CHANGED
@@ -58,7 +58,7 @@ function showProgress(message) {
58
58
  const oraImport = require('ora')
59
59
  const ora = /** @type {any} */ (oraImport.default || oraImport)
60
60
  return ora(message).start()
61
- } catch {
61
+ } catch (_error) {
62
62
  // Fallback if ora not installed (graceful degradation)
63
63
  console.log(formatMessage('working', message))
64
64
  return {
@@ -40,8 +40,14 @@ class CacheManager {
40
40
  try {
41
41
  const content = fs.readFileSync(filePath, 'utf8')
42
42
  return crypto.createHash('sha256').update(content).digest('hex')
43
- } catch {
43
+ } catch (error) {
44
44
  // If file doesn't exist or can't be read, return timestamp-based key
45
+ // Silent failure fix: Log unexpected errors in debug mode
46
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
47
+ console.warn(
48
+ `⚠️ Cache key generation failed for ${filePath}: ${error.message}`
49
+ )
50
+ }
45
51
  return crypto
46
52
  .createHash('sha256')
47
53
  .update(`${filePath}-${Date.now()}`)
@@ -89,8 +95,12 @@ class CacheManager {
89
95
  }
90
96
 
91
97
  return cached.result
92
- } catch {
98
+ } catch (error) {
93
99
  // If cache file is corrupted or unreadable, treat as cache miss
100
+ // Silent failure fix: Log unexpected errors in debug mode
101
+ if (process.env.DEBUG && error.code !== 'ENOENT') {
102
+ console.warn(`⚠️ Cache read failed: ${error.message}`)
103
+ }
94
104
  return null
95
105
  }
96
106
  }
@@ -127,24 +137,44 @@ class CacheManager {
127
137
  */
128
138
  clear() {
129
139
  if (!this.enabled) {
130
- return
140
+ return { success: true, cleared: 0 }
131
141
  }
132
142
 
143
+ const results = { success: true, cleared: 0, errors: [] }
144
+
133
145
  try {
134
146
  if (fs.existsSync(this.cacheDir)) {
135
147
  const files = fs.readdirSync(this.cacheDir)
136
148
  files.forEach(file => {
137
149
  if (file.endsWith('.json')) {
138
- fs.unlinkSync(path.join(this.cacheDir, file))
150
+ try {
151
+ fs.unlinkSync(path.join(this.cacheDir, file))
152
+ results.cleared++
153
+ } catch (error) {
154
+ // DR11 fix: Track individual file errors
155
+ results.errors.push({ file, error: error.message })
156
+ results.success = false
157
+ }
139
158
  }
140
159
  })
141
160
  }
142
161
  } catch (error) {
143
- // Silently fail if cache clear fails
162
+ // DR11 fix: Report directory-level failures
163
+ results.success = false
164
+ results.errors.push({ directory: this.cacheDir, error: error.message })
165
+
144
166
  if (this.verbose) {
145
- console.warn(`Cache clear failed: ${error.message}`)
167
+ console.warn(`❌ Cache clear failed: ${error.message}`)
146
168
  }
147
169
  }
170
+
171
+ // DR11 fix: Warn if any failures occurred
172
+ if (!results.success && this.verbose) {
173
+ console.warn(`⚠️ Failed to clear ${results.errors.length} cache file(s)`)
174
+ console.warn(' Some cached validation results may be stale')
175
+ }
176
+
177
+ return results
148
178
  }
149
179
 
150
180
  /**
@@ -5,7 +5,7 @@ const path = require('path')
5
5
  const os = require('os')
6
6
  const crypto = require('crypto')
7
7
  const https = require('https')
8
- const { execSync } = require('child_process')
8
+ const { execSync, spawnSync } = require('child_process')
9
9
  const { showProgress } = require('../ui-helpers')
10
10
 
11
11
  // Pinned gitleaks version for reproducible security scanning
@@ -37,11 +37,19 @@ class ConfigSecurityScanner {
37
37
  // checksumMap dependency injection - FOR TESTING ONLY
38
38
  // WARNING: Do not use in production CLI - this bypasses security verification!
39
39
  if (options.checksumMap) {
40
- if (process.env.NODE_ENV === 'production') {
40
+ // Security fix: Block checksumMap override in production
41
+ // Only allow in explicit test/development mode
42
+ const isTestMode = process.env.NODE_ENV === 'test'
43
+ const isDeveloperMode = process.env.QAA_DEVELOPER === 'true'
44
+ const isProduction = process.env.NODE_ENV === 'production'
45
+
46
+ if (isProduction || (!isTestMode && !isDeveloperMode)) {
41
47
  throw new Error(
42
- 'checksumMap override not allowed in production environment'
48
+ 'checksumMap override not allowed in production. ' +
49
+ 'Only permitted when NODE_ENV=test or QAA_DEVELOPER=true'
43
50
  )
44
51
  }
52
+
45
53
  if (!options.quiet) {
46
54
  console.warn(
47
55
  '⚠️ WARNING: Using custom checksum map - FOR TESTING ONLY!'
@@ -118,23 +126,14 @@ class ConfigSecurityScanner {
118
126
  )
119
127
  }
120
128
 
121
- // Check if fallback to unpinned gitleaks is explicitly allowed
122
- if (this.options.allowLatestGitleaks) {
123
- console.warn(
124
- '🚨 WARNING: Using npx gitleaks (supply chain risk - downloads latest version)'
125
- )
126
- console.warn(
127
- '📌 Consider: brew install gitleaks (macOS) or choco install gitleaks (Windows)'
128
- )
129
- return 'npx gitleaks'
130
- }
131
-
132
- // Security-first: fail hard instead of silent fallback
129
+ // Security-first: fail hard instead of any fallback
130
+ // SECURITY: Removed npx gitleaks fallback to prevent command injection
133
131
  throw new Error(
134
132
  `Cannot resolve secure gitleaks binary. Options:\n` +
135
133
  `1. Install globally: brew install gitleaks (macOS) or choco install gitleaks (Windows)\n` +
136
134
  `2. Set GITLEAKS_PATH to your preferred binary\n` +
137
- `3. Use --allow-latest-gitleaks flag (NOT RECOMMENDED - supply chain risk)`
135
+ `Note: --allow-latest-gitleaks flag is deprecated and no longer supported.\n` +
136
+ `npx gitleaks has been intentionally removed due to supply chain security risk.`
138
137
  )
139
138
  }
140
139
  }
@@ -213,20 +212,37 @@ class ConfigSecurityScanner {
213
212
  /**
214
213
  * Download file from URL to target path
215
214
  */
216
- downloadFile(url, targetPath) {
217
- return new Promise((resolve, reject) => {
218
- const file = fs.createWriteStream(targetPath)
215
+ downloadFile(url, targetPath, redirectCount = 0) {
216
+ const MAX_REDIRECTS = 5
219
217
 
218
+ return new Promise((resolve, reject) => {
220
219
  https
221
220
  .get(url, response => {
222
- if (response.statusCode === 302 || response.statusCode === 301) {
223
- // Follow redirect
224
- return this.downloadFile(response.headers.location, targetPath)
221
+ if (
222
+ response.statusCode === 302 ||
223
+ response.statusCode === 301 ||
224
+ response.statusCode === 303 ||
225
+ response.statusCode === 307 ||
226
+ response.statusCode === 308
227
+ ) {
228
+ if (redirectCount >= MAX_REDIRECTS) {
229
+ reject(new Error('Too many redirects while downloading gitleaks'))
230
+ return
231
+ }
232
+ const nextUrl = response.headers.location
233
+ if (!nextUrl) {
234
+ reject(new Error('Redirect without location header'))
235
+ return
236
+ }
237
+ response.resume()
238
+ this.downloadFile(nextUrl, targetPath, redirectCount + 1)
225
239
  .then(resolve)
226
240
  .catch(reject)
241
+ return
227
242
  }
228
243
 
229
244
  if (response.statusCode !== 200) {
245
+ response.resume()
230
246
  reject(
231
247
  new Error(
232
248
  `HTTP ${response.statusCode}: ${response.statusMessage}`
@@ -235,6 +251,7 @@ class ConfigSecurityScanner {
235
251
  return
236
252
  }
237
253
 
254
+ const file = fs.createWriteStream(targetPath)
238
255
  response.pipe(file)
239
256
 
240
257
  file.on('finish', () => {
@@ -351,6 +368,7 @@ class ConfigSecurityScanner {
351
368
  execSync(auditCmd, {
352
369
  stdio: 'pipe',
353
370
  timeout: 60000, // 60 second timeout for audit operations
371
+ maxBuffer: 10 * 1024 * 1024,
354
372
  encoding: 'utf8',
355
373
  })
356
374
  spinner.succeed('npm audit completed - no high/critical vulnerabilities')
@@ -368,11 +386,13 @@ class ConfigSecurityScanner {
368
386
  const auditResult = JSON.parse(error.stdout.toString())
369
387
  if (auditResult.metadata && auditResult.metadata.vulnerabilities) {
370
388
  const vulns = auditResult.metadata.vulnerabilities
371
- const total = vulns.high + vulns.critical + vulns.moderate
389
+ const total = vulns.high + vulns.critical
372
390
  if (total > 0) {
373
- spinner.fail(`npm audit found ${total} vulnerabilities`)
391
+ spinner.fail(
392
+ `npm audit found ${total} high/critical vulnerabilities`
393
+ )
374
394
  this.issues.push(
375
- `${packageManager} audit: ${total} vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
395
+ `${packageManager} audit: ${total} high/critical vulnerabilities found (${vulns.high} high, ${vulns.critical} critical). Run '${packageManager} audit fix' to resolve.`
376
396
  )
377
397
  } else {
378
398
  spinner.succeed(
@@ -383,11 +403,16 @@ class ConfigSecurityScanner {
383
403
  spinner.succeed('npm audit completed')
384
404
  }
385
405
  } catch {
386
- spinner.warn(`Could not parse ${packageManager} audit output`)
387
- console.warn(`Could not parse ${packageManager} audit output`)
406
+ spinner.fail(`Could not parse ${packageManager} audit output`)
407
+ this.issues.push(
408
+ `${packageManager} audit: Unable to parse audit output. Run '${packageManager} audit --json' manually to inspect vulnerabilities.`
409
+ )
388
410
  }
389
411
  } else {
390
- spinner.succeed('npm audit completed')
412
+ spinner.fail('npm audit returned an error with no output')
413
+ this.issues.push(
414
+ `${packageManager} audit: Failed with no output. Run '${packageManager} audit --json' manually to inspect vulnerabilities.`
415
+ )
391
416
  }
392
417
  }
393
418
  }
@@ -405,16 +430,34 @@ class ConfigSecurityScanner {
405
430
 
406
431
  // Build command - handle npx vs direct binary execution
407
432
  const isNpxCommand = gitleaksBinary.startsWith('npx ')
408
- const command = isNpxCommand
409
- ? `${gitleaksBinary} detect --source . --redact`
410
- : `"${gitleaksBinary}" detect --source . --redact`
433
+ const args = ['detect', '--source', '.', '--redact']
434
+ const command = isNpxCommand ? 'npx' : gitleaksBinary
435
+ const commandArgs = isNpxCommand ? ['gitleaks', ...args] : args
411
436
 
412
437
  // Run gitleaks with --redact to prevent secret exposure and timeout for safety
413
- execSync(command, {
438
+ const result = spawnSync(command, commandArgs, {
414
439
  stdio: 'pipe',
415
440
  timeout: 30000, // 30 second timeout to prevent hangs
416
441
  encoding: 'utf8',
417
442
  })
443
+
444
+ if (result.error) {
445
+ const error = result.error
446
+ error.stdout = result.stdout
447
+ error.stderr = result.stderr
448
+ error.status = result.status
449
+ error.signal = result.signal
450
+ throw error
451
+ }
452
+
453
+ if (result.status !== 0) {
454
+ const error = new Error('gitleaks exited with non-zero status')
455
+ error.status = result.status
456
+ error.stdout = result.stdout
457
+ error.stderr = result.stderr
458
+ error.signal = result.signal
459
+ throw error
460
+ }
418
461
  spinner.succeed('gitleaks scan completed - no secrets detected')
419
462
  } catch (error) {
420
463
  if (error.signal === 'SIGTERM') {
@@ -676,14 +719,38 @@ class ConfigSecurityScanner {
676
719
  const secretPattern =
677
720
  /(?:SECRET|PASSWORD|KEY|TOKEN)\s*=\s*["']?[^"\s']+/gi
678
721
  if (secretPattern.test(envStatement)) {
722
+ const redacted = this.redactDockerEnv(envStatement)
679
723
  this.issues.push(
680
- `Dockerfile: Hardcoded secret in ENV statement: ${envStatement.trim()}`
724
+ `Dockerfile: Hardcoded secret in ENV statement: ${redacted}`
681
725
  )
682
726
  }
683
727
  }
684
728
  }
685
729
  }
686
730
 
731
+ redactDockerEnv(statement) {
732
+ const trimmed = statement.trim()
733
+ const match = trimmed.match(/^ENV\s+(.+)$/i)
734
+ if (!match) return trimmed
735
+
736
+ const body = match[1].trim()
737
+ if (body.includes('=')) {
738
+ const redactedBody = body.replace(
739
+ /=\s*(?:'[^']*'|"[^"]*"|[^ \t\r\n]+)/g,
740
+ '=[REDACTED]'
741
+ )
742
+ return `ENV ${redactedBody}`
743
+ }
744
+
745
+ const parts = body.split(/\s+/)
746
+ if (parts.length >= 2) {
747
+ parts[1] = '[REDACTED]'
748
+ return `ENV ${parts.join(' ')}`
749
+ }
750
+
751
+ return `ENV ${body}`
752
+ }
753
+
687
754
  /**
688
755
  * Check .gitignore for security-sensitive files
689
756
  */