@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,339 @@
1
+ /**
2
+ * S038 Regex-Based Ana // Version header patterns
3
+ this.versionHeader // Version information patterns
4
+ this.versionPatterns = [
5
+ /\d+\.\d+/, // Version numbers like 1.0, 2.1.3
6
+ /v\d+/i, // Version prefixes like v1, V2
7
+ /version/i, // The word "version"
8
+ /express/i, // Framework names
9
+ /node/i,
10
+ /nginx/i,
11
+ /apache/i,
12
+ /iis/i,
13
+ /php/i,
14
+ /asp\.net/i,
15
+ /ruby/i,
16
+ /python/i,
17
+ /django/i,
18
+ /rails/i,
19
+ /laravel/i,
20
+ /mysql/i, // Database versions
21
+ /postgresql/i,
22
+ /mongodb/i,
23
+ /redis/i,
24
+ /sqlite/i,
25
+ /mariadb/i
26
+ ];r",
27
+ "X-Powered-By",
28
+ "X-AspNet-Version",
29
+ "X-AspNetMvc-Version",
30
+ "X-Generator",
31
+ "X-Runtime",
32
+ "X-Version",
33
+ "X-Framework",
34
+ "X-Drupal-Cache",
35
+ "X-Varnish",
36
+ "X-Cache",
37
+ "X-Served-By",
38
+ "X-Database"
39
+ ];expose version information in response headers
40
+ * Detects version header exposure using pattern matching.
41
+ */
42
+ const fs = require("fs");
43
+
44
+ class S038RegexBasedAnalyzer {
45
+ constructor() {
46
+ this.ruleId = "S038";
47
+
48
+ // Framework-specific route patterns
49
+ this.routePatterns = {
50
+ // Express.js
51
+ express:
52
+ /\b(app|router)\.(get|post|put|delete|patch|use)\s*\(\s*['"`][^'"`]+['"`]/,
53
+ // Next.js API routes
54
+ nextjs: /export\s+(default\s+)?async?\s+function\s+handler\s*\(/,
55
+ // Next.js 13+ App Router
56
+ nextjsApp:
57
+ /export\s+async?\s+function\s+(GET|POST|PUT|DELETE|PATCH)\s*\(/,
58
+ // NestJS controllers
59
+ nestjs: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*['"`]?\s*\)/,
60
+ // Nuxt.js server routes
61
+ nuxtjs:
62
+ /export\s+(default\s+|const\s+(GET|POST|PUT|DELETE|PATCH)\s*=\s*)?defineEventHandler\s*\(/,
63
+ };
64
+
65
+ // Version header patterns
66
+ this.versionHeaders = [
67
+ "Server",
68
+ "X-Powered-By",
69
+ "X-AspNet-Version",
70
+ "X-AspNetMvc-Version",
71
+ "X-Generator",
72
+ "X-Runtime",
73
+ "X-Version",
74
+ "X-Framework",
75
+ "X-Drupal-Cache",
76
+ "X-Varnish",
77
+ "X-Cache",
78
+ "X-Served-By",
79
+ ];
80
+
81
+ // Header setting patterns for different frameworks
82
+ this.headerSetPatterns = {
83
+ express:
84
+ /res\.set(Header|)\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
85
+ nestjs:
86
+ /res\.header\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
87
+ nextjs:
88
+ /res\.setHeader\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
89
+ headers:
90
+ /headers\s*\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
91
+ nuxtjs: /setHeader\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i,
92
+ };
93
+
94
+ // Bulk header setting patterns
95
+ this.bulkSetPattern = /res\.set\s*\(\s*\{([^}]+)\}/i;
96
+ this.nextHeadersPattern =
97
+ /\w+\.headers\s*\.set\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*['"`]([^'"`]+)['"`]/i;
98
+
99
+ // Security middleware patterns (skip analysis if present)
100
+ this.securityMiddleware =
101
+ /helmet|hidePoweredBy|disable.*x.*powered.*by|removeHeader.*powered|noSniff/i;
102
+
103
+ // Version information patterns
104
+ this.versionPatterns = [
105
+ /\d+\.\d+/, // Version numbers like 1.0, 2.1.3
106
+ /v\d+/i, // Version prefixes like v1, V2
107
+ /version/i, // The word "version"
108
+ /express/i, // Framework names
109
+ /node/i,
110
+ /nginx/i,
111
+ /apache/i,
112
+ /iis/i,
113
+ /php/i,
114
+ /asp\.net/i,
115
+ /ruby/i,
116
+ /python/i,
117
+ /django/i,
118
+ /rails/i,
119
+ /laravel/i,
120
+ ];
121
+ }
122
+
123
+ async analyze(filePath) {
124
+ // Skip files that are unlikely to be route handlers
125
+ const skipPatterns = [
126
+ /\.dto\.ts$/,
127
+ /\.interface\.ts$/,
128
+ /\.module\.ts$/,
129
+ /\.service\.spec\.ts$/,
130
+ /\.controller\.spec\.ts$/,
131
+ /\.spec\.ts$/,
132
+ /\.test\.ts$/,
133
+ /\.d\.ts$/,
134
+ /\.types\.ts$/,
135
+ /\.constants?\.ts$/,
136
+ /\.config\.ts$/,
137
+ ];
138
+
139
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
140
+ if (shouldSkip) {
141
+ return [];
142
+ }
143
+
144
+ const content = fs.readFileSync(filePath, "utf8");
145
+ const lines = content.split(/\r?\n/);
146
+ const violations = [];
147
+
148
+ let inRoute = false;
149
+ let braceDepth = 0;
150
+ let routeStartLine = 0;
151
+ let routeType = "";
152
+ let hasSecurityMiddleware = false;
153
+ let versionExposures = [];
154
+
155
+ const reset = () => {
156
+ inRoute = false;
157
+ braceDepth = 0;
158
+ routeStartLine = 0;
159
+ routeType = "";
160
+ hasSecurityMiddleware = false;
161
+ versionExposures = [];
162
+ };
163
+
164
+ const evaluate = () => {
165
+ if (!routeStartLine) return;
166
+
167
+ if (hasSecurityMiddleware) return; // assume middleware handles header security
168
+
169
+ // Report all version header exposures found in this route
170
+ for (const exposure of versionExposures) {
171
+ violations.push({
172
+ ruleId: this.ruleId,
173
+ message: `Exposing version information in '${exposure.header}' header`,
174
+ severity: "warning",
175
+ line: exposure.line,
176
+ column: 1,
177
+ });
178
+ }
179
+ };
180
+
181
+ for (let i = 0; i < lines.length; i++) {
182
+ const line = lines[i];
183
+
184
+ // Detect route start for different frameworks
185
+ if (!inRoute) {
186
+ for (const [framework, pattern] of Object.entries(this.routePatterns)) {
187
+ if (pattern.test(line)) {
188
+ if (process.env.SUNLINT_DEBUG)
189
+ console.log(
190
+ `🔧 [S038-Regex] Found ${framework} route at line ${
191
+ i + 1
192
+ }: ${line.trim()}`
193
+ );
194
+ inRoute = true;
195
+ routeStartLine = i + 1;
196
+ routeType = framework;
197
+ braceDepth =
198
+ (line.match(/\{/g) || []).length -
199
+ (line.match(/\}/g) || []).length;
200
+ hasSecurityMiddleware = this.securityMiddleware.test(line);
201
+ if (hasSecurityMiddleware && process.env.SUNLINT_DEBUG) {
202
+ console.log(
203
+ `🔧 [S038-Regex] Security middleware detected, skipping evaluation`
204
+ );
205
+ }
206
+
207
+ // If no opening brace on this line, look ahead for it
208
+ if (braceDepth === 0) {
209
+ for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
210
+ const nextLine = lines[j];
211
+ if (nextLine.includes("{")) {
212
+ braceDepth =
213
+ (nextLine.match(/\{/g) || []).length -
214
+ (nextLine.match(/\}/g) || []).length;
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ break;
220
+ }
221
+ }
222
+ }
223
+
224
+ if (inRoute) {
225
+ // Update brace depth
226
+ braceDepth +=
227
+ (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
228
+
229
+ // Check for security middleware within route
230
+ if (this.securityMiddleware.test(line)) {
231
+ hasSecurityMiddleware = true;
232
+ }
233
+
234
+ // Check for version header setting
235
+ this.checkVersionHeaderSetting(line, i + 1, versionExposures);
236
+
237
+ // End of route detection
238
+ if (
239
+ braceDepth <= 0 &&
240
+ (/\)\s*;?\s*$/.test(line) ||
241
+ /^\s*\}\s*$/.test(line) ||
242
+ /^export/.test(line))
243
+ ) {
244
+ if (process.env.SUNLINT_DEBUG) {
245
+ console.log(
246
+ `🔧 [S038-Regex] Route ended, evaluating: Exposures=${versionExposures.length}, Security=${hasSecurityMiddleware}`
247
+ );
248
+ }
249
+ evaluate();
250
+ reset();
251
+ }
252
+ }
253
+ }
254
+
255
+ // Safety evaluate if unbalanced at file end
256
+ if (inRoute) evaluate();
257
+
258
+ return violations;
259
+ }
260
+
261
+ checkVersionHeaderSetting(line, lineNumber, exposures) {
262
+ // Check each header setting pattern
263
+ for (const [framework, pattern] of Object.entries(this.headerSetPatterns)) {
264
+ const match = pattern.exec(line);
265
+ if (match) {
266
+ const headerName = match[2] || match[1]; // Different capture groups for different patterns
267
+ const headerValue = match[3] || match[2];
268
+
269
+ if (
270
+ this.isVersionHeader(headerName) &&
271
+ this.containsVersionInfo(headerValue)
272
+ ) {
273
+ exposures.push({
274
+ header: headerName,
275
+ value: headerValue,
276
+ line: lineNumber,
277
+ framework,
278
+ });
279
+ if (process.env.SUNLINT_DEBUG) {
280
+ console.log(
281
+ `🔧 [S038-Regex] Found version header exposure: ${headerName} = ${headerValue}`
282
+ );
283
+ }
284
+ }
285
+ break; // Found a match, no need to check other patterns
286
+ }
287
+ }
288
+
289
+ // Check bulk header setting
290
+ const bulkMatch = this.bulkSetPattern.exec(line);
291
+ if (bulkMatch) {
292
+ const headersContent = bulkMatch[1];
293
+ this.checkBulkHeaders(headersContent, lineNumber, exposures);
294
+ }
295
+ }
296
+
297
+ checkBulkHeaders(headersContent, lineNumber, exposures) {
298
+ // Parse object literal headers like { "X-Powered-By": "Express", "Server": "nginx/1.18" }
299
+ const headerMatches = headersContent.matchAll(
300
+ /['"`]([^'"`]+)['"`]\s*:\s*['"`]([^'"`]+)['"`]/g
301
+ );
302
+
303
+ for (const match of headerMatches) {
304
+ const headerName = match[1];
305
+ const headerValue = match[2];
306
+
307
+ if (
308
+ this.isVersionHeader(headerName) &&
309
+ this.containsVersionInfo(headerValue)
310
+ ) {
311
+ exposures.push({
312
+ header: headerName,
313
+ value: headerValue,
314
+ line: lineNumber,
315
+ framework: "bulk",
316
+ });
317
+ if (process.env.SUNLINT_DEBUG) {
318
+ console.log(
319
+ `🔧 [S038-Regex] Found bulk version header exposure: ${headerName} = ${headerValue}`
320
+ );
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ isVersionHeader(headerName) {
327
+ return this.versionHeaders.some(
328
+ (vh) => vh.toLowerCase() === headerName.toLowerCase()
329
+ );
330
+ }
331
+
332
+ containsVersionInfo(value) {
333
+ return this.versionPatterns.some((pattern) => pattern.test(value));
334
+ }
335
+
336
+ cleanup() {}
337
+ }
338
+
339
+ module.exports = S038RegexBasedAnalyzer;
@@ -0,0 +1,375 @@
1
+ /**
2
+ * S038 Sy "X-Generator",
3
+ "X-Runtime",
4
+ "X-Version",
5
+ "X-Framework",
6
+ "X-Drupal-Cache",
7
+ "X-Varnish",
8
+ "X-Cache",
9
+ "X-Served-By",
10
+ "X-Database"
11
+ ];yzer - Do not expose version information in response headers
12
+ * Detects version header exposure in route handlers
13
+ */
14
+
15
+ class S038SymbolBasedAnalyzer {
16
+ constructor(semanticEngine) {
17
+ this.ruleId = "S038";
18
+ this.semanticEngine = semanticEngine;
19
+ this.versionHeaders = [
20
+ "Server",
21
+ "X-Powered-By",
22
+ "X-AspNet-Version",
23
+ "X-AspNetMvc-Version",
24
+ "X-Generator",
25
+ "X-Runtime",
26
+ "X-Version",
27
+ "X-Framework",
28
+ "X-Drupal-Cache",
29
+ "X-Varnish",
30
+ "X-Cache",
31
+ "X-Served-By",
32
+ "X-Database",
33
+ "X-Framework",
34
+ "X-Drupal-Cache",
35
+ "X-Varnish",
36
+ "X-Cache",
37
+ "X-Served-By",
38
+ ];
39
+ this.securityMiddleware =
40
+ /helmet|hidePoweredBy|disable.*x.*powered.*by|removeHeader.*powered|noSniff/i;
41
+ }
42
+
43
+ async initialize() {}
44
+
45
+ analyze(sourceFile, filePath) {
46
+ const violations = [];
47
+
48
+ // Skip files that are unlikely to be route handlers
49
+ const skipPatterns = [
50
+ /\.dto\.ts$/,
51
+ /\.interface\.ts$/,
52
+ /\.module\.ts$/,
53
+ /\.service\.spec\.ts$/,
54
+ /\.controller\.spec\.ts$/,
55
+ /\.spec\.ts$/,
56
+ /\.test\.ts$/,
57
+ /\.d\.ts$/,
58
+ /\.types\.ts$/,
59
+ /\.constants?\.ts$/,
60
+ /\.config\.ts$/,
61
+ ];
62
+
63
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
64
+ if (shouldSkip) {
65
+ return violations;
66
+ }
67
+
68
+ try {
69
+ const { SyntaxKind } = require("ts-morph");
70
+
71
+ // Find all function expressions and arrow functions that could be route handlers
72
+ const routeHandlers = [];
73
+
74
+ // Express route patterns: app.get("/path", (req, res) => {...})
75
+ const callExpressions = sourceFile.getDescendantsOfKind(
76
+ SyntaxKind.CallExpression
77
+ );
78
+
79
+ for (const call of callExpressions) {
80
+ const expression = call.getExpression();
81
+
82
+ // Check for Express route methods
83
+ if (/\.(get|post|put|delete|patch|use)$/.test(expression.getText())) {
84
+ const args = call.getArguments();
85
+ if (args.length >= 2) {
86
+ const lastArg = args[args.length - 1];
87
+ // The last argument should be the handler function
88
+ if (
89
+ lastArg.getKind() === SyntaxKind.ArrowFunction ||
90
+ lastArg.getKind() === SyntaxKind.FunctionExpression
91
+ ) {
92
+ routeHandlers.push({
93
+ handler: lastArg,
94
+ routeCall: call,
95
+ type: "express",
96
+ });
97
+ }
98
+ }
99
+ }
100
+ }
101
+
102
+ // Next.js export functions
103
+ const exportAssignments = sourceFile.getDescendantsOfKind(
104
+ SyntaxKind.ExportAssignment
105
+ );
106
+ const exportDeclarations = sourceFile.getDescendantsOfKind(
107
+ SyntaxKind.ExportDeclaration
108
+ );
109
+ const functionDeclarations = sourceFile.getDescendantsOfKind(
110
+ SyntaxKind.FunctionDeclaration
111
+ );
112
+
113
+ for (const func of functionDeclarations) {
114
+ const name = func.getName();
115
+ if (name && /^(GET|POST|PUT|DELETE|PATCH|handler)$/.test(name)) {
116
+ routeHandlers.push({
117
+ handler: func,
118
+ type: "nextjs",
119
+ });
120
+ }
121
+ }
122
+
123
+ // NestJS Controller methods with decorators
124
+ const methodDeclarations = sourceFile.getDescendantsOfKind(
125
+ SyntaxKind.MethodDeclaration
126
+ );
127
+
128
+ for (const method of methodDeclarations) {
129
+ const decorators = method.getDecorators();
130
+ const hasRouteDecorator = decorators.some((decorator) => {
131
+ const decoratorName = decorator.getName();
132
+ return /^(Get|Post|Put|Delete|Patch|All)$/.test(decoratorName);
133
+ });
134
+
135
+ if (hasRouteDecorator) {
136
+ routeHandlers.push({
137
+ handler: method,
138
+ type: "nestjs",
139
+ });
140
+ }
141
+ }
142
+
143
+ // Nuxt.js defineEventHandler patterns
144
+ for (const call of callExpressions) {
145
+ const expression = call.getExpression();
146
+ if (expression.getKind() === SyntaxKind.Identifier) {
147
+ const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
148
+ if (identifier.getText() === "defineEventHandler") {
149
+ // Find the arrow function or function parameter
150
+ const args = call.getArguments();
151
+ if (args.length > 0) {
152
+ const firstArg = args[0];
153
+ if (
154
+ firstArg.getKind() === SyntaxKind.ArrowFunction ||
155
+ firstArg.getKind() === SyntaxKind.FunctionExpression
156
+ ) {
157
+ routeHandlers.push({
158
+ handler: firstArg,
159
+ type: "nuxtjs",
160
+ });
161
+ }
162
+ }
163
+ }
164
+ }
165
+ }
166
+
167
+ // Nuxt.js named exports (GET, POST, etc.)
168
+ const nuxtExportAssignments = sourceFile.getDescendantsOfKind(
169
+ SyntaxKind.ExportAssignment
170
+ );
171
+
172
+ for (const exportAssign of nuxtExportAssignments) {
173
+ const expr = exportAssign.getExpression();
174
+ if (expr.getKind() === SyntaxKind.ArrowFunction) {
175
+ routeHandlers.push({
176
+ handler: expr,
177
+ type: "nuxtjs",
178
+ });
179
+ }
180
+ }
181
+
182
+ // Analyze each route handler
183
+ for (const route of routeHandlers) {
184
+ const handlerViolations = this.analyzeRouteHandler(route);
185
+ violations.push(...handlerViolations);
186
+ }
187
+
188
+ // Only analyze actual route handlers - no file-level checking
189
+ // File-level checking removed to prevent false positives on DTO, module, service files
190
+ } catch (err) {
191
+ // Fallback - don't report errors from symbol analysis
192
+ console.warn(`Symbol analysis failed for ${filePath}:`, err.message);
193
+ }
194
+
195
+ return violations;
196
+ }
197
+
198
+ analyzeRouteHandler(route) {
199
+ const violations = [];
200
+ const { handler, routeCall, type } = route;
201
+
202
+ // Check if route contains security middleware (skip analysis if present)
203
+ const handlerText = handler.getFullText();
204
+ const routeText = routeCall ? routeCall.getFullText() : handlerText;
205
+
206
+ if (
207
+ this.securityMiddleware.test(routeText) ||
208
+ this.securityMiddleware.test(handlerText)
209
+ ) {
210
+ return violations; // Middleware handles header security
211
+ }
212
+
213
+ // Find version header exposures within this handler
214
+ const versionExposures = this.findVersionHeadersInNode(handler);
215
+
216
+ // Report violations for exposed version headers
217
+ for (const exposure of versionExposures) {
218
+ const startLine = exposure.node.getStartLineNumber();
219
+ violations.push({
220
+ ruleId: this.ruleId,
221
+ message: `Exposing version information in '${exposure.header}' header`,
222
+ severity: "warning",
223
+ line: startLine,
224
+ column: 1,
225
+ });
226
+ }
227
+
228
+ return violations;
229
+ }
230
+
231
+ findVersionHeadersInNode(node) {
232
+ const exposures = [];
233
+
234
+ try {
235
+ const { SyntaxKind } = require("ts-morph");
236
+
237
+ // Find all call expressions that set headers
238
+ const callExpressions = node.getDescendantsOfKind(
239
+ SyntaxKind.CallExpression
240
+ );
241
+
242
+ for (const call of callExpressions) {
243
+ const expression = call.getExpression();
244
+ const args = call.getArguments();
245
+
246
+ if (args.length >= 2) {
247
+ // Check different header setting patterns
248
+ const expressionText = expression.getText();
249
+
250
+ // Express/NestJS: res.setHeader("HeaderName", "value")
251
+ if (/\.(setHeader|set|header)$/.test(expressionText)) {
252
+ const headerArg = args[0];
253
+ const valueArg = args[1];
254
+
255
+ if (headerArg.getKind() === SyntaxKind.StringLiteral) {
256
+ const headerName = headerArg.getLiteralValue();
257
+ const isVersionHeader = this.versionHeaders.some(
258
+ (vh) => vh.toLowerCase() === headerName.toLowerCase()
259
+ );
260
+
261
+ if (isVersionHeader) {
262
+ // Check if the value contains version information
263
+ let hasVersionInfo = false;
264
+
265
+ if (valueArg.getKind() === SyntaxKind.StringLiteral) {
266
+ const value = valueArg.getLiteralValue();
267
+ hasVersionInfo = this.containsVersionInfo(value);
268
+ } else {
269
+ // For non-literal values, assume they might contain version info
270
+ hasVersionInfo = true;
271
+ }
272
+
273
+ if (hasVersionInfo) {
274
+ exposures.push({
275
+ node: call,
276
+ header: headerName,
277
+ value: valueArg.getText(),
278
+ });
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ // Next.js: headers.set("HeaderName", "value")
285
+ if (/headers\s*\.\s*set$/.test(expressionText)) {
286
+ const headerArg = args[0];
287
+ const valueArg = args[1];
288
+
289
+ if (headerArg.getKind() === SyntaxKind.StringLiteral) {
290
+ const headerName = headerArg.getLiteralValue();
291
+ const isVersionHeader = this.versionHeaders.some(
292
+ (vh) => vh.toLowerCase() === headerName.toLowerCase()
293
+ );
294
+
295
+ if (isVersionHeader) {
296
+ exposures.push({
297
+ node: call,
298
+ header: headerName,
299
+ value: valueArg.getText(),
300
+ });
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ // Check for object literal header settings
308
+ const objectLiterals = node.getDescendantsOfKind(
309
+ SyntaxKind.ObjectLiteralExpression
310
+ );
311
+
312
+ for (const obj of objectLiterals) {
313
+ const properties = obj.getProperties();
314
+
315
+ for (const prop of properties) {
316
+ if (prop.getKind() === SyntaxKind.PropertyAssignment) {
317
+ const nameNode = prop.getNameNode();
318
+
319
+ if (nameNode.getKind() === SyntaxKind.StringLiteral) {
320
+ const headerName = nameNode.getLiteralValue();
321
+ const isVersionHeader = this.versionHeaders.some(
322
+ (vh) => vh.toLowerCase() === headerName.toLowerCase()
323
+ );
324
+
325
+ if (isVersionHeader) {
326
+ exposures.push({
327
+ node: prop,
328
+ header: headerName,
329
+ value: prop.getInitializer()?.getText() || "unknown",
330
+ });
331
+ }
332
+ }
333
+ }
334
+ }
335
+ }
336
+ } catch (error) {
337
+ console.warn("Error analyzing version headers:", error.message);
338
+ }
339
+
340
+ return exposures;
341
+ }
342
+
343
+ containsVersionInfo(value) {
344
+ // Check if the value contains version patterns
345
+ const versionPatterns = [
346
+ /\d+\.\d+/, // Version numbers like 1.0, 2.1.3
347
+ /v\d+/i, // Version prefixes like v1, V2
348
+ /version/i, // The word "version"
349
+ /express/i, // Framework names
350
+ /node/i,
351
+ /nginx/i,
352
+ /apache/i,
353
+ /iis/i,
354
+ /php/i,
355
+ /asp\.net/i,
356
+ /ruby/i,
357
+ /python/i,
358
+ /django/i,
359
+ /rails/i,
360
+ /laravel/i,
361
+ /mysql/i, // Database versions
362
+ /postgresql/i,
363
+ /mongodb/i,
364
+ /redis/i,
365
+ /sqlite/i,
366
+ /mariadb/i,
367
+ ];
368
+
369
+ return versionPatterns.some((pattern) => pattern.test(value));
370
+ }
371
+
372
+ cleanup() {}
373
+ }
374
+
375
+ module.exports = S038SymbolBasedAnalyzer;