@sun-asterisk/sunlint 1.3.26 → 1.3.27

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 (43) hide show
  1. package/config/rules/enhanced-rules-registry.json +99 -16
  2. package/package.json +1 -1
  3. package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
  4. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
  5. package/rules/security/S003_open_redirect_protection/README.md +371 -0
  6. package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
  7. package/rules/security/S003_open_redirect_protection/config.json +58 -0
  8. package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
  9. package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
  10. package/rules/security/S004_sensitive_data_logging/config.json +62 -0
  11. package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
  12. package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
  13. package/rules/security/S012_hardcoded_secrets/config.json +75 -0
  14. package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
  15. package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
  16. package/rules/security/S019_smtp_injection_protection/config.json +35 -0
  17. package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
  18. package/rules/security/S022_escape_output_context/README.md +254 -0
  19. package/rules/security/S022_escape_output_context/analyzer.js +510 -0
  20. package/rules/security/S022_escape_output_context/config.json +229 -0
  21. package/rules/security/S023_no_json_injection/analyzer.js +15 -0
  22. package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
  23. package/rules/security/S023_no_json_injection/config.json +133 -0
  24. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
  25. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
  26. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
  27. package/rules/security/S029_csrf_protection/config.json +127 -0
  28. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
  29. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
  30. package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
  31. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
  32. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
  33. package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
  34. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
  35. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
  36. package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
  37. package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
  38. package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
  39. package/rules/security/S040_session_fixation_protection/config.json +20 -0
  40. package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
  41. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
  42. package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
  43. package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
@@ -496,16 +496,62 @@
496
496
  "tags": ["security", "idor", "access-control"]
497
497
  },
498
498
  "S003": {
499
- "name": "No Unvalidated Redirect",
500
- "description": "Prevent unvalidated redirects and forwards",
499
+ "name": "Open Redirect Protection",
500
+ "description": "URL redirects must validate against an allow list to prevent open redirect vulnerabilities",
501
501
  "category": "security",
502
502
  "severity": "error",
503
503
  "languages": ["typescript", "javascript"],
504
- "analyzer": "eslint",
505
- "eslintRule": "custom/typescript_s003",
504
+ "analyzer": "./rules/security/S003_open_redirect_protection/analyzer.js",
505
+ "config": "./rules/security/S003_open_redirect_protection/config.json",
506
+ "version": "1.0.0",
507
+ "status": "stable",
508
+ "tags": ["security", "owasp", "injection", "open-redirect", "phishing", "url-validation"],
509
+ "strategy": {
510
+ "preferred": "heuristic",
511
+ "fallbacks": ["heuristic"],
512
+ "accuracy": {
513
+ "heuristic": 95
514
+ }
515
+ },
516
+ "engineMappings": {
517
+ "heuristic": ["rules/security/S003_open_redirect_protection/analyzer.js"]
518
+ },
519
+ "metadata": {
520
+ "owaspCategory": "A03:2021 - Injection",
521
+ "cweId": "CWE-601",
522
+ "frameworks": ["Express", "NestJS", "Next.js", "Nuxt.js", "Spring Boot"],
523
+ "detectionPatterns": 28,
524
+ "testCases": 118
525
+ }
526
+ },
527
+ "S004": {
528
+ "name": "Sensitive Data Logging Protection",
529
+ "description": "Prevent logging of sensitive information like passwords, tokens, and payment data without proper redaction",
530
+ "category": "security",
531
+ "severity": "warning",
532
+ "languages": ["typescript", "javascript"],
533
+ "analyzer": "./rules/security/S004_sensitive_data_logging/analyzer.js",
534
+ "config": "./rules/security/S004_sensitive_data_logging/config.json",
506
535
  "version": "1.0.0",
507
536
  "status": "stable",
508
- "tags": ["security", "redirect", "validation"]
537
+ "tags": ["security", "owasp", "logging", "sensitive-data", "pii", "credentials", "data-exposure"],
538
+ "strategy": {
539
+ "preferred": "heuristic",
540
+ "fallbacks": ["heuristic"],
541
+ "accuracy": {
542
+ "heuristic": 90
543
+ }
544
+ },
545
+ "engineMappings": {
546
+ "heuristic": ["rules/security/S004_sensitive_data_logging/analyzer.js"]
547
+ },
548
+ "metadata": {
549
+ "owaspCategory": "A09:2021 - Security Logging and Monitoring Failures",
550
+ "cweId": "CWE-532",
551
+ "frameworks": ["Express", "NestJS", "Next.js", "Nuxt.js", "Spring Boot", "Winston", "Pino", "Bunyan"],
552
+ "detectionPatterns": 90,
553
+ "testCases": 45
554
+ }
509
555
  },
