@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,646 @@
1
+ /**
2
+ * S045 Symbol-Based Analyzer - Brute-force Protection
3
+ * Uses TypeScript compiler API for semantic analysis
4
+ */
5
+
6
+ const ts = require("typescript");
7
+
8
+ class S045SymbolBasedAnalyzer {
9
+ constructor(semanticEngine = null) {
10
+ this.semanticEngine = semanticEngine;
11
+ this.ruleId = "S045";
12
+ this.category = "security";
13
+
14
+ // Authentication endpoint patterns
15
+ this.authEndpoints = [
16
+ "login",
17
+ "signin",
18
+ "authenticate",
19
+ "auth",
20
+ "password",
21
+ "reset",
22
+ "forgot",
23
+ "signup",
24
+ "register"
25
+ ];
26
+
27
+ // Rate limiting libraries and decorators
28
+ this.rateLimitLibraries = [
29
+ "express-rate-limit",
30
+ "express-slow-down",
31
+ "@nestjs/throttler",
32
+ "rate-limiter-flexible",
33
+ "bottleneck",
34
+ "limiter"
35
+ ];
36
+
37
+ // Account lockout libraries
38
+ this.lockoutLibraries = [
39
+ "express-slow-down",
40
+ "rate-limiter-flexible",
41
+ "express-brute",
42
+ "express-brute-mongo"
43
+ ];
44
+
45
+ // CAPTCHA libraries
46
+ this.captchaLibraries = [
47
+ "recaptcha",
48
+ "hcaptcha",
49
+ "turnstile",
50
+ "captcha"
51
+ ];
52
+
53
+ // Protection decorators and middleware
54
+ this.protectionDecorators = [
55
+ "Throttle",
56
+ "RateLimit",
57
+ "ThrottleGuard",
58
+ "RateLimitGuard"
59
+ ];
60
+
61
+ // Protection patterns in code
62
+ this.protectionPatterns = [
63
+ "rate.*limit",
64
+ "throttle",
65
+ "lockout",
66
+ "captcha",
67
+ "brute.*force.*protection",
68
+ "max.*attempts",
69
+ "cooldown",
70
+ "windowMs",
71
+ "max.*requests"
72
+ ];
73
+
74
+ // Vulnerable patterns that indicate missing protection
75
+ this.vulnerablePatterns = [
76
+ "login.*without.*rate.*limit",
77
+ "auth.*without.*throttle",
78
+ "password.*without.*lockout",
79
+ "signin.*without.*captcha"
80
+ ];
81
+ }
82
+
83
+ /**
84
+ * Initialize analyzer with semantic engine
85
+ */
86
+ async initialize(semanticEngine) {
87
+ this.semanticEngine = semanticEngine;
88
+ if (this.verbose) {
89
+ console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
90
+ }
91
+ }
92
+
93
+ async analyze(filePath) {
94
+ if (this.verbose) {
95
+ console.log(
96
+ `🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
97
+ );
98
+ }
99
+
100
+ if (!this.semanticEngine) {
101
+ if (this.verbose) {
102
+ console.log(
103
+ `🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
104
+ );
105
+ }
106
+ return [];
107
+ }
108
+
109
+ try {
110
+ const sourceFile = this.semanticEngine.getSourceFile(filePath);
111
+ console.log('sourceFile', sourceFile);
112
+ if (!sourceFile) {
113
+ if (this.verbose) {
114
+ console.log(
115
+ `🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
116
+ );
117
+ }
118
+ return await this.analyzeTsMorph(filePath);
119
+ }
120
+
121
+ if (this.verbose) {
122
+ console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
123
+ }
124
+
125
+ return await this.analyzeSourceFile(sourceFile, filePath);
126
+ } catch (error) {
127
+ if (this.verbose) {
128
+ console.log(
129
+ `🔍 [${this.ruleId}] Symbol: Error in analysis:`,
130
+ error.message
131
+ );
132
+ }
133
+ return [];
134
+ }
135
+ }
136
+
137
+ async analyzeTsMorph(filePath) {
138
+ try {
139
+ if (this.verbose) {
140
+ console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
141
+ }
142
+
143
+ const { Project } = require("ts-morph");
144
+ const project = new Project();
145
+ const sourceFile = project.addSourceFileAtPath(filePath);
146
+
147
+ return await this.analyzeSourceFile(sourceFile, filePath);
148
+ } catch (error) {
149
+ if (this.verbose) {
150
+ console.log(
151
+ `🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`,
152
+ error.message
153
+ );
154
+ }
155
+ return [];
156
+ }
157
+ }
158
+
159
+ async analyzeSourceFile(sourceFile, filePath) {
160
+ const violations = [];
161
+ const startTime = Date.now();
162
+
163
+ try {
164
+ if (this.verbose) {
165
+ console.log(`🔍 [${this.ruleId}] Symbol: Starting symbol-based analysis for ${filePath}`);
166
+ }
167
+
168
+ // Find all method declarations and decorators
169
+ const methodDeclarations = sourceFile.getDescendantsOfKind
170
+ ? sourceFile.getDescendantsOfKind(
171
+ require("typescript").SyntaxKind.MethodDeclaration
172
+ )
173
+ : [];
174
+
175
+ const functionDeclarations = sourceFile.getDescendantsOfKind
176
+ ? sourceFile.getDescendantsOfKind(
177
+ require("typescript").SyntaxKind.FunctionDeclaration
178
+ )
179
+ : [];
180
+
181
+ const arrowFunctions = sourceFile.getDescendantsOfKind
182
+ ? sourceFile.getDescendantsOfKind(
183
+ require("typescript").SyntaxKind.ArrowFunction
184
+ )
185
+ : [];
186
+
187
+ const allFunctions = [
188
+ ...methodDeclarations,
189
+ ...functionDeclarations,
190
+ ...arrowFunctions
191
+ ];
192
+
193
+ if (this.verbose) {
194
+ console.log(
195
+ `🔍 [${this.ruleId}] Symbol: Found ${allFunctions.length} functions (${methodDeclarations.length} methods, ${functionDeclarations.length} functions, ${arrowFunctions.length} arrow functions)`
196
+ );
197
+ }
198
+
199
+ let authChecked = 0;
200
+ let rateLimitChecked = 0;
201
+ let lockoutChecked = 0;
202
+ let funcErrors = 0;
203
+
204
+ for (const func of allFunctions) {
205
+ try {
206
+ const funcName = this.getFunctionName(func);
207
+
208
+ if (this.verbose) {
209
+ console.log(`🔍 [${this.ruleId}] Symbol: Analyzing function '${funcName}'`);
210
+ }
211
+
212
+ // Check for authentication endpoints without protection
213
+ const authViolation = this.analyzeAuthenticationEndpoint(func, sourceFile);
214
+ authChecked++;
215
+ if (authViolation) {
216
+ violations.push(authViolation);
217
+ if (this.verbose) {
218
+ console.log(`⚠️ [${this.ruleId}] Symbol: Auth violation found in '${funcName}'`);
219
+ }
220
+ }
221
+
222
+ // Check for missing rate limiting
223
+ const rateLimitViolation = this.analyzeRateLimiting(func, sourceFile);
224
+ rateLimitChecked++;
225
+ if (rateLimitViolation) {
226
+ violations.push(rateLimitViolation);
227
+ if (this.verbose) {
228
+ console.log(`⚠️ [${this.ruleId}] Symbol: Rate limit violation found in '${funcName}'`);
229
+ }
230
+ }
231
+
232
+ // Check for missing account lockout
233
+ const lockoutViolation = this.analyzeAccountLockout(func, sourceFile);
234
+ lockoutChecked++;
235
+ if (lockoutViolation) {
236
+ violations.push(lockoutViolation);
237
+ if (this.verbose) {
238
+ console.log(`⚠️ [${this.ruleId}] Symbol: Account lockout violation found in '${funcName}'`);
239
+ }
240
+ }
241
+
242
+ } catch (error) {
243
+ funcErrors++;
244
+ if (this.verbose) {
245
+ console.log(
246
+ `🔍 [${this.ruleId}] Symbol: Error analyzing function:`,
247
+ error.message
248
+ );
249
+ }
250
+ }
251
+ }
252
+
253
+ // Check for missing protection in route definitions
254
+ const callExpressions = sourceFile.getDescendantsOfKind
255
+ ? sourceFile.getDescendantsOfKind(
256
+ require("typescript").SyntaxKind.CallExpression
257
+ )
258
+ : [];
259
+
260
+ if (this.verbose) {
261
+ console.log(`🔍 [${this.ruleId}] Symbol: Found ${callExpressions.length} call expressions to analyze`);
262
+ }
263
+
264
+ let routeChecked = 0;
265
+ let routeErrors = 0;
266
+
267
+ for (const callNode of callExpressions) {
268
+ try {
269
+ const routeViolation = this.analyzeRouteProtection(callNode, sourceFile);
270
+ routeChecked++;
271
+ if (routeViolation) {
272
+ violations.push(routeViolation);
273
+ if (this.verbose) {
274
+ console.log(`⚠️ [${this.ruleId}] Symbol: Route protection violation found`);
275
+ }
276
+ }
277
+ } catch (error) {
278
+ routeErrors++;
279
+ if (this.verbose) {
280
+ console.log(
281
+ `🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`,
282
+ error.message
283
+ );
284
+ }
285
+ }
286
+ }
287
+
288
+ const duration = Date.now() - startTime;
289
+
290
+ if (this.verbose) {
291
+ console.log(
292
+ `✅ [${this.ruleId}] Symbol: Analysis completed in ${duration}ms. ` +
293
+ `Found ${violations.length} violations from ${allFunctions.length} functions and ${callExpressions.length} call expressions. ` +
294
+ `Stats: auth=${authChecked}, rateLimit=${rateLimitChecked}, lockout=${lockoutChecked}, routes=${routeChecked}, ` +
295
+ `errors=${funcErrors + routeErrors}`
296
+ );
297
+ }
298
+
299
+ return violations;
300
+ } catch (error) {
301
+ if (this.verbose) {
302
+ console.log(
303
+ `🔍 [${this.ruleId}] Symbol: Error in source file analysis:`,
304
+ error.message
305
+ );
306
+ }
307
+ return [];
308
+ }
309
+ }
310
+
311
+ analyzeAuthenticationEndpoint(func, sourceFile) {
312
+ try {
313
+ const funcName = this.getFunctionName(func);
314
+ const funcText = func.getText();
315
+
316
+ // Check if this is an authentication endpoint
317
+ const isAuthEndpoint = this.authEndpoints.some(endpoint =>
318
+ funcName.toLowerCase().includes(endpoint) ||
319
+ funcText.toLowerCase().includes(`/${endpoint}`)
320
+ );
321
+
322
+ if (!isAuthEndpoint) {
323
+ return null;
324
+ }
325
+
326
+ if (this.verbose) {
327
+ console.log(
328
+ `🔍 [${this.ruleId}] Symbol: Authentication endpoint detected: ${funcName}`
329
+ );
330
+ }
331
+
332
+ // Check if protection mechanisms are present
333
+ const hasProtection = this.hasProtectionMechanisms(func, sourceFile);
334
+ if (!hasProtection) {
335
+ return this.createViolation(
336
+ sourceFile,
337
+ func,
338
+ `Authentication endpoint '${funcName}' lacks brute-force protection (rate limiting, account lockout, or CAPTCHA)`
339
+ );
340
+ }
341
+
342
+ return null;
343
+ } catch (error) {
344
+ if (this.verbose) {
345
+ console.log(
346
+ `🔍 [${this.ruleId}] Symbol: Error analyzing auth endpoint:`,
347
+ error.message
348
+ );
349
+ }
350
+ return null;
351
+ }
352
+ }
353
+
354
+ analyzeRateLimiting(func, sourceFile) {
355
+ try {
356
+ const funcName = this.getFunctionName(func);
357
+ const funcText = func.getText();
358
+
359
+ // Check if this is a login/auth function
360
+ const isAuthFunction = this.authEndpoints.some(endpoint =>
361
+ funcName.toLowerCase().includes(endpoint)
362
+ );
363
+
364
+ if (!isAuthFunction) {
365
+ return null;
366
+ }
367
+
368
+ // Check for rate limiting decorators or middleware
369
+ const hasRateLimit = this.hasRateLimiting(func, sourceFile);
370
+ if (!hasRateLimit) {
371
+ return this.createViolation(
372
+ sourceFile,
373
+ func,
374
+ `Authentication function '${funcName}' missing rate limiting protection`
375
+ );
376
+ }
377
+
378
+ return null;
379
+ } catch (error) {
380
+ if (this.verbose) {
381
+ console.log(
382
+ `🔍 [${this.ruleId}] Symbol: Error analyzing rate limiting:`,
383
+ error.message
384
+ );
385
+ }
386
+ return null;
387
+ }
388
+ }
389
+
390
+ analyzeAccountLockout(func, sourceFile) {
391
+ try {
392
+ const funcName = this.getFunctionName(func);
393
+ const funcText = func.getText();
394
+
395
+ // Check if this is a login/auth function
396
+ const isAuthFunction = this.authEndpoints.some(endpoint =>
397
+ funcName.toLowerCase().includes(endpoint)
398
+ );
399
+
400
+ if (!isAuthFunction) {
401
+ return null;
402
+ }
403
+
404
+ // Check for account lockout mechanisms
405
+ const hasLockout = this.hasAccountLockout(func, sourceFile);
406
+ if (!hasLockout) {
407
+ return this.createViolation(
408
+ sourceFile,
409
+ func,
410
+ `Authentication function '${funcName}' missing account lockout protection`
411
+ );
412
+ }
413
+
414
+ return null;
415
+ } catch (error) {
416
+ if (this.verbose) {
417
+ console.log(
418
+ `🔍 [${this.ruleId}] Symbol: Error analyzing account lockout:`,
419
+ error.message
420
+ );
421
+ }
422
+ return null;
423
+ }
424
+ }
425
+
426
+ analyzeRouteProtection(callNode, sourceFile) {
427
+ try {
428
+ const expression = callNode.getExpression();
429
+ const methodName = this.getMethodName(expression);
430
+
431
+ // Check for route definitions (app.post, router.get, etc.)
432
+ const routeMethods = ["post", "get", "put", "delete", "patch"];
433
+ if (!routeMethods.includes(methodName.toLowerCase())) {
434
+ return null;
435
+ }
436
+
437
+ const args = callNode.getArguments();
438
+ if (args.length === 0) {
439
+ return null;
440
+ }
441
+
442
+ const routePath = args[0].getText();
443
+ const isAuthRoute = this.authEndpoints.some(endpoint =>
444
+ routePath.toLowerCase().includes(`/${endpoint}`) ||
445
+ routePath.toLowerCase().includes(`"${endpoint}"`) ||
446
+ routePath.toLowerCase().includes(`'${endpoint}'`)
447
+ );
448
+
449
+ if (!isAuthRoute) {
450
+ return null;
451
+ }
452
+
453
+ // Check if route has protection middleware
454
+ const hasProtection = this.hasRouteProtection(callNode, sourceFile);
455
+ if (!hasProtection) {
456
+ return this.createViolation(
457
+ sourceFile,
458
+ callNode,
459
+ `Authentication route ${routePath} missing brute-force protection middleware`
460
+ );
461
+ }
462
+
463
+ return null;
464
+ } catch (error) {
465
+ if (this.verbose) {
466
+ console.log(
467
+ `🔍 [${this.ruleId}] Symbol: Error analyzing route protection:`,
468
+ error.message
469
+ );
470
+ }
471
+ return null;
472
+ }
473
+ }
474
+
475
+ hasProtectionMechanisms(func, sourceFile) {
476
+ try {
477
+ const funcText = func.getText();
478
+
479
+ // Check for decorators
480
+ const decorators = func.getDecorators ? func.getDecorators() : [];
481
+ const hasProtectionDecorator = decorators.some(decorator => {
482
+ const decoratorText = decorator.getText();
483
+ return this.protectionDecorators.some(pattern =>
484
+ decoratorText.includes(pattern)
485
+ );
486
+ });
487
+
488
+ if (hasProtectionDecorator) {
489
+ return true;
490
+ }
491
+
492
+ // Check for protection patterns in function body
493
+ const hasProtectionPattern = this.protectionPatterns.some(pattern => {
494
+ const regex = new RegExp(pattern, 'i');
495
+ return regex.test(funcText);
496
+ });
497
+
498
+ return hasProtectionPattern;
499
+ } catch (error) {
500
+ return false;
501
+ }
502
+ }
503
+
504
+ hasRateLimiting(func, sourceFile) {
505
+ try {
506
+ const funcText = func.getText();
507
+
508
+ // Check for rate limiting decorators
509
+ const decorators = func.getDecorators ? func.getDecorators() : [];
510
+ const hasRateLimitDecorator = decorators.some(decorator => {
511
+ const decoratorText = decorator.getText();
512
+ return decoratorText.includes('Throttle') ||
513
+ decoratorText.includes('RateLimit') ||
514
+ decoratorText.includes('ThrottleGuard');
515
+ });
516
+
517
+ if (hasRateLimitDecorator) {
518
+ return true;
519
+ }
520
+
521
+ // Check for rate limiting patterns
522
+ const rateLimitPatterns = [
523
+ /rate.*limit/i,
524
+ /throttle/i,
525
+ /windowMs/i,
526
+ /max.*requests/i,
527
+ /express-rate-limit/i,
528
+ /@nestjs\/throttler/i
529
+ ];
530
+
531
+ return rateLimitPatterns.some(pattern => pattern.test(funcText));
532
+ } catch (error) {
533
+ return false;
534
+ }
535
+ }
536
+
537
+ hasAccountLockout(func, sourceFile) {
538
+ try {
539
+ const funcText = func.getText();
540
+
541
+ // Check for lockout patterns
542
+ const lockoutPatterns = [
543
+ /lockout/i,
544
+ /max.*attempts/i,
545
+ /cooldown/i,
546
+ /account.*lock/i,
547
+ /brute.*force.*protection/i,
548
+ /express-brute/i,
549
+ /rate-limiter-flexible/i
550
+ ];
551
+
552
+ return lockoutPatterns.some(pattern => pattern.test(funcText));
553
+ } catch (error) {
554
+ return false;
555
+ }
556
+ }
557
+
558
+ hasRouteProtection(callNode, sourceFile) {
559
+ try {
560
+ // Check if there are middleware functions before the handler
561
+ const parent = callNode.getParent();
562
+ if (!parent) {
563
+ return false;
564
+ }
565
+
566
+ const parentText = parent.getText();
567
+
568
+ // Look for middleware patterns
569
+ const middlewarePatterns = [
570
+ /rateLimit/i,
571
+ /throttle/i,
572
+ /lockout/i,
573
+ /brute.*force/i,
574
+ /express-rate-limit/i,
575
+ /express-slow-down/i
576
+ ];
577
+
578
+ return middlewarePatterns.some(pattern => pattern.test(parentText));
579
+ } catch (error) {
580
+ return false;
581
+ }
582
+ }
583
+
584
+ getFunctionName(func) {
585
+ try {
586
+ if (func.getName) {
587
+ return func.getName();
588
+ }
589
+
590
+ // For arrow functions or anonymous functions
591
+ const parent = func.getParent();
592
+ if (parent && parent.getName) {
593
+ return parent.getName();
594
+ }
595
+
596
+ return "anonymous";
597
+ } catch (error) {
598
+ return "unknown";
599
+ }
600
+ }
601
+
602
+ getMethodName(expression) {
603
+ try {
604
+ const ts = require("typescript");
605
+
606
+ if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
607
+ return expression.getNameNode().getText();
608
+ }
609
+
610
+ if (expression.getKind() === ts.SyntaxKind.Identifier) {
611
+ return expression.getText();
612
+ }
613
+
614
+ return "";
615
+ } catch (error) {
616
+ return "";
617
+ }
618
+ }
619
+
620
+ createViolation(sourceFile, node, message) {
621
+ try {
622
+ const start = node.getStart();
623
+ const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
624
+
625
+ return {
626
+ rule: this.ruleId,
627
+ source: sourceFile.getFilePath(),
628
+ category: this.category,
629
+ line: lineAndChar.line,
630
+ column: lineAndChar.column,
631
+ message: message,
632
+ severity: "error",
633
+ };
634
+ } catch (error) {
635
+ if (this.verbose) {
636
+ console.log(
637
+ `🔍 [${this.ruleId}] Symbol: Error creating violation:`,
638
+ error.message
639
+ );
640
+ }
641
+ return null;
642
+ }
643
+ }
644
+ }
645
+
646
+ module.exports = S045SymbolBasedAnalyzer;