@sun-asterisk/sunlint 1.3.7 → 1.3.9

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 (51) hide show
  1. package/CHANGELOG.md +63 -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 +247 -53
  5. package/core/file-targeting-service.js +98 -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/S020_no_eval_dynamic_code/README.md +136 -0
  10. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
  11. package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
  12. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
  13. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
  14. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
  15. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
  16. package/rules/security/S030_directory_browsing_protection/README.md +128 -0
  17. package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
  18. package/rules/security/S030_directory_browsing_protection/config.json +63 -0
  19. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
  20. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
  21. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
  22. package/rules/security/S037_cache_headers/README.md +128 -0
  23. package/rules/security/S037_cache_headers/analyzer.js +263 -0
  24. package/rules/security/S037_cache_headers/config.json +50 -0
  25. package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
  26. package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
  27. package/rules/security/S038_no_version_headers/README.md +234 -0
  28. package/rules/security/S038_no_version_headers/analyzer.js +262 -0
  29. package/rules/security/S038_no_version_headers/config.json +49 -0
  30. package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
  31. package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
  32. package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
  33. package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
  34. package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
  35. package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
  36. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
  37. package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
  38. package/rules/security/S049_short_validity_tokens/config.json +124 -0
  39. package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
  40. package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
  41. package/rules/security/S051_password_length_policy/analyzer.js +410 -0
  42. package/rules/security/S051_password_length_policy/config.json +83 -0
  43. package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
  44. package/rules/security/S052_weak_otp_entropy/config.json +57 -0
  45. package/rules/security/S054_no_default_accounts/README.md +129 -0
  46. package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
  47. package/rules/security/S054_no_default_accounts/config.json +101 -0
  48. package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
  49. package/rules/security/S056_log_injection_protection/config.json +148 -0
  50. package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
  51. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +246 -0
