@sun-asterisk/sunlint 1.3.18 → 1.3.20

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 (35) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/cli-program.js +9 -1
  3. package/core/github-annotate-service.js +986 -0
  4. package/core/output-service.js +294 -6
  5. package/core/summary-report-service.js +30 -30
  6. package/docs/GITHUB_ACTIONS_INTEGRATION.md +421 -0
  7. package/package.json +2 -1
  8. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  9. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  10. package/rules/common/C017_constructor_logic/config.json +50 -0
  11. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  12. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  13. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  14. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  15. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  16. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  17. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  18. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  19. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  20. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  21. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  22. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  23. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  24. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  25. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  26. package/rules/security/S044_re_authentication_required/README.md +136 -0
  27. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  28. package/rules/security/S044_re_authentication_required/config.json +161 -0
  29. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  30. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  31. package/rules/security/S045_brute_force_protection/README.md +345 -0
  32. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  33. package/rules/security/S045_brute_force_protection/config.json +139 -0
  34. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  35. package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
@@ -63,9 +63,56 @@ class S006SymbolBasedAnalyzer {
63
63
  "pointstatus", // User point status (business state, not security code)
64
64
  "memberstatus", // User member status
65
65
  "friendshipstatus", // User friendship status
66
+ "groupinvoicedetailid", // Invoice detail identifier (business ID, not security code)
67
+ "invoiceid", // Invoice identifier
68
+ "orderid", // Order identifier
69
+ "detailid", // Detail record identifier
70
+ "transactionid", // Transaction identifier
71
+ "paymentid", // Payment identifier
72
+ "subscriptionid", // Subscription identifier
73
+ "customerid", // Customer identifier
74
+ "userid", // User identifier (not sensitive in business context)
75
+ "accountid", // Account identifier
76
+ // API/Endpoint related (not sensitive codes)
77
+ "generateotpapi", // API endpoint name
78
+ "adduserbyemailnopasswordapi", // API endpoint name
79
+ "setpasswordurl", // URL configuration
80
+ "responseotp", // Response field name in config
81
+ "apidocsauthbasicpassword", // API docs authentication (config)
82
+ "basicauthpassword", // Basic auth config (not actual password)
83
+ // Time/interval configurations (not sensitive codes)
84
+ "dedupinginterval", // SWR deduping interval
85
+ "refreshinterval", // Refresh interval
86
+ "retryinterval", // Retry interval
87
+ "pollinginterval", // Polling interval
88
+ "timeoutinterval", // Timeout interval
89
+ // Message/validation constants (not actual codes/passwords)
90
+ "invalidpassword", // Error message constant
91
+ "incorrectpassword", // Error message constant
92
+ "incorrectcurrentpassword", // Error message constant
93
+ "confirmationpasswordnotmatch", // Error message constant
94
+ // Form input fields (user input, not server response with actual codes)
95
+ // Note: These are commonly used as field names in registration/verification forms
96
+ // The actual verification should happen server-side with hashed codes
97
+ // Database metadata fields (counters, flags, not sensitive data)
98
+ "failedpasswordattempts", // Password attempt counter
99
+ "failedloginattempts", // Login attempt counter
100
+ "passwordattempts", // Attempt counter
101
+ "loginattempts", // Attempt counter
102
+ // Swagger/Validation schema properties (not actual data)
103
+ "ispassword", // Swagger schema validation flag
104
+ "isotp", // Swagger schema validation flag
105
+ "isotpcode", // Swagger schema validation flag
106
+ "isactivationcode", // Swagger schema validation flag
107
+ "isverificationcode", // Swagger schema validation flag
108
+ // Validation constants
109
+ "password_min_length", // Validation constant
110
+ "password_max_length", // Validation constant
111
+ "otp_length", // Validation constant
112
+ "code_length", // Validation constant
66
113
  ]);
67
114
 
