@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,884 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S003 - Open Redirect Protection (Symbol-based Analyzer)
|
|
3
|
+
*
|
|
4
|
+
* Detects unvalidated URL redirects from user input that can lead to
|
|
5
|
+
* phishing attacks and malware distribution.
|
|
6
|
+
*
|
|
7
|
+
* Based on:
|
|
8
|
+
* - OWASP A03:2021 - Injection
|
|
9
|
+
* - CWE-601: URL Redirection to Untrusted Site ('Open Redirect')
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { SyntaxKind } = require("ts-morph");
|
|
13
|
+
|
|
14
|
+
class S003SymbolBasedAnalyzer {
|
|
15
|
+
constructor(semanticEngine = null) {
|
|
16
|
+
this.ruleId = "S003";
|
|
17
|
+
this.semanticEngine = semanticEngine;
|
|
18
|
+
|
|
19
|
+
// Redirect function patterns (server-side)
|
|
20
|
+
this.redirectFunctions = [
|
|
21
|
+
"redirect", // Express: res.redirect(), NestJS: @Redirect()
|
|
22
|
+
"sendredirect", // Java/Spring: response.sendRedirect()
|
|
23
|
+
"redirectview", // Spring: new RedirectView()
|
|
24
|
+
"setheader", // Generic: res.setHeader('Location', ...)
|
|
25
|
+
"@redirect", // NestJS decorator: @Redirect(url)
|
|
26
|
+
"permanentredirect", // Next.js: permanentRedirect()
|
|
27
|
+
"navigateto", // Nuxt.js: navigateTo()
|
|
28
|
+
"sendredirect", // Nuxt.js: sendRedirect()
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// Client-side redirect patterns
|
|
32
|
+
this.clientRedirectPatterns = [
|
|
33
|
+
"window.location",
|
|
34
|
+
"location.href",
|
|
35
|
+
"location.replace",
|
|
36
|
+
"location.assign",
|
|
37
|
+
"router.push", // Next.js/Nuxt.js router
|
|
38
|
+
"router.replace", // Next.js/Nuxt.js router
|
|
39
|
+
"navigateto", // Nuxt.js composable
|
|
40
|
+
"userouter", // Next.js/Nuxt.js hook
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// User input source patterns
|
|
44
|
+
this.userInputSources = [
|
|
45
|
+
"req.query",
|
|
46
|
+
"req.params",
|
|
47
|
+
"req.body",
|
|
48
|
+
"request.getparameter",
|
|
49
|
+
"request.getquerystring",
|
|
50
|
+
"urlsearchparams",
|
|
51
|
+
"searchparams.get",
|
|
52
|
+
"params.get",
|
|
53
|
+
"query.get",
|
|
54
|
+
"@requestparam", // Spring annotation
|
|
55
|
+
"@queryparam", // JAX-RS annotation
|
|
56
|
+
"@query", // NestJS decorator: @Query()
|
|
57
|
+
"@param", // NestJS decorator: @Param()
|
|
58
|
+
"@body", // NestJS decorator: @Body()
|
|
59
|
+
"usesearchparams", // Next.js hook
|
|
60
|
+
"useparams", // Next.js/React Router hook
|
|
61
|
+
"usequeryparams", // Next.js query params
|
|
62
|
+
"useroute", // Nuxt.js composable
|
|
63
|
+
"event.query", // Nuxt.js h3 event
|
|
64
|
+
"event.context.params", // Nuxt.js context
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// Validation/safe patterns that indicate proper validation
|
|
68
|
+
this.validationPatterns = [
|
|
69
|
+
"allowed", // ALLOWED_URLS, allowedDomains
|
|
70
|
+
"whitelist", // WHITELIST, whitelistedUrls
|
|
71
|
+
"allowlist", // allowList
|
|
72
|
+
"safe", // SAFE_URLS, safeRedirects
|
|
73
|
+
"includes", // array.includes(url)
|
|
74
|
+
"new url", // URL parsing for validation
|
|
75
|
+
"startswith('/')", // Relative URL check
|
|
76
|
+
"startswith(\"/\")", // Relative URL check (double quotes)
|
|
77
|
+
"isvalid", // isValidUrl(), isValidDomain()
|
|
78
|
+
"validate", // validateUrl(), validateRedirect()
|
|
79
|
+
"check", // checkUrl(), checkDomain()
|
|
80
|
+
"istrust", // isTrustedUrl()
|
|
81
|
+
"trusted", // TRUSTED_URLS
|
|
82
|
+
"verify", // verifyUrl()
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Framework-specific safe patterns
|
|
86
|
+
this.safeFrameworkPatterns = [
|
|
87
|
+
"redirectmap", // Indirect mapping
|
|
88
|
+
"redirect_map",
|
|
89
|
+
"urlmap",
|
|
90
|
+
"destinationmap",
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// Safe URL construction patterns (not user-controllable)
|
|
94
|
+
this.safeUrlPatterns = [
|
|
95
|
+
"process.env.", // Environment variables
|
|
96
|
+
"env.", // Short env reference
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
// Hardcoded path patterns (relative URLs are generally safe)
|
|
100
|
+
this.hardcodedPathPatterns = [
|
|
101
|
+
"'/", // Single quote relative path
|
|
102
|
+
'"//', // Double quote relative path (but not protocol)
|
|
103
|
+
"`/", // Template literal relative path
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async initialize(semanticEngine) {
|
|
108
|
+
this.semanticEngine = semanticEngine;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async analyze(sourceFile, filePath) {
|
|
112
|
+
const violations = [];
|
|
113
|
+
const reportedLines = new Set();
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
// Build dataflow map: variable name -> user input source
|
|
117
|
+
const taintedVariables = this.buildTaintedVariablesMap(sourceFile);
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
// Check server-side redirect functions
|
|
121
|
+
this.checkServerRedirects(
|
|
122
|
+
sourceFile,
|
|
123
|
+
filePath,
|
|
124
|
+
violations,
|
|
125
|
+
reportedLines,
|
|
126
|
+
taintedVariables
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Check client-side redirects
|
|
130
|
+
this.checkClientRedirects(
|
|
131
|
+
sourceFile,
|
|
132
|
+
filePath,
|
|
133
|
+
violations,
|
|
134
|
+
reportedLines,
|
|
135
|
+
taintedVariables
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Check Location header assignments
|
|
139
|
+
this.checkLocationHeaders(
|
|
140
|
+
sourceFile,
|
|
141
|
+
filePath,
|
|
142
|
+
violations,
|
|
143
|
+
reportedLines,
|
|
144
|
+
taintedVariables
|
|
145
|
+
);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.warn(`⚠ [S003] Analysis error in ${filePath}: ${error.message}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return violations;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build a map of variables that are tainted with user input
|
|
155
|
+
* Returns: Map<variableName, userInputSource>
|
|
156
|
+
*/
|
|
157
|
+
buildTaintedVariablesMap(sourceFile) {
|
|
158
|
+
const taintedVars = new Map();
|
|
159
|
+
|
|
160
|
+
// Find all variable declarations
|
|
161
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(
|
|
162
|
+
SyntaxKind.VariableDeclaration
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
for (const varDecl of variableDeclarations) {
|
|
166
|
+
const varName = varDecl.getName();
|
|
167
|
+
const initializer = varDecl.getInitializer();
|
|
168
|
+
|
|
169
|
+
if (!initializer) continue;
|
|
170
|
+
|
|
171
|
+
const initText = initializer.getText().toLowerCase();
|
|
172
|
+
const initOriginal = initializer.getText();
|
|
173
|
+
|
|
174
|
+
// Check if initializer is from user input (including chained calls and template literals)
|
|
175
|
+
const isUserInput = this.isUserInput(initText) ||
|
|
176
|
+
this.isUserInputCall(initText) ||
|
|
177
|
+
this.isChainedUserInputCall(initText) ||
|
|
178
|
+
this.containsUserInputInTemplate(initText);
|
|
179
|
+
|
|
180
|
+
// But exclude if it's a safe URL construction
|
|
181
|
+
if (isUserInput && !this.isSafeUrlConstruction(initOriginal)) {
|
|
182
|
+
taintedVars.set(varName.toLowerCase(), initText);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Also track binary expressions (assignments)
|
|
187
|
+
const binaryExprs = sourceFile.getDescendantsOfKind(
|
|
188
|
+
SyntaxKind.BinaryExpression
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
for (const binaryExpr of binaryExprs) {
|
|
192
|
+
const operator = binaryExpr.getOperatorToken().getText();
|
|
193
|
+
if (operator !== "=") continue;
|
|
194
|
+
|
|
195
|
+
const left = binaryExpr.getLeft();
|
|
196
|
+
const right = binaryExpr.getRight();
|
|
197
|
+
|
|
198
|
+
const leftText = left.getText();
|
|
199
|
+
const rightText = right.getText().toLowerCase();
|
|
200
|
+
const rightOriginal = right.getText();
|
|
201
|
+
|
|
202
|
+
// Check if right side is user input or tainted variable
|
|
203
|
+
const isUserInput = this.isUserInput(rightText) ||
|
|
204
|
+
this.isUserInputCall(rightText) ||
|
|
205
|
+
this.isChainedUserInputCall(rightText) ||
|
|
206
|
+
this.containsUserInputInTemplate(rightText);
|
|
207
|
+
|
|
208
|
+
// But exclude if it's a safe URL construction
|
|
209
|
+
if (isUserInput && !this.isSafeUrlConstruction(rightOriginal)) {
|
|
210
|
+
taintedVars.set(leftText.toLowerCase(), rightText);
|
|
211
|
+
} else if (taintedVars.has(rightText)) {
|
|
212
|
+
// Propagate taint
|
|
213
|
+
taintedVars.set(leftText.toLowerCase(), taintedVars.get(rightText));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return taintedVars;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if text contains chained user input calls
|
|
222
|
+
* e.g., new URLSearchParams().get(), params.get()
|
|
223
|
+
*/
|
|
224
|
+
isChainedUserInputCall(text) {
|
|
225
|
+
const chainedPatterns = [
|
|
226
|
+
"urlsearchparams",
|
|
227
|
+
"searchparams",
|
|
228
|
+
"params.get",
|
|
229
|
+
"query.get",
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
return chainedPatterns.some(pattern => text.includes(pattern));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if template literal contains user input variable
|
|
237
|
+
* Now checks if variables inside template are from known user input sources
|
|
238
|
+
*/
|
|
239
|
+
containsUserInputInTemplate(text, taintedVariables = new Map()) {
|
|
240
|
+
// Check if it's a template literal with ${...}
|
|
241
|
+
if (!text.includes('${') || !text.includes('}')) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Extract variable names from template literals
|
|
246
|
+
const varMatches = text.match(/\$\{([^}]+)\}/g);
|
|
247
|
+
if (!varMatches) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Check if any variable looks like user input
|
|
252
|
+
for (const varMatch of varMatches) {
|
|
253
|
+
const varContent = varMatch.slice(2, -1).toLowerCase(); // Remove ${ and }
|
|
254
|
+
const varName = varContent.split('.')[0].trim(); // Get variable name without properties
|
|
255
|
+
|
|
256
|
+
// Check if it's a known user input source
|
|
257
|
+
if (this.isUserInput(varContent) || this.isUserInputCall(varContent)) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Check if variable is in tainted variables map
|
|
262
|
+
if (taintedVariables.has(varName)) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Check for suspicious variable names that commonly hold user input
|
|
267
|
+
const suspiciousNames = ['redirect', 'url', 'next', 'return', 'callback', 'target', 'dest', 'destination'];
|
|
268
|
+
if (suspiciousNames.some(name => varName.includes(name))) {
|
|
269
|
+
// But exclude if it's clearly a hardcoded endpoint
|
|
270
|
+
const hasHardcodedEndpoint = /\/(register|login|account|auth|logout|home|dashboard|en|ja)/.test(text.toLowerCase());
|
|
271
|
+
if (!hasHardcodedEndpoint) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Conservative: if we can't determine, assume not tainted
|
|
278
|
+
// This reduces false positives
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Check if text is a user input function call
|
|
284
|
+
* e.g., params.get(), getQueryParam(), new URLSearchParams().get()
|
|
285
|
+
*/
|
|
286
|
+
isUserInputCall(text) {
|
|
287
|
+
const userInputCallPatterns = [
|
|
288
|
+
"getqueryParam",
|
|
289
|
+
".get(",
|
|
290
|
+
"geturlparameter",
|
|
291
|
+
"getparameter(",
|
|
292
|
+
"getquerystring(",
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return userInputCallPatterns.some(pattern => text.includes(pattern.toLowerCase()));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Check if URL construction appears to be safe (not user-controllable)
|
|
300
|
+
* Returns true if the URL is constructed from:
|
|
301
|
+
* - Environment variables (process.env.*)
|
|
302
|
+
* - Hardcoded relative paths
|
|
303
|
+
* - Template literals with mostly hardcoded content
|
|
304
|
+
*/
|
|
305
|
+
isSafeUrlConstruction(urlText) {
|
|
306
|
+
const lowerText = urlText.toLowerCase();
|
|
307
|
+
|
|
308
|
+
// Check for environment variables with hardcoded paths
|
|
309
|
+
if (this.safeUrlPatterns.some(pattern => lowerText.includes(pattern))) {
|
|
310
|
+
// If it contains env var + any path segments, check if paths are hardcoded
|
|
311
|
+
// e.g., `${process.env.BASE_URL}/en/account/login`
|
|
312
|
+
if (urlText.includes('/')) {
|
|
313
|
+
// Check for specific hardcoded endpoint patterns
|
|
314
|
+
const hasHardcodedEndpoint = /\/(register|login|account|auth|logout|home|dashboard|en|ja|profile|settings)/.test(lowerText);
|
|
315
|
+
if (hasHardcodedEndpoint) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Count template vars vs path segments
|
|
320
|
+
const templateVars = (urlText.match(/\$\{[^}]+\}/g) || []).length;
|
|
321
|
+
const pathSegments = urlText.split('/').filter(s => s.length > 0).length;
|
|
322
|
+
|
|
323
|
+
// If we have hardcoded path segments (more segments than template vars)
|
|
324
|
+
if (pathSegments > templateVars) {
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Check if it's a pure relative path construction
|
|
331
|
+
// e.g., `/register?fc_id=${fanclubId}` where only query param is dynamic
|
|
332
|
+
if (urlText.startsWith("'/") || urlText.startsWith('"/') || urlText.startsWith("`/")) {
|
|
333
|
+
// Extract the base path before query string
|
|
334
|
+
const pathMatch = urlText.match(/^['"`](\/[^?$}]+)/);
|
|
335
|
+
if (pathMatch) {
|
|
336
|
+
const basePath = pathMatch[1];
|
|
337
|
+
// If base path is hardcoded (no template vars), it's relatively safe
|
|
338
|
+
// Only dynamic parts should be query params or fragments
|
|
339
|
+
if (!basePath.includes('${')) {
|
|
340
|
+
return true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Check if it's a ternary with only hardcoded alternatives
|
|
346
|
+
// e.g., fanclubId ? '/register?fc_id=${fanclubId}' : '/register'
|
|
347
|
+
// This is safe because both branches have hardcoded base paths
|
|
348
|
+
if (urlText.includes('?') && urlText.includes(':')) {
|
|
349
|
+
// Count how many env vars or hardcoded paths
|
|
350
|
+
let safePatternCount = 0;
|
|
351
|
+
if (this.safeUrlPatterns.some(p => lowerText.includes(p))) safePatternCount++;
|
|
352
|
+
if (this.hardcodedPathPatterns.some(p => urlText.includes(p))) safePatternCount++;
|
|
353
|
+
|
|
354
|
+
// If multiple safe patterns, likely a safe ternary
|
|
355
|
+
if (safePatternCount >= 2) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check server-side redirect functions (res.redirect, response.sendRedirect, etc.)
|
|
365
|
+
*/
|
|
366
|
+
checkServerRedirects(sourceFile, filePath, violations, reportedLines, taintedVariables) {
|
|
367
|
+
const callExprs = sourceFile.getDescendantsOfKind(
|
|
368
|
+
SyntaxKind.CallExpression
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
for (const callExpr of callExprs) {
|
|
372
|
+
const expression = callExpr.getExpression();
|
|
373
|
+
const expressionText = expression.getText().toLowerCase();
|
|
374
|
+
|
|
375
|
+
// Check if this is a redirect function call
|
|
376
|
+
if (!this.isRedirectFunction(expressionText)) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Get the redirect URL argument early for checks
|
|
381
|
+
const args = callExpr.getArguments();
|
|
382
|
+
if (args.length === 0) {
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Skip if this is a function definition (e.g., function redirect() or @Get() redirect())
|
|
387
|
+
// For decorated methods like "@Get() redirect(...)", the call expression is actually the function name
|
|
388
|
+
// Check if the expression is an identifier without property access (not res.redirect but just redirect)
|
|
389
|
+
const hasPropertyAccess = expressionText.includes('.');
|
|
390
|
+
const isStandaloneRedirect = expressionText === 'redirect' ||
|
|
391
|
+
expressionText === 'permanentredirect' ||
|
|
392
|
+
expressionText === 'navigateto';
|
|
393
|
+
|
|
394
|
+
// If it's a standalone redirect call and first arg is a decorator, skip (likely function def)
|
|
395
|
+
if (isStandaloneRedirect && !hasPropertyAccess) {
|
|
396
|
+
const firstArg = args[0];
|
|
397
|
+
const firstArgText = firstArg.getText();
|
|
398
|
+
// Check if first arg looks like a decorator parameter (@Query('url'))
|
|
399
|
+
if (firstArgText.includes('@')) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const line = callExpr.getStartLineNumber();
|
|
405
|
+
if (reportedLines.has(line)) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const urlArg = args[0];
|
|
410
|
+
const urlArgText = urlArg.getText();
|
|
411
|
+
const urlArgTextLower = urlArgText.toLowerCase();
|
|
412
|
+
|
|
413
|
+
// Skip object-based navigation (e.g., navigateTo({ path: ... }))
|
|
414
|
+
// These are routing helpers, not direct redirects
|
|
415
|
+
if (this.isObjectBasedNavigation(urlArg)) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check if URL comes from user input (direct or via tainted variable)
|
|
420
|
+
const isTainted = this.isUserInput(urlArgTextLower) ||
|
|
421
|
+
this.isUserInputCall(urlArgTextLower) ||
|
|
422
|
+
taintedVariables.has(urlArgTextLower);
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if (!isTainted) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check if there's validation in the surrounding context
|
|
430
|
+
const hasValidation = this.hasValidationInContext(callExpr);
|
|
431
|
+
if (hasValidation) {
|
|
432
|
+
continue; // Safe - has validation
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// VIOLATION: Redirect with user input without validation
|
|
436
|
+
violations.push({
|
|
437
|
+
ruleId: this.ruleId,
|
|
438
|
+
severity: "warning",
|
|
439
|
+
message: `Open redirect vulnerability: '${expression.getText()}(${urlArgText})' uses user input without validation - validate against allow list or use relative URLs only`,
|
|
440
|
+
line: line,
|
|
441
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
442
|
+
filePath: filePath,
|
|
443
|
+
file: filePath,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
reportedLines.add(line);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Also check for new RedirectView() constructor calls
|
|
450
|
+
const newExprs = sourceFile.getDescendantsOfKind(
|
|
451
|
+
SyntaxKind.NewExpression
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
for (const newExpr of newExprs) {
|
|
455
|
+
const expression = newExpr.getExpression();
|
|
456
|
+
const expressionText = expression.getText().toLowerCase();
|
|
457
|
+
|
|
458
|
+
// Check if this is a RedirectView constructor
|
|
459
|
+
if (!expressionText.includes("redirectview")) {
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const line = newExpr.getStartLineNumber();
|
|
464
|
+
if (reportedLines.has(line)) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const args = newExpr.getArguments();
|
|
469
|
+
if (!args || args.length === 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const urlArg = args[0];
|
|
474
|
+
const urlArgText = urlArg.getText();
|
|
475
|
+
const urlArgTextLower = urlArgText.toLowerCase();
|
|
476
|
+
|
|
477
|
+
// Check if URL comes from user input
|
|
478
|
+
const isTainted = this.isUserInput(urlArgTextLower) ||
|
|
479
|
+
this.isUserInputCall(urlArgTextLower) ||
|
|
480
|
+
taintedVariables.has(urlArgTextLower);
|
|
481
|
+
|
|
482
|
+
if (!isTainted) {
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const hasValidation = this.hasValidationInContext(newExpr);
|
|
487
|
+
if (hasValidation) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
violations.push({
|
|
492
|
+
ruleId: this.ruleId,
|
|
493
|
+
severity: "warning",
|
|
494
|
+
message: `Open redirect vulnerability: 'new ${expression.getText()}(${urlArgText})' uses user input without validation`,
|
|
495
|
+
line: line,
|
|
496
|
+
column: newExpr.getStart() - newExpr.getStartLinePos() + 1,
|
|
497
|
+
filePath: filePath,
|
|
498
|
+
file: filePath,
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
reportedLines.add(line);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Check client-side redirects (window.location, location.href, etc.)
|
|
507
|
+
*/
|
|
508
|
+
checkClientRedirects(sourceFile, filePath, violations, reportedLines, taintedVariables) {
|
|
509
|
+
// Check binary expressions (assignments)
|
|
510
|
+
const binaryExprs = sourceFile.getDescendantsOfKind(
|
|
511
|
+
SyntaxKind.BinaryExpression
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
for (const binaryExpr of binaryExprs) {
|
|
515
|
+
const operator = binaryExpr.getOperatorToken().getText();
|
|
516
|
+
if (operator !== "=") {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const left = binaryExpr.getLeft();
|
|
521
|
+
const right = binaryExpr.getRight();
|
|
522
|
+
const leftText = left.getText().toLowerCase();
|
|
523
|
+
const rightText = right.getText().toLowerCase();
|
|
524
|
+
|
|
525
|
+
// Check if left side is a location redirect
|
|
526
|
+
if (!this.isClientRedirect(leftText)) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const line = binaryExpr.getStartLineNumber();
|
|
531
|
+
if (reportedLines.has(line)) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Check if right side is user input (direct or via tainted variable)
|
|
536
|
+
// Also check for template literals containing tainted variables
|
|
537
|
+
const rightOriginal = right.getText();
|
|
538
|
+
const isTainted = this.isUserInput(rightText) ||
|
|
539
|
+
this.isUserInputCall(rightText) ||
|
|
540
|
+
this.isChainedUserInputCall(rightText) ||
|
|
541
|
+
this.containsUserInputInTemplate(rightOriginal) ||
|
|
542
|
+
taintedVariables.has(rightText);
|
|
543
|
+
|
|
544
|
+
if (!isTainted) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Check if URL construction is safe (env vars + hardcoded paths)
|
|
549
|
+
if (this.isSafeUrlConstruction(rightOriginal)) {
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Check for validation
|
|
554
|
+
const hasValidation = this.hasValidationInContext(binaryExpr);
|
|
555
|
+
if (hasValidation) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// VIOLATION: Client-side redirect with user input
|
|
560
|
+
violations.push({
|
|
561
|
+
ruleId: this.ruleId,
|
|
562
|
+
severity: "warning",
|
|
563
|
+
message: `Open redirect vulnerability: '${left.getText()} = ${right.getText()}' uses user input without validation - validate URL before redirecting`,
|
|
564
|
+
line: line,
|
|
565
|
+
column: binaryExpr.getStart() - binaryExpr.getStartLinePos() + 1,
|
|
566
|
+
filePath: filePath,
|
|
567
|
+
file: filePath,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
reportedLines.add(line);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Also check method calls (location.replace, location.assign)
|
|
574
|
+
const callExprs = sourceFile.getDescendantsOfKind(
|
|
575
|
+
SyntaxKind.CallExpression
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
for (const callExpr of callExprs) {
|
|
579
|
+
const expression = callExpr.getExpression();
|
|
580
|
+
const expressionText = expression.getText().toLowerCase();
|
|
581
|
+
|
|
582
|
+
if (!this.isClientRedirect(expressionText)) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const line = callExpr.getStartLineNumber();
|
|
587
|
+
if (reportedLines.has(line)) {
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const args = callExpr.getArguments();
|
|
592
|
+
if (args.length === 0) {
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const urlArg = args[0];
|
|
597
|
+
const urlArgText = urlArg.getText().toLowerCase();
|
|
598
|
+
const urlArgOriginal = urlArg.getText();
|
|
599
|
+
|
|
600
|
+
// Skip object-based navigation (e.g., navigateTo({ path: ... }))
|
|
601
|
+
// These are routing helpers, not direct redirects
|
|
602
|
+
if (this.isObjectBasedNavigation(urlArg)) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Check if argument is tainted
|
|
607
|
+
const isTainted = this.isUserInput(urlArgText) ||
|
|
608
|
+
this.isUserInputCall(urlArgText) ||
|
|
609
|
+
taintedVariables.has(urlArgText);
|
|
610
|
+
|
|
611
|
+
if (!isTainted) {
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Check if URL construction is safe
|
|
616
|
+
if (this.isSafeUrlConstruction(urlArgOriginal)) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const hasValidation = this.hasValidationInContext(callExpr);
|
|
621
|
+
if (hasValidation) {
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
violations.push({
|
|
626
|
+
ruleId: this.ruleId,
|
|
627
|
+
severity: "warning",
|
|
628
|
+
message: `Open redirect vulnerability: '${expression.getText()}(${urlArg.getText()})' uses user input without validation`,
|
|
629
|
+
line: line,
|
|
630
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
631
|
+
filePath: filePath,
|
|
632
|
+
file: filePath,
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
reportedLines.add(line);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Check if argument is an object-based navigation (React Router style)
|
|
641
|
+
* e.g., navigateTo({ path: '/home', ... }) instead of navigateTo('/home')
|
|
642
|
+
*/
|
|
643
|
+
isObjectBasedNavigation(arg) {
|
|
644
|
+
// Check if argument is an object literal
|
|
645
|
+
const kind = arg.getKind();
|
|
646
|
+
if (kind === SyntaxKind.ObjectLiteralExpression) {
|
|
647
|
+
// It's an object literal - this is likely a routing config object
|
|
648
|
+
// Not a direct URL redirect
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Check Location header assignments (res.setHeader('Location', url))
|
|
656
|
+
*/
|
|
657
|
+
checkLocationHeaders(sourceFile, filePath, violations, reportedLines, taintedVariables) {
|
|
658
|
+
const callExprs = sourceFile.getDescendantsOfKind(
|
|
659
|
+
SyntaxKind.CallExpression
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
for (const callExpr of callExprs) {
|
|
663
|
+
const expression = callExpr.getExpression();
|
|
664
|
+
const expressionText = expression.getText().toLowerCase();
|
|
665
|
+
|
|
666
|
+
// Check if this is setHeader call
|
|
667
|
+
if (!expressionText.includes("setheader")) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const args = callExpr.getArguments();
|
|
672
|
+
if (args.length < 2) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Check if first argument is 'Location'
|
|
677
|
+
const firstArg = args[0].getText();
|
|
678
|
+
const firstArgLower = firstArg.toLowerCase();
|
|
679
|
+
if (
|
|
680
|
+
!firstArgLower.includes("location") &&
|
|
681
|
+
!firstArgLower.includes("'location'") &&
|
|
682
|
+
!firstArgLower.includes('"location"')
|
|
683
|
+
) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const line = callExpr.getStartLineNumber();
|
|
688
|
+
if (reportedLines.has(line)) {
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Check if second argument (URL) is from user input (direct or via tainted variable)
|
|
693
|
+
const urlArg = args[1];
|
|
694
|
+
const urlArgText = urlArg.getText().toLowerCase();
|
|
695
|
+
|
|
696
|
+
const isTainted = this.isUserInput(urlArgText) ||
|
|
697
|
+
this.isUserInputCall(urlArgText) ||
|
|
698
|
+
taintedVariables.has(urlArgText);
|
|
699
|
+
|
|
700
|
+
if (!isTainted) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const hasValidation = this.hasValidationInContext(callExpr);
|
|
705
|
+
if (hasValidation) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
violations.push({
|
|
710
|
+
ruleId: this.ruleId,
|
|
711
|
+
severity: "warning",
|
|
712
|
+
message: `Open redirect vulnerability: setHeader('Location', ${urlArg.getText()}) uses user input without validation`,
|
|
713
|
+
line: line,
|
|
714
|
+
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
715
|
+
filePath: filePath,
|
|
716
|
+
file: filePath,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
reportedLines.add(line);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Helper: Check if expression text is a redirect function
|
|
725
|
+
* Excludes validation functions that contain redirect in their name
|
|
726
|
+
*/
|
|
727
|
+
isRedirectFunction(text) {
|
|
728
|
+
// Exclude validation functions
|
|
729
|
+
const validationFunctionPatterns = [
|
|
730
|
+
'isvalid',
|
|
731
|
+
'validate',
|
|
732
|
+
'check',
|
|
733
|
+
'verify',
|
|
734
|
+
'istrust',
|
|
735
|
+
];
|
|
736
|
+
|
|
737
|
+
// If text contains validation patterns, it's not a redirect function
|
|
738
|
+
if (validationFunctionPatterns.some(pattern => text.includes(pattern))) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Exclude service/repository method calls (backend patterns)
|
|
743
|
+
// e.g., this.service.getRedirectById(), userRepository.findRedirect()
|
|
744
|
+
const servicePatterns = [
|
|
745
|
+
'.service.',
|
|
746
|
+
'.repository.',
|
|
747
|
+
'service.',
|
|
748
|
+
'repository.',
|
|
749
|
+
'getredirect',
|
|
750
|
+
'findredirect',
|
|
751
|
+
'fetchredirect',
|
|
752
|
+
'loadredirect',
|
|
753
|
+
];
|
|
754
|
+
|
|
755
|
+
if (servicePatterns.some(pattern => text.includes(pattern))) {
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return this.redirectFunctions.some((func) => text.includes(func));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Helper: Check if expression text is a client-side redirect
|
|
764
|
+
*/
|
|
765
|
+
isClientRedirect(text) {
|
|
766
|
+
return this.clientRedirectPatterns.some((pattern) =>
|
|
767
|
+
text.includes(pattern)
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Helper: Check if text represents user input
|
|
773
|
+
*/
|
|
774
|
+
isUserInput(text) {
|
|
775
|
+
return this.userInputSources.some((source) => text.includes(source));
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Helper: Check if there's validation in the surrounding context
|
|
780
|
+
* Looks for validation patterns in parent blocks (if statements, functions)
|
|
781
|
+
*/
|
|
782
|
+
hasValidationInContext(node) {
|
|
783
|
+
// First check: if statement condition (most direct validation)
|
|
784
|
+
const parentIf = this.findParentIfStatement(node);
|
|
785
|
+
if (parentIf) {
|
|
786
|
+
const condition = parentIf.getExpression();
|
|
787
|
+
const conditionText = condition.getText().toLowerCase();
|
|
788
|
+
|
|
789
|
+
// Check for array.includes(), allowlist checks, validation functions etc.
|
|
790
|
+
if (
|
|
791
|
+
conditionText.includes("includes") ||
|
|
792
|
+
conditionText.includes("allowed") ||
|
|
793
|
+
conditionText.includes("whitelist") ||
|
|
794
|
+
conditionText.includes("startswith('/')") ||
|
|
795
|
+
conditionText.includes('startswith("/")') ||
|
|
796
|
+
conditionText.includes("isvalid") ||
|
|
797
|
+
conditionText.includes("validate") ||
|
|
798
|
+
conditionText.includes("check") ||
|
|
799
|
+
conditionText.includes("verify") ||
|
|
800
|
+
conditionText.includes("istrust") ||
|
|
801
|
+
conditionText.includes("safe")
|
|
802
|
+
) {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Second check: Get surrounding code context (parent statements/function)
|
|
808
|
+
const parentFunction = this.findParentFunction(node);
|
|
809
|
+
if (!parentFunction) {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const functionText = parentFunction.getText().toLowerCase();
|
|
814
|
+
|
|
815
|
+
// Check for validation patterns
|
|
816
|
+
const hasValidationPattern = this.validationPatterns.some((pattern) =>
|
|
817
|
+
functionText.includes(pattern)
|
|
818
|
+
);
|
|
819
|
+
|
|
820
|
+
if (hasValidationPattern) {
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Check for safe framework patterns (mapping)
|
|
825
|
+
const hasSafePattern = this.safeFrameworkPatterns.some((pattern) =>
|
|
826
|
+
functionText.includes(pattern)
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
if (hasSafePattern) {
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
return false;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Helper: Find parent function
|
|
838
|
+
*/
|
|
839
|
+
findParentFunction(node) {
|
|
840
|
+
let current = node.getParent();
|
|
841
|
+
let depth = 0;
|
|
842
|
+
|
|
843
|
+
while (current && depth < 15) {
|
|
844
|
+
const kind = current.getKind();
|
|
845
|
+
if (
|
|
846
|
+
kind === SyntaxKind.FunctionDeclaration ||
|
|
847
|
+
kind === SyntaxKind.FunctionExpression ||
|
|
848
|
+
kind === SyntaxKind.ArrowFunction ||
|
|
849
|
+
kind === SyntaxKind.MethodDeclaration
|
|
850
|
+
) {
|
|
851
|
+
return current;
|
|
852
|
+
}
|
|
853
|
+
current = current.getParent();
|
|
854
|
+
depth++;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Helper: Find parent if statement
|
|
862
|
+
*/
|
|
863
|
+
findParentIfStatement(node) {
|
|
864
|
+
let current = node.getParent();
|
|
865
|
+
let depth = 0;
|
|
866
|
+
|
|
867
|
+
while (current && depth < 10) {
|
|
868
|
+
const kind = current.getKind();
|
|
869
|
+
if (kind === SyntaxKind.IfStatement) {
|
|
870
|
+
return current;
|
|
871
|
+
}
|
|
872
|
+
current = current.getParent();
|
|
873
|
+
depth++;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
cleanup() {
|
|
880
|
+
// Cleanup if needed
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
module.exports = S003SymbolBasedAnalyzer;
|