@sun-asterisk/sunlint 1.3.16 → 1.3.17

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 (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
@@ -0,0 +1,575 @@
1
+ /**
2
+ * C041 Symbol-based Analyzer - Do not hardcode or push sensitive information
3
+ * Purpose: Prevent hardcoded secrets, tokens, API keys, passwords, and sensitive URLs in code
4
+ */
5
+ const { SyntaxKind } = require('ts-morph');
6
+
7
+ class C041SymbolBasedAnalyzer {
8
+ constructor(semanticEngine = null) {
9
+ this.ruleId = 'C041';
10
+ this.ruleName = 'Do not hardcode or push sensitive information (token, API key, secret, URL)';
11
+ this.semanticEngine = semanticEngine;
12
+ this.verbose = false;
13
+
14
+ // === Sensitive Patterns ===
15
+ this.sensitivePatterns = {
16
+ // API Keys and Tokens
17
+ apiKey: /\b(api[_-]?key|apikey|api[_-]?token)\s*[:=]\s*["'`]([a-zA-Z0-9_\-]{20,})["'`]/gi,
18
+
19
+ // AWS credentials
20
+ awsAccessKey: /\b(aws[_-]?access[_-]?key[_-]?id|aws[_-]?secret[_-]?access[_-]?key)\s*[:=]\s*["'`]([A-Z0-9]{20,})["'`]/gi,
21
+
22
+ // Private keys
23
+ privateKey: /(-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----)/gi,
24
+
25
+ // Database connection strings
26
+ dbConnection: /(mongodb|mysql|postgresql|postgres):\/\/[^:]+:[^@]+@/gi,
27
+
28
+ // JWT secrets
29
+ jwtSecret: /\b(jwt[_-]?secret|secret[_-]?key)\s*[:=]\s*["'`]([a-zA-Z0-9_\-]{16,})["'`]/gi,
30
+
31
+ // Bearer tokens
32
+ bearerToken: /\b(bearer|authorization)\s*[:=]\s*["'`]([a-zA-Z0-9_\-\.]{20,})["'`]/gi,
33
+
34
+ // OAuth tokens
35
+ oauthToken: /\b(oauth[_-]?token|access[_-]?token|refresh[_-]?token)\s*[:=]\s*["'`]([a-zA-Z0-9_\-\.]{20,})["'`]/gi,
36
+
37
+ // Generic secrets
38
+ secret: /\b(secret|password|passwd|pwd)\s*[:=]\s*["'`](?!.*(\$\{|process\.env|env\.|config\.))([^"'`\s]{8,})["'`]/gi,
39
+
40
+ // API endpoints with embedded credentials
41
+ credentialUrl: /(https?:\/\/[^:\/]+:[^@\/]+@[^\s"'`]+)/gi,
42
+
43
+ // SSH keys
44
+ sshKey: /(ssh-rsa\s+[A-Za-z0-9+\/=]{100,})/gi,
45
+
46
+ // Google API keys
47
+ googleApiKey: /AIza[0-9A-Za-z\-_]{35}/gi,
48
+
49
+ // Slack tokens
50
+ slackToken: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/gi,
51
+
52
+ // GitHub tokens
53
+ githubToken: /gh[pousr]_[A-Za-z0-9_]{36,}/gi,
54
+
55
+ // Stripe keys
56
+ stripeKey: /sk_live_[0-9a-zA-Z]{24,}/gi,
57
+
58
+ // Generic high-entropy strings (potential secrets) - DISABLED, too many false positives
59
+ // highEntropy: /["'`]([a-zA-Z0-9+\/=_\-]{32,})["'`]/g
60
+ };
61
+
62
+ // === Variable name patterns that suggest sensitive data ===
63
+ this.sensitiveVariableNames = [
64
+ /api[_-]?key/i,
65
+ /secret/i,
66
+ /password/i,
67
+ /passwd/i,
68
+ /pass/i,
69
+ /token/i,
70
+ /auth/i,
71
+ /credential/i,
72
+ /private[_-]?key/i,
73
+ /access[_-]?key/i,
74
+ /client[_-]?secret/i,
75
+ /encryption[_-]?key/i
76
+ ];
77
+
78
+ // === Safe patterns to exclude (env vars, config imports) ===
79
+ this.safePatterns = [
80
+ /process\.env\./,
81
+ /import\.meta\.env\./,
82
+ /env\./,
83
+ /config\./,
84
+ /Config\./,
85
+ /\$\{.*\}/, // Template literals with variables
86
+ /getEnv\(/,
87
+ /loadConfig\(/,
88
+ /readSecret\(/,
89
+ /vault\./i,
90
+ /secretManager\./i,
91
+ /keyVault\./i
92
+ ];
93
+
94
+ // === Ignore Configuration ===
95
+ this.ignoredFilePatterns = [
96
+ /test\//,
97
+ /tests\//,
98
+ /__tests__\//,
99
+ /\.test\./,
100
+ /\.spec\./,
101
+ /node_modules\//,
102
+ /\.env\.example$/,
103
+ /\.env\.template$/,
104
+ /mock/i,
105
+ /sample/i,
106
+ ];
107
+
108
+ // Common placeholder/dummy values to ignore
109
+ this.ignoredValues = [
110
+ 'your-api-key-here',
111
+ 'your_api_key',
112
+ 'mock',
113
+ 'xxx',
114
+ 'yyy',
115
+ 'zzz',
116
+ 'example',
117
+ 'test',
118
+ 'dummy',
119
+ 'placeholder',
120
+ 'changeme',
121
+ 'replace-me'
122
+ ];
123
+
124
+ // Patterns for non-sensitive constant values (event names, actions, etc.)
125
+ this.nonSensitivePatterns = [
126
+ /^(use|on|handle)[A-Z]/, // React hooks: usePassword, onPasswordChange
127
+ /_trigger$/i, // Event triggers: password_trigger
128
+ /_event$/i, // Event names: password_event
129
+ /_action$/i, // Action names: change_password_action
130
+ /_validation$/i, // Validation keys: password_validation
131
+ /_field$/i, // Form fields: password_field
132
+ /_input$/i, // Input names: password_input
133
+ /_activity$/i, // Activity names: updatePassword-activity
134
+ /_service$/i, // Service names: auth-service
135
+ /_handler$/i, // Handler names: password-handler
136
+ /_method$/i, // Method names: resetPassword-method
137
+ /^(change|update|set|get|renew|reset|forgot)[A-Z_]/i, // Action verbs: changePassword, renewpassword
138
+ /^[a-z_]+_(type|mode|status|state)$/i, // State keys: password_type, auth_mode
139
+ /^\/[a-zA-Z0-9_\/-]+$/i, // URL-like paths: /forgot_password, /reset_password
140
+ /^[a-z][a-zA-Z0-9]*-[a-z]+$/, // Kebab-case identifiers: updatePassword-activity
141
+ ];
142
+
143
+ // Variable/constant names that contain error messages or config (not secrets)
144
+ this.errorMessageConstants = [
145
+ /^[A-Z_]+ERROR[A-Z_]*$/, // COGNITO_ERROR, API_ERROR
146
+ /^[A-Z_]+MESSAGE[A-Z_]*$/, // MESSAGE_LOG, ERROR_MESSAGE
147
+ /^[A-Z_]+EXCEPTION[A-Z_]*$/, // AUTH_EXCEPTION
148
+ /^[A-Z_]+CODE[A-Z_]*$/, // ERROR_CODE, STATUS_CODE
149
+ /^[A-Z_]+STATUS[A-Z_]*$/, // USER_STATUS
150
+ /^[A-Z_]+TYPE[A-Z_]*$/, // MESSAGE_TYPE
151
+ ];
152
+
153
+ // Parent object names that contain service/activity mappings (not secrets)
154
+ this.serviceConfigConstants = [
155
+ /^activity$/i,
156
+ /^activities$/i,
157
+ /^service$/i,
158
+ /^services$/i,
159
+ /^handler$/i,
160
+ /^handlers$/i,
161
+ /^method$/i,
162
+ /^methods$/i,
163
+ /^action$/i,
164
+ /^actions$/i,
165
+ /^route$/i,
166
+ /^routes$/i,
167
+ ];
168
+
169
+ // Patterns in values that indicate error messages (not secrets)
170
+ this.errorMessagePatterns = [
171
+ /Exception$/, // NotAuthorizedException
172
+ /Error$/, // ValidationError
173
+ /\s+(not\s+found|invalid|expired|exceeded|failed)/i, // "User not found", "Token expired"
174
+ /^[A-Z][a-z]+([A-Z][a-z]+)*Exception$/, // PascalCaseException
175
+ /^[A-Z][a-z]+\s/, // Sentence case messages: "Email not found"
176
+ ];
177
+
178
+ // Patterns that indicate legitimate long strings (not secrets)
179
+ this.legitimateStringPatterns = [
180
+ /\sas\s+["'`]/i, // SQL aliases: AS "columnName"
181
+ /SELECT\s/i, // SQL queries
182
+ /FROM\s/i, // SQL queries
183
+ /WHERE\s/i, // SQL queries
184
+ /JOIN\s/i, // SQL queries
185
+ /\.[a-z_]+\s+as\s+/i, // SQL column aliases
186
+ /^[a-z_]+\.[a-z_]+/i, // Dot notation: table.column
187
+ /\s+AS\s+/, // SQL AS keyword
188
+ /<[^>]+>/, // HTML/XML tags
189
+ /^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP)/i, // SQL statements
190
+ /^\/[^\/]+\//, // Regex patterns or paths
191
+ /^\w+:\/\//, // URLs (but not with credentials)
192
+ ];
193
+ }
194
+
195
+ async initialize(semanticEngine = null) {
196
+ if (semanticEngine) {
197
+ this.semanticEngine = semanticEngine;
198
+ }
199
+ this.verbose = semanticEngine?.verbose || false;
200
+ if (process.env.SUNLINT_DEBUG) {
201
+ console.log(`🔧 [C041 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
202
+ }
203
+ }
204
+
205
+ async analyzeFileBasic(filePath, options = {}) {
206
+ return await this.analyzeFileWithSymbols(filePath, options);
207
+ }
208
+
209
+ async analyzeFileWithSymbols(filePath, options = {}) {
210
+ const violations = [];
211
+ const verbose = options.verbose || this.verbose;
212
+
213
+ if (!this.semanticEngine?.project) {
214
+ if (verbose) {
215
+ console.warn('[C041 Symbol-Based] No semantic engine available, skipping analysis');
216
+ }
217
+ return violations;
218
+ }
219
+
220
+ if (this.shouldIgnoreFile(filePath)) {
221
+ if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
222
+ return violations;
223
+ }
224
+
225
+ if (verbose) {
226
+ console.log(`🔍 [C041 Symbol-Based] Starting analysis for ${filePath}`);
227
+ }
228
+
229
+ try {
230
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
231
+ if (!sourceFile) {
232
+ if (verbose) {
233
+ console.log(`⚠️ [C041] Could not load source file: ${filePath}`);
234
+ }
235
+ return violations;
236
+ }
237
+
238
+ // Check string literals
239
+ this.checkStringLiterals(sourceFile, violations);
240
+
241
+ // Check variable declarations
242
+ this.checkVariableDeclarations(sourceFile, violations);
243
+
244
+ // Check property assignments
245
+ this.checkPropertyAssignments(sourceFile, violations);
246
+
247
+ // Check template literals
248
+ this.checkTemplateLiterals(sourceFile, violations);
249
+
250
+ if (verbose) {
251
+ console.log(`✅ [C041] Found ${violations.length} violations in ${filePath}`);
252
+ }
253
+
254
+ } catch (error) {
255
+ if (verbose) {
256
+ console.warn(`[C041 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
257
+ }
258
+ }
259
+
260
+ return violations;
261
+ }
262
+
263
+ checkStringLiterals(sourceFile, violations) {
264
+ const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral);
265
+
266
+ stringLiterals.forEach(literal => {
267
+ const text = literal.getText();
268
+ const value = literal.getLiteralValue();
269
+
270
+ // Skip if it's a legitimate long string (SQL, HTML, etc.)
271
+ if (this.isLegitimateString(value)) {
272
+ return;
273
+ }
274
+
275
+ // Skip if it's a safe pattern (env variable reference)
276
+ if (this.isSafeContext(literal)) {
277
+ return;
278
+ }
279
+
280
+ // Check against sensitive patterns
281
+ this.checkAgainstPatterns(literal, value, sourceFile, violations);
282
+ });
283
+ }
284
+
285
+ checkVariableDeclarations(sourceFile, violations) {
286
+ const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
287
+
288
+ variableDeclarations.forEach(varDecl => {
289
+ const name = varDecl.getName();
290
+ const initializer = varDecl.getInitializer();
291
+
292
+ if (!initializer) return;
293
+
294
+ // Check if variable name suggests sensitive data
295
+ const isSensitiveName = this.sensitiveVariableNames.some(pattern => pattern.test(name));
296
+
297
+ if (isSensitiveName) {
298
+ const initText = initializer.getText();
299
+
300
+ // Skip if using env variables or config
301
+ if (this.isSafeValue(initText)) {
302
+ return;
303
+ }
304
+
305
+ // Check if it's a hardcoded value
306
+ if (SyntaxKind.StringLiteral === initializer.getKind() ||
307
+ SyntaxKind.NoSubstitutionTemplateLiteral === initializer.getKind()) {
308
+ const value = initializer.getText().replace(/^["'`]|["'`]$/g, '');
309
+
310
+ // Skip if the value itself is a non-sensitive constant (event names, actions, etc.)
311
+ if (this.isNonSensitiveConstant(value)) {
312
+ return;
313
+ }
314
+
315
+ // More lenient check: any non-trivial string in a sensitive variable is suspicious
316
+ if (value.length >= 8 && !this.isIgnoredValue(value)) {
317
+ violations.push(this.createViolation(
318
+ varDecl,
319
+ sourceFile,
320
+ `Variable '${name}' appears to contain hardcoded sensitive information`
321
+ ));
322
+ }
323
+ } else if (SyntaxKind.TemplateExpression === initializer.getKind()) {
324
+ // Check template literals for hardcoded secrets in the string parts
325
+ const templateText = initializer.getText();
326
+
327
+ // Check if template has hardcoded secrets (not just variables)
328
+ if (!this.isSafeValue(templateText) && this.hasHardcodedSecretInTemplate(templateText)) {
329
+ violations.push(this.createViolation(
330
+ varDecl,
331
+ sourceFile,
332
+ `Variable '${name}' template literal contains hardcoded sensitive information`
333
+ ));
334
+ }
335
+ }
336
+ }
337
+ });
338
+ }
339
+
340
+ checkPropertyAssignments(sourceFile, violations) {
341
+ const propertyAssignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
342
+
343
+ propertyAssignments.forEach(prop => {
344
+ const name = prop.getName();
345
+ const initializer = prop.getInitializer();
346
+
347
+ if (!initializer) return;
348
+
349
+ // Skip if parent is an error/message constant object
350
+ const parent = prop.getParent()?.getParent();
351
+ if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
352
+ const parentName = parent.asKind(SyntaxKind.VariableDeclaration)?.getName();
353
+ if (parentName && (this.isErrorMessageConstant(parentName) || this.isServiceConfigConstant(parentName))) {
354
+ return; // Skip properties inside error/service constant objects
355
+ }
356
+ }
357
+
358
+ // Also check grandparent for nested objects like activity.account
359
+ const grandParent = prop.getParent()?.getParent()?.getParent()?.getParent();
360
+ if (grandParent && grandParent.getKind() === SyntaxKind.VariableDeclaration) {
361
+ const grandParentName = grandParent.asKind(SyntaxKind.VariableDeclaration)?.getName();
362
+ if (grandParentName && (this.isErrorMessageConstant(grandParentName) || this.isServiceConfigConstant(grandParentName))) {
363
+ return; // Skip properties inside nested service config objects
364
+ }
365
+ }
366
+
367
+ const isSensitiveName = this.sensitiveVariableNames.some(pattern => pattern.test(name));
368
+
369
+ if (isSensitiveName) {
370
+ const initText = initializer.getText();
371
+
372
+ if (this.isSafeValue(initText)) {
373
+ return;
374
+ }
375
+
376
+ if (SyntaxKind.StringLiteral === initializer.getKind() ||
377
+ SyntaxKind.NoSubstitutionTemplateLiteral === initializer.getKind()) {
378
+ const value = initializer.getText().replace(/^["'`]|["'`]$/g, '');
379
+
380
+ // Skip if the value itself is a non-sensitive constant
381
+ if (this.isNonSensitiveConstant(value)) {
382
+ return;
383
+ }
384
+
385
+ // Skip if the value looks like an error message
386
+ if (this.isErrorMessage(value)) {
387
+ return;
388
+ }
389
+
390
+ // More lenient check for properties with sensitive names
391
+ if (value.length >= 8 && !this.isIgnoredValue(value)) {
392
+ violations.push(this.createViolation(
393
+ prop,
394
+ sourceFile,
395
+ `Property '${name}' appears to contain hardcoded sensitive information`
396
+ ));
397
+ }
398
+ } else if (SyntaxKind.TemplateExpression === initializer.getKind()) {
399
+ const templateText = initializer.getText();
400
+
401
+ if (!this.isSafeValue(templateText) && this.hasHardcodedSecretInTemplate(templateText)) {
402
+ violations.push(this.createViolation(
403
+ prop,
404
+ sourceFile,
405
+ `Property '${name}' template literal contains hardcoded sensitive information`
406
+ ));
407
+ }
408
+ }
409
+ }
410
+ });
411
+ }
412
+
413
+ checkTemplateLiterals(sourceFile, violations) {
414
+ const templates = sourceFile.getDescendantsOfKind(SyntaxKind.TemplateExpression);
415
+
416
+ templates.forEach(template => {
417
+ const text = template.getText();
418
+
419
+ // Check if template contains hardcoded secrets even with variables
420
+ if (this.hasHardcodedSecretInTemplate(text)) {
421
+ // Check against patterns
422
+ this.checkAgainstPatterns(template, text, sourceFile, violations);
423
+ }
424
+ });
425
+ }
426
+
427
+ checkAgainstPatterns(node, value, sourceFile, violations) {
428
+ for (const [patternName, pattern] of Object.entries(this.sensitivePatterns)) {
429
+ pattern.lastIndex = 0; // Reset regex
430
+ const matches = pattern.exec(value);
431
+
432
+ if (matches) {
433
+ const matchedValue = matches[2] || matches[1] || matches[0];
434
+
435
+ if (this.isIgnoredValue(matchedValue)) {
436
+ continue;
437
+ }
438
+
439
+ violations.push(this.createViolation(
440
+ node,
441
+ sourceFile,
442
+ `Potential hardcoded sensitive data detected (${patternName})`
443
+ ));
444
+ }
445
+ }
446
+ }
447
+
448
+ isSafeContext(node) {
449
+ let parent = node.getParent();
450
+ let depth = 0;
451
+ const maxDepth = 5;
452
+
453
+ while (parent && depth < maxDepth) {
454
+ const text = parent.getText();
455
+
456
+ if (this.safePatterns.some(pattern => pattern.test(text))) {
457
+ return true;
458
+ }
459
+
460
+ parent = parent.getParent();
461
+ depth++;
462
+ }
463
+
464
+ return false;
465
+ }
466
+
467
+ isSafeValue(value) {
468
+ return this.safePatterns.some(pattern => pattern.test(value));
469
+ }
470
+
471
+ isLikelySecret(value) {
472
+ // Check length
473
+ if (value.length < 8) return false;
474
+
475
+ // Check if it looks like a placeholder
476
+ if (this.isIgnoredValue(value.toLowerCase())) return false;
477
+
478
+ // Check entropy (randomness)
479
+ const entropy = this.calculateEntropy(value);
480
+ return entropy > 3.5; // High entropy suggests random/generated secret
481
+ }
482
+
483
+ calculateEntropy(str) {
484
+ const len = str.length;
485
+ const frequencies = {};
486
+
487
+ for (let i = 0; i < len; i++) {
488
+ const char = str[i];
489
+ frequencies[char] = (frequencies[char] || 0) + 1;
490
+ }
491
+
492
+ let entropy = 0;
493
+ for (const freq of Object.values(frequencies)) {
494
+ const p = freq / len;
495
+ entropy -= p * Math.log2(p);
496
+ }
497
+
498
+ return entropy;
499
+ }
500
+
501
+ isIgnoredValue(value) {
502
+ const lowerValue = value.toLowerCase();
503
+ return this.ignoredValues.some(ignored =>
504
+ lowerValue.includes(ignored.toLowerCase())
505
+ );
506
+ }
507
+
508
+ isNonSensitiveConstant(nameOrValue) {
509
+ return this.nonSensitivePatterns.some(pattern => pattern.test(nameOrValue));
510
+ }
511
+
512
+ isLegitimateString(value) {
513
+ // Check if the string matches legitimate patterns (SQL, HTML, etc.)
514
+ return this.legitimateStringPatterns.some(pattern => pattern.test(value));
515
+ }
516
+
517
+ isErrorMessageConstant(name) {
518
+ return this.errorMessageConstants.some(pattern => pattern.test(name));
519
+ }
520
+
521
+ isErrorMessage(value) {
522
+ return this.errorMessagePatterns.some(pattern => pattern.test(value));
523
+ }
524
+
525
+ isServiceConfigConstant(name) {
526
+ return this.serviceConfigConstants.some(pattern => pattern.test(name));
527
+ }
528
+
529
+ hasHardcodedSecretInTemplate(templateText) {
530
+ // Extract the static string parts (not the ${...} parts)
531
+ const staticParts = templateText.split(/\$\{[^}]+\}/);
532
+
533
+ // Check if any static part contains what looks like a secret
534
+ for (const part of staticParts) {
535
+ // Check for patterns like "secret=xxx", "token=xxx", "key=xxx"
536
+ if (/(?:secret|token|key|password|passwd)[:=][^&\s`]{8,}/i.test(part)) {
537
+ return true;
538
+ }
539
+
540
+ // Check for high entropy strings in URL parameters
541
+ if (/[:=]([a-zA-Z0-9_\-]{16,})/.test(part)) {
542
+ const match = part.match(/[:=]([a-zA-Z0-9_\-]{16,})/);
543
+ if (match && match[1] && !this.isIgnoredValue(match[1])) {
544
+ const entropy = this.calculateEntropy(match[1]);
545
+ if (entropy > 3.5) {
546
+ return true;
547
+ }
548
+ }
549
+ }
550
+ }
551
+
552
+ return false;
553
+ }
554
+
555
+ createViolation(node, sourceFile, message) {
556
+ return {
557
+ ruleId: this.ruleId,
558
+ severity: 'warning',
559
+ message: message,
560
+ source: this.ruleId,
561
+ file: sourceFile.getFilePath(),
562
+ line: node.getStartLineNumber(),
563
+ column: node.getStart() - node.getStartLinePos(),
564
+ description: `[SYMBOL-BASED] ${message}`,
565
+ suggestion: 'Remove hardcoded sensitive information and use secure storage or environment variables instead.',
566
+ category: 'security',
567
+ };
568
+ }
569
+
570
+ shouldIgnoreFile(filePath) {
571
+ return this.ignoredFilePatterns.some((pattern) => pattern.test(filePath));
572
+ }
573
+ }
574
+
575
+ module.exports = C041SymbolBasedAnalyzer;
@@ -1,15 +1,16 @@
1
1
  // rules/common/C067_no_hardcoded_config/analyzer.js
2
- const C067SymbolBasedAnalyzer = require('./symbol-based-analyzer.js');
2
+ const C067SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
3
3
 
4
4
  class C067Analyzer {
5
5
  constructor(semanticEngine = null) {
6
- this.ruleId = 'C067';
7
- this.ruleName = 'No Hardcoded Configuration';
8
- this.description = 'Improve configurability, reduce risk when changing environments, and make configuration management flexible and maintainable.';
6
+ this.ruleId = "C067";
7
+ this.ruleName = "Do not hardcode configuration inside code";
8
+ this.description =
9
+ "Improve configurability, reduce risk when changing environments, and make configuration management flexible and maintainable. Avoid hardcoding API URLs, credentials, timeouts, retry intervals, batch sizes, and feature flags.";
9
10
  this.semanticEngine = semanticEngine;
10
11
  this.verbose = false;
11
-
12
- // Use symbol-based only for accuracy
12
+
13
+ // Use symbol-based analyzer with ts-morph for accurate AST analysis
13
14
  this.symbolAnalyzer = new C067SymbolBasedAnalyzer(semanticEngine);
14
15
  }
15
16
 
@@ -24,12 +25,12 @@ class C067Analyzer {
24
25
  // Main analyze method required by heuristic engine
25
26
  async analyze(files, language, options = {}) {
26
27
  const violations = [];
27
-
28
+
28
29
  for (const filePath of files) {
29
30
  if (options.verbose) {
30
- console.log(`[DEBUG] 🎯 C067: Analyzing ${filePath.split('/').pop()}`);
31
+ console.log(`[DEBUG] 🎯 C067: Analyzing ${filePath.split("/").pop()}`);
31
32
  }
32
-
33
+
33
34
  try {
34
35
  const fileViolations = await this.analyzeFileBasic(filePath, options);
35
36
  violations.push(...fileViolations);
@@ -37,7 +38,7 @@ class C067Analyzer {
37
38
  console.warn(`C067: Skipping ${filePath}: ${error.message}`);
38
39
  }
39
40
  }
40
-
41
+
41
42
  return violations;
42
43
  }
43
44
 
@@ -46,11 +47,11 @@ class C067Analyzer {
46
47
  // Try semantic engine first, fallback to standalone ts-morph
47
48
  if (this.semanticEngine?.isSymbolEngineReady?.() && this.semanticEngine.project) {
48
49
  if (this.verbose) {
49
- console.log(`[DEBUG] 🎯 C067: Using semantic engine for ${filePath.split('/').pop()}`);
50
+ console.log(`[DEBUG] 🎯 C067: Using semantic engine for ${filePath.split("/").pop()}`);
50
51
  }
51
-
52
+
52
53
  const violations = await this.symbolAnalyzer.analyzeFileBasic(filePath, options);
53
-
54
+
54
55
  if (this.verbose) {
55
56
  console.log(`[DEBUG] 🎯 C067: Symbol-based analysis found ${violations.length} violations`);
56
57
  }
@@ -59,11 +60,11 @@ class C067Analyzer {
59
60
  } else {
60
61
  // Fallback to standalone analysis
61
62
  if (this.verbose) {
62
- console.log(`[DEBUG] 🎯 C067: Using standalone analysis for ${filePath.split('/').pop()}`);
63
+ console.log(`[DEBUG] 🎯 C067: Using standalone analysis for ${filePath.split("/").pop()}`);
63
64
  }
64
-
65
+
65
66
  const violations = await this.symbolAnalyzer.analyzeFileStandalone(filePath, options);
66
-
67
+
67
68
  if (this.verbose) {
68
69
  console.log(`[DEBUG] 🎯 C067: Standalone analysis found ${violations.length} violations`);
69
70
  }