@sun-asterisk/sunlint 1.3.4 → 1.3.6

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 (32) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/config/presets/all.json +49 -48
  3. package/config/presets/beginner.json +7 -18
  4. package/config/presets/ci.json +63 -27
  5. package/config/presets/maintainability.json +6 -4
  6. package/config/presets/performance.json +4 -3
  7. package/config/presets/quality.json +11 -50
  8. package/config/presets/recommended.json +83 -10
  9. package/config/presets/security.json +20 -19
  10. package/config/presets/strict.json +6 -13
  11. package/config/rule-analysis-strategies.js +5 -0
  12. package/config/rules/enhanced-rules-registry.json +87 -7
  13. package/core/config-preset-resolver.js +7 -2
  14. package/package.json +1 -1
  15. package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
  16. package/rules/common/C067_no_hardcoded_config/config.json +81 -0
  17. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
  18. package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
  19. package/rules/common/C070_no_real_time_tests/config.json +78 -0
  20. package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
  21. package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
  22. package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
  23. package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
  24. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
  25. package/rules/security/S025_server_side_validation/README.md +179 -0
  26. package/rules/security/S025_server_side_validation/analyzer.js +242 -0
  27. package/rules/security/S025_server_side_validation/config.json +111 -0
  28. package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
  29. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
  30. package/scripts/README.md +83 -0
  31. package/scripts/analyze-core-rules.js +151 -0
  32. package/scripts/generate-presets.js +202 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * S025 Regex-Based Analyzer - Always validate client-side data on the server
