@sun-asterisk/sunlint 1.3.4 → 1.3.5

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 (28) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/config/presets/all.json +49 -48
  3. package/config/presets/beginner.json +7 -18
  4. package/config/presets/ci.json +63 -27
  5. package/config/presets/maintainability.json +6 -4
  6. package/config/presets/performance.json +4 -3
  7. package/config/presets/quality.json +11 -50
  8. package/config/presets/recommended.json +83 -10
  9. package/config/presets/security.json +20 -19
  10. package/config/presets/strict.json +6 -13
  11. package/config/rules/enhanced-rules-registry.json +64 -7
  12. package/core/config-preset-resolver.js +7 -2
  13. package/package.json +1 -1
  14. package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
  15. package/rules/common/C067_no_hardcoded_config/config.json +81 -0
  16. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1000 -0
  17. package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
  18. package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
  19. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
  20. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
  21. package/rules/security/S025_server_side_validation/README.md +179 -0
  22. package/rules/security/S025_server_side_validation/analyzer.js +242 -0
  23. package/rules/security/S025_server_side_validation/config.json +111 -0
  24. package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
  25. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
  26. package/scripts/README.md +83 -0
  27. package/scripts/analyze-core-rules.js +151 -0
  28. package/scripts/generate-presets.js +202 -0
