@sun-asterisk/sunlint 1.3.35 → 1.3.37

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 (103) hide show
  1. package/cli.js +33 -0
  2. package/config/rules/enhanced-rules-registry.json +354 -98
  3. package/config/rules/rules-registry-generated.json +197 -171
  4. package/core/architecture-integration.js +115 -17
  5. package/core/cli-action-handler.js +110 -26
  6. package/core/cli-program.js +14 -3
  7. package/core/github-annotate-service.js +62 -0
  8. package/core/impact-integration.js +309 -176
  9. package/core/init-command.js +227 -0
  10. package/core/output-service.js +53 -5
  11. package/core/summary-report-service.js +46 -0
  12. package/core/unified-rule-registry.js +2 -1
  13. package/engines/eslint-engine.js +6 -0
  14. package/engines/impact/core/detectors/database-detector.js +1 -1
  15. package/engines/impact/core/detectors/endpoint-detector.js +1 -1
  16. package/engines/impact/core/report-generator.js +235 -73
  17. package/origin-rules/security-en.md +470 -282
  18. package/package.json +1 -1
  19. package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
  20. package/rules/security/S001_backend_auth_communications/index.js +87 -0
  21. package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
  22. package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
  23. package/rules/security/S002_os_command_injection/index.js +87 -0
  24. package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
  25. package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
  26. package/rules/security/S008_svg_content_validation/index.js +87 -0
  27. package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
  28. package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
  29. package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
  30. package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
  31. package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
  32. package/rules/security/S021_referrer_policy/index.js +86 -0
  33. package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
  34. package/rules/security/S023_no_json_injection/config.json +133 -44
  35. package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
  36. package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
  37. package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
  38. package/rules/security/S026_tls_all_connections/config.json +30 -0
  39. package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
  40. package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
  41. package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
  42. package/rules/security/S035_separate_app_hostnames/config.json +28 -0
  43. package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
  44. package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
  45. package/rules/security/S039_tls_certificate_validation/config.json +29 -0
  46. package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
  47. package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
  48. package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
  49. package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
  50. package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
  51. package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
  52. package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
  53. package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
  54. package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
  55. package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
  56. package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
  57. package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
  58. package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
  59. package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
  60. package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
  61. package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
  62. package/rules/security/S053_generic_error_messages/config.json +28 -0
  63. package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
  64. package/rules/security/S053_generic_error_messages/index.js +86 -0
  65. package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
  66. package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
  67. package/rules/security/S059_disable_debug_mode/config.json +28 -0
  68. package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
  69. package/rules/security/S059_disable_debug_mode/index.js +86 -0
  70. package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
  71. package/rules/security/S060_password_minimum_length/config.json +28 -0
  72. package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
  73. package/rules/security/S060_password_minimum_length/index.js +86 -0
  74. package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
  75. package/rules/security/S026_json_schema_validation/config.json +0 -27
  76. package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
  77. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  78. package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
  79. package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
  80. package/rules/security/S035_path_session_cookies/config.json +0 -99
  81. package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
  82. package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
  83. package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
  84. package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
  85. package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
  86. package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
  87. package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
  88. package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
  89. package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
  90. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
  91. /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
  92. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
  93. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
  94. /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
  95. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
  96. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
  97. /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
  98. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
  99. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
  100. /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
  101. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
  102. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
  103. /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "S026",
