@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.
- package/config/rules/enhanced-rules-registry.json +99 -16
- package/package.json +1 -1
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- 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": "
|
|
500
|
-
"description": "
|
|
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": "
|
|
505
|
-
"
|
|
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", "
|
|
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": "
|
|
640
|
-
"description": "
|
|
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": "
|
|
645
|
-
"
|
|
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": "
|
|
740
|
-
"description": "
|
|
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": "
|
|
745
|
-
"
|
|
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", "
|
|
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": "
|
|
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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
371
|
-
|
|
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.${
|
|
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 (
|
|
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
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
}
|