@sun-asterisk/sunlint 1.3.26 → 1.3.28

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 (69) hide show
  1. package/config/rules/enhanced-rules-registry.json +101 -17
  2. package/config/rules/rules-registry-generated.json +22 -22
  3. package/origin-rules/security-en.md +351 -338
  4. package/package.json +1 -1
  5. package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
  6. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
  8. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  9. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  10. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
  11. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  12. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  13. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  14. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  15. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  16. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  17. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  18. package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
  19. package/rules/security/S005_no_origin_auth/config.json +28 -67
  20. package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
  21. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
  22. package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
  23. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  24. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  25. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  26. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
  27. package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
  28. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
  29. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  30. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  31. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  32. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
  33. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
  34. package/rules/security/S022_escape_output_context/README.md +254 -0
  35. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  36. package/rules/security/S022_escape_output_context/config.json +229 -0
  37. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  38. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  39. package/rules/security/S023_no_json_injection/config.json +133 -0
  40. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  41. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  42. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  43. package/rules/security/S029_csrf_protection/config.json +127 -0
  44. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  45. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  46. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  47. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  48. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  49. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  50. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  51. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  52. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  53. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  54. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  55. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  56. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  57. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  58. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  59. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
  60. package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
  61. package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
  62. package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
  63. package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
  64. package/docs/COMMAND-EXAMPLES.md +0 -390
  65. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
  66. package/docs/FOLDER_STRUCTURE.md +0 -59
  67. package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
  68. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
  69. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
