@sun-asterisk/sunlint 1.3.2 → 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 (59) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +5 -3
  3. package/config/rules/enhanced-rules-registry.json +144 -33
  4. package/core/analysis-orchestrator.js +167 -42
  5. package/core/auto-performance-manager.js +243 -0
  6. package/core/cli-action-handler.js +9 -1
  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/heuristic-engine.js +182 -5
  18. package/package.json +2 -1
  19. package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
  20. package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
  21. package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
  22. package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
  23. package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
  24. package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
  25. package/rules/index.js +2 -0
  26. package/rules/security/S017_use_parameterized_queries/README.md +128 -0
  27. package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
  28. package/rules/security/S017_use_parameterized_queries/config.json +109 -0
  29. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
  30. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
  31. package/rules/security/S031_secure_session_cookies/README.md +127 -0
  32. package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
  33. package/rules/security/S031_secure_session_cookies/config.json +86 -0
  34. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
  35. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
  36. package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
  37. package/rules/security/S032_httponly_session_cookies/README.md +184 -0
  38. package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
  39. package/rules/security/S032_httponly_session_cookies/config.json +96 -0
  40. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
  41. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
  42. package/rules/security/S033_samesite_session_cookies/README.md +227 -0
  43. package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
  44. package/rules/security/S033_samesite_session_cookies/config.json +87 -0
  45. package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
  46. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
  47. package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
  48. package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
  49. package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
  50. package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
  51. package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
  52. package/rules/security/S035_path_session_cookies/README.md +257 -0
  53. package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
  54. package/rules/security/S035_path_session_cookies/config.json +99 -0
  55. package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
  56. package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
  57. package/scripts/batch-processing-demo.js +334 -0
  58. package/scripts/performance-test.js +541 -0
  59. package/scripts/quick-performance-test.js +108 -0
