@sun-asterisk/sunlint 1.3.4 → 1.3.6

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 (32) hide show
  1. package/CHANGELOG.md +62 -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/rule-analysis-strategies.js +5 -0
  12. package/config/rules/enhanced-rules-registry.json +87 -7
  13. package/core/config-preset-resolver.js +7 -2
  14. package/package.json +1 -1
  15. package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
  16. package/rules/common/C067_no_hardcoded_config/config.json +81 -0
  17. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
  18. package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
  19. package/rules/common/C070_no_real_time_tests/config.json +78 -0
  20. package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
  21. package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
  22. package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
  23. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
  24. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
  25. package/rules/security/S025_server_side_validation/README.md +179 -0
  26. package/rules/security/S025_server_side_validation/analyzer.js +242 -0
  27. package/rules/security/S025_server_side_validation/config.json +111 -0
  28. package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
  29. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
  30. package/scripts/README.md +83 -0
  31. package/scripts/analyze-core-rules.js +151 -0
  32. package/scripts/generate-presets.js +202 -0
@@ -0,0 +1,1034 @@
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, including dummy/test data files
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
+ /\/dummy\//, // Skip dummy data files
269
+ /dummy\.(ts|js)$/, // Skip dummy files
270
+ /test-fixtures\//, // Skip test fixture files
271
+ /\.fixture\.(ts|js)$/, // Skip fixture files
272
+ /entity\.(ts|js)$/ // Skip entity/ORM files (contain DB constraints)
273
+ ];
274
+
275
+ return configPatterns.some(pattern => pattern.test(fileName)) ||
276
+ testPatterns.some(pattern => pattern.test(fileName));
277
+ }
278
+
279
+ findHardcodedConfigs(sourceFile) {
280
+ const configs = [];
281
+
282
+ // Traverse all nodes in the source file
283
+ sourceFile.forEachDescendant((node) => {
284
+ // Check string literals
285
+ if (node.getKind() === SyntaxKind.StringLiteral) {
286
+ const config = this.analyzeStringLiteral(node, sourceFile);
287
+ if (config) {
288
+ configs.push(config);
289
+ }
290
+ }
291
+
292
+ // Check numeric literals
293
+ if (node.getKind() === SyntaxKind.NumericLiteral) {
294
+ const config = this.analyzeNumericLiteral(node, sourceFile);
295
+ if (config) {
296
+ configs.push(config);
297
+ }
298
+ }
299
+
300
+ // Check template literals (for URLs with variables)
301
+ if (node.getKind() === SyntaxKind.TemplateExpression) {
302
+ const config = this.analyzeTemplateLiteral(node, sourceFile);
303
+ if (config) {
304
+ configs.push(config);
305
+ }
306
+ }
307
+
308
+ // Check property assignments
309
+ if (node.getKind() === SyntaxKind.PropertyAssignment) {
310
+ const config = this.analyzePropertyAssignment(node, sourceFile);
311
+ if (config) {
312
+ configs.push(config);
313
+ }
314
+ }
315
+
316
+ // Check variable declarations
317
+ if (node.getKind() === SyntaxKind.VariableDeclaration) {
318
+ const config = this.analyzeVariableDeclaration(node, sourceFile);
319
+ if (config) {
320
+ configs.push(config);
321
+ }
322
+ }
323
+ });
324
+
325
+ return configs;
326
+ }
327
+
328
+ analyzeStringLiteral(node, sourceFile) {
329
+ const value = node.getLiteralValue();
330
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
331
+
332
+ // Skip short strings and common UI values
333
+ if (value.length < 4) return null;
334
+
335
+ const parentContext = this.getParentContext(node);
336
+
337
+ // Skip import paths and module names
338
+ if (this.isImportPath(value, node)) return null;
339
+
340
+ // Skip UI strings and labels
341
+ if (this.isUIString(value)) return null;
342
+
343
+ // Skip test data and mocks
344
+ if (this.isTestData(value, parentContext)) return null;
345
+
346
+ // Skip validation messages and error messages
347
+ if (this.isValidationMessage(value, parentContext)) return null;
348
+
349
+ // Skip file names and descriptions
350
+ if (this.isFileNameOrDescription(value, parentContext)) return null;
351
+
352
+ // Skip config keys (like 'api.baseUrl', 'features.newUI', etc.)
353
+ if (this.looksLikeConfigKey(value)) {
354
+ return null;
355
+ }
356
+
357
+ // Skip if this is used in a config service call
358
+ if (parentContext.includes('config.get') || parentContext.includes('config.getString') ||
359
+ parentContext.includes('config.getBoolean') || parentContext.includes('config.getNumber')) {
360
+ return null;
361
+ }
362
+
363
+ // Skip if this is a property key in an object literal
364
+ if (this.isPropertyKey(node)) {
365
+ return null;
366
+ }
367
+
368
+ // Check for environment-dependent URLs only
369
+ if (this.configPatterns.urls.regex.test(value)) {
370
+ if (!this.isExcludedUrl(value, node) && this.isEnvironmentDependentUrl(value)) {
371
+ return {
372
+ type: 'url',
373
+ value: value,
374
+ line: position.line,
375
+ column: position.column,
376
+ node: node
377
+ };
378
+ }
379
+ }
380
+
381
+ // Check for real credentials (not validation messages)
382
+ if (this.isRealCredential(value, parentContext)) {
383
+ return {
384
+ type: 'credential',
385
+ value: value,
386
+ line: position.line,
387
+ column: position.column,
388
+ node: node,
389
+ context: parentContext
390
+ };
391
+ }
392
+
393
+ // Check for connection strings
394
+ if (this.configPatterns.connections.regex.test(value)) {
395
+ return {
396
+ type: 'connection',
397
+ value: value,
398
+ line: position.line,
399
+ column: position.column,
400
+ node: node
401
+ };
402
+ }
403
+
404
+ return null;
405
+ }
406
+
407
+ analyzeNumericLiteral(node, sourceFile) {
408
+ const value = node.getLiteralValue();
409
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
410
+ const parentContext = this.getParentContext(node);
411
+
412
+ // Only check for environment-dependent numbers
413
+ if (this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
414
+ return {
415
+ type: 'environment_config',
416
+ value: value,
417
+ line: position.line,
418
+ column: position.column,
419
+ node: node,
420
+ context: parentContext
421
+ };
422
+ }
423
+
424
+ return null;
425
+ }
426
+
427
+ analyzeTemplateLiteral(node, sourceFile) {
428
+ // For now, focus on simple template literals that might contain URLs
429
+ const templateText = node.getFullText();
430
+ if (templateText.includes('http://') || templateText.includes('https://')) {
431
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
432
+
433
+ // Check if it's using environment variables or config
434
+ if (!templateText.includes('process.env') && !templateText.includes('config.')) {
435
+ return {
436
+ type: 'template_url',
437
+ value: templateText.trim(),
438
+ line: position.line,
439
+ column: position.column,
440
+ node: node
441
+ };
442
+ }
443
+ }
444
+
445
+ return null;
446
+ }
447
+
448
+ analyzePropertyAssignment(node, sourceFile) {
449
+ const nameNode = node.getNameNode();
450
+ const valueNode = node.getInitializer();
451
+
452
+ if (!nameNode || !valueNode) return null;
453
+
454
+ const propertyName = nameNode.getText();
455
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
456
+
457
+ // Skip ALL field mapping objects and ORM/database entity configurations
458
+ const ancestorObj = node.getParent();
459
+ if (ancestorObj && Node.isObjectLiteralExpression(ancestorObj)) {
460
+ const objParent = ancestorObj.getParent();
461
+ if (objParent && Node.isVariableDeclaration(objParent)) {
462
+ const varName = objParent.getName();
463
+ // Skip field mappings, database schemas, etc.
464
+ if (/mapping|map|field|column|decode|schema|entity|constraint|table/i.test(varName)) {
465
+ return null;
466
+ }
467
+ }
468
+
469
+ // Check if this looks like a table column definition or field mapping
470
+ const objText = ancestorObj.getText();
471
+ if (/primaryKeyConstraintName|foreignKeyConstraintName|key.*may contain/i.test(objText)) {
472
+ return null; // Skip database constraint definitions
473
+ }
474
+ }
475
+
476
+ // Skip properties that are clearly field mappings or business data
477
+ const businessLogicProperties = [
478
+ // Field mappings
479
+ 'key', 'field', 'dataKey', 'valueKey', 'labelKey', 'sortKey',
480
+ // Business logic
481
+ 'endpoint', 'path', 'route', 'method',
482
+ 'limit', 'pageSize', 'batchSize', 'maxResults',
483
+ 'retry', 'retries', 'maxRetries', 'attempts',
484
+ 'count', 'max', 'min', 'size', 'length',
485
+ // UI properties
486
+ 'className', 'style', 'disabled', 'readonly',
487
+ // Database/ORM
488
+ 'primaryKeyConstraintName', 'foreignKeyConstraintName', 'constraintName',
489
+ 'tableName', 'columnName', 'schemaName'
490
+ ];
491
+
492
+ const lowerPropertyName = propertyName.toLowerCase();
493
+ if (businessLogicProperties.some(prop => lowerPropertyName.includes(prop))) {
494
+ return null; // Skip these completely
495
+ }
496
+
497
+ // Only check for CLEARLY environment-dependent properties
498
+ const trulyEnvironmentDependentProps = [
499
+ 'baseurl', 'baseURL', 'host', 'hostname', 'server', 'endpoint',
500
+ 'apikey', 'api_key', 'secret_key', 'client_secret',
501
+ 'database', 'connectionstring', 'dbhost', 'dbport',
502
+ 'port', 'timeout', // Only when they have suspicious values
503
+ 'bucket', 'region', // Cloud-specific
504
+ 'clientid', 'tenantid' // OAuth-specific
505
+ ];
506
+
507
+ if (!trulyEnvironmentDependentProps.some(prop => lowerPropertyName.includes(prop))) {
508
+ return null; // Not clearly environment-dependent
509
+ }
510
+
511
+ let value = null;
512
+ let configType = null;
513
+
514
+ if (valueNode.getKind() === SyntaxKind.StringLiteral) {
515
+ value = valueNode.getLiteralValue();
516
+
517
+ // Only flag URLs or clearly sensitive values
518
+ if (this.configPatterns.urls.regex.test(value) && this.isEnvironmentDependentUrl(value)) {
519
+ configType = 'url';
520
+ } else if (this.isRealCredential(value, propertyName)) {
521
+ configType = 'credential';
522
+ } else {
523
+ return null; // Skip other string values
524
+ }
525
+ } else if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
526
+ value = valueNode.getLiteralValue();
527
+ const parentContext = this.getParentContext(node);
528
+
529
+ // Only flag numbers that are clearly environment-dependent
530
+ if (this.configPatterns.environmentNumbers.isEnvironmentDependent(value, parentContext)) {
531
+ configType = 'environment_config';
532
+ } else {
533
+ return null;
534
+ }
535
+ } else {
536
+ return null; // Skip other value types
537
+ }
538
+
539
+ if (configType) {
540
+ return {
541
+ type: configType,
542
+ value: value,
543
+ line: position.line,
544
+ column: position.column,
545
+ node: node,
546
+ propertyName: propertyName
547
+ };
548
+ }
549
+
550
+ return null;
551
+ }
552
+
553
+ analyzeVariableDeclaration(node, sourceFile) {
554
+ const nameNode = node.getNameNode();
555
+ const initializer = node.getInitializer();
556
+
557
+ if (!nameNode || !initializer) return null;
558
+
559
+ const variableName = nameNode.getText();
560
+ const position = sourceFile.getLineAndColumnAtPos(node.getStart());
561
+
562
+ // Check if variable name suggests environment-dependent configuration
563
+ if (this.isEnvironmentDependentProperty(variableName)) {
564
+ let value = null;
565
+
566
+ if (initializer.getKind() === SyntaxKind.StringLiteral) {
567
+ value = initializer.getLiteralValue();
568
+ } else if (initializer.getKind() === SyntaxKind.NumericLiteral) {
569
+ value = initializer.getLiteralValue();
570
+ }
571
+
572
+ if (value !== null && this.looksLikeEnvironmentConfig(variableName, value)) {
573
+ return {
574
+ type: 'variable_config',
575
+ value: value,
576
+ line: position.line,
577
+ column: position.column,
578
+ node: node,
579
+ variableName: variableName
580
+ };
581
+ }
582
+ }
583
+
584
+ return null;
585
+ }
586
+
587
+ getParentContext(node) {
588
+ // Get surrounding context to understand the purpose of the literal
589
+ let parent = node.getParent();
590
+ let context = '';
591
+
592
+ // Check if this is a method call argument or property access
593
+ while (parent && context.length < 100) {
594
+ const parentText = parent.getText();
595
+
596
+ // If parent is CallExpression and this node is an argument, it might be a config key
597
+ if (parent.getKind() === SyntaxKind.CallExpression) {
598
+ const callExpr = parent;
599
+ const methodName = this.getMethodName(callExpr);
600
+ if (['get', 'getBoolean', 'getNumber', 'getArray', 'getString'].includes(methodName)) {
601
+ return `config.${methodName}()`; // This indicates it's a config key
602
+ }
603
+ }
604
+
605
+ if (parentText.length < 200) {
606
+ context = parentText;
607
+ break;
608
+ }
609
+ parent = parent.getParent();
610
+ }
611
+
612
+ return context;
613
+ }
614
+
615
+ getMethodName(callExpression) {
616
+ const expression = callExpression.getExpression();
617
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
618
+ return expression.getName();
619
+ }
620
+ if (expression.getKind() === SyntaxKind.Identifier) {
621
+ return expression.getText();
622
+ }
623
+ return '';
624
+ }
625
+
626
+ isExcludedUrl(value, node) {
627
+ return this.configPatterns.urls.exclude.some(pattern => pattern.test(value));
628
+ }
629
+
630
+ isExcludedCredential(value, node) {
631
+ return this.configPatterns.credentials.exclude.some(pattern => pattern.test(value));
632
+ }
633
+
634
+ containsCredentialKeyword(context) {
635
+ const lowerContext = context.toLowerCase();
636
+
637
+ // Skip if this looks like a header name or property key definition
638
+ if (context.includes("':") || context.includes('": ') || context.includes(' = ')) {
639
+ // This might be a key-value pair where the string is the key
640
+ return false;
641
+ }
642
+
643
+ return this.configPatterns.credentials.keywords.some(keyword =>
644
+ lowerContext.includes(keyword)
645
+ );
646
+ }
647
+
648
+ looksLikeUIValue(value, context) {
649
+ // Check if it's likely a UI-related value (like input type, label, etc.)
650
+ const uiKeywords = ['input', 'type', 'field', 'label', 'placeholder', 'text', 'button'];
651
+ const lowerContext = context.toLowerCase();
652
+ return uiKeywords.some(keyword => lowerContext.includes(keyword));
653
+ }
654
+
655
+ looksLikeConfigKey(value) {
656
+ // Check if it looks like a config key path (e.g., 'api.baseUrl', 'features.newUI')
657
+ if (/^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/.test(value)) {
658
+ return true;
659
+ }
660
+
661
+ // Check for other config key patterns
662
+ const configKeyPatterns = [
663
+ /^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z]/, // dotted notation like 'api.url'
664
+ /^[A-Z_][A-Z0-9_]*$/, // CONSTANT_CASE like 'API_URL'
665
+ /^get[A-Z]/, // getter methods like 'getApiUrl'
666
+ /^config\./, // config namespace
667
+ /^settings\./, // settings namespace
668
+ /^env\./ // env namespace
669
+ ];
670
+
671
+ return configKeyPatterns.some(pattern => pattern.test(value));
672
+ }
673
+
674
+ isPropertyKey(node) {
675
+ // Check if this string literal is used as a property key in an object literal
676
+ const parent = node.getParent();
677
+
678
+ // If parent is PropertyAssignment and this node is the name, it's a property key
679
+ if (parent && parent.getKind() === SyntaxKind.PropertyAssignment) {
680
+ const nameNode = parent.getNameNode();
681
+ return nameNode === node;
682
+ }
683
+
684
+ return false;
685
+ }
686
+
687
+ isImportPath(value, node) {
688
+ // Check if this is likely an import path or module name
689
+ const parent = node.getParent();
690
+
691
+ // Check if it's in an import statement
692
+ let currentNode = parent;
693
+ while (currentNode) {
694
+ const kind = currentNode.getKind();
695
+ if (kind === SyntaxKind.ImportDeclaration ||
696
+ kind === SyntaxKind.ExportDeclaration ||
697
+ kind === SyntaxKind.CallExpression) {
698
+ const text = currentNode.getText();
699
+ if (text.includes('require(') || text.includes('import ') || text.includes('from ')) {
700
+ return true;
701
+ }
702
+ }
703
+ currentNode = currentNode.getParent();
704
+ }
705
+
706
+ // Check for common import path patterns
707
+ return /^[@a-z][a-z0-9\-_]*\/|^[a-z][a-z0-9\-_]*$|^\.{1,2}\//.test(value) ||
708
+ value.endsWith('.js') || value.endsWith('.ts') ||
709
+ value.endsWith('.json') || value.endsWith('.css') ||
710
+ value.endsWith('.scss') || value.endsWith('.html');
711
+ }
712
+
713
+ isUIString(value) {
714
+ // Check against predefined UI string patterns, but don't skip credentials
715
+ if (typeof value === 'string' && value.length > 20 &&
716
+ (/token|key|secret|bearer|auth/i.test(value) || /^[a-f0-9-]{30,}$/i.test(value))) {
717
+ // Don't skip potential credentials/tokens even if they contain UI keywords
718
+ return false;
719
+ }
720
+
721
+ return this.UI_STRINGS.some(pattern => {
722
+ if (typeof pattern === 'string') {
723
+ return value === pattern; // Exact match only, not includes
724
+ } else {
725
+ return pattern.test(value);
726
+ }
727
+ });
728
+ }
729
+
730
+ isTestData(value, context) {
731
+ // Don't skip credentials/tokens even in dummy files
732
+ if (typeof value === 'string' && value.length > 15 &&
733
+ (/token|key|secret|auth|bearer|jwt/i.test(value) ||
734
+ /^[a-f0-9-]{20,}$/i.test(value) || // Hex tokens
735
+ /^[A-Za-z0-9_-]{20,}$/i.test(value))) { // Base64-like tokens
736
+ return false; // Don't skip potential credentials
737
+ }
738
+
739
+ // Check for test patterns in value - but be more restrictive
740
+ if (this.TEST_PATTERNS.some(pattern => pattern.test(value))) {
741
+ // Only skip if it's clearly test data, not production dummy data
742
+ const isInTestFile = /\.(test|spec)\.(ts|tsx|js|jsx)$/i.test(context) ||
743
+ /\/__tests__\//i.test(context) ||
744
+ /\/test\//i.test(context);
745
+ return isInTestFile;
746
+ }
747
+
748
+ // Check for test context
749
+ const lowerContext = context.toLowerCase();
750
+ const testKeywords = ['test', 'spec', 'mock', 'fixture', 'stub', 'describe', 'it('];
751
+ return testKeywords.some(keyword => lowerContext.includes(keyword));
752
+ }
753
+
754
+ isValidationMessage(value, context) {
755
+ // Skip validation/error messages
756
+ const validationPatterns = [
757
+ /must contain|should contain|invalid|error|required|missing/i,
758
+ /password|username|email/i, // Common validation contexts
759
+ /^[A-Z][a-z\s]{10,}$/, // Sentence-like messages
760
+ /\s(at least|one|letter|uppercase|lowercase|numeric)/i
761
+ ];
762
+
763
+ return validationPatterns.some(pattern => pattern.test(value)) ||
764
+ /message|error|validation|description/i.test(context);
765
+ }
766
+
767
+ isFileNameOrDescription(value, context) {
768
+ // Skip file names and descriptions
769
+ const filePatterns = [
770
+ /\.(csv|json|xml|txt|md)$/i,
771
+ /^[a-z_\-]+\.(csv|json|xml|txt)$/i,
772
+ /description|comment|note|foreign key|identity/i
773
+ ];
774
+
775
+ return filePatterns.some(pattern => pattern.test(value)) ||
776
+ /description|comment|note|identity|foreign|table/i.test(context);
777
+ }
778
+
779
+ isEnvironmentDependentUrl(value) {
780
+ // Only flag URLs that are likely to differ between environments
781
+ const envDependentPatterns = [
782
+ /\.amazonaws\.com/, // AWS services
783
+ /\.azure\.com/, // Azure services
784
+ /\.googleapis\.com/, // Google services
785
+ /api\./, // API endpoints
786
+ /\.dev|\.staging|\.prod/i // Environment-specific domains
787
+ ];
788
+
789
+ return envDependentPatterns.some(pattern => pattern.test(value));
790
+ }
791
+
792
+ isRealCredential(value, context) {
793
+ // Check for real credentials, not validation messages
794
+ const credentialKeywords = this.configPatterns.credentials.keywords;
795
+ const lowerContext = context.toLowerCase();
796
+
797
+ // Must have credential keyword in context
798
+ if (!credentialKeywords.some(keyword => lowerContext.includes(keyword))) {
799
+ return false;
800
+ }
801
+
802
+ // Skip if it's excluded (validation messages, etc.)
803
+ if (this.configPatterns.credentials.exclude.some(pattern => pattern.test(value))) {
804
+ return false;
805
+ }
806
+
807
+ // Skip validation messages and descriptions
808
+ if (this.isValidationMessage(value, context)) {
809
+ return false;
810
+ }
811
+
812
+ // Must be reasonably long and not look like UI text
813
+ return value.length >= 6 && !this.looksLikeUIValue(value, context);
814
+ }
815
+
816
+ isEnvironmentDependentProperty(propertyName) {
817
+ // Skip UI/framework related property names
818
+ const uiPropertyPatterns = [
819
+ /^key[A-Z]/, // keyXxx (UI field keys)
820
+ /^field[A-Z]/, // fieldXxx
821
+ /^prop[A-Z]/, // propXxx
822
+ /^data[A-Z]/, // dataXxx
823
+ /CheckDisplay/, // UI display control keys
824
+ /InputPossible/, // UI input control keys
825
+ /Flag$/, // UI flags
826
+ /Class$/, // CSS classes
827
+ /^(disabled|readonly|active)Class$/i // UI state classes
828
+ ];
829
+
830
+ if (uiPropertyPatterns.some(pattern => pattern.test(propertyName))) {
831
+ return false;
832
+ }
833
+
834
+ // Properties that are likely to differ between environments
835
+ const envDependentProps = [
836
+ 'baseurl', 'baseURL', 'host', 'hostname', 'server',
837
+ 'apikey', 'api_key', 'secret', 'token', 'password', 'credential',
838
+ 'database', 'db', 'connection', 'connectionstring',
839
+ 'timeout', // Only long timeouts
840
+ 'port', // Only non-standard ports
841
+ 'authorization', 'auth', 'authentication', // Auth headers and codes
842
+ 'apptoken', 'devicetoken', 'accesstoken', 'refreshtoken', // App tokens
843
+ 'code', 'hash', 'signature', 'key', // Various security values
844
+ 'clientsecret', 'clientid', 'sessionkey', // OAuth and session
845
+ 'requestid', 'sessionid', 'transactionid', 'otp' // Request/session tracking
846
+ ];
847
+
848
+ const lowerName = propertyName.toLowerCase();
849
+ return envDependentProps.some(prop => lowerName.includes(prop));
850
+ }
851
+
852
+ looksLikeEnvironmentConfig(propertyName, value) {
853
+ // Check if this property/value combination looks like environment config
854
+ const lowerPropertyName = propertyName.toLowerCase();
855
+
856
+ if (typeof value === 'string') {
857
+ // Skip test data (common test passwords, etc.)
858
+ const testDataPatterns = [
859
+ /^(password123|test123|admin123|user123|wrongpassword|testpassword)$/i,
860
+ /^(test|mock|dummy|sample|example)/i,
861
+ /^\/(api|mock|test)/, // Test API paths
862
+ /^[a-z]+\d+$/i // Simple test values like 'user1', 'test2'
863
+ ];
864
+
865
+ // Don't skip common test patterns if they appear in credential contexts
866
+ const isCredentialContext = /token|key|secret|auth|otp|code|password|credential/i.test(propertyName);
867
+
868
+ if (!isCredentialContext && testDataPatterns.some(pattern => pattern.test(value))) {
869
+ return false;
870
+ }
871
+
872
+ // Skip object property paths and field names
873
+ const propertyPathPatterns = [
874
+ /^[a-zA-Z][a-zA-Z0-9]*(\[[0-9]+\])?\.[a-zA-Z][a-zA-Z0-9]*$/, // obj[0].prop, obj.prop
875
+ /^[a-zA-Z][a-zA-Z0-9]*\.[a-zA-Z][a-zA-Z0-9]*(\.[a-zA-Z][a-zA-Z0-9]*)*$/, // obj.prop.subprop
876
+ /^[a-zA-Z][a-zA-Z0-9]*(\[[0-9]+\])+$/, // obj[0], obj[0][1]
877
+ /^(key|field|prop|data)[A-Z]/, // keyXxx, fieldXxx, propXxx, dataXxx
878
+ /CheckDisplay|InputPossible|Flag$/i, // Common UI field patterns
879
+ /^exflg|^flg|Support$/i, // Business logic flags
880
+ ];
881
+
882
+ if (propertyPathPatterns.some(pattern => pattern.test(value))) {
883
+ return false;
884
+ }
885
+
886
+ // Skip CSS classes and UI constants
887
+ const uiPatterns = [
888
+ /^bg-|text-|cursor-|border-|flex-|grid-/, // CSS classes
889
+ /^(disabled|readonly|active|inactive)$/i, // UI states
890
+ /class$/i // className values
891
+ ];
892
+
893
+ if (uiPatterns.some(pattern => pattern.test(value))) {
894
+ return false;
895
+ }
896
+
897
+ // Skip internal system identifiers (queue names, service names, route names)
898
+ const systemIdentifierPatterns = [
899
+ /-queue$/i, // Queue names
900
+ /-task$/i, // Task names
901
+ /-activity$/i, // Activity names
902
+ /-service$/i, // Service names
903
+ /-worker$/i, // Worker names
904
+ /^[A-Z_]+_QUEUE$/, // CONSTANT_QUEUE names
905
+ /^[A-Z_]+_TASK$/, // CONSTANT_TASK names
906
+ /^(register|login|logout|reset-password|verify|update)$/i, // Route names
907
+ /password|token/i && /invalid|expired|attempts|exceeded/i // Error messages
908
+ ];
909
+
910
+ if (systemIdentifierPatterns.some(pattern => pattern.test(value))) {
911
+ return false;
912
+ }
913
+
914
+ // Skip error messages and validation messages
915
+ const messagePatterns = [
916
+ /invalid|expired|exceeded|failed|error|success/i,
917
+ /attempts|required|missing|not found/i,
918
+ /^[A-Z][a-z\s]{10,}$/, // Sentence-like messages
919
+ /は|が|を|に|で|と/, // Japanese particles (UI text)
920
+ /情報|画面|ボタン|入力/ // Japanese UI terms
921
+ ];
922
+
923
+ if (messagePatterns.some(pattern => pattern.test(value))) {
924
+ return false;
925
+ }
926
+
927
+ // URLs are environment-dependent
928
+ if (this.configPatterns.urls.regex.test(value)) {
929
+ return this.isEnvironmentDependentUrl(value);
930
+ }
931
+
932
+ // Credentials - but exclude test data
933
+ if (lowerPropertyName.includes('key') || lowerPropertyName.includes('secret') ||
934
+ lowerPropertyName.includes('token') || lowerPropertyName.includes('password')) {
935
+ return value.length > 10; // Real secrets are usually longer
936
+ }
937
+
938
+ // Skip short endpoint names or simple strings
939
+ if (value.length < 10 && !value.includes('.') && !value.includes('/')) {
940
+ return false;
941
+ }
942
+ }
943
+
944
+ if (typeof value === 'number') {
945
+ // Only flag environment-dependent numbers
946
+ return this.configPatterns.environmentNumbers.isEnvironmentDependent(value, propertyName);
947
+ }
948
+
949
+ return true;
950
+ }
951
+
952
+ isCommonConstant(value) {
953
+ // Common constants that are usually OK to hardcode
954
+ const commonConstants = [100, 200, 300, 400, 500, 1000, 2000, 3000, 5000, 8080, 3000];
955
+ return commonConstants.includes(value);
956
+ }
957
+
958
+ isConfigProperty(propertyName) {
959
+ const configProps = [
960
+ 'url', 'endpoint', 'baseurl', 'apiurl', 'host', 'port',
961
+ 'timeout', 'interval', 'delay', 'retry', 'retries',
962
+ 'username', 'password', 'apikey', 'secret', 'token',
963
+ 'database', 'connection', 'connectionstring',
964
+ 'maxsize', 'batchsize', 'pagesize', 'limit'
965
+ ];
966
+
967
+ const lowerName = propertyName.toLowerCase();
968
+ return configProps.some(prop => lowerName.includes(prop));
969
+ }
970
+
971
+ isConfigVariable(variableName) {
972
+ const configVars = [
973
+ 'api', 'url', 'endpoint', 'host', 'port',
974
+ 'timeout', 'interval', 'delay', 'retry',
975
+ 'config', 'setting', 'constant'
976
+ ];
977
+
978
+ const lowerName = variableName.toLowerCase();
979
+ return configVars.some(var_ => lowerName.includes(var_));
980
+ }
981
+
982
+ looksLikeHardcodedConfig(name, value) {
983
+ // Skip obvious constants and UI values
984
+ if (typeof value === 'string') {
985
+ if (value.length < 3) return false;
986
+ if (/^(ok|yes|no|true|false|success|error|info|warn)$/i.test(value)) return false;
987
+ }
988
+
989
+ if (typeof value === 'number') {
990
+ if (this.isCommonConstant(value)) return false;
991
+ }
992
+
993
+ return true;
994
+ }
995
+
996
+ createMessage(config) {
997
+ const baseMessage = 'Environment-dependent configuration should not be hardcoded.';
998
+
999
+ switch (config.type) {
1000
+ case 'url':
1001
+ return `${baseMessage} External URL '${config.value}' should be loaded from environment variables or configuration files.`;
1002
+ case 'credential':
1003
+ return `${baseMessage} Credential value '${config.value}' should be loaded from secure environment variables.`;
1004
+ case 'environment_config':
1005
+ return `${baseMessage} Environment-dependent value ${config.value} should be configurable via environment variables or config files.`;
1006
+ case 'connection':
1007
+ return `${baseMessage} Connection string should be loaded from environment variables.`;
1008
+ case 'property_config':
1009
+ return `${baseMessage} Property '${config.propertyName}' may contain environment-dependent value '${config.value}'.`;
1010
+ case 'variable_config':
1011
+ return `${baseMessage} Variable '${config.variableName}' may contain environment-dependent value '${config.value}'.`;
1012
+ case 'config_key':
1013
+ return `${baseMessage} Configuration key '${config.value}' should not be hardcoded.`;
1014
+ default:
1015
+ return `${baseMessage} Value '${config.value}' may differ between environments.`;
1016
+ }
1017
+ }
1018
+
1019
+ getSuggestion(type) {
1020
+ const suggestions = {
1021
+ 'url': 'Use process.env.API_URL or config.get("api.url")',
1022
+ 'credential': 'Use process.env.SECRET_KEY or secure vault',
1023
+ 'environment_config': 'Move to environment variables or config service',
1024
+ 'connection': 'Use process.env.DATABASE_URL',
1025
+ 'property_config': 'Consider if this differs between dev/staging/production',
1026
+ 'variable_config': 'Use environment variables if this differs between environments',
1027
+ 'config_key': 'Use constants or enums for configuration keys'
1028
+ };
1029
+
1030
+ return suggestions[type] || 'Consider if this value should differ between dev/staging/production environments';
1031
+ }
1032
+ }
1033
+
1034
+ module.exports = C067SymbolBasedAnalyzer;