@sun-asterisk/sunlint 1.3.18 → 1.3.20
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/config/rules/enhanced-rules-registry.json +77 -18
- package/core/cli-program.js +9 -1
- package/core/github-annotate-service.js +986 -0
- package/core/output-service.js +294 -6
- package/core/summary-report-service.js +30 -30
- package/docs/GITHUB_ACTIONS_INTEGRATION.md +421 -0
- package/package.json +2 -1
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S041 Symbol-Based Analyzer - Session Tokens must be invalidated after logout or expiration
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S041SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S041";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Session management methods that should invalidate tokens
|
|
15
|
+
this.sessionMethods = [
|
|
16
|
+
"logout",
|
|
17
|
+
"signOut",
|
|
18
|
+
"logOut",
|
|
19
|
+
"destroy",
|
|
20
|
+
"clear",
|
|
21
|
+
"invalidate",
|
|
22
|
+
"revoke",
|
|
23
|
+
"expire",
|
|
24
|
+
"endSession",
|
|
25
|
+
"end-session",
|
|
26
|
+
"clear-session",
|
|
27
|
+
"destroy-session",
|
|
28
|
+
"remove-session"
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Token-related methods and properties
|
|
32
|
+
this.tokenMethods = [
|
|
33
|
+
"removeToken",
|
|
34
|
+
"clearToken",
|
|
35
|
+
"invalidateToken",
|
|
36
|
+
"revokeToken",
|
|
37
|
+
"deleteToken",
|
|
38
|
+
"destroyToken"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Session storage methods
|
|
42
|
+
this.sessionStorageMethods = [
|
|
43
|
+
"removeItem",
|
|
44
|
+
"clear",
|
|
45
|
+
"destroy",
|
|
46
|
+
"delete"
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// JWT token methods
|
|
50
|
+
this.jwtMethods = [
|
|
51
|
+
"sign",
|
|
52
|
+
"verify",
|
|
53
|
+
"decode",
|
|
54
|
+
"invalidate",
|
|
55
|
+
"blacklist"
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
// Express session methods
|
|
59
|
+
this.expressSessionMethods = [
|
|
60
|
+
"destroy",
|
|
61
|
+
"regenerate",
|
|
62
|
+
"reload",
|
|
63
|
+
"save"
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Session data clearing patterns
|
|
67
|
+
this.sessionClearPatterns = [
|
|
68
|
+
"req.session.destroy",
|
|
69
|
+
"req.session = null",
|
|
70
|
+
"req.session = {}",
|
|
71
|
+
"session.destroy",
|
|
72
|
+
"session.clear",
|
|
73
|
+
"session.remove"
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
// Token invalidation patterns
|
|
77
|
+
this.tokenInvalidationPatterns = [
|
|
78
|
+
"blacklist",
|
|
79
|
+
"revoke",
|
|
80
|
+
"invalidate",
|
|
81
|
+
"expire",
|
|
82
|
+
"remove",
|
|
83
|
+
"delete"
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// User input sources for session data
|
|
87
|
+
this.userInputSources = [
|
|
88
|
+
"req",
|
|
89
|
+
"request",
|
|
90
|
+
"params",
|
|
91
|
+
"query",
|
|
92
|
+
"body",
|
|
93
|
+
"headers",
|
|
94
|
+
"cookies",
|
|
95
|
+
"session"
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Initialize analyzer with semantic engine
|
|
101
|
+
*/
|
|
102
|
+
async initialize(semanticEngine) {
|
|
103
|
+
this.semanticEngine = semanticEngine;
|
|
104
|
+
if (this.verbose) {
|
|
105
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async analyze(filePath) {
|
|
110
|
+
if (this.verbose) {
|
|
111
|
+
console.log(
|
|
112
|
+
`🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!this.semanticEngine) {
|
|
117
|
+
if (this.verbose) {
|
|
118
|
+
console.log(
|
|
119
|
+
`🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
127
|
+
if (!sourceFile) {
|
|
128
|
+
if (this.verbose) {
|
|
129
|
+
console.log(
|
|
130
|
+
`🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
return await this.analyzeTsMorph(filePath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (this.verbose) {
|
|
137
|
+
console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (this.verbose) {
|
|
143
|
+
console.log(
|
|
144
|
+
`🔍 [${this.ruleId}] Symbol: Error in analysis:`,
|
|
145
|
+
error.message
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async analyzeTsMorph(filePath) {
|
|
153
|
+
try {
|
|
154
|
+
if (this.verbose) {
|
|
155
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const { Project } = require("ts-morph");
|
|
159
|
+
const project = new Project();
|
|
160
|
+
const sourceFile = project.addSourceFileAtPath(filePath);
|
|
161
|
+
|
|
162
|
+
return await this.analyzeSourceFile(sourceFile, filePath);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
if (this.verbose) {
|
|
165
|
+
console.log(
|
|
166
|
+
`🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`,
|
|
167
|
+
error.message
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async analyzeSourceFile(sourceFile, filePath) {
|
|
175
|
+
const violations = [];
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
if (this.verbose) {
|
|
179
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Starting symbol-based analysis`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const callExpressions = sourceFile.getDescendantsOfKind
|
|
183
|
+
? sourceFile.getDescendantsOfKind(
|
|
184
|
+
require("typescript").SyntaxKind.CallExpression
|
|
185
|
+
)
|
|
186
|
+
: [];
|
|
187
|
+
|
|
188
|
+
if (this.verbose) {
|
|
189
|
+
console.log(
|
|
190
|
+
`🔍 [${this.ruleId}] Symbol: Found ${callExpressions.length} call expressions`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const callNode of callExpressions) {
|
|
195
|
+
try {
|
|
196
|
+
// Analyze logout methods without proper session cleanup
|
|
197
|
+
const logoutViolation = this.analyzeLogoutMethod(callNode, sourceFile);
|
|
198
|
+
if (logoutViolation) {
|
|
199
|
+
violations.push(logoutViolation);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Analyze session methods without token invalidation
|
|
203
|
+
const sessionViolation = this.analyzeSessionMethod(callNode, sourceFile);
|
|
204
|
+
if (sessionViolation) {
|
|
205
|
+
violations.push(sessionViolation);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Analyze JWT token handling
|
|
209
|
+
const jwtViolation = this.analyzeJWTTokenHandling(callNode, sourceFile);
|
|
210
|
+
if (jwtViolation) {
|
|
211
|
+
violations.push(jwtViolation);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (this.verbose) {
|
|
216
|
+
console.log(
|
|
217
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`,
|
|
218
|
+
error.message
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Also check for function declarations that might be logout handlers
|
|
225
|
+
const functionDeclarations = sourceFile.getDescendantsOfKind
|
|
226
|
+
? sourceFile.getDescendantsOfKind(
|
|
227
|
+
require("typescript").SyntaxKind.FunctionDeclaration
|
|
228
|
+
)
|
|
229
|
+
: [];
|
|
230
|
+
|
|
231
|
+
for (const funcNode of functionDeclarations) {
|
|
232
|
+
try {
|
|
233
|
+
const logoutHandlerViolation = this.analyzeLogoutHandler(funcNode, sourceFile);
|
|
234
|
+
if (logoutHandlerViolation) {
|
|
235
|
+
violations.push(logoutHandlerViolation);
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (this.verbose) {
|
|
239
|
+
console.log(
|
|
240
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing function declaration:`,
|
|
241
|
+
error.message
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (this.verbose) {
|
|
248
|
+
console.log(
|
|
249
|
+
`🔍 [${this.ruleId}] Symbol: Analysis completed. Found ${violations.length} violations`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return violations;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
if (this.verbose) {
|
|
256
|
+
console.log(
|
|
257
|
+
`🔍 [${this.ruleId}] Symbol: Error in source file analysis:`,
|
|
258
|
+
error.message
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
analyzeLogoutMethod(callNode, sourceFile) {
|
|
266
|
+
try {
|
|
267
|
+
const expression = callNode.getExpression();
|
|
268
|
+
const methodName = this.getMethodName(expression);
|
|
269
|
+
|
|
270
|
+
// Only check actual logout methods, not session cleanup methods
|
|
271
|
+
const actualLogoutMethods = ['logout', 'signOut', 'logOut', 'invalidate', 'revoke', 'expire'];
|
|
272
|
+
if (!actualLogoutMethods.includes(methodName)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.verbose) {
|
|
277
|
+
console.log(
|
|
278
|
+
`🔍 [${this.ruleId}] Symbol: Logout method detected: ${methodName}`
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check if session cleanup is performed in the same function
|
|
283
|
+
const hasSessionCleanup = this.hasSessionCleanupInContext(callNode, sourceFile);
|
|
284
|
+
if (!hasSessionCleanup) {
|
|
285
|
+
return this.createViolation(
|
|
286
|
+
sourceFile,
|
|
287
|
+
callNode,
|
|
288
|
+
`Session token invalidation vulnerability: ${methodName}() method should invalidate session tokens and clear session data`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return null;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
if (this.verbose) {
|
|
295
|
+
console.log(
|
|
296
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing logout method:`,
|
|
297
|
+
error.message
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
analyzeSessionMethod(callNode, sourceFile) {
|
|
305
|
+
try {
|
|
306
|
+
const expression = callNode.getExpression();
|
|
307
|
+
const methodName = this.getMethodName(expression);
|
|
308
|
+
|
|
309
|
+
// Check if it's a session-related method
|
|
310
|
+
const isSessionMethod = this.isSessionRelatedMethod(expression);
|
|
311
|
+
if (!isSessionMethod) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Only check in logout context
|
|
316
|
+
const isInLogoutContext = this.isInLogoutContext(callNode, sourceFile);
|
|
317
|
+
if (!isInLogoutContext) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (this.verbose) {
|
|
322
|
+
console.log(
|
|
323
|
+
`🔍 [${this.ruleId}] Symbol: Session method detected in logout context: ${methodName}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Check if token invalidation is performed
|
|
328
|
+
const hasTokenInvalidation = this.hasTokenInvalidationInContext(callNode, sourceFile);
|
|
329
|
+
if (!hasTokenInvalidation) {
|
|
330
|
+
return this.createViolation(
|
|
331
|
+
sourceFile,
|
|
332
|
+
callNode,
|
|
333
|
+
`Session token invalidation vulnerability: Session method ${methodName}() should invalidate tokens during logout`
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return null;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
if (this.verbose) {
|
|
340
|
+
console.log(
|
|
341
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing session method:`,
|
|
342
|
+
error.message
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
analyzeJWTTokenHandling(callNode, sourceFile) {
|
|
350
|
+
try {
|
|
351
|
+
const expression = callNode.getExpression();
|
|
352
|
+
const methodName = this.getMethodName(expression);
|
|
353
|
+
|
|
354
|
+
if (!this.jwtMethods.includes(methodName)) {
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (this.verbose) {
|
|
359
|
+
console.log(
|
|
360
|
+
`🔍 [${this.ruleId}] Symbol: JWT method detected: ${methodName}`
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Check if it's in a logout context and if token is properly invalidated
|
|
365
|
+
const isInLogoutContext = this.isInLogoutContext(callNode, sourceFile);
|
|
366
|
+
if (isInLogoutContext && methodName === "sign") {
|
|
367
|
+
return this.createViolation(
|
|
368
|
+
sourceFile,
|
|
369
|
+
callNode,
|
|
370
|
+
`Session token invalidation vulnerability: JWT token should be invalidated/blacklisted during logout, not signed`
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return null;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (this.verbose) {
|
|
377
|
+
console.log(
|
|
378
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing JWT token handling:`,
|
|
379
|
+
error.message
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
analyzeLogoutHandler(funcNode, sourceFile) {
|
|
387
|
+
try {
|
|
388
|
+
const functionName = funcNode.getName();
|
|
389
|
+
const functionText = funcNode.getText();
|
|
390
|
+
|
|
391
|
+
// Check if it's a logout handler
|
|
392
|
+
const isLogoutHandler = this.isLogoutHandler(functionName, functionText);
|
|
393
|
+
if (!isLogoutHandler) {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (this.verbose) {
|
|
398
|
+
console.log(
|
|
399
|
+
`🔍 [${this.ruleId}] Symbol: Logout handler detected: ${functionName}`
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Check if session cleanup is performed
|
|
404
|
+
const hasSessionCleanup = this.hasSessionCleanupInFunction(funcNode);
|
|
405
|
+
if (!hasSessionCleanup) {
|
|
406
|
+
return this.createViolation(
|
|
407
|
+
sourceFile,
|
|
408
|
+
funcNode,
|
|
409
|
+
`Session token invalidation vulnerability: Logout handler should clear session data and invalidate tokens`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return null;
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (this.verbose) {
|
|
416
|
+
console.log(
|
|
417
|
+
`🔍 [${this.ruleId}] Symbol: Error analyzing logout handler:`,
|
|
418
|
+
error.message
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
isSessionRelatedMethod(expression) {
|
|
426
|
+
try {
|
|
427
|
+
const expressionText = expression.getText();
|
|
428
|
+
|
|
429
|
+
// Don't detect session cleanup methods as violations
|
|
430
|
+
const sessionCleanupMethods = [
|
|
431
|
+
'req.session.destroy',
|
|
432
|
+
'session.destroy',
|
|
433
|
+
'res.clearCookie',
|
|
434
|
+
'sessionStorage.clear',
|
|
435
|
+
'localStorage.clear',
|
|
436
|
+
'req.session.reload',
|
|
437
|
+
'req.session.regenerate'
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
if (sessionCleanupMethods.some(method => expressionText.includes(method))) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return this.sessionStorageMethods.some(method =>
|
|
445
|
+
expressionText.includes(method)
|
|
446
|
+
) || this.expressSessionMethods.some(method =>
|
|
447
|
+
expressionText.includes(method)
|
|
448
|
+
);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
isLogoutRelatedMethod(methodName) {
|
|
455
|
+
return this.sessionMethods.includes(methodName) ||
|
|
456
|
+
this.tokenMethods.includes(methodName);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
isLogoutHandler(functionName, functionText) {
|
|
460
|
+
const logoutPatterns = [
|
|
461
|
+
/logout/i,
|
|
462
|
+
/signout/i,
|
|
463
|
+
/sign_out/i,
|
|
464
|
+
/log_out/i,
|
|
465
|
+
/destroy.*session/i,
|
|
466
|
+
/clear.*session/i,
|
|
467
|
+
/end.*session/i,
|
|
468
|
+
/endSession/i
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
return logoutPatterns.some(pattern =>
|
|
472
|
+
pattern.test(functionName) || pattern.test(functionText)
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
isInLogoutContext(node, sourceFile) {
|
|
477
|
+
try {
|
|
478
|
+
// Get the parent function to check context
|
|
479
|
+
let parent = node.getParent();
|
|
480
|
+
while (parent && !this.isFunctionLike(parent)) {
|
|
481
|
+
parent = parent.getParent();
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!parent) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const functionText = parent.getText();
|
|
489
|
+
const functionName = this.getFunctionName(parent);
|
|
490
|
+
|
|
491
|
+
// Check if function name or content indicates logout
|
|
492
|
+
const logoutPatterns = [
|
|
493
|
+
/logout/i,
|
|
494
|
+
/signout/i,
|
|
495
|
+
/sign_out/i,
|
|
496
|
+
/log_out/i,
|
|
497
|
+
/destroy.*session/i,
|
|
498
|
+
/clear.*session/i
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
// Check function name
|
|
502
|
+
if (functionName && logoutPatterns.some(pattern => pattern.test(functionName))) {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Check function content for logout patterns
|
|
507
|
+
return logoutPatterns.some(pattern => pattern.test(functionText));
|
|
508
|
+
} catch (error) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
getFunctionName(node) {
|
|
514
|
+
try {
|
|
515
|
+
if (node.getName) {
|
|
516
|
+
return node.getName();
|
|
517
|
+
}
|
|
518
|
+
return "";
|
|
519
|
+
} catch (error) {
|
|
520
|
+
return "";
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
hasSessionCleanupInContext(node, sourceFile) {
|
|
525
|
+
try {
|
|
526
|
+
// Get the parent function to check for session cleanup
|
|
527
|
+
let parent = node.getParent();
|
|
528
|
+
while (parent && !this.isFunctionLike(parent)) {
|
|
529
|
+
parent = parent.getParent();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (!parent) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const functionText = parent.getText();
|
|
537
|
+
|
|
538
|
+
// Check for session cleanup patterns
|
|
539
|
+
const cleanupPatterns = [
|
|
540
|
+
/req\.session\.destroy/,
|
|
541
|
+
/req\.session\s*=\s*null/,
|
|
542
|
+
/req\.session\s*=\s*\{\}/,
|
|
543
|
+
/session\.destroy/,
|
|
544
|
+
/session\.clear/,
|
|
545
|
+
/session\.remove/,
|
|
546
|
+
/\.removeItem/,
|
|
547
|
+
/\.clear\(\)/,
|
|
548
|
+
/\.delete\(/,
|
|
549
|
+
/res\.clearCookie/,
|
|
550
|
+
/sessionStorage\.clear/,
|
|
551
|
+
/localStorage\.clear/,
|
|
552
|
+
/req\.session\.regenerate/
|
|
553
|
+
];
|
|
554
|
+
|
|
555
|
+
return cleanupPatterns.some(pattern => pattern.test(functionText));
|
|
556
|
+
} catch (error) {
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
hasSessionCleanupInFunction(funcNode) {
|
|
562
|
+
try {
|
|
563
|
+
const functionText = funcNode.getText();
|
|
564
|
+
|
|
565
|
+
// Check for session cleanup patterns
|
|
566
|
+
const cleanupPatterns = [
|
|
567
|
+
/req\.session\.destroy/,
|
|
568
|
+
/req\.session\s*=\s*null/,
|
|
569
|
+
/req\.session\s*=\s*\{\}/,
|
|
570
|
+
/session\.destroy/,
|
|
571
|
+
/session\.clear/,
|
|
572
|
+
/session\.remove/,
|
|
573
|
+
/\.removeItem/,
|
|
574
|
+
/\.clear\(\)/,
|
|
575
|
+
/\.delete\(/
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
return cleanupPatterns.some(pattern => pattern.test(functionText));
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
hasTokenInvalidationInContext(node, sourceFile) {
|
|
585
|
+
try {
|
|
586
|
+
// Get the parent function to check for token invalidation
|
|
587
|
+
let parent = node.getParent();
|
|
588
|
+
while (parent && !this.isFunctionLike(parent)) {
|
|
589
|
+
parent = parent.getParent();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!parent) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const functionText = parent.getText();
|
|
597
|
+
|
|
598
|
+
// Check for token invalidation patterns
|
|
599
|
+
const invalidationPatterns = [
|
|
600
|
+
/blacklist/,
|
|
601
|
+
/revoke/,
|
|
602
|
+
/invalidate/,
|
|
603
|
+
/expire/,
|
|
604
|
+
/remove.*token/,
|
|
605
|
+
/delete.*token/,
|
|
606
|
+
/clear.*token/,
|
|
607
|
+
/destroy.*token/
|
|
608
|
+
];
|
|
609
|
+
|
|
610
|
+
return invalidationPatterns.some(pattern => pattern.test(functionText));
|
|
611
|
+
} catch (error) {
|
|
612
|
+
return false;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
isFunctionLike(node) {
|
|
617
|
+
try {
|
|
618
|
+
const SyntaxKind = require("typescript").SyntaxKind;
|
|
619
|
+
const kind = node.getKind();
|
|
620
|
+
|
|
621
|
+
return kind === SyntaxKind.FunctionDeclaration ||
|
|
622
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
623
|
+
kind === SyntaxKind.ArrowFunction ||
|
|
624
|
+
kind === SyntaxKind.MethodDeclaration;
|
|
625
|
+
} catch (error) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
getMethodName(expression) {
|
|
631
|
+
try {
|
|
632
|
+
const ts = require("typescript");
|
|
633
|
+
|
|
634
|
+
if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
|
|
635
|
+
return expression.getNameNode().getText();
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (expression.getKind() === ts.SyntaxKind.Identifier) {
|
|
639
|
+
return expression.getText();
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
return "";
|
|
643
|
+
} catch (error) {
|
|
644
|
+
return "";
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
createViolation(sourceFile, node, message) {
|
|
649
|
+
try {
|
|
650
|
+
const start = node.getStart();
|
|
651
|
+
const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
rule: this.ruleId,
|
|
655
|
+
source: sourceFile.getFilePath(),
|
|
656
|
+
category: this.category,
|
|
657
|
+
line: lineAndChar.line,
|
|
658
|
+
column: lineAndChar.column,
|
|
659
|
+
message: message,
|
|
660
|
+
severity: "error",
|
|
661
|
+
};
|
|
662
|
+
} catch (error) {
|
|
663
|
+
if (this.verbose) {
|
|
664
|
+
console.log(
|
|
665
|
+
`🔍 [${this.ruleId}] Symbol: Error creating violation:`,
|
|
666
|
+
error.message
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
module.exports = S041SymbolBasedAnalyzer;
|