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.
- package/.github/workflows/auto-release.yml +49 -0
- package/.github/workflows/quality.yml +11 -11
- package/.github/workflows/shell-ci.yml.example +82 -0
- package/.github/workflows/shell-quality.yml.example +148 -0
- package/README.md +165 -12
- package/config/shell-ci.yml +82 -0
- package/config/shell-quality.yml +148 -0
- package/docs/ADOPTION-SUMMARY.md +41 -0
- package/docs/ARCHITECTURE-REVIEW.md +67 -0
- package/docs/ARCHITECTURE.md +29 -45
- package/docs/CI-COST-ANALYSIS.md +323 -0
- package/docs/CODE-REVIEW.md +100 -0
- package/docs/REQUIREMENTS.md +148 -0
- package/docs/SECURITY-AUDIT.md +68 -0
- package/docs/test-trace-matrix.md +28 -0
- package/eslint.config.cjs +2 -0
- package/lib/commands/analyze-ci.js +616 -0
- package/lib/commands/deps.js +293 -0
- package/lib/commands/index.js +29 -0
- package/lib/commands/validate.js +85 -0
- package/lib/config-validator.js +28 -45
- package/lib/error-reporter.js +14 -2
- package/lib/github-api.js +138 -13
- package/lib/license-signing.js +125 -0
- package/lib/license-validator.js +359 -71
- package/lib/licensing.js +434 -106
- package/lib/package-utils.js +9 -9
- package/lib/prelaunch-validator.js +828 -0
- package/lib/project-maturity.js +58 -6
- package/lib/quality-tools-generator.js +495 -0
- package/lib/result-types.js +112 -0
- package/lib/security-enhancements.js +1 -1
- package/lib/smart-strategy-generator.js +46 -10
- package/lib/telemetry.js +1 -1
- package/lib/template-loader.js +52 -19
- package/lib/ui-helpers.js +1 -1
- package/lib/validation/cache-manager.js +36 -6
- package/lib/validation/config-security.js +100 -33
- package/lib/validation/index.js +68 -97
- package/lib/validation/workflow-validation.js +28 -7
- package/package.json +4 -6
- package/scripts/check-test-coverage.sh +46 -0
- package/scripts/validate-claude-md.js +80 -0
- package/setup.js +923 -301
- 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(
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
package/lib/template-loader.js
CHANGED
|
@@ -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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
122
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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 (
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
389
|
+
const total = vulns.high + vulns.critical
|
|
372
390
|
if (total > 0) {
|
|
373
|
-
spinner.fail(
|
|
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.
|
|
387
|
-
|
|
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.
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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: ${
|
|
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
|
*/
|