@@ -0,0 +1,1000 @@
1
+ // rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js
2
+ const { SyntaxKind, Project, Node } = require('ts-morph');
3
+
4
+ class C067SymbolBasedAnalyzer {
5
+ constructor(semanticEngine = null) {
6
+ this.semanticEngine = semanticEngine;
7
+ this.verbose = false;
8
+
9
+ // Common UI/framework strings that should be excluded
10
+ this.UI_STRINGS = [
11
+ 'checkbox', 'button', 'search', 'remove', 'submit', 'cancel', 'ok', 'close',
12
+ 'Authorization', 'User-Agent', 'Content-Type', 'Accept', 'Bearer',
13
+ 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight', 'bottom', 'top', 'left', 'right',
14
+ 'next-auth/react', '@nestjs/swagger', '@nestjs/common', 'nestjs-pino'
15
+ ];
16
+
17
+ // Test-related strings to exclude
18
+ this.TEST_PATTERNS = [
19
+ /^(test|mock|example|dummy|placeholder|fixture|stub)/i,
20
+ /^(User \d+|Test User|Admin User)/i,
21
+ /^(group\d+|item\d+|element\d+)/i,
22
+ /^(abcdef\d+|123456|test-\w+)/i
23
+ ];
24
+
25
+ // Configuration patterns to detect - focused on environment-dependent config
26
+ this.configPatterns = {
27
+ // API endpoints and URLs - only external URLs, not internal endpoints
28
+ urls: {
29
+ regex: /^https?:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)([a-zA-Z0-9-]+\.[a-zA-Z]{2,}|[^\/\s]+\.[^\/\s]+)(\/[^\s]*)?$/,
30
+ exclude: [
31
+ /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0)(:\d+)?/, // Local development
32
+ /^https?:\/\/(example\.com|test\.com|dummy\.com)/, // Test domains
33
+ /^(http|https):\/\/\$\{.+\}/ // Template URLs with variables
34
+ ]
35
+ },
36
+
37
+ // Environment-dependent numeric values (ports, timeouts that differ by env)
38
+ environmentNumbers: {
39
+ // Only consider numbers that are commonly different between environments
40
+ isEnvironmentDependent: (value, context) => {
41
+ const lowerContext = context.toLowerCase();
42
+
43
+ // Business logic numbers are NOT environment config
44
+ const businessLogicPatterns = [
45
+ /limit|max|min|size|count|length|threshold/i,
46
+ /page|record|item|batch|chunk|export/i,
47
+ /width|height|margin|padding/i,
48
+ /attempt|retry|step/i
49
+ ];
50
+
51
+ if (businessLogicPatterns.some(pattern => pattern.test(context))) {
52
+ return false;
53
+ }
54
+
55
+ // Very specific values that are usually business constants
56
+ const businessConstants = [
57
+ 20000, 10000, 5000, 1000, 500, 100, 50, 20, 10, 5, // Common limits
58
+ 404, 500, 200, 201, 400, 401, 403, // HTTP status codes
59
+ 24, 60, 3600, 86400, // Time constants (hours, minutes, seconds)
60
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 // Simple counters
61
+ ];
62
+
63
+ if (businessConstants.includes(value)) {
64
+ return false;
65
+ }
66
+
67
+ // Port numbers (except common ones like 80, 443, 3000, 8080)
68
+ if (typeof value === 'number' && value > 1000 && value < 65536) {
69
+ const commonPorts = [3000, 8000, 8080, 9000, 5000, 4200, 4000];
70
+ if (!commonPorts.includes(value)) {
71
+ // Check if context suggests it's a port
72
+ return /port|listen|bind|server/i.test(context);
73
+ }
74
+ }
75
+
76
+ // Large timeout values that might differ by environment (> 10 seconds)
77
+ if (typeof value === 'number' && value > 10000) {
78
+ return /timeout|interval|delay|duration/i.test(context) &&
79
+ !businessLogicPatterns.some(pattern => pattern.test(context));
80
+ }
81
+
82
+ return false;
83
+ }
84
+ },
85
+
86
+ // Database and connection strings
87
+ connections: {
88
+ regex: /^(mongodb|mysql|postgres|redis|elasticsearch):\/\/|^jdbc:|^Server=|^Data Source=/i
89
+ },
90
+
91
+ // API Keys and tokens (but exclude validation messages)
92
+ credentials: {
93
+ keywords: ['apikey', 'api_key', 'secret_key', 'access_token', 'client_secret'],
94
+ exclude: [
95
+ /must contain|should contain|invalid|error|message/i, // Validation messages
96
+ /description|comment|note/i, // Descriptions
97
+ /^[a-z\s]{10,}$/i // Long descriptive text
98
+ ]
99
+ }
100
+ };
101
+ }
102
+
103
+ async initialize(semanticEngine = null) {
104
+ if (semanticEngine) {
105
+ this.semanticEngine = semanticEngine;
106
+ }
107
+ this.verbose = semanticEngine?.verbose || false;
108
+ }
109
+
110
+ async analyzeFileBasic(filePath, options = {}) {
111
+ const violations = [];
112
+
113
+ try {
114
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
115
+ if (!sourceFile) {
116
+ if (this.verbose) {
117
+ console.log(`[DEBUG] 🔍 C067: File not in semantic project, trying standalone: ${filePath.split('/').pop()}`);
118
+ }
119
+ // Fallback to standalone analysis if file not in semantic project
120
+ return await this.analyzeFileStandalone(filePath, options);
121
+ }
122
+
123
+ if (this.verbose) {
124
+ console.log(`[DEBUG] 🔍 C067: Analyzing hardcoded config in ${filePath.split('/').pop()}`);
125
+ }
126
+
127
+ // Skip test files and config files themselves
128
+ if (this.isConfigOrTestFile(filePath)) {
129
+ if (this.verbose) {
130
+ console.log(`[DEBUG] 🔍 C067: Skipping config/test file: ${filePath.split('/').pop()}`);
131
+ }
132
+ return violations;
133
+ }
134
+
135
+ // Find hardcoded configuration values
136
+ const hardcodedConfigs = this.findHardcodedConfigs(sourceFile);
137
+
138
+ for (const config of hardcodedConfigs) {
139
+ violations.push({
140
+ ruleId: 'C067',
141
+ message: this.createMessage(config),
142
+ filePath: filePath,
143
+ line: config.line,
144
+ column: config.column,
145
+ severity: 'warning',
146
+ category: 'configuration',
147
+ type: config.type,
148
+ value: config.value,
149
+ suggestion: this.getSuggestion(config.type)
150
+ });
151
+ }
152
+
153
+ if (this.verbose) {
154
+ console.log(`[DEBUG] 🔍 C067: Found ${violations.length} hardcoded config violations`);
155
+ }
156
+
157
+ return violations;
158
+ } catch (error) {
159
+ if (this.verbose) {
160
+ console.error(`[DEBUG] ❌ C067: Symbol analysis error: ${error.message}`);
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+
166
+ async analyzeFileStandalone(filePath, options = {}) {
167
+ const violations = [];
168
+
169
+ try {
170
+ // Create a standalone ts-morph project for this analysis
171
+ const project = new Project({
172
+ compilerOptions: {
173
+ target: 'ES2020',
174
+ module: 'CommonJS',
175
+ allowJs: true,
176
+ allowSyntheticDefaultImports: true,
177
+ esModuleInterop: true,
178
+ skipLibCheck: true,
179
+ strict: false
180
+ },
181
+ useInMemoryFileSystem: true
182
+ });
183
+
184
+ // Add the source file to the project
185
+ const fs = require('fs');
186
+ const path = require('path');
187
+
188
+ // Check if file exists first
189
+ if (!fs.existsSync(filePath)) {
190
+ throw new Error(`File not found on filesystem: ${filePath}`);
191
+ }
192
+
193
+ // Read file content and create source file
194
+ const fileContent = fs.readFileSync(filePath, 'utf8');
195
+ const fileName = path.basename(filePath);
196
+ const sourceFile = project.createSourceFile(fileName, fileContent);
197
+
198
+ if (!sourceFile) {
199
+ throw new Error(`Source file not found: ${filePath}`);
200
+ }
201
+
202
+ if (this.verbose) {
203
+ console.log(`[DEBUG] 🔍 C067: Analyzing hardcoded config in ${filePath.split('/').pop()} (standalone)`);
204
+ }
205
+
206
+ // Skip test files and config files themselves
207
+ if (this.isConfigOrTestFile(filePath)) {
208
+ if (this.verbose) {
209
+ console.log(`[DEBUG] 🔍 C067: Skipping config/test file: ${filePath.split('/').pop()}`);
210
+ }
211
+ return violations;
212
+ }
213
+
214
+ // Find hardcoded configuration values
215
+ const hardcodedConfigs = this.findHardcodedConfigs(sourceFile);
216
+
217
+ for (const config of hardcodedConfigs) {
218
+ violations.push({
219
+ ruleId: 'C067',
220
+ message: this.createMessage(config),
221
+ filePath: filePath,
222
+ line: config.line,
223
+ column: config.column,
224
+ severity: 'warning',
225
+ category: 'configuration',
226
+ type: config.type,
227
+ value: config.value,
228
+ suggestion: this.getSuggestion(config.type)
229
+ });
230
+ }
231
+
232
+ if (this.verbose) {
233
+ console.log(`[DEBUG] 🔍 C067: Found ${violations.length} hardcoded config violations (standalone)`);
234
+ }
235
+
236
+ // Clean up the project
237
+ project.removeSourceFile(sourceFile);
238
+
239
+ return violations;
240
+ } catch (error) {
241
+ if (this.verbose) {
242
+ console.error(`[DEBUG] ❌ C067: Standalone analysis error: ${error.message}`);
243
+ }
244
+ throw error;
245
+ }
246
+ }
247
+
248
+ isConfigOrTestFile(filePath) {
249
+ // Skip config files themselves and test files (NOT dummy files used in production)
250
+ const fileName = filePath.toLowerCase();
251
+ const configPatterns = [
252
+ /config\.(ts|js|json)$/,
253
+ /\.config\.(ts|js)$/,
254
+ /\.env$/,
255
+ /\.env\./,
256
+ /constants\.(ts|js)$/,
257
+ /settings\.(ts|js)$/,
258
+ /defaults\.(ts|js)$/
259
+ ];
260
+
261
+ const testPatterns = [
262
+ /\.(test|spec)\.(ts|tsx|js|jsx)$/,
263
+ /\/__tests__\//,
264
+ /\/test\//,
265
+ /\/tests\//,
266
+ /\.stories\.(ts|tsx|js|jsx)$/,
267
+ /\.mock\.(ts|tsx|js|jsx)$/
268
+ // NOTE: Deliberately NOT including /dummy/ because dummy files
269
+ // in production code often contain hardcoded config that should be flagged
270
+ ];
271
+
272
+ return configPatterns.some(pattern => pattern.test(fileName)) ||
273
+ testPatterns.some(pattern => pattern.test(fileName));
274
+ }
275
+
276
+ findHardcodedConfigs(sourceFile) {
277
+ const configs = [];
278
+
279
+ // Traverse all nodes in the source file
280
+ sourceFile.forEachDescendant((node) => {
281
+ // Check string literals
282
+ if (node.getKind() === SyntaxKind.StringLiteral) {
283
+ const config = this.analyzeStringLiteral(node, sourceFile);
284
+ if (config) {
285
+ configs.push(config);
286
+ }
287
+ }
288
+
289
+ // Check numeric literals
290
+ if (node.getKind() === SyntaxKind.NumericLiteral) {
291
+ const config = this.analyzeNumericLiteral(node, sourceFile);
292
+ if (config) {
293
+ configs.push(config);
294
+ }
295
+ }
296
+
297
+ // Check template literals (for URLs with variables)
298
+ if (node.getKind() === SyntaxKind.TemplateExpression) {
299
+ const config = this.analyzeTemplateLiteral(node, sourceFile);
300
+ if (config) {
301
+ configs.push(config);
302
+ }
303
+ }
304
+
305
+ // Check property assignments
306
+ if (node.getKind() === SyntaxKind.PropertyAssignment) {
307
+ const config = this.analyzePropertyAssignment(node, sourceFile);
308
+ if (config) {
309
+ configs.push(config);
310
+ }
311
+ }
312
+
313
+ // Check variable declarations
314
+ if (node.getKind() === SyntaxKind.VariableDeclaration) {
315
+ const config = this.analyzeVariableDeclaration(node, sourceFile);
316
+ if (config) {
317
+ configs.push(config);
318
+ }
319
+ }
320
+ });
321
+
322
+ return configs;
323
+ }
324
+
325
+ analyzeStringLiteral(node, sourceFile) {
326
+ const value = node.getLiteralValue();
327
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
328
+
329
+ // Skip short strings and common UI values
330
+ if (value.length < 4) return null;
331
+
332
+ const parentContext = this.getParentContext(node);
333
+
334
+ // Skip import paths and module names
335
+ if (this.isImportPath(value, node)) return null;
336
+
337
+ // Skip UI strings and labels
338
+ if (this.isUIString(value)) return null;
339
+
340
+ // Skip test data and mocks
341
+ if (this.isTestData(value, parentContext)) return null;
342
+
343
+ // Skip validation messages and error messages
344
+ if (this.isValidationMessage(value, parentContext)) return null;
345
+
346
+ // Skip file names and descriptions
347
+ if (this.isFileNameOrDescription(value, parentContext)) return null;
348
+
349
+ // Skip config keys (like 'api.baseUrl', 'features.newUI', etc.)
350
+ if (this.looksLikeConfigKey(value)) {
351
+ return null;
352
+ }
353
+
354
+ // Skip if this is used in a config service call
355
+ if (parentContext.includes('config.get') || parentContext.includes('config.getString') ||
356
+ parentContext.includes('config.getBoolean') || parentContext.includes('config.getNumber')) {
357
+ return null;
358
+ }
359
+
360
+ // Skip if this is a property key in an object literal
361
+ if (this.isPropertyKey(node)) {
362
+ return null;
363
+ }
364
+
365
+ // Check for environment-dependent URLs only
366
+ if (this.configPatterns.urls.regex.test(value)) {
367
+ if (!this.isExcludedUrl(value, node) && this.isEnvironmentDependentUrl(value)) {
368
+ return {
369
+ type: 'url',
370
+ value: value,
371
+ line: position.line,
372
+ column: position.column,
373
+ node: node
374
+ };
375
+ }
376
+ }
377
+
378
+ // Check for real credentials (not validation messages)
379
+ if (this.isRealCredential(value, parentContext)) {
380
+ return {
381
+ type: 'credential',
382
+ value: value,
383
+ line: position.line,
384
+ column: position.column,
385
+ node: node,
386
+ context: parentContext
387
+ };
388
+ }
389
+
390
+ // Check for connection strings
391
+ if (this.configPatterns.connections.regex.test(value)) {
392
+ return {
393
+ type: 'connection',
394
+ value: value,
395
+ line: position.line,
396
+ column: position.column,
397
+ node: node
398
+ };
399
+ }
400
+
401
+ return null;
402
+ }
403
+
404
+ analyzeNumericLiteral(node, sourceFile) {
405
+ const value = node.getLiteralValue();
406
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
407
+ const parentContext = this.getParentContext(node);
408
+
409
+ // Only check for environment-dependent numbers
410
+ if (this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
411
+ return {
412
+ type: 'environment_config',
413
+ value: value,
414
+ line: position.line,
415
+ column: position.column,
416
+ node: node,
417
+ context: parentContext
418
+ };
419
+ }
420
+
421
+ return null;
422
+ }
423
+
424
+ analyzeTemplateLiteral(node, sourceFile) {
425
+ // For now, focus on simple template literals that might contain URLs
426
+ const templateText = node.getFullText();
427
+ if (templateText.includes('http://') || templateText.includes('https://')) {
428
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
429
+
430
+ // Check if it's using environment variables or config
431
+ if (!templateText.includes('process.env') && !templateText.includes('config.')) {
432
+ return {
433
+ type: 'template_url',
434
+ value: templateText.trim(),
435
+ line: position.line,
436
+ column: position.column,
437
+ node: node
438
+ };
439
+ }
440
+ }
441
+
442
+ return null;
443
+ }
444
+
445
+ analyzePropertyAssignment(node, sourceFile) {
446
+ const nameNode = node.getNameNode();
447
+ const valueNode = node.getInitializer();
448
+
449
+ if (!nameNode || !valueNode) return null;
450
+
451
+ const propertyName = nameNode.getText();
452
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
453
+
454
+ // Skip field mapping objects
455
+ const ancestorObj = node.getParent();
456
+ if (ancestorObj && Node.isObjectLiteralExpression(ancestorObj)) {
457
+ const objParent = ancestorObj.getParent();
458
+ if (objParent && Node.isVariableDeclaration(objParent)) {
459
+ const varName = objParent.getName();
460
+ if (/mapping|map|field|column|decode/i.test(varName)) {
461
+ return null; // Skip field mapping objects
462
+ }
463
+ }
464
+ }
465
+
466
+ // Skip common business logic properties that aren't environment-dependent
467
+ const businessLogicProperties = [
468
+ 'endpoint', 'path', 'route', // API routing
469
+ 'limit', 'pageSize', 'batchSize', // Pagination (usually business logic)
470
+ 'retry', 'retries', 'maxRetries', // Retry logic (usually business logic)
471
+ 'count', 'max', 'min' // Common limits
472
+ ];
473
+
474
+ const lowerPropertyName = propertyName.toLowerCase();
475
+ if (businessLogicProperties.some(prop => lowerPropertyName.includes(prop))) {
476
+ // Only flag if it's clearly environment-dependent
477
+ let value = null;
478
+ if (valueNode.getKind() === SyntaxKind.StringLiteral) {
479
+ value = valueNode.getLiteralValue();
480
+ // Only flag URLs or clearly environment-dependent strings
481
+ if (!this.configPatterns.urls.regex.test(value) || !this.isEnvironmentDependentUrl(value)) {
482
+ return null;
483
+ }
484
+ } else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
485
+ value = valueNode.getLiteralValue();
486
+ const parentContext = this.getParentContext(node);
487
+ // Only flag if it's clearly environment-dependent (like ports, large timeouts)
488
+ if (!this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
489
+ return null;
490
+ }
491
+ }
492
+ }
493
+
494
+ // Check if property name suggests environment-dependent configuration
495
+ if (this.isEnvironmentDependentProperty(propertyName)) {
496
+ let value = null;
497
+
498
+ if (valueNode.getKind() === SyntaxKind.StringLiteral) {
499
+ value = valueNode.getLiteralValue();
500
+ } else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
501
+ value = valueNode.getLiteralValue();
502
+ }
503
+
504
+ if (value !== null && this.looksLikeEnvironmentConfig(propertyName, value)) {
505
+ return {
506
+ type: 'property_config',
507
+ value: value,
508
+ line: position.line,
509
+ column: position.column,
510
+ node: node,
511
+ propertyName: propertyName
512
+ };
513
+ }
514
+ }
515
+
516
+ return null;
517
+ }
518
+
519
+ analyzeVariableDeclaration(node, sourceFile) {
520
+ const nameNode = node.getNameNode();
521
+ const initializer = node.getInitializer();
522
+
523
+ if (!nameNode || !initializer) return null;
524
+
525
+ const variableName = nameNode.getText();
526
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
527
+
528
+ // Check if variable name suggests environment-dependent configuration
529
+ if (this.isEnvironmentDependentProperty(variableName)) {
530
+ let value = null;
531
+
532
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
533
+ value = initializer.getLiteralValue();
534
+ } else if (initializer.getKind() === SyntaxKind.NumericLiteral) {
535
+ value = initializer.getLiteralValue();
536
+ }
537
+
538
+ if (value !== null && this.looksLikeEnvironmentConfig(variableName, value)) {
539
+ return {
540
+ type: 'variable_config',
541
+ value: value,
542
+ line: position.line,
543
+ column: position.column,
544
+ node: node,
545
+ variableName: variableName
546
+ };
547
+ }
548
+ }
549
+
550
+ return null;
551
+ }
552
+
553
+ getParentContext(node) {
554
+ // Get surrounding context to understand the purpose of the literal
555
+ let parent = node.getParent();
556
+ let context = '';
557
+
558
+ // Check if this is a method call argument or property access
559
+ while (parent && context.length < 100) {
560
+ const parentText = parent.getText();
561
+
562
+ // If parent is CallExpression and this node is an argument, it might be a config key
563
+ if (parent.getKind() === SyntaxKind.CallExpression) {
564
+ const callExpr = parent;
565
+ const methodName = this.getMethodName(callExpr);
566
+ if (['get', 'getBoolean', 'getNumber', 'getArray', 'getString'].includes(methodName)) {
567
+ return `config.${methodName}()`; // This indicates it's a config key
568
+ }
569
+ }
570
+
571
+ if (parentText.length < 200) {
572
+ context = parentText;
573
+ break;
574
+ }
575
+ parent = parent.getParent();
576
+ }
577
+
578
+ return context;
579
+ }
580
+
581
+ getMethodName(callExpression) {
582
+ const expression = callExpression.getExpression();
583
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
584
+ return expression.getName();
585
+ }
586
+ if (expression.getKind() === SyntaxKind.Identifier) {
587
+ return expression.getText();
588
+ }
589
+ return '';
590
+ }
591
+
592
+ isExcludedUrl(value, node) {
593
+ return this.configPatterns.urls.exclude.some(pattern => pattern.test(value));
594
+ }
595
+
596
+ isExcludedCredential(value, node) {
597
+ return this.configPatterns.credentials.exclude.some(pattern => pattern.test(value));
598
+ }
599
+
600
+ containsCredentialKeyword(context) {
601
+ const lowerContext = context.toLowerCase();
602
+
603
+ // Skip if this looks like a header name or property key definition
604
+ if (context.includes("':") || context.includes('": ') || context.includes(' = ')) {
605
+ // This might be a key-value pair where the string is the key
606
+ return false;
607
+ }
608
+
609
+ return this.configPatterns.credentials.keywords.some(keyword =>
610
+ lowerContext.includes(keyword)
611
+ );
612
+ }
613
+
614
+ looksLikeUIValue(value, context) {
615
+ // Check if it's likely a UI-related value (like input type, label, etc.)
616
+ const uiKeywords = ['input', 'type', 'field', 'label', 'placeholder', 'text', 'button'];
617
+ const lowerContext = context.toLowerCase();
618
+ return uiKeywords.some(keyword => lowerContext.includes(keyword));
619
+ }
620
+
621
+ looksLikeConfigKey(value) {
622
+ // Check if it looks like a config key path (e.g., 'api.baseUrl', 'features.newUI')
623
+ if (/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/.test(value)) {
624
+ return true;
625
+ }
626
+
627
+ // Check for other config key patterns
628
+ const configKeyPatterns = [
629
+ /^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z]/, // dotted notation like 'api.url'
630
+ /^[A-Z_][A-Z0-9_]*$/, // CONSTANT_CASE like 'API_URL'
631
+ /^get[A-Z]/, // getter methods like 'getApiUrl'
632
+ /^config\./, // config namespace
633
+ /^settings\./, // settings namespace
634
+ /^env\./ // env namespace
635
+ ];
636
+
637
+ return configKeyPatterns.some(pattern => pattern.test(value));
638
+ }
639
+
640
+ isPropertyKey(node) {
641
+ // Check if this string literal is used as a property key in an object literal
642
+ const parent = node.getParent();
643
+
644
+ // If parent is PropertyAssignment and this node is the name, it's a property key
645
+ if (parent && parent.getKind() === SyntaxKind.PropertyAssignment) {
646
+ const nameNode = parent.getNameNode();
647
+ return nameNode === node;
648
+ }
649
+
650
+ return false;
651
+ }
652
+
653
+ isImportPath(value, node) {
654
+ // Check if this is likely an import path or module name
655
+ const parent = node.getParent();
656
+
657
+ // Check if it's in an import statement
658
+ let currentNode = parent;
659
+ while (currentNode) {
660
+ const kind = currentNode.getKind();
661
+ if (kind === SyntaxKind.ImportDeclaration ||
662
+ kind === SyntaxKind.ExportDeclaration ||
663
+ kind === SyntaxKind.CallExpression) {
664
+ const text = currentNode.getText();
665
+ if (text.includes('require(') || text.includes('import ') || text.includes('from ')) {
666
+ return true;
667
+ }
668
+ }
669
+ currentNode = currentNode.getParent();
670
+ }
671
+
672
+ // Check for common import path patterns
673
+ return /^[@a-z][a-z0-9\-_]*\/|^[a-z][a-z0-9\-_]*$|^\.{1,2}\//.test(value) ||
674
+ value.endsWith('.js') || value.endsWith('.ts') ||
675
+ value.endsWith('.json') || value.endsWith('.css') ||
676
+ value.endsWith('.scss') || value.endsWith('.html');
677
+ }
678
+
679
+ isUIString(value) {
680
+ // Check against predefined UI string patterns, but don't skip credentials
681
+ if (typeof value === 'string' && value.length > 20 &&
682
+ (/token|key|secret|bearer|auth/i.test(value) || /^[a-f0-9-]{30,}$/i.test(value))) {
683
+ // Don't skip potential credentials/tokens even if they contain UI keywords
684
+ return false;
685
+ }
686
+
687
+ return this.UI_STRINGS.some(pattern => {
688
+ if (typeof pattern === 'string') {
689
+ return value === pattern; // Exact match only, not includes
690
+ } else {
691
+ return pattern.test(value);
692
+ }
693
+ });
694
+ }
695
+
696
+ isTestData(value, context) {
697
+ // Don't skip credentials/tokens even in dummy files
698
+ if (typeof value === 'string' && value.length > 15 &&
699
+ (/token|key|secret|auth|bearer|jwt/i.test(value) ||
700
+ /^[a-f0-9-]{20,}$/i.test(value) || // Hex tokens
701
+ /^[A-Za-z0-9_-]{20,}$/i.test(value))) { // Base64-like tokens
702
+ return false; // Don't skip potential credentials
703
+ }
704
+
705
+ // Check for test patterns in value - but be more restrictive
706
+ if (this.TEST_PATTERNS.some(pattern => pattern.test(value))) {
707
+ // Only skip if it's clearly test data, not production dummy data
708
+ const isInTestFile = /\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(context) ||
709
+ /\/__tests__\//i.test(context) ||
710
+ /\/test\//i.test(context);
711
+ return isInTestFile;
712
+ }
713
+
714
+ // Check for test context
715
+ const lowerContext = context.toLowerCase();
716
+ const testKeywords = ['test', 'spec', 'mock', 'fixture', 'stub', 'describe', 'it('];
717
+ return testKeywords.some(keyword => lowerContext.includes(keyword));
718
+ }
719
+
720
+ isValidationMessage(value, context) {
721
+ // Skip validation/error messages
722
+ const validationPatterns = [
723
+ /must contain|should contain|invalid|error|required|missing/i,
724
+ /password|username|email/i, // Common validation contexts
725
+ /^[A-Z][a-z\s]{10,}$/, // Sentence-like messages
726
+ /\s(at least|one|letter|uppercase|lowercase|numeric)/i
727
+ ];
728
+
729
+ return validationPatterns.some(pattern => pattern.test(value)) ||
730
+ /message|error|validation|description/i.test(context);
731
+ }
732
+
733
+ isFileNameOrDescription(value, context) {
734
+ // Skip file names and descriptions
735
+ const filePatterns = [
736
+ /\.(csv|json|xml|txt|md)$/i,
737
+ /^[a-z_\-]+\.(csv|json|xml|txt)$/i,
738
+ /description|comment|note|foreign key|identity/i
739
+ ];
740
+
741
+ return filePatterns.some(pattern => pattern.test(value)) ||
742
+ /description|comment|note|identity|foreign|table/i.test(context);
743
+ }
744
+
745
+ isEnvironmentDependentUrl(value) {
746
+ // Only flag URLs that are likely to differ between environments
747
+ const envDependentPatterns = [
748
+ /\.amazonaws\.com/, // AWS services
749
+ /\.azure\.com/, // Azure services
750
+ /\.googleapis\.com/, // Google services
751
+ /api\./, // API endpoints
752
+ /\.dev|\.staging|\.prod/i // Environment-specific domains
753
+ ];
754
+
755
+ return envDependentPatterns.some(pattern => pattern.test(value));
756
+ }
757
+
758
+ isRealCredential(value, context) {
759
+ // Check for real credentials, not validation messages
760
+ const credentialKeywords = this.configPatterns.credentials.keywords;
761
+ const lowerContext = context.toLowerCase();
762
+
763
+ // Must have credential keyword in context
764
+ if (!credentialKeywords.some(keyword => lowerContext.includes(keyword))) {
765
+ return false;
766
+ }
767
+
768
+ // Skip if it's excluded (validation messages, etc.)
769
+ if (this.configPatterns.credentials.exclude.some(pattern => pattern.test(value))) {
770
+ return false;
771
+ }
772
+
773
+ // Skip validation messages and descriptions
774
+ if (this.isValidationMessage(value, context)) {
775
+ return false;
776
+ }
777
+
778
+ // Must be reasonably long and not look like UI text
779
+ return value.length >= 6 && !this.looksLikeUIValue(value, context);
780
+ }
781
+
782
+ isEnvironmentDependentProperty(propertyName) {
783
+ // Skip UI/framework related property names
784
+ const uiPropertyPatterns = [
785
+ /^key[A-Z]/, // keyXxx (UI field keys)
786
+ /^field[A-Z]/, // fieldXxx
787
+ /^prop[A-Z]/, // propXxx
788
+ /^data[A-Z]/, // dataXxx
789
+ /CheckDisplay/, // UI display control keys
790
+ /InputPossible/, // UI input control keys
791
+ /Flag$/, // UI flags
792
+ /Class$/, // CSS classes
793
+ /^(disabled|readonly|active)Class$/i // UI state classes
794
+ ];
795
+
796
+ if (uiPropertyPatterns.some(pattern => pattern.test(propertyName))) {
797
+ return false;
798
+ }
799
+
800
+ // Properties that are likely to differ between environments
801
+ const envDependentProps = [
802
+ 'baseurl', 'baseURL', 'host', 'hostname', 'server',
803
+ 'apikey', 'api_key', 'secret', 'token', 'password', 'credential',
804
+ 'database', 'db', 'connection', 'connectionstring',
805
+ 'timeout', // Only long timeouts
806
+ 'port', // Only non-standard ports
807
+ 'authorization', 'auth', 'authentication', // Auth headers and codes
808
+ 'apptoken', 'devicetoken', 'accesstoken', 'refreshtoken', // App tokens
809
+ 'code', 'hash', 'signature', 'key', // Various security values
810
+ 'clientsecret', 'clientid', 'sessionkey', // OAuth and session
811
+ 'requestid', 'sessionid', 'transactionid', 'otp' // Request/session tracking
812
+ ];
813
+
814
+ const lowerName = propertyName.toLowerCase();
815
+ return envDependentProps.some(prop => lowerName.includes(prop));
816
+ }
817
+
818
+ looksLikeEnvironmentConfig(propertyName, value) {
819
+ // Check if this property/value combination looks like environment config
820
+ const lowerPropertyName = propertyName.toLowerCase();
821
+
822
+ if (typeof value === 'string') {
823
+ // Skip test data (common test passwords, etc.)
824
+ const testDataPatterns = [
825
+ /^(password123|test123|admin123|user123|wrongpassword|testpassword)$/i,
826
+ /^(test|mock|dummy|sample|example)/i,
827
+ /^\/(api|mock|test)/, // Test API paths
828
+ /^[a-z]+\d+$/i // Simple test values like 'user1', 'test2'
829
+ ];
830
+
831
+ // Don't skip common test patterns if they appear in credential contexts
832
+ const isCredentialContext = /token|key|secret|auth|otp|code|password|credential/i.test(propertyName);
833
+
834
+ if (!isCredentialContext && testDataPatterns.some(pattern => pattern.test(value))) {
835
+ return false;
836
+ }
837
+
838
+ // Skip object property paths and field names
839
+ const propertyPathPatterns = [
840
+ /^[a-zA-Z][a-zA-Z0-9]*(\[[0-9]+\])?\.[a-zA-Z][a-zA-Z0-9]*$/, // obj[0].prop, obj.prop
841
+ /^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/, // obj.prop.subprop
842
+ /^[a-zA-Z][a-zA-Z0-9]*(\[[0-9]+\])+$/, // obj[0], obj[0][1]
843
+ /^(key|field|prop|data)[A-Z]/, // keyXxx, fieldXxx, propXxx, dataXxx
844
+ /CheckDisplay|InputPossible|Flag$/i, // Common UI field patterns
845
+ /^exflg|^flg|Support$/i, // Business logic flags
846
+ ];
847
+
848
+ if (propertyPathPatterns.some(pattern => pattern.test(value))) {
849
+ return false;
850
+ }
851
+
852
+ // Skip CSS classes and UI constants
853
+ const uiPatterns = [
854
+ /^bg-|text-|cursor-|border-|flex-|grid-/, // CSS classes
855
+ /^(disabled|readonly|active|inactive)$/i, // UI states
856
+ /class$/i // className values
857
+ ];
858
+
859
+ if (uiPatterns.some(pattern => pattern.test(value))) {
860
+ return false;
861
+ }
862
+
863
+ // Skip internal system identifiers (queue names, service names, route names)
864
+ const systemIdentifierPatterns = [
865
+ /-queue$/i, // Queue names
866
+ /-task$/i, // Task names
867
+ /-activity$/i, // Activity names
868
+ /-service$/i, // Service names
869
+ /-worker$/i, // Worker names
870
+ /^[A-Z_]+_QUEUE$/, // CONSTANT_QUEUE names
871
+ /^[A-Z_]+_TASK$/, // CONSTANT_TASK names
872
+ /^(register|login|logout|reset-password|verify|update)$/i, // Route names
873
+ /password|token/i && /invalid|expired|attempts|exceeded/i // Error messages
874
+ ];
875
+
876
+ if (systemIdentifierPatterns.some(pattern => pattern.test(value))) {
877
+ return false;
878
+ }
879
+
880
+ // Skip error messages and validation messages
881
+ const messagePatterns = [
882
+ /invalid|expired|exceeded|failed|error|success/i,
883
+ /attempts|required|missing|not found/i,
884
+ /^[A-Z][a-z\s]{10,}$/, // Sentence-like messages
885
+ /は|が|を|に|で|と/, // Japanese particles (UI text)
886
+ /情報|画面|ボタン|入力/ // Japanese UI terms
887
+ ];
888
+
889
+ if (messagePatterns.some(pattern => pattern.test(value))) {
890
+ return false;
891
+ }
892
+
893
+ // URLs are environment-dependent
894
+ if (this.configPatterns.urls.regex.test(value)) {
895
+ return this.isEnvironmentDependentUrl(value);
896
+ }
897
+
898
+ // Credentials - but exclude test data
899
+ if (lowerPropertyName.includes('key') || lowerPropertyName.includes('secret') ||
900
+ lowerPropertyName.includes('token') || lowerPropertyName.includes('password')) {
901
+ return value.length > 10; // Real secrets are usually longer
902
+ }
903
+
904
+ // Skip short endpoint names or simple strings
905
+ if (value.length < 10 && !value.includes('.') && !value.includes('/')) {
906
+ return false;
907
+ }
908
+ }
909
+
910
+ if (typeof value === 'number') {
911
+ // Only flag environment-dependent numbers
912
+ return this.configPatterns.environmentNumbers.isEnvironmentDependent(value, propertyName);
913
+ }
914
+
915
+ return true;
916
+ }
917
+
918
+ isCommonConstant(value) {
919
+ // Common constants that are usually OK to hardcode
920
+ const commonConstants = [100, 200, 300, 400, 500, 1000, 2000, 3000, 5000, 8080, 3000];
921
+ return commonConstants.includes(value);
922
+ }
923
+
924
+ isConfigProperty(propertyName) {
925
+ const configProps = [
926
+ 'url', 'endpoint', 'baseurl', 'apiurl', 'host', 'port',
927
+ 'timeout', 'interval', 'delay', 'retry', 'retries',
928
+ 'username', 'password', 'apikey', 'secret', 'token',
929
+ 'database', 'connection', 'connectionstring',
930
+ 'maxsize', 'batchsize', 'pagesize', 'limit'
931
+ ];
932
+
933
+ const lowerName = propertyName.toLowerCase();
934
+ return configProps.some(prop => lowerName.includes(prop));
935
+ }
936
+
937
+ isConfigVariable(variableName) {
938
+ const configVars = [
939
+ 'api', 'url', 'endpoint', 'host', 'port',
940
+ 'timeout', 'interval', 'delay', 'retry',
941
+ 'config', 'setting', 'constant'
942
+ ];
943
+
944
+ const lowerName = variableName.toLowerCase();
945
+ return configVars.some(var_ => lowerName.includes(var_));
946
+ }
947
+
948
+ looksLikeHardcodedConfig(name, value) {
949
+ // Skip obvious constants and UI values
950
+ if (typeof value === 'string') {
951
+ if (value.length < 3) return false;
952
+ if (/^(ok|yes|no|true|false|success|error|info|warn)$/i.test(value)) return false;
953
+ }
954
+
955
+ if (typeof value === 'number') {
956
+ if (this.isCommonConstant(value)) return false;
957
+ }
958
+
959
+ return true;
960
+ }
961
+
962
+ createMessage(config) {
963
+ const baseMessage = 'Environment-dependent configuration should not be hardcoded.';
964
+
965
+ switch (config.type) {
966
+ case 'url':
967
+ return `${baseMessage} External URL '${config.value}' should be loaded from environment variables or configuration files.`;
968
+ case 'credential':
969
+ return `${baseMessage} Credential value '${config.value}' should be loaded from secure environment variables.`;
970
+ case 'environment_config':
971
+ return `${baseMessage} Environment-dependent value ${config.value} should be configurable via environment variables or config files.`;
972
+ case 'connection':
973
+ return `${baseMessage} Connection string should be loaded from environment variables.`;
974
+ case 'property_config':
975
+ return `${baseMessage} Property '${config.propertyName}' may contain environment-dependent value '${config.value}'.`;
976
+ case 'variable_config':
977
+ return `${baseMessage} Variable '${config.variableName}' may contain environment-dependent value '${config.value}'.`;
978
+ case 'config_key':
979
+ return `${baseMessage} Configuration key '${config.value}' should not be hardcoded.`;
980
+ default:
981
+ return `${baseMessage} Value '${config.value}' may differ between environments.`;
982
+ }
983
+ }
984
+
985
+ getSuggestion(type) {
986
+ const suggestions = {
987
+ 'url': 'Use process.env.API_URL or config.get("api.url")',
988
+ 'credential': 'Use process.env.SECRET_KEY or secure vault',
989
+ 'environment_config': 'Move to environment variables or config service',
990
+ 'connection': 'Use process.env.DATABASE_URL',
991
+ 'property_config': 'Consider if this differs between dev/staging/production',
992
+ 'variable_config': 'Use environment variables if this differs between environments',
993
+ 'config_key': 'Use constants or enums for configuration keys'
994
+ };
995
+
996
+ return suggestions[type] || 'Consider if this value should differ between dev/staging/production environments';
997
+ }
998
+ }
999
+
1000
+ module.exports = C067SymbolBasedAnalyzer;