@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,715 @@
1
+ /**
2
+ * S032 Regex-Based Analyzer - Set HttpOnly attribute for Session Cookies
3
+ * Fallback analysis using regex patterns
4
+ */
5
+
6
+ const fs = require("fs");
7
+
8
+ class S032RegexBasedAnalyzer {
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
+ // Regex patterns for cookie detection
50
+ this.cookiePatterns = [
51
+ // Express/Node.js cookie patterns
52
+ /res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
53
+ /response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
54
+
55
+ // NestJS specific patterns (more specific to avoid overlap)
56
+ /@Res\(\)\s*\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
57
+
58
+ // Next.js patterns
59
+ /NextResponse\.next\(\)\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
60
+ /cookies\(\)\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
61
+ /\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
62
+
63
+ // Nuxt.js patterns
64
+ /useCookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*({[^}]+})/gi,
65
+ /\$cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
66
+
67
+ // Set-Cookie header patterns (array format)
68
+ /setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*\[\s*([^\]]+)\s*\]/gi,
69
+
70
+ // Set-Cookie header patterns (single string)
71
+ /setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"`]([^'"`]+)['"`]/gi,
72
+
73
+ // Session middleware patterns
74
+ /session\s*\(\s*({[^}]+})/gi,
75
+ /\.use\s*\(\s*session\s*\(\s*({[^}]+})/gi,
76
+
77
+ // Framework-specific session patterns
78
+ /NextAuth\s*\(\s*({[^}]+})/gi,
79
+
80
+ // Generic cookie method (only if not caught by above patterns)
81
+ /(?<!response\.)(?<!res\.)(?<!@Res\(\)\s*)\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*[^,)]+\s*,\s*({[^}]+})/gi,
82
+ ];
83
+
84
+ // NextAuth configuration patterns
85
+ this.nextAuthPatterns = [
86
+ // Individual cookie configuration patterns for sessionToken
87
+ /sessionToken\s*:\s*\{[^{]*?name\s*:\s*['"`]([^'"`]+)['"`][^{]*?options\s*:\s*\{([^}]+)\}/gis,
88
+
89
+ // Individual cookie configuration patterns for csrfToken
90
+ /csrfToken\s*:\s*\{[^{]*?name\s*:\s*['"`]([^'"`]+)['"`][^{]*?options\s*:\s*\{([^}]+)\}/gis,
91
+
92
+ // Generic cookie configuration pattern (fallback)
93
+ /(\w+Token)\s*:\s*\{[^{]*?name\s*:\s*['"`]([^'"`]+)['"`][^{]*?options\s*:\s*\{([^}]+)\}/gis,
94
+ ];
95
+ }
96
+
97
+ /**
98
+ * Initialize analyzer
99
+ */
100
+ async initialize(semanticEngine) {
101
+ this.semanticEngine = semanticEngine;
102
+ if (process.env.SUNLINT_DEBUG) {
103
+ console.log(`🔧 [S032] Regex-based analyzer initialized`);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Main analysis method
109
+ */
110
+ async analyze(filePath) {
111
+ if (process.env.SUNLINT_DEBUG) {
112
+ console.log(`🔍 [S032] Regex-based analysis for: ${filePath}`);
113
+ }
114
+
115
+ const violations = [];
116
+ const violationMap = new Map(); // Deduplication map
117
+
118
+ try {
119
+ const content = fs.readFileSync(filePath, "utf8");
120
+ const lines = content.split("\n");
121
+
122
+ // Check each pattern
123
+ for (const pattern of this.cookiePatterns) {
124
+ this.checkPattern(pattern, content, lines, violations, filePath);
125
+ }
126
+
127
+ // Check NextAuth configuration patterns
128
+ for (const pattern of this.nextAuthPatterns) {
129
+ this.checkNextAuthPattern(
130
+ pattern,
131
+ content,
132
+ lines,
133
+ violations,
134
+ filePath
135
+ );
136
+ }
137
+
138
+ // Check for session middleware without cookie config
139
+ // This method is now mainly handled by checkPattern, but keep for edge cases
140
+ } catch (error) {
141
+ console.warn(
142
+ `⚠ [S032] Regex analysis failed for ${filePath}:`,
143
+ error.message
144
+ );
145
+ }
146
+
147
+ // Deduplicate violations based on line, column, and message
148
+ violations.forEach((violation) => {
149
+ const key = `${violation.line}:${violation.column}:${violation.message}`;
150
+ if (!violationMap.has(key)) {
151
+ violationMap.set(key, violation);
152
+ }
153
+ });
154
+
155
+ const uniqueViolations = Array.from(violationMap.values());
156
+
157
+ if (process.env.SUNLINT_DEBUG) {
158
+ console.log(
159
+ `🔧 [S032] Regex analysis completed: ${violations.length} total, ${uniqueViolations.length} unique violations`
160
+ );
161
+ }
162
+
163
+ return uniqueViolations;
164
+ }
165
+
166
+ /**
167
+ * Check a specific regex pattern for violations
168
+ */
169
+ checkPattern(pattern, content, lines, violations, filePath) {
170
+ pattern.lastIndex = 0; // Reset regex state
171
+ let match;
172
+
173
+ while ((match = pattern.exec(content)) !== null) {
174
+ const matchText = match[0];
175
+ const cookieName = match[1] || "";
176
+ const cookieConfig = match[2] || match[1] || matchText;
177
+
178
+ if (process.env.SUNLINT_DEBUG) {
179
+ console.log(
180
+ `🔍 [S032] Regex: Pattern match - cookieName: "${cookieName}", config: "${cookieConfig.substring(
181
+ 0,
182
+ 50
183
+ )}..."`
184
+ );
185
+ }
186
+
187
+ // Handle different patterns
188
+ if (matchText.includes("setHeader") && matchText.includes("Set-Cookie")) {
189
+ this.checkSetCookieHeaderPattern(match, content, violations, filePath);
190
+ } else if (matchText.includes("session(")) {
191
+ this.checkSessionMiddlewarePattern(
192
+ match,
193
+ content,
194
+ violations,
195
+ filePath
196
+ );
197
+ } else if (
198
+ matchText.includes("NextAuth(") ||
199
+ matchText.includes("useCookie(")
200
+ ) {
201
+ this.checkFrameworkSpecificPattern(
202
+ match,
203
+ content,
204
+ violations,
205
+ filePath
206
+ );
207
+ } else {
208
+ // Regular cookie patterns
209
+ if (this.isSessionCookie(cookieName, matchText)) {
210
+ if (!this.hasHttpOnlyFlag(cookieConfig)) {
211
+ const lineNumber = this.getLineNumber(content, match.index);
212
+
213
+ if (process.env.SUNLINT_DEBUG) {
214
+ console.log(
215
+ `🔍 [S032] Regex: ⚠️ VIOLATION FOUND: Line ${lineNumber} - "${cookieName}" missing httpOnly`
216
+ );
217
+ }
218
+
219
+ violations.push({
220
+ rule: this.ruleId,
221
+ source: filePath,
222
+ category: this.category,
223
+ line: lineNumber,
224
+ column: 1,
225
+ message: `Insecure session cookie: Session cookie "${cookieName}" missing HttpOnly attribute`,
226
+ severity: "error",
227
+ });
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Check NextAuth configuration patterns for missing httpOnly
236
+ */
237
+ checkNextAuthPattern(pattern, content, lines, violations, filePath) {
238
+ pattern.lastIndex = 0; // Reset regex state
239
+ let match;
240
+
241
+ while ((match = pattern.exec(content)) !== null) {
242
+ const matchText = match[0];
243
+
244
+ // Handle different pattern capture groups
245
+ let cookieName, optionsConfig;
246
+ if (match[3]) {
247
+ // Generic pattern: match[1] = tokenType, match[2] = name, match[3] = options
248
+ cookieName = match[2];
249
+ optionsConfig = match[3];
250
+ } else {
251
+ // Specific patterns: match[1] = name, match[2] = options
252
+ cookieName = match[1] || "session-cookie";
253
+ optionsConfig = match[2] || "";
254
+ }
255
+
256
+ if (process.env.SUNLINT_DEBUG) {
257
+ console.log(
258
+ `🔍 [S032] NextAuth: Pattern match - cookieName: "${cookieName}", options: "${optionsConfig.substring(
259
+ 0,
260
+ 50
261
+ )}..."`
262
+ );
263
+ }
264
+
265
+ // Check if httpOnly is missing or false
266
+ if (!this.hasHttpOnlyTrue(optionsConfig)) {
267
+ const lineNumber = this.getLineNumber(content, match.index);
268
+
269
+ if (process.env.SUNLINT_DEBUG) {
270
+ console.log(
271
+ `❌ [S032] NextAuth: Missing HttpOnly for cookie "${cookieName}" at line ${lineNumber}`
272
+ );
273
+ }
274
+
275
+ violations.push({
276
+ rule: this.ruleId,
277
+ source: filePath,
278
+ category: this.category,
279
+ line: lineNumber,
280
+ column: 1,
281
+ message: `NextAuth session cookie: Cookie "${cookieName}" missing HttpOnly attribute in authOptions configuration`,
282
+ severity: "error",
283
+ });
284
+ } else {
285
+ if (process.env.SUNLINT_DEBUG) {
286
+ console.log(
287
+ `✅ [S032] NextAuth: Cookie "${cookieName}" has HttpOnly set correctly`
288
+ );
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ /**
295
+ * Check Set-Cookie header patterns
296
+ */
297
+ checkSetCookieHeaderPattern(match, content, violations, filePath) {
298
+ const matchText = match[0];
299
+
300
+ if (process.env.SUNLINT_DEBUG) {
301
+ console.log(
302
+ `🔍 [S032] Regex: Checking Set-Cookie header pattern: ${matchText.substring(
303
+ 0,
304
+ 100
305
+ )}...`
306
+ );
307
+ }
308
+
309
+ // Extract cookie strings from setHeader array format
310
+ if (matchText.includes("[")) {
311
+ const arrayMatch = matchText.match(/\[\s*([^\]]+)\s*\]/);
312
+ if (arrayMatch) {
313
+ const cookiesContent = arrayMatch[1];
314
+
315
+ // Split by comma but preserve template literals
316
+ const cookieStrings = this.splitCookieStrings(cookiesContent);
317
+
318
+ for (const cookieString of cookieStrings) {
319
+ const cookieName = this.extractCookieNameFromString(cookieString);
320
+
321
+ if (process.env.SUNLINT_DEBUG) {
322
+ console.log(
323
+ `🔍 [S032] Regex: Checking Set-Cookie string: "${cookieString.substring(
324
+ 0,
325
+ 50
326
+ )}..." - name: "${cookieName}"`
327
+ );
328
+ }
329
+
330
+ if (this.isSessionCookie(cookieName, cookieString)) {
331
+ const hasHttpOnly = cookieString.toLowerCase().includes("httponly");
332
+
333
+ if (!hasHttpOnly) {
334
+ const lineNumber = this.getLineNumber(content, match.index);
335
+
336
+ violations.push({
337
+ rule: this.ruleId,
338
+ source: filePath,
339
+ category: this.category,
340
+ line: lineNumber,
341
+ column: 1,
342
+ message: `Insecure session cookie: Session cookie "${cookieName}" in Set-Cookie header missing HttpOnly attribute`,
343
+ severity: "error",
344
+ });
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Split cookie strings while preserving template literals
354
+ */
355
+ splitCookieStrings(cookiesContent) {
356
+ const cookieStrings = [];
357
+ let current = "";
358
+ let inTemplate = false;
359
+ let quoteChar = null;
360
+
361
+ for (let i = 0; i < cookiesContent.length; i++) {
362
+ const char = cookiesContent[i];
363
+
364
+ if ((char === '"' || char === "'" || char === "`") && !quoteChar) {
365
+ quoteChar = char;
366
+ current += char;
367
+ } else if (char === quoteChar) {
368
+ quoteChar = null;
369
+ current += char;
370
+ } else if (char === "," && !quoteChar) {
371
+ if (current.trim()) {
372
+ cookieStrings.push(current.trim());
373
+ current = "";
374
+ }
375
+ } else {
376
+ current += char;
377
+ }
378
+ }
379
+
380
+ if (current.trim()) {
381
+ cookieStrings.push(current.trim());
382
+ }
383
+
384
+ return cookieStrings;
385
+ }
386
+
387
+ /**
388
+ * Extract cookie name from string like "auth=${tokens.auth}; ..." or `auth=${value}; ...`
389
+ */
390
+ extractCookieNameFromString(cookieString) {
391
+ try {
392
+ // Remove quotes and backticks
393
+ const cleaned = cookieString
394
+ .replace(/^[`'"]/g, "")
395
+ .replace(/[`'"]$/g, "");
396
+
397
+ // Match cookie name before = sign
398
+ const match = cleaned.match(/^(\w+)=/);
399
+ return match ? match[1] : null;
400
+ } catch (error) {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Check session middleware pattern
407
+ */
408
+ checkSessionMiddlewarePattern(match, content, violations, filePath) {
409
+ const sessionConfig = match[1];
410
+
411
+ if (process.env.SUNLINT_DEBUG) {
412
+ console.log(
413
+ `🔍 [S032] Regex: Checking session middleware: ${sessionConfig.substring(
414
+ 0,
415
+ 100
416
+ )}...`
417
+ );
418
+ }
419
+
420
+ // Check if session has cookie configuration
421
+ if (sessionConfig.includes("cookie:")) {
422
+ // Has cookie config, check for httpOnly
423
+ if (!this.hasHttpOnlyFlag(sessionConfig)) {
424
+ const lineNumber = this.getLineNumber(content, match.index);
425
+
426
+ violations.push({
427
+ rule: this.ruleId,
428
+ source: filePath,
429
+ category: this.category,
430
+ line: lineNumber,
431
+ column: 1,
432
+ message:
433
+ "Insecure session cookie: Session middleware missing httpOnly attribute",
434
+ severity: "error",
435
+ });
436
+ }
437
+ } else {
438
+ // No cookie config at all
439
+ const lineNumber = this.getLineNumber(content, match.index);
440
+
441
+ violations.push({
442
+ rule: this.ruleId,
443
+ source: filePath,
444
+ category: this.category,
445
+ line: lineNumber,
446
+ column: 1,
447
+ message:
448
+ "Insecure session cookie: Session middleware missing cookie configuration with httpOnly",
449
+ severity: "error",
450
+ });
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Check if cookie name indicates session cookie
456
+ */
457
+ isSessionCookie(cookieName, fullMatch) {
458
+ if (!cookieName && fullMatch.includes("session")) {
459
+ return true; // Session middleware
460
+ }
461
+
462
+ if (!cookieName) return false;
463
+
464
+ const lowerName = cookieName.toLowerCase();
465
+ return this.sessionIndicators.some((indicator) =>
466
+ lowerName.includes(indicator.toLowerCase())
467
+ );
468
+ }
469
+
470
+ /**
471
+ * Check if configuration has httpOnly flag
472
+ */
473
+ hasHttpOnlyFlag(configText) {
474
+ // Skip if this is a reference to external config
475
+ if (this.isConfigReference(configText)) {
476
+ if (process.env.SUNLINT_DEBUG) {
477
+ console.log(
478
+ `🔍 [S032] Regex: Skipping config reference: ${configText.substring(
479
+ 0,
480
+ 30
481
+ )}...`
482
+ );
483
+ }
484
+ return true; // Assume external config is secure (avoid false positives)
485
+ }
486
+
487
+ // Remove comments to avoid false positives from "// Missing: httpOnly: true"
488
+ const codeOnly = configText
489
+ .replace(/\/\/.*$/gm, "")
490
+ .replace(/\/\*[\s\S]*?\*\//g, "");
491
+
492
+ const httpOnlyPatterns = [
493
+ /httpOnly\s*:\s*true/i,
494
+ /httpOnly\s*=\s*true/i,
495
+ /['"]httpOnly['"]\s*:\s*true/i,
496
+ /HttpOnly/i, // For Set-Cookie header format
497
+ ];
498
+
499
+ // Check for explicitly disabled httpOnly (should be treated as violation)
500
+ const httpOnlyDisabledPatterns = [
501
+ /httpOnly\s*:\s*false/i,
502
+ /httpOnly\s*=\s*false/i,
503
+ /['"]httpOnly['"]\s*:\s*false/i,
504
+ ];
505
+
506
+ const hasHttpOnlyFalse = httpOnlyDisabledPatterns.some((pattern) =>
507
+ pattern.test(codeOnly)
508
+ );
509
+
510
+ const hasHttpOnly = httpOnlyPatterns.some((pattern) =>
511
+ pattern.test(codeOnly)
512
+ );
513
+
514
+ // If httpOnly is explicitly set to false, it's a violation
515
+ if (hasHttpOnlyFalse) {
516
+ if (process.env.SUNLINT_DEBUG) {
517
+ console.log(
518
+ `🔍 [S032] Regex: HttpOnly explicitly disabled (violation): ${configText.substring(
519
+ 0,
520
+ 50
521
+ )}...`
522
+ );
523
+ }
524
+ return false; // Violation: explicitly disabled
525
+ }
526
+
527
+ if (process.env.SUNLINT_DEBUG) {
528
+ console.log(
529
+ `🔍 [S032] Regex: HttpOnly check result: ${hasHttpOnly} for config (without comments): ${codeOnly.substring(
530
+ 0,
531
+ 50
532
+ )}...`
533
+ );
534
+ }
535
+
536
+ return hasHttpOnly;
537
+ }
538
+
539
+ /**
540
+ * Check if the config text is a reference to external configuration
541
+ */
542
+ isConfigReference(configText) {
543
+ const refPatterns = [
544
+ /this\.\w+/, // this.cookieConfig
545
+ /\w+Config/, // someConfig
546
+ /\.\.\.\w+/, // ...spread
547
+ /\w+\.\w+/, // object.property
548
+ ];
549
+
550
+ return refPatterns.some((pattern) => pattern.test(configText.trim()));
551
+ }
552
+
553
+ /**
554
+ * Get line number from character index
555
+ */
556
+ getLineNumber(content, index) {
557
+ const beforeMatch = content.substring(0, index);
558
+ return beforeMatch.split("\n").length;
559
+ }
560
+
561
+ /**
562
+ * Check framework-specific patterns (Next.js, Nuxt.js, etc.)
563
+ */
564
+ checkFrameworkSpecificPattern(match, content, violations, filePath) {
565
+ const matchText = match[0];
566
+ const cookieName = match[1] || "";
567
+ const cookieConfig = match[2] || match[1] || "";
568
+
569
+ if (process.env.SUNLINT_DEBUG) {
570
+ console.log(
571
+ `🔍 [S032] Regex: Checking framework-specific pattern: ${matchText.substring(
572
+ 0,
573
+ 100
574
+ )}...`
575
+ );
576
+ }
577
+
578
+ // Handle NextAuth patterns
579
+ if (matchText.includes("NextAuth(")) {
580
+ if (!this.hasHttpOnlyInNextAuthConfig(cookieConfig)) {
581
+ const lineNumber = this.getLineNumber(content, match.index);
582
+ violations.push({
583
+ rule: this.ruleId,
584
+ source: filePath,
585
+ category: this.category,
586
+ line: lineNumber,
587
+ column: 1,
588
+ message:
589
+ "Insecure session cookie: NextAuth configuration missing httpOnly for session cookies",
590
+ severity: "error",
591
+ });
592
+ }
593
+ return;
594
+ }
595
+
596
+ // Handle Nuxt useCookie patterns
597
+ if (matchText.includes("useCookie(")) {
598
+ if (
599
+ this.isSessionCookie(cookieName, matchText) &&
600
+ !this.hasHttpOnlyFlag(cookieConfig)
601
+ ) {
602
+ const lineNumber = this.getLineNumber(content, match.index);
603
+ violations.push({
604
+ rule: this.ruleId,
605
+ source: filePath,
606
+ category: this.category,
607
+ line: lineNumber,
608
+ column: 1,
609
+ message: `Insecure session cookie: Nuxt useCookie "${cookieName}" missing httpOnly attribute`,
610
+ severity: "error",
611
+ });
612
+ }
613
+ return;
614
+ }
615
+
616
+ // Handle other framework patterns
617
+ if (this.isSessionCookie(cookieName, matchText)) {
618
+ if (!this.hasHttpOnlyFlag(cookieConfig)) {
619
+ const lineNumber = this.getLineNumber(content, match.index);
620
+ const framework = this.detectFramework(matchText);
621
+
622
+ violations.push({
623
+ rule: this.ruleId,
624
+ source: filePath,
625
+ category: this.category,
626
+ line: lineNumber,
627
+ column: 1,
628
+ message: `Insecure session cookie: ${framework} session cookie "${cookieName}" missing HttpOnly attribute`,
629
+ severity: "error",
630
+ });
631
+ }
632
+ }
633
+ }
634
+
635
+ /**
636
+ * Check NextAuth configuration for httpOnly
637
+ */
638
+ hasHttpOnlyInNextAuthConfig(config) {
639
+ // NextAuth typically has cookies.sessionToken.httpOnly configuration
640
+ return (
641
+ config.includes("httpOnly: true") ||
642
+ (config.includes("sessionToken") && config.includes("httpOnly")) ||
643
+ (config.includes("cookies") && config.includes("httpOnly"))
644
+ );
645
+ }
646
+
647
+ /**
648
+ * Detect framework from match text
649
+ */
650
+ detectFramework(matchText) {
651
+ if (matchText.includes("@Res()") || matchText.includes("NestJS")) {
652
+ return "NestJS";
653
+ } else if (
654
+ matchText.includes("NextResponse") ||
655
+ matchText.includes("NextAuth")
656
+ ) {
657
+ return "Next.js";
658
+ } else if (
659
+ matchText.includes("useCookie") ||
660
+ matchText.includes("$cookies") ||
661
+ matchText.includes("nuxt")
662
+ ) {
663
+ return "Nuxt.js";
664
+ }
665
+ return "Framework";
666
+ }
667
+
668
+ /**
669
+ * Clean up resources
670
+ */
671
+ cleanup() {
672
+ // No resources to clean up for regex analyzer
673
+ }
674
+
675
+ /**
676
+ * Check if configuration has httpOnly set to true
677
+ */
678
+ hasHttpOnlyTrue(configText) {
679
+ // Remove comments to avoid false positives
680
+ const codeOnly = configText
681
+ .replace(/\/\/.*$/gm, "")
682
+ .replace(/\/\*[\s\S]*?\*\//g, "");
683
+
684
+ const httpOnlyPatterns = [
685
+ /httpOnly\s*:\s*true/i,
686
+ /httpOnly\s*=\s*true/i,
687
+ /['"]httpOnly['"]\s*:\s*true/i,
688
+ /HttpOnly/i, // For Set-Cookie header format
689
+ ];
690
+
691
+ // Check for explicitly disabled httpOnly (should be treated as violation)
692
+ const httpOnlyDisabledPatterns = [
693
+ /httpOnly\s*:\s*false/i,
694
+ /httpOnly\s*=\s*false/i,
695
+ /['"]httpOnly['"]\s*:\s*false/i,
696
+ ];
697
+
698
+ const hasHttpOnlyFalse = httpOnlyDisabledPatterns.some((pattern) =>
699
+ pattern.test(codeOnly)
700
+ );
701
+
702
+ const hasHttpOnly = httpOnlyPatterns.some((pattern) =>
703
+ pattern.test(codeOnly)
704
+ );
705
+
706
+ // If httpOnly is explicitly set to false, it's a violation
707
+ if (hasHttpOnlyFalse) {
708
+ return false; // Violation: explicitly disabled
709
+ }
710
+
711
+ return hasHttpOnly;
712
+ }
713
+ }
714
+
715
+ module.exports = S032RegexBasedAnalyzer;