@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.
@@ -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;