@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,337 @@
1
+ /**
2
+ * S039 Regex-Based Analyzer - Do not pass Session Tokens via URL parameters
3
+ * Detects session token exposure in URL parameters across different frameworks
4
+ */
5
+ const fs = require("fs");
6
+
7
+ class S039RegexBasedAnalyzer {
8
+ constructor() {
9
+ this.ruleId = "S039";
10
+
11
+ // Framework-specific route patterns
12
+ this.routePatterns = {
13
+ // Express.js
14
+ express:
15
+ /\b(app|router)\.(get|post|put|delete|patch|use)\s*\(\s*['"`][^'"`]+['"`]/,
16
+ // Next.js API routes (legacy handler)
17
+ nextjs: /export\s+(default\s+)?async?\s+function\s+handler\s*\(/,
18
+ // Next.js 13+ App Router HTTP methods
19
+ nextjsApp:
20
+ /export\s+async?\s+function\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s*\(/,
21
+ // NestJS controllers
22
+ nestjs: /@(Get|Post|Put|Delete|Patch)\s*\(\s*['"`][^'"`]*['"`]?\s*\)/,
23
+ // Nuxt.js server routes
24
+ nuxtjs:
25
+ /export\s+(default\s+|const\s+(GET|POST|PUT|DELETE|PATCH)\s*=\s*)?defineEventHandler\s*\(/,
26
+ };
27
+
28
+ // Session token parameter names to detect
29
+ this.sessionTokenParams = [
30
+ "sessionId",
31
+ "session_id",
32
+ "session-id",
33
+ "sessionToken",
34
+ "session_token",
35
+ "session-token",
36
+ "authToken",
37
+ "auth_token",
38
+ "auth-token",
39
+ "authorization",
40
+ "bearer",
41
+ "jwt",
42
+ "jwtToken",
43
+ "jwt_token",
44
+ "jwt-token",
45
+ "accessToken",
46
+ "access_token",
47
+ "access-token",
48
+ "refreshToken",
49
+ "refresh_token",
50
+ "refresh-token",
51
+ "apiKey",
52
+ "api_key",
53
+ "api-key",
54
+ "csrfToken",
55
+ "csrf_token",
56
+ "csrf-token",
57
+ "xsrfToken",
58
+ "xsrf_token",
59
+ "xsrf-token",
60
+ "token",
61
+ "apiToken",
62
+ "api_token",
63
+ "api-token",
64
+ "sid",
65
+ "sessionkey",
66
+ "session_key",
67
+ "session-key",
68
+ "userToken",
69
+ "user_token",
70
+ "user-token",
71
+ "authKey",
72
+ "auth_key",
73
+ "auth-key",
74
+ "securityToken",
75
+ "security_token",
76
+ "security-token",
77
+ ];
78
+
79
+ // URL parameter access patterns for different frameworks
80
+ this.urlParamPatterns = {
81
+ // Express.js: req.query.sessionToken, req.params.authToken
82
+ express: /req\.(query|params)\.(\w+)/g,
83
+ // Express.js bracket notation: req.query["access-token"]
84
+ expressBracket: /req\.(query|params)\[['"`]([^'"`]+)['"`]\]/g,
85
+ // NestJS: @Query('sessionToken'), @Param('authToken')
86
+ nestjs: /@(Query|Param)\s*\(\s*['"`](\w+)['"`]\s*\)/g,
87
+ // Next.js: searchParams.get('sessionToken')
88
+ nextjs: /searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
89
+ // Generic destructuring: { sessionToken } = req.query
90
+ destructuring: /\{\s*([^}]+)\s*\}\s*=\s*req\.(query|params)/g,
91
+ // URL constructor patterns: new URL().searchParams.get()
92
+ urlConstructor:
93
+ /new\s+URL\([^)]+\)\.searchParams\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
94
+ // URLSearchParams patterns: params.get("token")
95
+ urlSearchParams: /(\w+)\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g,
96
+ };
97
+
98
+ // Pattern to detect session token-like values
99
+ this.tokenValuePattern = /^[a-zA-Z0-9+/=\-_.]{16,}$/;
100
+ }
101
+
102
+ async analyze(filePath) {
103
+ // Skip files that are unlikely to be route handlers
104
+ const skipPatterns = [
105
+ /\.dto\.ts$/,
106
+ /\.interface\.ts$/,
107
+ /\.module\.ts$/,
108
+ /\.service\.spec\.ts$/,
109
+ /\.controller\.spec\.ts$/,
110
+ /\.spec\.ts$/,
111
+ /\.test\.ts$/,
112
+ /\.d\.ts$/,
113
+ /\.types\.ts$/,
114
+ /\.constants?\.ts$/,
115
+ /\.config\.ts$/,
116
+ ];
117
+
118
+ const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
119
+ if (shouldSkip) {
120
+ return [];
121
+ }
122
+
123
+ const content = fs.readFileSync(filePath, "utf8");
124
+ const lines = content.split(/\r?\n/);
125
+ const violations = [];
126
+
127
+ let inRoute = false;
128
+ let braceDepth = 0;
129
+ let routeStartLine = 0;
130
+ let routeType = null;
131
+ const tokenExposures = [];
132
+
133
+ // Helper functions
134
+ const reset = () => {
135
+ inRoute = false;
136
+ braceDepth = 0;
137
+ routeStartLine = 0;
138
+ routeType = null;
139
+ tokenExposures.length = 0;
140
+ };
141
+
142
+ const evaluate = () => {
143
+ // Report violations for exposed session tokens
144
+ for (const exposure of tokenExposures) {
145
+ violations.push({
146
+ ruleId: this.ruleId,
147
+ message: `Session token '${exposure.paramName}' passed via URL parameter - use secure headers or request body instead`,
148
+ severity: "warning",
149
+ line: exposure.line,
150
+ column: 1,
151
+ });
152
+ }
153
+ };
154
+
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const line = lines[i];
157
+
158
+ // Detect route start for different frameworks
159
+ if (!inRoute) {
160
+ for (const [framework, pattern] of Object.entries(this.routePatterns)) {
161
+ if (pattern.test(line)) {
162
+ if (process.env.SUNLINT_DEBUG)
163
+ console.log(
164
+ `🔧 [S039-Regex] Found ${framework} route at line ${
165
+ i + 1
166
+ }: ${line.trim()}`
167
+ );
168
+ inRoute = true;
169
+ routeStartLine = i + 1;
170
+ routeType = framework;
171
+ braceDepth =
172
+ (line.match(/\{/g) || []).length -
173
+ (line.match(/\}/g) || []).length;
174
+
175
+ // If no opening brace on this line, look ahead for it
176
+ if (braceDepth === 0) {
177
+ for (let j = i + 1; j < Math.min(i + 3, lines.length); j++) {
178
+ const nextLine = lines[j];
179
+ if (nextLine.includes("{")) {
180
+ braceDepth =
181
+ (nextLine.match(/\{/g) || []).length -
182
+ (nextLine.match(/\}/g) || []).length;
183
+ break;
184
+ }
185
+ }
186
+ }
187
+ break;
188
+ }
189
+ }
190
+ }
191
+
192
+ if (inRoute) {
193
+ // Update brace depth
194
+ braceDepth +=
195
+ (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
196
+
197
+ // Check for session token parameter access
198
+ this.checkTokenParameterAccess(line, i + 1, tokenExposures);
199
+
200
+ // End of route detection
201
+ if (
202
+ braceDepth <= 0 &&
203
+ (/\)\s*;?\s*$/.test(line) ||
204
+ /^\s*\}\s*$/.test(line) ||
205
+ /^export/.test(line))
206
+ ) {
207
+ if (process.env.SUNLINT_DEBUG) {
208
+ console.log(
209
+ `🔧 [S039-Regex] Route ended, evaluating: Token exposures=${tokenExposures.length}`
210
+ );
211
+ }
212
+ evaluate();
213
+ reset();
214
+ }
215
+ }
216
+ }
217
+
218
+ // Safety evaluate if unbalanced at file end
219
+ if (inRoute) evaluate();
220
+
221
+ return violations;
222
+ }
223
+
224
+ checkTokenParameterAccess(line, lineNumber, exposures) {
225
+ // Check each URL parameter access pattern
226
+ for (const [framework, pattern] of Object.entries(this.urlParamPatterns)) {
227
+ const matches = [...line.matchAll(pattern)];
228
+
229
+ for (const match of matches) {
230
+ let paramName = null;
231
+
232
+ if (framework === "express") {
233
+ // req.query.sessionToken or req.params.authToken
234
+ paramName = match[2];
235
+ } else if (framework === "expressBracket") {
236
+ // req.query["access-token"] or req.params["auth-token"]
237
+ paramName = match[2];
238
+ } else if (framework === "nestjs") {
239
+ // @Query('sessionToken') or @Param('authToken')
240
+ paramName = match[2];
241
+ } else if (framework === "nextjs" || framework === "urlConstructor") {
242
+ // searchParams.get('sessionToken'), new URL().searchParams.get('token')
243
+ paramName = match[1];
244
+ } else if (framework === "urlSearchParams") {
245
+ // params.get('sessionToken') - general URLSearchParams pattern
246
+ paramName = match[2];
247
+ } else if (framework === "destructuring") {
248
+ // { sessionToken, authToken } = req.query
249
+ const destructuredParams = match[1].split(",").map((p) => p.trim());
250
+ for (const param of destructuredParams) {
251
+ const cleanParam = param.replace(/['"]/g, "");
252
+ if (this.isSessionTokenParam(cleanParam)) {
253
+ exposures.push({
254
+ paramName: cleanParam,
255
+ line: lineNumber,
256
+ framework: framework,
257
+ accessType: match[2], // query or params
258
+ });
259
+ if (process.env.SUNLINT_DEBUG) {
260
+ console.log(
261
+ `🔧 [S039-Regex] Found token parameter exposure: ${cleanParam} via ${framework}`
262
+ );
263
+ }
264
+ }
265
+ }
266
+ continue; // Skip the normal parameter check below
267
+ }
268
+
269
+ if (paramName && this.isSessionTokenParam(paramName)) {
270
+ exposures.push({
271
+ paramName: paramName,
272
+ line: lineNumber,
273
+ framework: framework,
274
+ accessType: framework === "express" ? match[1] : "parameter",
275
+ });
276
+ if (process.env.SUNLINT_DEBUG) {
277
+ console.log(
278
+ `🔧 [S039-Regex] Found token parameter exposure: ${paramName} via ${framework}`
279
+ );
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ // Additional patterns for hardcoded URL parsing
286
+ const hardcodedPatterns = [
287
+ // window.location.search, location.search
288
+ /(?:window\.)?location\.search/g,
289
+ // URLSearchParams(location.search)
290
+ /URLSearchParams\s*\(\s*(?:window\.)?location\.search\s*\)/g,
291
+ // document.location.search
292
+ /document\.location\.search/g,
293
+ ];
294
+
295
+ for (const pattern of hardcodedPatterns) {
296
+ if (pattern.test(line)) {
297
+ // Look for subsequent .get() calls or parameter access in nearby lines
298
+ for (
299
+ let j = Math.max(0, lineNumber - 3);
300
+ j < Math.min(lineNumber + 3, exposures.length + lineNumber);
301
+ j++
302
+ ) {
303
+ const nearbyLine = exposures[j] || line;
304
+ const getMatches = [
305
+ ...nearbyLine.matchAll(/\.get\s*\(\s*['"`](\w+)['"`]\s*\)/g),
306
+ ];
307
+ for (const getMatch of getMatches) {
308
+ const paramName = getMatch[1];
309
+ if (this.isSessionTokenParam(paramName)) {
310
+ exposures.push({
311
+ paramName: paramName,
312
+ line: lineNumber,
313
+ framework: "client-side",
314
+ accessType: "url-search",
315
+ });
316
+ if (process.env.SUNLINT_DEBUG) {
317
+ console.log(
318
+ `🔧 [S039-Regex] Found client-side token parameter: ${paramName}`
319
+ );
320
+ }
321
+ }
322
+ }
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ isSessionTokenParam(paramName) {
329
+ return this.sessionTokenParams.some(
330
+ (tokenParam) => tokenParam.toLowerCase() === paramName.toLowerCase()
331
+ );
332
+ }
333
+
334
+ cleanup() {}
335
+ }
336
+
337
+ module.exports = S039RegexBasedAnalyzer;