@@ -0,0 +1,1204 @@
1
+ /**
2
+ * Rule S012: Hardcoded Secrets Detection
3
+ *
4
+ * Detects hardcoded secrets, API keys, passwords, tokens, and other sensitive
5
+ * credentials in source code. This rule helps prevent accidental exposure of
6
+ * secrets through version control and reduces security risks.
7
+ *
8
+ * OWASP: A02:2021 - Cryptographic Failures
9
+ * CWE: CWE-798 - Use of Hard-coded Credentials
10
+ */
11
+
12
+ const { SyntaxKind } = require("ts-morph");
13
+
14
+ class S012SymbolBasedAnalyzer {
15
+ constructor(semanticEngine = null) {
16
+ this.ruleId = "S012";
17
+ this.semanticEngine = semanticEngine;
18
+
19
+ // Patterns for secret variable names
20
+ this.secretVariablePatterns = [
21
+ 'password',
22
+ 'passwd',
23
+ 'pwd',
24
+ 'secret',
25
+ 'api_key',
26
+ 'apikey',
27
+ 'api-key',
28
+ 'access_key',
29
+ 'accesskey',
30
+ 'secret_key',
31
+ 'secretkey',
32
+ 'private_key',
33
+ 'privatekey',
34
+ 'token',
35
+ 'auth_token',
36
+ 'authtoken',
37
+ 'access_token',
38
+ 'accesstoken',
39
+ 'refresh_token',
40
+ 'refreshtoken',
41
+ 'jwt_secret',
42
+ 'jwtsecret',
43
+ 'encryption_key',
44
+ 'encryptionkey',
45
+ 'database_password',
46
+ 'db_password',
47
+ 'db_pass',
48
+ 'connection_string',
49
+ 'connectionstring',
50
+ 'credentials',
51
+ 'auth_token',
52
+ 'auth_key',
53
+ 'auth_secret',
54
+ 'bearer_token',
55
+ 'bearer',
56
+ 'certificate',
57
+ 'ssh_key',
58
+ 'sshkey',
59
+ 'oauth_secret',
60
+ 'oauthsecret',
61
+ 'client_secret',
62
+ 'clientsecret',
63
+ 'master_key',
64
+ 'masterkey',
65
+ 'admin_password',
66
+ 'root_password',
67
+ 'app_secret',
68
+ 'appsecret',
69
+ ];
70
+
71
+ // Patterns for secret values (high entropy strings)
72
+ this.secretValuePatterns = [
73
+ // AWS Access Keys
74
+ /AKIA[0-9A-Z]{16}/,
75
+ // AWS Secret Keys (40 characters base64)
76
+ /aws(.{0,20})?['\"][0-9a-zA-Z\/+]{40}['\"]/i,
77
+ // Generic API Keys (32+ chars with mixed case/numbers)
78
+ /['\"][a-z0-9_-]{32,}['\"]/i,
79
+ // JWT tokens
80
+ /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/,
81
+ // Generic secrets (high entropy, 20+ chars)
82
+ /['\"][a-zA-Z0-9+/=_-]{20,}['\"]/,
83
+ // GitHub tokens
84
+ /gh[pousr]_[A-Za-z0-9_]{36,}/,
85
+ // Slack tokens
86
+ /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}-[a-zA-Z0-9]{24,}/,
87
+ // Google API keys
88
+ /AIza[0-9A-Za-z_-]{35}/,
89
+ // Firebase URLs with secrets
90
+ /[a-z0-9-]+\.firebaseio\.com/,
91
+ // Private keys
92
+ /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
93
+ // Connection strings with passwords
94
+ /(mongodb|postgres|mysql):\/\/[^:]+:[^@]+@/i,
95
+ ];
96
+
97
+ // Safe patterns to exclude (environment variables, function calls, etc.)
98
+ this.safePatterns = [
99
+ 'process.env',
100
+ 'env.',
101
+ 'getenv',
102
+ 'config.get',
103
+ 'configservice.get', // NestJS ConfigService
104
+ '.get(', // Any .get() method call
105
+ '.getstring(',
106
+ '.getnumber(',
107
+ 'vault.get',
108
+ 'secrets.get',
109
+ 'secretsmanager',
110
+ 'keyvault',
111
+ 'parameter.store',
112
+ 'ssm.get',
113
+ 'dotenv',
114
+ '.env',
115
+ 'import.meta.env',
116
+ 'vite_',
117
+ 'next_public_',
118
+ 'react_app_',
119
+ 'vue_app_',
120
+ 'example',
121
+ 'test',
122
+ 'mock',
123
+ 'dummy',
124
+ 'fake',
125
+ 'placeholder',
126
+ 'your_',
127
+ 'your-',
128
+ '<your',
129
+ 'xxx',
130
+ '***',
131
+ '...',
132
+ 'todo',
133
+ 'fixme',
134
+ 'changeme',
135
+ 'replace',
136
+ 'insert',
137
+ '.split(', // token.split() is not a secret
138
+ '.slice(', // string slicing
139
+ '.substring(', // string operations
140
+ ];
141
+
142
+ // Common test/example values that are safe
143
+ this.safeTestValues = [
144
+ 'test',
145
+ 'testing',
146
+ 'example',
147
+ 'demo',
148
+ 'sample',
149
+ 'mock',
150
+ 'dummy',
151
+ 'fake',
152
+ 'localhost',
153
+ '127.0.0.1',
154
+ '0.0.0.0',
155
+ 'admin',
156
+ '12345',
157
+ 'password',
158
+ 'secret',
159
+ 'changeme',
160
+ 'your-secret-here',
161
+ 'your-key-here',
162
+ 'xxx',
163
+ '***',
164
+ ];
165
+
166
+ // Minimum entropy threshold for considering a string as secret
167
+ this.minEntropy = 3.5;
168
+
169
+ // Minimum length for secret strings
170
+ this.minSecretLength = 16;
171
+ }
172
+
173
+ /**
174
+ * Main analysis method
175
+ */
176
+ async analyze(sourceFile, filePath = "") {
177
+ const violations = [];
178
+
179
+ // Skip test files and config files that commonly have example secrets
180
+ if (this.shouldSkipFile(filePath)) {
181
+ return violations;
182
+ }
183
+
184
+ // Check variable declarations
185
+ const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
186
+ for (const varDecl of variableDeclarations) {
187
+ const violation = this.checkVariableDeclaration(varDecl, filePath);
188
+ if (violation) {
189
+ violations.push(violation);
190
+ }
191
+ }
192
+
193
+ // Check property assignments
194
+ const propertyAssignments = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAssignment);
195
+ for (const propAssign of propertyAssignments) {
196
+ const violation = this.checkPropertyAssignment(propAssign, filePath);
197
+ if (violation) {
198
+ violations.push(violation);
199
+ }
200
+ }
201
+
202
+ // Check string literals for hardcoded secrets
203
+ const stringLiterals = sourceFile.getDescendantsOfKind(SyntaxKind.StringLiteral);
204
+ for (const strLit of stringLiterals) {
205
+ const violation = this.checkStringLiteral(strLit, filePath);
206
+ if (violation) {
207
+ violations.push(violation);
208
+ }
209
+ }
210
+
211
+ return violations;
212
+ }
213
+
214
+ /**
215
+ * Check if file should be skipped
216
+ */
217
+ shouldSkipFile(filePath) {
218
+ const skipPatterns = [
219
+ /test\//i,
220
+ /tests\//i,
221
+ /__tests__\//i,
222
+ /\.test\./i,
223
+ /\.spec\./i,
224
+ /node_modules\//i,
225
+ /\.example\./i,
226
+ /\.sample\./i,
227
+ /\.template\./i,
228
+ /dist\//i,
229
+ /build\//i,
230
+ /coverage\//i,
231
+ ];
232
+
233
+ return skipPatterns.some(pattern => pattern.test(filePath));
234
+ }
235
+
236
+ /**
237
+ * Check variable declarations for hardcoded secrets
238
+ */
239
+ checkVariableDeclaration(varDecl, filePath) {
240
+ const name = varDecl.getName();
241
+ const initializer = varDecl.getInitializer();
242
+
243
+ if (!initializer) {
244
+ return null;
245
+ }
246
+
247
+ const nameLower = name.toLowerCase();
248
+
249
+ // Skip error constant names
250
+ if (this.isErrorConstant(name)) {
251
+ return null;
252
+ }
253
+
254
+ // Skip destructuring patterns like { token } or { password }
255
+ if (this.isDestructuringPattern(name)) {
256
+ return null;
257
+ }
258
+
259
+ // Skip documentation/schema variables (Swagger, Docs, Schema, Dto)
260
+ if (this.isDocumentationVariable(name)) {
261
+ return null;
262
+ }
263
+
264
+ // Skip counter/attempt variables (failedPasswordAttempts, loginAttempts)
265
+ if (this.isCounterVariable(name)) {
266
+ return null;
267
+ }
268
+
269
+ // Skip error collection variables (tokenErrors, passwordErrors)
270
+ if (this.isErrorCollectionVariable(name)) {
271
+ return null;
272
+ }
273
+
274
+ // Skip boolean check variables (isValidToken, hasPassword)
275
+ if (this.isBooleanCheckVariable(name)) {
276
+ return null;
277
+ }
278
+
279
+ // Skip utility function/class names (generatePassword, IsPassword)
280
+ if (this.isUtilityFunctionOrClass(name)) {
281
+ return null;
282
+ }
283
+
284
+ // Skip framework-specific internal variables (__next_navigation_guard_token)
285
+ if (this.isFrameworkInternalVariable(name)) {
286
+ return null;
287
+ }
288
+
289
+ // Skip error constant properties (tokenExpired, invalidToken)
290
+ if (this.isErrorConstantProperty(name)) {
291
+ return null;
292
+ }
293
+
294
+ // Skip React/Vue state variables and selectors
295
+ if (this.isStateVariableOrSelector(name)) {
296
+ return null;
297
+ }
298
+
299
+ // Skip format/pattern/regex definitions
300
+ if (this.isFormatOrPatternDefinition(name)) {
301
+ return null;
302
+ }
303
+
304
+ // Check if variable name suggests it's a secret
305
+ const isSuspiciousName = this.secretVariablePatterns.some(pattern =>
306
+ nameLower.includes(pattern)
307
+ );
308
+
309
+ if (!isSuspiciousName) {
310
+ return null;
311
+ }
312
+
313
+ // Check if it's using a safe pattern (env variable, config, etc.)
314
+ const initText = initializer.getText();
315
+ if (this.isSafePattern(initText)) {
316
+ return null;
317
+ }
318
+
319
+ // Skip if it's a URL/endpoint variable (apiGetTokenUrl = '/api/token')
320
+ if (this.isURLOrEndpointVariable(name, initText)) {
321
+ return null;
322
+ }
323
+
324
+ // Skip if extracting from query parameters (get(searchParams, 'token'))
325
+ if (this.isQueryParameterExtraction(initText)) {
326
+ return null;
327
+ }
328
+
329
+ // Skip if accessing framework internal property (nextState?.__next_navigation_guard_token)
330
+ if (this.isAccessingFrameworkInternal(initText)) {
331
+ return null;
332
+ }
333
+
334
+ // Skip if initializer is a property access (e.g., AWS.config.credentials)
335
+ if (this.isPropertyAccess(initText)) {
336
+ return null;
337
+ }
338
+
339
+ // Skip if initializer is a constructor call (e.g., new SecretClient(...))
340
+ if (this.isConstructorCall(initText)) {
341
+ return null;
342
+ }
343
+
344
+ // Skip if initializer is an await expression for secret retrieval
345
+ if (this.isAsyncSecretRetrieval(initText)) {
346
+ return null;
347
+ }
348
+
349
+ // Skip if initializer is a function/method call
350
+ if (this.isFunctionOrMethodCall(initText)) {
351
+ return null;
352
+ }
353
+
354
+ // Skip if extracting from API response (response.data['...'].AccessToken)
355
+ if (this.isExtractingFromAPIResponse(initText)) {
356
+ return null;
357
+ }
358
+
359
+ // Skip if it's an object literal with only references (no hardcoded values)
360
+ if (this.isObjectLiteralWithOnlyReferences(initializer)) {
361
+ return null;
362
+ }
363
+
364
+ // For UPPERCASE constants, be more strict - only flag if value is actually hardcoded
365
+ // This allows PASSWORD_REGEX = /.../ but flags API_KEY = "hardcoded-key"
366
+ const isConstant = this.isConstantName(name);
367
+
368
+ // Check if the value looks like a hardcoded secret
369
+ if (this.isHardcodedSecret(initText, nameLower, isConstant)) {
370
+ return {
371
+ line: varDecl.getStartLineNumber(),
372
+ column: varDecl.getStart() - varDecl.getStartLinePos(),
373
+ message: `Hardcoded secret detected: Variable '${name}' appears to contain a hardcoded secret. Use environment variables or a secret management system instead.`,
374
+ severity: "error",
375
+ ruleId: this.ruleId,
376
+ secretType: this.identifySecretType(nameLower),
377
+ };
378
+ }
379
+
380
+ return null;
381
+ }
382
+
383
+ /**
384
+ * Check property assignments for hardcoded secrets
385
+ */
386
+ checkPropertyAssignment(propAssign, filePath) {
387
+ const name = propAssign.getName();
388
+ const initializer = propAssign.getInitializer();
389
+
390
+ if (!initializer) {
391
+ return null;
392
+ }
393
+
394
+ const nameLower = name.toLowerCase();
395
+
396
+ // Skip error constant property names
397
+ if (this.isErrorConstant(name)) {
398
+ return null;
399
+ }
400
+
401
+ // Skip function-like property names (verbs + nouns)
402
+ if (this.isFunctionName(name)) {
403
+ return null;
404
+ }
405
+
406
+ // Skip documentation/schema properties
407
+ if (this.isDocumentationVariable(name)) {
408
+ return null;
409
+ }
410
+
411
+ // Skip counter/attempt properties
412
+ if (this.isCounterVariable(name)) {
413
+ return null;
414
+ }
415
+
416
+ // Skip error collection properties
417
+ if (this.isErrorCollectionVariable(name)) {
418
+ return null;
419
+ }
420
+
421
+ // Skip boolean check properties
422
+ if (this.isBooleanCheckVariable(name)) {
423
+ return null;
424
+ }
425
+
426
+ // Skip utility function/class names
427
+ if (this.isUtilityFunctionOrClass(name)) {
428
+ return null;
429
+ }
430
+
431
+ // Skip framework-specific internal variables
432
+ if (this.isFrameworkInternalVariable(name)) {
433
+ return null;
434
+ }
435
+
436
+ // Skip error constant properties (tokenExpired, invalidToken)
437
+ if (this.isErrorConstantProperty(name)) {
438
+ return null;
439
+ }
440
+
441
+ // Skip if this is part of an error object structure
442
+ if (this.isPartOfErrorObject(propAssign)) {
443
+ return null;
444
+ }
445
+
446
+ // Skip if this is just a property name (not a hardcoded value)
447
+ if (this.isPropertyNameOnly(propAssign)) {
448
+ return null;
449
+ }
450
+
451
+ // Skip React/Vue state variables and selectors
452
+ if (this.isStateVariableOrSelector(name)) {
453
+ return null;
454
+ }
455
+
456
+ // Skip format/pattern/regex definitions
457
+ if (this.isFormatOrPatternDefinition(name)) {
458
+ return null;
459
+ }
460
+
461
+ // Check if property name suggests it's a secret
462
+ const isSuspiciousName = this.secretVariablePatterns.some(pattern =>
463
+ nameLower.includes(pattern)
464
+ );
465
+
466
+ if (!isSuspiciousName) {
467
+ return null;
468
+ }
469
+
470
+ const initText = initializer.getText();
471
+ if (this.isSafePattern(initText)) {
472
+ return null;
473
+ }
474
+
475
+ // Skip if initializer is a function/method call
476
+ if (this.isFunctionOrMethodCall(initText)) {
477
+ return null;
478
+ }
479
+
480
+ // Skip if it's a simple string literal in constant definitions (message strings)
481
+ if (this.isMessageString(initText)) {
482
+ return null;
483
+ }
484
+
485
+ if (this.isHardcodedSecret(initText, nameLower)) {
486
+ return {
487
+ line: propAssign.getStartLineNumber(),
488
+ column: propAssign.getStart() - propAssign.getStartLinePos(),
489
+ message: `Hardcoded secret detected: Property '${name}' appears to contain a hardcoded secret. Use environment variables or a secret management system instead.`,
490
+ severity: "error",
491
+ ruleId: this.ruleId,
492
+ secretType: this.identifySecretType(nameLower),
493
+ };
494
+ }
495
+
496
+ return null;
497
+ }
498
+
499
+ /**
500
+ * Check string literals for patterns that look like secrets
501
+ */
502
+ checkStringLiteral(strLit, filePath) {
503
+ const text = strLit.getText();
504
+ const literalValue = strLit.getLiteralValue();
505
+
506
+ // Skip short strings
507
+ if (literalValue.length < this.minSecretLength) {
508
+ return null;
509
+ }
510
+
511
+ // Skip if it's a safe test value
512
+ if (this.isSafeTestValue(literalValue)) {
513
+ return null;
514
+ }
515
+
516
+ // Skip if this is part of SQL query (addSelect, select, query strings)
517
+ if (this.isPartOfSQLQuery(strLit)) {
518
+ return null;
519
+ }
520
+
521
+ // Check if parent is using safe patterns
522
+ const parent = strLit.getParent();
523
+ if (parent) {
524
+ const parentText = parent.getText();
525
+ if (this.isSafePattern(parentText)) {
526
+ return null;
527
+ }
528
+ }
529
+
530
+ // Check for known secret patterns
531
+ for (const pattern of this.secretValuePatterns) {
532
+ if (pattern.test(literalValue)) {
533
+ // Additional validation for high entropy
534
+ if (this.calculateEntropy(literalValue) >= this.minEntropy) {
535
+ return {
536
+ line: strLit.getStartLineNumber(),
537
+ column: strLit.getStart() - strLit.getStartLinePos(),
538
+ message: `Potential hardcoded secret detected: String literal matches known secret pattern. Consider using environment variables or a secret management system.`,
539
+ severity: "error",
540
+ ruleId: this.ruleId,
541
+ secretType: "unknown",
542
+ };
543
+ }
544
+ }
545
+ }
546
+
547
+ return null;
548
+ }
549
+
550
+ /**
551
+ * Check if text contains safe patterns
552
+ */
553
+ isSafePattern(text) {
554
+ const textLower = text.toLowerCase();
555
+ return this.safePatterns.some(pattern => textLower.includes(pattern));
556
+ }
557
+
558
+ /**
559
+ * Check if value is a safe test value
560
+ */
561
+ isSafeTestValue(value) {
562
+ const valueLower = value.toLowerCase();
563
+ return this.safeTestValues.some(testVal => valueLower === testVal);
564
+ }
565
+
566
+ /**
567
+ * Check if a value is likely a hardcoded secret
568
+ */
569
+ isHardcodedSecret(value, variableName = '', isConstant = false) {
570
+ // Remove quotes if present
571
+ const cleanValue = value.replace(/^['"`]|['"`]$/g, '');
572
+
573
+ // Skip empty or very short values
574
+ if (cleanValue.length < 8) {
575
+ return false;
576
+ }
577
+
578
+ // Skip if it's a safe test value
579
+ if (this.isSafeTestValue(cleanValue)) {
580
+ return false;
581
+ }
582
+
583
+ // For UPPERCASE constants, skip if it's not a string literal
584
+ // This allows: PASSWORD_REGEX = /.../, TOKEN_STATUS = {...}
585
+ // But flags: API_KEY = "hardcoded-secret"
586
+ if (isConstant) {
587
+ // Only flag if it's a quoted string (likely a hardcoded secret)
588
+ if (!value.match(/^['"`]/)) {
589
+ return false;
590
+ }
591
+ }
592
+
593
+ // Check for specific secret patterns
594
+ for (const pattern of this.secretValuePatterns) {
595
+ if (pattern.test(value)) {
596
+ return true;
597
+ }
598
+ }
599
+
600
+ // Check entropy for generic secrets
601
+ if (cleanValue.length >= this.minSecretLength) {
602
+ const entropy = this.calculateEntropy(cleanValue);
603
+
604
+ // Higher entropy threshold if variable name is suspicious
605
+ const isSuspiciousVar = this.secretVariablePatterns.some(p =>
606
+ variableName.includes(p)
607
+ );
608
+
609
+ const threshold = isSuspiciousVar ? 3.0 : this.minEntropy;
610
+
611
+ if (entropy >= threshold) {
612
+ // Additional check: ensure it has mixed characters
613
+ const hasMixedCase = /[a-z]/.test(cleanValue) && /[A-Z]/.test(cleanValue);
614
+ const hasNumbers = /[0-9]/.test(cleanValue);
615
+ const hasSpecialChars = /[^a-zA-Z0-9]/.test(cleanValue);
616
+
617
+ // At least 2 of 3 characteristics for high confidence
618
+ const characteristics = [hasMixedCase, hasNumbers, hasSpecialChars].filter(Boolean).length;
619
+
620
+ if (characteristics >= 2) {
621
+ return true;
622
+ }
623
+ }
624
+ }
625
+
626
+ return false;
627
+ }
628
+
629
+ /**
630
+ * Calculate Shannon entropy of a string
631
+ */
632
+ calculateEntropy(str) {
633
+ const len = str.length;
634
+ const frequencies = {};
635
+
636
+ for (let i = 0; i < len; i++) {
637
+ const char = str[i];
638
+ frequencies[char] = (frequencies[char] || 0) + 1;
639
+ }
640
+
641
+ let entropy = 0;
642
+ for (const char in frequencies) {
643
+ const probability = frequencies[char] / len;
644
+ entropy -= probability * Math.log2(probability);
645
+ }
646
+
647
+ return entropy;
648
+ }
649
+
650
+ /**
651
+ * Identify the type of secret based on variable name
652
+ */
653
+ identifySecretType(name) {
654
+ if (name.includes('password') || name.includes('passwd') || name.includes('pwd')) {
655
+ return 'password';
656
+ }
657
+ if (name.includes('api') && (name.includes('key') || name.includes('token'))) {
658
+ return 'api_key';
659
+ }
660
+ if (name.includes('access') && name.includes('key')) {
661
+ return 'access_key';
662
+ }
663
+ if (name.includes('secret') && name.includes('key')) {
664
+ return 'secret_key';
665
+ }
666
+ if (name.includes('private') && name.includes('key')) {
667
+ return 'private_key';
668
+ }
669
+ if (name.includes('token')) {
670
+ return 'token';
671
+ }
672
+ if (name.includes('jwt')) {
673
+ return 'jwt_secret';
674
+ }
675
+ if (name.includes('connection') && name.includes('string')) {
676
+ return 'connection_string';
677
+ }
678
+ if (name.includes('credentials')) {
679
+ return 'credentials';
680
+ }
681
+
682
+ return 'secret';
683
+ }
684
+
685
+ /**
686
+ * Check if name is a constant (UPPER_CASE style)
687
+ */
688
+ isConstantName(name) {
689
+ // All uppercase with underscores (e.g., TOKEN_STATUS, PASSWORD_REGEX, DB_PASSWORD)
690
+ return /^[A-Z_][A-Z0-9_]*$/.test(name);
691
+ }
692
+
693
+ /**
694
+ * Check if name is an error constant
695
+ */
696
+ isErrorConstant(name) {
697
+ // Pattern: *_E_* or FE_E_* or BE_E_* or ERROR_*
698
+ return /_E_/i.test(name) || /^(FE|BE|API)_E_/i.test(name) || /^ERROR_/i.test(name);
699
+ }
700
+
701
+ /**
702
+ * Check if name is a destructuring pattern
703
+ */
704
+ isDestructuringPattern(name) {
705
+ // Destructuring patterns like { token } will have curly braces
706
+ return name.includes('{') || name.includes('}');
707
+ }
708
+
709
+ /**
710
+ * Check if name looks like a function name
711
+ */
712
+ isFunctionName(name) {
713
+ // Common function name patterns (verb + noun)
714
+ const functionVerbs = [
715
+ 'get', 'set', 'send', 'receive', 'fetch', 'update', 'delete', 'create',
716
+ 'verify', 'validate', 'check', 'is', 'has', 'can', 'should',
717
+ 'reset', 'change', 'generate', 'encode', 'decode', 'encrypt', 'decrypt',
718
+ 'handle', 'process', 'execute', 'run', 'start', 'stop'
719
+ ];
720
+
721
+ const nameLower = name.toLowerCase();
722
+
723
+ // Check if starts with a verb
724
+ return functionVerbs.some(verb => {
725
+ const verbPattern = new RegExp(`^${verb}[A-Z]`);
726
+ return verbPattern.test(name) || nameLower.startsWith(verb + '_');
727
+ });
728
+ }
729
+
730
+ /**
731
+ * Check if text is a property access (e.g., AWS.config.credentials)
732
+ */
733
+ isPropertyAccess(text) {
734
+ // Contains dot notation and doesn't start with a quote
735
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*\.[a-zA-Z0-9_.]+$/.test(text.trim());
736
+ }
737
+
738
+ /**
739
+ * Check if text is a constructor call (e.g., new SecretClient(...))
740
+ */
741
+ isConstructorCall(text) {
742
+ return /^new\s+[A-Z][a-zA-Z0-9_]*\s*\(/.test(text.trim());
743
+ }
744
+
745
+ /**
746
+ * Check if text is an async secret retrieval (e.g., await secretClient.getSecret(...))
747
+ */
748
+ isAsyncSecretRetrieval(text) {
749
+ const textLower = text.toLowerCase();
750
+ return textLower.startsWith('await') &&
751
+ (textLower.includes('getsecret') ||
752
+ textLower.includes('get(') ||
753
+ textLower.includes('fetch'));
754
+ }
755
+
756
+ /**
757
+ * Check if text is a function or method call
758
+ */
759
+ isFunctionOrMethodCall(text) {
760
+ const trimmed = text.trim();
761
+ // Function call: functionName(...) or obj.method(...) or await func(...)
762
+ return /^(await\s+)?[a-zA-Z_$][a-zA-Z0-9_$.]*\s*\(/.test(trimmed) ||
763
+ // Method chain: obj.method1().method2()
764
+ /\)\s*\.\s*[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/.test(trimmed);
765
+ }
766
+
767
+ /**
768
+ * Check if text is a message string (descriptive text, not a secret)
769
+ */
770
+ isMessageString(text) {
771
+ const trimmed = text.trim();
772
+
773
+ // Must be a quoted string
774
+ if (!trimmed.match(/^['"`]/)) {
775
+ return false;
776
+ }
777
+
778
+ const content = trimmed.replace(/^['"`]|['"`]$/g, '').toLowerCase();
779
+
780
+ // Message strings typically contain spaces and common words
781
+ const messageIndicators = [
782
+ ' is ',
783
+ ' are ',
784
+ ' has ',
785
+ ' have ',
786
+ ' invalid',
787
+ ' expired',
788
+ ' not found',
789
+ ' failed',
790
+ ' error',
791
+ ' exceeded',
792
+ ' attempts',
793
+ ' required',
794
+ ' missing',
795
+ ];
796
+
797
+ return messageIndicators.some(indicator => content.includes(indicator));
798
+ }
799
+
800
+ /**
801
+ * Check if name is a documentation/schema variable
802
+ */
803
+ isDocumentationVariable(name) {
804
+ const docSuffixes = ['Swagger', 'Docs', 'Doc', 'Schema', 'Dto', 'Documentation', 'Spec'];
805
+ return docSuffixes.some(suffix => name.endsWith(suffix));
806
+ }
807
+
808
+ /**
809
+ * Check if name is a counter/attempt variable
810
+ */
811
+ isCounterVariable(name) {
812
+ const counterPatterns = [
813
+ /attempts?$/i, // loginAttempts, failedAttempts
814
+ /count$/i, // passwordCount, tokenCount
815
+ /counter$/i, // loginCounter
816
+ /limit$/i, // passwordLimit
817
+ /max.*attempts?/i, // maxFailedAttempts
818
+ /failed.*attempts?/i, // failedPasswordAttempts
819
+ ];
820
+ return counterPatterns.some(pattern => pattern.test(name));
821
+ }
822
+
823
+ /**
824
+ * Check if name is an error collection variable
825
+ */
826
+ isErrorCollectionVariable(name) {
827
+ const nameLower = name.toLowerCase();
828
+ return nameLower.endsWith('errors') || // tokenErrors, passwordErrors
829
+ nameLower.endsWith('error') || // tokenError
830
+ nameLower.includes('errormessage') || // errorMessages
831
+ nameLower.includes('errorlist'); // errorList
832
+ }
833
+
834
+ /**
835
+ * Check if name is a boolean check variable
836
+ */
837
+ isBooleanCheckVariable(name) {
838
+ const booleanPrefixes = ['is', 'has', 'can', 'should', 'will', 'did'];
839
+ const nameLower = name.toLowerCase();
840
+
841
+ return booleanPrefixes.some(prefix => {
842
+ // Check camelCase: isValidToken, hasPassword
843
+ const camelPattern = new RegExp(`^${prefix}[A-Z]`);
844
+ // Check snake_case: is_valid_token
845
+ const snakePattern = new RegExp(`^${prefix}_`);
846
+ return camelPattern.test(name) || snakePattern.test(nameLower);
847
+ });
848
+ }
849
+
850
+ /**
851
+ * Check if name is an error constant property in error object
852
+ * Examples: tokenExpired, invalidToken, passwordSameAsMail, incorrectOldPassword
853
+ */
854
+ isErrorConstantProperty(name) {
855
+ // Common error property patterns
856
+ const errorPatterns = [
857
+ /^invalid/i, // invalidToken, invalidRefreshToken
858
+ /^incorrect/i, // incorrectOldPassword, incorrectPassword
859
+ /^empty/i, // emptyAccessToken
860
+ /^missing/i, // missingPassword
861
+ /expired$/i, // tokenExpired, passwordExpired
862
+ /notfound$/i, // tokenNotFound, userNotFound
863
+ /notactive$/i, // tokenIsNotActive
864
+ /revoked$/i, // tokenHasBeenRevoked
865
+ /locked$/i, // userLocked, accountLocked
866
+ /sameas/i, // passwordSameAsMail
867
+ /^forgot/i, // forgotPassword
868
+ /^confirm.*reset/i, // confirmResetPassword
869
+ ];
870
+
871
+ return errorPatterns.some(pattern => pattern.test(name));
872
+ }
873
+
874
+ /**
875
+ * Check if name is a utility function or class name
876
+ * Examples: generatePassword, IsPassword, validateToken, encodeJwt
877
+ */
878
+ isUtilityFunctionOrClass(name) {
879
+ // Utility function prefixes
880
+ const utilityPrefixes = [
881
+ 'generate', 'create', 'build', 'make',
882
+ 'validate', 'verify', 'check',
883
+ 'encode', 'decode', 'encrypt', 'decrypt', 'hash',
884
+ 'extract', 'parse', 'format',
885
+ 'get', 'set', 'update', 'delete',
886
+ ];
887
+
888
+ // React/Vue hooks and handlers
889
+ const frameworkPrefixes = [
890
+ 'use', // React hooks: useToken, usePassword, useTokenExpiration
891
+ 'handle', // Event handlers: handleResetPassword, handleTokenChange
892
+ 'on', // Event handlers: onPasswordChange, onTokenSubmit
893
+ ];
894
+
895
+ // Check if it's a PascalCase class/decorator (starts with uppercase)
896
+ if (/^[A-Z]/.test(name)) {
897
+ // Decorator/Class patterns: IsPassword, ValidateToken
898
+ return true;
899
+ }
900
+
901
+ // Check if it has utility prefix
902
+ const nameLower = name.toLowerCase();
903
+ const allPrefixes = [...utilityPrefixes, ...frameworkPrefixes];
904
+ return allPrefixes.some(prefix => nameLower.startsWith(prefix));
905
+ }
906
+
907
+ /**
908
+ * Check if name is a framework-specific internal variable
909
+ * Examples: __next_navigation_guard_token, __webpack_*, __vite_*
910
+ */
911
+ isFrameworkInternalVariable(name) {
912
+ const internalPatterns = [
913
+ /^__next_/i, // Next.js internals: __next_navigation_guard_token
914
+ /^__webpack_/i, // Webpack internals
915
+ /^__vite_/i, // Vite internals
916
+ /^__nuxt_/i, // Nuxt internals
917
+ /^_app/i, // Framework app internals
918
+ /^_document/i, // Framework document internals
919
+ ];
920
+
921
+ return internalPatterns.some(pattern => pattern.test(name));
922
+ }
923
+
924
+ /**
925
+ * Check if variable is accessing framework internal property
926
+ * Examples: nextState?.__next_navigation_guard_token
927
+ */
928
+ isAccessingFrameworkInternal(initText) {
929
+ const frameworkPropertyPatterns = [
930
+ /\.__next_/i, // nextState.__next_navigation_guard_token
931
+ /\??\.__next_/i, // nextState?.__next_navigation_guard_token
932
+ /\['__next_/i, // state['__next_navigation_guard_token']
933
+ /\["__next_/i, // state["__next_navigation_guard_token"]
934
+ ];
935
+
936
+ return frameworkPropertyPatterns.some(pattern => pattern.test(initText));
937
+ }
938
+
939
+ /**
940
+ * Check if name is a React/Vue state variable or selector
941
+ * Examples: [token, setToken], receivedToken, selectPasswordState
942
+ */
943
+ isStateVariableOrSelector(name) {
944
+ // React state patterns
945
+ const statePatterns = [
946
+ /^\[.*,\s*set/i, // [token, setToken], [password, setPassword]
947
+ /^set[A-Z]/, // setToken, setPassword
948
+ /^received[A-Z]/i, // receivedToken, receivedPassword
949
+ /^select.*state$/i, // selectTokenState, selectPasswordState
950
+ /state$/i, // tokenState, passwordState (if not part of API)
951
+ ];
952
+
953
+ return statePatterns.some(pattern => pattern.test(name));
954
+ }
955
+
956
+ /**
957
+ * Check if name is a format/pattern/regex definition
958
+ * Examples: passwordFormat, tokenPattern, PASSWORD_REGEX
959
+ */
960
+ isFormatOrPatternDefinition(name) {
961
+ const nameLower = name.toLowerCase();
962
+ const formatSuffixes = [
963
+ 'format', 'pattern', 'regex', 'regexp',
964
+ 'rule', 'rules', 'validation', 'validator',
965
+ 'schema', 'constraint', 'character',
966
+ ];
967
+
968
+ return formatSuffixes.some(suffix => nameLower.endsWith(suffix));
969
+ }
970
+
971
+ /**
972
+ * Check if variable is a URL/endpoint/path string
973
+ * Examples: apiGetTokenUrl = '/api/token', tokenEndpoint = 'https://...'
974
+ */
975
+ isURLOrEndpointVariable(name, initText) {
976
+ const nameLower = name.toLowerCase();
977
+
978
+ // Check variable name patterns
979
+ const urlPatterns = [
980
+ 'url', 'uri', 'endpoint', 'path', 'route', 'api',
981
+ ];
982
+
983
+ const hasUrlPattern = urlPatterns.some(pattern => nameLower.includes(pattern));
984
+
985
+ if (hasUrlPattern) {
986
+ // Check if value looks like a URL/path
987
+ const urlValuePatterns = [
988
+ /^['"`]\//, // Starts with forward slash: '/api/token'
989
+ /^['"`]http/, // Starts with http/https
990
+ /^['"`]\.\//, // Relative path: './token'
991
+ /^['"`]\.\.\//, // Parent path: '../token'
992
+ ];
993
+
994
+ return urlValuePatterns.some(pattern => pattern.test(initText.trim()));
995
+ }
996
+
997
+ return false;
998
+ }
999
+
1000
+ /**
1001
+ * Check if variable is extracting from query parameters
1002
+ * Examples: get(searchParams, 'token'), params.get('token'), get(params, 't', '')
1003
+ */
1004
+ isQueryParameterExtraction(initText) {
1005
+ const queryPatterns = [
1006
+ /get<[^>]+>\(.*?,\s*['"`]\w+['"`].*?\)/, // get<Type, string>(obj, 'key', default)
1007
+ /\.get\(['"`]\w+['"`]\)/, // params.get('token')
1008
+ /searchParams\[['"`]\w+['"`]\]/, // searchParams['token']
1009
+ /query\[['"`]\w+['"`]\]/, // query['token']
1010
+ /req\.query\./, // req.query.token
1011
+ /router\.query\./, // router.query.token
1012
+ /useSearchParams/, // useSearchParams hook
1013
+ ];
1014
+
1015
+ return queryPatterns.some(pattern => pattern.test(initText));
1016
+ }
1017
+
1018
+ /**
1019
+ * Check if this is a property name in a data structure (not a hardcoded value)
1020
+ * Detects patterns like: { accessToken: someVariable } vs { accessToken: "hardcoded-value" }
1021
+ */
1022
+ isPropertyNameOnly(propAssignment) {
1023
+ try {
1024
+ const initializer = propAssignment.getInitializer();
1025
+ if (!initializer) return true; // No value assigned, just property name
1026
+
1027
+ const initText = initializer.getText().trim();
1028
+
1029
+ // If it's a variable reference (not a string literal), it's safe
1030
+ // Examples: accessToken: token, password: userPassword
1031
+ if (!initText.match(/^['"`]/)) {
1032
+ return true;
1033
+ }
1034
+
1035
+ return false;
1036
+ } catch {
1037
+ return false;
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Check if this is part of an error constant object definition
1043
+ * Detects: const errors = { tokenExpired: { code: '...', message: '...' } }
1044
+ */
1045
+ isPartOfErrorObject(node) {
1046
+ try {
1047
+ let parent = node.getParent();
1048
+ let depth = 0;
1049
+ const maxDepth = 5;
1050
+
1051
+ while (parent && depth < maxDepth) {
1052
+ // Check if parent is an object literal with error-like properties
1053
+ if (parent.getKind() === SyntaxKind.ObjectLiteralExpression) {
1054
+ const parentText = parent.getText();
1055
+
1056
+ // Check for error object patterns
1057
+ if (parentText.includes('code:') && parentText.includes('message:')) {
1058
+ return true;
1059
+ }
1060
+
1061
+ // Check for variable name containing 'error' or 'errors'
1062
+ const grandParent = parent.getParent();
1063
+ if (grandParent && grandParent.getKind() === SyntaxKind.VariableDeclaration) {
1064
+ const varName = grandParent.getName ? grandParent.getName() : '';
1065
+ if (/error/i.test(varName)) {
1066
+ return true;
1067
+ }
1068
+ }
1069
+ }
1070
+
1071
+ parent = parent.getParent();
1072
+ depth++;
1073
+ }
1074
+
1075
+ return false;
1076
+ } catch {
1077
+ return false;
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * Check if string literal is part of SQL query
1083
+ * Detects: .addSelect([...]), .select([...]), SQL column aliases
1084
+ */
1085
+ isPartOfSQLQuery(stringLiteral) {
1086
+ try {
1087
+ let parent = stringLiteral.getParent();
1088
+ let depth = 0;
1089
+ const maxDepth = 10;
1090
+
1091
+ while (parent && depth < maxDepth) {
1092
+ const parentText = parent.getText();
1093
+
1094
+ // Check for SQL query method calls
1095
+ const sqlMethods = [
1096
+ '.addSelect(',
1097
+ '.select(',
1098
+ '.where(',
1099
+ '.andWhere(',
1100
+ '.orWhere(',
1101
+ '.orderBy(',
1102
+ '.groupBy(',
1103
+ '.having(',
1104
+ '.leftJoin(',
1105
+ '.innerJoin(',
1106
+ '.rightJoin(',
1107
+ 'createQueryBuilder(',
1108
+ '.from(',
1109
+ '.into(',
1110
+ ];
1111
+
1112
+ if (sqlMethods.some(method => parentText.includes(method))) {
1113
+ return true;
1114
+ }
1115
+
1116
+ // Check if inside array literal passed to SQL methods
1117
+ if (parent.getKind() === SyntaxKind.ArrayLiteralExpression) {
1118
+ const arrayParent = parent.getParent();
1119
+ if (arrayParent) {
1120
+ const arrayParentText = arrayParent.getText();
1121
+ if (sqlMethods.some(method => arrayParentText.includes(method))) {
1122
+ return true;
1123
+ }
1124
+ }
1125
+ }
1126
+
1127
+ // Check for SQL alias pattern: "table.column as alias"
1128
+ const literalValue = stringLiteral.getLiteralValue();
1129
+ if (literalValue && /\s+as\s+["']?\w+["']?/i.test(literalValue)) {
1130
+ return true;
1131
+ }
1132
+
1133
+ parent = parent.getParent();
1134
+ depth++;
1135
+ }
1136
+
1137
+ return false;
1138
+ } catch {
1139
+ return false;
1140
+ }
1141
+ }
1142
+
1143
+ /**
1144
+ * Check if variable is extracting data from API response
1145
+ * Detects: response.data.token, (res.payload as Type).token
1146
+ */
1147
+ isExtractingFromAPIResponse(initText) {
1148
+ const apiResponsePatterns = [
1149
+ /\.data\[['"].*['"]\]\./, // .data['AuthenticationResult'].
1150
+ /\.data\.\w+\./, // .data.AuthenticationResult.
1151
+ /\.payload\s+as\s+\w+\)\.\w+/, // (res.payload as Type).token
1152
+ /response\.\w+/, // response.token, response.AccessToken
1153
+ /result\.\w+/, // result.token, result.RefreshToken
1154
+ /authResult\./, // authResult.data.
1155
+ /loginInCognito\./, // loginInCognito.data.
1156
+ /\.AuthenticationResult\./, // .AuthenticationResult.AccessToken
1157
+ /\.Credentials\./, // .Credentials.AccessKeyId
1158
+ /res\.payload/, // res.payload.token
1159
+ ];
1160
+
1161
+ return apiResponsePatterns.some(pattern => pattern.test(initText));
1162
+ }
1163
+
1164
+ /**
1165
+ * Check if object literal only contains references (no hardcoded values)
1166
+ * Returns true if ALL property values are variables/function calls, not string literals
1167
+ */
1168
+ isObjectLiteralWithOnlyReferences(initializer) {
1169
+ try {
1170
+ if (initializer.getKind() !== SyntaxKind.ObjectLiteralExpression) {
1171
+ return false;
1172
+ }
1173
+
1174
+ const properties = initializer.getProperties();
1175
+ let hasHardcodedString = false;
1176
+
1177
+ for (const prop of properties) {
1178
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
1179
+ const propInit = prop.getInitializer();
1180
+ if (propInit) {
1181
+ const propText = propInit.getText().trim();
1182
+
1183
+ // If it's a string literal (not a variable), mark as having hardcoded string
1184
+ if (propText.match(/^['"`]/) && !propText.match(/^['"`]\s*$/)) {
1185
+ // Check if it's a short descriptive string (likely not a secret)
1186
+ const cleanValue = propText.replace(/^['"`]|['"`]$/g, '');
1187
+ if (cleanValue.length > this.minSecretLength &&
1188
+ !this.isMessageString(propText)) {
1189
+ hasHardcodedString = true;
1190
+ break;
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ }
1196
+
1197
+ return !hasHardcodedString;
1198
+ } catch {
1199
+ return false;
1200
+ }
1201
+ }
1202
+ }
1203
+
1204
+ module.exports = S012SymbolBasedAnalyzer;