@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,546 @@
1
+ /**
2
+ * S037 Symbol-Based Analyzer - Configure comprehensive cache headers
3
+ * Enhanced to analyze per route handler instead of per file
4
+ */
5
+
6
+ class S037SymbolBasedAnalyzer {
7
+ constructor(semanticEngine) {
8
+ this.ruleId = "S037";
9
+ this.semanticEngine = semanticEngine;
10
+ this.requiredDirectives = ["no-store", "no-cache", "must-revalidate"];
11
+ this.sensitiveIndicators =
12
+ /\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;
13
+ }
14
+
15
+ async initialize() {}
16
+
17
+ analyze(sourceFile, filePath) {
18
+ const violations = [];
19
+
20
+ // Skip files that are unlikely to be route handlers
21
+ const skipPatterns = [
22
+ /\.dto\.ts$/,
23
+ /\.interface\.ts$/,
24
+ /\.module\.ts$/,
25
+ /\.service\.spec\.ts$/,
26
+ /\.controller\.spec\.ts$/,
27
+ /\.spec\.ts$/,
28
+ /\.test\.ts$/,
29
+ /\.d\.ts$/,
30
+ /\.types\.ts$/,
31
+ /\.constants?.ts$/,
32
+ /\.config\.ts$/,
33
+ ];
34
+
35
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
36
+ if (shouldSkip) {
37
+ return violations;
38
+ }
39
+
40
+ try {
41
+ const { SyntaxKind } = require("ts-morph");
42
+
43
+ // Find all function expressions and arrow functions that could be route handlers
44
+ const routeHandlers = [];
45
+
46
+ // Express route patterns: app.get("/path", (req, res) => {...})
47
+ const callExpressions = sourceFile.getDescendantsOfKind(
48
+ SyntaxKind.CallExpression
49
+ );
50
+
51
+ for (const call of callExpressions) {
52
+ const expression = call.getExpression();
53
+
54
+ // Check for Express route methods
55
+ if (/\.(get|post|put|delete|patch|use)$/.test(expression.getText())) {
56
+ const args = call.getArguments();
57
+ if (args.length >= 2) {
58
+ const lastArg = args[args.length - 1];
59
+ // The last argument should be the handler function
60
+ if (
61
+ lastArg.getKind() === SyntaxKind.ArrowFunction ||
62
+ lastArg.getKind() === SyntaxKind.FunctionExpression
63
+ ) {
64
+ routeHandlers.push({
65
+ handler: lastArg,
66
+ routeCall: call,
67
+ type: "express",
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // Next.js export functions
75
+ const exportAssignments = sourceFile.getDescendantsOfKind(
76
+ SyntaxKind.ExportAssignment
77
+ );
78
+ const exportDeclarations = sourceFile.getDescendantsOfKind(
79
+ SyntaxKind.ExportDeclaration
80
+ );
81
+ const functionDeclarations = sourceFile.getDescendantsOfKind(
82
+ SyntaxKind.FunctionDeclaration
83
+ );
84
+
85
+ for (const func of functionDeclarations) {
86
+ const name = func.getName();
87
+ if (name && /^(GET|POST|PUT|DELETE|PATCH|handler)$/.test(name)) {
88
+ routeHandlers.push({
89
+ handler: func,
90
+ type: "nextjs",
91
+ });
92
+ }
93
+ }
94
+
95
+ // NestJS Controller methods with decorators
96
+ const methodDeclarations = sourceFile.getDescendantsOfKind(
97
+ SyntaxKind.MethodDeclaration
98
+ );
99
+
100
+ for (const method of methodDeclarations) {
101
+ const decorators = method.getDecorators();
102
+ const hasRouteDecorator = decorators.some((d) => {
103
+ const decoratorName = d.getName();
104
+ return ["Get", "Post", "Put", "Delete", "Patch"].includes(
105
+ decoratorName
106
+ );
107
+ });
108
+
109
+ if (hasRouteDecorator) {
110
+ routeHandlers.push({
111
+ handler: method,
112
+ type: "nestjs",
113
+ });
114
+ }
115
+ }
116
+
117
+ // Nuxt.js defineEventHandler patterns
118
+ for (const call of callExpressions) {
119
+ const expression = call.getExpression();
120
+ if (expression.getKind() === SyntaxKind.Identifier) {
121
+ const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
122
+ if (identifier.getText() === "defineEventHandler") {
123
+ // Find the arrow function or function parameter
124
+ const args = call.getArguments();
125
+ if (args.length > 0) {
126
+ const firstArg = args[0];
127
+ if (
128
+ firstArg.getKind() === SyntaxKind.ArrowFunction ||
129
+ firstArg.getKind() === SyntaxKind.FunctionExpression
130
+ ) {
131
+ routeHandlers.push({
132
+ handler: firstArg,
133
+ type: "nuxtjs",
134
+ });
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+
141
+ // Nuxt.js named exports (GET, POST, etc.)
142
+ const nuxtExportAssignments = sourceFile.getDescendantsOfKind(
143
+ SyntaxKind.ExportAssignment
144
+ );
145
+
146
+ for (const exportAssign of nuxtExportAssignments) {
147
+ const expr = exportAssign.getExpression();
148
+ if (expr && expr.getKind() === SyntaxKind.CallExpression) {
149
+ const callExpr = expr.asKindOrThrow(SyntaxKind.CallExpression);
150
+ const callExpression = callExpr.getExpression();
151
+ if (callExpression.getKind() === SyntaxKind.Identifier) {
152
+ const identifier = callExpression.asKindOrThrow(
153
+ SyntaxKind.Identifier
154
+ );
155
+ if (identifier.getText() === "defineEventHandler") {
156
+ const args = callExpr.getArguments();
157
+ if (args.length > 0) {
158
+ const firstArg = args[0];
159
+ if (
160
+ firstArg.getKind() === SyntaxKind.ArrowFunction ||
161
+ firstArg.getKind() === SyntaxKind.FunctionExpression
162
+ ) {
163
+ routeHandlers.push({
164
+ handler: firstArg,
165
+ type: "nuxtjs",
166
+ });
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ const variableDeclarations = sourceFile.getDescendantsOfKind(
175
+ SyntaxKind.VariableDeclaration
176
+ );
177
+
178
+ for (const varDecl of variableDeclarations) {
179
+ const name = varDecl.getName();
180
+ if (name && /^(GET|POST|PUT|DELETE|PATCH)$/.test(name)) {
181
+ const initializer = varDecl.getInitializer();
182
+ if (
183
+ initializer &&
184
+ initializer.getKind() === SyntaxKind.CallExpression
185
+ ) {
186
+ const callExpr = initializer.asKindOrThrow(
187
+ SyntaxKind.CallExpression
188
+ );
189
+ const callExpression = callExpr.getExpression();
190
+ if (callExpression.getKind() === SyntaxKind.Identifier) {
191
+ const identifier = callExpression.asKindOrThrow(
192
+ SyntaxKind.Identifier
193
+ );
194
+ if (identifier.getText() === "defineEventHandler") {
195
+ const args = callExpr.getArguments();
196
+ if (args.length > 0) {
197
+ const firstArg = args[0];
198
+ if (
199
+ firstArg.getKind() === SyntaxKind.ArrowFunction ||
200
+ firstArg.getKind() === SyntaxKind.FunctionExpression
201
+ ) {
202
+ routeHandlers.push({
203
+ handler: firstArg,
204
+ type: "nuxtjs",
205
+ });
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ // Analyze each route handler
215
+ if (process.env.SUNLINT_DEBUG)
216
+ console.log(
217
+ `🔧 [S037-Symbol] Found ${routeHandlers.length} route handlers`
218
+ );
219
+
220
+ for (const route of routeHandlers) {
221
+ const handlerViolations = this.analyzeRouteHandler(route);
222
+ violations.push(...handlerViolations);
223
+ }
224
+
225
+ // Only analyze actual route handlers - no file-level checking
226
+ // File-level checking removed to prevent false positives on DTO, module, service files
227
+ } catch (err) {
228
+ // Fallback - don't report errors from symbol analysis
229
+ console.warn(`Symbol analysis failed for ${filePath}:`, err.message);
230
+ }
231
+
232
+ return violations;
233
+ }
234
+
235
+ analyzeRouteHandler(route) {
236
+ const violations = [];
237
+ const { handler, routeCall, type } = route;
238
+
239
+ // Check if route contains sensitive indicators
240
+ const handlerText = handler.getFullText();
241
+ const routeText = routeCall ? routeCall.getFullText() : handlerText;
242
+
243
+ // Check for secure middleware (skip analysis if present)
244
+ if (/secureNoCache|noCache|antiCache/.test(routeText)) {
245
+ return violations; // Middleware handles headers
246
+ }
247
+
248
+ const isSensitive =
249
+ this.sensitiveIndicators.test(handlerText) ||
250
+ this.sensitiveIndicators.test(routeText);
251
+
252
+ // Find header setting calls within this handler
253
+ const headerState = this.findHeadersInNode(handler);
254
+
255
+ // Only analyze if route is sensitive OR headers are being set
256
+ const touchedHeaders =
257
+ headerState.hasCC || headerState.hasPragma || headerState.hasExpires;
258
+ if (!isSensitive && !touchedHeaders) {
259
+ return violations; // Skip non-sensitive routes without headers
260
+ }
261
+
262
+ // Check for missing headers
263
+ const missing = [];
264
+ if (!headerState.hasCC) missing.push("Cache-Control");
265
+ if (!headerState.hasPragma) missing.push("Pragma");
266
+ if (!headerState.hasExpires) missing.push("Expires");
267
+
268
+ if (missing.length > 0) {
269
+ violations.push({
270
+ ruleId: this.ruleId,
271
+ message: `Handler missing anti-cache headers: ${missing.join(
272
+ ", "
273
+ )} (${type})`,
274
+ severity: "warning",
275
+ line: handler.getStartLineNumber(),
276
+ column: 1,
277
+ });
278
+ } else {
279
+ // Validate Cache-Control directives
280
+ if (headerState.ccValues.length > 0) {
281
+ const combined = headerState.ccValues.join(",").toLowerCase();
282
+ const missingDir = this.requiredDirectives.filter(
283
+ (d) => !combined.includes(d)
284
+ );
285
+ if (missingDir.length > 0) {
286
+ violations.push({
287
+ ruleId: this.ruleId,
288
+ message: `Cache-Control missing directives: ${missingDir.join(
289
+ ", "
290
+ )} (${type})`,
291
+ severity: "warning",
292
+ line: handler.getStartLineNumber(),
293
+ column: 1,
294
+ });
295
+ }
296
+ }
297
+ }
298
+
299
+ return violations;
300
+ }
301
+
302
+ findHeadersInNode(node) {
303
+ const { SyntaxKind } = require("ts-morph");
304
+ const headerState = {
305
+ hasCC: false,
306
+ hasPragma: false,
307
+ hasExpires: false,
308
+ ccValues: [],
309
+ expiresValue: null,
310
+ };
311
+
312
+ // Find all call expressions within this node
313
+ const calls = node.getDescendantsOfKind(SyntaxKind.CallExpression);
314
+ const newExpressions = node.getDescendantsOfKind(SyntaxKind.NewExpression);
315
+
316
+ // Check for Next.js new Response() constructor with headers
317
+ for (const newExpr of newExpressions) {
318
+ const identifier = newExpr.getExpression();
319
+ if (identifier.getText() === "Response") {
320
+ const args = newExpr.getArguments();
321
+ if (args.length >= 2) {
322
+ const optionsArg = args[1];
323
+ if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
324
+ const headersProp = optionsArg.getProperty("headers");
325
+ if (
326
+ headersProp &&
327
+ headersProp.getKind() === SyntaxKind.PropertyAssignment
328
+ ) {
329
+ const headersValue = headersProp.getInitializer();
330
+ if (
331
+ headersValue &&
332
+ headersValue.getKind() === SyntaxKind.ObjectLiteralExpression
333
+ ) {
334
+ this.parseHeadersObject(headersValue, headerState);
335
+ }
336
+ }
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ for (const call of calls) {
343
+ const expression = call.getExpression();
344
+ const methodName = expression.getText();
345
+
346
+ // Look for header setting methods: Express (res.set, res.setHeader), NestJS (res.header)
347
+ if (
348
+ /\.set(Header)?$/.test(methodName) ||
349
+ /setHeader$/.test(methodName) ||
350
+ /\.set$/.test(methodName) ||
351
+ /\.header$/.test(methodName)
352
+ ) {
353
+ const args = call.getArguments();
354
+ if (args.length >= 2) {
355
+ const headerArg = args[0];
356
+ const valueArg = args[1];
357
+ const headerName = headerArg.getText().replace(/['"`]/g, "");
358
+ const headerValue = valueArg.getText().replace(/['"`]/g, "");
359
+
360
+ if (headerName.toLowerCase() === "cache-control") {
361
+ headerState.hasCC = true;
362
+ headerState.ccValues.push(headerValue);
363
+ } else if (headerName.toLowerCase() === "pragma") {
364
+ headerState.hasPragma = true;
365
+ } else if (headerName.toLowerCase() === "expires") {
366
+ headerState.hasExpires = this.isValidExpires(headerValue);
367
+ headerState.expiresValue = headerValue;
368
+ }
369
+ }
370
+ }
371
+
372
+ // Check for Nuxt.js setHeader(event, name, value) pattern
373
+ if (methodName === "setHeader") {
374
+ const args = call.getArguments();
375
+ if (args.length >= 3) {
376
+ // First arg should be 'event', second is header name, third is value
377
+ const eventArg = args[0];
378
+ const headerArg = args[1];
379
+ const valueArg = args[2];
380
+
381
+ if (eventArg.getText() === "event") {
382
+ const headerName = headerArg.getText().replace(/['"`]/g, "");
383
+ const headerValue = valueArg.getText().replace(/['"`]/g, "");
384
+
385
+ if (headerName.toLowerCase() === "cache-control") {
386
+ headerState.hasCC = true;
387
+ headerState.ccValues.push(headerValue);
388
+ } else if (headerName.toLowerCase() === "pragma") {
389
+ headerState.hasPragma = true;
390
+ } else if (headerName.toLowerCase() === "expires") {
391
+ headerState.hasExpires = this.isValidExpires(headerValue);
392
+ headerState.expiresValue = headerValue;
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ // Check for Next.js Response constructor with headers object
399
+ if (
400
+ call.getExpression().getText() === "Response" ||
401
+ call.getExpression().getKind() === SyntaxKind.NewExpression
402
+ ) {
403
+ const args = call.getArguments();
404
+ if (args.length >= 2) {
405
+ const optionsArg = args[1];
406
+ if (optionsArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
407
+ const headersProp = optionsArg.getProperty("headers");
408
+ if (
409
+ headersProp &&
410
+ headersProp.getKind() === SyntaxKind.PropertyAssignment
411
+ ) {
412
+ const headersValue = headersProp.getInitializer();
413
+ if (
414
+ headersValue &&
415
+ headersValue.getKind() === SyntaxKind.ObjectLiteralExpression
416
+ ) {
417
+ this.parseHeadersObject(headersValue, headerState);
418
+ }
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ // Check for bulk header setting: res.set({...})
425
+ if (/\.set$/.test(methodName)) {
426
+ const args = call.getArguments();
427
+ if (args.length === 1) {
428
+ const objArg = args[0];
429
+ if (objArg.getKind() === SyntaxKind.ObjectLiteralExpression) {
430
+ const props = objArg.getProperties();
431
+ for (const prop of props) {
432
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
433
+ const name = prop.getName().replace(/['"`]/g, "").toLowerCase();
434
+ const value =
435
+ prop.getInitializer()?.getText().replace(/['"`]/g, "") || "";
436
+
437
+ if (name === "cache-control") {
438
+ headerState.hasCC = true;
439
+ headerState.ccValues.push(value);
440
+ } else if (name === "pragma") {
441
+ headerState.hasPragma = true;
442
+ } else if (name === "expires") {
443
+ headerState.hasExpires = true;
444
+ }
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ return headerState;
453
+ }
454
+
455
+ isValidExpires(expiresValue) {
456
+ // Remove quotes and trim
457
+ const value = expiresValue.replace(/['"`]/g, "").trim();
458
+
459
+ // Valid immediate expiry values
460
+ if (value === "0" || value === "-1") {
461
+ return true;
462
+ }
463
+
464
+ // Check for past dates (Thu, 01 Jan 1970...)
465
+ if (value.includes("1970") || value.includes("Jan 1970")) {
466
+ return true;
467
+ }
468
+
469
+ // Check for invalid formats
470
+ if (
471
+ value === "never" ||
472
+ value === "invalid-date" ||
473
+ value === "invalid-date-format"
474
+ ) {
475
+ return false;
476
+ }
477
+
478
+ // Try parsing as date to check if it's a future date
479
+ try {
480
+ const date = new Date(value);
481
+ if (isNaN(date.getTime())) {
482
+ return false; // Invalid date format
483
+ }
484
+
485
+ // If it's a future date (after current time), it's invalid for cache prevention
486
+ const now = new Date();
487
+ if (date > now) {
488
+ return false; // Future dates allow caching
489
+ }
490
+
491
+ return true; // Past date is valid
492
+ } catch (e) {
493
+ return false; // Invalid format
494
+ }
495
+ }
496
+
497
+ parseHeadersObject(headersObject, headerState) {
498
+ const { SyntaxKind } = require("ts-morph");
499
+ const props = headersObject.getProperties();
500
+
501
+ for (const prop of props) {
502
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
503
+ const name = prop.getName().replace(/['"`]/g, "").toLowerCase();
504
+ const value =
505
+ prop.getInitializer()?.getText().replace(/['"`]/g, "") || "";
506
+
507
+ if (name === "cache-control") {
508
+ headerState.hasCC = true;
509
+ headerState.ccValues.push(value);
510
+ } else if (name === "pragma") {
511
+ headerState.hasPragma = true;
512
+ } else if (name === "expires") {
513
+ headerState.hasExpires = true;
514
+ }
515
+ }
516
+ }
517
+ }
518
+
519
+ analyzeFileLevel(sourceFile) {
520
+ const violations = [];
521
+ const headerState = this.findHeadersInNode(sourceFile);
522
+
523
+ const missing = [];
524
+ if (!headerState.hasCC) missing.push("Cache-Control");
525
+ if (!headerState.hasPragma) missing.push("Pragma");
526
+ if (!headerState.hasExpires) missing.push("Expires");
527
+
528
+ if (missing.length > 0) {
529
+ violations.push({
530
+ ruleId: this.ruleId,
531
+ message: `File contains sensitive data but missing anti-cache headers: ${missing.join(
532
+ ", "
533
+ )}`,
534
+ severity: "warning",
535
+ line: 1,
536
+ column: 1,
537
+ });
538
+ }
539
+
540
+ return violations;
541
+ }
542
+
543
+ cleanup() {}
544
+ }
545
+
546
+ module.exports = S037SymbolBasedAnalyzer;