510
556
  "S005": {
511
557
  "name": "No Origin Header Authentication",
@@ -636,16 +682,34 @@
636
682
  "tags": ["security", "uuid", "random"]
637
683
  },
638
684
  "S012": {
639
- "name": "No Hardcoded Secrets",
640
- "description": "Prevent hardcoded secrets in source code",
685
+ "name": "Hardcoded Secrets Protection",
686
+ "description": "Detects hardcoded secrets, API keys, passwords, tokens, and credentials in source code to prevent accidental exposure through version control",
641
687
  "category": "security",
642
688
  "severity": "error",
643
689
  "languages": ["typescript", "javascript"],
644
- "analyzer": "eslint",
645
- "eslintRule": "custom/typescript_s012",
690
+ "analyzer": "./rules/security/S012_hardcoded_secrets/analyzer.js",
691
+ "config": "./rules/security/S012_hardcoded_secrets/config.json",
646
692
  "version": "1.0.0",
647
693
  "status": "stable",
648
- "tags": ["security", "secrets", "hardcoded"]
694
+ "tags": ["security", "owasp", "secrets", "credentials", "cryptographic-failures", "hardcoded-secrets", "api-keys", "passwords", "tokens"],
695
+ "strategy": {
696
+ "preferred": "heuristic",
697
+ "fallbacks": ["heuristic"],
698
+ "accuracy": {
699
+ "heuristic": 92
700
+ }
701
+ },
702
+ "engineMappings": {
703
+ "heuristic": ["rules/security/S012_hardcoded_secrets/analyzer.js"]
704
+ },
705
+ "metadata": {
706
+ "owaspCategory": "A02:2021 - Cryptographic Failures",
707
+ "cweId": "CWE-798",
708
+ "frameworks": ["Node.js", "Express", "NestJS", "Next.js", "React", "Vue", "Angular"],
709
+ "secretTypes": ["API Keys", "Passwords", "Access Tokens", "Private Keys", "JWT Secrets", "Database Credentials", "OAuth Secrets", "AWS Keys", "GitHub Tokens", "Slack Tokens"],
710
+ "detectionPatterns": 50,
711
+ "testCases": 30
712
+ }
649
713
  },
650
714
  "S013": {
651
715
  "name": "Verify TLS Connection",
@@ -736,16 +800,34 @@
736
800
  "tags": ["security", "validation", "input"]
737
801
  },
738
802
  "S019": {
739
- "name": "No Raw User Input in Email",
740
- "description": "Prevent raw user input in email content",
803
+ "name": "SMTP Injection Protection",
804
+ "description": "Detects potential SMTP/IMAP injection vulnerabilities by identifying unsanitized user input in email fields and direct SMTP protocol manipulation",
741
805
  "category": "security",
742
806
  "severity": "error",
743
807
  "languages": ["typescript", "javascript"],
744
- "analyzer": "eslint",
745
- "eslintRule": "custom/typescript_s019",
808
+ "analyzer": "./rules/security/S019_smtp_injection_protection/analyzer.js",
809
+ "config": "./rules/security/S019_smtp_injection_protection/config.json",
746
810
  "version": "1.0.0",
747
811
  "status": "stable",
748
- "tags": ["security", "email", "injection"]
812
+ "tags": ["security", "owasp", "injection", "smtp", "email", "crlf"],
813
+ "strategy": {
814
+ "preferred": "heuristic",
815
+ "fallbacks": ["heuristic"],
816
+ "accuracy": {
817
+ "heuristic": 90
818
+ }
819
+ },
820
+ "engineMappings": {
821
+ "heuristic": ["rules/security/S019_smtp_injection_protection/analyzer.js"]
822
+ },
823
+ "metadata": {
824
+ "owaspCategory": "A03:2021 - Injection",
825
+ "cweId": "CWE-93, CWE-144",
826
+ "frameworks": ["Node.js", "Express", "NestJS", "Next.js"],
827
+ "emailLibraries": ["nodemailer", "sendgrid", "mailgun", "aws-ses", "postmark"],
828
+ "detectionTypes": ["Unsanitized email fields", "SMTP command injection", "CRLF injection"],
829
+ "testCases": 40
830
+ }
749
831
  },
