@sun-asterisk/sunlint 1.2.1 → 1.2.2
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/config/rule-analysis-strategies.js +18 -2
- package/engines/eslint-engine.js +9 -11
- package/engines/heuristic-engine.js +55 -31
- package/package.json +2 -1
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/common/C006_function_naming/analyzer.js +504 -0
- package/rules/common/C006_function_naming/config.json +86 -0
- package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C012_command_query_separation/analyzer.js +481 -0
- package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +362 -0
- package/rules/common/C019_log_level_usage/config.json +121 -0
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
- package/rules/common/C031_validation_separation/analyzer.js +186 -0
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/docs/C031_validation_separation.md +72 -0
- package/rules/index.js +155 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/parser/constants.js +31 -0
- package/rules/parser/file-config.js +80 -0
- package/rules/parser/rule-parser-simple.js +305 -0
- package/rules/parser/rule-parser.js +527 -0
- package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
- package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
- package/rules/security/S023_no_json_injection/analyzer.js +278 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +330 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/generate_insights.js +188 -0
- package/scripts/merge-reports.js +0 -424
- package/scripts/test-scripts/README.md +0 -22
- package/scripts/test-scripts/test-c041-comparison.js +0 -114
- package/scripts/test-scripts/test-c041-eslint.js +0 -67
- package/scripts/test-scripts/test-eslint-rules.js +0 -146
- package/scripts/test-scripts/test-real-world.js +0 -44
- package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class S023ASTAnalyzer {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.ruleId = 'S023';
|
|
7
|
+
this.ruleName = 'No JSON Injection Prevention (AST-Enhanced)';
|
|
8
|
+
this.description = 'AST-based detection of unsafe JSON parsing and injection vulnerabilities';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async analyze(files, language, options = {}) {
|
|
12
|
+
const violations = [];
|
|
13
|
+
|
|
14
|
+
for (const filePath of files) {
|
|
15
|
+
if (options.verbose) {
|
|
16
|
+
console.log(`🎯 Running S023 AST analysis on ${path.basename(filePath)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
21
|
+
const fileViolations = await this.analyzeFile(filePath, content, language, options);
|
|
22
|
+
violations.push(...fileViolations);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return violations;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async analyzeFile(filePath, content, language, config) {
|
|
32
|
+
switch (language) {
|
|
33
|
+
case 'typescript':
|
|
34
|
+
case 'javascript':
|
|
35
|
+
return this.analyzeJSTS(filePath, content, config);
|
|
36
|
+
default:
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyzeJSTS(filePath, content, config) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Try AST analysis first (like ESLint approach)
|
|
46
|
+
const astViolations = await this.analyzeWithAST(filePath, content, config);
|
|
47
|
+
if (astViolations.length > 0) {
|
|
48
|
+
violations.push(...astViolations);
|
|
49
|
+
}
|
|
50
|
+
} catch (astError) {
|
|
51
|
+
if (config.verbose) {
|
|
52
|
+
console.log(`⚠️ AST analysis failed for ${path.basename(filePath)}, falling back to regex`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback to regex-based analysis
|
|
56
|
+
const regexViolations = await this.analyzeWithRegex(filePath, content, config);
|
|
57
|
+
violations.push(...regexViolations);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async analyzeWithAST(filePath, content, config) {
|
|
64
|
+
const violations = [];
|
|
65
|
+
|
|
66
|
+
// Import AST modules dynamically
|
|
67
|
+
let astModules;
|
|
68
|
+
try {
|
|
69
|
+
astModules = require('../../../core/ast-modules');
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new Error('AST modules not available');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Try to parse with AST
|
|
75
|
+
let ast;
|
|
76
|
+
try {
|
|
77
|
+
ast = await astModules.parseCode(content, 'javascript', filePath);
|
|
78
|
+
if (!ast) {
|
|
79
|
+
throw new Error('AST parsing returned null');
|
|
80
|
+
}
|
|
81
|
+
} catch (parseError) {
|
|
82
|
+
throw new Error(`Parse error: ${parseError.message}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Traverse AST to find JSON injection vulnerabilities - mimicking ESLint's approach
|
|
86
|
+
const rootNode = ast.program || ast;
|
|
87
|
+
this.traverseAST(rootNode, (node) => {
|
|
88
|
+
// Check JSON.parse() calls
|
|
89
|
+
if (this.isJsonParseCall(node)) {
|
|
90
|
+
const violation = this.checkJsonParseForUnsafeUsage(node, filePath, content);
|
|
91
|
+
if (violation) {
|
|
92
|
+
violations.push(violation);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check eval() with JSON patterns
|
|
97
|
+
if (this.isEvalCall(node)) {
|
|
98
|
+
const violation = this.checkEvalForJsonUsage(node, filePath, content);
|
|
99
|
+
if (violation) {
|
|
100
|
+
violations.push(violation);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check JSON.stringify in HTML context
|
|
105
|
+
if (this.isJsonStringifyCall(node)) {
|
|
106
|
+
const violation = this.checkJsonStringifyInHtmlContext(node, filePath, content);
|
|
107
|
+
if (violation) {
|
|
108
|
+
violations.push(violation);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return violations;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
traverseAST(node, callback) {
|
|
117
|
+
if (!node || typeof node !== 'object') return;
|
|
118
|
+
|
|
119
|
+
callback(node);
|
|
120
|
+
|
|
121
|
+
for (const key in node) {
|
|
122
|
+
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
123
|
+
|
|
124
|
+
const child = node[key];
|
|
125
|
+
if (Array.isArray(child)) {
|
|
126
|
+
child.forEach(item => this.traverseAST(item, callback));
|
|
127
|
+
} else if (child && typeof child === 'object') {
|
|
128
|
+
this.traverseAST(child, callback);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
isJsonParseCall(node) {
|
|
134
|
+
return node.type === 'CallExpression' &&
|
|
135
|
+
node.callee &&
|
|
136
|
+
node.callee.type === 'MemberExpression' &&
|
|
137
|
+
node.callee.object && node.callee.object.name === 'JSON' &&
|
|
138
|
+
node.callee.property && node.callee.property.name === 'parse';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
isEvalCall(node) {
|
|
142
|
+
return node.type === 'CallExpression' &&
|
|
143
|
+
node.callee &&
|
|
144
|
+
node.callee.type === 'Identifier' &&
|
|
145
|
+
node.callee.name === 'eval';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
isJsonStringifyCall(node) {
|
|
149
|
+
return node.type === 'CallExpression' &&
|
|
150
|
+
node.callee &&
|
|
151
|
+
node.callee.type === 'MemberExpression' &&
|
|
152
|
+
node.callee.object && node.callee.object.name === 'JSON' &&
|
|
153
|
+
node.callee.property && node.callee.property.name === 'stringify';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
checkJsonParseForUnsafeUsage(node, filePath, content) {
|
|
157
|
+
if (!node.arguments || node.arguments.length === 0) return null;
|
|
158
|
+
|
|
159
|
+
const lines = content.split('\n');
|
|
160
|
+
const lineNumber = node.loc.start.line;
|
|
161
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
162
|
+
|
|
163
|
+
// Check if the argument is from user input (similar to ESLint logic)
|
|
164
|
+
const argument = node.arguments[0];
|
|
165
|
+
if (this.isUserInputSource(argument, content)) {
|
|
166
|
+
// Check if there's validation before JSON.parse
|
|
167
|
+
if (!this.hasValidationBefore(node, content)) {
|
|
168
|
+
return {
|
|
169
|
+
ruleId: this.ruleId,
|
|
170
|
+
file: filePath,
|
|
171
|
+
line: lineNumber,
|
|
172
|
+
column: node.loc.start.column + 1,
|
|
173
|
+
message: 'Unsafe JSON parsing - validate input before parsing',
|
|
174
|
+
severity: 'warning',
|
|
175
|
+
code: lineText.trim(),
|
|
176
|
+
type: 'unsafe_json_parse',
|
|
177
|
+
confidence: 0.8,
|
|
178
|
+
suggestion: 'Validate input before parsing JSON'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
checkEvalForJsonUsage(node, filePath, content) {
|
|
187
|
+
if (!node.arguments || node.arguments.length === 0) return null;
|
|
188
|
+
|
|
189
|
+
const lines = content.split('\n');
|
|
190
|
+
const lineNumber = node.loc.start.line;
|
|
191
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
192
|
+
|
|
193
|
+
// Check if eval contains JSON patterns
|
|
194
|
+
const argument = node.arguments[0];
|
|
195
|
+
if (this.containsJsonPattern(argument, content)) {
|
|
196
|
+
return {
|
|
197
|
+
ruleId: this.ruleId,
|
|
198
|
+
file: filePath,
|
|
199
|
+
line: lineNumber,
|
|
200
|
+
column: node.loc.start.column + 1,
|
|
201
|
+
message: 'Never use eval() to process JSON data - use JSON.parse() instead',
|
|
202
|
+
severity: 'error',
|
|
203
|
+
code: lineText.trim(),
|
|
204
|
+
type: 'eval_json',
|
|
205
|
+
confidence: 0.9,
|
|
206
|
+
suggestion: 'Use JSON.parse() instead of eval()'
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
checkJsonStringifyInHtmlContext(node, filePath, content) {
|
|
214
|
+
const lines = content.split('\n');
|
|
215
|
+
const lineNumber = node.loc.start.line;
|
|
216
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
217
|
+
|
|
218
|
+
// Check if JSON.stringify is used in HTML context
|
|
219
|
+
if (this.isInHtmlContext(node, content)) {
|
|
220
|
+
return {
|
|
221
|
+
ruleId: this.ruleId,
|
|
222
|
+
file: filePath,
|
|
223
|
+
line: lineNumber,
|
|
224
|
+
column: node.loc.start.column + 1,
|
|
225
|
+
message: 'JSON.stringify output should be escaped when used in HTML context',
|
|
226
|
+
severity: 'warning',
|
|
227
|
+
code: lineText.trim(),
|
|
228
|
+
type: 'json_stringify_html',
|
|
229
|
+
confidence: 0.7,
|
|
230
|
+
suggestion: 'Escape JSON.stringify output in HTML context'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
isUserInputSource(node, content) {
|
|
238
|
+
if (!node) return false;
|
|
239
|
+
|
|
240
|
+
// Check for common user input patterns (similar to ESLint)
|
|
241
|
+
const userInputPatterns = [
|
|
242
|
+
/localStorage\.getItem/,
|
|
243
|
+
/sessionStorage\.getItem/,
|
|
244
|
+
/window\.location/,
|
|
245
|
+
/location\.(search|hash)/,
|
|
246
|
+
/URLSearchParams/,
|
|
247
|
+
/req\.(body|query|params)/,
|
|
248
|
+
/request\.(body|query|params)/
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
return userInputPatterns.some(pattern => {
|
|
252
|
+
const nodeText = this.getNodeText(node, content);
|
|
253
|
+
return pattern.test(nodeText);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
hasValidationBefore(node, content) {
|
|
258
|
+
// Simple check for validation patterns before JSON.parse
|
|
259
|
+
const lines = content.split('\n');
|
|
260
|
+
const lineNumber = node.loc.start.line;
|
|
261
|
+
|
|
262
|
+
// Check previous lines for validation patterns
|
|
263
|
+
for (let i = Math.max(0, lineNumber - 5); i < lineNumber - 1; i++) {
|
|
264
|
+
const line = lines[i] || '';
|
|
265
|
+
if (this.containsValidationPattern(line)) {
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
containsValidationPattern(line) {
|
|
274
|
+
const validationPatterns = [
|
|
275
|
+
/try\s*{/,
|
|
276
|
+
/catch\s*\(/,
|
|
277
|
+
/if\s*\(/,
|
|
278
|
+
/typeof\s+/,
|
|
279
|
+
/instanceof\s+/,
|
|
280
|
+
/\.length\s*>/,
|
|
281
|
+
/validate/i,
|
|
282
|
+
/check/i,
|
|
283
|
+
/isValid/i
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
return validationPatterns.some(pattern => pattern.test(line));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
containsJsonPattern(node, content) {
|
|
290
|
+
const nodeText = this.getNodeText(node, content);
|
|
291
|
+
return /json|JSON|\{|\[/.test(nodeText);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
isInHtmlContext(node, content) {
|
|
295
|
+
// Check if JSON.stringify is used in HTML context
|
|
296
|
+
const htmlPatterns = [
|
|
297
|
+
/innerHTML/,
|
|
298
|
+
/outerHTML/,
|
|
299
|
+
/insertAdjacentHTML/,
|
|
300
|
+
/document\.write/,
|
|
301
|
+
/\.html\(/
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const surroundingText = this.getSurroundingText(node, content, 3);
|
|
305
|
+
return htmlPatterns.some(pattern => pattern.test(surroundingText));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
getNodeText(node, content) {
|
|
309
|
+
if (!node || !node.loc) return '';
|
|
310
|
+
const lines = content.split('\n');
|
|
311
|
+
return lines[node.loc.start.line - 1] || '';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
getSurroundingText(node, content, radius = 2) {
|
|
315
|
+
if (!node || !node.loc) return '';
|
|
316
|
+
const lines = content.split('\n');
|
|
317
|
+
const lineNumber = node.loc.start.line;
|
|
318
|
+
|
|
319
|
+
const startLine = Math.max(0, lineNumber - radius - 1);
|
|
320
|
+
const endLine = Math.min(lines.length, lineNumber + radius);
|
|
321
|
+
|
|
322
|
+
return lines.slice(startLine, endLine).join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async analyzeWithRegex(filePath, content, config) {
|
|
326
|
+
// Fallback regex analysis for basic JSON.parse detection
|
|
327
|
+
const violations = [];
|
|
328
|
+
const lines = content.split('\n');
|
|
329
|
+
|
|
330
|
+
const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
|
|
331
|
+
let match;
|
|
332
|
+
|
|
333
|
+
while ((match = jsonParsePattern.exec(content)) !== null) {
|
|
334
|
+
const line = content.substring(0, match.index).split('\n').length;
|
|
335
|
+
const lineText = lines[line - 1] || '';
|
|
336
|
+
|
|
337
|
+
// Simple check for unsafe patterns
|
|
338
|
+
const argument = match[1];
|
|
339
|
+
if (/localStorage\.getItem|sessionStorage\.getItem/.test(argument)) {
|
|
340
|
+
violations.push({
|
|
341
|
+
ruleId: this.ruleId,
|
|
342
|
+
file: filePath,
|
|
343
|
+
line: line,
|
|
344
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
345
|
+
message: 'Unsafe JSON parsing - validate input before parsing',
|
|
346
|
+
severity: 'warning',
|
|
347
|
+
code: lineText.trim(),
|
|
348
|
+
type: 'unsafe_json_parse_regex',
|
|
349
|
+
confidence: 0.6,
|
|
350
|
+
suggestion: 'Validate input before parsing JSON'
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return violations;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = new S023ASTAnalyzer();
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic analyzer for: S026 – JSON Schema Validation cho dữ liệu đầu vào
|
|
3
|
+
* Purpose: Detect unvalidated JSON inputs while avoiding false positives on styles/config objects
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class S026Analyzer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.ruleId = 'S026';
|
|
9
|
+
this.ruleName = 'JSON Schema Validation Required';
|
|
10
|
+
this.description = 'Áp dụng JSON Schema Validation cho dữ liệu đầu vào để đảm bảo an toàn';
|
|
11
|
+
|
|
12
|
+
// Patterns that indicate actual HTTP/API input (should be validated)
|
|
13
|
+
this.httpInputPatterns = [
|
|
14
|
+
'req.body', 'req.query', 'request.body', 'request.query',
|
|
15
|
+
'ctx.body', 'ctx.query', 'context.body', 'context.query',
|
|
16
|
+
'event.body', 'event.queryStringParameters'
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
// Patterns that are NOT JSON inputs (should not be flagged)
|
|
20
|
+
this.nonInputPatterns = [
|
|
21
|
+
'styles.', 'css.', 'theme.', 'colors.',
|
|
22
|
+
'config.', 'settings.', 'options.',
|
|
23
|
+
'data.', 'props.', 'state.',
|
|
24
|
+
'const.', 'static.', 'default.'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Validation patterns that indicate input is being validated
|
|
28
|
+
this.validationPatterns = [
|
|
29
|
+
'schema.validate', 'joi.validate', 'ajv.validate',
|
|
30
|
+
'validate(', 'validateInput(', 'validateBody(',
|
|
31
|
+
'isValid(', 'checkSchema(', 'parseSchema(',
|
|
32
|
+
'.validate(', '.valid(', '.check('
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Express/HTTP framework patterns
|
|
36
|
+
this.httpFrameworkPatterns = [
|
|
37
|
+
'app.post(', 'app.put(', 'app.patch(',
|
|
38
|
+
'router.post(', 'router.put(', 'router.patch(',
|
|
39
|
+
'express()', '.post(', '.put(', '.patch('
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async analyze(files, language, options = {}) {
|
|
44
|
+
const violations = [];
|
|
45
|
+
|
|
46
|
+
for (const filePath of files) {
|
|
47
|
+
if (options.verbose) {
|
|
48
|
+
console.log(`🔍 Running S026 analysis on ${require('path').basename(filePath)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
53
|
+
const fileViolations = this.analyzeFile(content, filePath);
|
|
54
|
+
violations.push(...fileViolations);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
analyzeFile(content, filePath) {
|
|
64
|
+
const violations = [];
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
|
|
67
|
+
// Find all potential JSON inputs
|
|
68
|
+
const potentialInputs = this.findPotentialInputs(lines);
|
|
69
|
+
|
|
70
|
+
// Check if they're validated
|
|
71
|
+
const validatedInputs = this.findValidatedInputs(content);
|
|
72
|
+
|
|
73
|
+
// Report unvalidated inputs
|
|
74
|
+
potentialInputs.forEach(input => {
|
|
75
|
+
if (!this.isInputValidated(input, validatedInputs) &&
|
|
76
|
+
this.isActualJSONInput(input, content)) {
|
|
77
|
+
violations.push({
|
|
78
|
+
file: filePath,
|
|
79
|
+
line: input.line,
|
|
80
|
+
column: input.column,
|
|
81
|
+
message: `JSON input '${input.expression}' should be validated using a JSON schema before use. Consider using schema.validate(), joi.validate(), or similar validation library.`,
|
|
82
|
+
severity: 'warning',
|
|
83
|
+
ruleId: this.ruleId,
|
|
84
|
+
type: 'unvalidated_json_input',
|
|
85
|
+
inputExpression: input.expression
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return violations;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
findPotentialInputs(lines) {
|
|
94
|
+
const inputs = [];
|
|
95
|
+
|
|
96
|
+
lines.forEach((line, index) => {
|
|
97
|
+
const trimmedLine = line.trim();
|
|
98
|
+
|
|
99
|
+
// Skip comments and imports
|
|
100
|
+
if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') ||
|
|
101
|
+
trimmedLine.startsWith('import') || trimmedLine.startsWith('export')) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Look for .body or .query patterns
|
|
106
|
+
const bodyMatches = [...line.matchAll(/(\w+\.\w*body\w*)/g)];
|
|
107
|
+
const queryMatches = [...line.matchAll(/(\w+\.\w*query\w*)/g)];
|
|
108
|
+
|
|
109
|
+
[...bodyMatches, ...queryMatches].forEach(match => {
|
|
110
|
+
const expression = match[1];
|
|
111
|
+
const column = match.index + 1;
|
|
112
|
+
|
|
113
|
+
inputs.push({
|
|
114
|
+
expression,
|
|
115
|
+
line: index + 1,
|
|
116
|
+
column,
|
|
117
|
+
originalLine: line
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return inputs;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
findValidatedInputs(content) {
|
|
126
|
+
const validatedInputs = new Set();
|
|
127
|
+
|
|
128
|
+
// Find validation calls
|
|
129
|
+
this.validationPatterns.forEach(pattern => {
|
|
130
|
+
const regex = new RegExp(pattern.replace('(', '\\(') + '\\s*\\(([^)]+)\\)', 'g');
|
|
131
|
+
let match;
|
|
132
|
+
|
|
133
|
+
while ((match = regex.exec(content)) !== null) {
|
|
134
|
+
const validatedInput = match[1].trim();
|
|
135
|
+
validatedInputs.add(validatedInput);
|
|
136
|
+
|
|
137
|
+
// Also add simplified version (e.g., req.body from schema.validate(req.body))
|
|
138
|
+
const simplifiedInput = validatedInput.replace(/^\w+\./, '').replace(/\s+/g, '');
|
|
139
|
+
if (simplifiedInput.includes('.')) {
|
|
140
|
+
validatedInputs.add(simplifiedInput);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return validatedInputs;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
isInputValidated(input, validatedInputs) {
|
|
149
|
+
const expression = input.expression;
|
|
150
|
+
|
|
151
|
+
// Check exact match
|
|
152
|
+
if (validatedInputs.has(expression)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check if any validated input contains this expression
|
|
157
|
+
for (const validated of validatedInputs) {
|
|
158
|
+
if (validated.includes(expression) || expression.includes(validated)) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check if validation happens in the same line or nearby
|
|
164
|
+
const lineContent = input.originalLine.toLowerCase();
|
|
165
|
+
if (this.validationPatterns.some(pattern => lineContent.includes(pattern.toLowerCase()))) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
isActualJSONInput(input, content) {
|
|
173
|
+
const expression = input.expression.toLowerCase();
|
|
174
|
+
|
|
175
|
+
// Skip known non-input patterns (user feedback - styles, config, etc.)
|
|
176
|
+
if (this.nonInputPatterns.some(pattern => expression.startsWith(pattern.toLowerCase()))) {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Skip React/CSS style objects
|
|
181
|
+
if (this.isStyleOrConfigObject(input, content)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check if it matches HTTP input patterns
|
|
186
|
+
if (this.httpInputPatterns.some(pattern => expression.includes(pattern.toLowerCase()))) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check if it's in HTTP handler context
|
|
191
|
+
if (this.isInHTTPHandlerContext(input, content)) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Default to false to avoid false positives
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
isStyleOrConfigObject(input, content) {
|
|
200
|
+
const expression = input.expression;
|
|
201
|
+
const lineContent = input.originalLine.toLowerCase();
|
|
202
|
+
|
|
203
|
+
// Check for React/CSS style usage patterns
|
|
204
|
+
const styleIndicators = [
|
|
205
|
+
'style=', 'css=', 'theme=', 'styles=',
|
|
206
|
+
'background', 'color:', 'font', 'margin:', 'padding:',
|
|
207
|
+
'import', 'const styles', 'const css', 'const theme'
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
if (styleIndicators.some(indicator => lineContent.includes(indicator))) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check context around the input for style/config patterns
|
|
215
|
+
const lines = content.split('\n');
|
|
216
|
+
const inputLineIndex = input.line - 1;
|
|
217
|
+
const contextStart = Math.max(0, inputLineIndex - 3);
|
|
218
|
+
const contextEnd = Math.min(lines.length, inputLineIndex + 3);
|
|
219
|
+
const contextLines = lines.slice(contextStart, contextEnd).join('\n').toLowerCase();
|
|
220
|
+
|
|
221
|
+
const contextIndicators = [
|
|
222
|
+
'const styles', 'const css', 'const config', 'const theme',
|
|
223
|
+
'styleshet.create', 'react', 'jsx', '<div', '</div>', 'component',
|
|
224
|
+
'export default', 'props', 'state'
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
return contextIndicators.some(indicator => contextLines.includes(indicator));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
isInHTTPHandlerContext(input, content) {
|
|
231
|
+
const lines = content.split('\n');
|
|
232
|
+
const inputLineIndex = input.line - 1;
|
|
233
|
+
|
|
234
|
+
// Check surrounding context for HTTP framework patterns
|
|
235
|
+
const contextStart = Math.max(0, inputLineIndex - 10);
|
|
236
|
+
const contextEnd = Math.min(lines.length, inputLineIndex + 5);
|
|
237
|
+
const contextLines = lines.slice(contextStart, contextEnd).join('\n').toLowerCase();
|
|
238
|
+
|
|
239
|
+
// Look for HTTP handler patterns in context
|
|
240
|
+
const httpIndicators = [
|
|
241
|
+
'app.post', 'app.put', 'app.patch', 'app.delete',
|
|
242
|
+
'router.post', 'router.put', 'router.patch',
|
|
243
|
+
'(req, res)', 'request, response', 'ctx.body', 'ctx.query',
|
|
244
|
+
'express', 'fastify', 'koa', 'hapi'
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
return httpIndicators.some(indicator => contextLines.includes(indicator));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
module.exports = S026Analyzer;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S026",
|
|
3
|
+
"name": "JSON Schema Validation for Input Data",
|
|
4
|
+
"description": "Ensure all user input data (from HTTP requests, APIs) is validated using JSON schemas before processing to prevent injection attacks.",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "warning",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"engines": ["heuristic"],
|
|
9
|
+
"enginePreference": ["heuristic"],
|
|
10
|
+
"tags": ["security", "validation", "input", "json-schema", "http"],
|
|
11
|
+
"examples": {
|
|
12
|
+
"valid": [
|
|
13
|
+
"const schema = joi.object({ name: joi.string() }); const { error } = schema.validate(req.body);",
|
|
14
|
+
"const ajv = new Ajv(); const valid = ajv.validate(schema, req.body);",
|
|
15
|
+
"const styles = { body: { color: 'red' } }; // Style object - OK"
|
|
16
|
+
],
|
|
17
|
+
"invalid": [
|
|
18
|
+
"const data = req.body; processUser(data); // No validation",
|
|
19
|
+
"const query = req.query; database.find(query); // Direct usage without validation"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"fixable": false,
|
|
23
|
+
"docs": {
|
|
24
|
+
"description": "This rule ensures that all user input data from HTTP requests is validated using JSON schemas before processing. Direct usage of req.body, req.query, req.params without validation can lead to injection attacks and data corruption.",
|
|
25
|
+
"url": "https://owasp.org/Top10/A03_2021-Injection/"
|
|
26
|
+
}
|
|
27
|
+
}
|