@sun-asterisk/sunlint 1.3.4 → 1.3.5
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 +32 -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/rules/enhanced-rules-registry.json +64 -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 +1000 -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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S025 Main Analyzer - Always validate client-side data on the server
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S025 --input=examples/rule-test-fixtures/rules/S025_server_side_validation --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S025SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S025RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S025Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S025] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S025] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S025";
|
|
23
|
+
this.ruleName = "Always validate client-side data on the server";
|
|
24
|
+
this.description =
|
|
25
|
+
"Ensure all client-side data is validated on the server. Client-side validation is not sufficient for security as it can be bypassed by attackers. Server-side validation is mandatory for data integrity and security.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
30
|
+
this.config = {
|
|
31
|
+
useSymbolBased: true, // Primary approach
|
|
32
|
+
fallbackToRegex: true, // Secondary approach
|
|
33
|
+
regexBasedOnly: false, // Can be set to true for pure mode
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize analyzers
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S025SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
40
|
+
console.log(`🔧 [S025] Symbol analyzer created successfully`);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`🔧 [S025] Error creating symbol analyzer:`, error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.regexAnalyzer = new S025RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
+
console.log(`🔧 [S025] Regex analyzer created successfully`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`🔧 [S025] Error creating regex analyzer:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize analyzer with semantic engine
|
|
58
|
+
*/
|
|
59
|
+
async initialize(semanticEngine) {
|
|
60
|
+
this.semanticEngine = semanticEngine;
|
|
61
|
+
|
|
62
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
63
|
+
console.log(`🔧 [S025] Main analyzer initializing...`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize both analyzers
|
|
67
|
+
if (this.symbolAnalyzer) {
|
|
68
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
69
|
+
}
|
|
70
|
+
if (this.regexAnalyzer) {
|
|
71
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clean up if needed
|
|
75
|
+
if (this.regexAnalyzer) {
|
|
76
|
+
this.regexAnalyzer.cleanup?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.log(`🔧 [S025] Main analyzer initialized successfully`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Single file analysis method for testing
|
|
86
|
+
*/
|
|
87
|
+
analyzeSingle(filePath, options = {}) {
|
|
88
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
89
|
+
console.log(`🔧 [S025] analyzeSingle() called for: ${filePath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Return result using same format as analyze method
|
|
93
|
+
return this.analyze([filePath], "typescript", options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async analyze(files, language, options = {}) {
|
|
97
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
+
console.log(
|
|
99
|
+
`🔧 [S025] analyze() method called with ${files.length} files, language: ${language}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
for (const filePath of files) {
|
|
106
|
+
try {
|
|
107
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
108
|
+
console.log(`🔧 [S025] Processing file: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
112
|
+
violations.push(...fileViolations);
|
|
113
|
+
|
|
114
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
115
|
+
console.log(
|
|
116
|
+
`🔧 [S025] File ${filePath}: Found ${fileViolations.length} violations`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠ [S025] Analysis failed for ${filePath}:`,
|
|
122
|
+
error.message
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
128
|
+
console.log(`🔧 [S025] Total violations found: ${violations.length}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async analyzeFile(filePath, options = {}) {
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(`🔍 [S025] analyzeFile() called for: ${filePath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create a Map to track unique violations and prevent duplicates
|
|
140
|
+
const violationMap = new Map();
|
|
141
|
+
|
|
142
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
143
|
+
if (
|
|
144
|
+
this.config.useSymbolBased &&
|
|
145
|
+
this.semanticEngine?.project &&
|
|
146
|
+
this.semanticEngine?.initialized
|
|
147
|
+
) {
|
|
148
|
+
try {
|
|
149
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
150
|
+
console.log(`🔧 [S025] Trying symbol-based analysis...`);
|
|
151
|
+
}
|
|
152
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
153
|
+
if (sourceFile) {
|
|
154
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
155
|
+
console.log(`🔧 [S025] Source file found, analyzing...`);
|
|
156
|
+
}
|
|
157
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
158
|
+
sourceFile,
|
|
159
|
+
filePath
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Add to violation map with deduplication
|
|
163
|
+
symbolViolations.forEach((violation) => {
|
|
164
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
165
|
+
if (!violationMap.has(key)) {
|
|
166
|
+
violationMap.set(key, violation);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
171
|
+
console.log(
|
|
172
|
+
`🔧 [S025] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
177
|
+
console.log(`🔧 [S025] Source file not found, falling back...`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠ [S025] Symbol analysis failed:`, error.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Try Regex-based analysis (fallback or additional)
|
|
186
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
187
|
+
try {
|
|
188
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
189
|
+
console.log(`🔧 [S025] Trying regex-based analysis...`);
|
|
190
|
+
}
|
|
191
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
192
|
+
|
|
193
|
+
// Add to violation map with deduplication
|
|
194
|
+
regexViolations.forEach((violation) => {
|
|
195
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
196
|
+
if (!violationMap.has(key)) {
|
|
197
|
+
violationMap.set(key, violation);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
202
|
+
console.log(
|
|
203
|
+
`🔧 [S025] Regex analysis completed: ${regexViolations.length} violations`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`⚠ [S025] Regex analysis failed:`, error.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert Map values to array and add filePath to each violation
|
|
212
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
213
|
+
(violation) => ({
|
|
214
|
+
...violation,
|
|
215
|
+
filePath: filePath,
|
|
216
|
+
file: filePath, // Also add 'file' for compatibility
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
221
|
+
console.log(
|
|
222
|
+
`🔧 [S025] File analysis completed: ${finalViolations.length} unique violations`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return finalViolations;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up resources
|
|
231
|
+
*/
|
|
232
|
+
cleanup() {
|
|
233
|
+
if (this.symbolAnalyzer?.cleanup) {
|
|
234
|
+
this.symbolAnalyzer.cleanup();
|
|
235
|
+
}
|
|
236
|
+
if (this.regexAnalyzer?.cleanup) {
|
|
237
|
+
this.regexAnalyzer.cleanup();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = S025Analyzer;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S025",
|
|
3
|
+
"name": "Always validate client-side data on the server",
|
|
4
|
+
"description": "Ensure all client-side data is validated on the server. Client-side validation is not sufficient for security as it can be bypassed by attackers. Server-side validation is mandatory for data integrity and security.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"languages": ["typescript", "javascript"],
|
|
8
|
+
"tags": ["security", "validation", "server-side", "owasp", "input-validation"],
|
|
9
|
+
|
|
10
|
+
"patterns": {
|
|
11
|
+
"nestjs": {
|
|
12
|
+
"missingValidationPipe": {
|
|
13
|
+
"description": "NestJS routes missing ValidationPipe or DTO validation",
|
|
14
|
+
"severity": "error"
|
|
15
|
+
},
|
|
16
|
+
"unsafeBodyUsage": {
|
|
17
|
+
"description": "Using @Body() with 'any' type instead of validated DTO",
|
|
18
|
+
"severity": "error"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"express": {
|
|
22
|
+
"directReqUsage": {
|
|
23
|
+
"description": "Direct use of req.body/req.query/req.params without validation",
|
|
24
|
+
"severity": "error"
|
|
25
|
+
},
|
|
26
|
+
"missingMiddleware": {
|
|
27
|
+
"description": "Express routes missing validation middleware",
|
|
28
|
+
"severity": "error"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"sensitiveFields": {
|
|
32
|
+
"clientTrusted": {
|
|
33
|
+
"description": "Sensitive fields (userId, role, price, isAdmin) trusted from client",
|
|
34
|
+
"severity": "error",
|
|
35
|
+
"fields": ["userId", "user_id", "id", "role", "roles", "permissions", "price", "amount", "total", "cost", "isAdmin", "is_admin", "admin", "discount", "balance", "credits", "isActive", "is_active", "enabled", "status", "state"]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"sqlInjection": {
|
|
39
|
+
"stringConcatenation": {
|
|
40
|
+
"description": "SQL queries using string concatenation or template literals with user input",
|
|
41
|
+
"severity": "error"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"fileUpload": {
|
|
45
|
+
"missingValidation": {
|
|
46
|
+
"description": "File uploads missing server-side validation (type, size, content)",
|
|
47
|
+
"severity": "error"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
"validationIndicators": [
|
|
53
|
+
"ValidationPipe",
|
|
54
|
+
"class-validator",
|
|
55
|
+
"IsString", "IsInt", "IsEmail", "IsUUID", "IsOptional", "IsArray",
|
|
56
|
+
"validateOrReject", "plainToClass",
|
|
57
|
+
"joi.validate", "yup.validate", "zod.parse",
|
|
58
|
+
"fileFilter", "mimetype", "file.size"
|
|
59
|
+
],
|
|
60
|
+
|
|
61
|
+
"frameworkSupport": {
|
|
62
|
+
"nestjs": {
|
|
63
|
+
"patterns": ["@Controller", "@Post", "@Get", "@Put", "@Delete", "@Body()", "@nestjs/"],
|
|
64
|
+
"validationMethods": ["ValidationPipe", "class-validator", "DTO"]
|
|
65
|
+
},
|
|
66
|
+
"express": {
|
|
67
|
+
"patterns": ["express", "req.body", "req.query", "req.params", "app.post", "app.get"],
|
|
68
|
+
"validationMethods": ["joi", "yup", "express-validator", "middleware"]
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
"examples": {
|
|
73
|
+
"violations": [
|
|
74
|
+
{
|
|
75
|
+
"code": "@Post('/checkout') checkout(@Body() body: any) { return this.service.checkout(body); }",
|
|
76
|
+
"issue": "Using @Body() with 'any' type - vulnerable to parameter tampering"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"code": "const { userId, discount } = req.body; const order = await this.service.create(userId, discount);",
|
|
80
|
+
"issue": "Trusting sensitive fields (userId, discount) from client without validation"
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"code": "const query = `SELECT * FROM users WHERE id = ${req.params.id}`;",
|
|
84
|
+
"issue": "SQL injection via string concatenation with user input"
|
|
85
|
+
}
|
|
86
|
+
],
|
|
87
|
+
"fixes": [
|
|
88
|
+
{
|
|
89
|
+
"code": "app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }));",
|
|
90
|
+
"description": "Configure ValidationPipe globally"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"code": "@Post('/checkout') checkout(@Body() dto: CheckoutDto, @Req() req) { const userId = req.user.sub; }",
|
|
94
|
+
"description": "Use DTO validation and get userId from authenticated session"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"code": "const query = 'SELECT * FROM users WHERE id = ?'; await db.query(query, [req.params.id]);",
|
|
98
|
+
"description": "Use parameterized queries"
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
"owaspMapping": {
|
|
104
|
+
"category": "A03:2021 – Injection",
|
|
105
|
+
"subcategories": [
|
|
106
|
+
"A04:2021 – Insecure Design",
|
|
107
|
+
"A07:2021 – Identification and Authentication Failures"
|
|
108
|
+
],
|
|
109
|
+
"description": "Validates that all user input is properly validated server-side to prevent injection attacks and parameter tampering"
|
|
110
|
+
}
|
|
111
|
+
}
|