@sun-asterisk/sunlint 1.3.7 → 1.3.8

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 (38) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/config/defaults/default.json +2 -1
  3. package/config/rule-analysis-strategies.js +20 -0
  4. package/config/rules/enhanced-rules-registry.json +190 -35
  5. package/core/file-targeting-service.js +83 -7
  6. package/package.json +1 -1
  7. package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
  8. package/rules/common/C065_one_behavior_per_test/config.json +95 -0
  9. package/rules/security/S037_cache_headers/README.md +128 -0
  10. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  11. package/rules/security/S037_cache_headers/config.json +50 -0
  12. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  13. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  14. package/rules/security/S038_no_version_headers/README.md +234 -0
  15. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  16. package/rules/security/S038_no_version_headers/config.json +49 -0
  17. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  18. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  19. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  20. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  21. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  22. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  23. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
  24. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  25. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  26. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  27. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  28. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  29. package/rules/security/S051_password_length_policy/config.json +83 -0
  30. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  31. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  32. package/rules/security/S054_no_default_accounts/README.md +129 -0
  33. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  34. package/rules/security/S054_no_default_accounts/config.json +101 -0
  35. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  36. package/rules/security/S056_log_injection_protection/config.json +148 -0
  37. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  38. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
@@ -0,0 +1,463 @@
1
+ /**
2
+ * S037 Regex-Based Analyzer - Configure comprehensive cache headers
3
+ * Refactored for per-route evaluation with framework support.
4
+ */
5
+ const fs = require("fs");
6
+
7
+ class S037RegexBasedAnalyzer {
8
+ constructor() {
9
+ this.ruleId = "S037";
10
+
11
+ // Framework-specific route patterns
12
+ this.routePatterns = {
13
+ // Express.js
14
+ express:
15
+ /\b(app|router)\.(get|post|put|delete|patch|use)\s*\(\s*['"`][^'"`]+['"`]/,
16
+ // Next.js API routes
17
+ nextjs: /export\s+(default\s+)?async?\s+function\s+handler\s*\(/,
18
+ // Next.js 13+ App Router
19
+ nextjsApp:
20
+ /export\s+async?\s+function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/,
21
+ // NestJS controllers
22
+ nestjs: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*['"`]?\s*\)/,
23
+ // Nuxt.js server routes
24
+ nuxtjs:
25
+ /export\s+(default\s+|const\s+(GET|POST|PUT|DELETE|PATCH)\s*=\s*)?defineEventHandler\s*\(/,
26
+ };
27
+
28
+ // Header detection patterns
29
+ this.cacheControlPattern =
30
+ /res\.set(Header|)\s*\(\s*['"`]Cache-Control['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
31
+ this.pragmaPattern =
32
+ /res\.set(Header|)\s*\(\s*['"`]Pragma['"`]\s*,\s*['"`]no-cache['"`]/i;
33
+ this.expiresPattern =
34
+ /res\.set(Header|)\s*\(\s*['"`]Expires['"`]\s*,\s*['"`](0|Thu,? 01 Jan 1970[^'"`]*)['"`]/i;
35
+ this.bulkSetPattern = /res\.set\s*\(\s*\{([^}]+)\}/i;
36
+
37
+ // Next.js specific patterns
38
+ this.nextHeaderPattern =
39
+ /res\.setHeader\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
40
+ this.nextHeadersPattern =
41
+ /\w+\.headers\s*\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
42
+
43
+ this.requiredDirectives = ["no-store", "no-cache", "must-revalidate"];
44
+ this.sensitiveIndicators =
45
+ /\b(session|auth(?:enticate|orization)?|token|jwt|csrf|login|logout|banking|crypto|salary|ssn|social[-_]?security|medical|prescription|biometric|audit|tax|legal|contract|personal[-_]?data|identity|finance|wallet|private[-_]?key|secret|sensitive[-_]?data|confidential|admin[-_]?panel|payment[-_]?process|credit[-_]?card|oauth|password|reset)\b/i;
46
+ }
47
+
48
+ async analyze(filePath) {
49
+ // Skip files that are unlikely to be route handlers
50
+ const skipPatterns = [
51
+ /\.dto\.ts$/,
52
+ /\.interface\.ts$/,
53
+ /\.module\.ts$/,
54
+ /\.service\.spec\.ts$/,
55
+ /\.controller\.spec\.ts$/,
56
+ /\.spec\.ts$/,
57
+ /\.test\.ts$/,
58
+ /\.d\.ts$/,
59
+ /\.types\.ts$/,
60
+ /\.constants?\.ts$/,
61
+ /\.config\.ts$/,
62
+ ];
63
+
64
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
65
+ if (shouldSkip) {
66
+ return [];
67
+ }
68
+
69
+ const content = fs.readFileSync(filePath, "utf8");
70
+ const lines = content.split(/\r?\n/);
71
+ const violations = [];
72
+
73
+ let inRoute = false;
74
+ let braceDepth = 0;
75
+ let routeStartLine = 0;
76
+ let routeType = "";
77
+ let ccRaw = "";
78
+ let hasCC = false;
79
+ let hasPragma = false;
80
+ let hasExpires = false;
81
+ let sawSensitive = false;
82
+ let secureMiddlewareImplied = false;
83
+ let inBulkSet = false;
84
+ let bulkSetContent = "";
85
+
86
+ const reset = () => {
87
+ inRoute = false;
88
+ braceDepth = 0;
89
+ routeStartLine = 0;
90
+ routeType = "";
91
+ ccRaw = "";
92
+ hasCC =
93
+ hasPragma =
94
+ hasExpires =
95
+ sawSensitive =
96
+ secureMiddlewareImplied =
97
+ inBulkSet =
98
+ false;
99
+ bulkSetContent = "";
100
+ };
101
+
102
+ const evaluate = () => {
103
+ if (!routeStartLine) return;
104
+
105
+ // Only evaluate if route appears sensitive OR we actually saw headers being set
106
+ const touchedHeaders = hasCC || hasPragma || hasExpires;
107
+ if (!(sawSensitive || touchedHeaders || secureMiddlewareImplied)) return;
108
+
109
+ if (secureMiddlewareImplied) return; // assume middleware sets all headers
110
+
111
+ const missing = [];
112
+ if (!hasCC) missing.push("Cache-Control");
113
+ if (!hasPragma) missing.push("Pragma");
114
+ if (!hasExpires) missing.push("Expires");
115
+
116
+ if (missing.length) {
117
+ violations.push({
118
+ ruleId: this.ruleId,
119
+ message: `Handler missing anti-cache headers: ${missing.join(
120
+ ", "
121
+ )} (${routeType})`,
122
+ severity: "warning",
123
+ line: routeStartLine,
124
+ column: 1,
125
+ });
126
+ } else if (hasCC) {
127
+ const lower = ccRaw.toLowerCase();
128
+ const missingDir = this.requiredDirectives.filter(
129
+ (d) => !lower.includes(d)
130
+ );
131
+ if (missingDir.length) {
132
+ violations.push({
133
+ ruleId: this.ruleId,
134
+ message: `Cache-Control missing directives: ${missingDir.join(
135
+ ", "
136
+ )} (${routeType})`,
137
+ severity: "warning",
138
+ line: routeStartLine,
139
+ column: 1,
140
+ });
141
+ }
142
+ }
143
+ };
144
+
145
+ for (let i = 0; i < lines.length; i++) {
146
+ const line = lines[i];
147
+
148
+ // Detect route start for different frameworks
149
+ if (!inRoute) {
150
+ for (const [framework, pattern] of Object.entries(this.routePatterns)) {
151
+ if (pattern.test(line)) {
152
+ if (process.env.SUNLINT_DEBUG)
153
+ console.log(
154
+ `🔧 [S037-Regex] Found ${framework} route at line ${
155
+ i + 1
156
+ }: ${line.trim()}`
157
+ );
158
+ inRoute = true;
159
+ routeStartLine = i + 1;
160
+ routeType = framework;
161
+ braceDepth =
162
+ (line.match(/\{/g) || []).length -
163
+ (line.match(/\}/g) || []).length;
164
+ secureMiddlewareImplied = /secureNoCache|noCache|antiCache/.test(
165
+ line
166
+ );
167
+ if (secureMiddlewareImplied && process.env.SUNLINT_DEBUG) {
168
+ console.log(
169
+ `🔧 [S037-Regex] Secure middleware detected, skipping evaluation`
170
+ );
171
+ }
172
+
173
+ // If no opening brace on this line, look ahead for it
174
+ if (braceDepth === 0) {
175
+ for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
176
+ const nextLine = lines[j];
177
+ if (nextLine.includes("{")) {
178
+ braceDepth =
179
+ (nextLine.match(/\{/g) || []).length -
180
+ (nextLine.match(/\}/g) || []).length;
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ break;
186
+ }
187
+ }
188
+ continue;
189
+ }
190
+
191
+ // Track braces for route scope
192
+ for (const ch of line) {
193
+ if (ch === "{") braceDepth++;
194
+ else if (ch === "}") braceDepth--;
195
+ }
196
+
197
+ // Check for sensitive indicators
198
+ if (this.sensitiveIndicators.test(line)) sawSensitive = true;
199
+
200
+ // Detect headers based on framework type
201
+ if (routeType === "express") {
202
+ // Check for start of bulk set
203
+ if (/res\.set\s*\(\s*\{/.test(line)) {
204
+ inBulkSet = true;
205
+ bulkSetContent = line;
206
+ if (process.env.SUNLINT_DEBUG)
207
+ console.log(`🔧 [S037-Regex] Starting bulk set detection`);
208
+ } else if (inBulkSet) {
209
+ bulkSetContent += line;
210
+ if (line.includes("}")) {
211
+ inBulkSet = false;
212
+ // Parse accumulated bulk set content
213
+ const cc =
214
+ /['"`]Cache-Control['"`]\s*:\s*['"`]([^'"`]+)['"`]/i.exec(
215
+ bulkSetContent
216
+ );
217
+ if (cc) {
218
+ hasCC = true;
219
+ ccRaw = cc[1];
220
+ if (process.env.SUNLINT_DEBUG)
221
+ console.log(
222
+ `🔧 [S037-Regex] Found bulk Cache-Control: ${ccRaw}`
223
+ );
224
+ }
225
+ if (
226
+ /(['"`]?Pragma['"`]?)\s*:\s*['"`]no-cache['"`]/i.test(
227
+ bulkSetContent
228
+ )
229
+ ) {
230
+ hasPragma = true;
231
+ if (process.env.SUNLINT_DEBUG)
232
+ console.log(`🔧 [S037-Regex] Found bulk Pragma`);
233
+ }
234
+ if (
235
+ /(['"`]?Expires['"`]?)\s*:\s*['"`](0|Thu, 01 Jan 1970)/i.test(
236
+ bulkSetContent
237
+ )
238
+ ) {
239
+ hasExpires = true;
240
+ if (process.env.SUNLINT_DEBUG)
241
+ console.log(`🔧 [S037-Regex] Found bulk Expires`);
242
+ }
243
+ bulkSetContent = "";
244
+ }
245
+ }
246
+
247
+ // Express.js patterns
248
+ const bulk = this.bulkSetPattern.exec(line);
249
+ if (bulk) {
250
+ const body = bulk[1];
251
+ const cc = /['"`]Cache-Control['"`]\s*:\s*['"`]([^'"`]+)['"`]/i.exec(
252
+ body
253
+ );
254
+ if (cc) {
255
+ hasCC = true;
256
+ ccRaw = cc[1];
257
+ }
258
+ if (/['"`]Pragma['"`]\s*:\s*['"`]no-cache['"`]/i.test(body))
259
+ hasPragma = true;
260
+ if (/['"`]Expires['"`]\s*:\s*['"`](0|Thu, 01 Jan 1970)/i.test(body))
261
+ hasExpires = true;
262
+ }
263
+
264
+ const ccM = this.cacheControlPattern.exec(line);
265
+ if (ccM) {
266
+ hasCC = true;
267
+ ccRaw = ccM[2];
268
+ if (process.env.SUNLINT_DEBUG)
269
+ console.log(`🔧 [S037-Regex] Found Cache-Control: ${ccRaw}`);
270
+ }
271
+ if (this.pragmaPattern.test(line)) {
272
+ hasPragma = true;
273
+ if (process.env.SUNLINT_DEBUG)
274
+ console.log(`🔧 [S037-Regex] Found Pragma`);
275
+ }
276
+ if (this.expiresPattern.test(line)) {
277
+ hasExpires = true;
278
+ if (process.env.SUNLINT_DEBUG)
279
+ console.log(`🔧 [S037-Regex] Found Expires`);
280
+ }
281
+ } else if (routeType === "nestjs") {
282
+ // NestJS patterns - res.header()
283
+ const nestHeaderPattern =
284
+ /res\.header\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
285
+ const headerM = nestHeaderPattern.exec(line);
286
+ if (headerM) {
287
+ const [, headerName, headerValue] = headerM;
288
+ if (headerName.toLowerCase() === "cache-control") {
289
+ hasCC = true;
290
+ ccRaw = headerValue;
291
+ if (process.env.SUNLINT_DEBUG)
292
+ console.log(
293
+ `🔧 [S037-Regex] Found NestJS Cache-Control: ${ccRaw}`
294
+ );
295
+ } else if (
296
+ headerName.toLowerCase() === "pragma" &&
297
+ headerValue.toLowerCase() === "no-cache"
298
+ ) {
299
+ hasPragma = true;
300
+ if (process.env.SUNLINT_DEBUG)
301
+ console.log(`🔧 [S037-Regex] Found NestJS Pragma`);
302
+ } else if (
303
+ headerName.toLowerCase() === "expires" &&
304
+ (headerValue === "0" || headerValue.includes("1970"))
305
+ ) {
306
+ hasExpires = true;
307
+ if (process.env.SUNLINT_DEBUG)
308
+ console.log(`🔧 [S037-Regex] Found NestJS Expires`);
309
+ }
310
+ }
311
+ } else if (routeType === "nextjs" || routeType === "nextjsApp") {
312
+ // Next.js patterns - support both res.setHeader and response.headers.set
313
+ const headerM = this.nextHeaderPattern.exec(line);
314
+ if (headerM) {
315
+ const [, headerName, headerValue] = headerM;
316
+ if (headerName.toLowerCase() === "cache-control") {
317
+ hasCC = true;
318
+ ccRaw = headerValue;
319
+ } else if (
320
+ headerName.toLowerCase() === "pragma" &&
321
+ headerValue.toLowerCase() === "no-cache"
322
+ ) {
323
+ hasPragma = true;
324
+ } else if (
325
+ headerName.toLowerCase() === "expires" &&
326
+ (headerValue === "0" || headerValue.includes("1970"))
327
+ ) {
328
+ hasExpires = true;
329
+ }
330
+ }
331
+
332
+ const headersM = this.nextHeadersPattern.exec(line);
333
+ if (headersM) {
334
+ const [, headerName, headerValue] = headersM;
335
+ if (headerName.toLowerCase() === "cache-control") {
336
+ hasCC = true;
337
+ ccRaw = headerValue;
338
+ if (process.env.SUNLINT_DEBUG)
339
+ console.log(
340
+ `🔧 [S037-Regex] Found Next.js headers.set Cache-Control: ${ccRaw}`
341
+ );
342
+ } else if (
343
+ headerName.toLowerCase() === "pragma" &&
344
+ headerValue.toLowerCase() === "no-cache"
345
+ ) {
346
+ hasPragma = true;
347
+ if (process.env.SUNLINT_DEBUG)
348
+ console.log(`🔧 [S037-Regex] Found Next.js headers.set Pragma`);
349
+ } else if (
350
+ headerName.toLowerCase() === "expires" &&
351
+ (headerValue === "0" || headerValue.includes("1970"))
352
+ ) {
353
+ hasExpires = true;
354
+ if (process.env.SUNLINT_DEBUG)
355
+ console.log(`🔧 [S037-Regex] Found Next.js headers.set Expires`);
356
+ }
357
+ }
358
+ } else if (routeType === "nuxtjs") {
359
+ // Nuxt.js patterns - setHeader(event, name, value)
360
+ const nuxtHeaderPattern =
361
+ /setHeader\s*\(\s*event\s*,\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
362
+ const nuxtHeaderM = nuxtHeaderPattern.exec(line);
363
+ if (nuxtHeaderM) {
364
+ const [, headerName, headerValue] = nuxtHeaderM;
365
+ if (headerName.toLowerCase() === "cache-control") {
366
+ hasCC = true;
367
+ ccRaw = headerValue;
368
+ if (process.env.SUNLINT_DEBUG)
369
+ console.log(
370
+ `🔧 [S037-Regex] Found Nuxt.js setHeader Cache-Control: ${ccRaw}`
371
+ );
372
+ } else if (
373
+ headerName.toLowerCase() === "pragma" &&
374
+ headerValue.toLowerCase() === "no-cache"
375
+ ) {
376
+ hasPragma = true;
377
+ if (process.env.SUNLINT_DEBUG)
378
+ console.log(`🔧 [S037-Regex] Found Nuxt.js setHeader Pragma`);
379
+ } else if (
380
+ headerName.toLowerCase() === "expires" &&
381
+ (headerValue === "0" || headerValue.includes("1970"))
382
+ ) {
383
+ hasExpires = true;
384
+ if (process.env.SUNLINT_DEBUG)
385
+ console.log(`🔧 [S037-Regex] Found Nuxt.js setHeader Expires`);
386
+ }
387
+ }
388
+
389
+ // Next.js response constructor with headers object
390
+ if (/headers:\s*\{/.test(line)) {
391
+ let j = i;
392
+ while (j < Math.min(i + 10, lines.length)) {
393
+ const headerLine = lines[j];
394
+ if (
395
+ /(['"`]?Cache-Control['"`]?)\s*:\s*['"`]([^'"`]+)['"`]/.test(
396
+ headerLine
397
+ )
398
+ ) {
399
+ hasCC = true;
400
+ ccRaw = RegExp.$2;
401
+ if (process.env.SUNLINT_DEBUG)
402
+ console.log(
403
+ `🔧 [S037-Regex] Found Next.js constructor Cache-Control: ${ccRaw}`
404
+ );
405
+ }
406
+ if (
407
+ /(['"`]?Pragma['"`]?)\s*:\s*['"`]no-cache['"`]/.test(headerLine)
408
+ ) {
409
+ hasPragma = true;
410
+ if (process.env.SUNLINT_DEBUG)
411
+ console.log(`🔧 [S037-Regex] Found Next.js constructor Pragma`);
412
+ }
413
+ if (
414
+ /(['"`]?Expires['"`]?)\s*:\s*['"`](0|Thu.*1970)['"`]/.test(
415
+ headerLine
416
+ )
417
+ ) {
418
+ hasExpires = true;
419
+ if (process.env.SUNLINT_DEBUG)
420
+ console.log(
421
+ `🔧 [S037-Regex] Found Next.js constructor Expires`
422
+ );
423
+ }
424
+ if (headerLine.includes("}")) break;
425
+ j++;
426
+ }
427
+ }
428
+ } else if (routeType === "nestjs") {
429
+ // NestJS patterns (similar to Express but with decorators)
430
+ if (/res\.header\s*\(\s*['"`]Cache-Control['"`]/.test(line))
431
+ hasCC = true;
432
+ if (/res\.header\s*\(\s*['"`]Pragma['"`]/.test(line)) hasPragma = true;
433
+ if (/res\.header\s*\(\s*['"`]Expires['"`]/.test(line))
434
+ hasExpires = true;
435
+ }
436
+
437
+ // End of route detection
438
+ if (
439
+ braceDepth <= 0 &&
440
+ (/\)\s*;?\s*$/.test(line) ||
441
+ /^\s*\}\s*$/.test(line) ||
442
+ /^export/.test(line))
443
+ ) {
444
+ if (process.env.SUNLINT_DEBUG) {
445
+ console.log(
446
+ `🔧 [S037-Regex] Route ended, evaluating: CC=${hasCC}, Pragma=${hasPragma}, Expires=${hasExpires}, Sensitive=${sawSensitive}`
447
+ );
448
+ }
449
+ evaluate();
450
+ reset();
451
+ }
452
+ }
453
+
454
+ // Safety evaluate if unbalanced at file end
455
+ if (inRoute) evaluate();
456
+
457
+ return violations;
458
+ }
459
+
460
+ cleanup() {}
461
+ }
462
+
463
+ module.exports = S037RegexBasedAnalyzer;