@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.
- package/cli.js +33 -0
- package/config/rules/enhanced-rules-registry.json +354 -98
- package/config/rules/rules-registry-generated.json +197 -171
- package/core/architecture-integration.js +115 -17
- package/core/cli-action-handler.js +110 -26
- package/core/cli-program.js +14 -3
- package/core/github-annotate-service.js +62 -0
- package/core/impact-integration.js +309 -176
- package/core/init-command.js +227 -0
- package/core/output-service.js +53 -5
- package/core/summary-report-service.js +46 -0
- package/core/unified-rule-registry.js +2 -1
- package/engines/eslint-engine.js +6 -0
- package/engines/impact/core/detectors/database-detector.js +1 -1
- package/engines/impact/core/detectors/endpoint-detector.js +1 -1
- package/engines/impact/core/report-generator.js +235 -73
- package/origin-rules/security-en.md +470 -282
- package/package.json +1 -1
- package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
- package/rules/security/S001_backend_auth_communications/index.js +87 -0
- package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
- package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
- package/rules/security/S002_os_command_injection/index.js +87 -0
- package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
- package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
- package/rules/security/S008_svg_content_validation/index.js +87 -0
- package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
- package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
- package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
- package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
- package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
- package/rules/security/S021_referrer_policy/index.js +86 -0
- package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
- package/rules/security/S023_no_json_injection/config.json +133 -44
- package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
- package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
- package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
- package/rules/security/S026_tls_all_connections/config.json +30 -0
- package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
- package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
- package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
- package/rules/security/S035_separate_app_hostnames/config.json +28 -0
- package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
- package/rules/security/S039_tls_certificate_validation/config.json +29 -0
- package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
- package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
- package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
- package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
- package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
- package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
- package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
- package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
- package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
- package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
- package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
- package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
- package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
- package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
- package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
- package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
- package/rules/security/S053_generic_error_messages/config.json +28 -0
- package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
- package/rules/security/S053_generic_error_messages/index.js +86 -0
- package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
- package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
- package/rules/security/S059_disable_debug_mode/config.json +28 -0
- package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
- package/rules/security/S059_disable_debug_mode/index.js +86 -0
- package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
- package/rules/security/S060_password_minimum_length/config.json +28 -0
- package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
- package/rules/security/S060_password_minimum_length/index.js +86 -0
- package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
- package/rules/security/S026_json_schema_validation/config.json +0 -27
- package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
- package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
- package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
- package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
- package/rules/security/S035_path_session_cookies/config.json +0 -99
- package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
- package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
- package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
- package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
- package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
- package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
- package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
- package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
- package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
- /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
|
+
}
|