@sun-asterisk/sunlint 1.3.1 → 1.3.3
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 +85 -0
- package/CONTRIBUTING.md +210 -1691
- package/README.md +5 -3
- package/config/rule-analysis-strategies.js +17 -1
- package/config/rules/enhanced-rules-registry.json +506 -1161
- package/config/rules/rules-registry-generated.json +1 -1
- package/core/analysis-orchestrator.js +167 -42
- package/core/auto-performance-manager.js +243 -0
- package/core/cli-action-handler.js +9 -1
- package/core/cli-program.js +19 -5
- package/core/constants/defaults.js +56 -0
- package/core/enhanced-rules-registry.js +2 -1
- package/core/performance-optimizer.js +271 -0
- package/core/semantic-engine.js +15 -3
- package/core/semantic-rule-base.js +4 -2
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
- package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
- package/docs/PERFORMANCE.md +311 -0
- package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
- package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
- package/docs/QUICK_FILE_LIMITS.md +64 -0
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
- package/engines/heuristic-engine.js +247 -9
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
- package/origin-rules/common-en.md +11 -7
- package/package.json +2 -1
- package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
- package/rules/common/C006_function_naming/analyzer.js +29 -3
- package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
- package/rules/common/C010_limit_block_nesting/config.json +64 -0
- package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
- package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
- package/rules/common/C013_no_dead_code/analyzer.js +75 -177
- package/rules/common/C013_no_dead_code/config.json +61 -0
- package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
- package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
- package/rules/common/C014_dependency_injection/analyzer.js +48 -313
- package/rules/common/C014_dependency_injection/config.json +26 -0
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
- package/rules/common/C018_no_throw_generic_error/config.json +50 -0
- package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +110 -317
- package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
- package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
- package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
- package/rules/common/C023_no_duplicate_variable/config.json +50 -0
- package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
- package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
- package/rules/common/C035_error_logging_context/analyzer.js +3 -1
- package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
- package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
- package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
- package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
- package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
- package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
- package/rules/index.js +7 -1
- package/rules/security/S009_no_insecure_encryption/README.md +158 -0
- package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
- package/rules/security/S009_no_insecure_encryption/config.json +55 -0
- package/rules/security/S010_no_insecure_encryption/README.md +224 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
- package/rules/security/S010_no_insecure_encryption/config.json +48 -0
- package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
- package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
- package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
- package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
- package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
- package/rules/security/S017_use_parameterized_queries/README.md +128 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
- package/rules/security/S017_use_parameterized_queries/config.json +109 -0
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
- package/rules/security/S031_secure_session_cookies/README.md +127 -0
- package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
- package/rules/security/S031_secure_session_cookies/config.json +86 -0
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
- package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
- package/rules/security/S032_httponly_session_cookies/README.md +184 -0
- package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
- package/rules/security/S032_httponly_session_cookies/config.json +96 -0
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
- package/rules/security/S033_samesite_session_cookies/README.md +227 -0
- package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
- package/rules/security/S033_samesite_session_cookies/config.json +87 -0
- package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
- package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
- package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
- package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
- package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
- package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
- package/rules/security/S035_path_session_cookies/README.md +257 -0
- package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
- package/rules/security/S035_path_session_cookies/config.json +99 -0
- package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
- package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
- package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
- package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
- package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
- package/rules/security/S055_content_type_validation/README.md +176 -0
- package/rules/security/S055_content_type_validation/analyzer.js +312 -0
- package/rules/security/S055_content_type_validation/config.json +48 -0
- package/rules/utils/rule-helpers.js +140 -1
- package/scripts/batch-processing-demo.js +334 -0
- package/scripts/consolidate-config.js +116 -0
- package/scripts/performance-test.js +541 -0
- package/scripts/quick-performance-test.js +108 -0
- package/config/rules/S027-categories.json +0 -122
- package/config/rules/rules-registry.json +0 -777
- package/rules/common/C006_function_naming/smart-analyzer.js +0 -503
|
@@ -0,0 +1,1348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S032 Symbol-Based Analyzer - Set HttpOnly attribute for Session Cookies
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S032SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S032";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Session cookie indicators
|
|
15
|
+
this.sessionIndicators = [
|
|
16
|
+
"session",
|
|
17
|
+
"sessionid",
|
|
18
|
+
"sessid",
|
|
19
|
+
"jsessionid",
|
|
20
|
+
"phpsessid",
|
|
21
|
+
"asp.net_sessionid",
|
|
22
|
+
"connect.sid",
|
|
23
|
+
"auth",
|
|
24
|
+
"token",
|
|
25
|
+
"jwt",
|
|
26
|
+
"csrf",
|
|
27
|
+
"refresh",
|
|
28
|
+
// NestJS specific
|
|
29
|
+
"nest-session",
|
|
30
|
+
"nest-auth",
|
|
31
|
+
// Next.js specific
|
|
32
|
+
"next-auth.session-token",
|
|
33
|
+
"next-auth.csrf-token",
|
|
34
|
+
"__Host-next-auth.csrf-token",
|
|
35
|
+
"__Secure-next-auth.session-token",
|
|
36
|
+
// Nuxt.js specific
|
|
37
|
+
"nuxt-session",
|
|
38
|
+
"nuxt-auth",
|
|
39
|
+
"auth._token",
|
|
40
|
+
"auth._refresh_token",
|
|
41
|
+
// General framework patterns
|
|
42
|
+
"access_token",
|
|
43
|
+
"refresh_token",
|
|
44
|
+
"id_token",
|
|
45
|
+
"state_token",
|
|
46
|
+
"nonce",
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Cookie methods that need security checking
|
|
50
|
+
this.cookieMethods = [
|
|
51
|
+
"setCookie",
|
|
52
|
+
"cookie",
|
|
53
|
+
"set",
|
|
54
|
+
"append",
|
|
55
|
+
"session",
|
|
56
|
+
"setHeader",
|
|
57
|
+
"writeHead",
|
|
58
|
+
// Framework-specific methods
|
|
59
|
+
"useCookie", // Nuxt.js
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize analyzer with semantic engine
|
|
65
|
+
*/
|
|
66
|
+
async initialize(semanticEngine) {
|
|
67
|
+
this.semanticEngine = semanticEngine;
|
|
68
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
69
|
+
console.log(`🔧 [S032] Symbol-based analyzer initialized`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Main analysis method for ts-morph source files
|
|
75
|
+
*/
|
|
76
|
+
async analyze(sourceFile, filePath) {
|
|
77
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
78
|
+
console.log(`🔍 [S032] Symbol: Starting analysis for ${filePath}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const violations = [];
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Use ts-morph API for more detailed analysis
|
|
85
|
+
this.analyzeMorphSyntaxTree(sourceFile, violations);
|
|
86
|
+
} catch (morphError) {
|
|
87
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
88
|
+
console.log(
|
|
89
|
+
`🔍 [S032] Symbol: ts-morph analysis failed, trying TypeScript compiler API:`,
|
|
90
|
+
morphError.message
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
// Fallback to TypeScript compiler API
|
|
96
|
+
const sourceCode = sourceFile.getFullText();
|
|
97
|
+
const tsSourceFile = ts.createSourceFile(
|
|
98
|
+
filePath,
|
|
99
|
+
sourceCode,
|
|
100
|
+
ts.ScriptTarget.Latest,
|
|
101
|
+
true
|
|
102
|
+
);
|
|
103
|
+
this.visitNode(tsSourceFile, violations, tsSourceFile);
|
|
104
|
+
} catch (tsError) {
|
|
105
|
+
console.warn(
|
|
106
|
+
`⚠ [S032] Symbol: Both analysis methods failed:`,
|
|
107
|
+
tsError.message
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
113
|
+
console.log(
|
|
114
|
+
`🔍 [S032] Symbol: Analysis completed. Found ${violations.length} violations`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return violations;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Analyze using ts-morph syntax tree (preferred method)
|
|
123
|
+
*/
|
|
124
|
+
analyzeMorphSyntaxTree(sourceFile, violations) {
|
|
125
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
126
|
+
console.log(`🔍 [S032] Symbol: Starting ts-morph analysis`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Use SyntaxKind enum from ts-morph
|
|
130
|
+
const SyntaxKind =
|
|
131
|
+
sourceFile.getProject().getTypeChecker().compilerObject.SyntaxKind ||
|
|
132
|
+
require("typescript").SyntaxKind;
|
|
133
|
+
|
|
134
|
+
// Find all call expressions using proper SyntaxKind
|
|
135
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
136
|
+
SyntaxKind.CallExpression
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
140
|
+
console.log(
|
|
141
|
+
`🔍 [S032] Symbol: Found ${callExpressions.length} call expressions`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const callNode of callExpressions) {
|
|
146
|
+
this.checkMorphCookieMethodCall(callNode, violations, sourceFile);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check cookie method calls using ts-morph (more accurate)
|
|
152
|
+
*/
|
|
153
|
+
checkMorphCookieMethodCall(callNode, violations, sourceFile) {
|
|
154
|
+
const methodName = this.getMorphMethodName(callNode);
|
|
155
|
+
|
|
156
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
157
|
+
console.log(
|
|
158
|
+
`🔍 [S032] Symbol: ts-morph Method call detected: "${methodName}"`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!this.cookieMethods.includes(methodName)) {
|
|
163
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
164
|
+
console.log(
|
|
165
|
+
`🔍 [S032] Symbol: Method "${methodName}" not in cookieMethods list`
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
172
|
+
console.log(
|
|
173
|
+
`🔍 [S032] Symbol: Method "${methodName}" found in cookieMethods, proceeding...`
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Skip middleware setup patterns
|
|
178
|
+
const callText = callNode.getText();
|
|
179
|
+
if (
|
|
180
|
+
methodName === "session" &&
|
|
181
|
+
this.isMiddlewareSetup(callText, methodName)
|
|
182
|
+
) {
|
|
183
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
184
|
+
console.log(
|
|
185
|
+
`🔍 [S032] Symbol: Line ${
|
|
186
|
+
callNode.getStartLineNumber?.() || "unknown"
|
|
187
|
+
} - Skipping properly configured session middleware`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Special handling for setHeader("Set-Cookie", [...]) pattern
|
|
194
|
+
if (methodName === "setHeader") {
|
|
195
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
196
|
+
console.log(
|
|
197
|
+
`🔍 [S032] Symbol: Special setHeader handling triggered for line ${
|
|
198
|
+
callNode.getStartLineNumber?.() || "unknown"
|
|
199
|
+
}`
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
this.checkSetHeaderCookies(callNode, violations, sourceFile);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if this is setting a session-related cookie
|
|
207
|
+
const cookieName = this.extractMorphCookieName(callNode);
|
|
208
|
+
if (!this.isSessionCookie(cookieName, callNode)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for httpOnly flag in options
|
|
213
|
+
const hasHttpOnlyFlag = this.checkMorphHttpOnlyFlag(callNode);
|
|
214
|
+
|
|
215
|
+
if (!hasHttpOnlyFlag) {
|
|
216
|
+
this.addMorphViolation(
|
|
217
|
+
callNode,
|
|
218
|
+
violations,
|
|
219
|
+
sourceFile,
|
|
220
|
+
`Session cookie "${cookieName || "unknown"}" missing HttpOnly attribute`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check setHeader("Set-Cookie", [...]) pattern for insecure session cookies
|
|
227
|
+
*/
|
|
228
|
+
checkSetHeaderCookies(callNode, violations, sourceFile) {
|
|
229
|
+
try {
|
|
230
|
+
const args = callNode.getArguments();
|
|
231
|
+
if (!args || args.length < 2) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check if first argument is "Set-Cookie"
|
|
236
|
+
const firstArg = args[0];
|
|
237
|
+
const headerName = firstArg.getText().replace(/['"]/g, "");
|
|
238
|
+
|
|
239
|
+
if (headerName !== "Set-Cookie") {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Get the array of cookie strings from second argument
|
|
244
|
+
const secondArg = args[1];
|
|
245
|
+
if (!secondArg) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Parse cookie strings from array
|
|
250
|
+
const cookieStrings = this.extractCookieStringsFromArray(secondArg);
|
|
251
|
+
|
|
252
|
+
for (const cookieString of cookieStrings) {
|
|
253
|
+
const cookieName = this.extractCookieNameFromString(cookieString);
|
|
254
|
+
|
|
255
|
+
if (this.isSessionCookieName(cookieName)) {
|
|
256
|
+
const hasHttpOnly = cookieString.toLowerCase().includes("httponly");
|
|
257
|
+
|
|
258
|
+
if (!hasHttpOnly) {
|
|
259
|
+
this.addMorphViolation(
|
|
260
|
+
callNode,
|
|
261
|
+
violations,
|
|
262
|
+
sourceFile,
|
|
263
|
+
`Session cookie "${cookieName}" in Set-Cookie header missing HttpOnly attribute`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
270
|
+
console.log(
|
|
271
|
+
`🔍 [S032] Symbol: Error checking setHeader cookies:`,
|
|
272
|
+
error.message
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract cookie strings from array literal or template strings
|
|
280
|
+
*/
|
|
281
|
+
extractCookieStringsFromArray(arrayNode) {
|
|
282
|
+
const cookieStrings = [];
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
if (arrayNode.getKind() === 196) {
|
|
286
|
+
// ArrayLiteralExpression
|
|
287
|
+
const elements = arrayNode.getElements();
|
|
288
|
+
|
|
289
|
+
for (const element of elements) {
|
|
290
|
+
let cookieString = element.getText();
|
|
291
|
+
|
|
292
|
+
// Remove quotes and template literal markers
|
|
293
|
+
cookieString = cookieString
|
|
294
|
+
.replace(/^[`'"]/g, "")
|
|
295
|
+
.replace(/[`'"]$/g, "");
|
|
296
|
+
|
|
297
|
+
// Handle template literals with variables
|
|
298
|
+
if (cookieString.includes("${")) {
|
|
299
|
+
// Extract cookie name from template pattern like `auth=${tokens.auth}; ...`
|
|
300
|
+
const match = cookieString.match(/^(\w+)=/);
|
|
301
|
+
if (match) {
|
|
302
|
+
cookieStrings.push(cookieString);
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
cookieStrings.push(cookieString);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
311
|
+
console.log(
|
|
312
|
+
`🔍 [S032] Symbol: Error extracting cookie strings:`,
|
|
313
|
+
error.message
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return cookieStrings;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Extract cookie name from cookie string like "auth=value; HttpOnly; ..."
|
|
323
|
+
*/
|
|
324
|
+
extractCookieNameFromString(cookieString) {
|
|
325
|
+
try {
|
|
326
|
+
const match = cookieString.match(/^(\w+)=/);
|
|
327
|
+
return match ? match[1] : null;
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get method name from ts-morph call expression
|
|
335
|
+
*/
|
|
336
|
+
getMorphMethodName(callNode) {
|
|
337
|
+
try {
|
|
338
|
+
const expression = callNode.getExpression();
|
|
339
|
+
|
|
340
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
341
|
+
console.log(
|
|
342
|
+
`🔍 [S032] Symbol: Expression kind: ${expression.getKindName()}, text: "${expression
|
|
343
|
+
.getText()
|
|
344
|
+
.substring(0, 30)}..."`
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Handle PropertyAccessExpression (e.g., res.cookie)
|
|
349
|
+
if (expression.getKindName() === "PropertyAccessExpression") {
|
|
350
|
+
const name = expression.getName();
|
|
351
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
352
|
+
console.log(
|
|
353
|
+
`🔍 [S032] Symbol: PropertyAccess method name: "${name}"`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return name;
|
|
357
|
+
}
|
|
358
|
+
// Handle Identifier (e.g., session)
|
|
359
|
+
else if (expression.getKindName() === "Identifier") {
|
|
360
|
+
const name = expression.getText();
|
|
361
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
362
|
+
console.log(`🔍 [S032] Symbol: Identifier method name: "${name}"`);
|
|
363
|
+
}
|
|
364
|
+
return name;
|
|
365
|
+
}
|
|
366
|
+
// Handle CallExpression chains
|
|
367
|
+
else if (expression.getKindName() === "CallExpression") {
|
|
368
|
+
// This is a chained call, look for the immediate property access
|
|
369
|
+
const parentText = expression.getText();
|
|
370
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
371
|
+
console.log(
|
|
372
|
+
`🔍 [S032] Symbol: CallExpression chain: "${parentText.substring(
|
|
373
|
+
0,
|
|
374
|
+
50
|
|
375
|
+
)}..."`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Try to extract method name from the chain
|
|
380
|
+
const methodMatch = parentText.match(/\.(\w+)\s*\([^)]*\)\s*$/);
|
|
381
|
+
if (methodMatch) {
|
|
382
|
+
const name = methodMatch[1];
|
|
383
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
384
|
+
console.log(`🔍 [S032] Symbol: Extracted from chain: "${name}"`);
|
|
385
|
+
}
|
|
386
|
+
return name;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
391
|
+
console.log(
|
|
392
|
+
`🔍 [S032] Symbol: Error getting method name:`,
|
|
393
|
+
error.message
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
399
|
+
console.log(
|
|
400
|
+
`🔍 [S032] Symbol: Could not extract method name, returning empty string`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return "";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Extract cookie name from ts-morph method call
|
|
408
|
+
*/
|
|
409
|
+
extractMorphCookieName(callNode) {
|
|
410
|
+
try {
|
|
411
|
+
const args = callNode.getArguments();
|
|
412
|
+
if (args && args.length > 0) {
|
|
413
|
+
const methodName = this.getMorphMethodName(callNode);
|
|
414
|
+
|
|
415
|
+
// Handle setCookie(event, "cookieName", "value", options) pattern
|
|
416
|
+
if (methodName === "setCookie" && args.length >= 2) {
|
|
417
|
+
const secondArg = args[1]; // Cookie name is second argument
|
|
418
|
+
if (secondArg && secondArg.getText) {
|
|
419
|
+
const text = secondArg.getText();
|
|
420
|
+
return text.replace(/['"]/g, ""); // Remove quotes
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Handle standard cookie methods (cookieName is first argument)
|
|
425
|
+
const firstArg = args[0];
|
|
426
|
+
if (firstArg && firstArg.getText) {
|
|
427
|
+
const text = firstArg.getText();
|
|
428
|
+
return text.replace(/['"]/g, ""); // Remove quotes
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
433
|
+
console.log(
|
|
434
|
+
`🔍 [S032] Symbol: Error extracting cookie name:`,
|
|
435
|
+
error.message
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Check for httpOnly flag in ts-morph method call options
|
|
444
|
+
*/
|
|
445
|
+
checkMorphHttpOnlyFlag(callNode) {
|
|
446
|
+
try {
|
|
447
|
+
const args = callNode.getArguments();
|
|
448
|
+
if (!args || args.length < 2) {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const methodName = this.getMorphMethodName(callNode);
|
|
453
|
+
|
|
454
|
+
// For setCookie(event, name, value, options), options is at index 3
|
|
455
|
+
let startIndex = 1;
|
|
456
|
+
if (methodName === "setCookie" && args.length >= 4) {
|
|
457
|
+
startIndex = 3; // Start checking from the options argument
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Check options object (usually second or third argument, or fourth for setCookie)
|
|
461
|
+
for (let i = startIndex; i < args.length; i++) {
|
|
462
|
+
const arg = args[i];
|
|
463
|
+
if (arg && arg.getKind) {
|
|
464
|
+
const SyntaxKind = require("typescript").SyntaxKind;
|
|
465
|
+
|
|
466
|
+
if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
467
|
+
// ObjectLiteralExpression
|
|
468
|
+
let text = arg.getText();
|
|
469
|
+
|
|
470
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
471
|
+
console.log(
|
|
472
|
+
`🔍 [S032] Symbol: Checking object literal: ${text.substring(
|
|
473
|
+
0,
|
|
474
|
+
200
|
|
475
|
+
)}...`
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Remove comments to avoid false positives
|
|
480
|
+
const textWithoutComments = text
|
|
481
|
+
.replace(/\/\/.*$/gm, "")
|
|
482
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
483
|
+
|
|
484
|
+
// Check for explicitly disabled httpOnly (should be treated as violation)
|
|
485
|
+
if (
|
|
486
|
+
textWithoutComments.includes("httpOnly") &&
|
|
487
|
+
(textWithoutComments.includes("false") ||
|
|
488
|
+
textWithoutComments.includes(": false"))
|
|
489
|
+
) {
|
|
490
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
491
|
+
console.log(
|
|
492
|
+
`🔍 [S032] Symbol: HttpOnly explicitly disabled (violation)`
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
return false; // Violation: explicitly disabled
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Check for explicitly enabled httpOnly
|
|
499
|
+
if (
|
|
500
|
+
textWithoutComments.includes("httpOnly") &&
|
|
501
|
+
(textWithoutComments.includes("true") ||
|
|
502
|
+
textWithoutComments.includes(": true"))
|
|
503
|
+
) {
|
|
504
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
505
|
+
console.log(
|
|
506
|
+
`🔍 [S032] Symbol: HttpOnly explicitly enabled (secure)`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Check for spread elements within the object literal
|
|
513
|
+
const hasSpreadElements = text.includes("...");
|
|
514
|
+
if (hasSpreadElements) {
|
|
515
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
516
|
+
console.log(
|
|
517
|
+
`🔍 [S032] Symbol: Object literal contains spread elements, checking each...`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Get spread elements from the object literal
|
|
522
|
+
const spreadMatches = text.match(/\.\.\.([^,}]+)/g);
|
|
523
|
+
if (spreadMatches) {
|
|
524
|
+
for (const spreadMatch of spreadMatches) {
|
|
525
|
+
const reference = spreadMatch.replace(/^\.\.\./g, "").trim();
|
|
526
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
527
|
+
console.log(
|
|
528
|
+
`🔍 [S032] Symbol: Checking spread reference: ${reference}`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (this.isSecureConfigReference(reference, callNode)) {
|
|
533
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
534
|
+
console.log(
|
|
535
|
+
`🔍 [S032] Symbol: ✅ Secure spread reference detected: ${reference}`
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
return true;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// If no httpOnly found in literal and no secure spread elements, it's a violation
|
|
545
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
546
|
+
console.log(
|
|
547
|
+
`🔍 [S032] Symbol: Object literal missing httpOnly and no secure spreads`
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
} else if (
|
|
552
|
+
arg.getKind() === SyntaxKind.Identifier ||
|
|
553
|
+
arg.getKind() === SyntaxKind.PropertyAccessExpression
|
|
554
|
+
) {
|
|
555
|
+
// Handle this.cookieConfig or variable references
|
|
556
|
+
const argText = arg.getText();
|
|
557
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
558
|
+
console.log(`🔍 [S032] Symbol: Found reference: ${argText}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Check if this refers to a configuration object with httpOnly
|
|
562
|
+
if (this.isSecureConfigReference(argText, callNode)) {
|
|
563
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
564
|
+
console.log(
|
|
565
|
+
`🔍 [S032] Symbol: ✅ Secure config reference detected: ${argText}`
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
} else if (arg.getKind() === SyntaxKind.SpreadElement) {
|
|
571
|
+
// Handle spread syntax like { ...this.cookieConfig }
|
|
572
|
+
const spreadText = arg.getText();
|
|
573
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
574
|
+
console.log(
|
|
575
|
+
`🔍 [S032] Symbol: Found spread element: ${spreadText}`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (this.isSecureConfigSpread(spreadText, callNode)) {
|
|
580
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
581
|
+
console.log(
|
|
582
|
+
`🔍 [S032] Symbol: ✅ Secure config spread detected: ${spreadText}`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
} catch (error) {
|
|
591
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
592
|
+
console.log(
|
|
593
|
+
`🔍 [S032] Symbol: Error checking httpOnly flag:`,
|
|
594
|
+
error.message
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Check if reference points to secure configuration
|
|
603
|
+
*/
|
|
604
|
+
isSecureConfigReference(argText, callNode) {
|
|
605
|
+
try {
|
|
606
|
+
const sourceFile = callNode.getSourceFile();
|
|
607
|
+
const fileText = sourceFile.getFullText();
|
|
608
|
+
|
|
609
|
+
// Handle this.cookieConfig pattern
|
|
610
|
+
if (argText.includes("cookieConfig") || argText.includes("config")) {
|
|
611
|
+
const configName = argText.split(".").pop();
|
|
612
|
+
|
|
613
|
+
// Look for the exact config definition and check if it contains httpOnly: true
|
|
614
|
+
// More precise pattern to match the actual config object definition
|
|
615
|
+
const configDefPattern = new RegExp(
|
|
616
|
+
`(?:private|public|readonly|const|let|var)\\s+(?:readonly\\s+)?${configName}\\s*=\\s*{[^}]*}`,
|
|
617
|
+
"gis"
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const configMatch = fileText.match(configDefPattern);
|
|
621
|
+
|
|
622
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
623
|
+
console.log(
|
|
624
|
+
`🔍 [S032] Symbol: Looking for config definition of "${configName}"`
|
|
625
|
+
);
|
|
626
|
+
console.log(
|
|
627
|
+
`🔍 [S032] Symbol: Config match found:`,
|
|
628
|
+
configMatch ? configMatch[0] : "none"
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (configMatch) {
|
|
633
|
+
let configContent = configMatch[0];
|
|
634
|
+
|
|
635
|
+
// Remove comments to avoid false positives from "// Missing: httpOnly: true"
|
|
636
|
+
configContent = configContent
|
|
637
|
+
.replace(/\/\/.*$/gm, "")
|
|
638
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
639
|
+
|
|
640
|
+
const hasHttpOnlyTrue = /httpOnly\s*:\s*true/i.test(configContent);
|
|
641
|
+
|
|
642
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
643
|
+
console.log(
|
|
644
|
+
`🔍 [S032] Symbol: Config content (comments removed):`,
|
|
645
|
+
configContent
|
|
646
|
+
);
|
|
647
|
+
console.log(
|
|
648
|
+
`🔍 [S032] Symbol: httpOnly: true found:`,
|
|
649
|
+
hasHttpOnlyTrue
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return hasHttpOnlyTrue;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
657
|
+
console.log(
|
|
658
|
+
`🔍 [S032] Symbol: No config definition found for "${configName}"`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Handle variable references
|
|
666
|
+
const varPattern = new RegExp(
|
|
667
|
+
`(?:const|let|var)\\s+${argText}\\s*=\\s*{[^}]*httpOnly\\s*:\\s*true`,
|
|
668
|
+
"i"
|
|
669
|
+
);
|
|
670
|
+
return varPattern.test(fileText);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
673
|
+
console.log(
|
|
674
|
+
`🔍 [S032] Symbol: Error checking config reference:`,
|
|
675
|
+
error.message
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Check if spread element contains secure configuration
|
|
684
|
+
*/
|
|
685
|
+
isSecureConfigSpread(spreadText, callNode) {
|
|
686
|
+
try {
|
|
687
|
+
const sourceFile = callNode.getSourceFile();
|
|
688
|
+
const fileText = sourceFile.getFullText();
|
|
689
|
+
|
|
690
|
+
// Extract the reference from spread (e.g., ...this.cookieConfig -> this.cookieConfig)
|
|
691
|
+
const reference = spreadText.replace(/^\.\.\./g, "");
|
|
692
|
+
|
|
693
|
+
return this.isSecureConfigReference(reference, callNode);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
696
|
+
console.log(
|
|
697
|
+
`🔍 [S032] Symbol: Error checking spread config:`,
|
|
698
|
+
error.message
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
addMorphViolation(callNode, violations, sourceFile, message) {
|
|
705
|
+
try {
|
|
706
|
+
const start = callNode.getStart();
|
|
707
|
+
const lineAndChar = sourceFile.getLineAndColumnAtPos(start);
|
|
708
|
+
|
|
709
|
+
violations.push({
|
|
710
|
+
rule: this.ruleId,
|
|
711
|
+
source: sourceFile.getFilePath(),
|
|
712
|
+
category: this.category,
|
|
713
|
+
line: lineAndChar.line,
|
|
714
|
+
column: lineAndChar.column,
|
|
715
|
+
message: `Insecure session cookie: ${message}`,
|
|
716
|
+
severity: "error",
|
|
717
|
+
});
|
|
718
|
+
} catch (error) {
|
|
719
|
+
// Fallback violation without line/column info
|
|
720
|
+
violations.push({
|
|
721
|
+
rule: this.ruleId,
|
|
722
|
+
source: sourceFile.getFilePath ? sourceFile.getFilePath() : "unknown",
|
|
723
|
+
category: this.category,
|
|
724
|
+
line: 1,
|
|
725
|
+
column: 1,
|
|
726
|
+
message: `Insecure session cookie: ${message}`,
|
|
727
|
+
severity: "error",
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// TypeScript compiler API fallback methods
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Visit and analyze syntax tree nodes
|
|
736
|
+
*/
|
|
737
|
+
visitNode(node, violations, sourceFile) {
|
|
738
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
739
|
+
console.log(
|
|
740
|
+
`🔍 [S032] Symbol: Visiting ${ts.SyntaxKind[node.kind]} node`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Check for call expressions
|
|
745
|
+
if (ts.isCallExpression(node)) {
|
|
746
|
+
this.checkCookieMethodCall(node, violations, sourceFile);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Continue traversing
|
|
750
|
+
ts.forEachChild(node, (child) => {
|
|
751
|
+
this.visitNode(child, violations, sourceFile);
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Check cookie method calls for httpOnly flags
|
|
757
|
+
*/
|
|
758
|
+
checkCookieMethodCall(callNode, violations, sourceFile) {
|
|
759
|
+
const methodName = this.getMethodName(callNode);
|
|
760
|
+
|
|
761
|
+
// Get line number for debugging
|
|
762
|
+
let lineNumber = "unknown";
|
|
763
|
+
try {
|
|
764
|
+
const start = sourceFile.getLineAndCharacterOfPosition(
|
|
765
|
+
callNode.getStart(sourceFile)
|
|
766
|
+
);
|
|
767
|
+
lineNumber = start.line + 1;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
// Ignore line number errors
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
773
|
+
console.log(
|
|
774
|
+
`🔍 [S032] Symbol: Line ${lineNumber} - Method call detected: "${methodName}"`
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
if (!this.cookieMethods.includes(methodName)) {
|
|
779
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
780
|
+
console.log(
|
|
781
|
+
`🔍 [S032] Symbol: Line ${lineNumber} - Method "${methodName}" not in cookieMethods list`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Special handling for setHeader("Set-Cookie", [...]) pattern
|
|
788
|
+
if (methodName === "setHeader") {
|
|
789
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
790
|
+
console.log(
|
|
791
|
+
`🔍 [S032] Symbol: Line ${lineNumber} - Special setHeader handling triggered`
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
this.checkSetHeaderCookiesTS(callNode, violations, sourceFile);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Skip middleware setup patterns
|
|
799
|
+
const callText = callNode.getText();
|
|
800
|
+
if (this.isMiddlewareSetup(callText, methodName)) {
|
|
801
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
802
|
+
console.log(
|
|
803
|
+
`🔍 [S032] Symbol: Line ${lineNumber} - Skipping middleware setup for "${methodName}"`
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Check if this is setting a session-related cookie
|
|
810
|
+
const cookieName = this.extractCookieName(callNode);
|
|
811
|
+
|
|
812
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
813
|
+
console.log(`🔍 [S032] Symbol: Extracted cookie name: "${cookieName}"`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
if (!this.isSessionCookie(cookieName, callNode)) {
|
|
817
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
818
|
+
console.log(
|
|
819
|
+
`🔍 [S032] Symbol: Cookie "${cookieName}" not identified as session cookie`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
826
|
+
console.log(
|
|
827
|
+
`🔍 [S032] Symbol: Cookie "${cookieName}" IS a session cookie, checking httpOnly flag...`
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Check for httpOnly flag in options
|
|
832
|
+
const hasHttpOnlyFlag = this.checkHttpOnlyFlag(callNode);
|
|
833
|
+
|
|
834
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
835
|
+
console.log(
|
|
836
|
+
`🔍 [S032] Symbol: HttpOnly flag check result: ${hasHttpOnlyFlag}`
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (!hasHttpOnlyFlag) {
|
|
841
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
842
|
+
console.log(
|
|
843
|
+
`🔍 [S032] Symbol: ⚠️ VIOLATION ADDED: Session cookie "${cookieName}" missing HttpOnly attribute`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
this.addViolation(
|
|
848
|
+
callNode,
|
|
849
|
+
violations,
|
|
850
|
+
sourceFile,
|
|
851
|
+
`Session cookie "${cookieName}" missing HttpOnly attribute`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Check setHeader("Set-Cookie", [...]) pattern for insecure session cookies (TypeScript compiler API)
|
|
858
|
+
*/
|
|
859
|
+
checkSetHeaderCookiesTS(callNode, violations, sourceFile) {
|
|
860
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
861
|
+
console.log(`🔍 [S032] Symbol: checkSetHeaderCookiesTS called`);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
try {
|
|
865
|
+
const args = callNode.arguments;
|
|
866
|
+
if (!args || args.length < 2) {
|
|
867
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
868
|
+
console.log(
|
|
869
|
+
`🔍 [S032] Symbol: setHeader insufficient args: ${
|
|
870
|
+
args?.length || 0
|
|
871
|
+
}`
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Check if first argument is "Set-Cookie"
|
|
878
|
+
const firstArg = args[0];
|
|
879
|
+
let headerName = "";
|
|
880
|
+
if (firstArg.kind === ts.SyntaxKind.StringLiteral) {
|
|
881
|
+
headerName = firstArg.text;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (headerName !== "Set-Cookie") {
|
|
885
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
886
|
+
console.log(
|
|
887
|
+
`🔍 [S032] Symbol: Not Set-Cookie header: "${headerName}"`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
894
|
+
console.log(
|
|
895
|
+
`🔍 [S032] Symbol: Set-Cookie header detected, checking array...`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Get the array of cookie strings from second argument
|
|
900
|
+
const secondArg = args[1];
|
|
901
|
+
if (!secondArg) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Parse cookie strings from array
|
|
906
|
+
const cookieStrings = this.extractCookieStringsFromArrayTS(secondArg);
|
|
907
|
+
|
|
908
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
909
|
+
console.log(
|
|
910
|
+
`🔍 [S032] Symbol: Extracted ${cookieStrings.length} cookie strings`
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
for (const cookieString of cookieStrings) {
|
|
915
|
+
const cookieName = this.extractCookieNameFromString(cookieString);
|
|
916
|
+
|
|
917
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
918
|
+
console.log(
|
|
919
|
+
`🔍 [S032] Symbol: Checking cookie "${cookieName}" from string: "${cookieString}"`
|
|
920
|
+
);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (this.isSessionCookieName(cookieName)) {
|
|
924
|
+
const hasHttpOnly = cookieString.toLowerCase().includes("httponly");
|
|
925
|
+
|
|
926
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
927
|
+
console.log(
|
|
928
|
+
`🔍 [S032] Symbol: Session cookie "${cookieName}" has httpOnly: ${hasHttpOnly}`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (!hasHttpOnly) {
|
|
933
|
+
this.addViolation(
|
|
934
|
+
callNode,
|
|
935
|
+
violations,
|
|
936
|
+
sourceFile,
|
|
937
|
+
`Session cookie "${cookieName}" in Set-Cookie header missing HttpOnly attribute`
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} catch (error) {
|
|
943
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
944
|
+
console.log(
|
|
945
|
+
`🔍 [S032] Symbol: Error checking setHeader cookies:`,
|
|
946
|
+
error.message
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Extract cookie strings from array literal (TypeScript compiler API)
|
|
954
|
+
*/
|
|
955
|
+
extractCookieStringsFromArrayTS(arrayNode) {
|
|
956
|
+
const cookieStrings = [];
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
if (arrayNode.kind === ts.SyntaxKind.ArrayLiteralExpression) {
|
|
960
|
+
const elements = arrayNode.elements;
|
|
961
|
+
|
|
962
|
+
for (const element of elements) {
|
|
963
|
+
let cookieString = "";
|
|
964
|
+
|
|
965
|
+
if (element.kind === ts.SyntaxKind.StringLiteral) {
|
|
966
|
+
cookieString = element.text;
|
|
967
|
+
} else if (
|
|
968
|
+
element.kind === ts.SyntaxKind.TemplateExpression ||
|
|
969
|
+
element.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
|
|
970
|
+
) {
|
|
971
|
+
// Handle template literals
|
|
972
|
+
cookieString = element.getText();
|
|
973
|
+
// Remove backticks
|
|
974
|
+
cookieString = cookieString.replace(/^`/, "").replace(/`$/, "");
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (cookieString) {
|
|
978
|
+
cookieStrings.push(cookieString);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
} catch (error) {
|
|
983
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
984
|
+
console.log(
|
|
985
|
+
`🔍 [S032] Symbol: Error extracting cookie strings:`,
|
|
986
|
+
error.message
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
return cookieStrings;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Get method name from call expression
|
|
996
|
+
*/
|
|
997
|
+
getMethodName(callNode) {
|
|
998
|
+
try {
|
|
999
|
+
if (callNode.expression && callNode.expression.name) {
|
|
1000
|
+
return callNode.expression.name.text;
|
|
1001
|
+
} else if (callNode.expression && callNode.expression.property) {
|
|
1002
|
+
return callNode.expression.property.text;
|
|
1003
|
+
}
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1006
|
+
console.log(
|
|
1007
|
+
`🔍 [S032] Symbol: Error getting method name:`,
|
|
1008
|
+
error.message
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return "";
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Extract cookie name from method call
|
|
1017
|
+
*/
|
|
1018
|
+
extractCookieName(callNode) {
|
|
1019
|
+
try {
|
|
1020
|
+
if (callNode.arguments && callNode.arguments.length > 0) {
|
|
1021
|
+
const firstArg = callNode.arguments[0];
|
|
1022
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
1023
|
+
return firstArg.text;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1028
|
+
console.log(
|
|
1029
|
+
`🔍 [S032] Symbol: Error extracting cookie name:`,
|
|
1030
|
+
error.message
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return null;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Check if method setup is middleware configuration
|
|
1039
|
+
*/
|
|
1040
|
+
isMiddlewareSetup(callText, methodName) {
|
|
1041
|
+
// Remove comments before checking for cookie configuration
|
|
1042
|
+
const codeOnly = callText
|
|
1043
|
+
.replace(/\/\/.*$/gm, "")
|
|
1044
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
1045
|
+
|
|
1046
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1047
|
+
console.log(
|
|
1048
|
+
`🔍 [S032] Symbol: Checking middleware setup for method: ${methodName}`
|
|
1049
|
+
);
|
|
1050
|
+
console.log(
|
|
1051
|
+
`🔍 [S032] Symbol: Call text (code only): ${codeOnly.substring(
|
|
1052
|
+
0,
|
|
1053
|
+
100
|
|
1054
|
+
)}...`
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// Check for session middleware patterns
|
|
1059
|
+
if (methodName === "session" || codeOnly.includes("session(")) {
|
|
1060
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1061
|
+
console.log(
|
|
1062
|
+
`🔍 [S032] Symbol: Session middleware detected, checking for cookie config...`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// Check for existing cookie configuration
|
|
1067
|
+
if (codeOnly.includes("cookie:")) {
|
|
1068
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1069
|
+
console.log(
|
|
1070
|
+
`🔍 [S032] Symbol: Cookie config found, checking for httpOnly...`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Check if httpOnly is properly configured
|
|
1075
|
+
const httpOnlyPatterns = [
|
|
1076
|
+
/httpOnly\s*:\s*true/i,
|
|
1077
|
+
/httpOnly\s*=\s*true/i,
|
|
1078
|
+
/['"]httpOnly['"]\s*:\s*true/i,
|
|
1079
|
+
];
|
|
1080
|
+
|
|
1081
|
+
const hasProperHttpOnly = httpOnlyPatterns.some((pattern) =>
|
|
1082
|
+
pattern.test(codeOnly)
|
|
1083
|
+
);
|
|
1084
|
+
|
|
1085
|
+
if (hasProperHttpOnly) {
|
|
1086
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1087
|
+
console.log(
|
|
1088
|
+
`🔍 [S032] Symbol: ✅ Skipping - session middleware has proper httpOnly config`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
return true; // Skip - properly configured
|
|
1092
|
+
} else {
|
|
1093
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1094
|
+
console.log(
|
|
1095
|
+
`🔍 [S032] Symbol: ❌ Not skipping - session middleware missing httpOnly: true`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return false; // Don't skip - needs to be checked for missing httpOnly
|
|
1099
|
+
}
|
|
1100
|
+
} else {
|
|
1101
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1102
|
+
console.log(
|
|
1103
|
+
`🔍 [S032] Symbol: ❌ Not skipping - session middleware without cookie config (violation)`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
return false; // Don't skip - needs to be checked for missing cookie config
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// Other non-session middleware patterns can be skipped
|
|
1111
|
+
const nonSessionMiddlewarePatterns = [
|
|
1112
|
+
/middleware.*(?!session)/i, // middleware but not session
|
|
1113
|
+
/use\(.*(?!session)/i, // use() but not session
|
|
1114
|
+
/app\.use\((?!.*session)/i, // app.use() but not session
|
|
1115
|
+
];
|
|
1116
|
+
|
|
1117
|
+
const isNonSessionMiddleware = nonSessionMiddlewarePatterns.some(
|
|
1118
|
+
(pattern) => pattern.test(codeOnly)
|
|
1119
|
+
);
|
|
1120
|
+
|
|
1121
|
+
if (isNonSessionMiddleware) {
|
|
1122
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1123
|
+
console.log(`🔍 [S032] Symbol: ✅ Skipping - non-session middleware`);
|
|
1124
|
+
}
|
|
1125
|
+
return true;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
return false; // Don't skip by default
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Check if cookie name indicates session cookie
|
|
1133
|
+
*/
|
|
1134
|
+
isSessionCookie(cookieName, callNode) {
|
|
1135
|
+
const methodName = this.getMethodName(callNode);
|
|
1136
|
+
|
|
1137
|
+
if (process.env.SUNLINT_DEBUG && methodName === "session") {
|
|
1138
|
+
console.log(
|
|
1139
|
+
`🔍 [S032] Symbol: Checking isSessionCookie for session() call with cookieName: "${cookieName}"`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// For session() method calls, they ARE always session-related
|
|
1144
|
+
if (methodName === "session") {
|
|
1145
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1146
|
+
console.log(`🔍 [S032] Symbol: ✅ session() IS a session cookie setup`);
|
|
1147
|
+
}
|
|
1148
|
+
return true;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Check cookie name against session indicators
|
|
1152
|
+
if (!cookieName) {
|
|
1153
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1154
|
+
console.log(`🔍 [S032] Symbol: ❌ No cookie name provided`);
|
|
1155
|
+
}
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const lowerName = cookieName.toLowerCase();
|
|
1160
|
+
const isSession = this.sessionIndicators.some((indicator) =>
|
|
1161
|
+
lowerName.includes(indicator.toLowerCase())
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1165
|
+
console.log(
|
|
1166
|
+
`🔍 [S032] Symbol: Cookie "${cookieName}" session check: ${isSession}`
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
return isSession;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Check if cookie name indicates session cookie (for setHeader pattern)
|
|
1175
|
+
*/
|
|
1176
|
+
isSessionCookieName(cookieName) {
|
|
1177
|
+
if (!cookieName) return false;
|
|
1178
|
+
|
|
1179
|
+
const lowerName = cookieName.toLowerCase();
|
|
1180
|
+
|
|
1181
|
+
// Check against session cookie patterns
|
|
1182
|
+
return this.sessionIndicators.some((keyword) =>
|
|
1183
|
+
lowerName.includes(keyword.toLowerCase())
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Check for httpOnly flag in method call options
|
|
1189
|
+
*/
|
|
1190
|
+
checkHttpOnlyFlag(callNode) {
|
|
1191
|
+
try {
|
|
1192
|
+
if (!callNode.arguments || callNode.arguments.length < 2) {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Check options object (usually second or third argument)
|
|
1197
|
+
for (let i = 1; i < callNode.arguments.length; i++) {
|
|
1198
|
+
const arg = callNode.arguments[i];
|
|
1199
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
1200
|
+
const text = arg.getText();
|
|
1201
|
+
|
|
1202
|
+
// Check for explicitly disabled httpOnly (should be treated as violation)
|
|
1203
|
+
if (
|
|
1204
|
+
text.includes("httpOnly") &&
|
|
1205
|
+
(text.includes("false") || text.includes(": false"))
|
|
1206
|
+
) {
|
|
1207
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1208
|
+
console.log(
|
|
1209
|
+
`🔍 [S032] Symbol: HttpOnly explicitly disabled (violation) in TypeScript API`
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
return false; // Violation: explicitly disabled
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Check for httpOnly: true patterns
|
|
1216
|
+
if (
|
|
1217
|
+
text.includes("httpOnly") &&
|
|
1218
|
+
(text.includes("true") || text.includes(": true"))
|
|
1219
|
+
) {
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
} catch (error) {
|
|
1225
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1226
|
+
console.log(
|
|
1227
|
+
`🔍 [S032] Symbol: Error checking httpOnly flag:`,
|
|
1228
|
+
error.message
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Add violation to results
|
|
1237
|
+
*/
|
|
1238
|
+
addViolation(callNode, violations, sourceFile, message) {
|
|
1239
|
+
try {
|
|
1240
|
+
const start = sourceFile.getLineAndCharacterOfPosition(
|
|
1241
|
+
callNode.getStart(sourceFile)
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
violations.push({
|
|
1245
|
+
rule: this.ruleId,
|
|
1246
|
+
source: sourceFile.fileName,
|
|
1247
|
+
category: this.category,
|
|
1248
|
+
line: start.line + 1,
|
|
1249
|
+
column: start.character + 1,
|
|
1250
|
+
message: `Insecure session cookie: ${message}`,
|
|
1251
|
+
severity: "error",
|
|
1252
|
+
});
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
// Fallback violation
|
|
1255
|
+
violations.push({
|
|
1256
|
+
rule: this.ruleId,
|
|
1257
|
+
source: sourceFile.fileName || "unknown",
|
|
1258
|
+
category: this.category,
|
|
1259
|
+
line: 1,
|
|
1260
|
+
column: 1,
|
|
1261
|
+
message: `Insecure session cookie: ${message}`,
|
|
1262
|
+
severity: "error",
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Detect framework from method call context
|
|
1269
|
+
*/
|
|
1270
|
+
detectFramework(callNode, sourceFile) {
|
|
1271
|
+
const callText = callNode.getText();
|
|
1272
|
+
const fileContent = sourceFile.getFullText();
|
|
1273
|
+
|
|
1274
|
+
// Check imports to detect framework
|
|
1275
|
+
if (
|
|
1276
|
+
fileContent.includes("@nestjs/common") ||
|
|
1277
|
+
fileContent.includes("@Res()")
|
|
1278
|
+
) {
|
|
1279
|
+
return "NestJS";
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (
|
|
1283
|
+
fileContent.includes("next/server") ||
|
|
1284
|
+
fileContent.includes("NextResponse") ||
|
|
1285
|
+
fileContent.includes("NextAuth")
|
|
1286
|
+
) {
|
|
1287
|
+
return "Next.js";
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
if (
|
|
1291
|
+
callText.includes("useCookie") ||
|
|
1292
|
+
fileContent.includes("defineEventHandler") ||
|
|
1293
|
+
fileContent.includes("setCookie")
|
|
1294
|
+
) {
|
|
1295
|
+
return "Nuxt.js";
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
return "Framework";
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Enhanced method name detection with framework support
|
|
1303
|
+
*/
|
|
1304
|
+
getMorphMethodName(callNode) {
|
|
1305
|
+
try {
|
|
1306
|
+
const expression = callNode.getExpression();
|
|
1307
|
+
|
|
1308
|
+
// Handle property access expressions (obj.method)
|
|
1309
|
+
if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) {
|
|
1310
|
+
const propertyName = expression.getNameNode().getText();
|
|
1311
|
+
|
|
1312
|
+
// Check for chained method calls like response.cookies.set
|
|
1313
|
+
if (propertyName === "set" || propertyName === "cookie") {
|
|
1314
|
+
const objectExpression = expression.getExpression();
|
|
1315
|
+
if (
|
|
1316
|
+
objectExpression.getKind() ===
|
|
1317
|
+
ts.SyntaxKind.PropertyAccessExpression
|
|
1318
|
+
) {
|
|
1319
|
+
const parentProperty = objectExpression.getNameNode().getText();
|
|
1320
|
+
if (parentProperty === "cookies") {
|
|
1321
|
+
return "set"; // For cookies.set()
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return propertyName;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return propertyName;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Handle direct function calls
|
|
1331
|
+
if (expression.getKind() === ts.SyntaxKind.Identifier) {
|
|
1332
|
+
return expression.getText();
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
return "";
|
|
1336
|
+
} catch (error) {
|
|
1337
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
1338
|
+
console.log(
|
|
1339
|
+
`🔍 [S032] Symbol: Error getting method name:`,
|
|
1340
|
+
error.message
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
return "";
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
module.exports = S032SymbolBasedAnalyzer;
|