@sun-asterisk/sunlint 1.3.26 → 1.3.28
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 +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S043
|
|
3
|
+
* Require re-authentication for long-lived sessions or sensitive actions
|
|
4
|
+
* Objective: Reduce the risk of session hijacking or privilege misuse by forcing
|
|
5
|
+
* Enforce correct access control after sensitive updates.
|
|
6
|
+
* 1. Detects password change functions by matching common naming patterns like changePassword, updatePassword, resetPassword, etc.
|
|
7
|
+
* 2. Checks for session invalidation through multiple methods:
|
|
8
|
+
* - Direct session invalidation function calls
|
|
9
|
+
* - Database/cache clear operations
|
|
10
|
+
* - JWT token versioning or timestamp updates
|
|
11
|
+
* - Function calls that might invalidate sessions
|
|
12
|
+
* 3. Reports violations when password change functions don't properly invalidate sessions, with detailed guidance on how to fix the issue.
|
|
13
|
+
* 4. Breaks down logic into smaller, focused functions:
|
|
14
|
+
* - checkFunctionsAndMethods() - checks regular functions and class methods
|
|
15
|
+
* - checkArrowFunctionsAndExpressions() - checks arrow functions and function expressions
|
|
16
|
+
* - isPasswordChangeFunction() - identifies password change functions
|
|
17
|
+
* - checkForSessionInvalidation() - main validation logic
|
|
18
|
+
* - hasSessionInvalidationCall() - checks for direct invalidation calls
|
|
19
|
+
* - hasSessionClearOperation() - checks for DB/cache operations
|
|
20
|
+
* - hasTokenVersioningOrTimestamp() - checks for JWT versioning
|
|
21
|
+
* - checkFunctionCallsForSessionInvalidation() - examines nested function calls
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { SyntaxKind } = require('ts-morph');
|
|
25
|
+
|
|
26
|
+
class S043SymbolBasedAnalyzer {
|
|
27
|
+
constructor(semanticEngine = null) {
|
|
28
|
+
this.ruleId = "S043";
|
|
29
|
+
this.ruleName = 'Password changes must invalidate all other login sessions';
|
|
30
|
+
this.semanticEngine = semanticEngine;
|
|
31
|
+
this.verbose = false;
|
|
32
|
+
this.skipPatterns = [
|
|
33
|
+
/\/node_modules\//,
|
|
34
|
+
/\/tests?\//,
|
|
35
|
+
/\/dist\//,
|
|
36
|
+
/\/build\//,
|
|
37
|
+
/\.spec\.ts$/,
|
|
38
|
+
/\.test\.ts$/
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Password change function patterns
|
|
42
|
+
this.passwordChangePatterns = [
|
|
43
|
+
'changePassword',
|
|
44
|
+
'updatePassword',
|
|
45
|
+
'resetPassword',
|
|
46
|
+
'setPassword',
|
|
47
|
+
'modifyPassword',
|
|
48
|
+
'passwordUpdate',
|
|
49
|
+
'changeUserPassword',
|
|
50
|
+
'updateUserPassword'
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// Session invalidation indicators
|
|
54
|
+
this.sessionInvalidationIndicators = [
|
|
55
|
+
'invalidateSession',
|
|
56
|
+
'clearSession',
|
|
57
|
+
'destroySession',
|
|
58
|
+
'deleteSession',
|
|
59
|
+
'removeSession',
|
|
60
|
+
'clearAllSessions',
|
|
61
|
+
'invalidateAllTokens',
|
|
62
|
+
'revokeAllTokens',
|
|
63
|
+
'deleteAllSessions',
|
|
64
|
+
'destroyAllSessions',
|
|
65
|
+
'clearUserSessions',
|
|
66
|
+
'deleteAllByUserId',
|
|
67
|
+
'logout',
|
|
68
|
+
'signOut',
|
|
69
|
+
'logoutAll',
|
|
70
|
+
'logoutAllDevices',
|
|
71
|
+
'signOutAll',
|
|
72
|
+
'revokeToken',
|
|
73
|
+
'revokeRefreshToken',
|
|
74
|
+
'invalidateToken',
|
|
75
|
+
'deleteToken',
|
|
76
|
+
'globalSignOut',
|
|
77
|
+
'adminUserGlobalSignOut',
|
|
78
|
+
'clearCache',
|
|
79
|
+
'deleteCache',
|
|
80
|
+
'removeCache'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
this.maxDepth = 4;
|
|
84
|
+
this.visited = new Set();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async initialize(semanticEngine = null) {
|
|
88
|
+
if (semanticEngine) {
|
|
89
|
+
this.semanticEngine = semanticEngine;
|
|
90
|
+
}
|
|
91
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
95
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
analyzeFileWithSymbols(filePath, options = {}) {
|
|
99
|
+
const violations = [];
|
|
100
|
+
|
|
101
|
+
if (!this.semanticEngine?.project) {
|
|
102
|
+
return violations;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
106
|
+
return violations;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
111
|
+
if (!sourceFile) {
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Check functions and methods
|
|
116
|
+
const functions = [
|
|
117
|
+
...sourceFile.getFunctions(),
|
|
118
|
+
...sourceFile.getClasses().flatMap(cls => cls.getMethods())
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
for (const func of functions) {
|
|
122
|
+
const funcName = func.getName();
|
|
123
|
+
if (funcName && this.isPasswordChangeFunction(funcName)) {
|
|
124
|
+
this.visited.clear();
|
|
125
|
+
if (!this.hasSessionInvalidation(func, 0)) {
|
|
126
|
+
this.addViolation(sourceFile, func, violations, funcName);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check arrow functions
|
|
132
|
+
sourceFile.forEachDescendant((node) => {
|
|
133
|
+
if (node.getKind() === SyntaxKind.ArrowFunction ||
|
|
134
|
+
node.getKind() === SyntaxKind.FunctionExpression) {
|
|
135
|
+
const parent = node.getParent();
|
|
136
|
+
if (parent?.getKind() === SyntaxKind.VariableDeclaration) {
|
|
137
|
+
const funcName = parent.getName();
|
|
138
|
+
if (funcName && this.isPasswordChangeFunction(funcName)) {
|
|
139
|
+
this.visited.clear();
|
|
140
|
+
if (!this.hasSessionInvalidation(node, 0)) {
|
|
141
|
+
this.addViolation(sourceFile, node, violations, funcName);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return violations;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return violations;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
isPasswordChangeFunction(name) {
|
|
155
|
+
const lowerName = name.toLowerCase();
|
|
156
|
+
return this.passwordChangePatterns.some(pattern =>
|
|
157
|
+
lowerName === pattern.toLowerCase()
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
hasSessionInvalidation(node, depth) {
|
|
162
|
+
if (depth > this.maxDepth) return false;
|
|
163
|
+
|
|
164
|
+
const nodeId = this.getNodeId(node);
|
|
165
|
+
if (this.visited.has(nodeId)) return false;
|
|
166
|
+
this.visited.add(nodeId);
|
|
167
|
+
|
|
168
|
+
const text = node.getText();
|
|
169
|
+
|
|
170
|
+
// Check 1: Direct text patterns
|
|
171
|
+
if (this.checkTextForInvalidation(text)) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Check 2: Token versioning
|
|
176
|
+
if (this.hasTokenVersioning(text)) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check 3: Logout + cache clear pattern
|
|
181
|
+
if (this.hasLogoutAndCacheClear(text)) {
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check 4: Cognito operations
|
|
186
|
+
if (this.hasCognitoInvalidation(text)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check 5: Deep scan function calls
|
|
191
|
+
const calls = this.extractFunctionCalls(node);
|
|
192
|
+
for (const call of calls) {
|
|
193
|
+
// Check if call name indicates session invalidation
|
|
194
|
+
if (this.isInvalidationCall(call.name)) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Resolve and trace the called function
|
|
199
|
+
const calledNode = this.resolveAndFindFunction(call, node);
|
|
200
|
+
if (calledNode && this.hasSessionInvalidation(calledNode, depth + 1)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
checkTextForInvalidation(text) {
|
|
209
|
+
const lowerText = text.toLowerCase();
|
|
210
|
+
|
|
211
|
+
for (const indicator of this.sessionInvalidationIndicators) {
|
|
212
|
+
if (lowerText.includes(indicator.toLowerCase())) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if ((lowerText.includes('delete') || lowerText.includes('remove') ||
|
|
218
|
+
lowerText.includes('destroy') || lowerText.includes('clear')) &&
|
|
219
|
+
(lowerText.includes('session') || lowerText.includes('token'))) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if ((lowerText.includes('redis') || lowerText.includes('cache')) &&
|
|
224
|
+
(lowerText.includes('.del(') || lowerText.includes('.delete(') ||
|
|
225
|
+
lowerText.includes('.clear(') || lowerText.includes('.remove('))) {
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
hasTokenVersioning(text) {
|
|
233
|
+
const hasVersionField = /tokenVersion|token_version|jwtVersion|jwt_version|sessionVersion|passwordChangedAt|lastPasswordChange/i.test(text);
|
|
234
|
+
const hasIncrement = /\+\+|increment|\+ 1|\+= 1/i.test(text);
|
|
235
|
+
return hasVersionField && hasIncrement;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
hasLogoutAndCacheClear(text) {
|
|
239
|
+
const lowerText = text.toLowerCase();
|
|
240
|
+
const hasLogout = /logout|signout/i.test(text);
|
|
241
|
+
const hasCacheDelete = /cache.*\.del\(|cachemanager\.del\(|cache.*\.delete\(|cache.*\.remove\(/i.test(text);
|
|
242
|
+
return hasLogout && hasCacheDelete;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
hasCognitoInvalidation(text) {
|
|
246
|
+
const lowerText = text.toLowerCase();
|
|
247
|
+
|
|
248
|
+
if (lowerText.includes('cognito') && lowerText.includes('changepassword')) {
|
|
249
|
+
if (lowerText.includes('logout') || lowerText.includes('del(') ||
|
|
250
|
+
lowerText.includes('globalsignout')) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (/globalsignout|adminuserglobalsignout/i.test(text)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
isInvalidationCall(callName) {
|
|
263
|
+
const lowerName = callName.toLowerCase();
|
|
264
|
+
return this.sessionInvalidationIndicators.some(indicator => {
|
|
265
|
+
const lowerIndicator = indicator.toLowerCase();
|
|
266
|
+
return lowerName === lowerIndicator || lowerName.includes(lowerIndicator);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
extractFunctionCalls(node) {
|
|
271
|
+
const calls = [];
|
|
272
|
+
const seen = new Set();
|
|
273
|
+
|
|
274
|
+
node.forEachDescendant((child) => {
|
|
275
|
+
if (child.getKind() === SyntaxKind.CallExpression) {
|
|
276
|
+
const expr = child.getExpression();
|
|
277
|
+
|
|
278
|
+
if (expr.getKind() === SyntaxKind.Identifier) {
|
|
279
|
+
const name = expr.getText();
|
|
280
|
+
if (!seen.has(name)) {
|
|
281
|
+
calls.push({
|
|
282
|
+
name,
|
|
283
|
+
type: 'function',
|
|
284
|
+
node: child,
|
|
285
|
+
expression: expr
|
|
286
|
+
});
|
|
287
|
+
seen.add(name);
|
|
288
|
+
}
|
|
289
|
+
} else if (expr.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
290
|
+
const propAccess = expr;
|
|
291
|
+
const methodName = propAccess.getName();
|
|
292
|
+
const objectExpr = propAccess.getExpression();
|
|
293
|
+
const objectName = objectExpr.getText();
|
|
294
|
+
const fullName = expr.getText();
|
|
295
|
+
|
|
296
|
+
if (!seen.has(fullName)) {
|
|
297
|
+
calls.push({
|
|
298
|
+
name: methodName,
|
|
299
|
+
object: objectName,
|
|
300
|
+
fullName,
|
|
301
|
+
type: 'method',
|
|
302
|
+
node: child,
|
|
303
|
+
expression: expr,
|
|
304
|
+
objectExpression: objectExpr
|
|
305
|
+
});
|
|
306
|
+
seen.add(fullName);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
return calls;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Resolve the actual function/method being called
|
|
317
|
+
* Handles: this.service.method(), service.method(), functionName()
|
|
318
|
+
*/
|
|
319
|
+
resolveAndFindFunction(call, contextNode) {
|
|
320
|
+
if (!this.semanticEngine?.project) return null;
|
|
321
|
+
|
|
322
|
+
// Case 1: Direct function call - functionName()
|
|
323
|
+
if (call.type === 'function') {
|
|
324
|
+
return this.findFunctionByName(call.name);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Case 2: Method call - object.method()
|
|
328
|
+
if (call.type === 'method') {
|
|
329
|
+
// Try to resolve the object (service/class instance)
|
|
330
|
+
const serviceClass = this.resolveServiceClass(call.object, contextNode);
|
|
331
|
+
|
|
332
|
+
if (serviceClass) {
|
|
333
|
+
// Find the method in the service class
|
|
334
|
+
const method = serviceClass.getMethod(call.name);
|
|
335
|
+
if (method) {
|
|
336
|
+
return method;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Fallback: Search for method by name across all classes
|
|
341
|
+
return this.findMethodByName(call.name);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Resolve the service class from object name
|
|
349
|
+
* Example: this.commonChangePasswordService -> CommonChangePasswordService class
|
|
350
|
+
*/
|
|
351
|
+
resolveServiceClass(objectName, contextNode) {
|
|
352
|
+
if (!objectName) return null;
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
// Remove 'this.' prefix if exists
|
|
356
|
+
const cleanObjectName = objectName.replace(/^this\./, '');
|
|
357
|
+
|
|
358
|
+
// Get the containing class
|
|
359
|
+
let currentNode = contextNode;
|
|
360
|
+
while (currentNode && currentNode.getKind() !== SyntaxKind.ClassDeclaration) {
|
|
361
|
+
currentNode = currentNode.getParent();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!currentNode) return null;
|
|
365
|
+
|
|
366
|
+
const containingClass = currentNode;
|
|
367
|
+
|
|
368
|
+
// Find the property declaration (injected service)
|
|
369
|
+
const property = containingClass.getProperty(cleanObjectName);
|
|
370
|
+
if (property) {
|
|
371
|
+
const propertyType = property.getType();
|
|
372
|
+
const typeSymbol = propertyType.getSymbol();
|
|
373
|
+
|
|
374
|
+
if (typeSymbol) {
|
|
375
|
+
const declarations = typeSymbol.getDeclarations();
|
|
376
|
+
if (declarations && declarations.length > 0) {
|
|
377
|
+
const decl = declarations[0];
|
|
378
|
+
if (decl.getKind() === SyntaxKind.ClassDeclaration) {
|
|
379
|
+
return decl;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Fallback: Try to find by constructor parameter
|
|
386
|
+
const constructor = containingClass.getConstructors()[0];
|
|
387
|
+
if (constructor) {
|
|
388
|
+
const param = constructor.getParameter(cleanObjectName);
|
|
389
|
+
if (param) {
|
|
390
|
+
const paramType = param.getType();
|
|
391
|
+
const typeSymbol = paramType.getSymbol();
|
|
392
|
+
|
|
393
|
+
if (typeSymbol) {
|
|
394
|
+
const declarations = typeSymbol.getDeclarations();
|
|
395
|
+
if (declarations && declarations.length > 0) {
|
|
396
|
+
const decl = declarations[0];
|
|
397
|
+
if (decl.getKind() === SyntaxKind.ClassDeclaration) {
|
|
398
|
+
return decl;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Fallback: Search by service name pattern
|
|
406
|
+
return this.findClassByServiceName(cleanObjectName);
|
|
407
|
+
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Find class by service variable name
|
|
415
|
+
* Example: commonChangePasswordService -> CommonChangePasswordService
|
|
416
|
+
*/
|
|
417
|
+
findClassByServiceName(serviceName) {
|
|
418
|
+
if (!this.semanticEngine?.project) return null;
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
// Convert camelCase service name to PascalCase class name
|
|
422
|
+
const className = serviceName.charAt(0).toUpperCase() + serviceName.slice(1);
|
|
423
|
+
|
|
424
|
+
// Also try common patterns
|
|
425
|
+
const possibleClassNames = [
|
|
426
|
+
className,
|
|
427
|
+
className.replace('Service', '') + 'Service',
|
|
428
|
+
serviceName.split(/(?=[A-Z])/).map(part =>
|
|
429
|
+
part.charAt(0).toUpperCase() + part.slice(1)
|
|
430
|
+
).join('')
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
const sourceFiles = this.semanticEngine.project.getSourceFiles();
|
|
434
|
+
|
|
435
|
+
for (const file of sourceFiles) {
|
|
436
|
+
if (this.shouldIgnoreFile(file.getFilePath())) continue;
|
|
437
|
+
|
|
438
|
+
for (const cls of file.getClasses()) {
|
|
439
|
+
const clsName = cls.getName();
|
|
440
|
+
if (clsName && possibleClassNames.some(name =>
|
|
441
|
+
name.toLowerCase() === clsName.toLowerCase()
|
|
442
|
+
)) {
|
|
443
|
+
return cls;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Find function by name across all files
|
|
456
|
+
*/
|
|
457
|
+
findFunctionByName(functionName) {
|
|
458
|
+
if (!this.semanticEngine?.project || !functionName) return null;
|
|
459
|
+
|
|
460
|
+
try {
|
|
461
|
+
const sourceFiles = this.semanticEngine.project.getSourceFiles();
|
|
462
|
+
|
|
463
|
+
for (const file of sourceFiles) {
|
|
464
|
+
if (this.shouldIgnoreFile(file.getFilePath())) continue;
|
|
465
|
+
|
|
466
|
+
const func = file.getFunction(functionName);
|
|
467
|
+
if (func) return func;
|
|
468
|
+
|
|
469
|
+
const varDecl = file.getVariableDeclaration(functionName);
|
|
470
|
+
if (varDecl) {
|
|
471
|
+
const init = varDecl.getInitializer();
|
|
472
|
+
if (init && (init.getKind() === SyntaxKind.ArrowFunction ||
|
|
473
|
+
init.getKind() === SyntaxKind.FunctionExpression)) {
|
|
474
|
+
return init;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} catch (error) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Find method by name across all classes
|
|
487
|
+
*/
|
|
488
|
+
findMethodByName(methodName) {
|
|
489
|
+
if (!this.semanticEngine?.project || !methodName) return null;
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const sourceFiles = this.semanticEngine.project.getSourceFiles();
|
|
493
|
+
|
|
494
|
+
for (const file of sourceFiles) {
|
|
495
|
+
if (this.shouldIgnoreFile(file.getFilePath())) continue;
|
|
496
|
+
|
|
497
|
+
for (const cls of file.getClasses()) {
|
|
498
|
+
const method = cls.getMethod(methodName);
|
|
499
|
+
if (method) return method;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
getNodeId(node) {
|
|
510
|
+
try {
|
|
511
|
+
const sourceFile = node.getSourceFile();
|
|
512
|
+
const start = node.getStart();
|
|
513
|
+
return `${sourceFile.getFilePath()}:${start}`;
|
|
514
|
+
} catch {
|
|
515
|
+
return Math.random().toString();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
addViolation(sourceFile, node, violations, functionName) {
|
|
520
|
+
const startLine = node.getStartLineNumber();
|
|
521
|
+
const column = node.getStart() - node.getStartLinePos() + 1;
|
|
522
|
+
|
|
523
|
+
violations.push({
|
|
524
|
+
ruleId: this.ruleId,
|
|
525
|
+
ruleName: this.ruleName,
|
|
526
|
+
severity: 'medium',
|
|
527
|
+
message: `Password change function "${functionName}" must invalidate all active sessions`,
|
|
528
|
+
line: startLine,
|
|
529
|
+
column: column,
|
|
530
|
+
filePath: sourceFile.getFilePath(),
|
|
531
|
+
type: 'SESSION_INVALIDATION_MISSING',
|
|
532
|
+
details: `After password change, you must invalidate sessions. Examples: invalidateAllSessions(userId), logout(user), sessionRepository.delete({userId}), user.tokenVersion++, redis.del('session:*'), cacheManager.del(key)`
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
shouldIgnoreFile(filePath) {
|
|
537
|
+
return this.skipPatterns.some(pattern => pattern.test(filePath));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
module.exports = S043SymbolBasedAnalyzer;
|