@sun-asterisk/sunlint 1.3.6 → 1.3.7
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/CHANGELOG.md +38 -1
- package/config/rules/enhanced-rules-registry.json +42 -10
- package/core/analysis-orchestrator.js +9 -5
- package/core/performance-optimizer.js +8 -2
- package/package.json +1 -1
- package/rules/common/C073_validate_required_config_on_startup/README.md +110 -0
- package/rules/common/C073_validate_required_config_on_startup/analyzer.js +770 -0
- package/rules/common/C073_validate_required_config_on_startup/config.json +46 -0
- package/rules/common/C073_validate_required_config_on_startup/symbol-based-analyzer.js +370 -0
- package/rules/security/S057_utc_logging/README.md +152 -0
- package/rules/security/S057_utc_logging/analyzer.js +457 -0
- package/rules/security/S057_utc_logging/config.json +105 -0
- package/rules/security/S058_no_ssrf/README.md +180 -0
- package/rules/security/S058_no_ssrf/analyzer.js +403 -0
- package/rules/security/S058_no_ssrf/config.json +125 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
// rules/common/C073_validate_required_config_on_startup/analyzer.js
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { CommentDetector } = require('../../utils/rule-helpers');
|
|
5
|
+
|
|
6
|
+
class C073ConfigValidationAnalyzer {
|
|
7
|
+
constructor(semanticEngine = null) {
|
|
8
|
+
this.ruleId = 'C073';
|
|
9
|
+
this.ruleName = 'Validate Required Configuration on Startup';
|
|
10
|
+
this.description = 'C073 - Validate mandatory configuration at startup and fail fast on invalid/missing values';
|
|
11
|
+
this.semanticEngine = semanticEngine;
|
|
12
|
+
this.verbose = false;
|
|
13
|
+
|
|
14
|
+
// Load config from config.json
|
|
15
|
+
this.loadConfig();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadConfig() {
|
|
19
|
+
try {
|
|
20
|
+
const configPath = path.join(__dirname, 'config.json');
|
|
21
|
+
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
22
|
+
this.options = configData.options || {};
|
|
23
|
+
this.configModules = this.options.configModules || {};
|
|
24
|
+
this.envAccessors = this.options.envAccessors || {};
|
|
25
|
+
this.schemaDetectors = this.options.schemaDetectors || {};
|
|
26
|
+
this.failFastSignals = this.options.failFastSignals || {};
|
|
27
|
+
this.dangerousDefaults = this.options.dangerousDefaults || [];
|
|
28
|
+
this.thresholds = this.options.thresholds || {};
|
|
29
|
+
this.policy = this.options.policy || {};
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.warn(`[C073] Could not load config: ${error.message}`);
|
|
32
|
+
this.options = {};
|
|
33
|
+
this.configModules = {};
|
|
34
|
+
this.envAccessors = {};
|
|
35
|
+
this.schemaDetectors = {};
|
|
36
|
+
this.failFastSignals = {};
|
|
37
|
+
this.dangerousDefaults = [];
|
|
38
|
+
this.thresholds = {};
|
|
39
|
+
this.policy = {};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async initialize(semanticEngine = null) {
|
|
44
|
+
if (semanticEngine) {
|
|
45
|
+
this.semanticEngine = semanticEngine;
|
|
46
|
+
}
|
|
47
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Main analyze method required by heuristic engine
|
|
51
|
+
async analyze(files, language, options = {}) {
|
|
52
|
+
const violations = [];
|
|
53
|
+
|
|
54
|
+
if (this.verbose) {
|
|
55
|
+
console.log(`[DEBUG] 🎯 C073: Analyzing ${files.length} files for config validation`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const filePath of files) {
|
|
59
|
+
if (this.verbose) {
|
|
60
|
+
console.log(`[DEBUG] 🎯 C073: Analyzing ${filePath.split('/').pop()}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
65
|
+
const fileExtension = path.extname(filePath);
|
|
66
|
+
const fileViolations = this.analyzeFile(filePath, content, fileExtension);
|
|
67
|
+
violations.push(...fileViolations);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
console.warn(`[C073] Error analyzing ${filePath}: ${error.message}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.verbose) {
|
|
74
|
+
console.log(`[DEBUG] 🎯 C073: Found ${violations.length} violations`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return violations;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
analyzeFile(filePath, content, fileExtension) {
|
|
81
|
+
const violations = [];
|
|
82
|
+
const detectedLanguage = this.detectLanguage(fileExtension);
|
|
83
|
+
|
|
84
|
+
if (!detectedLanguage) {
|
|
85
|
+
return violations;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Use semantic engine (AST) if available, fallback to heuristic
|
|
89
|
+
if (this.semanticEngine && typeof this.semanticEngine.parseCode === 'function') {
|
|
90
|
+
return this.analyzeWithAST(filePath, content, detectedLanguage);
|
|
91
|
+
} else {
|
|
92
|
+
return this.analyzeWithHeuristic(filePath, content, detectedLanguage);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
analyzeWithAST(filePath, content, language) {
|
|
97
|
+
const violations = [];
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Parse AST using semantic engine
|
|
101
|
+
const ast = this.semanticEngine.parseCode(content, language);
|
|
102
|
+
if (!ast) {
|
|
103
|
+
// Fallback to heuristic if AST parsing fails
|
|
104
|
+
return this.analyzeWithHeuristic(filePath, content, language);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Advanced AST-based analysis
|
|
108
|
+
const analysis = this.performStaticAnalysis(ast, filePath, content, language);
|
|
109
|
+
|
|
110
|
+
// 1. Check for validate-at-startup patterns (pass signals)
|
|
111
|
+
this.checkValidateAtStartupPatterns(analysis, violations, filePath, language);
|
|
112
|
+
|
|
113
|
+
// 2. Detect scattered config reads (violations)
|
|
114
|
+
this.checkScatteredConfigReads(analysis, violations, filePath, language);
|
|
115
|
+
|
|
116
|
+
// 3. Dangerous defaults detection
|
|
117
|
+
this.checkDangerousDefaults(analysis, violations, filePath, language);
|
|
118
|
+
|
|
119
|
+
// 4. Late connection issues
|
|
120
|
+
this.checkLateConnections(analysis, violations, filePath, language);
|
|
121
|
+
|
|
122
|
+
// 5. Config propagation issues
|
|
123
|
+
this.checkConfigPropagation(analysis, violations, filePath, language);
|
|
124
|
+
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn(`[C073] AST analysis failed for ${filePath}: ${error.message}`);
|
|
127
|
+
// Fallback to heuristic
|
|
128
|
+
return this.analyzeWithHeuristic(filePath, content, language);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
analyzeWithHeuristic(filePath, content, language) {
|
|
135
|
+
const violations = [];
|
|
136
|
+
const analysis = this.analyzeConfigurationHandling(content, language, filePath);
|
|
137
|
+
const isConfigFile = this.isConfigOrStartupFile(filePath, language);
|
|
138
|
+
|
|
139
|
+
// Existing heuristic logic (as fallback)
|
|
140
|
+
if (isConfigFile && this.policy.requireSchemaOrExplicitChecks) {
|
|
141
|
+
this.checkSchemaValidation(content, language, analysis, violations, filePath);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isConfigFile && this.policy.requireFailFast) {
|
|
145
|
+
this.checkFailFastBehavior(content, language, analysis, violations, filePath);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (this.policy.forbidEnvReadsOutsideConfig) {
|
|
149
|
+
this.checkEnvAccessPattern(content, language, analysis, violations, filePath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (isConfigFile && this.policy.flagDangerousDefaults) {
|
|
153
|
+
this.checkDangerousDefaults(content, analysis, violations, filePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isConfigFile && this.policy.requireStartupConnectivityChecks) {
|
|
157
|
+
this.checkStartupConnectivityChecks(content, language, analysis, violations, filePath);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return violations;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
detectLanguage(fileExtension) {
|
|
164
|
+
const ext = fileExtension.toLowerCase();
|
|
165
|
+
if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) return 'typescript';
|
|
166
|
+
if (['.java'].includes(ext)) return 'java';
|
|
167
|
+
if (['.go'].includes(ext)) return 'go';
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
isConfigOrStartupFile(filePath, language) {
|
|
172
|
+
const configPatterns = this.configModules[language] || [];
|
|
173
|
+
return configPatterns.some(pattern => {
|
|
174
|
+
const globPattern = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*');
|
|
175
|
+
const regex = new RegExp(globPattern.replace(/\//g, '\\/'));
|
|
176
|
+
return regex.test(filePath);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
analyzeConfigurationHandling(content, language, filePath) {
|
|
181
|
+
const analysis = {
|
|
182
|
+
envAccess: [],
|
|
183
|
+
schemaValidation: [],
|
|
184
|
+
failFastMechanisms: [],
|
|
185
|
+
dangerousPatterns: [],
|
|
186
|
+
connectivityChecks: []
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// Filter out comment lines to avoid false positives
|
|
190
|
+
const lines = content.split('\n');
|
|
191
|
+
const filteredLines = CommentDetector.filterCommentLines(lines);
|
|
192
|
+
const contentWithoutComments = filteredLines
|
|
193
|
+
.filter(item => !item.isComment)
|
|
194
|
+
.map(item => item.line)
|
|
195
|
+
.join('\n');
|
|
196
|
+
|
|
197
|
+
// Find environment variable access
|
|
198
|
+
const envPatterns = this.envAccessors[language] || [];
|
|
199
|
+
envPatterns.forEach(pattern => {
|
|
200
|
+
const regex = this.createRegexFromPattern(pattern);
|
|
201
|
+
const matches = contentWithoutComments.match(regex) || [];
|
|
202
|
+
analysis.envAccess.push(...matches.map(match => ({
|
|
203
|
+
pattern,
|
|
204
|
+
match,
|
|
205
|
+
line: this.getLineNumber(content, match) // Use original content for line numbers
|
|
206
|
+
})));
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Find schema validation usage
|
|
210
|
+
const schemaPatterns = this.schemaDetectors[language] || [];
|
|
211
|
+
schemaPatterns.forEach(schema => {
|
|
212
|
+
if (contentWithoutComments.includes(schema)) {
|
|
213
|
+
analysis.schemaValidation.push({
|
|
214
|
+
schema,
|
|
215
|
+
line: this.getLineNumber(content, schema)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// Find fail-fast mechanisms
|
|
221
|
+
const failFastPatterns = this.failFastSignals[language] || [];
|
|
222
|
+
failFastPatterns.forEach(pattern => {
|
|
223
|
+
const regex = this.createRegexFromPattern(pattern);
|
|
224
|
+
const matches = contentWithoutComments.match(regex) || [];
|
|
225
|
+
analysis.failFastMechanisms.push(...matches.map(match => ({
|
|
226
|
+
pattern,
|
|
227
|
+
match,
|
|
228
|
+
line: this.getLineNumber(content, match)
|
|
229
|
+
})));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Find dangerous default patterns
|
|
233
|
+
this.dangerousDefaults.forEach(pattern => {
|
|
234
|
+
if (contentWithoutComments.includes(pattern)) {
|
|
235
|
+
analysis.dangerousPatterns.push({
|
|
236
|
+
pattern,
|
|
237
|
+
line: this.getLineNumber(content, pattern)
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
return analysis;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
checkSchemaValidation(content, language, analysis, violations, filePath) {
|
|
246
|
+
const hasEnvAccess = analysis.envAccess.length > 0;
|
|
247
|
+
const hasSchemaValidation = analysis.schemaValidation.length > 0;
|
|
248
|
+
const hasExplicitValidation = this.hasExplicitValidation(content, language);
|
|
249
|
+
|
|
250
|
+
if (hasEnvAccess && !hasSchemaValidation && !hasExplicitValidation) {
|
|
251
|
+
violations.push({
|
|
252
|
+
ruleId: 'C073',
|
|
253
|
+
severity: 'error',
|
|
254
|
+
message: 'Configuration values are accessed without schema validation or explicit checks. Use validation libraries like zod, joi, or implement explicit validation.',
|
|
255
|
+
line: analysis.envAccess[0].line,
|
|
256
|
+
column: 1,
|
|
257
|
+
filePath: filePath,
|
|
258
|
+
suggestions: this.getSchemaValidationSuggestions(language)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
checkFailFastBehavior(content, language, analysis, violations, filePath) {
|
|
264
|
+
const hasEnvAccess = analysis.envAccess.length > 0;
|
|
265
|
+
const hasFailFast = analysis.failFastMechanisms.length > 0;
|
|
266
|
+
|
|
267
|
+
if (hasEnvAccess && !hasFailFast) {
|
|
268
|
+
violations.push({
|
|
269
|
+
ruleId: 'C073',
|
|
270
|
+
severity: 'error',
|
|
271
|
+
message: 'Configuration access found but no fail-fast mechanism detected. Application should exit early if required configuration is missing.',
|
|
272
|
+
line: analysis.envAccess[0].line,
|
|
273
|
+
column: 1,
|
|
274
|
+
filePath: filePath,
|
|
275
|
+
suggestions: this.getFailFastSuggestions(language)
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
checkEnvAccessPattern(content, language, analysis, violations, filePath) {
|
|
281
|
+
if (!this.isConfigOrStartupFile(filePath, language)) {
|
|
282
|
+
const envAccessCount = analysis.envAccess.length;
|
|
283
|
+
const maxAllowed = this.thresholds.maxEnvReadsOutsideConfig || 3;
|
|
284
|
+
|
|
285
|
+
if (envAccessCount > maxAllowed) {
|
|
286
|
+
violations.push({
|
|
287
|
+
ruleId: 'C073',
|
|
288
|
+
severity: 'warning',
|
|
289
|
+
message: `Too many environment variable accesses (${envAccessCount}) outside configuration modules. Consider centralizing configuration.`,
|
|
290
|
+
line: analysis.envAccess[0].line,
|
|
291
|
+
column: 1,
|
|
292
|
+
filePath: filePath,
|
|
293
|
+
suggestions: ['Move environment variable access to dedicated configuration modules']
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
checkDangerousDefaults(content, analysis, violations, filePath) {
|
|
300
|
+
// Only check dangerous defaults in config/startup files
|
|
301
|
+
if (!this.isConfigOrStartupFile(filePath, this.detectLanguage(path.extname(filePath)))) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
analysis.dangerousPatterns.forEach(pattern => {
|
|
306
|
+
violations.push({
|
|
307
|
+
ruleId: 'C073',
|
|
308
|
+
severity: 'warning',
|
|
309
|
+
message: `Dangerous default value detected: "${pattern.pattern}". This may mask missing configuration.`,
|
|
310
|
+
line: pattern.line,
|
|
311
|
+
column: 1,
|
|
312
|
+
filePath: filePath,
|
|
313
|
+
suggestions: [
|
|
314
|
+
'Remove default value and fail fast if configuration is missing',
|
|
315
|
+
'Use explicit validation to ensure required values are present'
|
|
316
|
+
]
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
checkStartupConnectivityChecks(content, language, analysis, violations, filePath) {
|
|
322
|
+
const hasConnectionConfig = this.hasConnectionConfiguration(content, language);
|
|
323
|
+
const hasConnectivityCheck = this.hasConnectivityCheck(content, language);
|
|
324
|
+
|
|
325
|
+
// Only check this in config/startup files AND if there's connection config
|
|
326
|
+
if (this.isConfigOrStartupFile(filePath, language) && hasConnectionConfig && !hasConnectivityCheck) {
|
|
327
|
+
violations.push({
|
|
328
|
+
ruleId: 'C073',
|
|
329
|
+
severity: 'warning',
|
|
330
|
+
message: 'Database or external service configuration found but no startup connectivity check detected.',
|
|
331
|
+
line: 1,
|
|
332
|
+
column: 1,
|
|
333
|
+
filePath: filePath,
|
|
334
|
+
suggestions: [
|
|
335
|
+
'Add database connection validation at startup',
|
|
336
|
+
'Implement health checks for external dependencies',
|
|
337
|
+
'Test API endpoints during application initialization'
|
|
338
|
+
]
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
checkFailFastBehavior(content, language, analysis, violations, filePath) {
|
|
344
|
+
const hasEnvAccess = analysis.envAccess.length > 0;
|
|
345
|
+
const hasFailFast = analysis.failFastMechanisms.length > 0;
|
|
346
|
+
|
|
347
|
+
if (hasEnvAccess && !hasFailFast) {
|
|
348
|
+
violations.push({
|
|
349
|
+
ruleId: 'C073',
|
|
350
|
+
severity: 'error',
|
|
351
|
+
message: 'Configuration access found but no fail-fast mechanism detected. Application should exit early if required configuration is missing.',
|
|
352
|
+
line: analysis.envAccess[0].line,
|
|
353
|
+
column: 1,
|
|
354
|
+
suggestions: this.getFailFastSuggestions(language)
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
checkEnvAccessPattern(content, language, analysis, violations, filePath) {
|
|
360
|
+
if (!this.isConfigOrStartupFile(filePath, language)) {
|
|
361
|
+
const envAccessCount = analysis.envAccess.length;
|
|
362
|
+
const maxAllowed = this.thresholds.maxEnvReadsOutsideConfig || 3;
|
|
363
|
+
|
|
364
|
+
if (envAccessCount > maxAllowed) {
|
|
365
|
+
violations.push({
|
|
366
|
+
ruleId: 'C073',
|
|
367
|
+
severity: 'warning',
|
|
368
|
+
message: `Too many environment variable accesses (${envAccessCount}) outside configuration modules. Consider centralizing configuration.`,
|
|
369
|
+
line: analysis.envAccess[0].line,
|
|
370
|
+
column: 1,
|
|
371
|
+
suggestions: ['Move environment variable access to dedicated configuration modules']
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
checkDangerousDefaults(content, analysis, violations, filePath) {
|
|
378
|
+
// Only check dangerous defaults in config/startup files
|
|
379
|
+
if (!this.isConfigOrStartupFile(filePath, this.detectLanguage(filePath.split('.').pop()))) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
analysis.dangerousPatterns.forEach(pattern => {
|
|
384
|
+
violations.push({
|
|
385
|
+
ruleId: 'C073',
|
|
386
|
+
severity: 'warning',
|
|
387
|
+
message: `Dangerous default value detected: "${pattern.pattern}". This may mask missing configuration.`,
|
|
388
|
+
line: pattern.line,
|
|
389
|
+
column: 1,
|
|
390
|
+
suggestions: [
|
|
391
|
+
'Remove default value and fail fast if configuration is missing',
|
|
392
|
+
'Use explicit validation to ensure required values are present'
|
|
393
|
+
]
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
checkStartupConnectivityChecks(content, language, analysis, violations, filePath) {
|
|
399
|
+
const hasConnectionConfig = this.hasConnectionConfiguration(content, language);
|
|
400
|
+
const hasConnectivityCheck = this.hasConnectivityCheck(content, language);
|
|
401
|
+
|
|
402
|
+
// Only check this in config/startup files AND if there's connection config
|
|
403
|
+
if (this.isConfigOrStartupFile(filePath, language) && hasConnectionConfig && !hasConnectivityCheck) {
|
|
404
|
+
violations.push({
|
|
405
|
+
ruleId: 'C073',
|
|
406
|
+
severity: 'warning',
|
|
407
|
+
message: 'Database or external service configuration found but no startup connectivity check detected.',
|
|
408
|
+
line: 1,
|
|
409
|
+
column: 1,
|
|
410
|
+
suggestions: [
|
|
411
|
+
'Add database connection validation at startup',
|
|
412
|
+
'Implement health checks for external dependencies',
|
|
413
|
+
'Test API endpoints during application initialization'
|
|
414
|
+
]
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
hasExplicitValidation(content, language) {
|
|
420
|
+
const lines = content.split('\n');
|
|
421
|
+
const filteredLines = CommentDetector.filterCommentLines(lines);
|
|
422
|
+
const contentWithoutComments = filteredLines
|
|
423
|
+
.filter(item => !item.isComment)
|
|
424
|
+
.map(item => item.line)
|
|
425
|
+
.join('\n');
|
|
426
|
+
|
|
427
|
+
const validationPatterns = {
|
|
428
|
+
typescript: [
|
|
429
|
+
/if\s*\(\s*!.*process\.env\./,
|
|
430
|
+
/assert\s*\(/,
|
|
431
|
+
/require\s*\(/,
|
|
432
|
+
/\.required\s*\(/,
|
|
433
|
+
/throw.*Error.*config/i
|
|
434
|
+
],
|
|
435
|
+
java: [
|
|
436
|
+
/if\s*\(\s*.*isEmpty\s*\(\)/,
|
|
437
|
+
/Assert\./,
|
|
438
|
+
/Objects\.requireNonNull/,
|
|
439
|
+
/throw.*Exception.*config/i
|
|
440
|
+
],
|
|
441
|
+
go: [
|
|
442
|
+
/if.*==\s*""/,
|
|
443
|
+
/if.*==\s*nil/,
|
|
444
|
+
/log\.Fatal/,
|
|
445
|
+
/panic\(/
|
|
446
|
+
]
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const patterns = validationPatterns[language] || [];
|
|
450
|
+
return patterns.some(pattern => pattern.test(contentWithoutComments));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
hasConnectionConfiguration(content, language) {
|
|
454
|
+
const lines = content.split('\n');
|
|
455
|
+
const filteredLines = CommentDetector.filterCommentLines(lines);
|
|
456
|
+
const contentWithoutComments = filteredLines
|
|
457
|
+
.filter(item => !item.isComment)
|
|
458
|
+
.map(item => item.line)
|
|
459
|
+
.join('\n');
|
|
460
|
+
|
|
461
|
+
const connectionPatterns = [
|
|
462
|
+
/process\.env\..*DATABASE/i,
|
|
463
|
+
/process\.env\..*DB_/i,
|
|
464
|
+
/process\.env\..*REDIS/i,
|
|
465
|
+
/process\.env\..*API.*ENDPOINT/i,
|
|
466
|
+
/process\.env\..*SERVICE.*URL/i,
|
|
467
|
+
/database.*url/i,
|
|
468
|
+
/db.*host/i,
|
|
469
|
+
/redis.*url/i,
|
|
470
|
+
/api.*endpoint/i,
|
|
471
|
+
/service.*url/i,
|
|
472
|
+
/connection.*string/i
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
return connectionPatterns.some(pattern => pattern.test(contentWithoutComments));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
hasConnectivityCheck(content, language) {
|
|
479
|
+
const lines = content.split('\n');
|
|
480
|
+
const filteredLines = CommentDetector.filterCommentLines(lines);
|
|
481
|
+
const contentWithoutComments = filteredLines
|
|
482
|
+
.filter(item => !item.isComment)
|
|
483
|
+
.map(item => item.line)
|
|
484
|
+
.join('\n');
|
|
485
|
+
|
|
486
|
+
const checkPatterns = {
|
|
487
|
+
typescript: [
|
|
488
|
+
/\.connect\s*\(/,
|
|
489
|
+
/\.ping\s*\(/,
|
|
490
|
+
/healthcheck/i,
|
|
491
|
+
/\.test\s*\(/
|
|
492
|
+
],
|
|
493
|
+
java: [
|
|
494
|
+
/\.getConnection\s*\(/,
|
|
495
|
+
/\.ping\s*\(/,
|
|
496
|
+
/health.*check/i,
|
|
497
|
+
/\.testConnection/
|
|
498
|
+
],
|
|
499
|
+
go: [
|
|
500
|
+
/\.Ping\s*\(/,
|
|
501
|
+
/\.Connect\s*\(/,
|
|
502
|
+
/health.*check/i
|
|
503
|
+
]
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const patterns = checkPatterns[language] || [];
|
|
507
|
+
return patterns.some(pattern => pattern.test(contentWithoutComments));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
getSchemaValidationSuggestions(language) {
|
|
511
|
+
const suggestions = {
|
|
512
|
+
typescript: [
|
|
513
|
+
'Use zod: const config = z.object({API_KEY: z.string()}).parse(process.env)',
|
|
514
|
+
'Use joi: const {error, value} = schema.validate(process.env)',
|
|
515
|
+
'Use envalid: const env = cleanEnv(process.env, {API_KEY: str()})'
|
|
516
|
+
],
|
|
517
|
+
java: [
|
|
518
|
+
'Use @ConfigurationProperties with @Validated',
|
|
519
|
+
'Use @Value with validation annotations',
|
|
520
|
+
'Implement custom configuration validator'
|
|
521
|
+
],
|
|
522
|
+
go: [
|
|
523
|
+
'Use envconfig: err := envconfig.Process("app", &config)',
|
|
524
|
+
'Use viper with validation',
|
|
525
|
+
'Implement custom configuration validation'
|
|
526
|
+
]
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
return suggestions[language] || ['Implement configuration validation'];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
getFailFastSuggestions(language) {
|
|
533
|
+
const suggestions = {
|
|
534
|
+
typescript: [
|
|
535
|
+
'Use process.exit(1) after logging error',
|
|
536
|
+
'Throw Error in configuration validation',
|
|
537
|
+
'Use panic-like behavior for missing config'
|
|
538
|
+
],
|
|
539
|
+
java: [
|
|
540
|
+
'Use System.exit(1) or SpringApplication.exit()',
|
|
541
|
+
'Throw RuntimeException for invalid config',
|
|
542
|
+
'Use @PostConstruct validation'
|
|
543
|
+
],
|
|
544
|
+
go: [
|
|
545
|
+
'Use log.Fatal() for configuration errors',
|
|
546
|
+
'Use panic() for critical config missing',
|
|
547
|
+
'Use os.Exit(1) after logging'
|
|
548
|
+
]
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return suggestions[language] || ['Implement fail-fast behavior'];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
createRegexFromPattern(pattern) {
|
|
555
|
+
// Handle specific env access patterns
|
|
556
|
+
if (pattern === 'process.env.*') {
|
|
557
|
+
return /process\.env\.[A-Z_][A-Z0-9_]*/g;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Convert glob-like patterns to regex
|
|
561
|
+
const escaped = pattern
|
|
562
|
+
.replace(/\./g, '\\.')
|
|
563
|
+
.replace(/\*/g, '.*')
|
|
564
|
+
.replace(/\(/g, '\\(')
|
|
565
|
+
.replace(/\)/g, '\\)');
|
|
566
|
+
|
|
567
|
+
return new RegExp(escaped, 'g');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
getLineNumber(content, searchString) {
|
|
571
|
+
const lines = content.split('\n');
|
|
572
|
+
for (let i = 0; i < lines.length; i++) {
|
|
573
|
+
if (lines[i].includes(searchString)) {
|
|
574
|
+
return i + 1;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return 1;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Test method for unit testing
|
|
581
|
+
analyzeContent(content, filePath, fileExtension, options = {}) {
|
|
582
|
+
// Override options if provided
|
|
583
|
+
if (options && Object.keys(options).length > 0) {
|
|
584
|
+
this.configModules = options.configModules || this.configModules;
|
|
585
|
+
this.envAccessors = options.envAccessors || this.envAccessors;
|
|
586
|
+
this.schemaDetectors = options.schemaDetectors || this.schemaDetectors;
|
|
587
|
+
this.failFastSignals = options.failFastSignals || this.failFastSignals;
|
|
588
|
+
this.dangerousDefaults = options.dangerousDefaults || this.dangerousDefaults;
|
|
589
|
+
this.thresholds = options.thresholds || this.thresholds;
|
|
590
|
+
this.policy = options.policy || this.policy;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return this.analyzeFile(filePath, content, fileExtension);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Advanced AST-based static analysis
|
|
597
|
+
performStaticAnalysis(ast, filePath, content, language) {
|
|
598
|
+
const C073SymbolBasedAnalyzer = require('./symbol-based-analyzer');
|
|
599
|
+
const symbolAnalyzer = new C073SymbolBasedAnalyzer({
|
|
600
|
+
configModules: this.configModules,
|
|
601
|
+
envAccessors: this.envAccessors,
|
|
602
|
+
schemaDetectors: this.schemaDetectors,
|
|
603
|
+
failFastSignals: this.failFastSignals,
|
|
604
|
+
policy: this.policy
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
return symbolAnalyzer.analyze(ast, filePath, content);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// 1. Check for validate-at-startup patterns (pass signals)
|
|
611
|
+
checkValidateAtStartupPatterns(analysis, violations, filePath, language) {
|
|
612
|
+
const isConfigFile = this.isConfigOrStartupFile(filePath, language);
|
|
613
|
+
if (!isConfigFile) return;
|
|
614
|
+
|
|
615
|
+
const hasSchemaValidation = analysis.schemaValidation && analysis.schemaValidation.length > 0;
|
|
616
|
+
const hasFailFast = analysis.failFastMechanisms && analysis.failFastMechanisms.length > 0;
|
|
617
|
+
|
|
618
|
+
if (!hasSchemaValidation && this.policy.requireSchemaOrExplicitChecks) {
|
|
619
|
+
violations.push({
|
|
620
|
+
ruleId: 'C073',
|
|
621
|
+
severity: 'error',
|
|
622
|
+
message: 'No schema validation detected in configuration module. Use validation libraries or explicit checks.',
|
|
623
|
+
line: 1,
|
|
624
|
+
column: 1,
|
|
625
|
+
suggestions: this.getSchemaValidationSuggestions(language)
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!hasFailFast && this.policy.requireFailFast) {
|
|
630
|
+
violations.push({
|
|
631
|
+
ruleId: 'C073',
|
|
632
|
+
severity: 'error',
|
|
633
|
+
message: 'No fail-fast mechanism detected. Application should exit early on invalid configuration.',
|
|
634
|
+
line: 1,
|
|
635
|
+
column: 1,
|
|
636
|
+
suggestions: this.getFailFastSuggestions(language)
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 2. Detect scattered config reads (violations)
|
|
642
|
+
checkScatteredConfigReads(analysis, violations, filePath, language) {
|
|
643
|
+
const isConfigFile = this.isConfigOrStartupFile(filePath, language);
|
|
644
|
+
if (isConfigFile) return; // Skip config files
|
|
645
|
+
|
|
646
|
+
if (analysis.envAccess && analysis.envAccess.length > 0) {
|
|
647
|
+
const maxAllowed = this.thresholds.maxEnvReadsOutsideConfig || 3;
|
|
648
|
+
|
|
649
|
+
if (analysis.envAccess.length > maxAllowed) {
|
|
650
|
+
violations.push({
|
|
651
|
+
ruleId: 'C073',
|
|
652
|
+
severity: 'warning',
|
|
653
|
+
message: `Scattered environment variable access (${analysis.envAccess.length} reads) outside configuration modules. Consider centralizing configuration.`,
|
|
654
|
+
line: analysis.envAccess[0].line || 1,
|
|
655
|
+
column: 1,
|
|
656
|
+
suggestions: ['Move environment variable access to dedicated configuration modules', 'Use dependency injection for configuration']
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// 3. Enhanced dangerous defaults detection
|
|
663
|
+
checkDangerousDefaults(analysis, violations, filePath, language) {
|
|
664
|
+
if (analysis.dangerousDefaults && analysis.dangerousDefaults.length > 0) {
|
|
665
|
+
analysis.dangerousDefaults.forEach(dangerous => {
|
|
666
|
+
violations.push({
|
|
667
|
+
ruleId: 'C073',
|
|
668
|
+
severity: 'warning',
|
|
669
|
+
message: `Dangerous default value detected: "${dangerous.pattern}". This can mask configuration errors.`,
|
|
670
|
+
line: dangerous.line || 1,
|
|
671
|
+
column: 1,
|
|
672
|
+
suggestions: [
|
|
673
|
+
'Remove default values for critical configuration',
|
|
674
|
+
'Use explicit validation instead of fallback values',
|
|
675
|
+
'Fail fast when required configuration is missing'
|
|
676
|
+
]
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// 4. Check late connection issues
|
|
683
|
+
checkLateConnections(analysis, violations, filePath, language) {
|
|
684
|
+
const isConfigFile = this.isConfigOrStartupFile(filePath, language);
|
|
685
|
+
if (!isConfigFile) return;
|
|
686
|
+
|
|
687
|
+
// Check if there are database/API configurations but no startup connectivity checks
|
|
688
|
+
const hasExternalConfig = analysis.envAccess && analysis.envAccess.some(env =>
|
|
689
|
+
/DATABASE|DB_|API_|REDIS|MONGO|POSTGRES|MYSQL/i.test(env.variable || env.match)
|
|
690
|
+
);
|
|
691
|
+
|
|
692
|
+
const hasConnectivityCheck = analysis.connectivityChecks && analysis.connectivityChecks.length > 0;
|
|
693
|
+
|
|
694
|
+
if (hasExternalConfig && !hasConnectivityCheck && this.policy.requireStartupConnectivityChecks) {
|
|
695
|
+
violations.push({
|
|
696
|
+
ruleId: 'C073',
|
|
697
|
+
severity: 'warning',
|
|
698
|
+
message: 'External service configuration found but no startup connectivity check detected.',
|
|
699
|
+
line: 1,
|
|
700
|
+
column: 1,
|
|
701
|
+
suggestions: [
|
|
702
|
+
'Add database connection validation at startup',
|
|
703
|
+
'Implement health checks for external dependencies',
|
|
704
|
+
'Test API endpoints during application initialization'
|
|
705
|
+
]
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// 5. Check config propagation issues
|
|
711
|
+
checkConfigPropagation(analysis, violations, filePath, language) {
|
|
712
|
+
// This would require cross-file analysis - for now, use heuristics
|
|
713
|
+
const isServiceFile = /service|controller|repository|handler/i.test(filePath);
|
|
714
|
+
|
|
715
|
+
if (isServiceFile && analysis.envAccess && analysis.envAccess.length > 0) {
|
|
716
|
+
violations.push({
|
|
717
|
+
ruleId: 'C073',
|
|
718
|
+
severity: 'info',
|
|
719
|
+
message: 'Service layer accessing environment variables directly. Consider using dependency injection for configuration.',
|
|
720
|
+
line: analysis.envAccess[0].line || 1,
|
|
721
|
+
column: 1,
|
|
722
|
+
suggestions: [
|
|
723
|
+
'Inject configuration object instead of reading environment variables',
|
|
724
|
+
'Use configuration service or dependency injection container'
|
|
725
|
+
]
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
getSchemaValidationSuggestions(language) {
|
|
731
|
+
const suggestions = {
|
|
732
|
+
typescript: [
|
|
733
|
+
'Use zod: const config = z.object({API_KEY: z.string()}).parse(process.env)',
|
|
734
|
+
'Use joi: const {error, value} = schema.validate(process.env)',
|
|
735
|
+
'Use envalid: const env = cleanEnv(process.env, {API_KEY: str()})'
|
|
736
|
+
],
|
|
737
|
+
java: [
|
|
738
|
+
'Use @ConfigurationProperties with @Validated',
|
|
739
|
+
'Use @Value with validation annotations',
|
|
740
|
+
'Use Jakarta Bean Validation'
|
|
741
|
+
],
|
|
742
|
+
go: [
|
|
743
|
+
'Use envconfig with struct validation',
|
|
744
|
+
'Use viper with validation hooks'
|
|
745
|
+
]
|
|
746
|
+
};
|
|
747
|
+
return suggestions[language] || ['Implement configuration validation'];
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
getFailFastSuggestions(language) {
|
|
751
|
+
const suggestions = {
|
|
752
|
+
typescript: [
|
|
753
|
+
'Use process.exit(1) after logging error',
|
|
754
|
+
'Throw Error in configuration validation',
|
|
755
|
+
'Use panic-like behavior for missing config'
|
|
756
|
+
],
|
|
757
|
+
java: [
|
|
758
|
+
'Use @PostConstruct validation with RuntimeException',
|
|
759
|
+
'Use SpringApplication.exit() for critical config errors'
|
|
760
|
+
],
|
|
761
|
+
go: [
|
|
762
|
+
'Use log.Fatal() for missing configuration',
|
|
763
|
+
'Use panic() for critical startup errors'
|
|
764
|
+
]
|
|
765
|
+
};
|
|
766
|
+
return suggestions[language] || ['Implement fail-fast behavior'];
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
module.exports = C073ConfigValidationAnalyzer;
|