@sun-asterisk/sunlint 1.3.8 → 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.
@@ -0,0 +1,128 @@
1
+ # S030 - Disable Directory Browsing and Protect Sensitive Metadata Files
2
+
3
+ ## Overview
4
+
5
+ This rule detects and prevents directory browsing vulnerabilities and exposure of sensitive metadata files that could lead to information disclosure.
6
+
7
+ ## Security Concerns
8
+
9
+ ### Directory Browsing
10
+
11
+ - **Information Disclosure**: Directory listing can reveal application structure, file names, and potentially sensitive information
12
+ - **Attack Surface**: Exposed directories provide attackers with reconnaissance information
13
+ - **Compliance**: Many security standards require disabling directory browsing
14
+
15
+ ### Sensitive Metadata Files
16
+
17
+ - **Version Control**: `.git/`, `.svn/`, `.hg/` directories contain source code history
18
+ - **Configuration Files**: `.env`, `config.json`, `settings.yml` may contain secrets
19
+ - **Backup Files**: Database dumps, configuration backups can expose sensitive data
20
+ - **Development Files**: IDE settings, temporary files may contain sensitive information
21
+
22
+ ## Detection Patterns
23
+
24
+ ### ❌ Violations
25
+
26
+ ```javascript
27
+ // Express.js - Directory browsing enabled
28
+ app.use(express.static("public", { index: false, dotfiles: "allow" }));
29
+ app.use("/files", serveIndex("uploads")); // Directory listing middleware
30
+
31
+ // Serving sensitive directories
32
+ app.use("/backup", express.static("./backups/"));
33
+ app.use("/.git", express.static("./.git/"));
34
+
35
+ // Missing protection for sensitive files
36
+ app.get("/.env", (req, res) => {
37
+ res.sendFile(".env");
38
+ });
39
+
40
+ // Koa.js - Unsafe static serving
41
+ app.use(koaStatic("./public", { hidden: true }));
42
+
43
+ // Fastify - Directory browsing
44
+ fastify.register(require("@fastify/static"), {
45
+ root: path.join(__dirname, "public"),
46
+ list: true, // Enables directory listing
47
+ });
48
+ ```
49
+
50
+ ### ✅ Safe Alternatives
51
+
52
+ ```javascript
53
+ // Express.js - Secure static file serving
54
+ app.use(
55
+ express.static("public", {
56
+ index: ["index.html", "index.htm"],
57
+ dotfiles: "deny", // Block access to dotfiles
58
+ redirect: false,
59
+ })
60
+ );
61
+
62
+ // Explicit file serving with validation
63
+ app.get("/files/:filename", (req, res) => {
64
+ const filename = path.basename(req.params.filename);
65
+ const safePath = path.join(SAFE_DIRECTORY, filename);
66
+
67
+ // Validate file exists and is safe
68
+ if (fs.existsSync(safePath) && isAllowedFile(filename)) {
69
+ res.sendFile(safePath);
70
+ } else {
71
+ res.status(404).send("File not found");
72
+ }
73
+ });
74
+
75
+ // Koa.js - Secure configuration
76
+ app.use(
77
+ koaStatic("./public", {
78
+ hidden: false, // Don't serve hidden files
79
+ index: "index.html",
80
+ gzip: true,
81
+ })
82
+ );
83
+
84
+ // Fastify - Disable directory listing
85
+ fastify.register(require("@fastify/static"), {
86
+ root: path.join(__dirname, "public"),
87
+ list: false, // Disable directory listing
88
+ wildcard: false,
89
+ });
90
+
91
+ // Nginx-style protection in configuration
92
+ const protectedPaths = [".env", ".git", "config/", "backup/"];
93
+ app.use((req, res, next) => {
94
+ const isProtected = protectedPaths.some((path) => req.url.includes(path));
95
+
96
+ if (isProtected) {
97
+ return res.status(403).send("Access denied");
98
+ }
99
+
100
+ next();
101
+ });
102
+ ```
103
+
104
+ ## Configuration Options
105
+
106
+ - **Severity**: Error (high security impact)
107
+ - **Categories**: Security, Information Disclosure
108
+ - **Frameworks**: Express.js, Koa.js, Fastify, Hapi.js
109
+ - **File Types**: JavaScript, TypeScript, Configuration files
110
+
111
+ ## Implementation Notes
112
+
113
+ 1. **Symbol-based Analysis**: Detects static file serving configurations and middleware usage
114
+ 2. **Regex-based Fallback**: Identifies patterns in configuration files and string literals
115
+ 3. **Framework Detection**: Supports multiple Node.js web frameworks
116
+ 4. **Path Validation**: Checks for exposure of sensitive directories and files
117
+
118
+ ## Related Security Rules
119
+
120
+ - S027: No hardcoded secrets
121
+ - S038: No version headers
122
+ - S055: Content type validation
123
+
124
+ ## References
125
+
126
+ - [OWASP: Information Exposure](https://owasp.org/www-community/vulnerabilities/Information_exposure)
127
+ - [CWE-548: Exposure of Information Through Directory Listing](https://cwe.mitre.org/data/definitions/548.html)
128
+ - [Express.js Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html)
@@ -0,0 +1,264 @@
1
+ /**
2
+ * S030 Main Analyzer - Disable directory browsing and protect sensitive metadata files
3
+ * Primary: Symbol-based analysis (when available)
4
+ * Fallback: Regex-based for all other cases
5
+ * Command: node cli.js --rule=S030 --input=examples/rule-test-fixtures/rules/S030_directory_browsing_protection --engine=heuristic
6
+ */
7
+
8
+ const S030SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
9
+ const S030RegexBasedAnalyzer = require("./regex-based-analyzer.js");
10
+
11
+ class S030Analyzer {
12
+ constructor(options = {}) {
13
+ if (process.env.SUNLINT_DEBUG) {
14
+ console.log(`🔧 [S030] Constructor called with options:`, !!options);
15
+ console.log(
16
+ `🔧 [S030] Options type:`,
17
+ typeof options,
18
+ Object.keys(options || {})
19
+ );
20
+ }
21
+
22
+ this.ruleId = "S030";
23
+ this.ruleName =
24
+ "Disable directory browsing and protect sensitive metadata files";
25
+ this.description =
26
+ "Disable directory browsing and protect sensitive metadata files (.git/, .env, config files, etc.) to prevent information disclosure and potential security vulnerabilities.";
27
+ this.semanticEngine = options.semanticEngine || null;
28
+ this.verbose = options.verbose || false;
29
+
30
+ this.config = {
31
+ useSymbolBased: true,
32
+ fallbackToRegex: true,
33
+ regexBasedOnly: false,
34
+ prioritizeSymbolic: true, // Prefer symbol-based when available
35
+ fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
36
+ };
37
+
38
+ try {
39
+ this.symbolAnalyzer = new S030SymbolBasedAnalyzer(this.semanticEngine);
40
+ if (process.env.SUNLINT_DEBUG)
41
+ console.log(`🔧 [S030] Symbol analyzer created successfully`);
42
+ } catch (error) {
43
+ console.error(`🔧 [S030] Error creating symbol analyzer:`, error);
44
+ }
45
+
46
+ try {
47
+ this.regexAnalyzer = new S030RegexBasedAnalyzer(this.semanticEngine);
48
+ if (process.env.SUNLINT_DEBUG)
49
+ console.log(`🔧 [S030] Regex analyzer created successfully`);
50
+ } catch (error) {
51
+ console.error(`🔧 [S030] Error creating regex analyzer:`, error);
52
+ }
53
+ }
54
+
55
+ async initialize(semanticEngine) {
56
+ this.semanticEngine = semanticEngine;
57
+ if (process.env.SUNLINT_DEBUG)
58
+ console.log(`🔧 [S030] Main analyzer initializing...`);
59
+
60
+ if (this.symbolAnalyzer)
61
+ await this.symbolAnalyzer.initialize?.(semanticEngine);
62
+ if (this.regexAnalyzer)
63
+ await this.regexAnalyzer.initialize?.(semanticEngine);
64
+ if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
65
+
66
+ if (process.env.SUNLINT_DEBUG)
67
+ console.log(`🔧 [S030] Main analyzer initialized successfully`);
68
+ }
69
+
70
+ analyzeSingle(filePath, options = {}) {
71
+ if (process.env.SUNLINT_DEBUG)
72
+ console.log(`🔍 [S030] analyzeSingle() called for: ${filePath}`);
73
+ return this.analyze([filePath], "typescript", options);
74
+ }
75
+
76
+ async analyze(files, language, options = {}) {
77
+ if (process.env.SUNLINT_DEBUG) {
78
+ console.log(
79
+ `🔧 [S030] analyze() method called with ${files.length} files, language: ${language}`
80
+ );
81
+ }
82
+
83
+ const violations = [];
84
+ for (const filePath of files) {
85
+ try {
86
+ if (process.env.SUNLINT_DEBUG)
87
+ console.log(`🔧 [S030] Processing file: ${filePath}`);
88
+ const fileViolations = await this.analyzeFile(filePath, options);
89
+ violations.push(...fileViolations);
90
+ if (process.env.SUNLINT_DEBUG)
91
+ console.log(
92
+ `🔧 [S030] File ${filePath}: Found ${fileViolations.length} violations`
93
+ );
94
+ } catch (error) {
95
+ console.warn(
96
+ `⚠ [S030] Analysis failed for ${filePath}:`,
97
+ error.message
98
+ );
99
+ }
100
+ }
101
+
102
+ if (process.env.SUNLINT_DEBUG)
103
+ console.log(`🔧 [S030] Total violations found: ${violations.length}`);
104
+ return violations;
105
+ }
106
+
107
+ async analyzeFile(filePath, options = {}) {
108
+ if (process.env.SUNLINT_DEBUG)
109
+ console.log(`🔍 [S030] analyzeFile() called for: ${filePath}`);
110
+ const violationMap = new Map();
111
+
112
+ // Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
113
+ if (process.env.SUNLINT_DEBUG) {
114
+ console.log(
115
+ `🔧 [S030] Symbol check: useSymbolBased=${
116
+ this.config.useSymbolBased
117
+ }, semanticEngine=${!!this.semanticEngine}, project=${!!this
118
+ .semanticEngine?.project}, initialized=${!!this.semanticEngine
119
+ ?.initialized}`
120
+ );
121
+ }
122
+
123
+ const canUseSymbol =
124
+ this.config.useSymbolBased &&
125
+ ((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
126
+ (!this.semanticEngine && this.config.fallbackToSymbol !== false));
127
+
128
+ if (canUseSymbol) {
129
+ try {
130
+ if (process.env.SUNLINT_DEBUG)
131
+ console.log(`🔧 [S030] Trying symbol-based analysis...`);
132
+
133
+ let sourceFile = null;
134
+ if (this.semanticEngine?.project) {
135
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
136
+ if (process.env.SUNLINT_DEBUG) {
137
+ console.log(
138
+ `🔧 [S030] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
139
+ );
140
+ }
141
+ }
142
+
143
+ if (!sourceFile) {
144
+ // Create a minimal ts-morph project for this analysis
145
+ if (process.env.SUNLINT_DEBUG)
146
+ console.log(
147
+ `🔧 [S030] Creating temporary ts-morph project for: ${filePath}`
148
+ );
149
+ try {
150
+ const fs = require("fs");
151
+ const path = require("path");
152
+ const { Project } = require("ts-morph");
153
+
154
+ // Check if file exists and read content
155
+ if (!fs.existsSync(filePath)) {
156
+ throw new Error(`File not found: ${filePath}`);
157
+ }
158
+
159
+ const fileContent = fs.readFileSync(filePath, "utf8");
160
+ const fileName = path.basename(filePath);
161
+
162
+ const tempProject = new Project({
163
+ useInMemoryFileSystem: true,
164
+ compilerOptions: {
165
+ allowJs: true,
166
+ allowSyntheticDefaultImports: true,
167
+ },
168
+ });
169
+
170
+ // Add file content to in-memory project
171
+ sourceFile = tempProject.createSourceFile(fileName, fileContent);
172
+ if (process.env.SUNLINT_DEBUG)
173
+ console.log(
174
+ `🔧 [S030] Temporary project created successfully with file: ${fileName}`
175
+ );
176
+ } catch (projectError) {
177
+ if (process.env.SUNLINT_DEBUG)
178
+ console.log(
179
+ `🔧 [S030] Failed to create temporary project:`,
180
+ projectError.message
181
+ );
182
+ throw projectError;
183
+ }
184
+ }
185
+
186
+ if (sourceFile) {
187
+ const symbolViolations = await this.symbolAnalyzer.analyze(
188
+ sourceFile,
189
+ filePath
190
+ );
191
+ symbolViolations.forEach((v) => {
192
+ const key = `${v.line}:${v.column}:${v.message}`;
193
+ if (!violationMap.has(key)) violationMap.set(key, v);
194
+ });
195
+ if (process.env.SUNLINT_DEBUG)
196
+ console.log(
197
+ `🔧 [S030] Symbol analysis completed: ${symbolViolations.length} violations`
198
+ );
199
+
200
+ // If symbol-based found violations AND prioritizeSymbolic is true, skip regex
201
+ // But still run regex if symbol-based didn't find any violations
202
+ if (this.config.prioritizeSymbolic && symbolViolations.length > 0) {
203
+ const finalViolations = Array.from(violationMap.values()).map(
204
+ (v) => ({
205
+ ...v,
206
+ filePath,
207
+ file: filePath,
208
+ })
209
+ );
210
+ if (process.env.SUNLINT_DEBUG)
211
+ console.log(
212
+ `🔧 [S030] Symbol-based analysis prioritized: ${finalViolations.length} violations`
213
+ );
214
+ return finalViolations;
215
+ }
216
+ } else {
217
+ if (process.env.SUNLINT_DEBUG)
218
+ console.log(
219
+ `🔧 [S030] No source file found, skipping symbol analysis`
220
+ );
221
+ }
222
+ } catch (error) {
223
+ console.warn(`⚠ [S030] Symbol analysis failed:`, error.message);
224
+ }
225
+ }
226
+
227
+ // Fallback to regex-based analysis
228
+ if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
229
+ try {
230
+ if (process.env.SUNLINT_DEBUG)
231
+ console.log(`🔧 [S030] Trying regex-based analysis...`);
232
+ const regexViolations = await this.regexAnalyzer.analyze(filePath);
233
+ regexViolations.forEach((v) => {
234
+ const key = `${v.line}:${v.column}:${v.message}`;
235
+ if (!violationMap.has(key)) violationMap.set(key, v);
236
+ });
237
+ if (process.env.SUNLINT_DEBUG)
238
+ console.log(
239
+ `🔧 [S030] Regex analysis completed: ${regexViolations.length} violations`
240
+ );
241
+ } catch (error) {
242
+ console.warn(`⚠ [S030] Regex analysis failed:`, error.message);
243
+ }
244
+ }
245
+
246
+ const finalViolations = Array.from(violationMap.values()).map((v) => ({
247
+ ...v,
248
+ filePath,
249
+ file: filePath,
250
+ }));
251
+ if (process.env.SUNLINT_DEBUG)
252
+ console.log(
253
+ `🔧 [S030] File analysis completed: ${finalViolations.length} unique violations`
254
+ );
255
+ return finalViolations;
256
+ }
257
+
258
+ cleanup() {
259
+ if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
260
+ if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
261
+ }
262
+ }
263
+
264
+ module.exports = S030Analyzer;
@@ -0,0 +1,63 @@
1
+ {
2
+ "id": "S030",
3
+ "name": "Disable directory browsing and protect sensitive metadata files",
4
+ "category": "security",
5
+ "description": "S030 - Disable directory browsing and protect sensitive metadata files (.git/, .env, config files, etc.) to prevent information disclosure and potential security vulnerabilities.",
6
+ "severity": "error",
7
+ "enabled": true,
8
+ "semantic": {
9
+ "enabled": true,
10
+ "priority": "high",
11
+ "fallback": "heuristic"
12
+ },
13
+ "patterns": {
14
+ "include": [
15
+ "**/*.js",
16
+ "**/*.ts",
17
+ "**/*.jsx",
18
+ "**/*.tsx",
19
+ "**/*.json",
20
+ "**/*.yaml",
21
+ "**/*.yml",
22
+ "**/.*"
23
+ ],
24
+ "exclude": [
25
+ "**/*.test.js",
26
+ "**/*.test.ts",
27
+ "**/*.spec.js",
28
+ "**/*.spec.ts",
29
+ "**/node_modules/**",
30
+ "**/dist/**",
31
+ "**/build/**"
32
+ ]
33
+ },
34
+ "analysis": {
35
+ "approach": "symbol-based-primary",
36
+ "fallback": "regex-based",
37
+ "depth": 2,
38
+ "timeout": 5000
39
+ },
40
+ "validation": {
41
+ "serverConfigs": ["express", "koa", "fastify", "hapi"],
42
+ "sensitiveFiles": [
43
+ ".env",
44
+ ".git",
45
+ ".svn",
46
+ ".hg",
47
+ "config",
48
+ "settings",
49
+ "secrets",
50
+ "keys",
51
+ "backup",
52
+ "database"
53
+ ],
54
+ "directoryListingIndicators": [
55
+ "autoIndex",
56
+ "directory",
57
+ "listing",
58
+ "browse",
59
+ "index"
60
+ ],
61
+ "protectionMethods": ["serveStatic", "static", "staticFiles", "public"]
62
+ }
63
+ }