@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,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
+ }