@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,541 @@
1
+ /**
2
+ * S043
3
+ * Require re-authentication for long-lived sessions or sensitive actions
4
+ * Objective: Reduce the risk of session hijacking or privilege misuse by forcing
5
+ * Enforce correct access control after sensitive updates.
6
+ * 1. Detects password change functions by matching common naming patterns like changePassword, updatePassword, resetPassword, etc.
7
+ * 2. Checks for session invalidation through multiple methods:
8
+ * - Direct session invalidation function calls
9
+ * - Database/cache clear operations
10
+ * - JWT token versioning or timestamp updates
11
+ * - Function calls that might invalidate sessions
12
+ * 3. Reports violations when password change functions don't properly invalidate sessions, with detailed guidance on how to fix the issue.
13
+ * 4. Breaks down logic into smaller, focused functions:
14
+ * - checkFunctionsAndMethods() - checks regular functions and class methods
15
+ * - checkArrowFunctionsAndExpressions() - checks arrow functions and function expressions
16
+ * - isPasswordChangeFunction() - identifies password change functions
17
+ * - checkForSessionInvalidation() - main validation logic
18
+ * - hasSessionInvalidationCall() - checks for direct invalidation calls
19
+ * - hasSessionClearOperation() - checks for DB/cache operations
20
+ * - hasTokenVersioningOrTimestamp() - checks for JWT versioning
21
+ * - checkFunctionCallsForSessionInvalidation() - examines nested function calls
22
+ */
23
+
24
+ const { SyntaxKind } = require('ts-morph');
25
+
26
+ class S043SymbolBasedAnalyzer {
27
+ constructor(semanticEngine = null) {
28
+ this.ruleId = "S043";
29
+ this.ruleName = 'Password changes must invalidate all other login sessions';
30
+ this.semanticEngine = semanticEngine;
31
+ this.verbose = false;
32
+ this.skipPatterns = [
33
+ /\/node_modules\//,
34
+ /\/tests?\//,
35
+ /\/dist\//,
36
+ /\/build\//,
37
+ /\.spec\.ts$/,
38
+ /\.test\.ts$/
39
+ ];
40
+
41
+ // Password change function patterns
42
+ this.passwordChangePatterns = [
43
+ 'changePassword',
44
+ 'updatePassword',
45
+ 'resetPassword',
46
+ 'setPassword',
47
+ 'modifyPassword',
48
+ 'passwordUpdate',
49
+ 'changeUserPassword',
50
+ 'updateUserPassword'
51
+ ];
52
+
53
+ // Session invalidation indicators
54
+ this.sessionInvalidationIndicators = [
55
+ 'invalidateSession',
56
+ 'clearSession',
57
+ 'destroySession',
58
+ 'deleteSession',
59
+ 'removeSession',
60
+ 'clearAllSessions',
61
+ 'invalidateAllTokens',
62
+ 'revokeAllTokens',
63
+ 'deleteAllSessions',
64
+ 'destroyAllSessions',
65
+ 'clearUserSessions',
66
+ 'deleteAllByUserId',
67
+ 'logout',
68
+ 'signOut',
69
+ 'logoutAll',
70
+ 'logoutAllDevices',
71
+ 'signOutAll',
72
+ 'revokeToken',
73
+ 'revokeRefreshToken',
74
+ 'invalidateToken',
75
+ 'deleteToken',
76
+ 'globalSignOut',
77
+ 'adminUserGlobalSignOut',
78
+ 'clearCache',
79
+ 'deleteCache',
80
+ 'removeCache'
81
+ ];
82
+
83
+ this.maxDepth = 4;
84
+ this.visited = new Set();
85
+ }
86
+
87
+ async initialize(semanticEngine = null) {
88
+ if (semanticEngine) {
89
+ this.semanticEngine = semanticEngine;
90
+ }
91
+ this.verbose = semanticEngine?.verbose || false;
92
+ }
93
+
94
+ async analyzeFileBasic(filePath, options = {}) {
95
+ return await this.analyzeFileWithSymbols(filePath, options);
96
+ }
97
+
98
+ analyzeFileWithSymbols(filePath, options = {}) {
99
+ const violations = [];
100
+
101
+ if (!this.semanticEngine?.project) {
102
+ return violations;
103
+ }
104
+
105
+ if (this.shouldIgnoreFile(filePath)) {
106
+ return violations;
107
+ }
108
+
109
+ try {
110
+ const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
111
+ if (!sourceFile) {
112
+ return violations;
113
+ }
114
+
115
+ // Check functions and methods
116
+ const functions = [
117
+ ...sourceFile.getFunctions(),
118
+ ...sourceFile.getClasses().flatMap(cls => cls.getMethods())
119
+ ];
120
+
121
+ for (const func of functions) {
122
+ const funcName = func.getName();
123
+ if (funcName && this.isPasswordChangeFunction(funcName)) {
124
+ this.visited.clear();
125
+ if (!this.hasSessionInvalidation(func, 0)) {
126
+ this.addViolation(sourceFile, func, violations, funcName);
127
+ }
128
+ }
129
+ }
130
+
131
+ // Check arrow functions
132
+ sourceFile.forEachDescendant((node) => {
133
+ if (node.getKind() === SyntaxKind.ArrowFunction ||
134
+ node.getKind() === SyntaxKind.FunctionExpression) {
135
+ const parent = node.getParent();
136
+ if (parent?.getKind() === SyntaxKind.VariableDeclaration) {
137
+ const funcName = parent.getName();
138
+ if (funcName && this.isPasswordChangeFunction(funcName)) {
139
+ this.visited.clear();
140
+ if (!this.hasSessionInvalidation(node, 0)) {
141
+ this.addViolation(sourceFile, node, violations, funcName);
142
+ }
143
+ }
144
+ }
145
+ }
146
+ });
147
+
148
+ return violations;
149
+ } catch (error) {
150
+ return violations;
151
+ }
152
+ }
153
+
154
+ isPasswordChangeFunction(name) {
155
+ const lowerName = name.toLowerCase();
156
+ return this.passwordChangePatterns.some(pattern =>
157
+ lowerName === pattern.toLowerCase()
158
+ );
159
+ }
160
+
161
+ hasSessionInvalidation(node, depth) {
162
+ if (depth > this.maxDepth) return false;
163
+
164
+ const nodeId = this.getNodeId(node);
165
+ if (this.visited.has(nodeId)) return false;
166
+ this.visited.add(nodeId);
167
+
168
+ const text = node.getText();
169
+
170
+ // Check 1: Direct text patterns
171
+ if (this.checkTextForInvalidation(text)) {
172
+ return true;
173
+ }
174
+
175
+ // Check 2: Token versioning
176
+ if (this.hasTokenVersioning(text)) {
177
+ return true;
178
+ }
179
+
180
+ // Check 3: Logout + cache clear pattern
181
+ if (this.hasLogoutAndCacheClear(text)) {
182
+ return true;
183
+ }
184
+
185
+ // Check 4: Cognito operations
186
+ if (this.hasCognitoInvalidation(text)) {
187
+ return true;
188
+ }
189
+
190
+ // Check 5: Deep scan function calls
191
+ const calls = this.extractFunctionCalls(node);
192
+ for (const call of calls) {
193
+ // Check if call name indicates session invalidation
194
+ if (this.isInvalidationCall(call.name)) {
195
+ return true;
196
+ }
197
+
198
+ // Resolve and trace the called function
199
+ const calledNode = this.resolveAndFindFunction(call, node);
200
+ if (calledNode && this.hasSessionInvalidation(calledNode, depth + 1)) {
201
+ return true;
202
+ }
203
+ }
204
+
205
+ return false;
206
+ }
207
+
208
+ checkTextForInvalidation(text) {
209
+ const lowerText = text.toLowerCase();
210
+
211
+ for (const indicator of this.sessionInvalidationIndicators) {
212
+ if (lowerText.includes(indicator.toLowerCase())) {
213
+ return true;
214
+ }
215
+ }
216
+
217
+ if ((lowerText.includes('delete') || lowerText.includes('remove') ||
218
+ lowerText.includes('destroy') || lowerText.includes('clear')) &&
219
+ (lowerText.includes('session') || lowerText.includes('token'))) {
220
+ return true;
221
+ }
222
+
223
+ if ((lowerText.includes('redis') || lowerText.includes('cache')) &&
224
+ (lowerText.includes('.del(') || lowerText.includes('.delete(') ||
225
+ lowerText.includes('.clear(') || lowerText.includes('.remove('))) {
226
+ return true;
227
+ }
228
+
229
+ return false;
230
+ }
231
+
232
+ hasTokenVersioning(text) {
233
+ const hasVersionField = /tokenVersion|token_version|jwtVersion|jwt_version|sessionVersion|passwordChangedAt|lastPasswordChange/i.test(text);
234
+ const hasIncrement = /\+\+|increment|\+ 1|\+= 1/i.test(text);
235
+ return hasVersionField && hasIncrement;
236
+ }
237
+
238
+ hasLogoutAndCacheClear(text) {
239
+ const lowerText = text.toLowerCase();
240
+ const hasLogout = /logout|signout/i.test(text);
241
+ const hasCacheDelete = /cache.*\.del\(|cachemanager\.del\(|cache.*\.delete\(|cache.*\.remove\(/i.test(text);
242
+ return hasLogout && hasCacheDelete;
243
+ }
244
+
245
+ hasCognitoInvalidation(text) {
246
+ const lowerText = text.toLowerCase();
247
+
248
+ if (lowerText.includes('cognito') && lowerText.includes('changepassword')) {
249
+ if (lowerText.includes('logout') || lowerText.includes('del(') ||
250
+ lowerText.includes('globalsignout')) {
251
+ return true;
252
+ }
253
+ }
254
+
255
+ if (/globalsignout|adminuserglobalsignout/i.test(text)) {
256
+ return true;
257
+ }
258
+
259
+ return false;
260
+ }
261
+
262
+ isInvalidationCall(callName) {
263
+ const lowerName = callName.toLowerCase();
264
+ return this.sessionInvalidationIndicators.some(indicator => {
265
+ const lowerIndicator = indicator.toLowerCase();
266
+ return lowerName === lowerIndicator || lowerName.includes(lowerIndicator);
267
+ });
268
+ }
269
+
270
+ extractFunctionCalls(node) {
271
+ const calls = [];
272
+ const seen = new Set();
273
+
274
+ node.forEachDescendant((child) => {
275
+ if (child.getKind() === SyntaxKind.CallExpression) {
276
+ const expr = child.getExpression();
277
+
278
+ if (expr.getKind() === SyntaxKind.Identifier) {
279
+ const name = expr.getText();
280
+ if (!seen.has(name)) {
281
+ calls.push({
282
+ name,
283
+ type: 'function',
284
+ node: child,
285
+ expression: expr
286
+ });
287
+ seen.add(name);
288
+ }
289
+ } else if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
290
+ const propAccess = expr;
291
+ const methodName = propAccess.getName();
292
+ const objectExpr = propAccess.getExpression();
293
+ const objectName = objectExpr.getText();
294
+ const fullName = expr.getText();
295
+
296
+ if (!seen.has(fullName)) {
297
+ calls.push({
298
+ name: methodName,
299
+ object: objectName,
300
+ fullName,
301
+ type: 'method',
302
+ node: child,
303
+ expression: expr,
304
+ objectExpression: objectExpr
305
+ });
306
+ seen.add(fullName);
307
+ }
308
+ }
309
+ }
310
+ });
311
+
312
+ return calls;
313
+ }
314
+
315
+ /**
316
+ * Resolve the actual function/method being called
317
+ * Handles: this.service.method(), service.method(), functionName()
318
+ */
319
+ resolveAndFindFunction(call, contextNode) {
320
+ if (!this.semanticEngine?.project) return null;
321
+
322
+ // Case 1: Direct function call - functionName()
323
+ if (call.type === 'function') {
324
+ return this.findFunctionByName(call.name);
325
+ }
326
+
327
+ // Case 2: Method call - object.method()
328
+ if (call.type === 'method') {
329
+ // Try to resolve the object (service/class instance)
330
+ const serviceClass = this.resolveServiceClass(call.object, contextNode);
331
+
332
+ if (serviceClass) {
333
+ // Find the method in the service class
334
+ const method = serviceClass.getMethod(call.name);
335
+ if (method) {
336
+ return method;
337
+ }
338
+ }
339
+
340
+ // Fallback: Search for method by name across all classes
341
+ return this.findMethodByName(call.name);
342
+ }
343
+
344
+ return null;
345
+ }
346
+
347
+ /**
348
+ * Resolve the service class from object name
349
+ * Example: this.commonChangePasswordService -> CommonChangePasswordService class
350
+ */
351
+ resolveServiceClass(objectName, contextNode) {
352
+ if (!objectName) return null;
353
+
354
+ try {
355
+ // Remove 'this.' prefix if exists
356
+ const cleanObjectName = objectName.replace(/^this\./, '');
357
+
358
+ // Get the containing class
359
+ let currentNode = contextNode;
360
+ while (currentNode && currentNode.getKind() !== SyntaxKind.ClassDeclaration) {
361
+ currentNode = currentNode.getParent();
362
+ }
363
+
364
+ if (!currentNode) return null;
365
+
366
+ const containingClass = currentNode;
367
+
368
+ // Find the property declaration (injected service)
369
+ const property = containingClass.getProperty(cleanObjectName);
370
+ if (property) {
371
+ const propertyType = property.getType();
372
+ const typeSymbol = propertyType.getSymbol();
373
+
374
+ if (typeSymbol) {
375
+ const declarations = typeSymbol.getDeclarations();
376
+ if (declarations && declarations.length > 0) {
377
+ const decl = declarations[0];
378
+ if (decl.getKind() === SyntaxKind.ClassDeclaration) {
379
+ return decl;
380
+ }
381
+ }
382
+ }
383
+ }
384
+
385
+ // Fallback: Try to find by constructor parameter
386
+ const constructor = containingClass.getConstructors()[0];
387
+ if (constructor) {
388
+ const param = constructor.getParameter(cleanObjectName);
389
+ if (param) {
390
+ const paramType = param.getType();
391
+ const typeSymbol = paramType.getSymbol();
392
+
393
+ if (typeSymbol) {
394
+ const declarations = typeSymbol.getDeclarations();
395
+ if (declarations && declarations.length > 0) {
396
+ const decl = declarations[0];
397
+ if (decl.getKind() === SyntaxKind.ClassDeclaration) {
398
+ return decl;
399
+ }
400
+ }
401
+ }
402
+ }
403
+ }
404
+
405
+ // Fallback: Search by service name pattern
406
+ return this.findClassByServiceName(cleanObjectName);
407
+
408
+ } catch (error) {
409
+ return null;
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Find class by service variable name
415
+ * Example: commonChangePasswordService -> CommonChangePasswordService
416
+ */
417
+ findClassByServiceName(serviceName) {
418
+ if (!this.semanticEngine?.project) return null;
419
+
420
+ try {
421
+ // Convert camelCase service name to PascalCase class name
422
+ const className = serviceName.charAt(0).toUpperCase() + serviceName.slice(1);
423
+
424
+ // Also try common patterns
425
+ const possibleClassNames = [
426
+ className,
427
+ className.replace('Service', '') + 'Service',
428
+ serviceName.split(/(?=[A-Z])/).map(part =>
429
+ part.charAt(0).toUpperCase() + part.slice(1)
430
+ ).join('')
431
+ ];
432
+
433
+ const sourceFiles = this.semanticEngine.project.getSourceFiles();
434
+
435
+ for (const file of sourceFiles) {
436
+ if (this.shouldIgnoreFile(file.getFilePath())) continue;
437
+
438
+ for (const cls of file.getClasses()) {
439
+ const clsName = cls.getName();
440
+ if (clsName && possibleClassNames.some(name =>
441
+ name.toLowerCase() === clsName.toLowerCase()
442
+ )) {
443
+ return cls;
444
+ }
445
+ }
446
+ }
447
+ } catch (error) {
448
+ return null;
449
+ }
450
+
451
+ return null;
452
+ }
453
+
454
+ /**
455
+ * Find function by name across all files
456
+ */
457
+ findFunctionByName(functionName) {
458
+ if (!this.semanticEngine?.project || !functionName) return null;
459
+
460
+ try {
461
+ const sourceFiles = this.semanticEngine.project.getSourceFiles();
462
+
463
+ for (const file of sourceFiles) {
464
+ if (this.shouldIgnoreFile(file.getFilePath())) continue;
465
+
466
+ const func = file.getFunction(functionName);
467
+ if (func) return func;
468
+
469
+ const varDecl = file.getVariableDeclaration(functionName);
470
+ if (varDecl) {
471
+ const init = varDecl.getInitializer();
472
+ if (init && (init.getKind() === SyntaxKind.ArrowFunction ||
473
+ init.getKind() === SyntaxKind.FunctionExpression)) {
474
+ return init;
475
+ }
476
+ }
477
+ }
478
+ } catch (error) {
479
+ return null;
480
+ }
481
+
482
+ return null;
483
+ }
484
+
485
+ /**
486
+ * Find method by name across all classes
487
+ */
488
+ findMethodByName(methodName) {
489
+ if (!this.semanticEngine?.project || !methodName) return null;
490
+
491
+ try {
492
+ const sourceFiles = this.semanticEngine.project.getSourceFiles();
493
+
494
+ for (const file of sourceFiles) {
495
+ if (this.shouldIgnoreFile(file.getFilePath())) continue;
496
+
497
+ for (const cls of file.getClasses()) {
498
+ const method = cls.getMethod(methodName);
499
+ if (method) return method;
500
+ }
501
+ }
502
+ } catch (error) {
503
+ return null;
504
+ }
505
+
506
+ return null;
507
+ }
508
+
509
+ getNodeId(node) {
510
+ try {
511
+ const sourceFile = node.getSourceFile();
512
+ const start = node.getStart();
513
+ return `${sourceFile.getFilePath()}:${start}`;
514
+ } catch {
515
+ return Math.random().toString();
516
+ }
517
+ }
518
+
519
+ addViolation(sourceFile, node, violations, functionName) {
520
+ const startLine = node.getStartLineNumber();
521
+ const column = node.getStart() - node.getStartLinePos() + 1;
522
+
523
+ violations.push({
524
+ ruleId: this.ruleId,
525
+ ruleName: this.ruleName,
526
+ severity: 'medium',
527
+ message: `Password change function "${functionName}" must invalidate all active sessions`,
528
+ line: startLine,
529
+ column: column,
530
+ filePath: sourceFile.getFilePath(),
531
+ type: 'SESSION_INVALIDATION_MISSING',
532
+ details: `After password change, you must invalidate sessions. Examples: invalidateAllSessions(userId), logout(user), sessionRepository.delete({userId}), user.tokenVersion++, redis.del('session:*'), cacheManager.del(key)`
533
+ });
534
+ }
535
+
536
+ shouldIgnoreFile(filePath) {
537
+ return this.skipPatterns.some(pattern => pattern.test(filePath));
538
+ }
539
+ }
540
+
541
+ module.exports = S043SymbolBasedAnalyzer;