@@ -0,0 +1,483 @@
1
+ /**
2
+ * S030 Regex-Based // Fastify static serving
3
+ fastifyStatic:
4
+ /fastify\.register\s*\(\s*require\s*\(\s*['"\`]@?fastify\/static['"\`]\s*\)\s*,\s*\{([^}]*)\}/g,
5
+ fastifyStaticRoot:
6
+ /root\s*:\s*['"\`]([^'"\`]+)['"\`]/g,lyzer - Disa // NuxtJS static serving (in nuxt.config.js)
7
+ nuxtStatic: /static\s*:\s*['"\`]([^'"\`]+)['"\`]/g,
8
+ nuxtGenerate: /generate\s*:\s*\{[^}]*dir\s*:\s*['"\`]([^'"\`]+)['"\`]/g,
9
+ nuxtPublicAssets: /dir\s*:\s*['"\`]([^'"\`]+)['"\`]/g,
10
+
11
+ // Hapi.js directory serving
12
+ hapiDirectory: /directory\s*:\s*\{[^}]*path\s*:\s*['"\`]([^'"\`]+)['"\`]/g, directory browsing and protect sensitive metadata files
13
+ * Detects static file serving configurations and directory browsing vulnerabilities
14
+ */
15
+ const fs = require("fs");
16
+
17
+ class S030RegexBasedAnalyzer {
18
+ constructor() {
19
+ this.ruleId = "S030";
20
+
21
+ // Static file serving patterns
22
+ this.staticPatterns = {
23
+ // Express.js static serving
24
+ expressStatic:
25
+ /express\.static\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{([^}]*)\})?\s*\)/g,
26
+ appUseStatic:
27
+ /app\.use\s*\(\s*(?:['"`][^'"`]*['"`]\s*,\s*)?express\.static\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{([^}]*)\})?\s*\)/g,
28
+
29
+ // Koa.js static serving
30
+ koaStatic:
31
+ /koa-static\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{([^}]*)\})?\s*\)/g,
32
+ koaMount:
33
+ /mount\s*\(\s*['"`][^'"`]*['"`]\s*,\s*serve\s*\(\s*['"`]([^'"`]+)['"`]/g,
34
+
35
+ // Fastify static serving
36
+ fastifyStatic:
37
+ /fastify\.register\s*\(\s*require\s*\(\s*['"`]@?fastify\/static['"`]\s*\)\s*,\s*\{([^}]*)\}/g,
38
+
39
+ // NextJS static serving
40
+ nextStatic:
41
+ /express\.static\s*\(\s*path\.join\s*\(\s*__dirname\s*,\s*['"`]([^'"`]*(?:public|static|assets)[^'"`]*)['"`]\s*\)\s*(?:,\s*\{([^}]*)\})?\s*\)/g,
42
+ nextPublicDir:
43
+ /app\.use\s*\(\s*['"`]\/static['"`]\s*,\s*express\.static\s*\(\s*['"`]([^'"`]+)['"`]/g,
44
+
45
+ // NestJS static serving
46
+ nestServeStatic: /ServeStaticModule\.forRoot\s*\(\s*\{([^}]*)\}\s*\)/g,
47
+ nestUseStatic:
48
+ /app\.useStaticAssets\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{([^}]*)\})?\s*\)/g,
49
+
50
+ // NuxtJS static serving (in nuxt.config.js)
51
+ nuxtStatic: /static\s*:\s*['"`]([^'"`]+)['"`]/g,
52
+ nuxtGenerate: /generate\s*:\s*\{[^}]*dir\s*:\s*['"`]([^'"`]+)['"`]/g,
53
+
54
+ // Generic static serving
55
+ serveStatic:
56
+ /serveStatic\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{([^}]*)\})?\s*\)/g,
57
+ };
58
+
59
+ // Directory browsing middleware patterns
60
+ this.directoryBrowsingPatterns = {
61
+ serveIndex:
62
+ /serve-?index\s*\(\s*['"`]([^'"`]+)['"`](?:\s*,\s*\{([^}]*)\})?\s*\)/g,
63
+ autoIndex: /autoIndex\s*:\s*(true|1|"true"|'true')/g,
64
+ directoryListing: /directory\s*:\s*(true|1|"true"|'true')/g,
65
+ listDirectories: /list\s*:\s*(true|1|"true"|'true')/g,
66
+ listingTrue: /listing\s*:\s*(true|1|"true"|'true')/g,
67
+ };
68
+
69
+ // Sensitive file/directory patterns
70
+ this.sensitiveFiles = [
71
+ /\\.env/g,
72
+ /\\.git/g,
73
+ /\\.svn/g,
74
+ /\\.hg/g,
75
+ /\\.bzr/g,
76
+ /\\.CVS/g,
77
+ /config(?:s|uration)?/g,
78
+ /settings?/g,
79
+ /secrets?/g,
80
+ /keys?/g,
81
+ /backup(?:s)?/g,
82
+ /database(?:s)?/g,
83
+ /\\.aws/g,
84
+ /\\.ssh/g,
85
+ /credentials?/g,
86
+ /private/g,
87
+ ];
88
+
89
+ // Dangerous configuration patterns
90
+ this.dangerousConfigs = {
91
+ dotfilesAllow: /dotfiles\s*:\s*['"`]allow['"`]/g,
92
+ hiddenTrue: /hidden\s*:\s*(true|1|"true"|'true')/g,
93
+ indexFalse: /index\s*:\s*(false|0|"false"|'false')/g,
94
+ listingTrue: /listing\s*:\s*(true|1|"true"|'true')/g,
95
+ };
96
+
97
+ // Route patterns that expose sensitive paths
98
+ this.sensitiveRoutePatterns = {
99
+ // Express.js routes
100
+ envRoute:
101
+ /\.(get|post|put|delete|patch|all)\s*\(\s*['"`][^'"`]*\.env[^'"`]*['"`]/g,
102
+ gitRoute:
103
+ /\.(get|post|put|delete|patch|all)\s*\(\s*['"`][^'"`]*\.git[^'"`]*['"`]/g,
104
+ configRoute:
105
+ /\.(get|post|put|delete|patch|all)\s*\(\s*['"`][^'"`]*config[^'"`]*['"`]/g,
106
+ backupRoute:
107
+ /\.(get|post|put|delete|patch|all)\s*\(\s*['"`][^'"`]*backup[^'"`]*['"`]/g,
108
+
109
+ // NextJS API routes (export functions in /pages/api/ or /app/api/)
110
+ nextApiEnv:
111
+ /export\s+(default\s+)?(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\s*\([^)]*\)\s*{[^}]*['"`][^'"`]*\.env[^'"`]*['"`]/g,
112
+ nextApiGit:
113
+ /export\s+(default\s+)?(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\s*\([^)]*\)\s*{[^}]*['"`][^'"`]*\.git[^'"`]*['"`]/g,
114
+ nextApiSensitive:
115
+ /export\s+(default\s+)?(?:async\s+)?function\s+(?:GET|POST|PUT|DELETE|PATCH)\s*\([^)]*\)\s*{[^}]*['"`][^'"`]*(?:config|backup|secret|\.ssh)[^'"`]*['"`]/g,
116
+
117
+ // NestJS controller routes
118
+ nestControllerSensitive:
119
+ /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*(?:\.env|\.git|config|backup|secret|\.ssh)[^'"`]*['"`]\s*\)/g,
120
+ nestControllerPath:
121
+ /@Controller\s*\(\s*['"`][^'"`]*(?:\.env|\.git|config|backup|secret|\.ssh)[^'"`]*['"`]\s*\)/g,
122
+
123
+ // File serving in routes
124
+ sendFileSensitive:
125
+ /(?:res\.sendFile|sendFile)\s*\(\s*['"`][^'"`]*(?:\.env|\.git|config|backup|secret|\.ssh)[^'"`]*['"`]/g,
126
+ };
127
+ }
128
+
129
+ async analyze(filePath) {
130
+ // Skip files that are unlikely to contain server configurations
131
+ const skipPatterns = [
132
+ /\\.d\\.ts$/,
133
+ /\\.types\\.ts$/,
134
+ /\\.interface\\.ts$/,
135
+ /\\.constants?\\.ts$/,
136
+ /\\.spec\\.ts$/,
137
+ /\\.test\\.ts$/,
138
+ /\\.min\\.js$/,
139
+ /\\.bundle\\.js$/,
140
+ /\\.model\\.ts$/,
141
+ /\\.entity\\.ts$/,
142
+ /\\.dto\\.ts$/,
143
+ ];
144
+
145
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
146
+ if (shouldSkip) {
147
+ return [];
148
+ }
149
+
150
+ const content = fs.readFileSync(filePath, "utf8");
151
+ const lines = content.split(/\\r?\\n/);
152
+ const violations = [];
153
+
154
+ for (let i = 0; i < lines.length; i++) {
155
+ const line = lines[i];
156
+ const lineNumber = i + 1;
157
+
158
+ // Skip comments and imports
159
+ if (this.shouldSkipLine(line)) {
160
+ continue;
161
+ }
162
+
163
+ // Check for static file serving patterns
164
+ this.checkStaticFileServing(line, lineNumber, violations);
165
+
166
+ // Check for directory browsing configurations
167
+ this.checkDirectoryBrowsing(line, lineNumber, violations);
168
+
169
+ // Check for sensitive file exposure
170
+ this.checkSensitiveFileExposure(line, lineNumber, violations);
171
+
172
+ // Check for dangerous configurations
173
+ this.checkDangerousConfigurations(line, lineNumber, violations);
174
+
175
+ // Check for sensitive route definitions
176
+ this.checkSensitiveRoutes(line, lineNumber, violations);
177
+ }
178
+
179
+ return violations;
180
+ }
181
+
182
+ shouldSkipLine(line) {
183
+ const trimmed = line.trim();
184
+
185
+ // Skip empty lines
186
+ if (!trimmed) return true;
187
+
188
+ // Skip single-line comments
189
+ if (trimmed.startsWith("//")) return true;
190
+
191
+ // Skip import/export statements
192
+ if (trimmed.startsWith("import ") || trimmed.startsWith("export "))
193
+ return true;
194
+
195
+ // Skip require statements
196
+ if (trimmed.startsWith("const ") && trimmed.includes("require("))
197
+ return true;
198
+
199
+ // Skip JSDoc comments
200
+ if (
201
+ trimmed.startsWith("*") ||
202
+ trimmed.startsWith("/**") ||
203
+ trimmed.startsWith("*/")
204
+ )
205
+ return true;
206
+
207
+ return false;
208
+ }
209
+
210
+ checkStaticFileServing(line, lineNumber, violations) {
211
+ for (const [patternName, pattern] of Object.entries(this.staticPatterns)) {
212
+ const matches = [...line.matchAll(pattern)];
213
+
214
+ for (const match of matches) {
215
+ let servedPath;
216
+ let configStr = "";
217
+
218
+ // Handle different pattern structures
219
+ if (
220
+ patternName === "fastifyStaticRoot" ||
221
+ patternName === "nuxtPublicAssets" ||
222
+ patternName === "hapiDirectory"
223
+ ) {
224
+ servedPath = match[1];
225
+ } else {
226
+ servedPath = match[1];
227
+ configStr = match[2] || "";
228
+ }
229
+
230
+ // Check if serving sensitive directories
231
+ if (this.isSensitivePath(servedPath)) {
232
+ violations.push({
233
+ ruleId: this.ruleId,
234
+ message: `Static file serving exposes sensitive path '${servedPath}' - this could leak sensitive metadata files`,
235
+ severity: "error",
236
+ line: lineNumber,
237
+ column: match.index + 1,
238
+ });
239
+ }
240
+
241
+ // Check configuration for dangerous settings
242
+ if (configStr) {
243
+ this.checkStaticConfiguration(
244
+ configStr,
245
+ lineNumber,
246
+ violations,
247
+ match.index
248
+ );
249
+ }
250
+
251
+ if (process.env.SUNLINT_DEBUG) {
252
+ console.log(
253
+ `🔧 [S030-Regex] Found static serving at line ${lineNumber}: ${match[0]}`
254
+ );
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ checkDirectoryBrowsing(line, lineNumber, violations) {
261
+ for (const [patternName, pattern] of Object.entries(
262
+ this.directoryBrowsingPatterns
263
+ )) {
264
+ const matches = [...line.matchAll(pattern)];
265
+
266
+ for (const match of matches) {
267
+ let message;
268
+
269
+ switch (patternName) {
270
+ case "serveIndex":
271
+ message = `Directory browsing middleware detected - this enables directory listing which exposes file structure`;
272
+ break;
273
+ case "autoIndex":
274
+ case "directoryListing":
275
+ case "listDirectories":
276
+ case "listingTrue":
277
+ message = `Configuration enables directory browsing - set '${patternName}' to false to prevent directory listing`;
278
+ break;
279
+ default:
280
+ message = "Directory browsing configuration detected";
281
+ }
282
+
283
+ violations.push({
284
+ ruleId: this.ruleId,
285
+ message: message,
286
+ severity: "error",
287
+ line: lineNumber,
288
+ column: match.index + 1,
289
+ });
290
+
291
+ if (process.env.SUNLINT_DEBUG) {
292
+ console.log(
293
+ `🔧 [S030-Regex] Found directory browsing at line ${lineNumber}: ${match[0]}`
294
+ );
295
+ }
296
+ }
297
+ }
298
+ }
299
+
300
+ checkSensitiveFileExposure(line, lineNumber, violations) {
301
+ for (const sensitivePattern of this.sensitiveFiles) {
302
+ const matches = [...line.matchAll(sensitivePattern)];
303
+
304
+ for (const match of matches) {
305
+ // Check if it's in a serving context (not just a string mention)
306
+ if (this.isInServingContext(line)) {
307
+ violations.push({
308
+ ruleId: this.ruleId,
309
+ message: `Potential exposure of sensitive file/directory '${match[0]}' - ensure proper access controls`,
310
+ severity: "warning",
311
+ line: lineNumber,
312
+ column: match.index + 1,
313
+ });
314
+
315
+ if (process.env.SUNLINT_DEBUG) {
316
+ console.log(
317
+ `🔧 [S030-Regex] Found sensitive file exposure at line ${lineNumber}: ${match[0]}`
318
+ );
319
+ }
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ checkDangerousConfigurations(line, lineNumber, violations) {
326
+ for (const [configName, pattern] of Object.entries(this.dangerousConfigs)) {
327
+ const matches = [...line.matchAll(pattern)];
328
+
329
+ for (const match of matches) {
330
+ let message;
331
+
332
+ switch (configName) {
333
+ case "dotfilesAllow":
334
+ message =
335
+ "Dotfiles access is enabled - set dotfiles to 'deny' to protect sensitive files like .env";
336
+ break;
337
+ case "hiddenTrue":
338
+ message =
339
+ "Hidden files access is enabled - this may expose sensitive metadata files";
340
+ break;
341
+ case "indexFalse":
342
+ message =
343
+ "Index file serving is disabled - this may enable directory browsing";
344
+ break;
345
+ case "listingTrue":
346
+ message =
347
+ "Directory listing is enabled - disable listing to prevent directory browsing";
348
+ break;
349
+ default:
350
+ message = "Dangerous static file configuration detected";
351
+ }
352
+
353
+ violations.push({
354
+ ruleId: this.ruleId,
355
+ message: message,
356
+ severity: "warning",
357
+ line: lineNumber,
358
+ column: match.index + 1,
359
+ });
360
+
361
+ if (process.env.SUNLINT_DEBUG) {
362
+ console.log(
363
+ `🔧 [S030-Regex] Found dangerous config at line ${lineNumber}: ${match[0]}`
364
+ );
365
+ }
366
+ }
367
+ }
368
+ }
369
+
370
+ checkSensitiveRoutes(line, lineNumber, violations) {
371
+ for (const [routeName, pattern] of Object.entries(
372
+ this.sensitiveRoutePatterns
373
+ )) {
374
+ const matches = [...line.matchAll(pattern)];
375
+
376
+ for (const match of matches) {
377
+ violations.push({
378
+ ruleId: this.ruleId,
379
+ message: `Route exposes sensitive path - implement proper access controls for sensitive files/directories`,
380
+ severity: "error",
381
+ line: lineNumber,
382
+ column: match.index + 1,
383
+ });
384
+
385
+ if (process.env.SUNLINT_DEBUG) {
386
+ console.log(
387
+ `🔧 [S030-Regex] Found sensitive route at line ${lineNumber}: ${match[0]}`
388
+ );
389
+ }
390
+ }
391
+ }
392
+ }
393
+
394
+ checkStaticConfiguration(configStr, lineNumber, violations, columnOffset) {
395
+ // Check for dangerous configuration options within static serving
396
+ if (/dotfiles\\s*:\\s*['"`]allow['"`]/.test(configStr)) {
397
+ violations.push({
398
+ ruleId: this.ruleId,
399
+ message:
400
+ "Dotfiles access enabled in static serving - this exposes sensitive files like .env",
401
+ severity: "warning",
402
+ line: lineNumber,
403
+ column: columnOffset + 1,
404
+ });
405
+ }
406
+
407
+ if (/index\\s*:\\s*(false|0|"false"|'false')/.test(configStr)) {
408
+ violations.push({
409
+ ruleId: this.ruleId,
410
+ message:
411
+ "Index file serving disabled - this may enable directory browsing",
412
+ severity: "warning",
413
+ line: lineNumber,
414
+ column: columnOffset + 1,
415
+ });
416
+ }
417
+
418
+ if (/list\\s*:\\s*(true|1|"true"|'true')/.test(configStr)) {
419
+ violations.push({
420
+ ruleId: this.ruleId,
421
+ message:
422
+ "Directory listing enabled in static configuration - disable to prevent directory browsing",
423
+ severity: "error",
424
+ line: lineNumber,
425
+ column: columnOffset + 1,
426
+ });
427
+ }
428
+ }
429
+
430
+ isSensitivePath(path) {
431
+ if (!path || typeof path !== "string") return false;
432
+
433
+ const normalizedPath = path.toLowerCase();
434
+ const sensitiveKeywords = [
435
+ ".env",
436
+ ".git",
437
+ ".svn",
438
+ ".hg",
439
+ "config",
440
+ "settings",
441
+ "secrets",
442
+ "keys",
443
+ "backup",
444
+ "database",
445
+ ".aws",
446
+ ".ssh",
447
+ "credentials",
448
+ "private",
449
+ ];
450
+
451
+ return sensitiveKeywords.some(
452
+ (keyword) =>
453
+ normalizedPath.includes(keyword) ||
454
+ normalizedPath.startsWith("./" + keyword) ||
455
+ normalizedPath.endsWith("/" + keyword)
456
+ );
457
+ }
458
+
459
+ isInServingContext(line) {
460
+ const servingKeywords = [
461
+ "static",
462
+ "serve",
463
+ "use",
464
+ "mount",
465
+ "register",
466
+ "get",
467
+ "post",
468
+ "put",
469
+ "delete",
470
+ "patch",
471
+ "sendFile",
472
+ "express",
473
+ "app",
474
+ "router",
475
+ ];
476
+
477
+ return servingKeywords.some((keyword) => line.includes(keyword));
478
+ }
479
+
480
+ cleanup() {}
481
+ }
482
+
483
+ module.exports = S030RegexBasedAnalyzer;