750
832
  "S020": {
751
833
  "name": "Avoid using eval() or executing dynamic code",
@@ -1156,7 +1238,8 @@
1156
1238
  "category": "security",
1157
1239
  "severity": "error",
1158
1240
  "languages": ["typescript", "javascript"],
1159
- "analyzer": "eslint",
1241
+ "analyzer": "./rules/security/S042_require_re_authentication_for_long_lived/analyzer.js",
1242
+ "config": "./rules/security/S042_require_re_authentication_for_long_lived/config.json",
1160
1243
  "eslintRule": "custom/typescript_s042",
1161
1244
  "version": "1.0.0",
1162
1245
  "status": "stable",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.26",
3
+ "version": "1.3.27",
4
4
  "description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -215,14 +215,18 @@ class C029Analyzer {
215
215
  } else {
216
216
  // No logging found - check if there's valid error handling
217
217
  const hasErrorHandling = this.hasValidErrorHandling(block, exceptionVar);
218
-
219
- if (!hasErrorHandling && !this.isTestFile(filePath)) {
218
+
219
+ // Skip if underscore variable (intentional ignore) AND has error handling
220
+ const isIntentionallyIgnored = exceptionVar && exceptionVar.startsWith('_');
221
+ const shouldSkip = (isIntentionallyIgnored && hasErrorHandling) || this.isTestFile(filePath);
222
+
223
+ if (!hasErrorHandling && !shouldSkip) {
220
224
  violations.push(this.createViolation(
221
225
  filePath,
222
226
  startLine,
223
227
  startColumn,
224
228
  'no_logging',
225
- exceptionVar
229
+ exceptionVar
226
230
  ? `Catch block does not log exception '${exceptionVar}'`
227
231
  : 'Catch block does not log exception',
228
232
  'Add console.error() or logger.error() with error details',
@@ -232,7 +236,10 @@ class C029Analyzer {
232
236
  }
233
237
 
234
238
  // STAGE 4: Check for unused exception variable
235
- if (exceptionVar && !this.isExceptionUsed(block, exceptionVar) && !this.hasExplicitIgnoreComment(catchClause)) {
239
+ // Skip if variable name starts with underscore (conventional way to indicate intentional ignore)
240
+ const isIntentionallyIgnored = exceptionVar && exceptionVar.startsWith('_');
241
+
242
+ if (exceptionVar && !isIntentionallyIgnored && !this.isExceptionUsed(block, exceptionVar) && !this.hasExplicitIgnoreComment(catchClause)) {
236
243
  violations.push(this.createViolation(
237
244
  filePath,
238
245
  startLine,
@@ -277,7 +284,8 @@ class C029Analyzer {
277
284
  usesExceptionVar: false,
278
285
  logLevels: [],
279
286
  hasStackTrace: false,
280
- hasContextData: false
287
+ hasContextData: false,
288
+ loggingExpressions: [] // Track actual expressions used
281
289
  };
282
290
 
283
291
  // Find all call expressions
@@ -333,6 +341,9 @@ class C029Analyzer {
333
341
  arguments: argsText,
334
342
  line: call.getStartLineNumber()
335
343
  });
344
+
345
+ // Track expression for better error messages
346
+ loggingInfo.loggingExpressions.push(expressionText);
336
347
  }
337
348
  }
338
349
 
@@ -367,21 +378,32 @@ class C029Analyzer {
367
378
  const issues = [];
368
379
 
369
380
  // Issue 1: Using inappropriate log level (log/info/debug instead of error/warn)
370
- const hasInappropriateLevel = loggingInfo.logLevels.some(level =>
371
- this.config.inappropriateLevels.includes(level)
372
- );
381
+ // BUT only for console.xxx, not for logger.xxx (custom loggers may have different semantics)
382
+ const hasInappropriateConsoleLevel = loggingInfo.calls.some(call => {
383
+ const isConsole = call.expression.startsWith('console.');
384
+ const isInappropriateLevel = this.config.inappropriateLevels.includes(call.level);
385
+ return isConsole && isInappropriateLevel;
386
+ });
387
+
388
+ if (hasInappropriateConsoleLevel && !this.isTestFile(filePath)) {
389
+ // Get only console calls with inappropriate levels
390
+ const consoleLevels = loggingInfo.calls
391
+ .filter(c => c.expression.startsWith('console.') && this.config.inappropriateLevels.includes(c.level))
392
+ .map(c => c.level);
373
393
 
374
- if (hasInappropriateLevel && !this.isTestFile(filePath)) {
375
394
  issues.push({
376
395
  type: 'inappropriate_log_level',
377
- message: `Catch block uses console.${loggingInfo.logLevels.join('/')} instead of console.error or console.warn`,
396
+ message: `Catch block uses console.${[...new Set(consoleLevels)].join('/')} instead of console.error or console.warn`,
378
397
  suggestion: 'Use console.error() or console.warn() for error logging in catch blocks',
379
398
  confidence: 0.75
380
399
  });
381
400
  }
382
401
 
383
402
  // Issue 2: Exception variable not included in logging
384
- if (exceptionVar && !loggingInfo.usesExceptionVar) {
403
+ // Skip if variable is underscore (intentional ignore - developer explicitly doesn't want error details)
404
+ const isIntentionallyIgnored = exceptionVar && exceptionVar.startsWith('_');
405
+
406
+ if (exceptionVar && !isIntentionallyIgnored && !loggingInfo.usesExceptionVar) {
385
407
  issues.push({
386
408
  type: 'exception_not_logged',
387
409
  message: `Exception variable '${exceptionVar}' is not included in logging`,
@@ -462,7 +484,20 @@ class C029Analyzer {
462
484
  /utils?\.log/i,
463
485
  /helpers?\.log/i,
464
486
  /ErrorUtils/,
465
- /ErrorService/
487
+ /ErrorService/,
488
+ // UI Feedback patterns (toast, notification, alert, etc.)
489
+ /toast\.(error|failed|show|warning)\s*\(/,
490
+ /notification\.(error|show|warning)\s*\(/,
491
+ /message\.(error|show|warning)\s*\(/,
492
+ /alert\s*\(/,
493
+ /showError\s*\(/,
494
+ /showMessage\s*\(/,
495
+ // Fallback/recovery actions
496
+ /window\.open\s*\(/,
497
+ /window\.location/,
498
+ /navigate\s*\(/,
499
+ /redirect\s*\(/,
500
+ /router\.(push|replace)\s*\(/
466
501
  ];
467
502
 
468
503
  if (errorHandlerPatterns.some(pattern => pattern.test(text))) {
@@ -768,32 +768,52 @@ class C033SymbolBasedAnalyzer {
768
768
  analyzeControllerFile(sourceFile, filePath) {
769
769
  const violations = [];
770
770
  const classes = sourceFile.getClasses();
771
-
771
+
772
772
  for (const cls of classes) {
773
773
  const className = cls.getName() || 'UnnamedClass';
774
774
  const methods = cls.getMethods();
775
-
775
+
776
776
  for (const method of methods) {
777
777
  const methodBody = method.getBodyText() || '';
778
-
778
+
779
779
  // Controllers should not directly access database
780
780
  for (const operation of this.databaseOperations) {
781
- const pattern = new RegExp(`\\b${operation}\\s*\\(`, 'i');
782
- if (pattern.test(methodBody)) {
783
- violations.push({
784
- ruleId: this.ruleId,
785
- severity: 'warning',
786
- message: `Controller class '${className}' should not directly access database with '${operation}'. Use Service layer instead.`,
787
- file: filePath,
788
- line: method.getStartLineNumber(),
789
- column: 1
790
- });
791
- break;
781
+ const operationPattern = new RegExp(`\\b${operation}\\s*\\(`, 'gi');
782
+ const matches = methodBody.matchAll(operationPattern);
783
+
784
+ for (const match of matches) {
785
+ const matchIndex = match.index;
786
+ const contextStart = Math.max(0, matchIndex - 50);
787
+ const context = methodBody.substring(contextStart, matchIndex);
788
+
789
+ // Check if this is a call through service (ALLOWED)
790
+ // Matches: this.xxxService.method(), this.xxxUseCase.method(), this.xxxHandler.method()
791
+ const serviceCallPattern = /this\.([\w]+Service|[\w]+UseCase|[\w]+Handler)\s*\.\s*$/i;
792
+ if (serviceCallPattern.test(context)) {
793
+ continue; // Skip - this is allowed
794
+ }
795
+
796
+ // Check if this is direct database call (VIOLATION)
797
+ // Matches: this.repository, this.userRepository, this.entityManager, this.xxxClient, etc.
798
+ const directDbCallPattern = /this\.([\w]*Repository|[\w]*Repo|dataSource|connection|entityManager|manager|database|db|[\w]*Client|prisma)\./i;
799
+ const globalDbCallPattern = /(getRepository|getConnection|getManager|createQueryBuilder)\s*\([^)]*\)\s*\.?[\w.]*$/i;
800
+
801
+ if (directDbCallPattern.test(context) || globalDbCallPattern.test(context)) {
802
+ violations.push({
803
+ ruleId: this.ruleId,
804
+ severity: 'warning',
805
+ message: `Controller class '${className}' should not directly access database with '${operation}'. Use Service layer instead.`,
806
+ file: filePath,
807
+ line: method.getStartLineNumber(),
808
+ column: 1
809
+ });
810
+ break;
811
+ }
792
812
  }
793
813
  }
794
814
  }
795
815
  }
796
-
816
+
797
817
  return violations;
798
818
  }
799
819
  }