3
+ "name": "Use TLS encryption for all inbound and outbound connections",
4
+ "description": "Ensure all application connections use encrypted TLS protocol, with no fallback to insecure or unencrypted protocols. All inbound (API endpoints, webhooks) and outbound (external APIs, databases, partner systems) connections must use TLS 1.2 minimum.",
5
+ "category": "security",
6
+ "severity": "critical",
7
+ "enabled": true,
8
+ "engines": ["heuristic"],
9
+ "enginePreference": ["heuristic"],
10
+ "tags": ["security", "tls", "encryption", "https", "network", "connections"],
11
+ "examples": {
12
+ "valid": [
13
+ "const client = new Client({ ssl: true });",
14
+ "fetch('https://api.example.com/data');",
15
+ "mongoose.connect('mongodb+srv://...');",
16
+ "const redis = new Redis({ tls: {} });"
17
+ ],
18
+ "invalid": [
19
+ "fetch('http://api.example.com/data');",
20
+ "const client = new Client({ ssl: false });",
21
+ "mongoose.connect('mongodb://localhost:27017');",
22
+ "const redis = new Redis({ host: 'redis.example.com' }); // No TLS"
23
+ ]
24
+ },
25
+ "fixable": false,
26
+ "docs": {
27
+ "description": "This rule ensures all application connections use TLS encryption. Covers inbound connections (API endpoints, web interfaces, webhooks), outbound connections (external APIs, databases, partner systems), and internal connections (monitoring, management tools, middleware, message queues). TLS 1.2 minimum required, prefer TLS 1.3. No fallback to HTTP or unencrypted protocols allowed.",
28
+ "url": "https://owasp.org/Top10/A02_2021-Cryptographic_Failures/"
29
+ }
30
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * S026 – Use TLS encryption for all inbound and outbound connections
3
+ *
4
+ * Detects insecure connections that don't use TLS/SSL encryption:
5
+ * - HTTP URLs instead of HTTPS
6
+ * - Database connections without SSL/TLS
7
+ * - Redis/cache connections without TLS
8
+ * - WebSocket ws:// instead of wss://
9
+ * - SSL/TLS explicitly disabled
10
+ */
11
+ // Command: node cli.js --rule=S026 --input=examples/rule-test-fixtures/rules/S026_tls_all_connections --engine=heuristic
12
+
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+
16
+ class S026Analyzer {
17
+ constructor() {
18
+ this.ruleId = "S026";
19
+ this.ruleName =
20
+ "Use TLS encryption for all inbound and outbound connections";
21
+ this.description =
22
+ "Ensure all connections use TLS encryption, no fallback to unencrypted protocols";
23
+
24
+ // Patterns for insecure connections
25
+ this.insecurePatterns = [
26
+ // HTTP URLs (excluding localhost for dev)
27
+ {
28
+ pattern:
29
+ /['"`](http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)[^'"`]+)['"`]/g,
30
+ message: "Insecure HTTP connection detected. Use HTTPS instead",
31
+ type: "http_url",
32
+ },
33
+ // WebSocket without TLS
34
+ {
35
+ pattern: /['"`](ws:\/\/(?!localhost|127\.0\.0\.1)[^'"`]+)['"`]/g,
36
+ message: "Insecure WebSocket connection detected. Use WSS instead",
37
+ type: "ws_url",
38
+ },
39
+ // MongoDB without TLS
40
+ {
41
+ pattern: /['"`](mongodb:\/\/(?!localhost|127\.0\.0\.1)[^'"`]+)['"`]/g,
42
+ message:
43
+ "MongoDB connection without TLS. Use mongodb+srv:// or add ssl=true",
44
+ type: "mongodb_url",
45
+ },
46
+ // Redis without TLS (rediss:// is secure)
47
+ {
48
+ pattern: /['"`](redis:\/\/(?!localhost|127\.0\.0\.1)[^'"`]+)['"`]/g,
49
+ message: "Redis connection without TLS. Use rediss:// or configure TLS",
50
+ type: "redis_url",
51
+ },
52
+ // FTP instead of SFTP
53
+ {
54
+ pattern: /['"`](ftp:\/\/[^'"`]+)['"`]/g,
55
+ message: "Insecure FTP connection detected. Use SFTP instead",
56
+ type: "ftp_url",
57
+ },
58
+ // AMQP without TLS
59
+ {
60
+ pattern: /['"`](amqp:\/\/(?!localhost|127\.0\.0\.1)[^'"`]+)['"`]/g,
61
+ message: "AMQP connection without TLS. Use amqps:// instead",
62
+ type: "amqp_url",
63
+ },
64
+ ];
65
+
66
+ // Patterns for explicitly disabled SSL/TLS
67
+ this.disabledSslPatterns = [
68
+ {
69
+ pattern: /ssl\s*:\s*false/gi,
70
+ message:
71
+ "SSL explicitly disabled. Enable SSL/TLS for secure connections",
72
+ type: "ssl_disabled",
73
+ },
74
+ {
75
+ pattern: /tls\s*:\s*false/gi,
76
+ message: "TLS explicitly disabled. Enable TLS for secure connections",
77
+ type: "tls_disabled",
78
+ },
79
+ {
80
+ pattern: /rejectUnauthorized\s*:\s*false/gi,
81
+ message:
82
+ "TLS certificate validation disabled. This allows MITM attacks",
83
+ type: "reject_unauthorized_false",
84
+ },
85
+ {
86
+ pattern: /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"`]?0['"`]?/gi,
87
+ message:
88
+ "NODE_TLS_REJECT_UNAUTHORIZED=0 disables certificate validation",
89
+ type: "node_tls_reject",
90
+ },
91
+ {
92
+ pattern: /verify\s*=\s*False/g,
93
+ message: "SSL verification disabled in Python requests",
94
+ type: "python_verify_false",
95
+ },
96
+ {
97
+ pattern: /InsecureSkipVerify\s*:\s*true/gi,
98
+ message: "InsecureSkipVerify disables TLS certificate validation in Go",
99
+ type: "go_insecure_skip",
100
+ },
101
+ ];
102
+
103
+ // Files/paths to skip
104
+ this.skipPatterns = [
105
+ /\.test\./i,
106
+ /\.spec\./i,
107
+ /test\//i,
108
+ /tests\//i,
109
+ /__tests__\//i,
110
+ /mock/i,
111
+ /fixture/i,
112
+ /example/i,
113
+ /\.md$/i,
114
+ /node_modules/i,
115
+ ];
116
+ }
117
+
118
+ shouldSkipFile(filePath) {
119
+ return this.skipPatterns.some((pattern) => pattern.test(filePath));
120
+ }
121
+
122
+ async analyze(files, language, options = {}) {
123
+ const violations = [];
124
+
125
+ for (const filePath of files) {
126
+ if (this.shouldSkipFile(filePath)) {
127
+ continue;
128
+ }
129
+
130
+ try {
131
+ const content = fs.readFileSync(filePath, "utf8");
132
+ const fileViolations = this.analyzeFile(content, filePath);
133
+ violations.push(...fileViolations);
134
+ } catch (error) {
135
+ if (options.verbose) {
136
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ return violations;
142
+ }
143
+
144
+ analyzeFile(content, filePath) {
145
+ const violations = [];
146
+ const lines = content.split("\n");
147
+
148
+ // Check for insecure URL patterns
149
+ for (const { pattern, message, type } of this.insecurePatterns) {
150
+ // Reset regex lastIndex
151
+ pattern.lastIndex = 0;
152
+
153
+ let match;
154
+ while ((match = pattern.exec(content)) !== null) {
155
+ const url = match[1];
156
+
157
+ // Skip if it's in a comment
158
+ const lineNum = this.getLineNumber(content, match.index);
159
+ const line = lines[lineNum - 1];
160
+ if (
161
+ this.isComment(
162
+ line,
163
+ match.index - this.getLineStartIndex(content, lineNum),
164
+ )
165
+ ) {
166
+ continue;
167
+ }
168
+
169
+ // Skip localhost/dev URLs
170
+ if (this.isLocalhost(url)) {
171
+ continue;
172
+ }
173
+
174
+ // Skip excluded URLs (namespaces, examples, documentation)
175
+ if (this.isExcludedUrl(url, line)) {
176
+ continue;
177
+ }
178
+
179
+ // Skip URLs guarded by localhost/emulator checks
180
+ if (this.isLocalhostGuarded(lines, lineNum)) {
181
+ continue;
182
+ }
183
+
184
+ violations.push({
185
+ file: filePath,
186
+ line: lineNum,
187
+ column: this.getColumnNumber(content, match.index),
188
+ message: `${message}: "${url}"`,
189
+ severity: "warning",
190
+ ruleId: this.ruleId,
191
+ type: type,
192
+ insecureUrl: url,
193
+ });
194
+ }
195
+ }
196
+
197
+ // Check for disabled SSL/TLS patterns
198
+ for (const { pattern, message, type } of this.disabledSslPatterns) {
199
+ pattern.lastIndex = 0;
200
+
201
+ let match;
202
+ while ((match = pattern.exec(content)) !== null) {
203
+ const lineNum = this.getLineNumber(content, match.index);
204
+ const line = lines[lineNum - 1];
205
+
206
+ // Skip if it's in a comment
207
+ if (
208
+ this.isComment(
209
+ line,
210
+ match.index - this.getLineStartIndex(content, lineNum),
211
+ )
212
+ ) {
213
+ continue;
214
+ }
215
+
216
+ // Skip test/development context
217
+ if (this.isTestContext(lines, lineNum)) {
218
+ continue;
219
+ }
220
+
221
+ violations.push({
222
+ file: filePath,
223
+ line: lineNum,
224
+ column: this.getColumnNumber(content, match.index),
225
+ message: message,
226
+ severity: "warning",
227
+ ruleId: this.ruleId,
228
+ type: type,
229
+ });
230
+ }
231
+ }
232
+
233
+ return violations;
234
+ }
235
+
236
+ isComment(line, column) {
237
+ const trimmed = line.trim();
238
+ if (
239
+ trimmed.startsWith("//") ||
240
+ trimmed.startsWith("*") ||
241
+ trimmed.startsWith("/*")
242
+ ) {
243
+ return true;
244
+ }
245
+ // Check if the match is after a // in the line
246
+ const beforeMatch = line.substring(0, column);
247
+ return beforeMatch.includes("//");
248
+ }
249
+
250
+ isLocalhost(url) {
251
+ return /localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]/i.test(url);
252
+ }
253
+
254
+ isExcludedUrl(url, line) {
255
+ // XML/SVG namespaces - not actual connections
256
+ if (/w3\.org\/\d{4}\//.test(url)) {
257
+ return true;
258
+ }
259
+
260
+ // Schema.org - metadata schemas
261
+ if (/schema\.org/i.test(url)) {
262
+ return true;
263
+ }
264
+
265
+ // Example/placeholder URLs in documentation
266
+ if (/example\.(com|org|net)/i.test(url)) {
267
+ return true;
268
+ }
269
+
270
+ // URLs in @ApiProperty example, @example, or similar documentation decorators
271
+ if (
272
+ /@(ApiProperty|example|Example)\s*\(/.test(line) ||
273
+ /example\s*[:=]/i.test(line)
274
+ ) {
275
+ return true;
276
+ }
277
+
278
+ // XML namespace declarations
279
+ if (/xmlns\s*[:=]/i.test(line)) {
280
+ return true;
281
+ }
282
+
283
+ // HTML meta tags - not actual connections
284
+ if (/<meta\s+[^>]*content\s*=/i.test(line)) {
285
+ return true;
286
+ }
287
+
288
+ // URL parsing/validation - new URL() used for validation, not connection
289
+ if (/new\s+URL\s*\(/i.test(line)) {
290
+ return true;
291
+ }
292
+
293
+ return false;
294
+ }
295
+
296
+ isLocalhostGuarded(lines, lineNum) {
297
+ // Check if the code is guarded by localhost/emulator check
298
+ const start = Math.max(0, lineNum - 10);
299
+ const context = lines.slice(start, lineNum).join("\n").toLowerCase();
300
+
301
+ return (
302
+ /if\s*\([^)]*localhost[^)]*\)/i.test(context) ||
303
+ /if\s*\([^)]*islocal[^)]*\)/i.test(context) ||
304
+ /if\s*\([^)]*emulator[^)]*\)/i.test(context) ||
305
+ /hostname\s*===?\s*['"`]localhost['"`]/i.test(context)
306
+ );
307
+ }
308
+
309
+ isTestContext(lines, lineNum) {
310
+ // Check surrounding lines for test context
311
+ const start = Math.max(0, lineNum - 5);
312
+ const end = Math.min(lines.length, lineNum + 2);
313
+ const context = lines.slice(start, end).join("\n").toLowerCase();
314
+
315
+ return /describe\(|it\(|test\(|jest|mocha|beforeeach|aftereach|mock|stub|fake/i.test(
316
+ context,
317
+ );
318
+ }
319
+
320
+ getLineNumber(content, index) {
321
+ return content.substring(0, index).split("\n").length;
322
+ }
323
+
324
+ getColumnNumber(content, index) {
325
+ const lastNewline = content.lastIndexOf("\n", index - 1);
326
+ return index - lastNewline;
327
+ }
328
+
329
+ getLineStartIndex(content, lineNum) {
330
+ const lines = content.split("\n");
331
+ let index = 0;
332
+ for (let i = 0; i < lineNum - 1; i++) {
333
+ index += lines[i].length + 1;
334
+ }
335
+ return index;
336
+ }
337
+ }
338
+
339
+ module.exports = S026Analyzer;
@@ -0,0 +1,30 @@
1
+ {
2
+ "id": "S027",
3
+ "name": "Validate mTLS client certificates before authentication",
4
+ "description": "Ensure mTLS client certificates are properly validated and trusted before using certificate identity for authentication or authorization decisions. Verify certificate is signed by trusted CA, not expired, not revoked, and subject/SAN matches expected identity.",
5
+ "category": "security",
6
+ "severity": "critical",
7
+ "enabled": true,
8
+ "engines": ["heuristic"],
9
+ "enginePreference": ["heuristic"],
10
+ "tags": ["security", "mtls", "certificates", "authentication", "tls"],
11
+ "examples": {
12
+ "valid": [
13
+ "const cert = req.socket.getPeerCertificate(); if (!cert.valid_to || !cert.issuer) throw new Error('Invalid cert');",
14
+ "// Check certificate chain validation",
15
+ "// Verify certificate not expired: cert.valid_to > Date.now()",
16
+ "// Check certificate revocation via OCSP/CRL"
17
+ ],
18
+ "invalid": [
19
+ "const cert = req.socket.getPeerCertificate(); processRequest(cert.subject.CN); // No validation",
20
+ "// Accepting any valid certificate without identity check",
21
+ "// Skipping revocation checks",
22
+ "// Using certificate CN without proper validation"
23
+ ]
24
+ },
25
+ "fixable": false,
26
+ "docs": {
27
+ "description": "This rule ensures mTLS client certificates are properly validated before trusting certificate identity. Validation includes: verify certificate is signed by trusted CA, check certificate has not expired, validate certificate is not revoked (CRL/OCSP), confirm certificate subject/SAN matches expected identity, full chain validation up to root CA, check Extended Key Usage for client authentication.",
28
+ "url": "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
29
+ }
30
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * S027 – Validate mTLS client certificates before authentication
3
+ *
4
+ * Detects improper mTLS certificate validation:
5
+ * - Using certificate identity without validation
6
+ * - Missing expiration checks
7
+ * - Missing revocation checks (CRL/OCSP)
8
+ * - Missing CA chain validation
9
+ * - Accepting any certificate without identity verification
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ class S027Analyzer {
16
+ constructor() {
17
+ this.ruleId = 'S027';
18
+ this.ruleName = 'Validate mTLS client certificates before authentication';
19
+ this.description = 'Ensure mTLS client certificates are properly validated before trusting certificate identity';
20
+
21
+ // Patterns for certificate usage without validation
22
+ this.unsafePatterns = [
23
+ // Using certificate subject/CN directly without validation
24
+ {
25
+ pattern: /getPeerCertificate\(\)[^}]*\.subject\.CN/g,
26
+ message: 'Using certificate CN without proper validation. Verify certificate chain, expiration, and revocation first',
27
+ type: 'direct_cn_usage'
28
+ },
29
+ {
30
+ pattern: /getPeerCertificate\(\)[^}]*\.subject/g,
31
+ message: 'Using certificate subject without validation. Verify certificate is trusted before using identity',
32
+ type: 'direct_subject_usage'
33
+ },
34
+ // Certificate usage without checking authorized property
35
+ {
36
+ pattern: /socket\.authorized\s*!==\s*true|socket\.authorized\s*===\s*false/g,
37
+ message: 'Certificate authorization check failed but connection may continue',
38
+ type: 'auth_check_failed'
39
+ }
40
+ ];
41
+
42
+ // Patterns that indicate proper validation
43
+ this.validationPatterns = [
44
+ /\.authorized\s*===?\s*true/i,
45
+ /\.valid_to\s*[<>]/i,
46
+ /new Date\([^)]*valid/i,
47
+ /checkCRL|checkOCSP|ocsp|crl/i,
48
+ /verify.*chain|chain.*verify/i,
49
+ /issuer.*verify|verify.*issuer/i,
50
+ /ca.*verify|verify.*ca/i
51
+ ];
52
+
53
+ // Patterns for disabled certificate validation (critical issues)
54
+ this.disabledValidationPatterns = [
55
+ {
56
+ pattern: /rejectUnauthorized\s*:\s*false/gi,
57
+ message: 'Certificate validation disabled. This allows any certificate including self-signed and expired',
58
+ type: 'reject_unauthorized_false'
59
+ },
60
+ {
61
+ pattern: /requestCert\s*:\s*false/gi,
62
+ message: 'Client certificate not required. Enable requestCert for mTLS',
63
+ type: 'request_cert_disabled'
64
+ },
65
+ {
66
+ pattern: /checkServerIdentity\s*:\s*\(\)\s*=>\s*(undefined|null|true)/gi,
67
+ message: 'Server identity check bypassed. Implement proper hostname verification',
68
+ type: 'identity_check_bypassed'
69
+ }
70
+ ];
71
+
72
+ // Files to skip
73
+ this.skipPatterns = [
74
+ /\.test\./i,
75
+ /\.spec\./i,
76
+ /test\//i,
77
+ /tests\//i,
78
+ /__tests__\//i,
79
+ /mock/i,
80
+ /fixture/i,
81
+ /example/i,
82
+ /node_modules/i
83
+ ];
84
+ }
85
+
86
+ shouldSkipFile(filePath) {
87
+ return this.skipPatterns.some(pattern => pattern.test(filePath));
88
+ }
89
+
90
+ async analyze(files, language, options = {}) {
91
+ const violations = [];
92
+
93
+ for (const filePath of files) {
94
+ if (this.shouldSkipFile(filePath)) {
95
+ continue;
96
+ }
97
+
98
+ try {
99
+ const content = fs.readFileSync(filePath, 'utf8');
100
+ const fileViolations = this.analyzeFile(content, filePath);
101
+ violations.push(...fileViolations);
102
+ } catch (error) {
103
+ if (options.verbose) {
104
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
105
+ }
106
+ }
107
+ }
108
+
109
+ return violations;
110
+ }
111
+
112
+ analyzeFile(content, filePath) {
113
+ const violations = [];
114
+ const lines = content.split('\n');
115
+
116
+ // Check if file deals with certificates/mTLS
117
+ if (!this.isCertificateRelatedFile(content)) {
118
+ return violations;
119
+ }
120
+
121
+ // Check for disabled validation patterns (most critical)
122
+ for (const { pattern, message, type } of this.disabledValidationPatterns) {
123
+ pattern.lastIndex = 0;
124
+
125
+ let match;
126
+ while ((match = pattern.exec(content)) !== null) {
127
+ const lineNum = this.getLineNumber(content, match.index);
128
+ const line = lines[lineNum - 1];
129
+
130
+ if (this.isComment(line)) {
131
+ continue;
132
+ }
133
+
134
+ if (this.isTestContext(lines, lineNum)) {
135
+ continue;
136
+ }
137
+
138
+ violations.push({
139
+ file: filePath,
140
+ line: lineNum,
141
+ column: this.getColumnNumber(content, match.index),
142
+ message: message,
143
+ severity: 'error',
144
+ ruleId: this.ruleId,
145
+ type: type
146
+ });
147
+ }
148
+ }
149
+
150
+ // Check for unsafe certificate usage patterns
151
+ for (const { pattern, message, type } of this.unsafePatterns) {
152
+ pattern.lastIndex = 0;
153
+
154
+ let match;
155
+ while ((match = pattern.exec(content)) !== null) {
156
+ const lineNum = this.getLineNumber(content, match.index);
157
+ const line = lines[lineNum - 1];
158
+
159
+ if (this.isComment(line)) {
160
+ continue;
161
+ }
162
+
163
+ // Check if there's proper validation nearby
164
+ const contextStart = Math.max(0, lineNum - 10);
165
+ const contextEnd = Math.min(lines.length, lineNum + 5);
166
+ const context = lines.slice(contextStart, contextEnd).join('\n');
167
+
168
+ const hasValidation = this.validationPatterns.some(vp => vp.test(context));
169
+
170
+ if (!hasValidation) {
171
+ violations.push({
172
+ file: filePath,
173
+ line: lineNum,
174
+ column: this.getColumnNumber(content, match.index),
175
+ message: message,
176
+ severity: 'warning',
177
+ ruleId: this.ruleId,
178
+ type: type
179
+ });
180
+ }
181
+ }
182
+ }
183
+
184
+ return violations;
185
+ }
186
+
187
+ isCertificateRelatedFile(content) {
188
+ const certPatterns = [
189
+ /getPeerCertificate/i,
190
+ /requestCert/i,
191
+ /rejectUnauthorized/i,
192
+ /tls\.|https\./i,
193
+ /\.crt|\.pem|\.key/i,
194
+ /certificate|cert\b/i,
195
+ /mTLS|mtls/i,
196
+ /client.*cert|cert.*client/i
197
+ ];
198
+
199
+ return certPatterns.some(pattern => pattern.test(content));
200
+ }
201
+
202
+ isComment(line) {
203
+ const trimmed = line.trim();
204
+ return trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*');
205
+ }
206
+
207
+ isTestContext(lines, lineNum) {
208
+ const start = Math.max(0, lineNum - 5);
209
+ const end = Math.min(lines.length, lineNum + 2);
210
+ const context = lines.slice(start, end).join('\n').toLowerCase();
211
+
212
+ return /describe\(|it\(|test\(|jest|mocha|mock|stub|fake/i.test(context);
213
+ }
214
+
215
+ getLineNumber(content, index) {
216
+ return content.substring(0, index).split('\n').length;
217
+ }
218
+
219
+ getColumnNumber(content, index) {
220
+ const lastNewline = content.lastIndexOf('\n', index - 1);
221
+ return index - lastNewline;
222
+ }
223
+ }
224
+
225
+ module.exports = S027Analyzer;
@@ -0,0 +1,28 @@
1
+ {
2
+ "id": "S035",
3
+ "name": "Host separate applications on different hostnames",
4
+ "description": "Leverage same-origin policy restrictions by hosting separate applications on different hostnames to isolate resources, cookies, and prevent cross-application attacks. Each application should have its own hostname/subdomain.",
5
+ "category": "security",
6
+ "severity": "medium",
7
+ "enabled": true,
8
+ "engines": ["heuristic"],
9
+ "enginePreference": ["heuristic"],
10
+ "tags": ["security", "same-origin", "isolation", "cors", "hostname", "architecture"],
11
+ "examples": {
12
+ "valid": [
13
+ "// Good: Separate hostnames for different apps",
14
+ "// app1.example.com, app2.example.com, admin.example.com",
15
+ "// API: api.example.com, Frontend: www.example.com"
16
+ ],
17
+ "invalid": [
18
+ "// Bad: Same origin for multiple apps",
19
+ "// example.com/app1, example.com/app2",
20
+ "// Shared cookies and localStorage between apps"
21
+ ]
22
+ },
23
+ "fixable": false,
24
+ "docs": {
25
+ "description": "This rule ensures applications are hosted on separate hostnames to leverage same-origin policy benefits. Same-origin policy prevents scripts from one origin accessing resources from another, isolates cookies and storage per hostname, and limits impact of XSS to single application. Avoid hosting multiple apps on same origin with path-based routing (example.com/app1, example.com/app2).",
26
+ "url": "https://owasp.org/www-community/attacks/Cross-site_Scripting_(XSS)"
27
+ }
28
+ }