@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.
- package/CHANGELOG.md +62 -0
- package/config/presets/all.json +49 -48
- package/config/presets/beginner.json +7 -18
- package/config/presets/ci.json +63 -27
- package/config/presets/maintainability.json +6 -4
- package/config/presets/performance.json +4 -3
- package/config/presets/quality.json +11 -50
- package/config/presets/recommended.json +83 -10
- package/config/presets/security.json +20 -19
- package/config/presets/strict.json +6 -13
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +87 -7
- package/core/config-preset-resolver.js +7 -2
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
- package/rules/common/C067_no_hardcoded_config/config.json +81 -0
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
- package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
- package/rules/common/C070_no_real_time_tests/config.json +78 -0
- package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
- package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
- package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
- package/rules/security/S025_server_side_validation/README.md +179 -0
- package/rules/security/S025_server_side_validation/analyzer.js +242 -0
- package/rules/security/S025_server_side_validation/config.json +111 -0
- package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
- package/scripts/README.md +83 -0
- package/scripts/analyze-core-rules.js +151 -0
- 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;
|