@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,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based analyzer for S015 - Insecure TLS Certificate Detection
|
|
3
|
+
* Detects usage of insecure TLS certificate configurations like rejectUnauthorized: false
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const babel = require('@babel/parser');
|
|
7
|
+
const traverse = require('@babel/traverse').default;
|
|
8
|
+
|
|
9
|
+
class S015ASTAnalyzer {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.ruleId = 'S015';
|
|
12
|
+
this.ruleName = 'Insecure TLS Certificate';
|
|
13
|
+
this.description = 'Prevent usage of insecure TLS certificate configurations';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async analyze(files, language, options = {}) {
|
|
17
|
+
const violations = [];
|
|
18
|
+
|
|
19
|
+
for (const filePath of files) {
|
|
20
|
+
try {
|
|
21
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
22
|
+
const fileViolations = await this.analyzeFile(content, filePath, options);
|
|
23
|
+
violations.push(...fileViolations);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
if (options.verbose) {
|
|
26
|
+
console.warn(`⚠️ S015 AST analysis failed for ${filePath}: ${error.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return violations;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async analyzeFile(content, filePath, options = {}) {
|
|
35
|
+
const violations = [];
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Parse with TypeScript/JavaScript support
|
|
39
|
+
const ast = babel.parse(content, {
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
allowImportExportEverywhere: true,
|
|
42
|
+
allowReturnOutsideFunction: true,
|
|
43
|
+
plugins: [
|
|
44
|
+
'typescript',
|
|
45
|
+
'jsx',
|
|
46
|
+
'objectRestSpread',
|
|
47
|
+
'functionBind',
|
|
48
|
+
'exportDefaultFrom',
|
|
49
|
+
'decorators-legacy',
|
|
50
|
+
'classProperties',
|
|
51
|
+
'asyncGenerators',
|
|
52
|
+
'dynamicImport'
|
|
53
|
+
]
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Traverse AST to find insecure TLS configurations
|
|
57
|
+
traverse(ast, {
|
|
58
|
+
Property: (path) => {
|
|
59
|
+
this.checkTLSProperty(path, violations, filePath);
|
|
60
|
+
},
|
|
61
|
+
ObjectExpression: (path) => {
|
|
62
|
+
this.checkHTTPSOptions(path, violations, filePath);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
} catch (parseError) {
|
|
67
|
+
if (options.verbose) {
|
|
68
|
+
console.warn(`⚠️ S015 parse failed for ${filePath}: ${parseError.message}`);
|
|
69
|
+
}
|
|
70
|
+
// Fall back to regex analysis if AST parsing fails
|
|
71
|
+
return this.analyzeWithRegex(content, filePath, options);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return violations;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
checkTLSProperty(path, violations, filePath) {
|
|
78
|
+
const node = path.node;
|
|
79
|
+
|
|
80
|
+
// Check for rejectUnauthorized: false
|
|
81
|
+
if (this.isPropertyKey(node.key, 'rejectUnauthorized')) {
|
|
82
|
+
if (this.isFalseLiteral(node.value)) {
|
|
83
|
+
violations.push({
|
|
84
|
+
ruleId: this.ruleId,
|
|
85
|
+
severity: 'error',
|
|
86
|
+
message: 'Untrusted/self-signed/expired certificate accepted. Only use trusted certificates in production.',
|
|
87
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
88
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
89
|
+
filePath: filePath,
|
|
90
|
+
type: 'insecure_tls_config'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for other insecure TLS options
|
|
96
|
+
const insecureOptions = [
|
|
97
|
+
'checkServerIdentity',
|
|
98
|
+
'secureProtocol',
|
|
99
|
+
'ciphers',
|
|
100
|
+
'secureOptions'
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
if (insecureOptions.some(opt => this.isPropertyKey(node.key, opt))) {
|
|
104
|
+
if (this.hasInsecureValue(node.value, node.key.name || node.key.value)) {
|
|
105
|
+
violations.push({
|
|
106
|
+
ruleId: this.ruleId,
|
|
107
|
+
severity: 'warning',
|
|
108
|
+
message: `Potentially insecure TLS option '${node.key.name || node.key.value}'. Review configuration for security.`,
|
|
109
|
+
line: node.loc ? node.loc.start.line : 1,
|
|
110
|
+
column: node.loc ? node.loc.start.column + 1 : 1,
|
|
111
|
+
filePath: filePath,
|
|
112
|
+
type: 'potentially_insecure_tls'
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
checkHTTPSOptions(path, violations, filePath) {
|
|
119
|
+
const node = path.node;
|
|
120
|
+
|
|
121
|
+
// Look for HTTPS server configurations
|
|
122
|
+
const parent = path.parent;
|
|
123
|
+
if (parent && parent.type === 'CallExpression') {
|
|
124
|
+
const callee = parent.callee;
|
|
125
|
+
|
|
126
|
+
// Check for https.createServer(options, ...)
|
|
127
|
+
if (this.isHTTPSCreateServer(callee)) {
|
|
128
|
+
// Check each property in the options object
|
|
129
|
+
node.properties.forEach(prop => {
|
|
130
|
+
if (prop.type === 'ObjectProperty') {
|
|
131
|
+
if (this.isPropertyKey(prop.key, 'rejectUnauthorized') &&
|
|
132
|
+
this.isFalseLiteral(prop.value)) {
|
|
133
|
+
violations.push({
|
|
134
|
+
ruleId: this.ruleId,
|
|
135
|
+
severity: 'error',
|
|
136
|
+
message: 'HTTPS server configured with rejectUnauthorized: false. This disables certificate validation.',
|
|
137
|
+
line: prop.loc ? prop.loc.start.line : 1,
|
|
138
|
+
column: prop.loc ? prop.loc.start.column + 1 : 1,
|
|
139
|
+
filePath: filePath,
|
|
140
|
+
type: 'https_server_insecure'
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
isPropertyKey(key, expectedName) {
|
|
150
|
+
return (key.type === 'Identifier' && key.name === expectedName) ||
|
|
151
|
+
(key.type === 'StringLiteral' && key.value === expectedName) ||
|
|
152
|
+
(key.type === 'Literal' && key.value === expectedName);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
isFalseLiteral(value) {
|
|
156
|
+
return (value.type === 'BooleanLiteral' && value.value === false) ||
|
|
157
|
+
(value.type === 'Literal' && value.value === false);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
isHTTPSCreateServer(callee) {
|
|
161
|
+
return (callee.type === 'MemberExpression' &&
|
|
162
|
+
callee.object.name === 'https' &&
|
|
163
|
+
callee.property.name === 'createServer') ||
|
|
164
|
+
(callee.type === 'Identifier' && callee.name === 'createServer');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
hasInsecureValue(value, propertyName) {
|
|
168
|
+
// Check for potentially insecure values based on property type
|
|
169
|
+
if (propertyName === 'checkServerIdentity' && this.isFalseLiteral(value)) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (propertyName === 'secureProtocol' && value.type === 'StringLiteral') {
|
|
174
|
+
const insecureProtocols = ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1_method'];
|
|
175
|
+
return insecureProtocols.some(protocol =>
|
|
176
|
+
value.value.toLowerCase().includes(protocol.toLowerCase())
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (propertyName === 'ciphers' && value.type === 'StringLiteral') {
|
|
181
|
+
const weakCiphers = ['NULL', 'RC4', 'DES', 'MD5'];
|
|
182
|
+
return weakCiphers.some(cipher =>
|
|
183
|
+
value.value.toUpperCase().includes(cipher)
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fallback regex analysis
|
|
191
|
+
analyzeWithRegex(content, filePath, options = {}) {
|
|
192
|
+
const violations = [];
|
|
193
|
+
const lines = content.split('\n');
|
|
194
|
+
|
|
195
|
+
lines.forEach((line, index) => {
|
|
196
|
+
const lineNumber = index + 1;
|
|
197
|
+
|
|
198
|
+
// Check for rejectUnauthorized: false
|
|
199
|
+
if (/rejectUnauthorized\s*:\s*false/i.test(line)) {
|
|
200
|
+
violations.push({
|
|
201
|
+
ruleId: this.ruleId,
|
|
202
|
+
severity: 'error',
|
|
203
|
+
message: 'Untrusted/self-signed/expired certificate accepted. Only use trusted certificates in production.',
|
|
204
|
+
line: lineNumber,
|
|
205
|
+
column: line.indexOf('rejectUnauthorized') + 1,
|
|
206
|
+
filePath: filePath,
|
|
207
|
+
type: 'insecure_tls_config_regex'
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Check for other insecure patterns
|
|
212
|
+
const insecurePatterns = [
|
|
213
|
+
{ pattern: /checkServerIdentity\s*:\s*false/i, message: 'Server identity check disabled' },
|
|
214
|
+
{ pattern: /secureProtocol\s*:\s*['"]SSL/i, message: 'Insecure SSL protocol used' },
|
|
215
|
+
{ pattern: /secureProtocol\s*:\s*['"]TLSv1['"]/i, message: 'Insecure TLS v1.0 protocol used' }
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
insecurePatterns.forEach(({ pattern, message }) => {
|
|
219
|
+
if (pattern.test(line)) {
|
|
220
|
+
violations.push({
|
|
221
|
+
ruleId: this.ruleId,
|
|
222
|
+
severity: 'warning',
|
|
223
|
+
message: `${message}. Use secure TLS configuration.`,
|
|
224
|
+
line: lineNumber,
|
|
225
|
+
column: line.search(pattern) + 1,
|
|
226
|
+
filePath: filePath,
|
|
227
|
+
type: 'insecure_tls_pattern_regex'
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
return violations;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = S015ASTAnalyzer;
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic analyzer for: S023 – No JSON Injection
|
|
3
|
+
* Purpose: Prevent JSON injection attacks and unsafe JSON handling
|
|
4
|
+
* Detects: unsafe JSON.parse(), eval() with JSON, JSON.stringify in HTML context
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class S023Analyzer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.ruleId = 'S023';
|
|
10
|
+
this.ruleName = 'No JSON Injection';
|
|
11
|
+
this.description = 'Prevent JSON injection attacks and unsafe JSON handling';
|
|
12
|
+
|
|
13
|
+
// Patterns that indicate user input sources
|
|
14
|
+
this.userInputPatterns = [
|
|
15
|
+
/localStorage\.getItem/,
|
|
16
|
+
/sessionStorage\.getItem/,
|
|
17
|
+
/window\.location/,
|
|
18
|
+
/location\.(search|hash|href)/,
|
|
19
|
+
/URLSearchParams/,
|
|
20
|
+
/req\.(body|query|params)/,
|
|
21
|
+
/request\.(body|query|params)/,
|
|
22
|
+
/document\.cookie/,
|
|
23
|
+
/window\.name/,
|
|
24
|
+
/postMessage/,
|
|
25
|
+
/fetch\(/,
|
|
26
|
+
/axios\./,
|
|
27
|
+
/xhr\./
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Patterns that indicate validation/safety measures
|
|
31
|
+
this.validationPatterns = [
|
|
32
|
+
/try\s*\{/,
|
|
33
|
+
/catch\s*\(/,
|
|
34
|
+
/if\s*\(/,
|
|
35
|
+
/typeof\s+/,
|
|
36
|
+
/instanceof\s+/,
|
|
37
|
+
/\.length\s*[><=]/,
|
|
38
|
+
/validate/i,
|
|
39
|
+
/check/i,
|
|
40
|
+
/isValid/i,
|
|
41
|
+
/sanitize/i,
|
|
42
|
+
/escape/i,
|
|
43
|
+
/filter/i
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// HTML context patterns
|
|
47
|
+
this.htmlContextPatterns = [
|
|
48
|
+
/innerHTML/,
|
|
49
|
+
/outerHTML/,
|
|
50
|
+
/insertAdjacentHTML/,
|
|
51
|
+
/document\.write/,
|
|
52
|
+
/\.html\(/,
|
|
53
|
+
/<script/i,
|
|
54
|
+
/<\/script>/i
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// JSON patterns for eval detection
|
|
58
|
+
this.jsonPatterns = [
|
|
59
|
+
/json/i,
|
|
60
|
+
/\{.*\}/,
|
|
61
|
+
/\[.*\]/,
|
|
62
|
+
/parse/i,
|
|
63
|
+
/stringify/i
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async analyze(files, language, options = {}) {
|
|
68
|
+
const violations = [];
|
|
69
|
+
|
|
70
|
+
for (const filePath of files) {
|
|
71
|
+
try {
|
|
72
|
+
const fileViolations = await this.analyzeFile(filePath, language, options);
|
|
73
|
+
violations.push(...fileViolations);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (options.verbose) {
|
|
76
|
+
console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async analyzeFile(filePath, language, options = {}) {
|
|
85
|
+
switch (language) {
|
|
86
|
+
case 'typescript':
|
|
87
|
+
case 'javascript':
|
|
88
|
+
return this.analyzeJavaScript(filePath, options);
|
|
89
|
+
default:
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async analyzeJavaScript(filePath, options = {}) {
|
|
95
|
+
try {
|
|
96
|
+
// Try AST analysis first (preferred method)
|
|
97
|
+
const astAnalyzer = require('./ast-analyzer.js');
|
|
98
|
+
const astViolations = await astAnalyzer.analyze([filePath], 'javascript', options);
|
|
99
|
+
if (astViolations.length > 0) {
|
|
100
|
+
return astViolations;
|
|
101
|
+
}
|
|
102
|
+
} catch (astError) {
|
|
103
|
+
if (options.verbose) {
|
|
104
|
+
console.log(`⚠️ AST analysis failed for ${filePath}, falling back to regex`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fallback to regex analysis
|
|
109
|
+
return this.analyzeWithRegex(filePath, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async analyzeWithRegex(filePath, options = {}) {
|
|
113
|
+
const fs = require('fs');
|
|
114
|
+
const path = require('path');
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
118
|
+
const violations = [];
|
|
119
|
+
const lines = content.split('\n');
|
|
120
|
+
|
|
121
|
+
// 1. Check JSON.parse() calls
|
|
122
|
+
const jsonParseViolations = this.checkJsonParseCalls(content, lines, filePath);
|
|
123
|
+
violations.push(...jsonParseViolations);
|
|
124
|
+
|
|
125
|
+
// 2. Check eval() with JSON patterns
|
|
126
|
+
const evalViolations = this.checkEvalWithJson(content, lines, filePath);
|
|
127
|
+
violations.push(...evalViolations);
|
|
128
|
+
|
|
129
|
+
// 3. Check JSON.stringify in HTML context
|
|
130
|
+
const htmlViolations = this.checkJsonStringifyInHtml(content, lines, filePath);
|
|
131
|
+
violations.push(...htmlViolations);
|
|
132
|
+
|
|
133
|
+
return violations;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (options.verbose) {
|
|
136
|
+
console.warn(`⚠️ Failed to read file ${filePath}: ${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
checkJsonParseCalls(content, lines, filePath) {
|
|
143
|
+
const violations = [];
|
|
144
|
+
const jsonParsePattern = /JSON\.parse\s*\(\s*([^)]+)\)/g;
|
|
145
|
+
let match;
|
|
146
|
+
|
|
147
|
+
while ((match = jsonParsePattern.exec(content)) !== null) {
|
|
148
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
149
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
150
|
+
const argument = match[1].trim();
|
|
151
|
+
|
|
152
|
+
// Check if argument is from user input
|
|
153
|
+
if (this.isUserInputArgument(argument)) {
|
|
154
|
+
// Check if there's validation around this call
|
|
155
|
+
if (!this.hasValidationContext(content, match.index, lineNumber, lines)) {
|
|
156
|
+
violations.push({
|
|
157
|
+
ruleId: this.ruleId,
|
|
158
|
+
file: filePath,
|
|
159
|
+
line: lineNumber,
|
|
160
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
161
|
+
message: 'Unsafe JSON parsing - validate input before parsing',
|
|
162
|
+
severity: 'warning',
|
|
163
|
+
code: lineText.trim(),
|
|
164
|
+
type: 'unsafe_json_parse',
|
|
165
|
+
confidence: 0.8,
|
|
166
|
+
suggestion: 'Validate input before parsing JSON or use try-catch block'
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return violations;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
checkEvalWithJson(content, lines, filePath) {
|
|
176
|
+
const violations = [];
|
|
177
|
+
const evalPattern = /eval\s*\(\s*([^)]+)\)/g;
|
|
178
|
+
let match;
|
|
179
|
+
|
|
180
|
+
while ((match = evalPattern.exec(content)) !== null) {
|
|
181
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
182
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
183
|
+
const argument = match[1].trim();
|
|
184
|
+
|
|
185
|
+
// Check if eval contains JSON patterns
|
|
186
|
+
if (this.containsJsonPattern(argument)) {
|
|
187
|
+
violations.push({
|
|
188
|
+
ruleId: this.ruleId,
|
|
189
|
+
file: filePath,
|
|
190
|
+
line: lineNumber,
|
|
191
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
192
|
+
message: 'Never use eval() to process JSON data - use JSON.parse() instead',
|
|
193
|
+
severity: 'error',
|
|
194
|
+
code: lineText.trim(),
|
|
195
|
+
type: 'eval_json',
|
|
196
|
+
confidence: 0.9,
|
|
197
|
+
suggestion: 'Use JSON.parse() instead of eval() for parsing JSON'
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return violations;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
checkJsonStringifyInHtml(content, lines, filePath) {
|
|
206
|
+
const violations = [];
|
|
207
|
+
const jsonStringifyPattern = /JSON\.stringify\s*\([^)]+\)/g;
|
|
208
|
+
let match;
|
|
209
|
+
|
|
210
|
+
while ((match = jsonStringifyPattern.exec(content)) !== null) {
|
|
211
|
+
const lineNumber = content.substring(0, match.index).split('\n').length;
|
|
212
|
+
const lineText = lines[lineNumber - 1] || '';
|
|
213
|
+
|
|
214
|
+
// Check if JSON.stringify is used in HTML context
|
|
215
|
+
if (this.isInHtmlContext(content, match.index)) {
|
|
216
|
+
violations.push({
|
|
217
|
+
ruleId: this.ruleId,
|
|
218
|
+
file: filePath,
|
|
219
|
+
line: lineNumber,
|
|
220
|
+
column: match.index - content.lastIndexOf('\n', match.index),
|
|
221
|
+
message: 'JSON.stringify output should be escaped when used in HTML context',
|
|
222
|
+
severity: 'warning',
|
|
223
|
+
code: lineText.trim(),
|
|
224
|
+
type: 'json_stringify_html',
|
|
225
|
+
confidence: 0.7,
|
|
226
|
+
suggestion: 'Escape JSON.stringify output when inserting into HTML'
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return violations;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
isUserInputArgument(argument) {
|
|
235
|
+
return this.userInputPatterns.some(pattern => pattern.test(argument));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
hasValidationContext(content, matchIndex, lineNumber, lines) {
|
|
239
|
+
// Check surrounding lines for validation patterns
|
|
240
|
+
const startLine = Math.max(0, lineNumber - 3);
|
|
241
|
+
const endLine = Math.min(lines.length, lineNumber + 2);
|
|
242
|
+
|
|
243
|
+
for (let i = startLine; i < endLine; i++) {
|
|
244
|
+
const line = lines[i] || '';
|
|
245
|
+
if (this.validationPatterns.some(pattern => pattern.test(line))) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check if the call is inside a try block
|
|
251
|
+
const beforeText = content.substring(Math.max(0, matchIndex - 200), matchIndex);
|
|
252
|
+
const afterText = content.substring(matchIndex, Math.min(content.length, matchIndex + 100));
|
|
253
|
+
|
|
254
|
+
return /try\s*\{[^}]*$/.test(beforeText) || /catch\s*\(/.test(afterText);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
containsJsonPattern(text) {
|
|
258
|
+
return this.jsonPatterns.some(pattern => pattern.test(text));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
isInHtmlContext(content, matchIndex) {
|
|
262
|
+
// Check surrounding context for HTML patterns
|
|
263
|
+
const contextStart = Math.max(0, matchIndex - 100);
|
|
264
|
+
const contextEnd = Math.min(content.length, matchIndex + 100);
|
|
265
|
+
const context = content.substring(contextStart, contextEnd);
|
|
266
|
+
|
|
267
|
+
return this.htmlContextPatterns.some(pattern => pattern.test(context));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Utility method for file extension checking
|
|
271
|
+
isSupportedFile(filePath) {
|
|
272
|
+
const supportedExtensions = ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
273
|
+
const path = require('path');
|
|
274
|
+
return supportedExtensions.includes(path.extname(filePath));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = new S023Analyzer();
|