@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,805 @@
1
+ /**
2
+ * S006 Symbol-Based Analyzer - No Plaintext Recovery/Activation Codes
3
+ *
4
+ * Uses ts-morph AST analysis to detect plaintext recovery/activation codes
5
+ * in various contexts: API responses, logging, email templates, etc.
6
+ */
7
+
8
+ const { SyntaxKind } = require("ts-morph");
9
+
10
+ class S006SymbolBasedAnalyzer {
11
+ constructor(semanticEngine = null) {
12
+ this.semanticEngine = semanticEngine;
13
+ this.ruleId = "S006";
14
+
15
+ // Sensitive code-related identifiers (security/auth related only)
16
+ this.sensitiveIdentifiers = new Set([
17
+ "activationcode",
18
+ "recoverycode",
19
+ "resetcode",
20
+ "verificationcode",
21
+ "confirmationcode",
22
+ "otp",
23
+ "otpcode",
24
+ "totp",
25
+ "pin",
26
+ "pincode",
27
+ // Removed generic "code" - too many false positives with business codes
28
+ // Only specific security-related codes above
29
+ "secret",
30
+ "password",
31
+ "passphrase",
32
+ ]);
33
+
34
+ // Business/technical identifiers that should NOT be flagged (not security codes)
35
+ this.safeBusinessIdentifiers = new Set([
36
+ "statuscode",
37
+ "httpstatuscode",
38
+ "errorcode",
39
+ "responsecode",
40
+ "agencycode",
41
+ "companycode",
42
+ "eventcode",
43
+ "productcode",
44
+ "itemcode",
45
+ "categorycode",
46
+ "departmentcode",
47
+ "locationcode",
48
+ "branchcode",
49
+ "regioncode",
50
+ "countrycode",
51
+ "languagecode",
52
+ "currencycode",
53
+ "timecode",
54
+ "zipcode",
55
+ "postalcode",
56
+ "areacode",
57
+ "dialcode",
58
+ "apiname",
59
+ "adminapiname",
60
+ "appinstall",
61
+ "externalpointeventid", // External point system event identifier
62
+ "externalserviceid", // External service identifier
63
+ "pointstatus", // User point status (business state, not security code)
64
+ "memberstatus", // User member status
65
+ "friendshipstatus", // User friendship status
66
+ ]);
67
+
68
+ // Safe password-related patterns (template names, route names, not actual passwords)
69
+ this.safePasswordPatterns = [
70
+ "forgotpassword",
71
+ "resetpassword",
72
+ "changepassword",
73
+ "updatepassword",
74
+ "passwordreset",
75
+ "passwordchange",
76
+ "passwordupdate",
77
+ "passwordrecovery",
78
+ ];
79
+
80
+ // Safe token types (JWT, session tokens, etc.)
81
+ this.safeTokenTypes = new Set([
82
+ "resettoken",
83
+ "accesstoken",
84
+ "refreshtoken",
85
+ "sessiontoken",
86
+ "authtoken",
87
+ "jwttoken",
88
+ "bearertoken",
89
+ ]);
90
+
91
+ // Transmission/exposure contexts
92
+ this.exposureContexts = new Set([
93
+ "send",
94
+ "email",
95
+ "sms",
96
+ "text",
97
+ "message",
98
+ "mail",
99
+ "push",
100
+ "notify",
101
+ "response",
102
+ "body",
103
+ "json",
104
+ "data",
105
+ "payload",
106
+ "log",
107
+ "console",
108
+ "debug",
109
+ "info",
110
+ "warn",
111
+ "error",
112
+ "trace",
113
+ "render",
114
+ "template",
115
+ "view",
116
+ "html",
117
+ ]);
118
+
119
+ // Safe patterns to exclude
120
+ this.safePatterns = [
121
+ "hash",
122
+ "encrypt",
123
+ "cipher",
124
+ "bcrypt",
125
+ "crypto",
126
+ "secure",
127
+ "hashed",
128
+ "encrypted",
129
+ "sendsecure",
130
+ "hashcode",
131
+ "encryptcode",
132
+ "savehashed",
133
+ "storehashed",
134
+ "validatecode",
135
+ "verifycode",
136
+ "checkcode",
137
+ // Audit/internal logging (not external exposure)
138
+ "logadminpointhistory",
139
+ "adminpointhistory",
140
+ "auditlog",
141
+ "systemlog",
142
+ "internallog",
143
+ "logger.warn", // Warning logs are typically business messages, not sensitive data exposure
144
+ "logger.debug", // Debug logs are for development, not production exposure
145
+ "logger.info", // Info logs are for general information
146
+ ];
147
+
148
+ // Safe return/response patterns (just success messages, no actual codes)
149
+ this.safeResponsePatterns = [
150
+ "success",
151
+ "message",
152
+ "sent",
153
+ "instructions",
154
+ "check your",
155
+ "please enter",
156
+ "has been sent",
157
+ "successfully sent",
158
+ "sent to your email",
159
+ "sent to email",
160
+ "sent successfully",
161
+ "generated successfully",
162
+ ];
163
+ }
164
+
165
+ async initialize(semanticEngine) {
166
+ this.semanticEngine = semanticEngine;
167
+ }
168
+
169
+ async analyze(sourceFile, filePath) {
170
+ const violations = [];
171
+
172
+ try {
173
+ // Check return statements with sensitive codes
174
+ this.checkReturnStatements(sourceFile, filePath, violations);
175
+
176
+ // Check property assignments in objects
177
+ this.checkObjectLiterals(sourceFile, filePath, violations);
178
+
179
+ // Check call expressions (res.json, console.log, etc.)
180
+ this.checkCallExpressions(sourceFile, filePath, violations);
181
+
182
+ // Check template literals with codes
183
+ this.checkTemplateLiterals(sourceFile, filePath, violations);
184
+
185
+ // Check interface/type definitions
186
+ this.checkTypeDefinitions(sourceFile, filePath, violations);
187
+
188
+ // Removed: checkVariableDeclarations - too many false positives
189
+ // Variables can be used safely in many contexts (hashing, validation, etc.)
190
+ } catch (error) {
191
+ console.warn(`⚠️ [S006] Analysis error in ${filePath}: ${error.message}`);
192
+ }
193
+
194
+ return violations;
195
+ }
196
+
197
+ /**
198
+ * Check return statements that expose sensitive codes
199
+ * e.g., return { activationCode };
200
+ */
201
+ checkReturnStatements(sourceFile, filePath, violations) {
202
+ const returnStatements = sourceFile.getDescendantsOfKind(
203
+ SyntaxKind.ReturnStatement
204
+ );
205
+
206
+ for (const returnStmt of returnStatements) {
207
+ const expression = returnStmt.getExpression();
208
+ if (!expression) continue;
209
+
210
+ // Check if returning object with sensitive properties
211
+ if (expression.getKind() === SyntaxKind.ObjectLiteralExpression) {
212
+ const objLiteral = expression;
213
+ const properties = objLiteral.getProperties();
214
+
215
+ // Skip if return object contains only safe messages (success, message, etc.)
216
+ const hasOnlySafeProperties = properties.every((prop) => {
217
+ const name = prop.getName?.() || "";
218
+ const normalizedName = name.toLowerCase();
219
+ return (
220
+ normalizedName === "success" ||
221
+ normalizedName === "message" ||
222
+ normalizedName === "expiresat" ||
223
+ normalizedName === "timestamp" ||
224
+ normalizedName === "status"
225
+ );
226
+ });
227
+
228
+ if (hasOnlySafeProperties) {
229
+ continue;
230
+ }
231
+
232
+ for (const prop of properties) {
233
+ if (
234
+ prop.getKind() === SyntaxKind.PropertyAssignment ||
235
+ prop.getKind() === SyntaxKind.ShorthandPropertyAssignment
236
+ ) {
237
+ const name = prop.getName?.() || "";
238
+ const normalizedName = this.normalizeIdentifier(name);
239
+
240
+ // Skip statusCode - it's HTTP status code, not sensitive
241
+ if (normalizedName === "statuscode") {
242
+ continue;
243
+ }
244
+
245
+ if (
246
+ this.isSensitiveIdentifier(normalizedName) &&
247
+ !this.isInSafeContext(prop)
248
+ ) {
249
+ violations.push({
250
+ ruleId: this.ruleId,
251
+ severity: "error",
252
+ message: `Returning sensitive code '${name}' in plaintext - codes should be encrypted or excluded from responses`,
253
+ line: prop.getStartLineNumber(),
254
+ column: prop.getStart() - prop.getStartLinePos() + 1,
255
+ filePath: filePath,
256
+ file: filePath,
257
+ });
258
+ }
259
+ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Check object literals for sensitive code exposure
267
+ * e.g., { resetCode: code, ... }
268
+ */
269
+ checkObjectLiterals(sourceFile, filePath, violations) {
270
+ const objectLiterals = sourceFile.getDescendantsOfKind(
271
+ SyntaxKind.ObjectLiteralExpression
272
+ );
273
+
274
+ for (const objLiteral of objectLiterals) {
275
+ // Skip if in safe context
276
+ if (this.isInSafeContext(objLiteral)) continue;
277
+
278
+ const properties = objLiteral.getProperties();
279
+
280
+ for (const prop of properties) {
281
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
282
+ const name = prop.getName?.() || "";
283
+ const normalizedName = this.normalizeIdentifier(name);
284
+
285
+ if (this.isSensitiveIdentifier(normalizedName)) {
286
+ // Check if in exposure context (e.g., res.json, send, etc.)
287
+ const parent = this.findParentContext(objLiteral);
288
+ if (parent && this.isExposureContext(parent)) {
289
+ // Check if this is request input (not exposure)
290
+ // Look for JSON.stringify in parent chain with body/data context
291
+ let currentParent = parent;
292
+ let depth = 0;
293
+ let isRequestBody = false;
294
+
295
+ while (currentParent && depth < 10) {
296
+ const parentText = currentParent.getText().toLowerCase();
297
+
298
+ // Check if this object is being passed to JSON.stringify
299
+ // and is part of a fetch/axios request body
300
+ if (parentText.includes("json.stringify")) {
301
+ // Look further up for body: or data: context
302
+ let upperParent = currentParent.getParent();
303
+ let upperDepth = 0;
304
+
305
+ while (upperParent && upperDepth < 5) {
306
+ const upperText = upperParent.getText().toLowerCase();
307
+ if (
308
+ (upperText.includes("body:") ||
309
+ upperText.includes("data:")) &&
310
+ (upperText.includes("fetch") ||
311
+ upperText.includes("axios") ||
312
+ upperText.includes("method:") ||
313
+ upperText.includes("post") ||
314
+ upperText.includes("put"))
315
+ ) {
316
+ isRequestBody = true;
317
+ break;
318
+ }
319
+ upperParent = upperParent.getParent();
320
+ upperDepth++;
321
+ }
322
+ }
323
+
324
+ if (isRequestBody) break;
325
+ currentParent = currentParent.getParent();
326
+ depth++;
327
+ }
328
+
329
+ if (isRequestBody) {
330
+ // This is request body - user sending code to server for verification
331
+ // Not an exposure, so skip
332
+ continue;
333
+ }
334
+
335
+ // Use warning for JWT config properties (common pattern in DTOs)
336
+ const isJwtConfig =
337
+ normalizedName === "secret" &&
338
+ (objLiteral.getText().includes("expiresIn") ||
339
+ objLiteral.getText().includes("JWT") ||
340
+ objLiteral.getText().includes("process.env"));
341
+
342
+ violations.push({
343
+ ruleId: this.ruleId,
344
+ severity: isJwtConfig ? "warning" : "error",
345
+ message: isJwtConfig
346
+ ? `Object property '${name}' in JWT config - consider refactoring signing logic to service layer`
347
+ : `Object property '${name}' exposes sensitive code in plaintext - use encrypted transmission or exclude from response`,
348
+ line: prop.getStartLineNumber(),
349
+ column: prop.getStart() - prop.getStartLinePos() + 1,
350
+ filePath: filePath,
351
+ file: filePath,
352
+ });
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Check call expressions like res.json(), console.log(), etc.
362
+ * e.g., res.json({ code: verificationCode })
363
+ */
364
+ checkCallExpressions(sourceFile, filePath, violations) {
365
+ const callExpressions = sourceFile.getDescendantsOfKind(
366
+ SyntaxKind.CallExpression
367
+ );
368
+
369
+ for (const callExpr of callExpressions) {
370
+ const expression = callExpr.getExpression();
371
+ const expressionText = expression.getText().toLowerCase();
372
+
373
+ // Skip safe methods
374
+ if (
375
+ this.safePatterns.some((pattern) => expressionText.includes(pattern))
376
+ ) {
377
+ continue;
378
+ }
379
+
380
+ // Check for exposure methods
381
+ const isExposureMethod =
382
+ expressionText.includes("json") ||
383
+ expressionText.includes("send") ||
384
+ expressionText.includes("log") ||
385
+ expressionText.includes("console") ||
386
+ expressionText.includes("email") ||
387
+ expressionText.includes("sms") ||
388
+ expressionText.includes("notify") ||
389
+ expressionText.includes("render");
390
+
391
+ if (!isExposureMethod) continue;
392
+
393
+ // Skip JSON.stringify in request body context (client sending code to server for verification)
394
+ if (expressionText.includes("json.stringify")) {
395
+ // Check if this is part of fetch/axios request body
396
+ let parent = callExpr.getParent();
397
+ let depth = 0;
398
+ let isRequestBody = false;
399
+
400
+ while (parent && depth < 10) {
401
+ const parentText = parent.getText().toLowerCase();
402
+ if (
403
+ (parentText.includes("body:") || parentText.includes("data:")) &&
404
+ (parentText.includes("fetch") ||
405
+ parentText.includes("axios") ||
406
+ parentText.includes("method:") ||
407
+ parentText.includes("post"))
408
+ ) {
409
+ isRequestBody = true;
410
+ break;
411
+ }
412
+ parent = parent.getParent();
413
+ depth++;
414
+ }
415
+
416
+ if (isRequestBody) {
417
+ continue; // Skip - this is client sending code for verification, not exposure
418
+ }
419
+ }
420
+
421
+ // Check arguments for sensitive codes
422
+ const args = callExpr.getArguments();
423
+ for (const arg of args) {
424
+ // Skip if argument is in safe context
425
+ if (this.isInSafeContext(arg)) {
426
+ continue;
427
+ }
428
+
429
+ // Skip safe string messages
430
+ if (
431
+ arg.getKind() === SyntaxKind.StringLiteral ||
432
+ arg.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral
433
+ ) {
434
+ const text = arg.getText().toLowerCase();
435
+ if (
436
+ this.safeResponsePatterns.some((pattern) => text.includes(pattern))
437
+ ) {
438
+ continue;
439
+ }
440
+ }
441
+
442
+ if (this.containsSensitiveCode(arg)) {
443
+ const argText = arg.getText();
444
+ const match = argText.match(
445
+ /\b(activation|recovery|reset|verification|otp|code)\w*/i
446
+ );
447
+ const codeName = match ? match[0] : "sensitive code";
448
+
449
+ // Check if this is OTP/SMS transmission (accepted functional requirement)
450
+ const isOtpSms =
451
+ (expressionText.includes("sendmessage") ||
452
+ expressionText.includes("sendsms") ||
453
+ expressionText.includes("sendotp")) &&
454
+ (codeName.toLowerCase().includes("otp") ||
455
+ argText.toLowerCase().includes("otp"));
456
+
457
+ violations.push({
458
+ ruleId: this.ruleId,
459
+ severity: isOtpSms ? "warning" : "error",
460
+ message: isOtpSms
461
+ ? `OTP transmission via ${expressionText}() - ensure secure channel and proper expiration/rate limiting`
462
+ : `Exposing '${codeName}' via ${expressionText}() - codes should not be transmitted in plaintext`,
463
+ line: arg.getStartLineNumber(),
464
+ column: arg.getStart() - arg.getStartLinePos() + 1,
465
+ filePath: filePath,
466
+ file: filePath,
467
+ });
468
+ }
469
+ }
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Check template literals for code exposure
475
+ * e.g., `Your code is: ${activationCode}`
476
+ */
477
+ checkTemplateLiterals(sourceFile, filePath, violations) {
478
+ const templates = sourceFile.getDescendantsOfKind(
479
+ SyntaxKind.TemplateExpression
480
+ );
481
+
482
+ for (const template of templates) {
483
+ const spans = template.getTemplateSpans();
484
+
485
+ for (const span of spans) {
486
+ const expression = span.getExpression();
487
+ const expressionText = expression.getText().toLowerCase();
488
+ const normalizedExpr = this.normalizeIdentifier(expressionText);
489
+
490
+ // Skip JWT signing operations - these produce signed tokens (safe), not plaintext secrets
491
+ if (
492
+ expressionText.includes("jwtsecret") ||
493
+ expressionText.includes("jwtsign") ||
494
+ expressionText.includes("tokensign") ||
495
+ expressionText.includes("sign(") ||
496
+ expressionText.includes(".sign(")
497
+ ) {
498
+ continue;
499
+ }
500
+
501
+ if (this.isSensitiveIdentifier(normalizedExpr)) {
502
+ // Check if in exposure context
503
+ const parent = this.findParentContext(template);
504
+ if (parent && this.isExposureContext(parent)) {
505
+ violations.push({
506
+ ruleId: this.ruleId,
507
+ severity: "error",
508
+ message: `Template literal exposes sensitive code '${expression.getText()}' - avoid including codes in templates`,
509
+ line: expression.getStartLineNumber(),
510
+ column: expression.getStart() - expression.getStartLinePos() + 1,
511
+ filePath: filePath,
512
+ file: filePath,
513
+ });
514
+ }
515
+ }
516
+ }
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Check interface/type definitions
522
+ * e.g., interface Response { verificationCode: string }
523
+ */
524
+ checkTypeDefinitions(sourceFile, filePath, violations) {
525
+ const interfaces = sourceFile.getDescendantsOfKind(
526
+ SyntaxKind.InterfaceDeclaration
527
+ );
528
+
529
+ for (const iface of interfaces) {
530
+ const name = iface.getName();
531
+ const normalizedName = name.toLowerCase();
532
+
533
+ // Check if it's a response/DTO type
534
+ if (
535
+ normalizedName.includes("response") ||
536
+ normalizedName.includes("result") ||
537
+ normalizedName.includes("dto") ||
538
+ normalizedName.includes("payload")
539
+ ) {
540
+ const properties = iface.getProperties();
541
+
542
+ for (const prop of properties) {
543
+ const propName = prop.getName();
544
+ const normalizedPropName = this.normalizeIdentifier(propName);
545
+
546
+ if (this.isSensitiveIdentifier(normalizedPropName)) {
547
+ violations.push({
548
+ ruleId: this.ruleId,
549
+ severity: "warning",
550
+ message: `Interface '${name}' exposes sensitive property '${propName}' - consider excluding codes from response types`,
551
+ line: prop.getStartLineNumber(),
552
+ column: prop.getStart() - prop.getStartLinePos() + 1,
553
+ filePath: filePath,
554
+ file: filePath,
555
+ });
556
+ }
557
+ }
558
+ }
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Check variable declarations for sensitive code handling
564
+ */
565
+ checkVariableDeclarations(sourceFile, filePath, violations) {
566
+ const varDecls = sourceFile.getDescendantsOfKind(
567
+ SyntaxKind.VariableDeclaration
568
+ );
569
+
570
+ for (const varDecl of varDecls) {
571
+ const name = varDecl.getName();
572
+ const normalizedName = this.normalizeIdentifier(name);
573
+
574
+ if (!this.isSensitiveIdentifier(normalizedName)) continue;
575
+
576
+ const initializer = varDecl.getInitializer();
577
+ if (!initializer) continue;
578
+
579
+ // Check if variable is used in exposure context
580
+ const varStatement = varDecl.getFirstAncestorByKind(
581
+ SyntaxKind.VariableStatement
582
+ );
583
+ if (!varStatement) continue;
584
+
585
+ const scope = varStatement.getParent();
586
+ if (!scope) continue;
587
+
588
+ // Look for usage in exposure contexts
589
+ const identifiers = scope.getDescendantsOfKind(SyntaxKind.Identifier);
590
+ for (const identifier of identifiers) {
591
+ if (identifier.getText() === name) {
592
+ const parent = identifier.getParent();
593
+ if (parent && this.isExposureContext(parent)) {
594
+ violations.push({
595
+ ruleId: this.ruleId,
596
+ severity: "warning",
597
+ message: `Variable '${name}' containing sensitive code is used in exposure context - ensure proper encryption`,
598
+ line: identifier.getStartLineNumber(),
599
+ column: identifier.getStart() - identifier.getStartLinePos() + 1,
600
+ filePath: filePath,
601
+ file: filePath,
602
+ });
603
+ break; // Only report once per variable
604
+ }
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Helper: Normalize identifier by removing non-alphanumeric and lowercase
612
+ */
613
+ normalizeIdentifier(name) {
614
+ return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
615
+ }
616
+
617
+ /**
618
+ * Helper: Check if identifier is sensitive
619
+ */
620
+ isSensitiveIdentifier(normalizedName) {
621
+ // First check if it's a safe token type (JWT, session token, etc.)
622
+ if (this.safeTokenTypes.has(normalizedName)) {
623
+ return false;
624
+ }
625
+
626
+ // Check if it's a business/technical identifier (not security code)
627
+ if (this.safeBusinessIdentifiers.has(normalizedName)) {
628
+ return false;
629
+ }
630
+
631
+ // Check if it's a safe password pattern (template/route names, not actual passwords)
632
+ if (
633
+ this.safePasswordPatterns.some((pattern) =>
634
+ normalizedName.includes(pattern)
635
+ )
636
+ ) {
637
+ return false;
638
+ }
639
+
640
+ // Check if it ends with "token" (resetToken, authToken, etc. are secure JWT tokens)
641
+ if (normalizedName.endsWith("token")) {
642
+ return false;
643
+ }
644
+
645
+ // Check if it's just generic "code" without security context
646
+ // Only flag if it's specifically security-related
647
+ if (normalizedName === "code") {
648
+ // Check if it has security context in variable name
649
+ // e.g., "verificationCode", "otpCode" are sensitive
650
+ // but "errorCode", "statusCode" are not
651
+ return false; // Generic "code" alone is not sensitive
652
+ }
653
+
654
+ // Skip if it's "password" alone in object key context (likely config/route name)
655
+ if (normalizedName === "password") {
656
+ return false; // Will be caught by containsSensitiveCode if it's actual password value
657
+ }
658
+
659
+ return (
660
+ this.sensitiveIdentifiers.has(normalizedName) ||
661
+ Array.from(this.sensitiveIdentifiers).some((sensitive) =>
662
+ normalizedName.includes(sensitive)
663
+ )
664
+ );
665
+ }
666
+
667
+ /**
668
+ * Helper: Check if node is in safe context (hashed, encrypted, etc.)
669
+ */
670
+ isInSafeContext(node) {
671
+ // Check the node itself
672
+ const nodeText = node.getText().toLowerCase();
673
+ if (this.safePatterns.some((pattern) => nodeText.includes(pattern))) {
674
+ return true;
675
+ }
676
+
677
+ // Check if it's part of a safe message response (success: true, message: "...")
678
+ if (
679
+ this.safeResponsePatterns.some((pattern) => nodeText.includes(pattern))
680
+ ) {
681
+ return true;
682
+ }
683
+
684
+ // Check parent context
685
+ let current = node.getParent();
686
+ let depth = 0;
687
+
688
+ while (current && depth < 5) {
689
+ const text = current.getText().toLowerCase();
690
+
691
+ // Check for safe patterns
692
+ if (this.safePatterns.some((pattern) => text.includes(pattern))) {
693
+ return true;
694
+ }
695
+
696
+ // Check if parent is a variable with "hashed" or "encrypted" in name
697
+ if (current.getKind() === SyntaxKind.VariableDeclaration) {
698
+ const varName = current.getName?.() || "";
699
+ const normalizedVarName = varName.toLowerCase();
700
+ if (
701
+ normalizedVarName.includes("hashed") ||
702
+ normalizedVarName.includes("encrypted") ||
703
+ normalizedVarName.includes("secure") ||
704
+ normalizedVarName.includes("token")
705
+ ) {
706
+ return true;
707
+ }
708
+ }
709
+
710
+ // Check if parent is a safe function call (e.g., sendSecure, hash, encrypt)
711
+ if (current.getKind() === SyntaxKind.CallExpression) {
712
+ const callExpr = current;
713
+ const expression = callExpr.getExpression();
714
+ const exprText = expression.getText().toLowerCase();
715
+ if (this.safePatterns.some((pattern) => exprText.includes(pattern))) {
716
+ return true;
717
+ }
718
+ }
719
+
720
+ current = current.getParent();
721
+ depth++;
722
+ }
723
+
724
+ return false;
725
+ }
726
+
727
+ /**
728
+ * Helper: Find parent context (call expression, return, etc.)
729
+ */
730
+ findParentContext(node) {
731
+ let current = node.getParent();
732
+ let depth = 0;
733
+
734
+ while (current && depth < 10) {
735
+ const kind = current.getKind();
736
+ if (
737
+ kind === SyntaxKind.CallExpression ||
738
+ kind === SyntaxKind.ReturnStatement ||
739
+ kind === SyntaxKind.VariableDeclaration
740
+ ) {
741
+ return current;
742
+ }
743
+ current = current.getParent();
744
+ depth++;
745
+ }
746
+
747
+ return null;
748
+ }
749
+
750
+ /**
751
+ * Helper: Check if parent context is exposure context
752
+ */
753
+ isExposureContext(node) {
754
+ const text = node.getText().toLowerCase();
755
+ return Array.from(this.exposureContexts).some((context) =>
756
+ text.includes(context)
757
+ );
758
+ }
759
+
760
+ /**
761
+ * Helper: Check if argument contains sensitive code
762
+ */
763
+ containsSensitiveCode(node) {
764
+ const text = node.getText().toLowerCase();
765
+ const normalized = this.normalizeIdentifier(text);
766
+
767
+ // Check for sensitive identifiers
768
+ if (this.isSensitiveIdentifier(normalized)) {
769
+ return true;
770
+ }
771
+
772
+ // Check child nodes (for object literals, etc.)
773
+ if (node.getKind() === SyntaxKind.ObjectLiteralExpression) {
774
+ const properties = node.getProperties();
775
+ for (const prop of properties) {
776
+ const propName = prop.getName?.() || "";
777
+ const normalizedPropName = this.normalizeIdentifier(propName);
778
+ if (this.isSensitiveIdentifier(normalizedPropName)) {
779
+ return true;
780
+ }
781
+ }
782
+ }
783
+
784
+ // Check template spans
785
+ if (node.getKind() === SyntaxKind.TemplateExpression) {
786
+ const spans = node.getTemplateSpans();
787
+ for (const span of spans) {
788
+ const expr = span.getExpression();
789
+ const exprText = expr.getText().toLowerCase();
790
+ const normalizedExpr = this.normalizeIdentifier(exprText);
791
+ if (this.isSensitiveIdentifier(normalizedExpr)) {
792
+ return true;
793
+ }
794
+ }
795
+ }
796
+
797
+ return false;
798
+ }
799
+
800
+ cleanup() {
801
+ // No cleanup needed
802
+ }
803
+ }
804
+
805
+ module.exports = S006SymbolBasedAnalyzer;