@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
@@ -0,0 +1,674 @@
1
+ /**
2
+ * S041 Symbol-Based Analyzer - Session Tokens must be invalidated after logout or expiration
3
+ * Uses TypeScript compiler API for semantic analysis
4
+ */
5
+
6
+ const ts = require("typescript");
7
+
8
+ class S041SymbolBasedAnalyzer {
9
+ constructor(semanticEngine = null) {
10
+ this.semanticEngine = semanticEngine;
11
+ this.ruleId = "S041";
12
+ this.category = "security";
13
+
14
+ // Session management methods that should invalidate tokens
15
+ this.sessionMethods = [
16
+ "logout",
17
+ "signOut",
18
+ "logOut",
19
+ "destroy",
20
+ "clear",
21
+ "invalidate",
22
+ "revoke",
23
+ "expire",
24
+ "endSession",
25
+ "end-session",
26
+ "clear-session",
27
+ "destroy-session",
28
+ "remove-session"
29
+ ];
30
+
31
+ // Token-related methods and properties
32
+ this.tokenMethods = [
33
+ "removeToken",
34
+ "clearToken",
35
+ "invalidateToken",
36
+ "revokeToken",
37
+ "deleteToken",
38
+ "destroyToken"
39
+ ];
40
+
41
+ // Session storage methods
42
+ this.sessionStorageMethods = [
43
+ "removeItem",
44
+ "clear",
45
+ "destroy",
46
+ "delete"
47
+ ];
48
+
49
+ // JWT token methods
50
+ this.jwtMethods = [
51
+ "sign",
52
+ "verify",
53
+ "decode",
54
+ "invalidate",
55
+ "blacklist"
56
+ ];
57
+
58
+ // Express session methods
59
+ this.expressSessionMethods = [
60
+ "destroy",
61
+ "regenerate",
62
+ "reload",
63
+ "save"
64
+ ];
65
+
66
+ // Session data clearing patterns
67
+ this.sessionClearPatterns = [
68
+ "req.session.destroy",
69
+ "req.session = null",
70
+ "req.session = {}",
71
+ "session.destroy",
72
+ "session.clear",
73
+ "session.remove"
74
+ ];
75
+
76
+ // Token invalidation patterns
77
+ this.tokenInvalidationPatterns = [
78
+ "blacklist",
79
+ "revoke",
80
+ "invalidate",
81
+ "expire",
82
+ "remove",
83
+ "delete"
84
+ ];
85
+
86
+ // User input sources for session data
87
+ this.userInputSources = [
88
+ "req",
89
+ "request",
90
+ "params",
91
+ "query",
92
+ "body",
93
+ "headers",
94
+ "cookies",
95
+ "session"
96
+ ];
97
+ }
98
+
99
+ /**
100
+ * Initialize analyzer with semantic engine
101
+ */
102
+ async initialize(semanticEngine) {
103
+ this.semanticEngine = semanticEngine;
104
+ if (this.verbose) {
105
+ console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
106
+ }
107
+ }
108
+
109
+ async analyze(filePath) {
110
+ if (this.verbose) {
111
+ console.log(
112
+ `🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
113
+ );
114
+ }
115
+
116
+ if (!this.semanticEngine) {
117
+ if (this.verbose) {
118
+ console.log(
119
+ `🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
120
+ );
121
+ }
122
+ return [];
123
+ }
124
+
125
+ try {
126
+ const sourceFile = this.semanticEngine.getSourceFile(filePath);
127
+ if (!sourceFile) {
128
+ if (this.verbose) {
129
+ console.log(
130
+ `🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
131
+ );
132
+ }
133
+ return await this.analyzeTsMorph(filePath);
134
+ }
135
+
136
+ if (this.verbose) {
137
+ console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
138
+ }
139
+
140
+ return await this.analyzeSourceFile(sourceFile, filePath);
141
+ } catch (error) {
142
+ if (this.verbose) {
143
+ console.log(
144
+ `🔍 [${this.ruleId}] Symbol: Error in analysis:`,
145
+ error.message
146
+ );
147
+ }
148
+ return [];
149
+ }
150
+ }
151
+
152
+ async analyzeTsMorph(filePath) {
153
+ try {
154
+ if (this.verbose) {
155
+ console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
156
+ }
157
+
158
+ const { Project } = require("ts-morph");
159
+ const project = new Project();
160
+ const sourceFile = project.addSourceFileAtPath(filePath);
161
+
162
+ return await this.analyzeSourceFile(sourceFile, filePath);
163
+ } catch (error) {
164
+ if (this.verbose) {
165
+ console.log(
166
+ `🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`,
167
+ error.message
168
+ );
169
+ }
170
+ return [];
171
+ }
172
+ }
173
+
174
+ async analyzeSourceFile(sourceFile, filePath) {
175
+ const violations = [];
176
+
177
+ try {
178
+ if (this.verbose) {
179
+ console.log(`🔍 [${this.ruleId}] Symbol: Starting symbol-based analysis`);
180
+ }
181
+
182
+ const callExpressions = sourceFile.getDescendantsOfKind
183
+ ? sourceFile.getDescendantsOfKind(
184
+ require("typescript").SyntaxKind.CallExpression
185
+ )
186
+ : [];
187
+
188
+ if (this.verbose) {
189
+ console.log(
190
+ `🔍 [${this.ruleId}] Symbol: Found ${callExpressions.length} call expressions`
191
+ );
192
+ }
193
+
194
+ for (const callNode of callExpressions) {
195
+ try {
196
+ // Analyze logout methods without proper session cleanup
197
+ const logoutViolation = this.analyzeLogoutMethod(callNode, sourceFile);
198
+ if (logoutViolation) {
199
+ violations.push(logoutViolation);
200
+ }
201
+
202
+ // Analyze session methods without token invalidation
203
+ const sessionViolation = this.analyzeSessionMethod(callNode, sourceFile);
204
+ if (sessionViolation) {
205
+ violations.push(sessionViolation);
206
+ }
207
+
208
+ // Analyze JWT token handling
209
+ const jwtViolation = this.analyzeJWTTokenHandling(callNode, sourceFile);
210
+ if (jwtViolation) {
211
+ violations.push(jwtViolation);
212
+ }
213
+
214
+ } catch (error) {
215
+ if (this.verbose) {
216
+ console.log(
217
+ `🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`,
218
+ error.message
219
+ );
220
+ }
221
+ }
222
+ }
223
+
224
+ // Also check for function declarations that might be logout handlers
225
+ const functionDeclarations = sourceFile.getDescendantsOfKind
226
+ ? sourceFile.getDescendantsOfKind(
227
+ require("typescript").SyntaxKind.FunctionDeclaration
228
+ )
229
+ : [];
230
+
231
+ for (const funcNode of functionDeclarations) {
232
+ try {
233
+ const logoutHandlerViolation = this.analyzeLogoutHandler(funcNode, sourceFile);
234
+ if (logoutHandlerViolation) {
235
+ violations.push(logoutHandlerViolation);
236
+ }
237
+ } catch (error) {
238
+ if (this.verbose) {
239
+ console.log(
240
+ `🔍 [${this.ruleId}] Symbol: Error analyzing function declaration:`,
241
+ error.message
242
+ );
243
+ }
244
+ }
245
+ }
246
+
247
+ if (this.verbose) {
248
+ console.log(
249
+ `🔍 [${this.ruleId}] Symbol: Analysis completed. Found ${violations.length} violations`
250
+ );
251
+ }
252
+
253
+ return violations;
254
+ } catch (error) {
255
+ if (this.verbose) {
256
+ console.log(
257
+ `🔍 [${this.ruleId}] Symbol: Error in source file analysis:`,
258
+ error.message
259
+ );
260
+ }
261
+ return [];
262
+ }
263
+ }
264
+
265
+ analyzeLogoutMethod(callNode, sourceFile) {
266
+ try {
267
+ const expression = callNode.getExpression();
268
+ const methodName = this.getMethodName(expression);
269
+
270
+ // Only check actual logout methods, not session cleanup methods
271
+ const actualLogoutMethods = ['logout', 'signOut', 'logOut', 'invalidate', 'revoke', 'expire'];
272
+ if (!actualLogoutMethods.includes(methodName)) {
273
+ return null;
274
+ }
275
+
276
+ if (this.verbose) {
277
+ console.log(
278
+ `🔍 [${this.ruleId}] Symbol: Logout method detected: ${methodName}`
279
+ );
280
+ }
281
+
282
+ // Check if session cleanup is performed in the same function
283
+ const hasSessionCleanup = this.hasSessionCleanupInContext(callNode, sourceFile);
284
+ if (!hasSessionCleanup) {
285
+ return this.createViolation(
286
+ sourceFile,
287
+ callNode,
288
+ `Session token invalidation vulnerability: ${methodName}() method should invalidate session tokens and clear session data`
289
+ );
290
+ }
291
+
292
+ return null;
293
+ } catch (error) {
294
+ if (this.verbose) {
295
+ console.log(
296
+ `🔍 [${this.ruleId}] Symbol: Error analyzing logout method:`,
297
+ error.message
298
+ );
299
+ }
300
+ return null;
301
+ }
302
+ }
303
+
304
+ analyzeSessionMethod(callNode, sourceFile) {
305
+ try {
306
+ const expression = callNode.getExpression();
307
+ const methodName = this.getMethodName(expression);
308
+
309
+ // Check if it's a session-related method
310
+ const isSessionMethod = this.isSessionRelatedMethod(expression);
311
+ if (!isSessionMethod) {
312
+ return null;
313
+ }
314
+
315
+ // Only check in logout context
316
+ const isInLogoutContext = this.isInLogoutContext(callNode, sourceFile);
317
+ if (!isInLogoutContext) {
318
+ return null;
319
+ }
320
+
321
+ if (this.verbose) {
322
+ console.log(
323
+ `🔍 [${this.ruleId}] Symbol: Session method detected in logout context: ${methodName}`
324
+ );
325
+ }
326
+
327
+ // Check if token invalidation is performed
328
+ const hasTokenInvalidation = this.hasTokenInvalidationInContext(callNode, sourceFile);
329
+ if (!hasTokenInvalidation) {
330
+ return this.createViolation(
331
+ sourceFile,
332
+ callNode,
333
+ `Session token invalidation vulnerability: Session method ${methodName}() should invalidate tokens during logout`
334
+ );
335
+ }
336
+
337
+ return null;
338
+ } catch (error) {
339
+ if (this.verbose) {
340
+ console.log(
341
+ `🔍 [${this.ruleId}] Symbol: Error analyzing session method:`,
342
+ error.message
343
+ );
344
+ }
345
+ return null;
346
+ }
347
+ }
348
+
349
+ analyzeJWTTokenHandling(callNode, sourceFile) {
350
+ try {
351
+ const expression = callNode.getExpression();
352
+ const methodName = this.getMethodName(expression);
353
+
354
+ if (!this.jwtMethods.includes(methodName)) {
355
+ return null;
356
+ }
357
+
358
+ if (this.verbose) {
359
+ console.log(
360
+ `🔍 [${this.ruleId}] Symbol: JWT method detected: ${methodName}`
361
+ );
362
+ }
363
+
364
+ // Check if it's in a logout context and if token is properly invalidated
365
+ const isInLogoutContext = this.isInLogoutContext(callNode, sourceFile);
366
+ if (isInLogoutContext && methodName === "sign") {
367
+ return this.createViolation(
368
+ sourceFile,
369
+ callNode,
370
+ `Session token invalidation vulnerability: JWT token should be invalidated/blacklisted during logout, not signed`
371
+ );
372
+ }
373
+
374
+ return null;
375
+ } catch (error) {
376
+ if (this.verbose) {
377
+ console.log(
378
+ `🔍 [${this.ruleId}] Symbol: Error analyzing JWT token handling:`,
379
+ error.message
380
+ );
381
+ }
382
+ return null;
383
+ }
384
+ }
385
+
386
+ analyzeLogoutHandler(funcNode, sourceFile) {
387
+ try {
388
+ const functionName = funcNode.getName();
389
+ const functionText = funcNode.getText();
390
+
391
+ // Check if it's a logout handler
392
+ const isLogoutHandler = this.isLogoutHandler(functionName, functionText);
393
+ if (!isLogoutHandler) {
394
+ return null;
395
+ }
396
+
397
+ if (this.verbose) {
398
+ console.log(
399
+ `🔍 [${this.ruleId}] Symbol: Logout handler detected: ${functionName}`
400
+ );
401
+ }
402
+
403
+ // Check if session cleanup is performed
404
+ const hasSessionCleanup = this.hasSessionCleanupInFunction(funcNode);
405
+ if (!hasSessionCleanup) {
406
+ return this.createViolation(
407
+ sourceFile,
408
+ funcNode,
409
+ `Session token invalidation vulnerability: Logout handler should clear session data and invalidate tokens`
410
+ );
411
+ }
412
+
413
+ return null;
414
+ } catch (error) {
415
+ if (this.verbose) {
416
+ console.log(
417
+ `🔍 [${this.ruleId}] Symbol: Error analyzing logout handler:`,
418
+ error.message
419
+ );
420
+ }
421
+ return null;
422
+ }
423
+ }
424
+
425
+ isSessionRelatedMethod(expression) {
426
+ try {
427
+ const expressionText = expression.getText();
428
+
429
+ // Don't detect session cleanup methods as violations
430
+ const sessionCleanupMethods = [
431
+ 'req.session.destroy',
432
+ 'session.destroy',
433
+ 'res.clearCookie',
434
+ 'sessionStorage.clear',
435
+ 'localStorage.clear',
436
+ 'req.session.reload',
437
+ 'req.session.regenerate'
438
+ ];
439
+
440
+ if (sessionCleanupMethods.some(method => expressionText.includes(method))) {
441
+ return false;
442
+ }
443
+
444
+ return this.sessionStorageMethods.some(method =>
445
+ expressionText.includes(method)
446
+ ) || this.expressSessionMethods.some(method =>
447
+ expressionText.includes(method)
448
+ );
449
+ } catch (error) {
450
+ return false;
451
+ }
452
+ }
453
+
454
+ isLogoutRelatedMethod(methodName) {
455
+ return this.sessionMethods.includes(methodName) ||
456
+ this.tokenMethods.includes(methodName);
457
+ }
458
+
459
+ isLogoutHandler(functionName, functionText) {
460
+ const logoutPatterns = [
461
+ /logout/i,
462
+ /signout/i,
463
+ /sign_out/i,
464
+ /log_out/i,
465
+ /destroy.*session/i,
466
+ /clear.*session/i,
467
+ /end.*session/i,
468
+ /endSession/i
469
+ ];
470
+
471
+ return logoutPatterns.some(pattern =>
472
+ pattern.test(functionName) || pattern.test(functionText)
473
+ );
474
+ }
475
+
476
+ isInLogoutContext(node, sourceFile) {
477
+ try {
478
+ // Get the parent function to check context
479
+ let parent = node.getParent();
480
+ while (parent && !this.isFunctionLike(parent)) {
481
+ parent = parent.getParent();
482
+ }
483
+
484
+ if (!parent) {
485
+ return false;
486
+ }
487
+
488
+ const functionText = parent.getText();
489
+ const functionName = this.getFunctionName(parent);
490
+
491
+ // Check if function name or content indicates logout
492
+ const logoutPatterns = [
493
+ /logout/i,
494
+ /signout/i,
495
+ /sign_out/i,
496
+ /log_out/i,
497
+ /destroy.*session/i,
498
+ /clear.*session/i
499
+ ];
500
+
501
+ // Check function name
502
+ if (functionName && logoutPatterns.some(pattern => pattern.test(functionName))) {
503
+ return true;
504
+ }
505
+
506
+ // Check function content for logout patterns
507
+ return logoutPatterns.some(pattern => pattern.test(functionText));
508
+ } catch (error) {
509
+ return false;
510
+ }
511
+ }
512
+
513
+ getFunctionName(node) {
514
+ try {
515
+ if (node.getName) {
516
+ return node.getName();
517
+ }
518
+ return "";
519
+ } catch (error) {
520
+ return "";
521
+ }
522
+ }
523
+
524
+ hasSessionCleanupInContext(node, sourceFile) {
525
+ try {
526
+ // Get the parent function to check for session cleanup
527
+ let parent = node.getParent();
528
+ while (parent && !this.isFunctionLike(parent)) {
529
+ parent = parent.getParent();
530
+ }
531
+
532
+ if (!parent) {
533
+ return false;
534
+ }
535
+
536
+ const functionText = parent.getText();
537
+
538
+ // Check for session cleanup patterns
539
+ const cleanupPatterns = [
540
+ /req\.session\.destroy/,
541
+ /req\.session\s*=\s*null/,
542
+ /req\.session\s*=\s*\{\}/,
543
+ /session\.destroy/,
544
+ /session\.clear/,
545
+ /session\.remove/,
546
+ /\.removeItem/,
547
+ /\.clear\(\)/,
548
+ /\.delete\(/,
549
+ /res\.clearCookie/,
550
+ /sessionStorage\.clear/,
551
+ /localStorage\.clear/,
552
+ /req\.session\.regenerate/
553
+ ];
554
+
555
+ return cleanupPatterns.some(pattern => pattern.test(functionText));
556
+ } catch (error) {
557
+ return false;
558
+ }
559
+ }
560
+
561
+ hasSessionCleanupInFunction(funcNode) {
562
+ try {
563
+ const functionText = funcNode.getText();
564
+
565
+ // Check for session cleanup patterns
566
+ const cleanupPatterns = [
567
+ /req\.session\.destroy/,
568
+ /req\.session\s*=\s*null/,
569
+ /req\.session\s*=\s*\{\}/,
570
+ /session\.destroy/,
571
+ /session\.clear/,
572
+ /session\.remove/,
573
+ /\.removeItem/,
574
+ /\.clear\(\)/,
575
+ /\.delete\(/
576
+ ];
577
+
578
+ return cleanupPatterns.some(pattern => pattern.test(functionText));
579
+ } catch (error) {
580
+ return false;
581
+ }
582
+ }
583
+
584
+ hasTokenInvalidationInContext(node, sourceFile) {
585
+ try {
586
+ // Get the parent function to check for token invalidation
587
+ let parent = node.getParent();
588
+ while (parent && !this.isFunctionLike(parent)) {
589
+ parent = parent.getParent();
590
+ }
591
+
592
+ if (!parent) {
593
+ return false;
594
+ }
595
+
596
+ const functionText = parent.getText();
597
+
598
+ // Check for token invalidation patterns
599
+ const invalidationPatterns = [
600
+ /blacklist/,
601
+ /revoke/,
602
+ /invalidate/,
603
+ /expire/,
604
+ /remove.*token/,
605
+ /delete.*token/,
606
+ /clear.*token/,
607
+ /destroy.*token/
608
+ ];
609
+
610
+ return invalidationPatterns.some(pattern => pattern.test(functionText));
611
+ } catch (error) {
612
+ return false;
613
+ }
614
+ }
615
+
616
+ isFunctionLike(node) {
617
+ try {
618
+ const SyntaxKind = require("typescript").SyntaxKind;
619
+ const kind = node.getKind();
620
+
621
+ return kind === SyntaxKind.FunctionDeclaration ||
622
+ kind === SyntaxKind.FunctionExpression ||
623
+ kind === SyntaxKind.ArrowFunction ||
624
+ kind === SyntaxKind.MethodDeclaration;
625
+ } catch (error) {
626
+ return false;
627
+ }
628
+ }
629
+
630
+ getMethodName(expression) {
631
+ try {
632
+ const ts = require("typescript");
633
+
634
+ if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
635
+ return expression.getNameNode().getText();
636
+ }
637
+
638
+ if (expression.getKind() === ts.SyntaxKind.Identifier) {
639
+ return expression.getText();
640
+ }
641
+
642
+ return "";
643
+ } catch (error) {
644
+ return "";
645
+ }
646
+ }
647
+
648
+ createViolation(sourceFile, node, message) {
649
+ try {
650
+ const start = node.getStart();
651
+ const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
652
+
653
+ return {
654
+ rule: this.ruleId,
655
+ source: sourceFile.getFilePath(),
656
+ category: this.category,
657
+ line: lineAndChar.line,
658
+ column: lineAndChar.column,
659
+ message: message,
660
+ severity: "error",
661
+ };
662
+ } catch (error) {
663
+ if (this.verbose) {
664
+ console.log(
665
+ `🔍 [${this.ruleId}] Symbol: Error creating violation:`,
666
+ error.message
667
+ );
668
+ }
669
+ return null;
670
+ }
671
+ }
672
+ }
673
+
674
+ module.exports = S041SymbolBasedAnalyzer;