3
+ * Fallback analysis using regex patterns
4
+ *
5
+ * Detects common patterns of server-side validation violations:
6
+ * 1. Missing ValidationPipe in NestJS
7
+ * 2. @Body() without DTO validation
8
+ * 3. Direct req.body/req.query usage without validation
9
+ * 4. Sensitive fields trusted from client
10
+ * 5. SQL injection via string concatenation
11
+ * 6. File upload without server-side validation
12
+ */
13
+
14
+ const fs = require("fs");
15
+
16
+ class S025RegexBasedAnalyzer {
17
+ constructor(semanticEngine = null) {
18
+ this.semanticEngine = semanticEngine;
19
+ this.ruleId = "S025";
20
+ this.category = "security";
21
+
22
+ // Sensitive field patterns that should not come from client
23
+ this.sensitiveFields = [
24
+ "userId", "user_id", "id",
25
+ "role", "roles", "permissions",
26
+ "price", "amount", "total", "cost",
27
+ "isAdmin", "is_admin", "admin",
28
+ "discount", "balance", "credits",
29
+ "isActive", "is_active", "enabled",
30
+ "status", "state", "level"
31
+ ];
32
+
33
+ // Validation indicators
34
+ this.validationPatterns = [
35
+ /ValidationPipe/gi,
36
+ /class-validator/gi,
37
+ /IsString|IsInt|IsEmail|IsUUID|IsOptional|IsArray/gi,
38
+ /validateOrReject|plainToClass/gi,
39
+ /joi\.validate|yup\.validate|zod\.parse/gi,
40
+ /fileFilter|mimetype|file\.size/gi
41
+ ];
42
+
43
+ // Patterns for missing validation
44
+ this.missingValidationPatterns = [
45
+ // NestJS @Body() without DTO
46
+ {
47
+ pattern: /@Body\(\)\s+\w+:\s*any/g,
48
+ message: "Using @Body() with 'any' type - use DTO with validation instead"
49
+ },
50
+ {
51
+ pattern: /@Body\(\)\s+\w+:\s*Record<string,\s*any>/g,
52
+ message: "Using @Body() with Record<string,any> - use DTO with validation instead"
53
+ },
54
+
55
+ // Express req usage without validation
56
+ {
57
+ pattern: /(?:const|let|var)\s+\w+\s*=\s*req\.body(?:\.\w+)?(?:[^;]*;)/g,
58
+ message: "Direct use of req.body without server-side validation"
59
+ },
60
+ {
61
+ pattern: /(?:const|let|var)\s+\w+\s*=\s*req\.query(?:\.\w+)?(?:[^;]*;)/g,
62
+ message: "Direct use of req.query without server-side validation"
63
+ },
64
+ {
65
+ pattern: /(?:const|let|var)\s+\w+\s*=\s*req\.params(?:\.\w+)?(?:[^;]*;)/g,
66
+ message: "Direct use of req.params without server-side validation"
67
+ },
68
+
69
+ // SQL injection patterns
70
+ {
71
+ pattern: /\.query\s*\(\s*[`"'][^`"']*\$\{[^}]+\}[^`"']*[`"']/g,
72
+ message: "Potential SQL injection: use parameterized queries instead of template literals"
73
+ },
74
+ {
75
+ pattern: /\.query\s*\(\s*[`"'][^`"']*\+[^`"']*[`"']/g,
76
+ message: "Potential SQL injection: use parameterized queries instead of string concatenation"
77
+ },
78
+
79
+ // File upload without validation
80
+ {
81
+ pattern: /@UploadedFile\(\)\s+\w+(?![^}]*fileFilter)/g,
82
+ message: "File upload missing server-side validation (type, size, content)"
83
+ }
84
+ ];
85
+
86
+ // Sensitive field usage patterns - exclude req.user access
87
+ this.sensitiveFieldPatterns = this.sensitiveFields.map(field => ({
88
+ pattern: new RegExp(`(?:const|let|var)\\s+${field}\\s*=\\s*req\\.body(?:\\.|\\[)`, 'g'),
89
+ message: `Sensitive field "${field}" should not be trusted from client data - verify on server`
90
+ }));
91
+
92
+ // Additional patterns for destructuring from req.body
93
+ this.destructuringPatterns = this.sensitiveFields.map(field => ({
94
+ pattern: new RegExp(`\\{[^}]*\\b${field}\\b[^}]*\\}\\s*=\\s*req\\.body`, 'g'),
95
+ message: `Sensitive field "${field}" should not be trusted from client data - verify on server`
96
+ }));
97
+ }
98
+
99
+ async analyze(filePath) {
100
+ if (this.verbose) {
101
+ console.log(`🔍 [${this.ruleId}] Regex-based analysis for: ${filePath}`);
102
+ }
103
+
104
+ try {
105
+ const content = fs.readFileSync(filePath, "utf8");
106
+ return this.analyzeContent(content, filePath);
107
+ } catch (error) {
108
+ if (this.verbose) {
109
+ console.log(
110
+ `🔍 [${this.ruleId}] Regex: Error reading file:`,
111
+ error.message
112
+ );
113
+ }
114
+ return [];
115
+ }
116
+ }
117
+
118
+ analyzeContent(content, filePath) {
119
+ const violations = [];
120
+ const lines = content.split("\n");
121
+
122
+ // Remove comments to avoid false positives
123
+ const cleanContent = this.removeComments(content);
124
+
125
+ try {
126
+ // Check framework type
127
+ const isNestJS = this.isNestJSFile(cleanContent);
128
+ const isExpress = this.isExpressFile(cleanContent);
129
+
130
+ if (this.verbose) {
131
+ console.log(`🔍 [${this.ruleId}] Framework detection - NestJS: ${isNestJS}, Express: ${isExpress}`);
132
+ }
133
+
134
+ // 1. Check for missing ValidationPipe in NestJS
135
+ if (isNestJS) {
136
+ violations.push(...this.checkNestJSValidation(cleanContent, filePath));
137
+ }
138
+
139
+ // 2. Check general missing validation patterns
140
+ violations.push(...this.checkMissingValidationPatterns(cleanContent, filePath));
141
+
142
+ // 3. Check sensitive field usage
143
+ violations.push(...this.checkSensitiveFieldUsage(cleanContent, filePath));
144
+
145
+ // 4. Check Express specific patterns
146
+ if (isExpress) {
147
+ violations.push(...this.checkExpressPatterns(cleanContent, filePath));
148
+ }
149
+
150
+ // 5. Check file upload patterns
151
+ violations.push(...this.checkFileUploadPatterns(cleanContent, filePath));
152
+
153
+ } catch (error) {
154
+ if (this.verbose) {
155
+ console.log(
156
+ `🔍 [${this.ruleId}] Regex: Error in analysis:`,
157
+ error.message
158
+ );
159
+ }
160
+ }
161
+
162
+ return violations;
163
+ }
164
+
165
+ isNestJSFile(content) {
166
+ return /(@Controller|@Post|@Get|@Put|@Delete|@Body\(\)|@nestjs\/)/i.test(content);
167
+ }
168
+
169
+ isExpressFile(content) {
170
+ return /(express|req\.body|req\.query|req\.params|app\.post|app\.get)/i.test(content);
171
+ }
172
+
173
+ checkNestJSValidation(content, filePath) {
174
+ const violations = [];
175
+
176
+ // Check if ValidationPipe is configured
177
+ const hasGlobalValidationPipe = /useGlobalPipes.*ValidationPipe/i.test(content);
178
+
179
+ if (!hasGlobalValidationPipe) {
180
+ // Check individual route methods
181
+ const routeMethodPattern = /@(Post|Put|Patch|Delete)\s*\([^)]*\)[^{]*\{[^}]*@Body\(\)/gi;
182
+ let match;
183
+
184
+ while ((match = routeMethodPattern.exec(content)) !== null) {
185
+ const methodBody = this.extractMethodBody(content, match.index);
186
+
187
+ // Check if the method has validation
188
+ const hasValidation = this.validationPatterns.some(pattern =>
189
+ pattern.test(methodBody)
190
+ );
191
+
192
+ if (!hasValidation) {
193
+ const lineNumber = this.getLineNumber(content, match.index);
194
+ violations.push({
195
+ rule: this.ruleId,
196
+ source: filePath,
197
+ category: this.category,
198
+ line: lineNumber,
199
+ column: 1,
200
+ message: `NestJS route missing ValidationPipe or DTO validation`,
201
+ severity: "error",
202
+ });
203
+ }
204
+ }
205
+ }
206
+
207
+ return violations;
208
+ }
209
+
210
+ checkMissingValidationPatterns(content, filePath) {
211
+ const violations = [];
212
+
213
+ for (const patternConfig of this.missingValidationPatterns) {
214
+ let match;
215
+ while ((match = patternConfig.pattern.exec(content)) !== null) {
216
+ // Check if validation is present in the surrounding context
217
+ const context = this.getContext(content, match.index, 500);
218
+ const hasValidation = this.validationPatterns.some(pattern =>
219
+ pattern.test(context)
220
+ );
221
+
222
+ if (!hasValidation) {
223
+ const lineNumber = this.getLineNumber(content, match.index);
224
+ violations.push({
225
+ rule: this.ruleId,
226
+ source: filePath,
227
+ category: this.category,
228
+ line: lineNumber,
229
+ column: 1,
230
+ message: patternConfig.message,
231
+ severity: "error",
232
+ });
233
+ }
234
+ }
235
+ }
236
+
237
+ return violations;
238
+ }
239
+
240
+ checkSensitiveFieldUsage(content, filePath) {
241
+ const violations = [];
242
+
243
+ // Check both direct assignment and destructuring patterns
244
+ const allPatterns = [...this.sensitiveFieldPatterns, ...this.destructuringPatterns];
245
+
246
+ for (const fieldConfig of allPatterns) {
247
+ let match;
248
+ while ((match = fieldConfig.pattern.exec(content)) !== null) {
249
+ // Skip if this appears to be from req.user (auth context)
250
+ const context = this.getContext(content, match.index, 200);
251
+ if (/req\.user/i.test(context)) {
252
+ continue;
253
+ }
254
+
255
+ const lineNumber = this.getLineNumber(content, match.index);
256
+ violations.push({
257
+ rule: this.ruleId,
258
+ source: filePath,
259
+ category: this.category,
260
+ line: lineNumber,
261
+ column: 1,
262
+ message: fieldConfig.message,
263
+ severity: "error",
264
+ });
265
+ }
266
+ }
267
+
268
+ return violations;
269
+ }
270
+
271
+ checkExpressPatterns(content, filePath) {
272
+ const violations = [];
273
+
274
+ // Check for middleware usage patterns - more sophisticated check
275
+ const routePattern = /app\.(post|put|patch|delete)\s*\([^,]+,\s*([^{]*)\{[^}]*req\.body/gi;
276
+ let match;
277
+
278
+ while ((match = routePattern.exec(content)) !== null) {
279
+ const middlewareSection = match[2];
280
+
281
+ // Check if validation middleware is present
282
+ const hasValidationMiddleware = /validate|body\(|query\(|param\(/i.test(middlewareSection);
283
+
284
+ if (!hasValidationMiddleware) {
285
+ const context = this.getContext(content, match.index, 500);
286
+
287
+ // Check for nearby validation patterns or schema validation
288
+ const hasNearbyValidation = this.validationPatterns.some(pattern => pattern.test(context));
289
+ const hasSchemaValidation = /validateAsync|joi\.|yup\.|zod\./i.test(context);
290
+
291
+ if (!hasNearbyValidation && !hasSchemaValidation) {
292
+ const lineNumber = this.getLineNumber(content, match.index);
293
+ violations.push({
294
+ rule: this.ruleId,
295
+ source: filePath,
296
+ category: this.category,
297
+ line: lineNumber,
298
+ column: 1,
299
+ message: "Express route missing validation middleware",
300
+ severity: "error",
301
+ });
302
+ }
303
+ }
304
+ }
305
+
306
+ return violations;
307
+ }
308
+
309
+ checkFileUploadPatterns(content, filePath) {
310
+ const violations = [];
311
+
312
+ // Check for file upload without proper validation
313
+ const fileUploadPattern = /@UseInterceptors\s*\(\s*FileInterceptor[^)]*\)[^{]*\{[^}]*@UploadedFile\(\)/gi;
314
+ let match;
315
+
316
+ while ((match = fileUploadPattern.exec(content)) !== null) {
317
+ const context = this.getContext(content, match.index, 1000);
318
+
319
+ // Check if file validation is present
320
+ const hasFileValidation = /fileFilter|mimetype|file\.size|multer.*limits/i.test(context);
321
+
322
+ if (!hasFileValidation) {
323
+ const lineNumber = this.getLineNumber(content, match.index);
324
+ violations.push({
325
+ rule: this.ruleId,
326
+ source: filePath,
327
+ category: this.category,
328
+ line: lineNumber,
329
+ column: 1,
330
+ message: "File upload missing server-side validation (type, size, security checks)",
331
+ severity: "error",
332
+ });
333
+ }
334
+ }
335
+
336
+ return violations;
337
+ }
338
+
339
+ extractMethodBody(content, startIndex) {
340
+ // Find the opening brace
341
+ let braceCount = 0;
342
+ let i = startIndex;
343
+
344
+ // Find first opening brace
345
+ while (i < content.length && content[i] !== '{') {
346
+ i++;
347
+ }
348
+
349
+ if (i >= content.length) return "";
350
+
351
+ const start = i;
352
+ braceCount = 1;
353
+ i++;
354
+
355
+ // Find matching closing brace
356
+ while (i < content.length && braceCount > 0) {
357
+ if (content[i] === '{') braceCount++;
358
+ if (content[i] === '}') braceCount--;
359
+ i++;
360
+ }
361
+
362
+ return content.substring(start, i);
363
+ }
364
+
365
+ getContext(content, index, radius = 200) {
366
+ const start = Math.max(0, index - radius);
367
+ const end = Math.min(content.length, index + radius);
368
+ return content.substring(start, end);
369
+ }
370
+
371
+ removeComments(content) {
372
+ // Remove single-line comments
373
+ content = content.replace(/\/\/.*$/gm, "");
374
+ // Remove multi-line comments
375
+ content = content.replace(/\/\*[\s\S]*?\*\//g, "");
376
+ return content;
377
+ }
378
+
379
+ getLineNumber(content, index) {
380
+ return content.substring(0, index).split("\n").length;
381
+ }
382
+
383
+ cleanup() {
384
+ // Cleanup resources if needed
385
+ }
386
+ }
387
+
388
+ module.exports = S025RegexBasedAnalyzer;