@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,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S039 Symbol-Based Analyzer - Do not pass Session Tokens via URL parameters
|
|
3
|
+
* Enhanced to analyze per route handler for URL parameter token exposure
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class S039SymbolBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine) {
|
|
8
|
+
this.ruleId = "S039";
|
|
9
|
+
this.semanticEngine = semanticEngine;
|
|
10
|
+
|
|
11
|
+
// Session token parameter names to detect
|
|
12
|
+
this.sessionTokenParams = [
|
|
13
|
+
"sessionId",
|
|
14
|
+
"session_id",
|
|
15
|
+
"session-id",
|
|
16
|
+
"sessionToken",
|
|
17
|
+
"session_token",
|
|
18
|
+
"session-token",
|
|
19
|
+
"authToken",
|
|
20
|
+
"auth_token",
|
|
21
|
+
"auth-token",
|
|
22
|
+
"authorization",
|
|
23
|
+
"bearer",
|
|
24
|
+
"jwt",
|
|
25
|
+
"jwtToken",
|
|
26
|
+
"jwt_token",
|
|
27
|
+
"jwt-token",
|
|
28
|
+
"accessToken",
|
|
29
|
+
"access_token",
|
|
30
|
+
"access-token",
|
|
31
|
+
"refreshToken",
|
|
32
|
+
"refresh_token",
|
|
33
|
+
"refresh-token",
|
|
34
|
+
"apiKey",
|
|
35
|
+
"api_key",
|
|
36
|
+
"api-key",
|
|
37
|
+
"csrfToken",
|
|
38
|
+
"csrf_token",
|
|
39
|
+
"csrf-token",
|
|
40
|
+
"xsrfToken",
|
|
41
|
+
"xsrf_token",
|
|
42
|
+
"xsrf-token",
|
|
43
|
+
"token",
|
|
44
|
+
"apiToken",
|
|
45
|
+
"api_token",
|
|
46
|
+
"api-token",
|
|
47
|
+
"sid",
|
|
48
|
+
"sessionkey",
|
|
49
|
+
"session_key",
|
|
50
|
+
"session-key",
|
|
51
|
+
"userToken",
|
|
52
|
+
"user_token",
|
|
53
|
+
"user-token",
|
|
54
|
+
"authKey",
|
|
55
|
+
"auth_key",
|
|
56
|
+
"auth-key",
|
|
57
|
+
"securityToken",
|
|
58
|
+
"security_token",
|
|
59
|
+
"security-token",
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// Pattern to detect session token-like values
|
|
63
|
+
this.tokenValuePattern = /^[a-zA-Z0-9+/=\-_.]{16,}$/;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize() {}
|
|
67
|
+
|
|
68
|
+
analyze(sourceFile, filePath) {
|
|
69
|
+
const violations = [];
|
|
70
|
+
|
|
71
|
+
// Skip files that are unlikely to be route handlers
|
|
72
|
+
const skipPatterns = [
|
|
73
|
+
/\.dto\.ts$/,
|
|
74
|
+
/\.interface\.ts$/,
|
|
75
|
+
/\.module\.ts$/,
|
|
76
|
+
/\.service\.spec\.ts$/,
|
|
77
|
+
/\.controller\.spec\.ts$/,
|
|
78
|
+
/\.spec\.ts$/,
|
|
79
|
+
/\.test\.ts$/,
|
|
80
|
+
/\.d\.ts$/,
|
|
81
|
+
/\.types\.ts$/,
|
|
82
|
+
/\.constants?.ts$/,
|
|
83
|
+
/\.config\.ts$/,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
87
|
+
if (shouldSkip) {
|
|
88
|
+
return violations;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const { SyntaxKind } = require("ts-morph");
|
|
93
|
+
|
|
94
|
+
// Find all function expressions and arrow functions that could be route handlers
|
|
95
|
+
const routeHandlers = [];
|
|
96
|
+
|
|
97
|
+
// Express route patterns: app.get("/path", (req, res) => {...})
|
|
98
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
99
|
+
SyntaxKind.CallExpression
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (const call of callExpressions) {
|
|
103
|
+
const expression = call.getExpression();
|
|
104
|
+
if (expression && expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
105
|
+
const nameNode = expression.getNameNode();
|
|
106
|
+
if (nameNode) {
|
|
107
|
+
const methodName = nameNode.getText();
|
|
108
|
+
if (/^(get|post|put|delete|patch|all|use)$/.test(methodName)) {
|
|
109
|
+
const args = call.getArguments();
|
|
110
|
+
const lastArg = args[args.length - 1];
|
|
111
|
+
// The last argument should be the handler function
|
|
112
|
+
if (
|
|
113
|
+
lastArg && (
|
|
114
|
+
lastArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
115
|
+
lastArg.getKind() === SyntaxKind.FunctionExpression
|
|
116
|
+
)
|
|
117
|
+
) {
|
|
118
|
+
routeHandlers.push({
|
|
119
|
+
handler: lastArg,
|
|
120
|
+
routeCall: call,
|
|
121
|
+
type: "express",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Next.js export functions
|
|
130
|
+
const exportAssignments = sourceFile.getDescendantsOfKind(
|
|
131
|
+
SyntaxKind.ExportAssignment
|
|
132
|
+
);
|
|
133
|
+
const exportDeclarations = sourceFile.getDescendantsOfKind(
|
|
134
|
+
SyntaxKind.ExportDeclaration
|
|
135
|
+
);
|
|
136
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind(
|
|
137
|
+
SyntaxKind.FunctionDeclaration
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
for (const func of functionDeclarations) {
|
|
141
|
+
const name = func.getName();
|
|
142
|
+
if (name && /^(GET|POST|PUT|DELETE|PATCH|handler)$/.test(name)) {
|
|
143
|
+
routeHandlers.push({
|
|
144
|
+
handler: func,
|
|
145
|
+
type: "nextjs",
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// NestJS Controller methods with decorators
|
|
151
|
+
const methodDeclarations = sourceFile.getDescendantsOfKind(
|
|
152
|
+
SyntaxKind.MethodDeclaration
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
for (const method of methodDeclarations) {
|
|
156
|
+
const decorators = method.getDecorators();
|
|
157
|
+
const hasRouteDecorator = decorators.some((decorator) => {
|
|
158
|
+
const decoratorName = decorator.getName();
|
|
159
|
+
return /^(Get|Post|Put|Delete|Patch|All)$/.test(decoratorName);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (hasRouteDecorator) {
|
|
163
|
+
routeHandlers.push({
|
|
164
|
+
handler: method,
|
|
165
|
+
type: "nestjs",
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Nuxt.js defineEventHandler patterns
|
|
171
|
+
for (const call of callExpressions) {
|
|
172
|
+
const expression = call.getExpression();
|
|
173
|
+
if (expression && expression.getKind() === SyntaxKind.Identifier) {
|
|
174
|
+
const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
|
|
175
|
+
if (identifier.getText() === "defineEventHandler") {
|
|
176
|
+
// Find the arrow function or function parameter
|
|
177
|
+
const args = call.getArguments();
|
|
178
|
+
if (args.length > 0) {
|
|
179
|
+
const firstArg = args[0];
|
|
180
|
+
if (
|
|
181
|
+
firstArg && (
|
|
182
|
+
firstArg.getKind() === SyntaxKind.ArrowFunction ||
|
|
183
|
+
firstArg.getKind() === SyntaxKind.FunctionExpression
|
|
184
|
+
)
|
|
185
|
+
) {
|
|
186
|
+
routeHandlers.push({
|
|
187
|
+
handler: firstArg,
|
|
188
|
+
type: "nuxtjs",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Analyze each route handler for session token exposure in URL parameters
|
|
197
|
+
for (const { handler, routeCall, type } of routeHandlers) {
|
|
198
|
+
try {
|
|
199
|
+
const handlerViolations = this.analyzeRouteHandler(
|
|
200
|
+
handler,
|
|
201
|
+
routeCall,
|
|
202
|
+
type,
|
|
203
|
+
filePath
|
|
204
|
+
);
|
|
205
|
+
violations.push(...handlerViolations);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(
|
|
208
|
+
`⚠ [S039] Handler analysis failed for ${type}:`,
|
|
209
|
+
error.message
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.warn(
|
|
215
|
+
`⚠ [S039] Symbol analysis failed for ${filePath}:`,
|
|
216
|
+
error.message
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return violations;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
analyzeRouteHandler(handler, routeCall, type, filePath) {
|
|
224
|
+
const violations = [];
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const { SyntaxKind } = require("ts-morph");
|
|
228
|
+
|
|
229
|
+
// Find URL parameter access patterns within this handler
|
|
230
|
+
const tokenExposures = this.findTokenParametersInNode(handler);
|
|
231
|
+
|
|
232
|
+
// Report violations for exposed session tokens in URL parameters
|
|
233
|
+
for (const exposure of tokenExposures) {
|
|
234
|
+
const startLine = exposure.node.getStartLineNumber();
|
|
235
|
+
violations.push({
|
|
236
|
+
ruleId: this.ruleId,
|
|
237
|
+
message: `Session token '${exposure.paramName}' passed via URL parameter - use secure headers or request body instead`,
|
|
238
|
+
severity: "warning",
|
|
239
|
+
line: startLine,
|
|
240
|
+
column: 1,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.warn(`⚠ [S039] Route handler analysis failed:`, error.message);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return violations;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
findTokenParametersInNode(node) {
|
|
251
|
+
const exposures = [];
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const { SyntaxKind } = require("ts-morph");
|
|
255
|
+
|
|
256
|
+
// For NestJS, check decorator parameters first
|
|
257
|
+
if (node && node.getKind() === SyntaxKind.MethodDeclaration) {
|
|
258
|
+
const parameters = node.getParameters();
|
|
259
|
+
for (const param of parameters) {
|
|
260
|
+
const decorators = param.getDecorators();
|
|
261
|
+
for (const decorator of decorators) {
|
|
262
|
+
const decoratorName = decorator.getName();
|
|
263
|
+
if (decoratorName === "Query" || decoratorName === "Param") {
|
|
264
|
+
const args = decorator.getArguments();
|
|
265
|
+
if (args.length > 0) {
|
|
266
|
+
const firstArg = args[0];
|
|
267
|
+
if (firstArg && firstArg.getKind() === SyntaxKind.StringLiteral) {
|
|
268
|
+
const paramName = firstArg.getLiteralValue();
|
|
269
|
+
if (this.isSessionTokenParam(paramName)) {
|
|
270
|
+
exposures.push({
|
|
271
|
+
node: param,
|
|
272
|
+
paramName: paramName,
|
|
273
|
+
accessType: decoratorName.toLowerCase(),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Find all property access expressions for URL parameters
|
|
284
|
+
const propertyAccesses = node.getDescendantsOfKind(
|
|
285
|
+
SyntaxKind.PropertyAccessExpression
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
for (const propAccess of propertyAccesses) {
|
|
289
|
+
const expression = propAccess.getExpression();
|
|
290
|
+
const property = propAccess.getName();
|
|
291
|
+
|
|
292
|
+
// Check for req.query.paramName patterns
|
|
293
|
+
if (expression && expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
294
|
+
const parentExpression = expression.getExpression();
|
|
295
|
+
const parentProperty = expression.getName();
|
|
296
|
+
|
|
297
|
+
// req.query.sessionToken, req.params.authToken, etc.
|
|
298
|
+
if (
|
|
299
|
+
parentProperty === "query" ||
|
|
300
|
+
parentProperty === "params" ||
|
|
301
|
+
parentProperty === "searchParams"
|
|
302
|
+
) {
|
|
303
|
+
if (this.isSessionTokenParam(property)) {
|
|
304
|
+
exposures.push({
|
|
305
|
+
node: propAccess,
|
|
306
|
+
paramName: property,
|
|
307
|
+
accessType: parentProperty,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check for bracket notation access: req.query["access-token"]
|
|
315
|
+
const elementAccessExpressions = node.getDescendantsOfKind(
|
|
316
|
+
SyntaxKind.ElementAccessExpression
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
for (const elemAccess of elementAccessExpressions) {
|
|
320
|
+
const expression = elemAccess.getExpression();
|
|
321
|
+
const argumentExpression = elemAccess.getArgumentExpression();
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
expression && expression.getKind() === SyntaxKind.PropertyAccessExpression &&
|
|
325
|
+
argumentExpression &&
|
|
326
|
+
argumentExpression.getKind() === SyntaxKind.StringLiteral
|
|
327
|
+
) {
|
|
328
|
+
const parentProperty = expression.getName();
|
|
329
|
+
const paramName = argumentExpression.getLiteralValue();
|
|
330
|
+
|
|
331
|
+
// req.query["sessionToken"], req.params["authToken"], etc.
|
|
332
|
+
if (
|
|
333
|
+
(parentProperty === "query" ||
|
|
334
|
+
parentProperty === "params" ||
|
|
335
|
+
parentProperty === "searchParams") &&
|
|
336
|
+
this.isSessionTokenParam(paramName)
|
|
337
|
+
) {
|
|
338
|
+
exposures.push({
|
|
339
|
+
node: elemAccess,
|
|
340
|
+
paramName: paramName,
|
|
341
|
+
accessType: parentProperty,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Check for URL.searchParams.get() patterns
|
|
348
|
+
const callExpressions = node.getDescendantsOfKind(
|
|
349
|
+
SyntaxKind.CallExpression
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
for (const call of callExpressions) {
|
|
353
|
+
const callExpression = call.getExpression();
|
|
354
|
+
if (callExpression && callExpression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
355
|
+
const methodName = callExpression.getName();
|
|
356
|
+
const objectExpression = callExpression.getExpression();
|
|
357
|
+
|
|
358
|
+
// searchParams.get("sessionToken"), URLSearchParams.get("token")
|
|
359
|
+
if (
|
|
360
|
+
methodName === "get" &&
|
|
361
|
+
objectExpression && objectExpression.getText().includes("searchParams")
|
|
362
|
+
) {
|
|
363
|
+
const args = call.getArguments();
|
|
364
|
+
if (args.length > 0) {
|
|
365
|
+
const firstArg = args[0];
|
|
366
|
+
if (firstArg && firstArg.getKind() === SyntaxKind.StringLiteral) {
|
|
367
|
+
const paramName = firstArg.getLiteralValue();
|
|
368
|
+
if (this.isSessionTokenParam(paramName)) {
|
|
369
|
+
exposures.push({
|
|
370
|
+
node: call,
|
|
371
|
+
paramName: paramName,
|
|
372
|
+
accessType: "searchParams",
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Check for object destructuring patterns
|
|
382
|
+
const variableDeclarations = node.getDescendantsOfKind(
|
|
383
|
+
SyntaxKind.VariableDeclaration
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
for (const varDecl of variableDeclarations) {
|
|
387
|
+
const nameNode = varDecl.getNameNode();
|
|
388
|
+
if (nameNode && nameNode.getKind() === SyntaxKind.ObjectBindingPattern) {
|
|
389
|
+
const bindingPattern = nameNode.asKindOrThrow(
|
|
390
|
+
SyntaxKind.ObjectBindingPattern
|
|
391
|
+
);
|
|
392
|
+
const elements = bindingPattern.getElements();
|
|
393
|
+
|
|
394
|
+
const initializer = varDecl.getInitializer();
|
|
395
|
+
if (
|
|
396
|
+
initializer &&
|
|
397
|
+
(initializer.getText().includes("req.query") ||
|
|
398
|
+
initializer.getText().includes("req.params") ||
|
|
399
|
+
initializer.getText().includes("searchParams"))
|
|
400
|
+
) {
|
|
401
|
+
for (const element of elements) {
|
|
402
|
+
let paramName = null;
|
|
403
|
+
|
|
404
|
+
// Handle both { paramName } and { "param-name": alias } patterns
|
|
405
|
+
const propNameNode = element.getPropertyNameNode();
|
|
406
|
+
const nameNode = element.getNameNode();
|
|
407
|
+
|
|
408
|
+
if (propNameNode) {
|
|
409
|
+
// { "param-name": alias } or { paramName: alias }
|
|
410
|
+
paramName = propNameNode.getText().replace(/['"]/g, "");
|
|
411
|
+
} else if (nameNode) {
|
|
412
|
+
// { paramName } shorthand
|
|
413
|
+
paramName = nameNode.getText();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (this.isSessionTokenParam(paramName)) {
|
|
417
|
+
exposures.push({
|
|
418
|
+
node: element,
|
|
419
|
+
paramName: paramName,
|
|
420
|
+
accessType: "destructuring",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch (error) {
|
|
428
|
+
console.warn(`⚠ [S039] Parameter analysis failed:`, error.message);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return exposures;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
isSessionTokenParam(paramName) {
|
|
435
|
+
return this.sessionTokenParams.some(
|
|
436
|
+
(tokenParam) => tokenParam.toLowerCase() === paramName.toLowerCase()
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
cleanup() {}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
module.exports = S039SymbolBasedAnalyzer;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S049 Main Analyzer - Authentication tokens should have short validity periods
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S049 --input=examples/rule-test-fixtures/rules/S049_short_validity_tokens --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S049SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S049RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S049Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S049] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S049] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S049";
|
|
23
|
+
this.ruleName = "Authentication tokens should have short validity periods";
|
|
24
|
+
this.description =
|
|
25
|
+
"Authentication tokens (JWT, session tokens, etc.) should have appropriately short validity periods to minimize the risk of token compromise. Long-lived tokens increase the attack surface and potential impact of token theft.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
30
|
+
this.config = {
|
|
31
|
+
useSymbolBased: true, // Primary approach
|
|
32
|
+
fallbackToRegex: true, // Secondary approach
|
|
33
|
+
regexBasedOnly: false, // Can be set to true for pure mode
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize analyzers
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S049SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
40
|
+
console.log(`🔧 [S049] Symbol analyzer created successfully`);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`🔧 [S049] Error creating symbol analyzer:`, error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.regexAnalyzer = new S049RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
+
console.log(`🔧 [S049] Regex analyzer created successfully`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`🔧 [S049] Error creating regex analyzer:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize analyzer with semantic engine
|
|
58
|
+
*/
|
|
59
|
+
async initialize(semanticEngine) {
|
|
60
|
+
this.semanticEngine = semanticEngine;
|
|
61
|
+
|
|
62
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
63
|
+
console.log(`🔧 [S049] Main analyzer initializing...`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize both analyzers
|
|
67
|
+
if (this.symbolAnalyzer) {
|
|
68
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
69
|
+
}
|
|
70
|
+
if (this.regexAnalyzer) {
|
|
71
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clean up if needed
|
|
75
|
+
if (this.regexAnalyzer) {
|
|
76
|
+
this.regexAnalyzer.cleanup?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.log(`🔧 [S049] Main analyzer initialized successfully`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Single file analysis method for testing
|
|
86
|
+
*/
|
|
87
|
+
analyzeSingle(filePath, options = {}) {
|
|
88
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
89
|
+
console.log(`📊 [S049] analyzeSingle() called for: ${filePath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Return result using same format as analyze method
|
|
93
|
+
return this.analyze([filePath], "typescript", options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async analyze(files, language, options = {}) {
|
|
97
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
+
console.log(
|
|
99
|
+
`🔧 [S049] analyze() method called with ${files.length} files, language: ${language}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const results = [];
|
|
104
|
+
const preferredEngine = options.engine || "heuristic";
|
|
105
|
+
|
|
106
|
+
for (const filePath of files) {
|
|
107
|
+
let fileResults = [];
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Determine analysis strategy
|
|
111
|
+
let useSymbolBased = this.config.useSymbolBased && this.semanticEngine;
|
|
112
|
+
|
|
113
|
+
if (preferredEngine === "regex") {
|
|
114
|
+
useSymbolBased = false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Primary analysis: Symbol-based (when available and enabled)
|
|
118
|
+
if (useSymbolBased && this.symbolAnalyzer) {
|
|
119
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
120
|
+
console.log(`🔧 [S049] Using symbol-based analysis for: ${filePath}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
fileResults = await this.symbolAnalyzer.analyze(filePath, language, options);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(`🔧 [S049] Symbol-based analysis failed:`, error);
|
|
127
|
+
fileResults = [];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Fallback analysis: Regex-based
|
|
132
|
+
if ((!fileResults || fileResults.length === 0) && this.config.fallbackToRegex && this.regexAnalyzer) {
|
|
133
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
134
|
+
console.log(`🔧 [S049] Using regex-based analysis for: ${filePath}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
fileResults = await this.regexAnalyzer.analyze(filePath, language, options);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error(`🔧 [S049] Regex-based analysis failed:`, error);
|
|
141
|
+
fileResults = [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Add file results to overall results
|
|
146
|
+
if (fileResults && fileResults.length > 0) {
|
|
147
|
+
results.push(...fileResults);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error(`🔧 [S049] Error analyzing file ${filePath}:`, error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
156
|
+
console.log(`🔧 [S049] Analysis completed. Found ${results.length} issues.`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return results;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Clean up resources
|
|
164
|
+
*/
|
|
165
|
+
cleanup() {
|
|
166
|
+
if (this.symbolAnalyzer) {
|
|
167
|
+
this.symbolAnalyzer.cleanup?.();
|
|
168
|
+
}
|
|
169
|
+
if (this.regexAnalyzer) {
|
|
170
|
+
this.regexAnalyzer.cleanup?.();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = S049Analyzer;
|