@sun-asterisk/sunlint 1.3.2 → 1.3.4

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 (60) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +5 -3
  3. package/config/rules/enhanced-rules-registry.json +144 -33
  4. package/core/analysis-orchestrator.js +173 -42
  5. package/core/auto-performance-manager.js +243 -0
  6. package/core/cli-action-handler.js +24 -2
  7. package/core/cli-program.js +19 -5
  8. package/core/constants/defaults.js +56 -0
  9. package/core/performance-optimizer.js +271 -0
  10. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
  11. package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
  12. package/docs/PERFORMANCE.md +311 -0
  13. package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
  14. package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
  15. package/docs/QUICK_FILE_LIMITS.md +64 -0
  16. package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
  17. package/engines/engine-factory.js +7 -0
  18. package/engines/heuristic-engine.js +182 -5
  19. package/package.json +2 -1
  20. package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
  21. package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
  22. package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
  23. package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
  24. package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
  25. package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
  26. package/rules/index.js +2 -0
  27. package/rules/security/S017_use_parameterized_queries/README.md +128 -0
  28. package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
  29. package/rules/security/S017_use_parameterized_queries/config.json +109 -0
  30. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
  31. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
  32. package/rules/security/S031_secure_session_cookies/README.md +127 -0
  33. package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
  34. package/rules/security/S031_secure_session_cookies/config.json +86 -0
  35. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
  36. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
  37. package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
  38. package/rules/security/S032_httponly_session_cookies/README.md +184 -0
  39. package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
  40. package/rules/security/S032_httponly_session_cookies/config.json +96 -0
  41. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
  42. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
  43. package/rules/security/S033_samesite_session_cookies/README.md +227 -0
  44. package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
  45. package/rules/security/S033_samesite_session_cookies/config.json +87 -0
  46. package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
  47. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
  48. package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
  49. package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
  50. package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
  51. package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
  52. package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
  53. package/rules/security/S035_path_session_cookies/README.md +257 -0
  54. package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
  55. package/rules/security/S035_path_session_cookies/config.json +99 -0
  56. package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
  57. package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
  58. package/scripts/batch-processing-demo.js +334 -0
  59. package/scripts/performance-test.js +541 -0
  60. package/scripts/quick-performance-test.js +108 -0