@@ -0,0 +1,724 @@
1
+ /**
2
+ * S035 Regex-based Analyzer - Set Path attribute for Session Cookies
3
+ * Fallback analyzer for pattern-based detection
4
+ */
5
+
6
+ const fs = require("fs");
7
+
8
+ class S035RegexBasedAnalyzer {
9
+ constructor() {
10
+ this.ruleId = "S035";
11
+ this.ruleName = "Set Path attribute for Session Cookies";
12
+ this.description =
13
+ "Set Path attribute for Session Cookies to limit access scope";
14
+
15
+ // Regex patterns for detection
16
+ this.patterns = {
17
+ // Express.js patterns
18
+ cookieCall: /res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]/g,
19
+ setCookieHeader:
20
+ /res\.setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"]([^'"=]+)=/gi,
21
+ setCookieTemplate:
22
+ /res\.setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*`([^`=]+)=/gi,
23
+ setCookieArray:
24
+ /res\.setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*\[([^\]]+)\]/gi,
25
+ sessionMiddleware:
26
+ /session\s*\(\s*\{[^}]*name\s*:\s*['"`]([^'"`]+)['"`]/g,
27
+
28
+ // NestJS patterns
29
+ nestjsResCookie:
30
+ /@Res\(\)\s*\w+[^}]*\.cookie\s*\(\s*['"`]([^'"`]+)['"`]/g,
31
+ nestjsCookieDecorator: /@Cookies\s*\(\s*['"`]([^'"`]+)['"`]/g,
32
+ nestjsResponseCookie: /response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]/g,
33
+
34
+ // Next.js patterns
35
+ nextjsResponseCookiesSet:
36
+ /response\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]/g,
37
+ nextjsCookiesSet: /cookies\(\)\.set\s*\(\s*['"`]([^'"`]+)['"`]/g,
38
+ nextjsSetCookie:
39
+ /NextResponse\.next\(\)\.cookies\.set\s*\(\s*['"`]([^'"`]+)['"`]/g,
40
+
41
+ // NextAuth.js patterns
42
+ nextAuthSessionToken:
43
+ /sessionToken\s*:\s*\{[^}]*name\s*:\s*['"`]([^'"`]+)['"`]/g,
44
+ nextAuthCsrfToken:
45
+ /csrfToken\s*:\s*\{[^}]*name\s*:\s*['"`]([^'"`]+)['"`]/g,
46
+ nextAuthCookies: /cookies\s*:\s*\{[^}]*sessionToken\s*:/g,
47
+
48
+ // Session cookie names (expanded for frameworks)
49
+ sessionCookieNames:
50
+ /^(session|sessionid|session_id|sid|connect\.sid|auth|auth_token|authentication|jwt|token|csrf|csrf_token|xsrf|login|user|userid|user_id|sessionToken|csrfToken|next-auth\.session-token|next-auth\.csrf-token)$/i,
51
+
52
+ // Path attribute patterns
53
+ pathAttribute: /path\s*:\s*['"`]([^'"`]*)['"`]/gi,
54
+ pathInSetCookie: /Path=([^;\\s]*)/gi,
55
+ };
56
+
57
+ this.violations = [];
58
+ }
59
+
60
+ async initialize(semanticEngine) {
61
+ this.semanticEngine = semanticEngine;
62
+ }
63
+
64
+ async analyze(filePath) {
65
+ if (process.env.SUNLINT_DEBUG) {
66
+ console.log(`🔍 [S035] Regex analysis starting for: ${filePath}`);
67
+ }
68
+
69
+ this.violations = [];
70
+
71
+ try {
72
+ const content = fs.readFileSync(filePath, "utf8");
73
+ const lines = content.split("\n");
74
+
75
+ // Analyze patterns
76
+ this.checkCookieCalls(content, lines);
77
+ this.checkSetCookieHeaders(content, lines);
78
+ this.checkSessionMiddleware(content, lines);
79
+ this.checkNestJSPatterns(content, lines);
80
+ this.checkNextJSPatterns(content, lines);
81
+ this.analyzeNextAuthConfig(content, lines);
82
+ } catch (error) {
83
+ console.warn(`⚠ [S035] Regex analysis error:`, error.message);
84
+ }
85
+
86
+ if (process.env.SUNLINT_DEBUG) {
87
+ console.log(
88
+ `🔍 [S035] Regex analysis completed: ${this.violations.length} violations`
89
+ );
90
+ }
91
+
92
+ return this.violations;
93
+ }
94
+
95
+ checkCookieCalls(content, lines) {
96
+ let match;
97
+ this.patterns.cookieCall.lastIndex = 0;
98
+
99
+ while ((match = this.patterns.cookieCall.exec(content)) !== null) {
100
+ const cookieName = match[1];
101
+
102
+ if (this.isSessionCookie(cookieName)) {
103
+ // Check if this cookie call has path attribute
104
+ const cookieCallStart = match.index;
105
+ const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
106
+
107
+ if (cookieCallEnd > cookieCallStart) {
108
+ const cookieConfig = content.substring(
109
+ cookieCallStart,
110
+ cookieCallEnd
111
+ );
112
+
113
+ if (!this.hasPathAttribute(cookieConfig)) {
114
+ const lineInfo = this.findLineNumber(content, match.index, lines);
115
+
116
+ this.addViolation(
117
+ lineInfo.line,
118
+ lineInfo.column,
119
+ `Insecure session cookie: Session cookie "${cookieName}" (Express.js) missing Path attribute`
120
+ );
121
+ } else {
122
+ // Check if path is too broad (root path)
123
+ const pathMatch = cookieConfig.match(this.patterns.pathAttribute);
124
+ if (pathMatch && pathMatch[1] === "/") {
125
+ const lineInfo = this.findLineNumber(content, match.index, lines);
126
+
127
+ this.addViolation(
128
+ lineInfo.line,
129
+ lineInfo.column,
130
+ `Insecure session cookie: Session cookie "${cookieName}" (Express.js) uses root path "/", consider using a more specific path`
131
+ );
132
+ }
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ checkSetCookieHeaders(content, lines) {
140
+ // Check direct Set-Cookie headers
141
+ let match;
142
+ this.patterns.setCookieHeader.lastIndex = 0;
143
+
144
+ while ((match = this.patterns.setCookieHeader.exec(content)) !== null) {
145
+ const cookieName = match[1];
146
+
147
+ if (this.isSessionCookie(cookieName)) {
148
+ // Get the full Set-Cookie value
149
+ const headerStart = content.indexOf('"', match.index);
150
+ const headerEnd = content.indexOf('"', headerStart + 1);
151
+
152
+ if (headerEnd > headerStart) {
153
+ const headerValue = content.substring(headerStart + 1, headerEnd);
154
+
155
+ if (process.env.SUNLINT_DEBUG) {
156
+ console.log(
157
+ `🔍 [S035] Debug - Cookie: ${cookieName}, Header: ${headerValue}`
158
+ );
159
+ console.log(
160
+ `🔍 [S035] Debug - hasPath: ${this.hasPathInSetCookie(
161
+ headerValue
162
+ )}`
163
+ );
164
+ }
165
+
166
+ if (!this.hasPathInSetCookie(headerValue)) {
167
+ const lineInfo = this.findLineNumber(content, match.index, lines);
168
+
169
+ this.addViolation(
170
+ lineInfo.line,
171
+ lineInfo.column,
172
+ `Insecure session cookie: Session cookie "${cookieName}" (Express.js) in Set-Cookie header missing Path attribute`
173
+ );
174
+ } else {
175
+ // Check if path is root
176
+ const pathMatch = headerValue.match(this.patterns.pathInSetCookie);
177
+ if (pathMatch && pathMatch[1] === "/") {
178
+ const lineInfo = this.findLineNumber(content, match.index, lines);
179
+
180
+ this.addViolation(
181
+ lineInfo.line,
182
+ lineInfo.column,
183
+ `Insecure session cookie: Session cookie "${cookieName}" (Express.js) uses root path "/", consider using a more specific path`
184
+ );
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ // Check Set-Cookie headers with template literals
192
+ this.patterns.setCookieTemplate.lastIndex = 0;
193
+
194
+ while ((match = this.patterns.setCookieTemplate.exec(content)) !== null) {
195
+ const cookieName = match[1];
196
+
197
+ if (this.isSessionCookie(cookieName)) {
198
+ // Get the full template literal value
199
+ const templateStart = content.indexOf("`", match.index);
200
+ const templateEnd = content.indexOf("`", templateStart + 1);
201
+
202
+ if (templateEnd > templateStart) {
203
+ const templateValue = content.substring(
204
+ templateStart + 1,
205
+ templateEnd
206
+ );
207
+
208
+ if (process.env.SUNLINT_DEBUG) {
209
+ console.log(
210
+ `🔍 [S035] Debug Template - Cookie: ${cookieName}, Template: ${templateValue}`
211
+ );
212
+ console.log(
213
+ `🔍 [S035] Debug Template - hasPath: ${this.hasPathInSetCookie(
214
+ templateValue
215
+ )}`
216
+ );
217
+ }
218
+
219
+ if (!this.hasPathInSetCookie(templateValue)) {
220
+ const lineInfo = this.findLineNumber(content, match.index, lines);
221
+
222
+ this.addViolation(
223
+ lineInfo.line,
224
+ lineInfo.column,
225
+ `Session cookie "${cookieName}" in Set-Cookie header should specify Path attribute`
226
+ );
227
+ } else {
228
+ // Check for root path usage
229
+ const pathMatch = templateValue.match(/Path=([^;\\s]*)/gi);
230
+ if (pathMatch) {
231
+ const pathValue = pathMatch[0].replace(/Path=/gi, "");
232
+ if (
233
+ pathValue === "/" ||
234
+ pathValue === '""' ||
235
+ pathValue === "''"
236
+ ) {
237
+ const lineInfo = this.findLineNumber(
238
+ content,
239
+ match.index,
240
+ lines
241
+ );
242
+
243
+ this.addViolation(
244
+ lineInfo.line,
245
+ lineInfo.column,
246
+ `Session cookie "${cookieName}" uses root path "/", consider using a more specific path`
247
+ );
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+ }
254
+
255
+ // Check Set-Cookie arrays
256
+ this.patterns.setCookieArray.lastIndex = 0;
257
+
258
+ while ((match = this.patterns.setCookieArray.exec(content)) !== null) {
259
+ const arrayContent = match[1];
260
+ const cookieMatches = arrayContent.match(/['"`]([^'"`=]+)=/g);
261
+
262
+ if (cookieMatches) {
263
+ cookieMatches.forEach((cookieMatch) => {
264
+ const cookieName = cookieMatch.replace(/['"`]/g, "").replace("=", "");
265
+
266
+ if (this.isSessionCookie(cookieName)) {
267
+ if (process.env.SUNLINT_DEBUG) {
268
+ console.log(
269
+ `🔍 [S035] Debug Array - Cookie: ${cookieName}, ArrayContent: ${arrayContent}`
270
+ );
271
+ console.log(
272
+ `🔍 [S035] Debug Array - hasPath: ${this.hasPathInSetCookie(
273
+ arrayContent
274
+ )}`
275
+ );
276
+ console.log(
277
+ `🔍 [S035] Debug Array - !hasPath: ${!this.hasPathInSetCookie(
278
+ arrayContent
279
+ )}`
280
+ );
281
+ }
282
+
283
+ if (!this.hasPathInSetCookie(arrayContent)) {
284
+ const lineInfo = this.findLineNumber(content, match.index, lines);
285
+
286
+ this.addViolation(
287
+ lineInfo.line,
288
+ lineInfo.column,
289
+ `Session cookie "${cookieName}" in Set-Cookie array should specify Path attribute`
290
+ );
291
+ }
292
+ }
293
+ });
294
+ }
295
+ }
296
+ }
297
+
298
+ checkSessionMiddleware(content, lines) {
299
+ let match;
300
+ this.patterns.sessionMiddleware.lastIndex = 0;
301
+
302
+ while ((match = this.patterns.sessionMiddleware.exec(content)) !== null) {
303
+ const cookieName = match[1];
304
+
305
+ if (this.isSessionCookie(cookieName)) {
306
+ // Check if session middleware has cookie.path configuration
307
+ const sessionStart = match.index;
308
+ const sessionEnd = this.findMatchingBrace(content, sessionStart);
309
+
310
+ if (sessionEnd > sessionStart) {
311
+ const sessionConfig = content.substring(sessionStart, sessionEnd);
312
+
313
+ if (!this.hasPathInCookieConfig(sessionConfig)) {
314
+ const lineInfo = this.findLineNumber(content, match.index, lines);
315
+
316
+ this.addViolation(
317
+ lineInfo.line,
318
+ lineInfo.column,
319
+ `Insecure session cookie: Session middleware cookie "${cookieName}" (Express.js) missing Path attribute`
320
+ );
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Check NestJS specific patterns
329
+ */
330
+ checkNestJSPatterns(content, lines) {
331
+ // Check @Res() decorator response.cookie calls
332
+ let match;
333
+ this.patterns.nestjsResCookie.lastIndex = 0;
334
+
335
+ while ((match = this.patterns.nestjsResCookie.exec(content)) !== null) {
336
+ const cookieName = match[1];
337
+
338
+ if (this.isSessionCookie(cookieName)) {
339
+ const cookieCallStart = match.index;
340
+ const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
341
+
342
+ if (cookieCallEnd > cookieCallStart) {
343
+ const cookieConfig = content.substring(
344
+ cookieCallStart,
345
+ cookieCallEnd
346
+ );
347
+
348
+ if (!this.hasPathAttribute(cookieConfig)) {
349
+ const lineInfo = this.findLineNumber(content, match.index, lines);
350
+
351
+ this.addViolation(
352
+ lineInfo.line,
353
+ lineInfo.column,
354
+ `Insecure session cookie: Session cookie "${cookieName}" (NestJS @Res) missing Path attribute`
355
+ );
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ // Check response.cookie calls in NestJS
362
+ this.patterns.nestjsResponseCookie.lastIndex = 0;
363
+
364
+ while (
365
+ (match = this.patterns.nestjsResponseCookie.exec(content)) !== null
366
+ ) {
367
+ const cookieName = match[1];
368
+
369
+ if (this.isSessionCookie(cookieName)) {
370
+ const cookieCallStart = match.index;
371
+ const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
372
+
373
+ if (cookieCallEnd > cookieCallStart) {
374
+ const cookieConfig = content.substring(
375
+ cookieCallStart,
376
+ cookieCallEnd
377
+ );
378
+
379
+ if (!this.hasPathAttribute(cookieConfig)) {
380
+ const lineInfo = this.findLineNumber(content, match.index, lines);
381
+
382
+ this.addViolation(
383
+ lineInfo.line,
384
+ lineInfo.column,
385
+ `Insecure session cookie: Session cookie "${cookieName}" (NestJS) missing Path attribute`
386
+ );
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ // Check @Cookies decorator usage
393
+ this.patterns.nestjsCookieDecorator.lastIndex = 0;
394
+
395
+ while (
396
+ (match = this.patterns.nestjsCookieDecorator.exec(content)) !== null
397
+ ) {
398
+ const cookieName = match[1];
399
+
400
+ if (this.isSessionCookie(cookieName)) {
401
+ const lineInfo = this.findLineNumber(content, match.index, lines);
402
+
403
+ this.addViolation(
404
+ lineInfo.line,
405
+ lineInfo.column,
406
+ `Insecure session cookie: Session cookie "${cookieName}" (NestJS @Cookies) should specify Path attribute`
407
+ );
408
+ }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Check Next.js specific patterns
414
+ */
415
+ checkNextJSPatterns(content, lines) {
416
+ // Check response.cookies.set() calls
417
+ let match;
418
+ this.patterns.nextjsResponseCookiesSet.lastIndex = 0;
419
+
420
+ while (
421
+ (match = this.patterns.nextjsResponseCookiesSet.exec(content)) !== null
422
+ ) {
423
+ const cookieName = match[1];
424
+
425
+ if (this.isSessionCookie(cookieName)) {
426
+ const cookieCallStart = match.index;
427
+ const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
428
+
429
+ if (cookieCallEnd > cookieCallStart) {
430
+ const cookieConfig = content.substring(
431
+ cookieCallStart,
432
+ cookieCallEnd
433
+ );
434
+
435
+ if (!this.hasPathAttribute(cookieConfig)) {
436
+ const lineInfo = this.findLineNumber(content, match.index, lines);
437
+
438
+ this.addViolation(
439
+ lineInfo.line,
440
+ lineInfo.column,
441
+ `Insecure session cookie: Session cookie "${cookieName}" (Next.js) missing Path attribute`
442
+ );
443
+ }
444
+ }
445
+ }
446
+ }
447
+
448
+ // Check cookies().set() from next/headers
449
+ this.patterns.nextjsCookiesSet.lastIndex = 0;
450
+
451
+ while ((match = this.patterns.nextjsCookiesSet.exec(content)) !== null) {
452
+ const cookieName = match[1];
453
+
454
+ if (this.isSessionCookie(cookieName)) {
455
+ const cookieCallStart = match.index;
456
+ const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
457
+
458
+ if (cookieCallEnd > cookieCallStart) {
459
+ const cookieConfig = content.substring(
460
+ cookieCallStart,
461
+ cookieCallEnd
462
+ );
463
+
464
+ if (!this.hasPathAttribute(cookieConfig)) {
465
+ const lineInfo = this.findLineNumber(content, match.index, lines);
466
+
467
+ this.addViolation(
468
+ lineInfo.line,
469
+ lineInfo.column,
470
+ `Insecure session cookie: Session cookie "${cookieName}" (Next.js headers) missing Path attribute`
471
+ );
472
+ }
473
+ }
474
+ }
475
+ }
476
+
477
+ // Check NextResponse.next().cookies.set() calls
478
+ this.patterns.nextjsSetCookie.lastIndex = 0;
479
+
480
+ while ((match = this.patterns.nextjsSetCookie.exec(content)) !== null) {
481
+ const cookieName = match[1];
482
+
483
+ if (this.isSessionCookie(cookieName)) {
484
+ const cookieCallStart = match.index;
485
+ const cookieCallEnd = this.findMatchingBrace(content, cookieCallStart);
486
+
487
+ if (cookieCallEnd > cookieCallStart) {
488
+ const cookieConfig = content.substring(
489
+ cookieCallStart,
490
+ cookieCallEnd
491
+ );
492
+
493
+ if (!this.hasPathAttribute(cookieConfig)) {
494
+ const lineInfo = this.findLineNumber(content, match.index, lines);
495
+
496
+ this.addViolation(
497
+ lineInfo.line,
498
+ lineInfo.column,
499
+ `Insecure session cookie: Session cookie "${cookieName}" (Next.js Response) missing Path attribute`
500
+ );
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Analyze NextAuth.js configuration for session and CSRF tokens
509
+ */
510
+ analyzeNextAuthConfig(content, lines) {
511
+ // Check sessionToken configuration
512
+ let match;
513
+ this.patterns.nextAuthSessionToken.lastIndex = 0;
514
+
515
+ while (
516
+ (match = this.patterns.nextAuthSessionToken.exec(content)) !== null
517
+ ) {
518
+ const cookieName = match[1];
519
+
520
+ if (this.isSessionCookie(cookieName)) {
521
+ const tokenConfigStart = match.index;
522
+ const tokenConfigEnd = this.findMatchingBrace(
523
+ content,
524
+ tokenConfigStart
525
+ );
526
+
527
+ if (tokenConfigEnd > tokenConfigStart) {
528
+ const tokenConfig = content.substring(
529
+ tokenConfigStart,
530
+ tokenConfigEnd
531
+ );
532
+
533
+ if (!this.hasPathInOptionsConfig(tokenConfig)) {
534
+ const lineInfo = this.findLineNumber(content, match.index, lines);
535
+
536
+ this.addViolation(
537
+ lineInfo.line,
538
+ lineInfo.column,
539
+ `Insecure session cookie: NextAuth sessionToken "${cookieName}" missing Path attribute in options`
540
+ );
541
+ }
542
+ }
543
+ }
544
+ }
545
+
546
+ // Check csrfToken configuration
547
+ this.patterns.nextAuthCsrfToken.lastIndex = 0;
548
+
549
+ while ((match = this.patterns.nextAuthCsrfToken.exec(content)) !== null) {
550
+ const cookieName = match[1];
551
+
552
+ if (this.isSessionCookie(cookieName)) {
553
+ const tokenConfigStart = match.index;
554
+ const tokenConfigEnd = this.findMatchingBrace(
555
+ content,
556
+ tokenConfigStart
557
+ );
558
+
559
+ if (tokenConfigEnd > tokenConfigStart) {
560
+ const tokenConfig = content.substring(
561
+ tokenConfigStart,
562
+ tokenConfigEnd
563
+ );
564
+
565
+ if (!this.hasPathInOptionsConfig(tokenConfig)) {
566
+ const lineInfo = this.findLineNumber(content, match.index, lines);
567
+
568
+ this.addViolation(
569
+ lineInfo.line,
570
+ lineInfo.column,
571
+ `Insecure session cookie: NextAuth csrfToken "${cookieName}" missing Path attribute in options`
572
+ );
573
+ }
574
+ }
575
+ }
576
+ }
577
+
578
+ // Check general NextAuth cookies configuration
579
+ this.patterns.nextAuthCookies.lastIndex = 0;
580
+
581
+ while ((match = this.patterns.nextAuthCookies.exec(content)) !== null) {
582
+ const cookiesConfigStart = match.index;
583
+ const cookiesConfigEnd = this.findMatchingBrace(
584
+ content,
585
+ cookiesConfigStart
586
+ );
587
+
588
+ if (cookiesConfigEnd > cookiesConfigStart) {
589
+ const cookiesConfig = content.substring(
590
+ cookiesConfigStart,
591
+ cookiesConfigEnd
592
+ );
593
+
594
+ // Check if sessionToken is configured without path
595
+ if (
596
+ cookiesConfig.includes("sessionToken") &&
597
+ !this.hasPathInNextAuthConfig(cookiesConfig)
598
+ ) {
599
+ const lineInfo = this.findLineNumber(content, match.index, lines);
600
+
601
+ this.addViolation(
602
+ lineInfo.line,
603
+ lineInfo.column,
604
+ `Insecure session cookie: NextAuth cookies configuration missing Path attribute for session tokens`
605
+ );
606
+ }
607
+ }
608
+ }
609
+ }
610
+
611
+ hasPathAttribute(config) {
612
+ return this.patterns.pathAttribute.test(config);
613
+ }
614
+
615
+ hasPathInSetCookie(headerValue) {
616
+ this.patterns.pathInSetCookie.lastIndex = 0; // Reset global regex
617
+ return this.patterns.pathInSetCookie.test(headerValue);
618
+ }
619
+
620
+ hasPathInCookieConfig(config) {
621
+ // Check for cookie: { path: ... } pattern
622
+ return /cookie\s*:\s*\{[^}]*path\s*:/i.test(config);
623
+ }
624
+
625
+ /**
626
+ * Check if NextAuth options configuration has path attribute
627
+ */
628
+ hasPathInOptionsConfig(config) {
629
+ // Check for options: { path: ... } pattern in NextAuth
630
+ return /options\s*:\s*\{[^}]*path\s*:/i.test(config);
631
+ }
632
+
633
+ /**
634
+ * Check if NextAuth cookies configuration has path attribute
635
+ */
636
+ hasPathInNextAuthConfig(config) {
637
+ // Check for path attribute in NextAuth cookies configuration
638
+ return (
639
+ /path\s*:\s*['"`][^'"`]*['"`]/i.test(config) ||
640
+ /options\s*:\s*\{[^}]*path\s*:/i.test(config)
641
+ );
642
+ }
643
+
644
+ findMatchingBrace(content, startIndex) {
645
+ let braceCount = 0;
646
+ let inString = false;
647
+ let stringChar = "";
648
+
649
+ for (let i = startIndex; i < content.length; i++) {
650
+ const char = content[i];
651
+
652
+ if (!inString) {
653
+ if (char === '"' || char === "'" || char === "`") {
654
+ inString = true;
655
+ stringChar = char;
656
+ } else if (char === "{") {
657
+ braceCount++;
658
+ } else if (char === "}") {
659
+ braceCount--;
660
+ if (braceCount === 0) {
661
+ return i + 1;
662
+ }
663
+ }
664
+ } else {
665
+ if (char === stringChar && content[i - 1] !== "\\") {
666
+ inString = false;
667
+ stringChar = "";
668
+ }
669
+ }
670
+ }
671
+
672
+ return startIndex + 1000; // Fallback
673
+ }
674
+
675
+ isSessionCookie(cookieName) {
676
+ return this.patterns.sessionCookieNames.test(cookieName);
677
+ }
678
+
679
+ findLineNumber(content, position, lines) {
680
+ let currentPos = 0;
681
+
682
+ for (let i = 0; i < lines.length; i++) {
683
+ const lineLength = lines[i].length + 1; // +1 for newline
684
+
685
+ if (currentPos + lineLength > position) {
686
+ return {
687
+ line: i + 1,
688
+ column: position - currentPos + 1,
689
+ };
690
+ }
691
+
692
+ currentPos += lineLength;
693
+ }
694
+
695
+ return { line: lines.length, column: 1 };
696
+ }
697
+
698
+ addViolation(line, column, message) {
699
+ if (process.env.SUNLINT_DEBUG) {
700
+ console.log(
701
+ `🔍 [S035] Regex violation at line ${line}, column ${column}: ${message.substring(
702
+ 0,
703
+ 50
704
+ )}...`
705
+ );
706
+ }
707
+
708
+ this.violations.push({
709
+ ruleId: this.ruleId,
710
+ ruleName: this.ruleName,
711
+ severity: "warning",
712
+ message: message,
713
+ line: line,
714
+ column: column,
715
+ source: "regex-based",
716
+ });
717
+ }
718
+
719
+ cleanup() {
720
+ this.violations = [];
721
+ }
722
+ }
723
+
724
+ module.exports = S035RegexBasedAnalyzer;