@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.
- package/CHANGELOG.md +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -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/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 +436 -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 +287 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# S038 - Do Not Expose Version Information in Response Headers
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Rule S038 prevents the exposure of server and framework version information through HTTP response headers. This reduces information disclosure that could help attackers identify known vulnerabilities in specific versions of web servers, frameworks, or runtime environments.
|
|
6
|
+
|
|
7
|
+
Common version-revealing headers include:
|
|
8
|
+
|
|
9
|
+
- `Server: nginx/1.18.0`
|
|
10
|
+
- `X-Powered-By: Express`
|
|
11
|
+
- `X-AspNet-Version: 4.0.30319`
|
|
12
|
+
- `X-Generator: Drupal 7`
|
|
13
|
+
|
|
14
|
+
## Framework Support
|
|
15
|
+
|
|
16
|
+
This rule supports multiple web frameworks:
|
|
17
|
+
|
|
18
|
+
- **Express.js**: Route handlers using `app.get()`, `router.post()`, etc.
|
|
19
|
+
- **Next.js**: API routes (`export default function handler`) and App Router (`export async function GET`)
|
|
20
|
+
- **NestJS**: Controller methods with decorators (`@Get()`, `@Post()`, etc.)
|
|
21
|
+
- **Nuxt.js**: Server routes (`export default defineEventHandler`)
|
|
22
|
+
|
|
23
|
+
## Security Impact
|
|
24
|
+
|
|
25
|
+
Version information disclosure can:
|
|
26
|
+
|
|
27
|
+
- Help attackers identify known vulnerabilities in specific versions
|
|
28
|
+
- Facilitate targeted attacks against outdated software
|
|
29
|
+
- Provide reconnaissance information for further attacks
|
|
30
|
+
- Violate security best practices for information hiding
|
|
31
|
+
|
|
32
|
+
## Rule Details
|
|
33
|
+
|
|
34
|
+
### Prohibited Headers
|
|
35
|
+
|
|
36
|
+
The following headers should not expose version information:
|
|
37
|
+
|
|
38
|
+
- `Server`
|
|
39
|
+
- `X-Powered-By`
|
|
40
|
+
- `X-AspNet-Version`
|
|
41
|
+
- `X-AspNetMvc-Version`
|
|
42
|
+
- `X-Generator`
|
|
43
|
+
- `X-Runtime`
|
|
44
|
+
- `X-Version`
|
|
45
|
+
- `X-Framework`
|
|
46
|
+
- `X-Drupal-Cache`
|
|
47
|
+
- `X-Varnish`
|
|
48
|
+
- `X-Cache`
|
|
49
|
+
- `X-Served-By`
|
|
50
|
+
|
|
51
|
+
### Version Information Patterns
|
|
52
|
+
|
|
53
|
+
The rule detects these patterns in header values:
|
|
54
|
+
|
|
55
|
+
- Version numbers: `1.0`, `2.1.3`, `v1.2`
|
|
56
|
+
- Framework names: `Express`, `nginx`, `Apache`, `IIS`
|
|
57
|
+
- Runtime versions: `Node.js`, `PHP`, `ASP.NET`
|
|
58
|
+
|
|
59
|
+
## Violations
|
|
60
|
+
|
|
61
|
+
### Express.js Examples
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// ❌ Violation: Exposing server version
|
|
65
|
+
app.get("/api/data", (req, res) => {
|
|
66
|
+
res.setHeader("Server", "nginx/1.18.0");
|
|
67
|
+
res.setHeader("X-Powered-By", "Express 4.18.0");
|
|
68
|
+
res.json({ data: "value" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ❌ Violation: Custom version header
|
|
72
|
+
app.get("/api/info", (req, res) => {
|
|
73
|
+
res.set("X-Version", "MyApp v2.1.3");
|
|
74
|
+
res.json({ status: "ok" });
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### NestJS Examples
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// ❌ Violation: Framework version disclosure
|
|
82
|
+
@Get('status')
|
|
83
|
+
getStatus(@Res() res: Response) {
|
|
84
|
+
res.header('X-Powered-By', 'NestJS 9.0.0');
|
|
85
|
+
res.header('X-Framework', 'Node.js/Express');
|
|
86
|
+
return res.json({ status: 'running' });
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Next.js Examples
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// ❌ Violation: Runtime version exposure
|
|
94
|
+
export default function handler(req, res) {
|
|
95
|
+
res.setHeader("X-Runtime", "Node.js 18.16.0");
|
|
96
|
+
res.setHeader("Server", "Vercel");
|
|
97
|
+
res.json({ message: "Hello" });
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Compliant Code
|
|
102
|
+
|
|
103
|
+
### Express.js Solutions
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// ✅ Correct: Remove or hide version headers
|
|
107
|
+
app.disable("x-powered-by"); // Remove Express header
|
|
108
|
+
|
|
109
|
+
app.get("/api/data", (req, res) => {
|
|
110
|
+
// No version headers set
|
|
111
|
+
res.json({ data: "value" });
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ✅ Correct: Use helmet middleware
|
|
115
|
+
const helmet = require("helmet");
|
|
116
|
+
app.use(helmet.hidePoweredBy());
|
|
117
|
+
|
|
118
|
+
// ✅ Correct: Generic server header
|
|
119
|
+
app.get("/api/secure", (req, res) => {
|
|
120
|
+
res.setHeader("Server", "WebServer"); // Generic, no version
|
|
121
|
+
res.json({ data: "secure" });
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### NestJS Solutions
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
// ✅ Correct: Use helmet middleware
|
|
129
|
+
import helmet from "helmet";
|
|
130
|
+
|
|
131
|
+
@Controller()
|
|
132
|
+
export class AppController {
|
|
133
|
+
constructor() {
|
|
134
|
+
// Configure helmet in main.ts
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@Get("status")
|
|
138
|
+
getStatus() {
|
|
139
|
+
// No version headers
|
|
140
|
+
return { status: "running" };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// In main.ts:
|
|
145
|
+
app.use(
|
|
146
|
+
helmet({
|
|
147
|
+
hidePoweredBy: true,
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Next.js Solutions
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// ✅ Correct: No version headers
|
|
156
|
+
export default function handler(req, res) {
|
|
157
|
+
res.json({ message: "Hello" });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ✅ Correct: Configure in next.config.js
|
|
161
|
+
module.exports = {
|
|
162
|
+
async headers() {
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
source: "/(.*)",
|
|
166
|
+
headers: [
|
|
167
|
+
{
|
|
168
|
+
key: "Server",
|
|
169
|
+
value: "WebServer", // Generic server name
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Configuration Solutions
|
|
179
|
+
|
|
180
|
+
### nginx Configuration
|
|
181
|
+
|
|
182
|
+
```nginx
|
|
183
|
+
# Hide nginx version
|
|
184
|
+
server_tokens off;
|
|
185
|
+
|
|
186
|
+
# Remove specific headers
|
|
187
|
+
more_clear_headers 'Server';
|
|
188
|
+
more_clear_headers 'X-Powered-By';
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Apache Configuration
|
|
192
|
+
|
|
193
|
+
```apache
|
|
194
|
+
# Hide Apache version
|
|
195
|
+
ServerTokens Prod
|
|
196
|
+
ServerSignature Off
|
|
197
|
+
|
|
198
|
+
# Remove specific headers
|
|
199
|
+
Header unset Server
|
|
200
|
+
Header unset X-Powered-By
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Best Practices
|
|
204
|
+
|
|
205
|
+
1. **Use Security Middleware**: Implement helmet.js or similar security middleware
|
|
206
|
+
2. **Server Configuration**: Configure web servers to hide version information
|
|
207
|
+
3. **Regular Audits**: Regularly check response headers for version leaks
|
|
208
|
+
4. **Generic Headers**: Use generic server names instead of specific versions
|
|
209
|
+
5. **Reverse Proxy**: Use a reverse proxy to normalize all outgoing headers
|
|
210
|
+
|
|
211
|
+
## Exceptions
|
|
212
|
+
|
|
213
|
+
This rule may not apply when:
|
|
214
|
+
|
|
215
|
+
- Version information is intentionally exposed for debugging (development only)
|
|
216
|
+
- API versioning requires version headers (use API version, not server version)
|
|
217
|
+
- Third-party services require specific version headers
|
|
218
|
+
|
|
219
|
+
## Related Rules
|
|
220
|
+
|
|
221
|
+
- **S055**: Content-Type Validation
|
|
222
|
+
- **S031**: Secure Session Cookies
|
|
223
|
+
- **S037**: Cache Headers for Sensitive Data
|
|
224
|
+
|
|
225
|
+
## Implementation Notes
|
|
226
|
+
|
|
227
|
+
The rule uses both symbol-based (AST) and regex-based analysis to detect:
|
|
228
|
+
|
|
229
|
+
1. Direct header setting in route handlers
|
|
230
|
+
2. Bulk header object assignments
|
|
231
|
+
3. Framework-specific header methods
|
|
232
|
+
4. Version patterns in header values
|
|
233
|
+
|
|
234
|
+
Security middleware detection automatically skips analysis when proper security measures are in place.
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S038 Main Analyzer - Do not expose version information in response headers
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S038 --input=examples/rule-test-fixtures/rules/S038_no_version_headers --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S038SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S038RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S038Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S038] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S038] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S038";
|
|
23
|
+
this.ruleName = "Do not expose version information in response headers";
|
|
24
|
+
this.description =
|
|
25
|
+
"Prevent exposure of server version information through response headers (Server, X-Powered-By, X-AspNet-Version, etc.) to reduce information disclosure and potential attack vectors.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
this.config = {
|
|
30
|
+
useSymbolBased: true,
|
|
31
|
+
fallbackToRegex: true,
|
|
32
|
+
regexBasedOnly: false,
|
|
33
|
+
prioritizeSymbolic: true, // Prefer symbol-based when available
|
|
34
|
+
fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S038SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG)
|
|
40
|
+
console.log(`🔧 [S038] Symbol analyzer created successfully`);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`🔧 [S038] Error creating symbol analyzer:`, error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
this.regexAnalyzer = new S038RegexBasedAnalyzer(this.semanticEngine);
|
|
47
|
+
if (process.env.SUNLINT_DEBUG)
|
|
48
|
+
console.log(`🔧 [S038] Regex analyzer created successfully`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`🔧 [S038] Error creating regex analyzer:`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async initialize(semanticEngine) {
|
|
55
|
+
this.semanticEngine = semanticEngine;
|
|
56
|
+
if (process.env.SUNLINT_DEBUG)
|
|
57
|
+
console.log(`🔧 [S038] Main analyzer initializing...`);
|
|
58
|
+
|
|
59
|
+
if (this.symbolAnalyzer)
|
|
60
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
61
|
+
if (this.regexAnalyzer)
|
|
62
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
63
|
+
if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
|
|
64
|
+
|
|
65
|
+
if (process.env.SUNLINT_DEBUG)
|
|
66
|
+
console.log(`🔧 [S038] Main analyzer initialized successfully`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
analyzeSingle(filePath, options = {}) {
|
|
70
|
+
if (process.env.SUNLINT_DEBUG)
|
|
71
|
+
console.log(`🔍 [S038] analyzeSingle() called for: ${filePath}`);
|
|
72
|
+
return this.analyze([filePath], "typescript", options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async analyze(files, language, options = {}) {
|
|
76
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
77
|
+
console.log(
|
|
78
|
+
`🔧 [S038] analyze() method called with ${files.length} files, language: ${language}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const violations = [];
|
|
83
|
+
for (const filePath of files) {
|
|
84
|
+
try {
|
|
85
|
+
if (process.env.SUNLINT_DEBUG)
|
|
86
|
+
console.log(`🔧 [S038] Processing file: ${filePath}`);
|
|
87
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
88
|
+
violations.push(...fileViolations);
|
|
89
|
+
if (process.env.SUNLINT_DEBUG)
|
|
90
|
+
console.log(
|
|
91
|
+
`🔧 [S038] File ${filePath}: Found ${fileViolations.length} violations`
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`⚠ [S038] Analysis failed for ${filePath}:`,
|
|
96
|
+
error.message
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (process.env.SUNLINT_DEBUG)
|
|
102
|
+
console.log(`🔧 [S038] Total violations found: ${violations.length}`);
|
|
103
|
+
return violations;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async analyzeFile(filePath, options = {}) {
|
|
107
|
+
if (process.env.SUNLINT_DEBUG)
|
|
108
|
+
console.log(`🔍 [S038] analyzeFile() called for: ${filePath}`);
|
|
109
|
+
const violationMap = new Map();
|
|
110
|
+
|
|
111
|
+
// Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
|
|
112
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
113
|
+
console.log(
|
|
114
|
+
`🔧 [S038] Symbol check: useSymbolBased=${
|
|
115
|
+
this.config.useSymbolBased
|
|
116
|
+
}, semanticEngine=${!!this.semanticEngine}, project=${!!this
|
|
117
|
+
.semanticEngine?.project}, initialized=${!!this.semanticEngine
|
|
118
|
+
?.initialized}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const canUseSymbol =
|
|
123
|
+
this.config.useSymbolBased &&
|
|
124
|
+
((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
|
|
125
|
+
(!this.semanticEngine && this.config.fallbackToSymbol !== false));
|
|
126
|
+
|
|
127
|
+
if (canUseSymbol) {
|
|
128
|
+
try {
|
|
129
|
+
if (process.env.SUNLINT_DEBUG)
|
|
130
|
+
console.log(`🔧 [S038] Trying symbol-based analysis...`);
|
|
131
|
+
|
|
132
|
+
let sourceFile = null;
|
|
133
|
+
if (this.semanticEngine?.project) {
|
|
134
|
+
sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(
|
|
137
|
+
`🔧 [S038] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!sourceFile) {
|
|
143
|
+
// Create a minimal ts-morph project for this analysis
|
|
144
|
+
if (process.env.SUNLINT_DEBUG)
|
|
145
|
+
console.log(
|
|
146
|
+
`🔧 [S038] Creating temporary ts-morph project for: ${filePath}`
|
|
147
|
+
);
|
|
148
|
+
try {
|
|
149
|
+
const fs = require("fs");
|
|
150
|
+
const path = require("path");
|
|
151
|
+
const { Project } = require("ts-morph");
|
|
152
|
+
|
|
153
|
+
// Check if file exists and read content
|
|
154
|
+
if (!fs.existsSync(filePath)) {
|
|
155
|
+
throw new Error(`File not found: ${filePath}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
159
|
+
const fileName = path.basename(filePath);
|
|
160
|
+
|
|
161
|
+
const tempProject = new Project({
|
|
162
|
+
useInMemoryFileSystem: true,
|
|
163
|
+
compilerOptions: {
|
|
164
|
+
allowJs: true,
|
|
165
|
+
allowSyntheticDefaultImports: true,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Add file content to in-memory project
|
|
170
|
+
sourceFile = tempProject.createSourceFile(fileName, fileContent);
|
|
171
|
+
if (process.env.SUNLINT_DEBUG)
|
|
172
|
+
console.log(
|
|
173
|
+
`🔧 [S038] Temporary project created successfully with file: ${fileName}`
|
|
174
|
+
);
|
|
175
|
+
} catch (projectError) {
|
|
176
|
+
if (process.env.SUNLINT_DEBUG)
|
|
177
|
+
console.log(
|
|
178
|
+
`🔧 [S038] Failed to create temporary project:`,
|
|
179
|
+
projectError.message
|
|
180
|
+
);
|
|
181
|
+
throw projectError;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (sourceFile) {
|
|
186
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
187
|
+
sourceFile,
|
|
188
|
+
filePath
|
|
189
|
+
);
|
|
190
|
+
symbolViolations.forEach((v) => {
|
|
191
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
192
|
+
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
193
|
+
});
|
|
194
|
+
if (process.env.SUNLINT_DEBUG)
|
|
195
|
+
console.log(
|
|
196
|
+
`🔧 [S038] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// If symbol-based found violations or prioritizeSymbolic is true, skip regex
|
|
200
|
+
if (this.config.prioritizeSymbolic && symbolViolations.length >= 0) {
|
|
201
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
202
|
+
(v) => ({
|
|
203
|
+
...v,
|
|
204
|
+
filePath,
|
|
205
|
+
file: filePath,
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
if (process.env.SUNLINT_DEBUG)
|
|
209
|
+
console.log(
|
|
210
|
+
`🔧 [S038] Symbol-based analysis prioritized: ${finalViolations.length} violations`
|
|
211
|
+
);
|
|
212
|
+
return finalViolations;
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
if (process.env.SUNLINT_DEBUG)
|
|
216
|
+
console.log(
|
|
217
|
+
`🔧 [S038] No source file found, skipping symbol analysis`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.warn(`⚠ [S038] Symbol analysis failed:`, error.message);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Fallback to regex-based analysis
|
|
226
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
227
|
+
try {
|
|
228
|
+
if (process.env.SUNLINT_DEBUG)
|
|
229
|
+
console.log(`🔧 [S038] Trying regex-based analysis...`);
|
|
230
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
231
|
+
regexViolations.forEach((v) => {
|
|
232
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
233
|
+
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
234
|
+
});
|
|
235
|
+
if (process.env.SUNLINT_DEBUG)
|
|
236
|
+
console.log(
|
|
237
|
+
`🔧 [S038] Regex analysis completed: ${regexViolations.length} violations`
|
|
238
|
+
);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.warn(`⚠ [S038] Regex analysis failed:`, error.message);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const finalViolations = Array.from(violationMap.values()).map((v) => ({
|
|
245
|
+
...v,
|
|
246
|
+
filePath,
|
|
247
|
+
file: filePath,
|
|
248
|
+
}));
|
|
249
|
+
if (process.env.SUNLINT_DEBUG)
|
|
250
|
+
console.log(
|
|
251
|
+
`🔧 [S038] File analysis completed: ${finalViolations.length} unique violations`
|
|
252
|
+
);
|
|
253
|
+
return finalViolations;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
cleanup() {
|
|
257
|
+
if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
|
|
258
|
+
if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
module.exports = S038Analyzer;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S038",
|
|
3
|
+
"name": "Do not expose version information in response headers",
|
|
4
|
+
"category": "security",
|
|
5
|
+
"description": "S038 - Prevent exposure of server version information through response headers (Server, X-Powered-By, X-AspNet-Version, etc.) to reduce information disclosure and potential attack vectors.",
|
|
6
|
+
"severity": "warning",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"priority": "medium",
|
|
11
|
+
"fallback": "heuristic"
|
|
12
|
+
},
|
|
13
|
+
"patterns": {
|
|
14
|
+
"include": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"**/*.test.js",
|
|
17
|
+
"**/*.test.ts",
|
|
18
|
+
"**/*.spec.js",
|
|
19
|
+
"**/*.spec.ts",
|
|
20
|
+
"**/node_modules/**",
|
|
21
|
+
"**/dist/**",
|
|
22
|
+
"**/build/**"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"analysis": {
|
|
26
|
+
"approach": "symbol-based-primary",
|
|
27
|
+
"fallback": "regex-based",
|
|
28
|
+
"depth": 1,
|
|
29
|
+
"timeout": 4000
|
|
30
|
+
},
|
|
31
|
+
"validation": {
|
|
32
|
+
"headerSetters": ["setHeader", "set", "header"],
|
|
33
|
+
"versionHeaders": [
|
|
34
|
+
"Server",
|
|
35
|
+
"X-Powered-By",
|
|
36
|
+
"X-AspNet-Version",
|
|
37
|
+
"X-AspNetMvc-Version",
|
|
38
|
+
"X-Generator",
|
|
39
|
+
"X-Runtime",
|
|
40
|
+
"X-Version",
|
|
41
|
+
"X-Framework"
|
|
42
|
+
],
|
|
43
|
+
"middleware": {
|
|
44
|
+
"express": ["helmet", "disable-x-powered-by"],
|
|
45
|
+
"nestjs": ["helmet"],
|
|
46
|
+
"nextjs": ["security-headers"]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|