@@ -0,0 +1,1084 @@
1
+ /**
2
+ * S031 Symbol-Based Analyzer - Set Secure flag for Session Cookies
3
+ * Uses TypeScript compiler API for semantic analysis
4
+ */
5
+
6
+ const ts = require("typescript");
7
+
8
+ class S031SymbolBasedAnalyzer {
9
+ constructor(semanticEngine = null) {
10
+ this.semanticEngine = semanticEngine;
11
+ this.ruleId = "S031";
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
+ ];
29
+
30
+ // Cookie methods that need security checking
31
+ this.cookieMethods = [
32
+ "setCookie",
33
+ "cookie",
34
+ "set",
35
+ "append",
36
+ "session",
37
+ "setHeader",
38
+ "writeHead",
39
+ ];
40
+ }
41
+
42
+ /**
43
+ * Initialize analyzer with semantic engine
44
+ */
45
+ async initialize(semanticEngine) {
46
+ this.semanticEngine = semanticEngine;
47
+ if (process.env.SUNLINT_DEBUG) {
48
+ console.log(`🔧 [S031] Symbol-based analyzer initialized`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Analyze source file for insecure session cookies
54
+ */
55
+ async analyze(sourceFile, filePath) {
56
+ if (process.env.SUNLINT_DEBUG) {
57
+ console.log(`🔍 [S031] Symbol-based analysis for: ${filePath}`);
58
+ }
59
+
60
+ const violations = [];
61
+
62
+ try {
63
+ // Get the TypeScript compiler SourceFile from ts-morph
64
+ const compilerNode = sourceFile.compilerNode || sourceFile._compilerNode;
65
+ if (!compilerNode) {
66
+ if (process.env.SUNLINT_DEBUG) {
67
+ console.log(`⚠️ [S031] No compiler node found, using ts-morph API`);
68
+ }
69
+ // Use ts-morph API instead
70
+ this.visitMorphNode(sourceFile, violations, sourceFile);
71
+ } else {
72
+ if (process.env.SUNLINT_DEBUG) {
73
+ console.log(`✅ [S031] Using TypeScript compiler API`);
74
+ }
75
+ // Traverse AST to find cookie-related code
76
+ this.visitNode(compilerNode, violations, compilerNode);
77
+ }
78
+ } catch (error) {
79
+ if (process.env.SUNLINT_DEBUG) {
80
+ console.error(`❌ [S031] Symbol analysis error:`, error);
81
+ }
82
+ throw error;
83
+ }
84
+
85
+ return violations;
86
+ }
87
+
88
+ /**
89
+ * Visit ts-morph nodes recursively
90
+ */
91
+ visitMorphNode(node, violations, sourceFile) {
92
+ if (process.env.SUNLINT_DEBUG) {
93
+ const nodeKind = node.getKindName ? node.getKindName() : "Unknown";
94
+ if (
95
+ nodeKind &&
96
+ (nodeKind.includes("Call") || nodeKind.includes("Property"))
97
+ ) {
98
+ console.log(`🔍 [S031] Symbol: Visiting ${nodeKind} node (ts-morph)`);
99
+ }
100
+ }
101
+
102
+ // Check for call expressions
103
+ if (node.getKind && node.getKind() === 208) {
104
+ // CallExpression
105
+ this.checkMorphCookieMethodCall(node, violations, sourceFile);
106
+ }
107
+
108
+ // Continue traversing children
109
+ if (node.getChildren) {
110
+ node.getChildren().forEach((child) => {
111
+ this.visitMorphNode(child, violations, sourceFile);
112
+ });
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Check cookie method calls using ts-morph API
118
+ */
119
+ checkMorphCookieMethodCall(callNode, violations, sourceFile) {
120
+ const methodName = this.getMorphMethodName(callNode);
121
+
122
+ if (process.env.SUNLINT_DEBUG) {
123
+ console.log(
124
+ `🔍 [S031] Symbol: ts-morph Method call detected: "${methodName}"`
125
+ );
126
+ }
127
+
128
+ if (!this.cookieMethods.includes(methodName)) {
129
+ if (process.env.SUNLINT_DEBUG) {
130
+ console.log(
131
+ `🔍 [S031] Symbol: Method "${methodName}" not in cookieMethods list`
132
+ );
133
+ }
134
+ return;
135
+ }
136
+
137
+ if (process.env.SUNLINT_DEBUG) {
138
+ console.log(
139
+ `🔍 [S031] Symbol: Method "${methodName}" found in cookieMethods, proceeding...`
140
+ );
141
+ }
142
+
143
+ // Special handling for setHeader("Set-Cookie", [...]) pattern
144
+ if (methodName === "setHeader") {
145
+ if (process.env.SUNLINT_DEBUG) {
146
+ console.log(
147
+ `🔍 [S031] Symbol: Special setHeader handling triggered for line ${
148
+ callNode.getStartLineNumber?.() || "unknown"
149
+ }`
150
+ );
151
+ }
152
+ this.checkSetHeaderCookies(callNode, violations, sourceFile);
153
+ return;
154
+ }
155
+
156
+ // Check if this is setting a session-related cookie
157
+ const cookieName = this.extractMorphCookieName(callNode);
158
+ if (!this.isSessionCookie(cookieName, callNode)) {
159
+ return;
160
+ }
161
+
162
+ // Check for secure flag in options
163
+ const hasSecureFlag = this.checkMorphSecureFlag(callNode);
164
+
165
+ if (!hasSecureFlag) {
166
+ this.addMorphViolation(
167
+ callNode,
168
+ violations,
169
+ sourceFile,
170
+ `Session cookie "${cookieName || "unknown"}" missing Secure flag`
171
+ );
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Extract method name from ts-morph call expression
177
+ */
178
+ getMorphMethodName(callNode) {
179
+ try {
180
+ const expression = callNode.getExpression();
181
+ if (expression && expression.getKind() === 201) {
182
+ // PropertyAccessExpression
183
+ return expression.getName();
184
+ }
185
+ if (expression && expression.getText) {
186
+ return expression.getText().split(".").pop();
187
+ }
188
+ } catch (error) {
189
+ if (process.env.SUNLINT_DEBUG) {
190
+ console.log(
191
+ `🔍 [S031] Symbol: Error getting method name:`,
192
+ error.message
193
+ );
194
+ }
195
+ }
196
+ return "";
197
+ }
198
+
199
+ /**
200
+ * Check setHeader("Set-Cookie", [...]) pattern for insecure session cookies
201
+ */
202
+ checkSetHeaderCookies(callNode, violations, sourceFile) {
203
+ if (process.env.SUNLINT_DEBUG) {
204
+ console.log(
205
+ `🔍 [S031] Symbol: checkSetHeaderCookies called for line ${
206
+ callNode.getStartLineNumber?.() || "unknown"
207
+ }`
208
+ );
209
+ }
210
+
211
+ try {
212
+ const args = callNode.getArguments();
213
+ if (!args || args.length < 2) {
214
+ if (process.env.SUNLINT_DEBUG) {
215
+ console.log(
216
+ `🔍 [S031] Symbol: setHeader insufficient args: ${
217
+ args?.length || 0
218
+ }`
219
+ );
220
+ }
221
+ return;
222
+ }
223
+
224
+ // Check if first argument is "Set-Cookie"
225
+ const firstArg = args[0];
226
+ const headerName = firstArg.getText().replace(/['"]/g, "");
227
+
228
+ if (headerName !== "Set-Cookie") {
229
+ return;
230
+ }
231
+
232
+ // Get the array of cookie strings from second argument
233
+ const secondArg = args[1];
234
+ if (!secondArg) {
235
+ return;
236
+ }
237
+
238
+ // Parse cookie strings from array
239
+ const cookieStrings = this.extractCookieStringsFromArray(secondArg);
240
+
241
+ for (const cookieString of cookieStrings) {
242
+ const cookieName = this.extractCookieNameFromString(cookieString);
243
+
244
+ if (this.isSessionCookie(cookieName, null)) {
245
+ const hasSecure = cookieString.toLowerCase().includes("secure");
246
+
247
+ if (!hasSecure) {
248
+ this.addMorphViolation(
249
+ callNode,
250
+ violations,
251
+ sourceFile,
252
+ `Session cookie "${cookieName}" in Set-Cookie header missing Secure attribute`
253
+ );
254
+ }
255
+ }
256
+ }
257
+ } catch (error) {
258
+ if (process.env.SUNLINT_DEBUG) {
259
+ console.log(
260
+ `🔍 [S031] Symbol: Error checking setHeader cookies:`,
261
+ error.message
262
+ );
263
+ }
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Extract cookie strings from array literal or template strings
269
+ */
270
+ extractCookieStringsFromArray(arrayNode) {
271
+ const cookieStrings = [];
272
+
273
+ try {
274
+ if (arrayNode.getKind() === 196) {
275
+ // ArrayLiteralExpression
276
+ const elements = arrayNode.getElements();
277
+
278
+ for (const element of elements) {
279
+ let cookieString = element.getText();
280
+
281
+ // Remove quotes and template literal markers
282
+ cookieString = cookieString
283
+ .replace(/^[`'"]/g, "")
284
+ .replace(/[`'"]$/g, "");
285
+
286
+ // Handle template literals with variables
287
+ if (cookieString.includes("${")) {
288
+ // Extract cookie name from template pattern like `auth=${tokens.auth}; ...`
289
+ const match = cookieString.match(/^(\w+)=/);
290
+ if (match) {
291
+ cookieStrings.push(cookieString);
292
+ }
293
+ } else {
294
+ cookieStrings.push(cookieString);
295
+ }
296
+ }
297
+ }
298
+ } catch (error) {
299
+ if (process.env.SUNLINT_DEBUG) {
300
+ console.log(
301
+ `🔍 [S031] Symbol: Error extracting cookie strings:`,
302
+ error.message
303
+ );
304
+ }
305
+ }
306
+
307
+ return cookieStrings;
308
+ }
309
+
310
+ /**
311
+ * Extract cookie name from cookie string like "auth=value; HttpOnly; ..."
312
+ */
313
+ extractCookieNameFromString(cookieString) {
314
+ try {
315
+ const match = cookieString.match(/^(\w+)=/);
316
+ return match ? match[1] : null;
317
+ } catch (error) {
318
+ return null;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Extract cookie name from ts-morph method call
324
+ */
325
+ extractMorphCookieName(callNode) {
326
+ try {
327
+ const args = callNode.getArguments();
328
+ if (args && args.length > 0) {
329
+ const firstArg = args[0];
330
+ if (firstArg && firstArg.getText) {
331
+ const text = firstArg.getText();
332
+ return text.replace(/['"]/g, ""); // Remove quotes
333
+ }
334
+ }
335
+ } catch (error) {
336
+ if (process.env.SUNLINT_DEBUG) {
337
+ console.log(
338
+ `🔍 [S031] Symbol: Error extracting cookie name:`,
339
+ error.message
340
+ );
341
+ }
342
+ }
343
+ return null;
344
+ }
345
+
346
+ /**
347
+ * Check for secure flag in ts-morph method call options
348
+ */
349
+ checkMorphSecureFlag(callNode) {
350
+ try {
351
+ const args = callNode.getArguments();
352
+ if (!args || args.length < 2) {
353
+ return false;
354
+ }
355
+
356
+ // Check options object (usually second or third argument)
357
+ for (let i = 1; i < args.length; i++) {
358
+ const arg = args[i];
359
+ if (arg && arg.getKind && arg.getKind() === 195) {
360
+ // ObjectLiteralExpression
361
+ const text = arg.getText();
362
+ if (
363
+ text.includes("secure") &&
364
+ (text.includes("true") || text.includes(": true"))
365
+ ) {
366
+ return true;
367
+ }
368
+ }
369
+ }
370
+ } catch (error) {
371
+ if (process.env.SUNLINT_DEBUG) {
372
+ console.log(
373
+ `🔍 [S031] Symbol: Error checking secure flag:`,
374
+ error.message
375
+ );
376
+ }
377
+ }
378
+ return false;
379
+ }
380
+
381
+ /**
382
+ * Add violation using ts-morph API
383
+ */
384
+ addMorphViolation(node, violations, sourceFile, message) {
385
+ try {
386
+ const start = node.getStart();
387
+ const lineAndColumn = sourceFile.getLineAndColumnAtPos(start);
388
+ const source = node.getText();
389
+
390
+ violations.push({
391
+ ruleId: this.ruleId,
392
+ source: source,
393
+ category: this.category,
394
+ line: lineAndColumn.line,
395
+ column: lineAndColumn.column,
396
+ message: `Insecure session cookie: ${message}`,
397
+ severity: "error",
398
+ });
399
+
400
+ if (process.env.SUNLINT_DEBUG) {
401
+ console.log(
402
+ `🔍 [S031] Symbol: Added violation at line ${lineAndColumn.line}`
403
+ );
404
+ }
405
+ } catch (error) {
406
+ if (process.env.SUNLINT_DEBUG) {
407
+ console.error(
408
+ `🔍 [S031] Symbol: Error adding violation:`,
409
+ error.message
410
+ );
411
+ }
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Visit AST nodes recursively
417
+ */
418
+ visitNode(node, violations, sourceFile) {
419
+ if (process.env.SUNLINT_DEBUG) {
420
+ const nodeKind = ts.SyntaxKind[node.kind];
421
+ if (
422
+ nodeKind &&
423
+ (nodeKind.includes("Call") || nodeKind.includes("Property"))
424
+ ) {
425
+ console.log(`🔍 [S031] Symbol: Visiting ${nodeKind} node`);
426
+ }
427
+ }
428
+
429
+ // Check for cookie setting method calls
430
+ if (ts.isCallExpression(node)) {
431
+ this.checkCookieMethodCall(node, violations, sourceFile);
432
+ }
433
+
434
+ // Check for property assignments (e.g., response.cookie = ...)
435
+ if (ts.isPropertyAssignment(node) || ts.isBinaryExpression(node)) {
436
+ this.checkCookiePropertyAssignment(node, violations, sourceFile);
437
+ }
438
+
439
+ // Continue traversing
440
+ ts.forEachChild(node, (child) => {
441
+ this.visitNode(child, violations, sourceFile);
442
+ });
443
+ }
444
+
445
+ /**
446
+ * Check cookie method calls for security flags
447
+ */
448
+ checkCookieMethodCall(callNode, violations, sourceFile) {
449
+ const methodName = this.getMethodName(callNode);
450
+
451
+ // Get line number for debugging
452
+ let lineNumber = "unknown";
453
+ try {
454
+ const start = sourceFile.getLineAndCharacterOfPosition(
455
+ callNode.getStart(sourceFile)
456
+ );
457
+ lineNumber = start.line + 1;
458
+ } catch (error) {
459
+ // Ignore line number errors
460
+ }
461
+
462
+ if (process.env.SUNLINT_DEBUG) {
463
+ console.log(
464
+ `🔍 [S031] Symbol: Line ${lineNumber} - Method call detected: "${methodName}"`
465
+ );
466
+ }
467
+
468
+ if (!this.cookieMethods.includes(methodName)) {
469
+ if (process.env.SUNLINT_DEBUG) {
470
+ console.log(
471
+ `🔍 [S031] Symbol: Line ${lineNumber} - Method "${methodName}" not in cookieMethods list`
472
+ );
473
+ }
474
+ return;
475
+ }
476
+
477
+ // Special handling for setHeader("Set-Cookie", [...]) pattern
478
+ if (methodName === "setHeader") {
479
+ if (process.env.SUNLINT_DEBUG) {
480
+ console.log(
481
+ `🔍 [S031] Symbol: Line ${lineNumber} - Special setHeader handling triggered`
482
+ );
483
+ }
484
+ this.checkSetHeaderCookiesTS(callNode, violations, sourceFile);
485
+ return;
486
+ }
487
+
488
+ // Skip middleware setup patterns
489
+ const callText = callNode.getText();
490
+ if (this.isMiddlewareSetup(callText, methodName)) {
491
+ if (process.env.SUNLINT_DEBUG) {
492
+ console.log(
493
+ `🔍 [S031] Symbol: Line ${lineNumber} - Skipping middleware setup for "${methodName}"`
494
+ );
495
+ }
496
+ return;
497
+ }
498
+
499
+ // Check if this is setting a session-related cookie
500
+ const cookieName = this.extractCookieName(callNode);
501
+
502
+ if (process.env.SUNLINT_DEBUG) {
503
+ console.log(`🔍 [S031] Symbol: Extracted cookie name: "${cookieName}"`);
504
+ }
505
+
506
+ if (!this.isSessionCookie(cookieName, callNode)) {
507
+ if (process.env.SUNLINT_DEBUG) {
508
+ console.log(
509
+ `🔍 [S031] Symbol: Cookie "${cookieName}" not identified as session cookie`
510
+ );
511
+ }
512
+ return;
513
+ }
514
+
515
+ if (process.env.SUNLINT_DEBUG) {
516
+ console.log(
517
+ `🔍 [S031] Symbol: Cookie "${cookieName}" IS a session cookie, checking secure flag...`
518
+ );
519
+ }
520
+
521
+ // Check for secure flag in options
522
+ const hasSecureFlag = this.checkSecureFlag(callNode);
523
+
524
+ if (process.env.SUNLINT_DEBUG) {
525
+ console.log(
526
+ `🔍 [S031] Symbol: Secure flag check result: ${hasSecureFlag}`
527
+ );
528
+ }
529
+
530
+ if (!hasSecureFlag) {
531
+ // Improve message for session middleware
532
+ let violationMessage;
533
+ if (methodName === "session" && (!cookieName || cookieName === "null")) {
534
+ violationMessage = `Session middleware missing secure cookie configuration`;
535
+ } else {
536
+ violationMessage = `Session cookie "${
537
+ cookieName || "unknown"
538
+ }" missing Secure flag`;
539
+ }
540
+
541
+ this.addViolation(callNode, violations, sourceFile, violationMessage);
542
+
543
+ if (process.env.SUNLINT_DEBUG) {
544
+ console.log(
545
+ `🔍 [S031] Symbol: ⚠️ VIOLATION ADDED: ${violationMessage}`
546
+ );
547
+ }
548
+ } else {
549
+ if (process.env.SUNLINT_DEBUG) {
550
+ console.log(
551
+ `🔍 [S031] Symbol: ✅ Cookie "${cookieName}" has secure flag`
552
+ );
553
+ }
554
+ }
555
+ }
556
+
557
+ /**
558
+ * Check if this is middleware setup rather than direct cookie setting
559
+ */
560
+ isMiddlewareSetup(callText, methodName) {
561
+ if (process.env.SUNLINT_DEBUG && methodName === "session") {
562
+ console.log(`🔍 [S031] Symbol: Checking middleware for session call`);
563
+ console.log(`🔍 [S031] Symbol: Full callText: "${callText}"`);
564
+ console.log(
565
+ `🔍 [S031] Symbol: Contains "cookie:": ${callText.includes("cookie:")}`
566
+ );
567
+ }
568
+
569
+ // session() calls inside app.use() with proper cookie config can be skipped
570
+ if (methodName === "session" && callText.includes("cookie:")) {
571
+ // Remove comments to avoid false matches
572
+ const codeOnly = callText
573
+ .replace(/\/\/.*$/gm, "")
574
+ .replace(/\/\*[\s\S]*?\*\//g, "");
575
+
576
+ if (process.env.SUNLINT_DEBUG) {
577
+ console.log(`🔍 [S031] Symbol: Code only (no comments): "${codeOnly}"`);
578
+ }
579
+
580
+ // Check if the cookie config has secure: true (in actual code, not comments)
581
+ const cookieConfigMatch = codeOnly.match(/cookie:\s*{[^}]*}/s);
582
+ if (cookieConfigMatch) {
583
+ const cookieConfig = cookieConfigMatch[0];
584
+
585
+ if (process.env.SUNLINT_DEBUG) {
586
+ console.log(
587
+ `🔍 [S031] Symbol: Found cookie config: "${cookieConfig}"`
588
+ );
589
+ console.log(
590
+ `🔍 [S031] Symbol: Contains "secure:": ${cookieConfig.includes(
591
+ "secure:"
592
+ )}`
593
+ );
594
+ }
595
+
596
+ if (
597
+ cookieConfig.includes("secure:") &&
598
+ (cookieConfig.includes("secure: true") ||
599
+ cookieConfig.includes("secure:true"))
600
+ ) {
601
+ if (process.env.SUNLINT_DEBUG) {
602
+ console.log(
603
+ `🔍 [S031] Symbol: ✅ Skipping secure session middleware`
604
+ );
605
+ }
606
+ return true; // Skip secure middleware setup
607
+ }
608
+ }
609
+ if (process.env.SUNLINT_DEBUG) {
610
+ console.log(
611
+ `🔍 [S031] Symbol: ❌ Not skipping - has cookie config but no secure: true`
612
+ );
613
+ }
614
+ return false; // Don't skip insecure middleware setup
615
+ }
616
+
617
+ // For session() without cookie config, check if it's a violation case
618
+ if (methodName === "session") {
619
+ // If it's in app.use() but has no cookie config, it's likely a violation
620
+ if (callText.includes("app.use(") || callText.includes(".use(")) {
621
+ if (process.env.SUNLINT_DEBUG) {
622
+ console.log(
623
+ `🔍 [S031] Symbol: ❌ Not skipping - session middleware without cookie config (violation)`
624
+ );
625
+ }
626
+ return false; // Don't skip - needs to be checked for missing cookie config
627
+ }
628
+ }
629
+
630
+ // Other non-session middleware patterns can be skipped
631
+ const nonSessionMiddlewarePatterns = [
632
+ /middleware.*(?!session)/i, // middleware but not session
633
+ /setup.*(?!session)/i, // setup but not session
634
+ ];
635
+
636
+ const shouldSkip = nonSessionMiddlewarePatterns.some((pattern) =>
637
+ pattern.test(callText)
638
+ );
639
+ if (process.env.SUNLINT_DEBUG && shouldSkip) {
640
+ console.log(`🔍 [S031] Symbol: ✅ Skipping non-session middleware`);
641
+ }
642
+
643
+ return shouldSkip;
644
+ }
645
+
646
+ /**
647
+ * Check property assignments for cookie security
648
+ */
649
+ checkCookiePropertyAssignment(node, violations, sourceFile) {
650
+ const nodeText = node.getText(sourceFile);
651
+
652
+ // Check for document.cookie assignments
653
+ if (
654
+ nodeText.includes("document.cookie") ||
655
+ nodeText.includes("Set-Cookie")
656
+ ) {
657
+ if (
658
+ this.isSessionCookieString(nodeText) &&
659
+ !this.hasSecureInString(nodeText)
660
+ ) {
661
+ this.addViolation(
662
+ node,
663
+ violations,
664
+ sourceFile,
665
+ "Session cookie assignment missing Secure flag"
666
+ );
667
+ }
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Extract method name from call expression
673
+ */
674
+ getMethodName(callNode) {
675
+ if (ts.isPropertyAccessExpression(callNode.expression)) {
676
+ return callNode.expression.name.text;
677
+ }
678
+ if (ts.isIdentifier(callNode.expression)) {
679
+ return callNode.expression.text;
680
+ }
681
+ return "";
682
+ }
683
+
684
+ /**
685
+ * Extract cookie name from method call
686
+ */
687
+ extractCookieName(callNode) {
688
+ if (callNode.arguments && callNode.arguments.length > 0) {
689
+ const firstArg = callNode.arguments[0];
690
+ if (ts.isStringLiteral(firstArg)) {
691
+ return firstArg.text;
692
+ }
693
+ if (ts.isIdentifier(firstArg)) {
694
+ return firstArg.text;
695
+ }
696
+ }
697
+ return null;
698
+ }
699
+
700
+ /**
701
+ * Check if cookie name indicates session cookie
702
+ */
703
+ isSessionCookie(cookieName, callNode) {
704
+ const methodName = this.getMethodName(callNode);
705
+
706
+ if (process.env.SUNLINT_DEBUG && methodName === "session") {
707
+ console.log(
708
+ `🔍 [S031] Symbol: Checking isSessionCookie for session() call with cookieName: "${cookieName}"`
709
+ );
710
+ }
711
+
712
+ // For session() method calls, they ARE always session-related
713
+ if (methodName === "session") {
714
+ if (process.env.SUNLINT_DEBUG) {
715
+ console.log(`🔍 [S031] Symbol: ✅ session() IS a session cookie setup`);
716
+ }
717
+ return true;
718
+ }
719
+
720
+ if (!cookieName || cookieName === "null" || cookieName === "unknown") {
721
+ // If no explicit name, check call context more carefully
722
+ const callText = callNode.getText();
723
+
724
+ // Skip if it's obviously not a session cookie setting
725
+ if (
726
+ callText.includes(".json(") ||
727
+ callText.includes(".status(") ||
728
+ callText.includes("generateToken") ||
729
+ callText.includes("authenticateUser")
730
+ ) {
731
+ return false;
732
+ }
733
+
734
+ return this.sessionIndicators.some((indicator) =>
735
+ callText.toLowerCase().includes(indicator.toLowerCase())
736
+ );
737
+ }
738
+
739
+ return this.sessionIndicators.some((indicator) =>
740
+ cookieName.toLowerCase().includes(indicator.toLowerCase())
741
+ );
742
+ }
743
+
744
+ /**
745
+ * Check if string contains session cookie indicators
746
+ */
747
+ isSessionCookieString(text) {
748
+ const lowerText = text.toLowerCase();
749
+ return this.sessionIndicators.some((indicator) =>
750
+ lowerText.includes(indicator.toLowerCase())
751
+ );
752
+ }
753
+
754
+ /**
755
+ * Check for Secure flag in method call options
756
+ */
757
+ checkSecureFlag(callNode) {
758
+ if (!callNode.arguments || callNode.arguments.length < 1) {
759
+ return false;
760
+ }
761
+
762
+ // For session() middleware, check if cookie config exists and has secure
763
+ const methodName = this.getMethodName(callNode);
764
+ if (methodName === "session") {
765
+ return this.checkSessionMiddlewareSecure(callNode);
766
+ }
767
+
768
+ // For regular cookie methods, check options object (usually second or third argument)
769
+ if (callNode.arguments.length < 2) {
770
+ return false;
771
+ }
772
+
773
+ for (let i = 1; i < callNode.arguments.length; i++) {
774
+ const arg = callNode.arguments[i];
775
+ if (ts.isObjectLiteralExpression(arg)) {
776
+ if (this.hasSecureProperty(arg)) {
777
+ return true;
778
+ }
779
+
780
+ // Check for config references
781
+ const argText = arg.getText();
782
+ if (this.hasSecureConfigReference(argText)) {
783
+ return true;
784
+ }
785
+ } else {
786
+ // Check if argument references a secure config
787
+ const argText = arg.getText();
788
+ if (this.hasSecureConfigReference(argText)) {
789
+ return true;
790
+ }
791
+ }
792
+ }
793
+
794
+ return false;
795
+ }
796
+
797
+ /**
798
+ * Check session middleware for cookie.secure configuration
799
+ */
800
+ checkSessionMiddlewareSecure(callNode) {
801
+ if (!callNode.arguments || callNode.arguments.length === 0) {
802
+ return false;
803
+ }
804
+
805
+ // Session config is usually the first argument
806
+ const configArg = callNode.arguments[0];
807
+ if (!ts.isObjectLiteralExpression(configArg)) {
808
+ return false;
809
+ }
810
+
811
+ // Look for cookie property
812
+ for (const property of configArg.properties) {
813
+ if (ts.isPropertyAssignment(property)) {
814
+ const propName = property.name;
815
+ if (ts.isIdentifier(propName) && propName.text === "cookie") {
816
+ // Check cookie object for secure property
817
+ if (ts.isObjectLiteralExpression(property.initializer)) {
818
+ const cookieObj = property.initializer;
819
+
820
+ // Look for secure property in cookie config
821
+ for (const cookieProp of cookieObj.properties) {
822
+ if (ts.isPropertyAssignment(cookieProp)) {
823
+ const cookiePropName = cookieProp.name;
824
+ if (
825
+ ts.isIdentifier(cookiePropName) &&
826
+ cookiePropName.text === "secure"
827
+ ) {
828
+ // Check if secure is true, or a variable (like isProduction)
829
+ const secureValue = cookieProp.initializer;
830
+ if (secureValue.kind === ts.SyntaxKind.TrueKeyword) {
831
+ return true; // explicit secure: true
832
+ }
833
+ if (ts.isIdentifier(secureValue)) {
834
+ // Variable like isProduction - assume secure
835
+ return true;
836
+ }
837
+ if (secureValue.kind === ts.SyntaxKind.FalseKeyword) {
838
+ return false; // explicit secure: false
839
+ }
840
+ // Any other expression - assume secure (conservative)
841
+ return true;
842
+ }
843
+ }
844
+ }
845
+ return false; // cookie config exists but no secure property
846
+ }
847
+
848
+ // Check if cookie references a secure config
849
+ const cookieText = property.initializer.getText();
850
+ if (this.hasSecureConfigReference(cookieText)) {
851
+ return true;
852
+ }
853
+ return false; // cookie property exists but not secure
854
+ }
855
+ }
856
+ }
857
+
858
+ // No cookie property found = missing cookie config = violation
859
+ return false;
860
+ }
861
+
862
+ /**
863
+ * Check if text contains secure config references
864
+ */
865
+ hasSecureConfigReference(text) {
866
+ const secureConfigPatterns = [
867
+ /\bcookieConfig\b/i,
868
+ /\bsecureConfig\b/i,
869
+ /\.\.\..*config/i,
870
+ /secureOptions/i,
871
+ /cookieDefaults/i,
872
+ /httpOnly.*secure/i,
873
+ /secure.*httpOnly/i,
874
+ ];
875
+
876
+ return secureConfigPatterns.some((pattern) => pattern.test(text));
877
+ }
878
+
879
+ /**
880
+ * Check if object has secure property set to true
881
+ */
882
+ hasSecureProperty(objectNode) {
883
+ for (const property of objectNode.properties) {
884
+ if (ts.isPropertyAssignment(property)) {
885
+ const propName = property.name;
886
+ if (ts.isIdentifier(propName) && propName.text === "secure") {
887
+ // Check if value is true
888
+ if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
889
+ return true;
890
+ }
891
+ }
892
+ }
893
+ }
894
+ return false;
895
+ }
896
+
897
+ /**
898
+ * Check if string contains Secure flag
899
+ */
900
+ hasSecureInString(text) {
901
+ const securePatterns = [
902
+ /secure\s*[:=]\s*true/i,
903
+ /;\s*secure\s*[;\s]/i,
904
+ /;\s*secure$/i,
905
+ /secure\s*=\s*true/i,
906
+ ];
907
+
908
+ return securePatterns.some((pattern) => pattern.test(text));
909
+ }
910
+
911
+ /**
912
+ * Add violation to results
913
+ */
914
+ addViolation(node, violations, sourceFile, message) {
915
+ const start = sourceFile.getLineAndCharacterOfPosition(
916
+ node.getStart(sourceFile)
917
+ );
918
+ const source = node.getText(sourceFile);
919
+
920
+ violations.push({
921
+ ruleId: this.ruleId,
922
+ source: source,
923
+ category: this.category,
924
+ line: start.line + 1,
925
+ column: start.character + 1,
926
+ message: `Insecure session cookie: ${message}`,
927
+ severity: "error",
928
+ });
929
+ }
930
+
931
+ /**
932
+ * Check setHeader("Set-Cookie", [...]) pattern for insecure session cookies (TypeScript compiler API)
933
+ */
934
+ checkSetHeaderCookiesTS(callNode, violations, sourceFile) {
935
+ if (process.env.SUNLINT_DEBUG) {
936
+ console.log(`🔍 [S031] Symbol: checkSetHeaderCookiesTS called`);
937
+ }
938
+
939
+ try {
940
+ const args = callNode.arguments;
941
+ if (!args || args.length < 2) {
942
+ if (process.env.SUNLINT_DEBUG) {
943
+ console.log(
944
+ `🔍 [S031] Symbol: setHeader insufficient args: ${
945
+ args?.length || 0
946
+ }`
947
+ );
948
+ }
949
+ return;
950
+ }
951
+
952
+ // Check if first argument is "Set-Cookie"
953
+ const firstArg = args[0];
954
+ let headerName = "";
955
+ if (firstArg.kind === ts.SyntaxKind.StringLiteral) {
956
+ headerName = firstArg.text;
957
+ }
958
+
959
+ if (headerName !== "Set-Cookie") {
960
+ if (process.env.SUNLINT_DEBUG) {
961
+ console.log(
962
+ `🔍 [S031] Symbol: Not Set-Cookie header: "${headerName}"`
963
+ );
964
+ }
965
+ return;
966
+ }
967
+
968
+ if (process.env.SUNLINT_DEBUG) {
969
+ console.log(
970
+ `🔍 [S031] Symbol: Set-Cookie header detected, checking array...`
971
+ );
972
+ }
973
+
974
+ // Get the array of cookie strings from second argument
975
+ const secondArg = args[1];
976
+ if (!secondArg) {
977
+ return;
978
+ }
979
+
980
+ // Parse cookie strings from array
981
+ const cookieStrings = this.extractCookieStringsFromArrayTS(secondArg);
982
+
983
+ if (process.env.SUNLINT_DEBUG) {
984
+ console.log(
985
+ `🔍 [S031] Symbol: Extracted ${cookieStrings.length} cookie strings`
986
+ );
987
+ }
988
+
989
+ for (const cookieString of cookieStrings) {
990
+ const cookieName = this.extractCookieNameFromString(cookieString);
991
+
992
+ if (process.env.SUNLINT_DEBUG) {
993
+ console.log(
994
+ `🔍 [S031] Symbol: Checking cookie "${cookieName}" from string: "${cookieString}"`
995
+ );
996
+ }
997
+
998
+ if (this.isSessionCookieName(cookieName)) {
999
+ const hasSecure = cookieString.toLowerCase().includes("secure");
1000
+
1001
+ if (process.env.SUNLINT_DEBUG) {
1002
+ console.log(
1003
+ `🔍 [S031] Symbol: Session cookie "${cookieName}" has secure: ${hasSecure}`
1004
+ );
1005
+ }
1006
+
1007
+ if (!hasSecure) {
1008
+ this.addViolation(
1009
+ callNode,
1010
+ violations,
1011
+ sourceFile,
1012
+ `Session cookie "${cookieName}" in Set-Cookie header missing Secure attribute`
1013
+ );
1014
+ }
1015
+ }
1016
+ }
1017
+ } catch (error) {
1018
+ if (process.env.SUNLINT_DEBUG) {
1019
+ console.log(
1020
+ `🔍 [S031] Symbol: Error checking setHeader cookies:`,
1021
+ error.message
1022
+ );
1023
+ }
1024
+ }
1025
+ }
1026
+
1027
+ /**
1028
+ * Extract cookie strings from array literal (TypeScript compiler API)
1029
+ */
1030
+ extractCookieStringsFromArrayTS(arrayNode) {
1031
+ const cookieStrings = [];
1032
+
1033
+ try {
1034
+ if (arrayNode.kind === ts.SyntaxKind.ArrayLiteralExpression) {
1035
+ const elements = arrayNode.elements;
1036
+
1037
+ for (const element of elements) {
1038
+ let cookieString = "";
1039
+
1040
+ if (element.kind === ts.SyntaxKind.StringLiteral) {
1041
+ cookieString = element.text;
1042
+ } else if (
1043
+ element.kind === ts.SyntaxKind.TemplateExpression ||
1044
+ element.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
1045
+ ) {
1046
+ // Handle template literals
1047
+ cookieString = element.getText();
1048
+ // Remove backticks
1049
+ cookieString = cookieString.replace(/^`/, "").replace(/`$/, "");
1050
+ }
1051
+
1052
+ if (cookieString) {
1053
+ cookieStrings.push(cookieString);
1054
+ }
1055
+ }
1056
+ }
1057
+ } catch (error) {
1058
+ if (process.env.SUNLINT_DEBUG) {
1059
+ console.log(
1060
+ `🔍 [S031] Symbol: Error extracting cookie strings:`,
1061
+ error.message
1062
+ );
1063
+ }
1064
+ }
1065
+
1066
+ return cookieStrings;
1067
+ }
1068
+
1069
+ /**
1070
+ * Check if cookie name indicates session cookie (for setHeader pattern)
1071
+ */
1072
+ isSessionCookieName(cookieName) {
1073
+ if (!cookieName) return false;
1074
+
1075
+ const lowerName = cookieName.toLowerCase();
1076
+
1077
+ // Check against session cookie patterns
1078
+ return this.sessionIndicators.some((keyword) =>
1079
+ lowerName.includes(keyword.toLowerCase())
1080
+ );
1081
+ }
1082
+ }
1083
+
1084
+ module.exports = S031SymbolBasedAnalyzer;