@sun-asterisk/sunlint 1.3.36 → 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 +101 -27
- package/core/cli-program.js +5 -0
- package/core/github-annotate-service.js +62 -0
- package/core/impact-integration.js +31 -16
- 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,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S035 – Host separate applications on different hostnames
|
|
3
|
+
*
|
|
4
|
+
* Detects patterns that suggest multiple applications sharing the same origin:
|
|
5
|
+
* - Path-based application routing on same domain
|
|
6
|
+
* - Shared cookies across application paths
|
|
7
|
+
* - Missing CORS isolation between apps
|
|
8
|
+
* - Proxy configurations routing multiple apps to same origin
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
class S035Analyzer {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.ruleId = 'S035';
|
|
17
|
+
this.ruleName = 'Host separate applications on different hostnames';
|
|
18
|
+
this.description = 'Leverage same-origin policy by hosting applications on separate hostnames';
|
|
19
|
+
|
|
20
|
+
// Patterns indicating path-based multi-app hosting (bad practice)
|
|
21
|
+
this.pathBasedAppPatterns = [
|
|
22
|
+
// Proxy/routing configurations with path-based apps
|
|
23
|
+
{
|
|
24
|
+
pattern: /proxy\s*:\s*\{[^}]*['"`]\/app[12]|\/admin|\/portal['"`]\s*:/gi,
|
|
25
|
+
message: 'Path-based application routing detected. Consider using separate hostnames for isolation',
|
|
26
|
+
type: 'path_based_proxy'
|
|
27
|
+
},
|
|
28
|
+
// Express/route-based app mounting
|
|
29
|
+
{
|
|
30
|
+
pattern: /app\.use\s*\(\s*['"`]\/(app[12]|admin|portal|api)['"`]\s*,\s*\w+App/gi,
|
|
31
|
+
message: 'Multiple applications mounted on same origin with path prefix. Use separate hostnames',
|
|
32
|
+
type: 'app_mounting'
|
|
33
|
+
},
|
|
34
|
+
// Next.js/Nginx rewrite rules for multi-app
|
|
35
|
+
{
|
|
36
|
+
pattern: /rewrites?\s*.*['"`]\/(app[12]|admin|portal)\/\*['"`]/gi,
|
|
37
|
+
message: 'Rewrite rules suggesting multiple apps on same origin',
|
|
38
|
+
type: 'rewrite_rules'
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Patterns for shared cookies across paths (indicates shared origin issue)
|
|
43
|
+
this.sharedCookiePatterns = [
|
|
44
|
+
// Cookie with root path for multi-app
|
|
45
|
+
{
|
|
46
|
+
pattern: /cookie\s*\([^)]*path\s*:\s*['"`]\/['"`][^)]*\).*(?:app|admin|portal)/gi,
|
|
47
|
+
message: 'Root path cookie may be shared across multiple applications on same origin',
|
|
48
|
+
type: 'shared_root_cookie'
|
|
49
|
+
},
|
|
50
|
+
// Domain-wide cookie settings
|
|
51
|
+
{
|
|
52
|
+
pattern: /domain\s*:\s*['"`]\.?[^'"`]+['"`].*(?:shared|common)/gi,
|
|
53
|
+
message: 'Domain-wide cookie may leak across applications',
|
|
54
|
+
type: 'domain_cookie'
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Configuration patterns suggesting multi-app on single origin
|
|
59
|
+
this.configPatterns = [
|
|
60
|
+
// Docker/nginx config with location-based routing
|
|
61
|
+
{
|
|
62
|
+
pattern: /location\s+\/(app[12]|admin|portal)\s*\{[^}]*proxy_pass/gi,
|
|
63
|
+
message: 'Nginx location-based routing for multiple apps. Consider separate server blocks with different hostnames',
|
|
64
|
+
type: 'nginx_location_routing'
|
|
65
|
+
},
|
|
66
|
+
// Kubernetes ingress with path-based routing
|
|
67
|
+
{
|
|
68
|
+
pattern: /path:\s*\/(app[12]|admin|portal)/gi,
|
|
69
|
+
message: 'Kubernetes Ingress path-based routing. Consider separate Ingress resources with different hosts',
|
|
70
|
+
type: 'k8s_path_routing'
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// Files to analyze (configuration files)
|
|
75
|
+
this.targetPatterns = [
|
|
76
|
+
/nginx\.conf$/i,
|
|
77
|
+
/\.conf$/i,
|
|
78
|
+
/docker-compose/i,
|
|
79
|
+
/kubernetes|k8s/i,
|
|
80
|
+
/ingress/i,
|
|
81
|
+
/proxy/i,
|
|
82
|
+
/next\.config/i,
|
|
83
|
+
/nuxt\.config/i,
|
|
84
|
+
/vite\.config/i,
|
|
85
|
+
/webpack\.config/i
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Files to skip
|
|
89
|
+
this.skipPatterns = [
|
|
90
|
+
/\.test\./i,
|
|
91
|
+
/\.spec\./i,
|
|
92
|
+
/node_modules/i,
|
|
93
|
+
/\.md$/i
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
shouldSkipFile(filePath) {
|
|
98
|
+
return this.skipPatterns.some(pattern => pattern.test(filePath));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
isTargetFile(filePath) {
|
|
102
|
+
return this.targetPatterns.some(pattern => pattern.test(filePath));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async analyze(files, language, options = {}) {
|
|
106
|
+
const violations = [];
|
|
107
|
+
|
|
108
|
+
for (const filePath of files) {
|
|
109
|
+
if (this.shouldSkipFile(filePath)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Only analyze configuration-related files
|
|
114
|
+
if (!this.isTargetFile(filePath)) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
120
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
121
|
+
violations.push(...fileViolations);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (options.verbose) {
|
|
124
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return violations;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
analyzeFile(content, filePath) {
|
|
133
|
+
const violations = [];
|
|
134
|
+
const lines = content.split('\n');
|
|
135
|
+
|
|
136
|
+
const allPatterns = [
|
|
137
|
+
...this.pathBasedAppPatterns,
|
|
138
|
+
...this.sharedCookiePatterns,
|
|
139
|
+
...this.configPatterns
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const { pattern, message, type } of allPatterns) {
|
|
143
|
+
pattern.lastIndex = 0;
|
|
144
|
+
|
|
145
|
+
let match;
|
|
146
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
147
|
+
const lineNum = this.getLineNumber(content, match.index);
|
|
148
|
+
const line = lines[lineNum - 1];
|
|
149
|
+
|
|
150
|
+
if (this.isComment(line)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
violations.push({
|
|
155
|
+
file: filePath,
|
|
156
|
+
line: lineNum,
|
|
157
|
+
column: this.getColumnNumber(content, match.index),
|
|
158
|
+
message: message,
|
|
159
|
+
severity: 'warning',
|
|
160
|
+
ruleId: this.ruleId,
|
|
161
|
+
type: type,
|
|
162
|
+
matchedText: match[0]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return violations;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
isComment(line) {
|
|
171
|
+
const trimmed = line.trim();
|
|
172
|
+
return trimmed.startsWith('//') || trimmed.startsWith('#') ||
|
|
173
|
+
trimmed.startsWith('/*') || trimmed.startsWith('*');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
getLineNumber(content, index) {
|
|
177
|
+
return content.substring(0, index).split('\n').length;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getColumnNumber(content, index) {
|
|
181
|
+
const lastNewline = content.lastIndexOf('\n', index - 1);
|
|
182
|
+
return index - lastNewline;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = S035Analyzer;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "S036",
|
|
3
|
-
"name": "
|
|
3
|
+
"name": "Use internal data for file paths, validate user filenames strictly",
|
|
4
4
|
"category": "security",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Prevent path traversal, LFI, RFI, and SSRF attacks by using internally generated file paths instead of user-submitted filenames. When user input is unavoidable, apply strict validation with allowlists.",
|
|
6
6
|
"severity": "error",
|
|
7
7
|
"enabled": true,
|
|
8
8
|
"patterns": {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S039",
|
|
3
|
+
"name": "TLS clients must validate server certificates",
|
|
4
|
+
"description": "Ensure TLS clients validate certificates received from servers before establishing secure communication. Verify certificate is signed by trusted CA, check certificate chain, validate expiration, and confirm hostname matches certificate CN/SAN.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "critical",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"engines": ["heuristic"],
|
|
9
|
+
"enginePreference": ["heuristic"],
|
|
10
|
+
"tags": ["security", "tls", "ssl", "certificates", "validation", "mitm"],
|
|
11
|
+
"examples": {
|
|
12
|
+
"valid": [
|
|
13
|
+
"const https = require('https'); // Default validates certificates",
|
|
14
|
+
"fetch('https://api.example.com'); // Default validates",
|
|
15
|
+
"axios.get('https://api.example.com'); // Default validates"
|
|
16
|
+
],
|
|
17
|
+
"invalid": [
|
|
18
|
+
"{ rejectUnauthorized: false } // Disables validation",
|
|
19
|
+
"requests.get(url, verify=False) // Python: disables validation",
|
|
20
|
+
"InsecureSkipVerify: true // Go: disables validation",
|
|
21
|
+
"NODE_TLS_REJECT_UNAUTHORIZED=0 // Disables globally"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"fixable": false,
|
|
25
|
+
"docs": {
|
|
26
|
+
"description": "This rule ensures TLS clients properly validate server certificates to prevent MITM attacks. Required validation includes: verify certificate is signed by trusted CA, check certificate chain up to root CA, validate certificate has not expired, confirm hostname matches certificate CN/SAN. DO NOT disable validation with rejectUnauthorized: false, verify=False, or InsecureSkipVerify: true.",
|
|
27
|
+
"url": "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S039 – TLS clients must validate server certificates
|
|
3
|
+
*
|
|
4
|
+
* Detects patterns where TLS certificate validation is disabled:
|
|
5
|
+
* - rejectUnauthorized: false (Node.js)
|
|
6
|
+
* - verify=False (Python)
|
|
7
|
+
* - InsecureSkipVerify: true (Go)
|
|
8
|
+
* - NODE_TLS_REJECT_UNAUTHORIZED=0
|
|
9
|
+
* - Custom checkServerIdentity that bypasses validation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
class S039Analyzer {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.ruleId = 'S039';
|
|
18
|
+
this.ruleName = 'TLS clients must validate server certificates';
|
|
19
|
+
this.description = 'Ensure TLS clients validate server certificates to prevent MITM attacks';
|
|
20
|
+
|
|
21
|
+
// Critical patterns - certificate validation disabled
|
|
22
|
+
this.criticalPatterns = [
|
|
23
|
+
// Node.js - rejectUnauthorized: false
|
|
24
|
+
{
|
|
25
|
+
pattern: /rejectUnauthorized\s*:\s*false/gi,
|
|
26
|
+
message: 'TLS certificate validation disabled with rejectUnauthorized: false. This allows MITM attacks',
|
|
27
|
+
type: 'reject_unauthorized_false'
|
|
28
|
+
},
|
|
29
|
+
// Node.js environment variable
|
|
30
|
+
{
|
|
31
|
+
pattern: /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"`]?0['"`]?/gi,
|
|
32
|
+
message: 'NODE_TLS_REJECT_UNAUTHORIZED=0 globally disables certificate validation. This is dangerous',
|
|
33
|
+
type: 'node_env_disable'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
pattern: /process\.env\.NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"`]?0['"`]?/gi,
|
|
37
|
+
message: 'Setting NODE_TLS_REJECT_UNAUTHORIZED=0 disables all TLS validation',
|
|
38
|
+
type: 'node_env_set'
|
|
39
|
+
},
|
|
40
|
+
// Python requests
|
|
41
|
+
{
|
|
42
|
+
pattern: /verify\s*=\s*False/g,
|
|
43
|
+
message: 'SSL verification disabled with verify=False. Certificate validation required',
|
|
44
|
+
type: 'python_verify_false'
|
|
45
|
+
},
|
|
46
|
+
// Go
|
|
47
|
+
{
|
|
48
|
+
pattern: /InsecureSkipVerify\s*:\s*true/gi,
|
|
49
|
+
message: 'InsecureSkipVerify: true disables TLS certificate validation in Go',
|
|
50
|
+
type: 'go_insecure_skip'
|
|
51
|
+
},
|
|
52
|
+
// Axios
|
|
53
|
+
{
|
|
54
|
+
pattern: /httpsAgent\s*:\s*new\s+https\.Agent\s*\(\s*\{[^}]*rejectUnauthorized\s*:\s*false/gi,
|
|
55
|
+
message: 'Axios httpsAgent with rejectUnauthorized: false disables certificate validation',
|
|
56
|
+
type: 'axios_insecure'
|
|
57
|
+
},
|
|
58
|
+
// Java
|
|
59
|
+
{
|
|
60
|
+
pattern: /setHostnameVerifier\s*\(\s*.*ALLOW_ALL|NoopHostnameVerifier/gi,
|
|
61
|
+
message: 'Hostname verification disabled. This allows certificate spoofing',
|
|
62
|
+
type: 'java_hostname_verifier'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
pattern: /TrustAllCerts|TrustAllStrategy|AcceptAllCertificates/gi,
|
|
66
|
+
message: 'Trust-all certificate strategy detected. Validate certificates properly',
|
|
67
|
+
type: 'trust_all'
|
|
68
|
+
}
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
// Warning patterns - potentially insecure
|
|
72
|
+
this.warningPatterns = [
|
|
73
|
+
// Custom checkServerIdentity that returns undefined/null
|
|
74
|
+
{
|
|
75
|
+
pattern: /checkServerIdentity\s*:\s*\(\)\s*=>\s*(undefined|null|true|\{\})/gi,
|
|
76
|
+
message: 'Custom checkServerIdentity bypasses hostname verification',
|
|
77
|
+
type: 'custom_server_identity'
|
|
78
|
+
},
|
|
79
|
+
// SSL context with no verify
|
|
80
|
+
{
|
|
81
|
+
pattern: /ssl\.create_default_context\s*\([^)]*\)\s*[^;]*check_hostname\s*=\s*False/gi,
|
|
82
|
+
message: 'SSL context with hostname check disabled',
|
|
83
|
+
type: 'ssl_no_hostname'
|
|
84
|
+
},
|
|
85
|
+
// curl with insecure flag (in scripts)
|
|
86
|
+
{
|
|
87
|
+
pattern: /curl\s+[^|]*-k\b|curl\s+[^|]*--insecure\b/gi,
|
|
88
|
+
message: 'curl with -k/--insecure flag disables certificate validation',
|
|
89
|
+
type: 'curl_insecure'
|
|
90
|
+
},
|
|
91
|
+
// wget with no-check-certificate
|
|
92
|
+
{
|
|
93
|
+
pattern: /wget\s+[^|]*--no-check-certificate/gi,
|
|
94
|
+
message: 'wget with --no-check-certificate disables certificate validation',
|
|
95
|
+
type: 'wget_insecure'
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Files to skip
|
|
100
|
+
this.skipPatterns = [
|
|
101
|
+
/\.test\./i,
|
|
102
|
+
/\.spec\./i,
|
|
103
|
+
/test\//i,
|
|
104
|
+
/tests\//i,
|
|
105
|
+
/__tests__\//i,
|
|
106
|
+
/node_modules/i,
|
|
107
|
+
/\.md$/i
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
shouldSkipFile(filePath) {
|
|
112
|
+
return this.skipPatterns.some(pattern => pattern.test(filePath));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async analyze(files, language, options = {}) {
|
|
116
|
+
const violations = [];
|
|
117
|
+
|
|
118
|
+
for (const filePath of files) {
|
|
119
|
+
if (this.shouldSkipFile(filePath)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
125
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
126
|
+
violations.push(...fileViolations);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (options.verbose) {
|
|
129
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return violations;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
analyzeFile(content, filePath) {
|
|
138
|
+
const violations = [];
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
|
|
141
|
+
// Check critical patterns (errors)
|
|
142
|
+
for (const { pattern, message, type } of this.criticalPatterns) {
|
|
143
|
+
pattern.lastIndex = 0;
|
|
144
|
+
|
|
145
|
+
let match;
|
|
146
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
147
|
+
const lineNum = this.getLineNumber(content, match.index);
|
|
148
|
+
const line = lines[lineNum - 1];
|
|
149
|
+
|
|
150
|
+
if (this.isComment(line)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Skip if in test context
|
|
155
|
+
if (this.isTestContext(lines, lineNum)) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
violations.push({
|
|
160
|
+
file: filePath,
|
|
161
|
+
line: lineNum,
|
|
162
|
+
column: this.getColumnNumber(content, match.index),
|
|
163
|
+
message: message,
|
|
164
|
+
severity: 'error',
|
|
165
|
+
ruleId: this.ruleId,
|
|
166
|
+
type: type,
|
|
167
|
+
matchedText: match[0]
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check warning patterns
|
|
173
|
+
for (const { pattern, message, type } of this.warningPatterns) {
|
|
174
|
+
pattern.lastIndex = 0;
|
|
175
|
+
|
|
176
|
+
let match;
|
|
177
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
178
|
+
const lineNum = this.getLineNumber(content, match.index);
|
|
179
|
+
const line = lines[lineNum - 1];
|
|
180
|
+
|
|
181
|
+
if (this.isComment(line)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (this.isTestContext(lines, lineNum)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
violations.push({
|
|
190
|
+
file: filePath,
|
|
191
|
+
line: lineNum,
|
|
192
|
+
column: this.getColumnNumber(content, match.index),
|
|
193
|
+
message: message,
|
|
194
|
+
severity: 'warning',
|
|
195
|
+
ruleId: this.ruleId,
|
|
196
|
+
type: type,
|
|
197
|
+
matchedText: match[0]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return violations;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
isComment(line) {
|
|
206
|
+
const trimmed = line.trim();
|
|
207
|
+
return trimmed.startsWith('//') || trimmed.startsWith('#') ||
|
|
208
|
+
trimmed.startsWith('/*') || trimmed.startsWith('*');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
isTestContext(lines, lineNum) {
|
|
212
|
+
const start = Math.max(0, lineNum - 5);
|
|
213
|
+
const end = Math.min(lines.length, lineNum + 2);
|
|
214
|
+
const context = lines.slice(start, end).join('\n').toLowerCase();
|
|
215
|
+
|
|
216
|
+
return /describe\(|it\(|test\(|jest|mocha|mock|stub|fake|development|dev\s*mode/i.test(context);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
getLineNumber(content, index) {
|
|
220
|
+
return content.substring(0, index).split('\n').length;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getColumnNumber(content, index) {
|
|
224
|
+
const lastNewline = content.lastIndexOf('\n', index - 1);
|
|
225
|
+
return index - lastNewline;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = S039Analyzer;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S046",
|
|
3
|
+
"name": "Use algorithm allowlist for self-contained tokens",
|
|
4
|
+
"description": "Prevent algorithm confusion and downgrade attacks by restricting token signing/verification to an explicit allowlist of algorithms. Never allow the 'none' algorithm. Validate algorithm before processing token.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "critical",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"engines": ["heuristic"],
|
|
9
|
+
"enginePreference": ["heuristic"],
|
|
10
|
+
"tags": ["security", "jwt", "token", "algorithm", "authentication"],
|
|
11
|
+
"examples": {
|
|
12
|
+
"valid": [
|
|
13
|
+
"jwt.verify(token, key, { algorithms: ['RS256'] });",
|
|
14
|
+
"const options = { algorithms: ['HS256', 'HS384'] };",
|
|
15
|
+
"if (!['RS256', 'ES256'].includes(header.alg)) throw new Error();"
|
|
16
|
+
],
|
|
17
|
+
"invalid": [
|
|
18
|
+
"jwt.verify(token, key); // No algorithm specified",
|
|
19
|
+
"jwt.decode(token); // Decodes without verification",
|
|
20
|
+
"{ algorithms: ['none'] } // 'none' algorithm allowed"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"fixable": false,
|
|
24
|
+
"docs": {
|
|
25
|
+
"description": "This rule ensures JWT/token verification uses an explicit algorithm allowlist. Algorithm confusion attacks occur when an attacker can control which algorithm is used. Must use algorithm allowlist, never allow 'none' algorithm, prefer either symmetric OR asymmetric algorithms not both. If both needed, implement key type validation to prevent key confusion attacks.",
|
|
26
|
+
"url": "https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S046 Dart Analyzer - JWT Algorithm Allowlist
|
|
3
|
+
*
|
|
4
|
+
* This is a JS wrapper that delegates to DartAnalyzer binary.
|
|
5
|
+
* Actual implementation: dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart
|
|
6
|
+
*
|
|
7
|
+
* Rule: Use algorithm allowlist for self-contained tokens
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class DartS046Analyzer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ruleId = 'S046';
|
|
13
|
+
this.language = 'dart';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getMetadata() {
|
|
17
|
+
return {
|
|
18
|
+
ruleId: 'S046',
|
|
19
|
+
name: 'JWT Algorithm Allowlist',
|
|
20
|
+
language: 'dart',
|
|
21
|
+
delegateTo: 'dart_analyzer',
|
|
22
|
+
description: 'Use algorithm allowlist for self-contained tokens to prevent algorithm confusion attacks'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getConfig() {
|
|
27
|
+
return {
|
|
28
|
+
checkJwtVerify: true,
|
|
29
|
+
checkAlgorithmList: true,
|
|
30
|
+
severity: 'critical'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async analyze(files, language, options) {
|
|
35
|
+
// Delegated to DartAnalyzer binary via heuristic-engine.js
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
supportsLanguage(language) {
|
|
40
|
+
return language === 'dart';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = DartS046Analyzer;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S046 Rule Router - JWT Algorithm Allowlist
|
|
3
|
+
*
|
|
4
|
+
* Routes analysis to the appropriate language-specific analyzer.
|
|
5
|
+
* Supports: TypeScript, JavaScript, Dart
|
|
6
|
+
*
|
|
7
|
+
* Rule: Use algorithm allowlist for self-contained tokens to prevent
|
|
8
|
+
* algorithm confusion and downgrade attacks
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
class S046Router {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.analyzers = new Map();
|
|
16
|
+
this.ruleId = 'S046';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getAnalyzer(language) {
|
|
20
|
+
const normalizedLang = this.normalizeLanguage(language);
|
|
21
|
+
|
|
22
|
+
if (!this.analyzers.has(normalizedLang)) {
|
|
23
|
+
try {
|
|
24
|
+
const analyzerPath = path.join(__dirname, normalizedLang, 'analyzer.js');
|
|
25
|
+
const AnalyzerClass = require(analyzerPath);
|
|
26
|
+
this.analyzers.set(normalizedLang, new AnalyzerClass());
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.analyzers.get(normalizedLang);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
normalizeLanguage(language) {
|
|
36
|
+
if (typeof language !== 'string') {
|
|
37
|
+
return 'typescript';
|
|
38
|
+
}
|
|
39
|
+
const languageMap = {
|
|
40
|
+
'typescript': 'typescript',
|
|
41
|
+
'javascript': 'typescript',
|
|
42
|
+
'ts': 'typescript',
|
|
43
|
+
'js': 'typescript',
|
|
44
|
+
'dart': 'dart'
|
|
45
|
+
};
|
|
46
|
+
return languageMap[language.toLowerCase()] || language.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
supportsLanguage(language) {
|
|
50
|
+
if (typeof language !== 'string') return false;
|
|
51
|
+
const supported = ['typescript', 'javascript', 'ts', 'js', 'dart'];
|
|
52
|
+
return supported.includes(language.toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getSupportedLanguages() {
|
|
56
|
+
return ['typescript', 'javascript', 'dart'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async analyze(files, language, options = {}) {
|
|
60
|
+
const analyzer = this.getAnalyzer(language);
|
|
61
|
+
if (!analyzer) return [];
|
|
62
|
+
if (typeof analyzer.analyze === 'function') {
|
|
63
|
+
return analyzer.analyze(files, language, options);
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async initialize(semanticEngineOrLanguage = null, semanticEngine = null) {
|
|
69
|
+
let engine = semanticEngine;
|
|
70
|
+
let lang = null;
|
|
71
|
+
|
|
72
|
+
if (typeof semanticEngineOrLanguage === 'string') {
|
|
73
|
+
lang = semanticEngineOrLanguage;
|
|
74
|
+
} else if (semanticEngineOrLanguage && typeof semanticEngineOrLanguage === 'object') {
|
|
75
|
+
engine = semanticEngineOrLanguage;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (lang) {
|
|
79
|
+
const analyzer = this.getAnalyzer(lang);
|
|
80
|
+
if (analyzer && typeof analyzer.initialize === 'function') {
|
|
81
|
+
await analyzer.initialize(engine);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = new S046Router();
|