68
- // Safe password-related patterns (template names, route names, not actual passwords)
115
+ // Safe password-related patterns (template names, route names, form fields, not actual passwords)
69
116
  this.safePasswordPatterns = [
70
117
  "forgotpassword",
71
118
  "resetpassword",
@@ -75,6 +122,12 @@ class S006SymbolBasedAnalyzer {
75
122
  "passwordchange",
76
123
  "passwordupdate",
77
124
  "passwordrecovery",
125
+ "registerpassword", // Registration route/form
126
+ "currentpassword", // Form field for current password
127
+ "newpassword", // Form field for new password
128
+ "confirmpassword", // Form field for password confirmation
129
+ "confirmationpassword", // Form field for password confirmation
130
+ "passwordconfirm", // Form field for password confirmation
78
131
  ];
79
132
 
80
133
  // Safe token types (JWT, session tokens, etc.)
@@ -134,6 +187,16 @@ class S006SymbolBasedAnalyzer {
134
187
  "validatecode",
135
188
  "verifycode",
136
189
  "checkcode",
190
+ // Encoding operations (base64, etc.)
191
+ "btoa", // Base64 encoding (often used with env vars for Basic Auth)
192
+ "atob", // Base64 decoding
193
+ "base64",
194
+ // Parsing operations (input processing, not output exposure)
195
+ "json.parse",
196
+ "json.stringify",
197
+ "parse(",
198
+ "parseint",
199
+ "parsefloat",
137
200
  // Audit/internal logging (not external exposure)
138
201
  "logadminpointhistory",
139
202
  "adminpointhistory",
@@ -143,6 +206,15 @@ class S006SymbolBasedAnalyzer {
143
206
  "logger.warn", // Warning logs are typically business messages, not sensitive data exposure
144
207
  "logger.debug", // Debug logs are for development, not production exposure
145
208
  "logger.info", // Info logs are for general information
209
+ "logger.error", // Error logs are internal, not user-facing
210
+ // Configuration and endpoint definitions
211
+ "config.",
212
+ "endpoint.",
213
+ "_url",
214
+ "_api",
215
+ "_endpoint",
216
+ // Environment variables (should be managed separately)
217
+ "process.env",
146
218
  ];
147
219
 
148
220
  // Safe return/response patterns (just success messages, no actual codes)
@@ -170,6 +242,11 @@ class S006SymbolBasedAnalyzer {
170
242
  const violations = [];
171
243
 
172
244
  try {
245
+ // Skip configuration files - they contain config structures, not actual sensitive data
246
+ if (this.isConfigFile(filePath)) {
247
+ return violations; // Config files are safe
248
+ }
249
+
173
250
  // Check return statements with sensitive codes
174
251
  this.checkReturnStatements(sourceFile, filePath, violations);
175
252
 
@@ -194,6 +271,32 @@ class S006SymbolBasedAnalyzer {
194
271
  return violations;
195
272
  }
196
273
 
274
+ /**
275
+ * Check if file is a configuration file or Swagger schema file
276
+ */
277
+ isConfigFile(filePath) {
278
+ const configPatterns = [
279
+ "/config/",
280
+ "/configs/",
281
+ "/configuration/",
282
+ "config.ts",
283
+ "config.js",
284
+ ".config.ts",
285
+ ".config.js",
286
+ "Config.ts",
287
+ "Config.js",
288
+ // Swagger/OpenAPI schema files - contain validation schema, not actual data
289
+ "/swagger/",
290
+ ".swagger.ts",
291
+ ".swagger.js",
292
+ "swagger.config",
293
+ "/openapi/",
294
+ ".openapi.ts",
295
+ ".openapi.js",
296
+ ];
297
+ return configPatterns.some((pattern) => filePath.includes(pattern));
298
+ }
299
+
197
300
  /**
198
301
  * Check return statements that expose sensitive codes
199
302
  * e.g., return { activationCode };
@@ -212,6 +315,22 @@ class S006SymbolBasedAnalyzer {
212
315
  const objLiteral = expression;
213
316
  const properties = objLiteral.getProperties();
214
317
 
318
+ // Skip if in infrastructure config builder function
319
+ const parentFunc = this.findParentFunctionName(returnStmt);
320
+ if (parentFunc) {
321
+ const funcNameLower = parentFunc.toLowerCase();
322
+ if (
323
+ funcNameLower.includes("buildenv") ||
324
+ funcNameLower.includes("getenv") ||
325
+ funcNameLower.includes("loadenv") ||
326
+ funcNameLower.includes("config") ||
327
+ funcNameLower.includes("setup") ||
328
+ funcNameLower === "env"
329
+ ) {
330
+ continue; // Infrastructure config builders are safe
331
+ }
332
+ }
333
+
215
334
  // Skip if return object contains only safe messages (success, message, etc.)
216
335
  const hasOnlySafeProperties = properties.every((prop) => {
217
336
  const name = prop.getName?.() || "";
@@ -283,6 +402,41 @@ class S006SymbolBasedAnalyzer {
283
402
  const normalizedName = this.normalizeIdentifier(name);
284
403
 
285
404
  if (this.isSensitiveIdentifier(normalizedName)) {
405
+ // Skip error constants, configuration objects, and form states
406
+ const parentVarName = this.findParentVariableName(objLiteral);
407
+ if (
408
+ parentVarName &&
409
+ (parentVarName.includes("ERROR") ||
410
+ parentVarName.includes("ERRORS") ||
411
+ parentVarName.includes("MESSAGE") ||
412
+ parentVarName.includes("MESSAGES") ||
413
+ parentVarName.includes("MAPPING") ||
414
+ parentVarName.includes("FIELDS") ||
415
+ parentVarName.includes("CONFIG") ||
416
+ parentVarName.includes("CONSTANT") ||
417
+ parentVarName.includes("ENDPOINT") ||
418
+ parentVarName.includes("API") ||
419
+ parentVarName.includes("URL") ||
420
+ parentVarName.includes("ROUTE") ||
421
+ parentVarName.includes("ROUTES") ||
422
+ parentVarName.includes("INITIAL") ||
423
+ parentVarName.includes("DEFAULT") ||
424
+ parentVarName.includes("STATE") ||
425
+ parentVarName.includes("FORM"))
426
+ ) {
427
+ continue; // Error constants, configs, routes, and form states are safe
428
+ }
429
+
430
+ // Skip URL/endpoint configurations (e.g., set_password_url, generate_otp_api)
431
+ if (
432
+ normalizedName.endsWith("url") ||
433
+ normalizedName.endsWith("api") ||
434
+ normalizedName.endsWith("endpoint") ||
435
+ normalizedName.includes("apiname")
436
+ ) {
437
+ continue; // URL/API endpoint configurations are not sensitive data
438
+ }
439
+
286
440
  // Check if in exposure context (e.g., res.json, send, etc.)
287
441
  const parent = this.findParentContext(objLiteral);
288
442
  if (parent && this.isExposureContext(parent)) {
@@ -291,10 +445,75 @@ class S006SymbolBasedAnalyzer {
291
445
  let currentParent = parent;
292
446
  let depth = 0;
293
447
  let isRequestBody = false;
448
+ let isFormState = false;
449
+ let isStateDispatch = false;
450
+ let isServiceCall = false;
294
451
 
295
452
  while (currentParent && depth < 10) {
296
453
  const parentText = currentParent.getText().toLowerCase();
297
454
 
455
+ // Check if this is a service/API call (client sending data to server)
456
+ // Method calls: AuthService.verify(), apiClient.post()
457
+ if (
458
+ parentText.includes("service.") ||
459
+ parentText.includes("api.") ||
460
+ parentText.includes("client.") ||
461
+ parentText.match(/\w+service\./i)
462
+ ) {
463
+ isServiceCall = true;
464
+ break;
465
+ }
466
+
467
+ // Function calls for authentication/verification (client to server)
468
+ // e.g., confirmResetPassword(), verifyOtp(), loginUser(), registerUser()
469
+ if (currentParent.getKind() === SyntaxKind.CallExpression) {
470
+ const callExpr = currentParent;
471
+ const expression = callExpr.getExpression();
472
+ const funcName = expression.getText().toLowerCase();
473
+
474
+ // Check if function name indicates API call
475
+ if (
476
+ funcName.includes("verify") ||
477
+ funcName.includes("confirm") ||
478
+ funcName.includes("reset") ||
479
+ funcName.includes("login") ||
480
+ funcName.includes("signup") ||
481
+ funcName.includes("register") ||
482
+ funcName.includes("authenticate") ||
483
+ funcName.includes("validate") ||
484
+ funcName.includes("check")
485
+ ) {
486
+ isServiceCall = true;
487
+ break;
488
+ }
489
+ }
490
+
491
+ // Check if this is state management dispatch
492
+ // e.g., dispatch(setRegisterInfo({})), setState({})
493
+ if (
494
+ parentText.includes("dispatch(") ||
495
+ parentText.includes("setstate") ||
496
+ parentText.includes("updatestate") ||
497
+ parentText.includes("setregister") ||
498
+ parentText.includes("setuser") ||
499
+ parentText.includes("setform")
500
+ ) {
501
+ isStateDispatch = true;
502
+ break;
503
+ }
504
+
505
+ // Check if this is form state (useState, useForm, etc.)
506
+ if (
507
+ parentText.includes("usestate") ||
508
+ parentText.includes("useform") ||
509
+ parentText.includes("initialstate") ||
510
+ parentText.includes("defaultvalues") ||
511
+ parentText.includes("formdata")
512
+ ) {
513
+ isFormState = true;
514
+ break;
515
+ }
516
+
298
517
  // Check if this object is being passed to JSON.stringify
299
518
  // and is part of a fetch/axios request body
300
519
  if (parentText.includes("json.stringify")) {
@@ -321,13 +540,24 @@ class S006SymbolBasedAnalyzer {
321
540
  }
322
541
  }
323
542
 
324
- if (isRequestBody) break;
543
+ if (
544
+ isRequestBody ||
545
+ isFormState ||
546
+ isStateDispatch ||
547
+ isServiceCall
548
+ )
549
+ break;
325
550
  currentParent = currentParent.getParent();
326
551
  depth++;
327
552
  }
328
553
 
329
- if (isRequestBody) {
330
- // This is request body - user sending code to server for verification
554
+ if (
555
+ isRequestBody ||
556
+ isFormState ||
557
+ isStateDispatch ||
558
+ isServiceCall
559
+ ) {
560
+ // This is request body, form state, state dispatch, or service call - user input data
331
561
  // Not an exposure, so skip
332
562
  continue;
333
563
  }
@@ -370,23 +600,69 @@ class S006SymbolBasedAnalyzer {
370
600
  const expression = callExpr.getExpression();
371
601
  const expressionText = expression.getText().toLowerCase();
372
602
 
373
- // Skip safe methods
603
+ // Skip safe methods (parsing, configuration, internal logging)
374
604
  if (
375
605
  this.safePatterns.some((pattern) => expressionText.includes(pattern))
376
606
  ) {
377
607
  continue;
378
608
  }
379
609
 
380
- // Check for exposure methods
610
+ // Skip parsing operations (JSON.parse, parseInt, etc.) - these are INPUT processing, not OUTPUT exposure
611
+ if (
612
+ expressionText.includes("parse") ||
613
+ expressionText.includes("json.stringify") ||
614
+ expressionText.includes(".map(") ||
615
+ expressionText.includes(".filter(") ||
616
+ expressionText.includes(".reduce(")
617
+ ) {
618
+ continue; // Data processing/transformation, not exposure
619
+ }
620
+
621
+ // Skip internal logging methods
622
+ if (
623
+ expressionText.includes("logger.") ||
624
+ expressionText.includes("httplog") ||
625
+ expressionText.includes("requestlog") ||
626
+ expressionText.includes("console.debug") ||
627
+ expressionText.includes("console.info")
628
+ ) {
629
+ continue; // Internal logging is not external exposure
630
+ }
631
+
632
+ // Skip AWS Cognito admin operations (internal service management, not client exposure)
633
+ if (
634
+ expressionText.includes("cognito") &&
635
+ (expressionText.includes("adminsetuserpassword") ||
636
+ expressionText.includes("admindeleteuser") ||
637
+ expressionText.includes("adminresetuser") ||
638
+ expressionText.includes("deleteuserbyemail") ||
639
+ expressionText.includes("createuser") ||
640
+ expressionText.includes("updateuser"))
641
+ ) {
642
+ continue; // AWS Cognito admin SDK calls - server-side operations, not client exposure
643
+ }
644
+
645
+ // Skip UI rendering functions (not data exposure)
646
+ if (
647
+ expressionText.startsWith("render") &&
648
+ !expressionText.includes("response") &&
649
+ !expressionText.includes("send") &&
650
+ !expressionText.includes("json")
651
+ ) {
652
+ continue; // UI rendering components are safe
653
+ }
654
+
655
+ // Check for exposure methods (only actual external transmission)
381
656
  const isExposureMethod =
382
- expressionText.includes("json") ||
383
- expressionText.includes("send") ||
384
- expressionText.includes("log") ||
385
- expressionText.includes("console") ||
657
+ expressionText.includes("res.json") ||
658
+ expressionText.includes("res.send") ||
659
+ expressionText.includes("response.json") ||
660
+ expressionText.includes("response.send") ||
386
661
  expressionText.includes("email") ||
387
662
  expressionText.includes("sms") ||
388
663
  expressionText.includes("notify") ||
389
- expressionText.includes("render");
664
+ expressionText.includes("ws.send") ||
665
+ expressionText.includes("websocket.send");
390
666
 
391
667
  if (!isExposureMethod) continue;
392
668
 
@@ -440,6 +716,7 @@ class S006SymbolBasedAnalyzer {
440
716
  }
441
717
 
442
718
  if (this.containsSensitiveCode(arg)) {
719
+ // For other methods, use general check
443
720
  const argText = arg.getText();
444
721
  const match = argText.match(
445
722
  /\b(activation|recovery|reset|verification|otp|code)\w*/i
@@ -498,10 +775,81 @@ class S006SymbolBasedAnalyzer {
498
775
  continue;
499
776
  }
500
777
 
778
+ // Skip safe patterns (btoa, process.env, etc.)
779
+ if (
780
+ this.safePatterns.some((pattern) => expressionText.includes(pattern))
781
+ ) {
782
+ continue; // Safe encoding/config patterns
783
+ }
784
+
785
+ // Skip metadata access (.name, .length, .type, etc.) - these are not sensitive data
786
+ if (
787
+ expressionText.includes(".name") ||
788
+ expressionText.includes(".length") ||
789
+ expressionText.includes(".type") ||
790
+ expressionText.includes(".id") ||
791
+ expressionText.includes(".status") ||
792
+ expressionText.includes(".method")
793
+ ) {
794
+ continue; // Metadata properties are not sensitive codes
795
+ }
796
+
797
+ // Skip if this is user input data (registerInfo, formData, etc.)
798
+ if (
799
+ expressionText.includes("registerinfo.") ||
800
+ expressionText.includes("formdata.") ||
801
+ expressionText.includes("formvalues.") ||
802
+ expressionText.includes("inputdata.")
803
+ ) {
804
+ continue; // User input data, not server response
805
+ }
806
+
501
807
  if (this.isSensitiveIdentifier(normalizedExpr)) {
502
808
  // Check if in exposure context
503
809
  const parent = this.findParentContext(template);
504
810
  if (parent && this.isExposureContext(parent)) {
811
+ // Check if this is in fetch/axios body (server-to-server API call)
812
+ let currentParent = parent;
813
+ let depth = 0;
814
+ let isServerToServerCall = false;
815
+
816
+ while (currentParent && depth < 10) {
817
+ const parentText = currentParent.getText().toLowerCase();
818
+
819
+ // Check if this is fetch/axios body for external API
820
+ if (
821
+ (parentText.includes("body:") ||
822
+ parentText.includes("data:")) &&
823
+ (parentText.includes("fetch(") ||
824
+ parentText.includes("axios.") ||
825
+ parentText.includes("http.") ||
826
+ parentText.includes("https.") ||
827
+ parentText.includes("request("))
828
+ ) {
829
+ // Check if calling external API (not internal endpoint)
830
+ if (
831
+ parentText.includes("http://") ||
832
+ parentText.includes("https://") ||
833
+ parentText.includes("api.") ||
834
+ parentText.includes("siteverify") ||
835
+ parentText.includes("oauth") ||
836
+ parentText.includes("recaptcha")
837
+ ) {
838
+ isServerToServerCall = true;
839
+ break;
840
+ }
841
+ }
842
+
843
+ currentParent = currentParent.getParent();
844
+ depth++;
845
+ }
846
+
847
+ if (isServerToServerCall) {
848
+ // This is server-to-server API call (e.g., verify captcha with Google)
849
+ // Secret keys in external API calls are acceptable
850
+ continue;
851
+ }
852
+
505
853
  violations.push({
506
854
  ruleId: this.ruleId,
507
855
  severity: "error",
@@ -530,12 +878,39 @@ class S006SymbolBasedAnalyzer {
530
878
  const name = iface.getName();
531
879
  const normalizedName = name.toLowerCase();
532
880
 
533
- // Check if it's a response/DTO type
881
+ // Skip request/input payloads - these are data sent TO the server (not exposed BY server)
882
+ if (
883
+ normalizedName.includes("request") ||
884
+ normalizedName.includes("input") ||
885
+ normalizedName.includes("update") ||
886
+ normalizedName.includes("create") ||
887
+ normalizedName.includes("submit") ||
888
+ normalizedName.includes("login") ||
889
+ normalizedName.includes("signup") ||
890
+ normalizedName.includes("register") ||
891
+ normalizedName.includes("change") ||
892
+ normalizedName.includes("reset") ||
893
+ normalizedName.includes("forgot") ||
894
+ normalizedName.includes("verify") ||
895
+ (normalizedName.includes("payload") &&
896
+ (normalizedName.includes("update") ||
897
+ normalizedName.includes("create") ||
898
+ normalizedName.includes("input") ||
899
+ normalizedName.includes("request")))
900
+ ) {
901
+ continue; // Input payloads are safe - user sends data to server
902
+ }
903
+
904
+ // Check if it's a response/DTO type (output from server)
534
905
  if (
535
906
  normalizedName.includes("response") ||
536
907
  normalizedName.includes("result") ||
537
- normalizedName.includes("dto") ||
538
- normalizedName.includes("payload")
908
+ normalizedName.includes("output") ||
909
+ (normalizedName.includes("dto") && !normalizedName.includes("input")) ||
910
+ (normalizedName.includes("payload") &&
911
+ (normalizedName.includes("response") ||
912
+ normalizedName.includes("result") ||
913
+ normalizedName.includes("output")))
539
914
  ) {
540
915
  const properties = iface.getProperties();
541
916
 
@@ -656,12 +1031,31 @@ class S006SymbolBasedAnalyzer {
656
1031
  return false; // Will be caught by containsSensitiveCode if it's actual password value
657
1032
  }
658
1033
 
659
- return (
660
- this.sensitiveIdentifiers.has(normalizedName) ||
661
- Array.from(this.sensitiveIdentifiers).some((sensitive) =>
662
- normalizedName.includes(sensitive)
663
- )
664
- );
1034
+ // Exact match first
1035
+ if (this.sensitiveIdentifiers.has(normalizedName)) {
1036
+ return true;
1037
+ }
1038
+
1039
+ // For partial match, be more strict to avoid false positives
1040
+ // Only match if sensitive word is at start or end, or clearly separated
1041
+ return Array.from(this.sensitiveIdentifiers).some((sensitive) => {
1042
+ // Skip very short words (2-3 chars) to avoid matching inside other words
1043
+ // e.g., "pin" should not match "dedupingInterval", "shopping", etc.
1044
+ if (sensitive.length <= 3) {
1045
+ // For short words, only match if:
1046
+ // 1. Exact match (already checked above)
1047
+ // 2. At start: "pin" matches "pincode", "pintester"
1048
+ // 3. At end: "pin" matches "loginpin", "resetpin"
1049
+ // 4. Standalone with separators (camelCase boundaries)
1050
+ return (
1051
+ normalizedName.startsWith(sensitive) ||
1052
+ normalizedName.endsWith(sensitive)
1053
+ );
1054
+ }
1055
+
1056
+ // For longer sensitive words (4+ chars), use includes as before
1057
+ return normalizedName.includes(sensitive);
1058
+ });
665
1059
  }
666
1060
 
667
1061
  /**
@@ -724,6 +1118,54 @@ class S006SymbolBasedAnalyzer {
724
1118
  return false;
725
1119
  }
726
1120
 
1121
+ /**
1122
+ * Helper: Find parent variable name for object literals
1123
+ */
1124
+ findParentVariableName(node) {
1125
+ let current = node.getParent();
1126
+ let depth = 0;
1127
+
1128
+ while (current && depth < 5) {
1129
+ if (current.getKind() === SyntaxKind.VariableDeclaration) {
1130
+ return current.getName?.() || "";
1131
+ }
1132
+ current = current.getParent();
1133
+ depth++;
1134
+ }
1135
+
1136
+ return null;
1137
+ }
1138
+
1139
+ /**
1140
+ * Helper: Find parent function name for config builder detection
1141
+ */
1142
+ findParentFunctionName(node) {
1143
+ let current = node.getParent();
1144
+ let depth = 0;
1145
+
1146
+ while (current && depth < 10) {
1147
+ if (
1148
+ current.getKind() === SyntaxKind.FunctionDeclaration ||
1149
+ current.getKind() === SyntaxKind.FunctionExpression ||
1150
+ current.getKind() === SyntaxKind.ArrowFunction
1151
+ ) {
1152
+ // For named functions
1153
+ const funcName = current.getName?.();
1154
+ if (funcName) return funcName;
1155
+
1156
+ // For arrow functions assigned to const/let/var
1157
+ const parent = current.getParent();
1158
+ if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
1159
+ return parent.getName?.() || "";
1160
+ }
1161
+ }
1162
+ current = current.getParent();
1163
+ depth++;
1164
+ }
1165
+
1166
+ return null;
1167
+ }
1168
+
727
1169
  /**
728
1170
  * Helper: Find parent context (call expression, return, etc.)
729
1171
  */
@@ -764,7 +1206,7 @@ class S006SymbolBasedAnalyzer {
764
1206
  const text = node.getText().toLowerCase();
765
1207
  const normalized = this.normalizeIdentifier(text);
766
1208
 
767
- // Check for sensitive identifiers
1209
+ // Check for sensitive identifiers (variable/property access)
768
1210
  if (this.isSensitiveIdentifier(normalized)) {
769
1211
  return true;
770
1212
  }