@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.
Files changed (120) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/CONTRIBUTING.md +210 -1691
  3. package/README.md +5 -3
  4. package/config/rule-analysis-strategies.js +17 -1
  5. package/config/rules/enhanced-rules-registry.json +506 -1161
  6. package/config/rules/rules-registry-generated.json +1 -1
  7. package/core/analysis-orchestrator.js +167 -42
  8. package/core/auto-performance-manager.js +243 -0
  9. package/core/cli-action-handler.js +9 -1
  10. package/core/cli-program.js +19 -5
  11. package/core/constants/defaults.js +56 -0
  12. package/core/enhanced-rules-registry.js +2 -1
  13. package/core/performance-optimizer.js +271 -0
  14. package/core/semantic-engine.js +15 -3
  15. package/core/semantic-rule-base.js +4 -2
  16. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
  17. package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
  18. package/docs/PERFORMANCE.md +311 -0
  19. package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
  20. package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
  21. package/docs/QUICK_FILE_LIMITS.md +64 -0
  22. package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
  23. package/engines/heuristic-engine.js +247 -9
  24. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +59 -1
  25. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +26 -1
  26. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +54 -19
  27. package/origin-rules/common-en.md +11 -7
  28. package/package.json +2 -1
  29. package/rules/common/C002_no_duplicate_code/analyzer.js +334 -36
  30. package/rules/common/C003_no_vague_abbreviations/analyzer.js +220 -35
  31. package/rules/common/C006_function_naming/analyzer.js +29 -3
  32. package/rules/common/C010_limit_block_nesting/analyzer.js +181 -337
  33. package/rules/common/C010_limit_block_nesting/config.json +64 -0
  34. package/rules/common/C010_limit_block_nesting/regex-based-analyzer.js +379 -0
  35. package/rules/common/C010_limit_block_nesting/symbol-based-analyzer.js +231 -0
  36. package/rules/common/C013_no_dead_code/analyzer.js +75 -177
  37. package/rules/common/C013_no_dead_code/config.json +61 -0
  38. package/rules/common/C013_no_dead_code/regex-based-analyzer.js +345 -0
  39. package/rules/common/C013_no_dead_code/symbol-based-analyzer.js +640 -0
  40. package/rules/common/C014_dependency_injection/analyzer.js +48 -313
  41. package/rules/common/C014_dependency_injection/config.json +26 -0
  42. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +751 -0
  43. package/rules/common/C018_no_throw_generic_error/analyzer.js +232 -0
  44. package/rules/common/C018_no_throw_generic_error/config.json +50 -0
  45. package/rules/common/C018_no_throw_generic_error/regex-based-analyzer.js +387 -0
  46. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +314 -0
  47. package/rules/common/C019_log_level_usage/analyzer.js +110 -317
  48. package/rules/common/C019_log_level_usage/pattern-analyzer.js +88 -0
  49. package/rules/common/C019_log_level_usage/system-log-analyzer.js +1267 -0
  50. package/rules/common/C023_no_duplicate_variable/analyzer.js +180 -0
  51. package/rules/common/C023_no_duplicate_variable/config.json +50 -0
  52. package/rules/common/C023_no_duplicate_variable/symbol-based-analyzer.js +158 -0
  53. package/rules/common/C024_no_scatter_hardcoded_constants/analyzer.js +180 -0
  54. package/rules/common/C024_no_scatter_hardcoded_constants/config.json +50 -0
  55. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +181 -0
  56. package/rules/common/C030_use_custom_error_classes/analyzer.js +200 -0
  57. package/rules/common/C035_error_logging_context/analyzer.js +3 -1
  58. package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
  59. package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
  60. package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
  61. package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
  62. package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
  63. package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
  64. package/rules/index.js +7 -1
  65. package/rules/security/S009_no_insecure_encryption/README.md +158 -0
  66. package/rules/security/S009_no_insecure_encryption/analyzer.js +319 -0
  67. package/rules/security/S009_no_insecure_encryption/config.json +55 -0
  68. package/rules/security/S010_no_insecure_encryption/README.md +224 -0
  69. package/rules/security/S010_no_insecure_encryption/analyzer.js +493 -0
  70. package/rules/security/S010_no_insecure_encryption/config.json +48 -0
  71. package/rules/security/S016_no_sensitive_querystring/STRATEGY.md +149 -0
  72. package/rules/security/S016_no_sensitive_querystring/analyzer.js +276 -0
  73. package/rules/security/S016_no_sensitive_querystring/config.json +127 -0
  74. package/rules/security/S016_no_sensitive_querystring/regex-based-analyzer.js +258 -0
  75. package/rules/security/S016_no_sensitive_querystring/symbol-based-analyzer.js +495 -0
  76. package/rules/security/S017_use_parameterized_queries/README.md +128 -0
  77. package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
  78. package/rules/security/S017_use_parameterized_queries/config.json +109 -0
  79. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
  80. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
  81. package/rules/security/S031_secure_session_cookies/README.md +127 -0
  82. package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
  83. package/rules/security/S031_secure_session_cookies/config.json +86 -0
  84. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
  85. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
  86. package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
  87. package/rules/security/S032_httponly_session_cookies/README.md +184 -0
  88. package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
  89. package/rules/security/S032_httponly_session_cookies/config.json +96 -0
  90. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
  91. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
  92. package/rules/security/S033_samesite_session_cookies/README.md +227 -0
  93. package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
  94. package/rules/security/S033_samesite_session_cookies/config.json +87 -0
  95. package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
  96. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
  97. package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
  98. package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
  99. package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
  100. package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
  101. package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
  102. package/rules/security/S035_path_session_cookies/README.md +257 -0
  103. package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
  104. package/rules/security/S035_path_session_cookies/config.json +99 -0
  105. package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
  106. package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
  107. package/rules/security/S048_no_current_password_in_reset/README.md +222 -0
  108. package/rules/security/S048_no_current_password_in_reset/analyzer.js +366 -0
  109. package/rules/security/S048_no_current_password_in_reset/config.json +48 -0
  110. package/rules/security/S055_content_type_validation/README.md +176 -0
  111. package/rules/security/S055_content_type_validation/analyzer.js +312 -0
  112. package/rules/security/S055_content_type_validation/config.json +48 -0
  113. package/rules/utils/rule-helpers.js +140 -1
  114. package/scripts/batch-processing-demo.js +334 -0
  115. package/scripts/consolidate-config.js +116 -0
  116. package/scripts/performance-test.js +541 -0
  117. package/scripts/quick-performance-test.js +108 -0
  118. package/config/rules/S027-categories.json +0 -122
  119. package/config/rules/rules-registry.json +0 -777
  120. 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;