@sun-asterisk/sunlint 1.3.7 → 1.3.9
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 +63 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +247 -53
- package/core/file-targeting-service.js +98 -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/S020_no_eval_dynamic_code/README.md +136 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
- package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
- package/rules/security/S030_directory_browsing_protection/README.md +128 -0
- package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
- package/rules/security/S030_directory_browsing_protection/config.json +63 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
- 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 +443 -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 +246 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S030 Symbol-Based Analyzer - Disable directory browsing and protect sensitive metadata files
|
|
3
|
+
* Enhanced to analyze static file serving configurations and middleware
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class S030SymbolBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine) {
|
|
8
|
+
this.ruleId = "S030";
|
|
9
|
+
this.semanticEngine = semanticEngine;
|
|
10
|
+
|
|
11
|
+
// Web framework static serving methods
|
|
12
|
+
this.staticMethods = [
|
|
13
|
+
"static",
|
|
14
|
+
"serveStatic",
|
|
15
|
+
"staticFiles",
|
|
16
|
+
"useStaticAssets",
|
|
17
|
+
];
|
|
18
|
+
this.frameworkMethods = {
|
|
19
|
+
express: ["static", "use"],
|
|
20
|
+
koa: ["static"],
|
|
21
|
+
fastify: ["register"],
|
|
22
|
+
hapi: ["route"],
|
|
23
|
+
nestjs: ["useStaticAssets", "useGlobalPrefix"],
|
|
24
|
+
nextjs: ["static", "use"],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Sensitive file patterns that should be protected
|
|
28
|
+
this.sensitiveFiles = [
|
|
29
|
+
".env",
|
|
30
|
+
".git",
|
|
31
|
+
".svn",
|
|
32
|
+
".hg",
|
|
33
|
+
".bzr",
|
|
34
|
+
".CVS",
|
|
35
|
+
"config",
|
|
36
|
+
"settings",
|
|
37
|
+
"secrets",
|
|
38
|
+
"keys",
|
|
39
|
+
"backup",
|
|
40
|
+
"database",
|
|
41
|
+
".aws",
|
|
42
|
+
".ssh",
|
|
43
|
+
"credentials",
|
|
44
|
+
"private",
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Directory listing indicators (dangerous configurations)
|
|
48
|
+
this.directoryListingPatterns = [
|
|
49
|
+
"autoIndex",
|
|
50
|
+
"directory",
|
|
51
|
+
"listing",
|
|
52
|
+
"browse",
|
|
53
|
+
"index",
|
|
54
|
+
"serveIndex",
|
|
55
|
+
"list",
|
|
56
|
+
"dotfiles",
|
|
57
|
+
"hidden",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Middleware that enables directory browsing
|
|
61
|
+
this.dangerousMiddleware = [
|
|
62
|
+
"serveIndex",
|
|
63
|
+
"serve-index",
|
|
64
|
+
"directory",
|
|
65
|
+
"autoIndex",
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async initialize() {}
|
|
70
|
+
|
|
71
|
+
analyze(sourceFile, filePath) {
|
|
72
|
+
const violations = [];
|
|
73
|
+
|
|
74
|
+
// Skip files that are unlikely to contain server configurations
|
|
75
|
+
const skipPatterns = [
|
|
76
|
+
/\.d\.ts$/,
|
|
77
|
+
/\.types\.ts$/,
|
|
78
|
+
/\.interface\.ts$/,
|
|
79
|
+
/\.constants?\.ts$/,
|
|
80
|
+
/\.spec\.ts$/,
|
|
81
|
+
/\.test\.ts$/,
|
|
82
|
+
/\.model\.ts$/,
|
|
83
|
+
/\.entity\.ts$/,
|
|
84
|
+
/\.dto\.ts$/,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
88
|
+
if (shouldSkip) {
|
|
89
|
+
return violations;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const { SyntaxKind } = require("ts-morph");
|
|
94
|
+
|
|
95
|
+
// Find static file serving configurations
|
|
96
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
97
|
+
SyntaxKind.CallExpression
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
for (const call of callExpressions) {
|
|
101
|
+
try {
|
|
102
|
+
// Analyze static file serving configurations
|
|
103
|
+
this.analyzeStaticFileServing(call, violations, filePath);
|
|
104
|
+
|
|
105
|
+
// Analyze middleware registration
|
|
106
|
+
this.analyzeMiddlewareUsage(call, violations, filePath);
|
|
107
|
+
|
|
108
|
+
// Analyze route handlers for sensitive paths
|
|
109
|
+
this.analyzeSensitiveRoutes(call, violations, filePath);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(
|
|
112
|
+
`⚠ [S030] Call expression analysis failed:`,
|
|
113
|
+
error.message
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Analyze NestJS decorators
|
|
119
|
+
const decorators = sourceFile.getDescendantsOfKind(SyntaxKind.Decorator);
|
|
120
|
+
for (const decorator of decorators) {
|
|
121
|
+
try {
|
|
122
|
+
this.analyzeNestJSDecorators(decorator, violations, filePath);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.warn(`⚠ [S030] Decorator analysis failed:`, error.message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Analyze NextJS export functions (API routes)
|
|
129
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind(
|
|
130
|
+
SyntaxKind.FunctionDeclaration
|
|
131
|
+
);
|
|
132
|
+
for (const func of functionDeclarations) {
|
|
133
|
+
try {
|
|
134
|
+
this.analyzeNextJSAPIRoutes(func, violations, filePath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn(
|
|
137
|
+
`⚠ [S030] NextJS API route analysis failed:`,
|
|
138
|
+
error.message
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for variable declarations that might expose sensitive paths
|
|
144
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(
|
|
145
|
+
SyntaxKind.VariableDeclaration
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
for (const variable of variableDeclarations) {
|
|
149
|
+
try {
|
|
150
|
+
this.analyzeSensitivePathVariables(variable, violations, filePath);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.warn(`⚠ [S030] Variable analysis failed:`, error.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check property assignments in object literals (config objects)
|
|
157
|
+
const objectLiterals = sourceFile.getDescendantsOfKind(
|
|
158
|
+
SyntaxKind.ObjectLiteralExpression
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
for (const objLiteral of objectLiterals) {
|
|
162
|
+
try {
|
|
163
|
+
this.analyzeConfigurationObjects(objLiteral, violations, filePath);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.warn(
|
|
166
|
+
`⚠ [S030] Object literal analysis failed:`,
|
|
167
|
+
error.message
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn(
|
|
173
|
+
`⚠ [S030] Symbol analysis failed for ${filePath}:`,
|
|
174
|
+
error.message
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return violations;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
analyzeStaticFileServing(call, violations, filePath) {
|
|
182
|
+
const { SyntaxKind } = require("ts-morph");
|
|
183
|
+
|
|
184
|
+
const expression = call.getExpression();
|
|
185
|
+
let methodName = null;
|
|
186
|
+
let objectName = null;
|
|
187
|
+
|
|
188
|
+
// Handle property access: app.use(), express.static()
|
|
189
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
190
|
+
methodName = expression.getName();
|
|
191
|
+
const object = expression.getExpression();
|
|
192
|
+
if (object.getKind() === SyntaxKind.Identifier) {
|
|
193
|
+
objectName = object.getText();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for static file serving patterns
|
|
198
|
+
if (this.staticMethods.includes(methodName) || methodName === "use") {
|
|
199
|
+
const args = call.getArguments();
|
|
200
|
+
|
|
201
|
+
for (const arg of args) {
|
|
202
|
+
// Check if serving sensitive directories
|
|
203
|
+
if (arg.getKind() === SyntaxKind.StringLiteral) {
|
|
204
|
+
const path = arg.getLiteralValue();
|
|
205
|
+
if (this.isSensitivePath(path)) {
|
|
206
|
+
const startLine = call.getStartLineNumber();
|
|
207
|
+
violations.push({
|
|
208
|
+
ruleId: this.ruleId,
|
|
209
|
+
message: `Static file serving exposes sensitive path '${path}' - this could leak sensitive metadata files`,
|
|
210
|
+
severity: "error",
|
|
211
|
+
line: startLine,
|
|
212
|
+
column: 1,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Check configuration objects for dangerous settings
|
|
218
|
+
if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
219
|
+
this.analyzeStaticConfigObject(
|
|
220
|
+
arg,
|
|
221
|
+
violations,
|
|
222
|
+
call.getStartLineNumber()
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
analyzeMiddlewareUsage(call, violations, filePath) {
|
|
230
|
+
const { SyntaxKind } = require("ts-morph");
|
|
231
|
+
|
|
232
|
+
const expression = call.getExpression();
|
|
233
|
+
|
|
234
|
+
// Check for dangerous middleware usage
|
|
235
|
+
const args = call.getArguments();
|
|
236
|
+
for (const arg of args) {
|
|
237
|
+
if (arg.getKind() === SyntaxKind.CallExpression) {
|
|
238
|
+
const innerExpression = arg.getExpression();
|
|
239
|
+
|
|
240
|
+
if (innerExpression.getKind() === SyntaxKind.Identifier) {
|
|
241
|
+
const middlewareName = innerExpression.getText();
|
|
242
|
+
if (this.dangerousMiddleware.includes(middlewareName)) {
|
|
243
|
+
const startLine = call.getStartLineNumber();
|
|
244
|
+
violations.push({
|
|
245
|
+
ruleId: this.ruleId,
|
|
246
|
+
message: `Dangerous middleware '${middlewareName}' enables directory browsing - disable or configure securely`,
|
|
247
|
+
severity: "error",
|
|
248
|
+
line: startLine,
|
|
249
|
+
column: 1,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
analyzeSensitiveRoutes(call, violations, filePath) {
|
|
258
|
+
const { SyntaxKind } = require("ts-morph");
|
|
259
|
+
|
|
260
|
+
const expression = call.getExpression();
|
|
261
|
+
|
|
262
|
+
// Check for route definitions that expose sensitive paths
|
|
263
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
264
|
+
const methodName = expression.getName();
|
|
265
|
+
const httpMethods = ["get", "post", "put", "delete", "patch", "all"];
|
|
266
|
+
|
|
267
|
+
if (httpMethods.includes(methodName)) {
|
|
268
|
+
const args = call.getArguments();
|
|
269
|
+
if (args.length > 0 && args[0].getKind() === SyntaxKind.StringLiteral) {
|
|
270
|
+
const routePath = args[0].getLiteralValue();
|
|
271
|
+
|
|
272
|
+
if (this.isSensitivePath(routePath)) {
|
|
273
|
+
const startLine = call.getStartLineNumber();
|
|
274
|
+
violations.push({
|
|
275
|
+
ruleId: this.ruleId,
|
|
276
|
+
message: `Route '${routePath}' exposes sensitive path - implement proper access controls`,
|
|
277
|
+
severity: "error",
|
|
278
|
+
line: startLine,
|
|
279
|
+
column: 1,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
analyzeStaticConfigObject(configObj, violations, lineNumber) {
|
|
288
|
+
const properties = configObj.getProperties();
|
|
289
|
+
|
|
290
|
+
for (const prop of properties) {
|
|
291
|
+
if (
|
|
292
|
+
prop.getKind() === require("ts-morph").SyntaxKind.PropertyAssignment
|
|
293
|
+
) {
|
|
294
|
+
const name = prop.getName();
|
|
295
|
+
const value = prop.getInitializer();
|
|
296
|
+
|
|
297
|
+
// Check for dangerous configuration options
|
|
298
|
+
if (this.directoryListingPatterns.includes(name)) {
|
|
299
|
+
if (value && this.isTruthyValue(value)) {
|
|
300
|
+
violations.push({
|
|
301
|
+
ruleId: this.ruleId,
|
|
302
|
+
message: `Configuration option '${name}' enables directory browsing - set to false or remove`,
|
|
303
|
+
severity: "error",
|
|
304
|
+
line: lineNumber,
|
|
305
|
+
column: 1,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Check for dotfiles configuration
|
|
311
|
+
if (name === "dotfiles" && value) {
|
|
312
|
+
const dotfilesValue = this.getValueAsString(value);
|
|
313
|
+
if (dotfilesValue === "allow" || dotfilesValue === true) {
|
|
314
|
+
violations.push({
|
|
315
|
+
ruleId: this.ruleId,
|
|
316
|
+
message:
|
|
317
|
+
"Dotfiles access is enabled - set dotfiles to 'deny' to protect sensitive files",
|
|
318
|
+
severity: "warning",
|
|
319
|
+
line: lineNumber,
|
|
320
|
+
column: 1,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
analyzeSensitivePathVariables(variable, violations, filePath) {
|
|
329
|
+
const initializer = variable.getInitializer();
|
|
330
|
+
const varName = variable.getName();
|
|
331
|
+
|
|
332
|
+
if (
|
|
333
|
+
initializer &&
|
|
334
|
+
initializer.getKind() === require("ts-morph").SyntaxKind.StringLiteral
|
|
335
|
+
) {
|
|
336
|
+
const path = initializer.getLiteralValue();
|
|
337
|
+
|
|
338
|
+
if (this.isSensitivePath(path)) {
|
|
339
|
+
const startLine = variable.getStartLineNumber();
|
|
340
|
+
violations.push({
|
|
341
|
+
ruleId: this.ruleId,
|
|
342
|
+
message: `Variable '${varName}' contains sensitive path '${path}' - ensure proper access controls`,
|
|
343
|
+
severity: "warning",
|
|
344
|
+
line: startLine,
|
|
345
|
+
column: 1,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
analyzeConfigurationObjects(objLiteral, violations, filePath) {
|
|
352
|
+
const properties = objLiteral.getProperties();
|
|
353
|
+
|
|
354
|
+
for (const prop of properties) {
|
|
355
|
+
if (
|
|
356
|
+
prop.getKind() === require("ts-morph").SyntaxKind.PropertyAssignment
|
|
357
|
+
) {
|
|
358
|
+
const name = prop.getName();
|
|
359
|
+
const value = prop.getInitializer();
|
|
360
|
+
|
|
361
|
+
// Check for configuration that might enable directory browsing
|
|
362
|
+
if (
|
|
363
|
+
name === "list" ||
|
|
364
|
+
name === "directory" ||
|
|
365
|
+
name === "autoIndex" ||
|
|
366
|
+
name === "listing"
|
|
367
|
+
) {
|
|
368
|
+
if (value && this.isTruthyValue(value)) {
|
|
369
|
+
const startLine = prop.getStartLineNumber();
|
|
370
|
+
violations.push({
|
|
371
|
+
ruleId: this.ruleId,
|
|
372
|
+
message: `Configuration '${name}: true' enables directory browsing - disable for security`,
|
|
373
|
+
severity: "error",
|
|
374
|
+
line: startLine,
|
|
375
|
+
column: 1,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check for hidden files access
|
|
381
|
+
if (name === "hidden") {
|
|
382
|
+
if (value && this.isTruthyValue(value)) {
|
|
383
|
+
const startLine = prop.getStartLineNumber();
|
|
384
|
+
violations.push({
|
|
385
|
+
ruleId: this.ruleId,
|
|
386
|
+
message: `Configuration '${name}: true' enables hidden files access - disable for security`,
|
|
387
|
+
severity: "warning",
|
|
388
|
+
line: startLine,
|
|
389
|
+
column: 1,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
isSensitivePath(path) {
|
|
398
|
+
if (!path || typeof path !== "string") return false;
|
|
399
|
+
|
|
400
|
+
const normalizedPath = path.toLowerCase();
|
|
401
|
+
|
|
402
|
+
return this.sensitiveFiles.some((sensitive) => {
|
|
403
|
+
// Check if path contains sensitive file/directory patterns
|
|
404
|
+
return (
|
|
405
|
+
normalizedPath.includes(sensitive.toLowerCase()) ||
|
|
406
|
+
normalizedPath.startsWith("/" + sensitive.toLowerCase()) ||
|
|
407
|
+
normalizedPath.startsWith("./" + sensitive.toLowerCase()) ||
|
|
408
|
+
normalizedPath.endsWith("/" + sensitive.toLowerCase())
|
|
409
|
+
);
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
isTruthyValue(valueNode) {
|
|
414
|
+
const { SyntaxKind } = require("ts-morph");
|
|
415
|
+
|
|
416
|
+
if (valueNode.getKind() === SyntaxKind.TrueKeyword) return true;
|
|
417
|
+
if (valueNode.getKind() === SyntaxKind.StringLiteral) {
|
|
418
|
+
const value = valueNode.getLiteralValue();
|
|
419
|
+
return value === "true" || value === "allow" || value === "yes";
|
|
420
|
+
}
|
|
421
|
+
if (valueNode.getKind() === SyntaxKind.NumericLiteral) {
|
|
422
|
+
return valueNode.getLiteralValue() !== 0;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
getValueAsString(valueNode) {
|
|
429
|
+
const { SyntaxKind } = require("ts-morph");
|
|
430
|
+
|
|
431
|
+
if (valueNode.getKind() === SyntaxKind.StringLiteral) {
|
|
432
|
+
return valueNode.getLiteralValue();
|
|
433
|
+
}
|
|
434
|
+
if (valueNode.getKind() === SyntaxKind.TrueKeyword) return true;
|
|
435
|
+
if (valueNode.getKind() === SyntaxKind.FalseKeyword) return false;
|
|
436
|
+
|
|
437
|
+
return valueNode.getText();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
analyzeNestJSDecorators(decorator, violations, filePath) {
|
|
441
|
+
const { SyntaxKind } = require("ts-morph");
|
|
442
|
+
|
|
443
|
+
const expression = decorator.getCallExpression();
|
|
444
|
+
if (!expression) return;
|
|
445
|
+
|
|
446
|
+
const decoratorName = expression.getExpression().getText();
|
|
447
|
+
|
|
448
|
+
// Check for route decorators with sensitive paths
|
|
449
|
+
if (["Get", "Post", "Put", "Delete", "Patch"].includes(decoratorName)) {
|
|
450
|
+
const args = expression.getArguments();
|
|
451
|
+
if (args.length > 0 && args[0].getKind() === SyntaxKind.StringLiteral) {
|
|
452
|
+
const routePath = args[0].getLiteralValue();
|
|
453
|
+
|
|
454
|
+
if (this.isSensitivePath(routePath)) {
|
|
455
|
+
const startLine = decorator.getStartLineNumber();
|
|
456
|
+
violations.push({
|
|
457
|
+
ruleId: this.ruleId,
|
|
458
|
+
message: `NestJS route '${routePath}' exposes sensitive path - implement proper access controls`,
|
|
459
|
+
severity: "error",
|
|
460
|
+
line: startLine,
|
|
461
|
+
column: 1,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Check for Controller decorator with sensitive paths
|
|
468
|
+
if (decoratorName === "Controller") {
|
|
469
|
+
const args = expression.getArguments();
|
|
470
|
+
if (args.length > 0 && args[0].getKind() === SyntaxKind.StringLiteral) {
|
|
471
|
+
const controllerPath = args[0].getLiteralValue();
|
|
472
|
+
|
|
473
|
+
if (this.isSensitivePath(controllerPath)) {
|
|
474
|
+
const startLine = decorator.getStartLineNumber();
|
|
475
|
+
violations.push({
|
|
476
|
+
ruleId: this.ruleId,
|
|
477
|
+
message: `NestJS controller path '${controllerPath}' exposes sensitive path - review controller design`,
|
|
478
|
+
severity: "warning",
|
|
479
|
+
line: startLine,
|
|
480
|
+
column: 1,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
analyzeNextJSAPIRoutes(func, violations, filePath) {
|
|
488
|
+
const { SyntaxKind } = require("ts-morph");
|
|
489
|
+
|
|
490
|
+
// Check if this is an exported API route function
|
|
491
|
+
const exportKeyword = func
|
|
492
|
+
.getFirstAncestorByKind(SyntaxKind.SourceFile)
|
|
493
|
+
?.getExportedDeclarations()
|
|
494
|
+
?.get(func.getName() || "default");
|
|
495
|
+
|
|
496
|
+
if (!exportKeyword) return;
|
|
497
|
+
|
|
498
|
+
// Check if function name suggests HTTP method (GET, POST, etc.)
|
|
499
|
+
const funcName = func.getName();
|
|
500
|
+
const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
501
|
+
|
|
502
|
+
if (httpMethods.includes(funcName)) {
|
|
503
|
+
// Check function body for sensitive file operations
|
|
504
|
+
const body = func.getBody();
|
|
505
|
+
if (body) {
|
|
506
|
+
const bodyText = body.getText();
|
|
507
|
+
|
|
508
|
+
// Check for sensitive file paths in the function body
|
|
509
|
+
const sensitivePatterns = [
|
|
510
|
+
/['"`][^'"`]*\.env[^'"`]*['"`]/g,
|
|
511
|
+
/['"`][^'"`]*\.git[^'"`]*['"`]/g,
|
|
512
|
+
/['"`][^'"`]*config[^'"`]*['"`]/g,
|
|
513
|
+
/['"`][^'"`]*backup[^'"`]*['"`]/g,
|
|
514
|
+
/['"`][^'"`]*secret[^'"`]*['"`]/g,
|
|
515
|
+
/['"`][^'"`]*\.ssh[^'"`]*['"`]/g,
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
for (const pattern of sensitivePatterns) {
|
|
519
|
+
const matches = [...bodyText.matchAll(pattern)];
|
|
520
|
+
if (matches.length > 0) {
|
|
521
|
+
const startLine = func.getStartLineNumber();
|
|
522
|
+
violations.push({
|
|
523
|
+
ruleId: this.ruleId,
|
|
524
|
+
message: `NextJS API route '${funcName}' contains sensitive file references - ensure proper access controls`,
|
|
525
|
+
severity: "error",
|
|
526
|
+
line: startLine,
|
|
527
|
+
column: 1,
|
|
528
|
+
});
|
|
529
|
+
break; // Only report once per function
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
cleanup() {}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
module.exports = S030SymbolBasedAnalyzer;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S033 Symbol-Based Analyzer - Set SameSite attribute for Session Cookies
|
|
3
|
-
* Uses
|
|
3
|
+
* Uses ts-morph for semantic analysis (consistent with SunLint architecture)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const { SyntaxKind } = require("ts-morph");
|
|
7
7
|
|
|
8
8
|
class S033SymbolBasedAnalyzer {
|
|
9
9
|
constructor(semanticEngine = null) {
|
|
@@ -78,7 +78,7 @@ class S033SymbolBasedAnalyzer {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
try {
|
|
81
|
-
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
81
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
82
82
|
if (!sourceFile) {
|
|
83
83
|
if (this.verbose) {
|
|
84
84
|
console.log(
|
|
@@ -684,13 +684,12 @@ class S033SymbolBasedAnalyzer {
|
|
|
684
684
|
/**
|
|
685
685
|
* Enhanced method name detection with framework support
|
|
686
686
|
*/
|
|
687
|
-
|
|
687
|
+
extractMethodName(callExpression) {
|
|
688
688
|
try {
|
|
689
|
-
const expression =
|
|
690
|
-
const ts = require("typescript");
|
|
689
|
+
const expression = callExpression.getExpression();
|
|
691
690
|
|
|
692
691
|
// Handle property access expressions (obj.method)
|
|
693
|
-
if (expression.getKind() ===
|
|
692
|
+
if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
694
693
|
const propertyName = expression.getNameNode().getText();
|
|
695
694
|
|
|
696
695
|
// Check for chained method calls like response.cookies.set
|
|
@@ -698,7 +697,7 @@ class S033SymbolBasedAnalyzer {
|
|
|
698
697
|
const objectExpression = expression.getExpression();
|
|
699
698
|
if (
|
|
700
699
|
objectExpression.getKind() ===
|
|
701
|
-
|
|
700
|
+
SyntaxKind.PropertyAccessExpression
|
|
702
701
|
) {
|
|
703
702
|
const parentProperty = objectExpression.getNameNode().getText();
|
|
704
703
|
if (parentProperty === "cookies") {
|
|
@@ -712,7 +711,7 @@ class S033SymbolBasedAnalyzer {
|
|
|
712
711
|
}
|
|
713
712
|
|
|
714
713
|
// Handle direct function calls
|
|
715
|
-
if (expression.getKind() ===
|
|
714
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
716
715
|
return expression.getText();
|
|
717
716
|
}
|
|
718
717
|
|