@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.
- package/CHANGELOG.md +63 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +247 -53
- package/core/file-targeting-service.js +98 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
- package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
- package/rules/security/S030_directory_browsing_protection/README.md +128 -0
- package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
- package/rules/security/S030_directory_browsing_protection/config.json +63 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +443 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- 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;
|