@sun-asterisk/sunlint 1.3.7 → 1.3.8
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/CHANGELOG.md +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S038 Regex-Based Ana // Version header patterns
|
|
3
|
+
this.versionHeader // Version information patterns
|
|
4
|
+
this.versionPatterns = [
|
|
5
|
+
/\d+\.\d+/, // Version numbers like 1.0, 2.1.3
|
|
6
|
+
/v\d+/i, // Version prefixes like v1, V2
|
|
7
|
+
/version/i, // The word "version"
|
|
8
|
+
/express/i, // Framework names
|
|
9
|
+
/node/i,
|
|
10
|
+
/nginx/i,
|
|
11
|
+
/apache/i,
|
|
12
|
+
/iis/i,
|
|
13
|
+
/php/i,
|
|
14
|
+
/asp\.net/i,
|
|
15
|
+
/ruby/i,
|
|
16
|
+
/python/i,
|
|
17
|
+
/django/i,
|
|
18
|
+
/rails/i,
|
|
19
|
+
/laravel/i,
|
|
20
|
+
/mysql/i, // Database versions
|
|
21
|
+
/postgresql/i,
|
|
22
|
+
/mongodb/i,
|
|
23
|
+
/redis/i,
|
|
24
|
+
/sqlite/i,
|
|
25
|
+
/mariadb/i
|
|
26
|
+
];r",
|
|
27
|
+
"X-Powered-By",
|
|
28
|
+
"X-AspNet-Version",
|
|
29
|
+
"X-AspNetMvc-Version",
|
|
30
|
+
"X-Generator",
|
|
31
|
+
"X-Runtime",
|
|
32
|
+
"X-Version",
|
|
33
|
+
"X-Framework",
|
|
34
|
+
"X-Drupal-Cache",
|
|
35
|
+
"X-Varnish",
|
|
36
|
+
"X-Cache",
|
|
37
|
+
"X-Served-By",
|
|
38
|
+
"X-Database"
|
|
39
|
+
];expose version information in response headers
|
|
40
|
+
* Detects version header exposure using pattern matching.
|
|
41
|
+
*/
|
|
42
|
+
const fs = require("fs");
|
|
43
|
+
|
|
44
|
+
class S038RegexBasedAnalyzer {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.ruleId = "S038";
|
|
47
|
+
|
|
48
|
+
// Framework-specific route patterns
|
|
49
|
+
this.routePatterns = {
|
|
50
|
+
// Express.js
|
|
51
|
+
express:
|
|
52
|
+
/\b(app|router)\.(get|post|put|delete|patch|use)\s*\(\s*['"`][^'"`]+['"`]/,
|
|
53
|
+
// Next.js API routes
|
|
54
|
+
nextjs: /export\s+(default\s+)?async?\s+function\s+handler\s*\(/,
|
|
55
|
+
// Next.js 13+ App Router
|
|
56
|
+
nextjsApp:
|
|
57
|
+
/export\s+async?\s+function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/,
|
|
58
|
+
// NestJS controllers
|
|
59
|
+
nestjs: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*['"`]?\s*\)/,
|
|
60
|
+
// Nuxt.js server routes
|
|
61
|
+
nuxtjs:
|
|
62
|
+
/export\s+(default\s+|const\s+(GET|POST|PUT|DELETE|PATCH)\s*=\s*)?defineEventHandler\s*\(/,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Version header patterns
|
|
66
|
+
this.versionHeaders = [
|
|
67
|
+
"Server",
|
|
68
|
+
"X-Powered-By",
|
|
69
|
+
"X-AspNet-Version",
|
|
70
|
+
"X-AspNetMvc-Version",
|
|
71
|
+
"X-Generator",
|
|
72
|
+
"X-Runtime",
|
|
73
|
+
"X-Version",
|
|
74
|
+
"X-Framework",
|
|
75
|
+
"X-Drupal-Cache",
|
|
76
|
+
"X-Varnish",
|
|
77
|
+
"X-Cache",
|
|
78
|
+
"X-Served-By",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
// Header setting patterns for different frameworks
|
|
82
|
+
this.headerSetPatterns = {
|
|
83
|
+
express:
|
|
84
|
+
/res\.set(Header|)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
|
|
85
|
+
nestjs:
|
|
86
|
+
/res\.header\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
|
|
87
|
+
nextjs:
|
|
88
|
+
/res\.setHeader\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
|
|
89
|
+
headers:
|
|
90
|
+
/headers\s*\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
|
|
91
|
+
nuxtjs: /setHeader\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Bulk header setting patterns
|
|
95
|
+
this.bulkSetPattern = /res\.set\s*\(\s*\{([^}]+)\}/i;
|
|
96
|
+
this.nextHeadersPattern =
|
|
97
|
+
/\w+\.headers\s*\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
|
|
98
|
+
|
|
99
|
+
// Security middleware patterns (skip analysis if present)
|
|
100
|
+
this.securityMiddleware =
|
|
101
|
+
/helmet|hidePoweredBy|disable.*x.*powered.*by|removeHeader.*powered|noSniff/i;
|
|
102
|
+
|
|
103
|
+
// Version information patterns
|
|
104
|
+
this.versionPatterns = [
|
|
105
|
+
/\d+\.\d+/, // Version numbers like 1.0, 2.1.3
|
|
106
|
+
/v\d+/i, // Version prefixes like v1, V2
|
|
107
|
+
/version/i, // The word "version"
|
|
108
|
+
/express/i, // Framework names
|
|
109
|
+
/node/i,
|
|
110
|
+
/nginx/i,
|
|
111
|
+
/apache/i,
|
|
112
|
+
/iis/i,
|
|
113
|
+
/php/i,
|
|
114
|
+
/asp\.net/i,
|
|
115
|
+
/ruby/i,
|
|
116
|
+
/python/i,
|
|
117
|
+
/django/i,
|
|
118
|
+
/rails/i,
|
|
119
|
+
/laravel/i,
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async analyze(filePath) {
|
|
124
|
+
// Skip files that are unlikely to be route handlers
|
|
125
|
+
const skipPatterns = [
|
|
126
|
+
/\.dto\.ts$/,
|
|
127
|
+
/\.interface\.ts$/,
|
|
128
|
+
/\.module\.ts$/,
|
|
129
|
+
/\.service\.spec\.ts$/,
|
|
130
|
+
/\.controller\.spec\.ts$/,
|
|
131
|
+
/\.spec\.ts$/,
|
|
132
|
+
/\.test\.ts$/,
|
|
133
|
+
/\.d\.ts$/,
|
|
134
|
+
/\.types\.ts$/,
|
|
135
|
+
/\.constants?\.ts$/,
|
|
136
|
+
/\.config\.ts$/,
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
140
|
+
if (shouldSkip) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
145
|
+
const lines = content.split(/\r?\n/);
|
|
146
|
+
const violations = [];
|
|
147
|
+
|
|
148
|
+
let inRoute = false;
|
|
149
|
+
let braceDepth = 0;
|
|
150
|
+
let routeStartLine = 0;
|
|
151
|
+
let routeType = "";
|
|
152
|
+
let hasSecurityMiddleware = false;
|
|
153
|
+
let versionExposures = [];
|
|
154
|
+
|
|
155
|
+
const reset = () => {
|
|
156
|
+
inRoute = false;
|
|
157
|
+
braceDepth = 0;
|
|
158
|
+
routeStartLine = 0;
|
|
159
|
+
routeType = "";
|
|
160
|
+
hasSecurityMiddleware = false;
|
|
161
|
+
versionExposures = [];
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const evaluate = () => {
|
|
165
|
+
if (!routeStartLine) return;
|
|
166
|
+
|
|
167
|
+
if (hasSecurityMiddleware) return; // assume middleware handles header security
|
|
168
|
+
|
|
169
|
+
// Report all version header exposures found in this route
|
|
170
|
+
for (const exposure of versionExposures) {
|
|
171
|
+
violations.push({
|
|
172
|
+
ruleId: this.ruleId,
|
|
173
|
+
message: `Exposing version information in '${exposure.header}' header`,
|
|
174
|
+
severity: "warning",
|
|
175
|
+
line: exposure.line,
|
|
176
|
+
column: 1,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
const line = lines[i];
|
|
183
|
+
|
|
184
|
+
// Detect route start for different frameworks
|
|
185
|
+
if (!inRoute) {
|
|
186
|
+
for (const [framework, pattern] of Object.entries(this.routePatterns)) {
|
|
187
|
+
if (pattern.test(line)) {
|
|
188
|
+
if (process.env.SUNLINT_DEBUG)
|
|
189
|
+
console.log(
|
|
190
|
+
`🔧 [S038-Regex] Found ${framework} route at line ${
|
|
191
|
+
i + 1
|
|
192
|
+
}: ${line.trim()}`
|
|
193
|
+
);
|
|
194
|
+
inRoute = true;
|
|
195
|
+
routeStartLine = i + 1;
|
|
196
|
+
routeType = framework;
|
|
197
|
+
braceDepth =
|
|
198
|
+
(line.match(/\{/g) || []).length -
|
|
199
|
+
(line.match(/\}/g) || []).length;
|
|
200
|
+
hasSecurityMiddleware = this.securityMiddleware.test(line);
|
|
201
|
+
if (hasSecurityMiddleware && process.env.SUNLINT_DEBUG) {
|
|
202
|
+
console.log(
|
|
203
|
+
`🔧 [S038-Regex] Security middleware detected, skipping evaluation`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If no opening brace on this line, look ahead for it
|
|
208
|
+
if (braceDepth === 0) {
|
|
209
|
+
for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
|
|
210
|
+
const nextLine = lines[j];
|
|
211
|
+
if (nextLine.includes("{")) {
|
|
212
|
+
braceDepth =
|
|
213
|
+
(nextLine.match(/\{/g) || []).length -
|
|
214
|
+
(nextLine.match(/\}/g) || []).length;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (inRoute) {
|
|
225
|
+
// Update brace depth
|
|
226
|
+
braceDepth +=
|
|
227
|
+
(line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
228
|
+
|
|
229
|
+
// Check for security middleware within route
|
|
230
|
+
if (this.securityMiddleware.test(line)) {
|
|
231
|
+
hasSecurityMiddleware = true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check for version header setting
|
|
235
|
+
this.checkVersionHeaderSetting(line, i + 1, versionExposures);
|
|
236
|
+
|
|
237
|
+
// End of route detection
|
|
238
|
+
if (
|
|
239
|
+
braceDepth <= 0 &&
|
|
240
|
+
(/\)\s*;?\s*$/.test(line) ||
|
|
241
|
+
/^\s*\}\s*$/.test(line) ||
|
|
242
|
+
/^export/.test(line))
|
|
243
|
+
) {
|
|
244
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
245
|
+
console.log(
|
|
246
|
+
`🔧 [S038-Regex] Route ended, evaluating: Exposures=${versionExposures.length}, Security=${hasSecurityMiddleware}`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
evaluate();
|
|
250
|
+
reset();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Safety evaluate if unbalanced at file end
|
|
256
|
+
if (inRoute) evaluate();
|
|
257
|
+
|
|
258
|
+
return violations;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
checkVersionHeaderSetting(line, lineNumber, exposures) {
|
|
262
|
+
// Check each header setting pattern
|
|
263
|
+
for (const [framework, pattern] of Object.entries(this.headerSetPatterns)) {
|
|
264
|
+
const match = pattern.exec(line);
|
|
265
|
+
if (match) {
|
|
266
|
+
const headerName = match[2] || match[1]; // Different capture groups for different patterns
|
|
267
|
+
const headerValue = match[3] || match[2];
|
|
268
|
+
|
|
269
|
+
if (
|
|
270
|
+
this.isVersionHeader(headerName) &&
|
|
271
|
+
this.containsVersionInfo(headerValue)
|
|
272
|
+
) {
|
|
273
|
+
exposures.push({
|
|
274
|
+
header: headerName,
|
|
275
|
+
value: headerValue,
|
|
276
|
+
line: lineNumber,
|
|
277
|
+
framework,
|
|
278
|
+
});
|
|
279
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
280
|
+
console.log(
|
|
281
|
+
`🔧 [S038-Regex] Found version header exposure: ${headerName} = ${headerValue}`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
break; // Found a match, no need to check other patterns
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check bulk header setting
|
|
290
|
+
const bulkMatch = this.bulkSetPattern.exec(line);
|
|
291
|
+
if (bulkMatch) {
|
|
292
|
+
const headersContent = bulkMatch[1];
|
|
293
|
+
this.checkBulkHeaders(headersContent, lineNumber, exposures);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
checkBulkHeaders(headersContent, lineNumber, exposures) {
|
|
298
|
+
// Parse object literal headers like { "X-Powered-By": "Express", "Server": "nginx/1.18" }
|
|
299
|
+
const headerMatches = headersContent.matchAll(
|
|
300
|
+
/['"`]([^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
for (const match of headerMatches) {
|
|
304
|
+
const headerName = match[1];
|
|
305
|
+
const headerValue = match[2];
|
|
306
|
+
|
|
307
|
+
if (
|
|
308
|
+
this.isVersionHeader(headerName) &&
|
|
309
|
+
this.containsVersionInfo(headerValue)
|
|
310
|
+
) {
|
|
311
|
+
exposures.push({
|
|
312
|
+
header: headerName,
|
|
313
|
+
value: headerValue,
|
|
314
|
+
line: lineNumber,
|
|
315
|
+
framework: "bulk",
|
|
316
|
+
});
|
|
317
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
318
|
+
console.log(
|
|
319
|
+
`🔧 [S038-Regex] Found bulk version header exposure: ${headerName} = ${headerValue}`
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
isVersionHeader(headerName) {
|
|
327
|
+
return this.versionHeaders.some(
|
|
328
|
+
(vh) => vh.toLowerCase() === headerName.toLowerCase()
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
containsVersionInfo(value) {
|
|
333
|
+
return this.versionPatterns.some((pattern) => pattern.test(value));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
cleanup() {}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = S038RegexBasedAnalyzer;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S038 Sy "X-Generator",
|
|
3
|
+
"X-Runtime",
|
|
4
|
+
"X-Version",
|
|
5
|
+
"X-Framework",
|
|
6
|
+
"X-Drupal-Cache",
|
|
7
|
+
"X-Varnish",
|
|
8
|
+
"X-Cache",
|
|
9
|
+
"X-Served-By",
|
|
10
|
+
"X-Database"
|
|
11
|
+
];yzer - Do not expose version information in response headers
|
|
12
|
+
* Detects version header exposure in route handlers
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class S038SymbolBasedAnalyzer {
|
|
16
|
+
constructor(semanticEngine) {
|
|
17
|
+
this.ruleId = "S038";
|
|
18
|
+
this.semanticEngine = semanticEngine;
|
|
19
|
+
this.versionHeaders = [
|
|
20
|
+
"Server",
|
|
21
|
+
"X-Powered-By",
|
|
22
|
+
"X-AspNet-Version",
|
|
23
|
+
"X-AspNetMvc-Version",
|
|
24
|
+
"X-Generator",
|
|
25
|
+
"X-Runtime",
|
|
26
|
+
"X-Version",
|
|
27
|
+
"X-Framework",
|
|
28
|
+
"X-Drupal-Cache",
|
|
29
|
+
"X-Varnish",
|
|
30
|
+
"X-Cache",
|
|
31
|
+
"X-Served-By",
|
|
32
|
+
"X-Database",
|
|
33
|
+
"X-Framework",
|
|
34
|
+
"X-Drupal-Cache",
|
|
35
|
+
"X-Varnish",
|
|
36
|
+
"X-Cache",
|
|
37
|
+
"X-Served-By",
|
|
38
|
+
];
|
|
39
|
+
this.securityMiddleware =
|
|
40
|
+
/helmet|hidePoweredBy|disable.*x.*powered.*by|removeHeader.*powered|noSniff/i;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async initialize() {}
|
|
44
|
+
|
|
45
|
+
analyze(sourceFile, filePath) {
|
|
46
|
+
const violations = [];
|
|
47
|
+
|
|
48
|
+
// Skip files that are unlikely to be route handlers
|
|
49
|
+
const skipPatterns = [
|
|
50
|
+
/\.dto\.ts$/,
|
|
51
|
+
/\.interface\.ts$/,
|
|
52
|
+
/\.module\.ts$/,
|
|
53
|
+
/\.service\.spec\.ts$/,
|
|
54
|
+
/\.controller\.spec\.ts$/,
|
|
55
|
+
/\.spec\.ts$/,
|
|
56
|
+
/\.test\.ts$/,
|
|
57
|
+
/\.d\.ts$/,
|
|
58
|
+
/\.types\.ts$/,
|
|
59
|
+
/\.constants?\.ts$/,
|
|
60
|
+
/\.config\.ts$/,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
64
|
+
if (shouldSkip) {
|
|
65
|
+
return violations;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const { SyntaxKind } = require("ts-morph");
|
|
70
|
+
|
|
71
|
+
// Find all function expressions and arrow functions that could be route handlers
|
|
72
|
+
const routeHandlers = [];
|
|
73
|
+
|
|
74
|
+
// Express route patterns: app.get("/path", (req, res) => {...})
|
|
75
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
76
|
+
SyntaxKind.CallExpression
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
for (const call of callExpressions) {
|
|
80
|
+
const expression = call.getExpression();
|
|
81
|
+
|
|
82
|
+
// Check for Express route methods
|
|
83
|
+
if (/\.(get|post|put|delete|patch|use)$/.test(expression.getText())) {
|
|
84
|
+
const args = call.getArguments();
|
|
85
|
+
if (args.length >= 2) {
|
|
86
|
+
const lastArg = args[args.length - 1];
|
|
87
|
+
// The last argument should be the handler function
|
|
88
|
+
if (
|
|
89
|
+
lastArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
90
|
+
lastArg.getKind() === SyntaxKind.FunctionExpression
|
|
91
|
+
) {
|
|
92
|
+
routeHandlers.push({
|
|
93
|
+
handler: lastArg,
|
|
94
|
+
routeCall: call,
|
|
95
|
+
type: "express",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Next.js export functions
|
|
103
|
+
const exportAssignments = sourceFile.getDescendantsOfKind(
|
|
104
|
+
SyntaxKind.ExportAssignment
|
|
105
|
+
);
|
|
106
|
+
const exportDeclarations = sourceFile.getDescendantsOfKind(
|
|
107
|
+
SyntaxKind.ExportDeclaration
|
|
108
|
+
);
|
|
109
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind(
|
|
110
|
+
SyntaxKind.FunctionDeclaration
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
for (const func of functionDeclarations) {
|
|
114
|
+
const name = func.getName();
|
|
115
|
+
if (name && /^(GET|POST|PUT|DELETE|PATCH|handler)$/.test(name)) {
|
|
116
|
+
routeHandlers.push({
|
|
117
|
+
handler: func,
|
|
118
|
+
type: "nextjs",
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// NestJS Controller methods with decorators
|
|
124
|
+
const methodDeclarations = sourceFile.getDescendantsOfKind(
|
|
125
|
+
SyntaxKind.MethodDeclaration
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
for (const method of methodDeclarations) {
|
|
129
|
+
const decorators = method.getDecorators();
|
|
130
|
+
const hasRouteDecorator = decorators.some((decorator) => {
|
|
131
|
+
const decoratorName = decorator.getName();
|
|
132
|
+
return /^(Get|Post|Put|Delete|Patch|All)$/.test(decoratorName);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (hasRouteDecorator) {
|
|
136
|
+
routeHandlers.push({
|
|
137
|
+
handler: method,
|
|
138
|
+
type: "nestjs",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Nuxt.js defineEventHandler patterns
|
|
144
|
+
for (const call of callExpressions) {
|
|
145
|
+
const expression = call.getExpression();
|
|
146
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
147
|
+
const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
|
|
148
|
+
if (identifier.getText() === "defineEventHandler") {
|
|
149
|
+
// Find the arrow function or function parameter
|
|
150
|
+
const args = call.getArguments();
|
|
151
|
+
if (args.length > 0) {
|
|
152
|
+
const firstArg = args[0];
|
|
153
|
+
if (
|
|
154
|
+
firstArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
155
|
+
firstArg.getKind() === SyntaxKind.FunctionExpression
|
|
156
|
+
) {
|
|
157
|
+
routeHandlers.push({
|
|
158
|
+
handler: firstArg,
|
|
159
|
+
type: "nuxtjs",
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Nuxt.js named exports (GET, POST, etc.)
|
|
168
|
+
const nuxtExportAssignments = sourceFile.getDescendantsOfKind(
|
|
169
|
+
SyntaxKind.ExportAssignment
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
for (const exportAssign of nuxtExportAssignments) {
|
|
173
|
+
const expr = exportAssign.getExpression();
|
|
174
|
+
if (expr.getKind() === SyntaxKind.ArrowFunction) {
|
|
175
|
+
routeHandlers.push({
|
|
176
|
+
handler: expr,
|
|
177
|
+
type: "nuxtjs",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Analyze each route handler
|
|
183
|
+
for (const route of routeHandlers) {
|
|
184
|
+
const handlerViolations = this.analyzeRouteHandler(route);
|
|
185
|
+
violations.push(...handlerViolations);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Only analyze actual route handlers - no file-level checking
|
|
189
|
+
// File-level checking removed to prevent false positives on DTO, module, service files
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Fallback - don't report errors from symbol analysis
|
|
192
|
+
console.warn(`Symbol analysis failed for ${filePath}:`, err.message);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return violations;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
analyzeRouteHandler(route) {
|
|
199
|
+
const violations = [];
|
|
200
|
+
const { handler, routeCall, type } = route;
|
|
201
|
+
|
|
202
|
+
// Check if route contains security middleware (skip analysis if present)
|
|
203
|
+
const handlerText = handler.getFullText();
|
|
204
|
+
const routeText = routeCall ? routeCall.getFullText() : handlerText;
|
|
205
|
+
|
|
206
|
+
if (
|
|
207
|
+
this.securityMiddleware.test(routeText) ||
|
|
208
|
+
this.securityMiddleware.test(handlerText)
|
|
209
|
+
) {
|
|
210
|
+
return violations; // Middleware handles header security
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Find version header exposures within this handler
|
|
214
|
+
const versionExposures = this.findVersionHeadersInNode(handler);
|
|
215
|
+
|
|
216
|
+
// Report violations for exposed version headers
|
|
217
|
+
for (const exposure of versionExposures) {
|
|
218
|
+
const startLine = exposure.node.getStartLineNumber();
|
|
219
|
+
violations.push({
|
|
220
|
+
ruleId: this.ruleId,
|
|
221
|
+
message: `Exposing version information in '${exposure.header}' header`,
|
|
222
|
+
severity: "warning",
|
|
223
|
+
line: startLine,
|
|
224
|
+
column: 1,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return violations;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
findVersionHeadersInNode(node) {
|
|
232
|
+
const exposures = [];
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const { SyntaxKind } = require("ts-morph");
|
|
236
|
+
|
|
237
|
+
// Find all call expressions that set headers
|
|
238
|
+
const callExpressions = node.getDescendantsOfKind(
|
|
239
|
+
SyntaxKind.CallExpression
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
for (const call of callExpressions) {
|
|
243
|
+
const expression = call.getExpression();
|
|
244
|
+
const args = call.getArguments();
|
|
245
|
+
|
|
246
|
+
if (args.length >= 2) {
|
|
247
|
+
// Check different header setting patterns
|
|
248
|
+
const expressionText = expression.getText();
|
|
249
|
+
|
|
250
|
+
// Express/NestJS: res.setHeader("HeaderName", "value")
|
|
251
|
+
if (/\.(setHeader|set|header)$/.test(expressionText)) {
|
|
252
|
+
const headerArg = args[0];
|
|
253
|
+
const valueArg = args[1];
|
|
254
|
+
|
|
255
|
+
if (headerArg.getKind() === SyntaxKind.StringLiteral) {
|
|
256
|
+
const headerName = headerArg.getLiteralValue();
|
|
257
|
+
const isVersionHeader = this.versionHeaders.some(
|
|
258
|
+
(vh) => vh.toLowerCase() === headerName.toLowerCase()
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (isVersionHeader) {
|
|
262
|
+
// Check if the value contains version information
|
|
263
|
+
let hasVersionInfo = false;
|
|
264
|
+
|
|
265
|
+
if (valueArg.getKind() === SyntaxKind.StringLiteral) {
|
|
266
|
+
const value = valueArg.getLiteralValue();
|
|
267
|
+
hasVersionInfo = this.containsVersionInfo(value);
|
|
268
|
+
} else {
|
|
269
|
+
// For non-literal values, assume they might contain version info
|
|
270
|
+
hasVersionInfo = true;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (hasVersionInfo) {
|
|
274
|
+
exposures.push({
|
|
275
|
+
node: call,
|
|
276
|
+
header: headerName,
|
|
277
|
+
value: valueArg.getText(),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Next.js: headers.set("HeaderName", "value")
|
|
285
|
+
if (/headers\s*\.\s*set$/.test(expressionText)) {
|
|
286
|
+
const headerArg = args[0];
|
|
287
|
+
const valueArg = args[1];
|
|
288
|
+
|
|
289
|
+
if (headerArg.getKind() === SyntaxKind.StringLiteral) {
|
|
290
|
+
const headerName = headerArg.getLiteralValue();
|
|
291
|
+
const isVersionHeader = this.versionHeaders.some(
|
|
292
|
+
(vh) => vh.toLowerCase() === headerName.toLowerCase()
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
if (isVersionHeader) {
|
|
296
|
+
exposures.push({
|
|
297
|
+
node: call,
|
|
298
|
+
header: headerName,
|
|
299
|
+
value: valueArg.getText(),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check for object literal header settings
|
|
308
|
+
const objectLiterals = node.getDescendantsOfKind(
|
|
309
|
+
SyntaxKind.ObjectLiteralExpression
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
for (const obj of objectLiterals) {
|
|
313
|
+
const properties = obj.getProperties();
|
|
314
|
+
|
|
315
|
+
for (const prop of properties) {
|
|
316
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
317
|
+
const nameNode = prop.getNameNode();
|
|
318
|
+
|
|
319
|
+
if (nameNode.getKind() === SyntaxKind.StringLiteral) {
|
|
320
|
+
const headerName = nameNode.getLiteralValue();
|
|
321
|
+
const isVersionHeader = this.versionHeaders.some(
|
|
322
|
+
(vh) => vh.toLowerCase() === headerName.toLowerCase()
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
if (isVersionHeader) {
|
|
326
|
+
exposures.push({
|
|
327
|
+
node: prop,
|
|
328
|
+
header: headerName,
|
|
329
|
+
value: prop.getInitializer()?.getText() || "unknown",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (error) {
|
|
337
|
+
console.warn("Error analyzing version headers:", error.message);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return exposures;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
containsVersionInfo(value) {
|
|
344
|
+
// Check if the value contains version patterns
|
|
345
|
+
const versionPatterns = [
|
|
346
|
+
/\d+\.\d+/, // Version numbers like 1.0, 2.1.3
|
|
347
|
+
/v\d+/i, // Version prefixes like v1, V2
|
|
348
|
+
/version/i, // The word "version"
|
|
349
|
+
/express/i, // Framework names
|
|
350
|
+
/node/i,
|
|
351
|
+
/nginx/i,
|
|
352
|
+
/apache/i,
|
|
353
|
+
/iis/i,
|
|
354
|
+
/php/i,
|
|
355
|
+
/asp\.net/i,
|
|
356
|
+
/ruby/i,
|
|
357
|
+
/python/i,
|
|
358
|
+
/django/i,
|
|
359
|
+
/rails/i,
|
|
360
|
+
/laravel/i,
|
|
361
|
+
/mysql/i, // Database versions
|
|
362
|
+
/postgresql/i,
|
|
363
|
+
/mongodb/i,
|
|
364
|
+
/redis/i,
|
|
365
|
+
/sqlite/i,
|
|
366
|
+
/mariadb/i,
|
|
367
|
+
];
|
|
368
|
+
|
|
369
|
+
return versionPatterns.some((pattern) => pattern.test(value));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
cleanup() {}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
module.exports = S038SymbolBasedAnalyzer;
|