@sun-asterisk/sunlint 1.3.8 → 1.3.10
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 +25 -0
- package/config/rules/enhanced-rules-registry.json +79 -22
- package/core/cli-program.js +1 -1
- package/core/file-targeting-service.js +15 -0
- package/core/semantic-engine.js +4 -2
- package/package.json +1 -1
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +116 -10
- package/rules/common/C060_no_override_superclass/analyzer.js +180 -0
- package/rules/common/C060_no_override_superclass/config.json +50 -0
- package/rules/common/C060_no_override_superclass/symbol-based-analyzer.js +220 -0
- package/rules/index.js +1 -0
- package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
- package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
- package/rules/security/S030_directory_browsing_protection/README.md +128 -0
- package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
- package/rules/security/S030_directory_browsing_protection/config.json +63 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +33 -26
- package/rules/security/S056_log_injection_protection/analyzer.js +2 -2
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +77 -118
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C060 Symbol-based Analyzer - Advanced Do not scatter hardcoded constants throughout the logic
|
|
3
|
+
* Purpose: The rule prevents scattering hardcoded constants throughout the logic. Instead, constants should be defined in a single place to improve maintainability and readability.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { SyntaxKind } = require('ts-morph');
|
|
7
|
+
|
|
8
|
+
class C060SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.ruleId = 'C060';
|
|
11
|
+
this.ruleName = 'Do not override superclass methods (Symbol-Based)';
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.verbose = false;
|
|
14
|
+
|
|
15
|
+
// === Ignore Configuration ===
|
|
16
|
+
this.ignoredClasses = [
|
|
17
|
+
"React.Component", // React components
|
|
18
|
+
"React.PureComponent",
|
|
19
|
+
"BaseMock", // Custom test mocks
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
this.ignoredMethods = [
|
|
23
|
+
"render", // React render
|
|
24
|
+
"componentDidMount", // Some lifecycle hooks
|
|
25
|
+
"componentWillUnmount",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
this.ignoredFilePatterns = [
|
|
29
|
+
/node_modules/,
|
|
30
|
+
/\.d\.ts$/,
|
|
31
|
+
/\.test\.ts$/,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async initialize(semanticEngine = null) {
|
|
36
|
+
if (semanticEngine) {
|
|
37
|
+
this.semanticEngine = semanticEngine;
|
|
38
|
+
}
|
|
39
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
40
|
+
|
|
41
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
42
|
+
console.log(`🔧 [C060 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async analyzeFileBasic(filePath, options = {}) {
|
|
47
|
+
// This is the main entry point called by the hybrid analyzer
|
|
48
|
+
return await this.analyzeFileWithSymbols(filePath, options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
52
|
+
const violations = [];
|
|
53
|
+
|
|
54
|
+
// Enable verbose mode if requested
|
|
55
|
+
const verbose = options.verbose || this.verbose;
|
|
56
|
+
|
|
57
|
+
if (!this.semanticEngine?.project) {
|
|
58
|
+
if (verbose) {
|
|
59
|
+
console.warn('[C060 Symbol-Based] No semantic engine available, skipping analysis');
|
|
60
|
+
}
|
|
61
|
+
return violations;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.shouldIgnoreFile(filePath)) {
|
|
65
|
+
if (verbose) console.log(`[${this.ruleId}] Ignoring ${filePath}`);
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (verbose) {
|
|
70
|
+
console.log(`🔍 [C060 Symbol-Based] Starting analysis for ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
75
|
+
if (!sourceFile) {
|
|
76
|
+
return violations;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const classDeclarations = sourceFile.getClasses();
|
|
80
|
+
for (const classDeclaration of classDeclarations) {
|
|
81
|
+
const classViolations = this.analyzeClass(classDeclaration, filePath, verbose);
|
|
82
|
+
violations.push(...classViolations);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (verbose) {
|
|
86
|
+
console.log(`🔍 [C060 Symbol-Based] Total violations found: ${violations.length}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return violations;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (verbose) {
|
|
92
|
+
console.warn(`[C060 Symbol-Based] Analysis failed for ${filePath}:`, error.message);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return violations;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Analyze one class for override violations
|
|
101
|
+
*/
|
|
102
|
+
analyzeClass(classDeclaration, filePath, verbose) {
|
|
103
|
+
const violations = [];
|
|
104
|
+
const baseClass = classDeclaration.getBaseClass();
|
|
105
|
+
if (!baseClass) return violations;
|
|
106
|
+
|
|
107
|
+
// Check if this class should be ignored
|
|
108
|
+
if (this.shouldIgnoreClass(baseClass)) {
|
|
109
|
+
if (verbose) {
|
|
110
|
+
console.log(`[${this.ruleId}] Skipping ignored base class: ${baseClass.getName()}`);
|
|
111
|
+
}
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const baseMethods = this.collectBaseMethods(baseClass);
|
|
116
|
+
|
|
117
|
+
for (const method of classDeclaration.getMethods()) {
|
|
118
|
+
// Skip ignored methods
|
|
119
|
+
if (this.shouldIgnoreMethod(method)) {
|
|
120
|
+
if (verbose) {
|
|
121
|
+
console.log(`[${this.ruleId}] Skipping ignored method: ${method.getName()}`);
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (baseMethods.has(method.getName())) {
|
|
127
|
+
const violation = this.checkMethodOverride(method, classDeclaration, filePath, verbose);
|
|
128
|
+
if (violation) {
|
|
129
|
+
violations.push(violation);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return violations;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Collect all method names from base class
|
|
139
|
+
*/
|
|
140
|
+
collectBaseMethods(baseClass) {
|
|
141
|
+
return new Set(baseClass.getMethods().map((method) => method.getName()));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if overridden method properly calls super.method()
|
|
146
|
+
*/
|
|
147
|
+
checkMethodOverride(method, classDeclaration, filePath, verbose) {
|
|
148
|
+
const body = method.getBodyText();
|
|
149
|
+
if (!body) return null;
|
|
150
|
+
|
|
151
|
+
const methodName = method.getName();
|
|
152
|
+
const baseClass = classDeclaration.getBaseClass();
|
|
153
|
+
if (!baseClass) return null;
|
|
154
|
+
const baseClassName = baseClass.getName();
|
|
155
|
+
|
|
156
|
+
// 1 Get all CallExpressions in the method body
|
|
157
|
+
const calls = method.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
158
|
+
|
|
159
|
+
// 2 Check for super.method() or BaseClass.prototype.method.call(this)
|
|
160
|
+
const hasSuperCall = calls.some((call) => {
|
|
161
|
+
const expression = call.getExpression().getText();
|
|
162
|
+
return expression === `super.${methodName}`;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const hasBaseCall = calls.some((call) => {
|
|
166
|
+
const expression = call.getExpression().getText();
|
|
167
|
+
return expression === `${baseClassName}.prototype.${methodName}.call`;
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!hasSuperCall && !hasBaseCall) {
|
|
171
|
+
const violation = {
|
|
172
|
+
ruleId: this.ruleId,
|
|
173
|
+
severity: 'warning',
|
|
174
|
+
message: `Overridden method '${method.getName()}' in '${classDeclaration.getName()}' does not call 'super.${method.getName()}()', potentially skipping lifecycle/resource logic.`,
|
|
175
|
+
source: this.ruleId,
|
|
176
|
+
file: filePath,
|
|
177
|
+
line: method.getStartLineNumber(),
|
|
178
|
+
column: method.getStart() - method.getStartLinePos(),
|
|
179
|
+
description: `[SYMBOL-BASED] Overriding methods should call the superclass implementation to ensure proper behavior.`,
|
|
180
|
+
suggestion: `Add a call to 'super.${method.getName()}()' within the method body.`,
|
|
181
|
+
category: 'best-practices',
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (verbose) {
|
|
185
|
+
console.log(
|
|
186
|
+
`⚠️ [${this.ruleId}] Violation in ${filePath}: Method '${method.getName()}' overrides superclass method but does not call super.`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return violation;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Ignore logic for classes
|
|
198
|
+
*/
|
|
199
|
+
shouldIgnoreClass(baseClass) {
|
|
200
|
+
const baseName = baseClass.getName();
|
|
201
|
+
return this.ignoredClasses.includes(baseName);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Ignore logic for methods
|
|
206
|
+
*/
|
|
207
|
+
shouldIgnoreMethod(method) {
|
|
208
|
+
const methodName = method.getName();
|
|
209
|
+
return this.ignoredMethods.includes(methodName);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Ignore files based on patterns
|
|
214
|
+
*/
|
|
215
|
+
shouldIgnoreFile(filePath) {
|
|
216
|
+
return this.ignoredFilePatterns.some((pattern) => pattern.test(filePath));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = C060SymbolBasedAnalyzer;
|
package/rules/index.js
CHANGED
|
@@ -64,6 +64,7 @@ const commonRules = {
|
|
|
64
64
|
C048: loadRule('common', 'C048_no_bypass_architectural_layers'),
|
|
65
65
|
C052: loadRule('common', 'C052_parsing_or_data_transformation'),
|
|
66
66
|
C047: loadRule('common', 'C047_no_duplicate_retry_logic'),
|
|
67
|
+
C060: loadRule('common', 'C060_no_override_superclass'),
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
// 🔒 Security Rules (S-series) - Ready for migration
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# S020 - Avoid using eval() or executing dynamic code
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This rule detects and prevents the use of `eval()` and other dynamic code execution mechanisms that can lead to security vulnerabilities, particularly code injection attacks.
|
|
6
|
+
|
|
7
|
+
## Why This Rule Matters
|
|
8
|
+
|
|
9
|
+
Dynamic code execution through `eval()` and similar functions poses significant security risks:
|
|
10
|
+
|
|
11
|
+
1. **Code Injection Vulnerabilities**: Untrusted input can be executed as code
|
|
12
|
+
2. **Performance Issues**: Dynamic code execution is slower than static code
|
|
13
|
+
3. **Debugging Difficulties**: Dynamic code is harder to debug and analyze
|
|
14
|
+
4. **Security Auditing**: Static analysis tools cannot analyze dynamically generated code
|
|
15
|
+
|
|
16
|
+
## What This Rule Detects
|
|
17
|
+
|
|
18
|
+
### ❌ Dangerous Functions
|
|
19
|
+
|
|
20
|
+
- `eval()` - Direct code execution
|
|
21
|
+
- `new Function()` - Function constructor with string
|
|
22
|
+
- `setTimeout(string)` - Timer with string code
|
|
23
|
+
- `setInterval(string)` - Interval with string code
|
|
24
|
+
- `execScript()` - Legacy IE function
|
|
25
|
+
- `setImmediate(string)` - Immediate execution with string
|
|
26
|
+
|
|
27
|
+
### ❌ Global Access Patterns
|
|
28
|
+
|
|
29
|
+
- `window.eval()`
|
|
30
|
+
- `global.eval()`
|
|
31
|
+
- `globalThis.eval()`
|
|
32
|
+
- `self.eval()`
|
|
33
|
+
|
|
34
|
+
### ❌ Suspicious Variable Patterns
|
|
35
|
+
|
|
36
|
+
Variables containing dynamic code indicators:
|
|
37
|
+
|
|
38
|
+
- Variables named with `code`, `script`, `expression`, `formula`, `template`, `eval`
|
|
39
|
+
|
|
40
|
+
## Examples
|
|
41
|
+
|
|
42
|
+
### ❌ Violations
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// Direct eval usage
|
|
46
|
+
eval("console.log('Hello')"); // ERROR
|
|
47
|
+
|
|
48
|
+
// Function constructor
|
|
49
|
+
const fn = new Function("return 1 + 1"); // ERROR
|
|
50
|
+
|
|
51
|
+
// setTimeout with string
|
|
52
|
+
setTimeout("console.log('test')", 1000); // WARNING
|
|
53
|
+
|
|
54
|
+
// Global eval access
|
|
55
|
+
window.eval("alert('test')"); // ERROR
|
|
56
|
+
|
|
57
|
+
// Dynamic code variables
|
|
58
|
+
const userCode = req.body.code;
|
|
59
|
+
eval(userCode); // ERROR - code injection risk
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### ✅ Safe Alternatives
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// Instead of eval(), use proper parsing and validation
|
|
66
|
+
const result = JSON.parse(jsonString);
|
|
67
|
+
|
|
68
|
+
// Instead of Function constructor, use proper function definitions
|
|
69
|
+
function add(a, b) {
|
|
70
|
+
return a + b;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Instead of setTimeout with string, use function reference
|
|
74
|
+
setTimeout(() => console.log("test"), 1000);
|
|
75
|
+
|
|
76
|
+
// Instead of dynamic code execution, use configuration objects
|
|
77
|
+
const actions = {
|
|
78
|
+
add: (a, b) => a + b,
|
|
79
|
+
multiply: (a, b) => a * b,
|
|
80
|
+
};
|
|
81
|
+
const result = actions[operation](x, y);
|
|
82
|
+
|
|
83
|
+
// For template engines, use safe templating libraries
|
|
84
|
+
const template = handlebars.compile(templateString);
|
|
85
|
+
const result = template(data);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Configuration
|
|
89
|
+
|
|
90
|
+
The rule can be configured in `config.json`:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"validation": {
|
|
95
|
+
"dangerousFunctions": ["eval", "Function", "setTimeout", "setInterval"],
|
|
96
|
+
"dangerousPatterns": ["new Function", "window.eval", "global.eval"],
|
|
97
|
+
"dynamicCodeIndicators": ["code", "script", "expression", "formula"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Security Best Practices
|
|
103
|
+
|
|
104
|
+
1. **Input Validation**: Always validate and sanitize user input
|
|
105
|
+
2. **Use Safe Alternatives**: Prefer configuration objects over dynamic code
|
|
106
|
+
3. **Template Engines**: Use established, secure template libraries
|
|
107
|
+
4. **Content Security Policy**: Implement CSP headers to prevent code injection
|
|
108
|
+
5. **Code Review**: Carefully review any dynamic code patterns
|
|
109
|
+
|
|
110
|
+
## Related Rules
|
|
111
|
+
|
|
112
|
+
- **S023**: JSON Injection Protection
|
|
113
|
+
- **S025**: Server-side Validation
|
|
114
|
+
- **S056**: Log Injection Protection
|
|
115
|
+
|
|
116
|
+
## Severity Levels
|
|
117
|
+
|
|
118
|
+
- **ERROR**: Direct `eval()`, `Function()` constructor, global eval access
|
|
119
|
+
- **WARNING**: `setTimeout`/`setInterval` with strings, suspicious variable patterns
|
|
120
|
+
|
|
121
|
+
## Framework-Specific Notes
|
|
122
|
+
|
|
123
|
+
### Node.js
|
|
124
|
+
|
|
125
|
+
- Be especially careful with `vm` module usage
|
|
126
|
+
- Avoid `child_process.exec()` with user input
|
|
127
|
+
|
|
128
|
+
### Browser
|
|
129
|
+
|
|
130
|
+
- Consider Content Security Policy to prevent eval
|
|
131
|
+
- Be cautious with `innerHTML` and similar DOM manipulation
|
|
132
|
+
|
|
133
|
+
### React/Vue
|
|
134
|
+
|
|
135
|
+
- Avoid `dangerouslySetInnerHTML` with user content
|
|
136
|
+
- Use proper event handlers instead of inline scripts
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S020 Main Analyzer - Avoid using eval() or executing dynamic code
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S020 --input=examples/rule-test-fixtures/rules/S020_no_eval_dynamic_code --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S020SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S020RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S020Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S020] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S020] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S020";
|
|
23
|
+
this.ruleName = "Avoid using eval() or executing dynamic code";
|
|
24
|
+
this.description =
|
|
25
|
+
"Avoid using eval() or executing dynamic code as it can lead to code injection vulnerabilities and compromise application security.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
this.config = {
|
|
30
|
+
useSymbolBased: true,
|
|
31
|
+
fallbackToRegex: true,
|
|
32
|
+
regexBasedOnly: false,
|
|
33
|
+
prioritizeSymbolic: true, // Prefer symbol-based when available
|
|
34
|
+
fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S020SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG)
|
|
40
|
+
console.log(`🔧 [S020] Symbol analyzer created successfully`);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error(`🔧 [S020] Error creating symbol analyzer:`, error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
this.regexAnalyzer = new S020RegexBasedAnalyzer(this.semanticEngine);
|
|
47
|
+
if (process.env.SUNLINT_DEBUG)
|
|
48
|
+
console.log(`🔧 [S020] Regex analyzer created successfully`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(`🔧 [S020] Error creating regex analyzer:`, error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async initialize(semanticEngine) {
|
|
55
|
+
this.semanticEngine = semanticEngine;
|
|
56
|
+
if (process.env.SUNLINT_DEBUG)
|
|
57
|
+
console.log(`🔧 [S020] Main analyzer initializing...`);
|
|
58
|
+
|
|
59
|
+
if (this.symbolAnalyzer)
|
|
60
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
61
|
+
if (this.regexAnalyzer)
|
|
62
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
63
|
+
if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
|
|
64
|
+
|
|
65
|
+
if (process.env.SUNLINT_DEBUG)
|
|
66
|
+
console.log(`🔧 [S020] Main analyzer initialized successfully`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
analyzeSingle(filePath, options = {}) {
|
|
70
|
+
if (process.env.SUNLINT_DEBUG)
|
|
71
|
+
console.log(`🔍 [S020] analyzeSingle() called for: ${filePath}`);
|
|
72
|
+
return this.analyze([filePath], "typescript", options);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async analyze(files, language, options = {}) {
|
|
76
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
77
|
+
console.log(
|
|
78
|
+
`🔧 [S020] analyze() method called with ${files.length} files, language: ${language}`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const violations = [];
|
|
83
|
+
for (const filePath of files) {
|
|
84
|
+
try {
|
|
85
|
+
if (process.env.SUNLINT_DEBUG)
|
|
86
|
+
console.log(`🔧 [S020] Processing file: ${filePath}`);
|
|
87
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
88
|
+
violations.push(...fileViolations);
|
|
89
|
+
if (process.env.SUNLINT_DEBUG)
|
|
90
|
+
console.log(
|
|
91
|
+
`🔧 [S020] File ${filePath}: Found ${fileViolations.length} violations`
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.warn(
|
|
95
|
+
`⚠ [S020] Analysis failed for ${filePath}:`,
|
|
96
|
+
error.message
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (process.env.SUNLINT_DEBUG)
|
|
102
|
+
console.log(`🔧 [S020] Total violations found: ${violations.length}`);
|
|
103
|
+
return violations;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async analyzeFile(filePath, options = {}) {
|
|
107
|
+
if (process.env.SUNLINT_DEBUG)
|
|
108
|
+
console.log(`🔍 [S020] analyzeFile() called for: ${filePath}`);
|
|
109
|
+
const violationMap = new Map();
|
|
110
|
+
|
|
111
|
+
// Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
|
|
112
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
113
|
+
console.log(
|
|
114
|
+
`🔧 [S020] Symbol check: useSymbolBased=${
|
|
115
|
+
this.config.useSymbolBased
|
|
116
|
+
}, semanticEngine=${!!this.semanticEngine}, project=${!!this
|
|
117
|
+
.semanticEngine?.project}, initialized=${!!this.semanticEngine
|
|
118
|
+
?.initialized}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const canUseSymbol =
|
|
123
|
+
this.config.useSymbolBased &&
|
|
124
|
+
((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
|
|
125
|
+
(!this.semanticEngine && this.config.fallbackToSymbol !== false));
|
|
126
|
+
|
|
127
|
+
if (canUseSymbol) {
|
|
128
|
+
try {
|
|
129
|
+
if (process.env.SUNLINT_DEBUG)
|
|
130
|
+
console.log(`🔧 [S020] Trying symbol-based analysis...`);
|
|
131
|
+
|
|
132
|
+
let sourceFile = null;
|
|
133
|
+
if (this.semanticEngine?.project) {
|
|
134
|
+
sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(
|
|
137
|
+
`🔧 [S020] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!sourceFile) {
|
|
143
|
+
// Create a minimal ts-morph project for this analysis
|
|
144
|
+
if (process.env.SUNLINT_DEBUG)
|
|
145
|
+
console.log(
|
|
146
|
+
`🔧 [S020] Creating temporary ts-morph project for: ${filePath}`
|
|
147
|
+
);
|
|
148
|
+
try {
|
|
149
|
+
const fs = require("fs");
|
|
150
|
+
const path = require("path");
|
|
151
|
+
const { Project } = require("ts-morph");
|
|
152
|
+
|
|
153
|
+
// Check if file exists and read content
|
|
154
|
+
if (!fs.existsSync(filePath)) {
|
|
155
|
+
throw new Error(`File not found: ${filePath}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
159
|
+
const fileName = path.basename(filePath);
|
|
160
|
+
|
|
161
|
+
const tempProject = new Project({
|
|
162
|
+
useInMemoryFileSystem: true,
|
|
163
|
+
compilerOptions: {
|
|
164
|
+
allowJs: true,
|
|
165
|
+
allowSyntheticDefaultImports: true,
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Add file content to in-memory project
|
|
170
|
+
sourceFile = tempProject.createSourceFile(fileName, fileContent);
|
|
171
|
+
if (process.env.SUNLINT_DEBUG)
|
|
172
|
+
console.log(
|
|
173
|
+
`🔧 [S020] Temporary project created successfully with file: ${fileName}`
|
|
174
|
+
);
|
|
175
|
+
} catch (projectError) {
|
|
176
|
+
if (process.env.SUNLINT_DEBUG)
|
|
177
|
+
console.log(
|
|
178
|
+
`🔧 [S020] Failed to create temporary project:`,
|
|
179
|
+
projectError.message
|
|
180
|
+
);
|
|
181
|
+
throw projectError;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (sourceFile) {
|
|
186
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
187
|
+
sourceFile,
|
|
188
|
+
filePath
|
|
189
|
+
);
|
|
190
|
+
symbolViolations.forEach((v) => {
|
|
191
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
192
|
+
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
193
|
+
});
|
|
194
|
+
if (process.env.SUNLINT_DEBUG)
|
|
195
|
+
console.log(
|
|
196
|
+
`🔧 [S020] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// If symbol-based found violations AND prioritizeSymbolic is true, skip regex
|
|
200
|
+
// But still run regex if symbol-based didn't find any violations
|
|
201
|
+
if (this.config.prioritizeSymbolic && symbolViolations.length > 0) {
|
|
202
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
203
|
+
(v) => ({
|
|
204
|
+
...v,
|
|
205
|
+
filePath,
|
|
206
|
+
file: filePath,
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
if (process.env.SUNLINT_DEBUG)
|
|
210
|
+
console.log(
|
|
211
|
+
`🔧 [S020] Symbol-based analysis prioritized: ${finalViolations.length} violations`
|
|
212
|
+
);
|
|
213
|
+
return finalViolations;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
if (process.env.SUNLINT_DEBUG)
|
|
217
|
+
console.log(
|
|
218
|
+
`🔧 [S020] No source file found, skipping symbol analysis`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.warn(`⚠ [S020] Symbol analysis failed:`, error.message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fallback to regex-based analysis
|
|
227
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
228
|
+
try {
|
|
229
|
+
if (process.env.SUNLINT_DEBUG)
|
|
230
|
+
console.log(`🔧 [S020] Trying regex-based analysis...`);
|
|
231
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
232
|
+
regexViolations.forEach((v) => {
|
|
233
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
234
|
+
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
235
|
+
});
|
|
236
|
+
if (process.env.SUNLINT_DEBUG)
|
|
237
|
+
console.log(
|
|
238
|
+
`🔧 [S020] Regex analysis completed: ${regexViolations.length} violations`
|
|
239
|
+
);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.warn(`⚠ [S020] Regex analysis failed:`, error.message);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const finalViolations = Array.from(violationMap.values()).map((v) => ({
|
|
246
|
+
...v,
|
|
247
|
+
filePath,
|
|
248
|
+
file: filePath,
|
|
249
|
+
}));
|
|
250
|
+
if (process.env.SUNLINT_DEBUG)
|
|
251
|
+
console.log(
|
|
252
|
+
`🔧 [S020] File analysis completed: ${finalViolations.length} unique violations`
|
|
253
|
+
);
|
|
254
|
+
return finalViolations;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
cleanup() {
|
|
258
|
+
if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
|
|
259
|
+
if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = S020Analyzer;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S020",
|
|
3
|
+
"name": "Avoid using eval() or executing dynamic code",
|
|
4
|
+
"category": "security",
|
|
5
|
+
"description": "S020 - Avoid using eval() or executing dynamic code as it can lead to code injection vulnerabilities and compromise application security.",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"priority": "high",
|
|
11
|
+
"fallback": "heuristic"
|
|
12
|
+
},
|
|
13
|
+
"patterns": {
|
|
14
|
+
"include": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"],
|
|
15
|
+
"exclude": [
|
|
16
|
+
"**/*.test.js",
|
|
17
|
+
"**/*.test.ts",
|
|
18
|
+
"**/*.spec.js",
|
|
19
|
+
"**/*.spec.ts",
|
|
20
|
+
"**/node_modules/**",
|
|
21
|
+
"**/dist/**",
|
|
22
|
+
"**/build/**"
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
"analysis": {
|
|
26
|
+
"approach": "symbol-based-primary",
|
|
27
|
+
"fallback": "regex-based",
|
|
28
|
+
"depth": 2,
|
|
29
|
+
"timeout": 5000
|
|
30
|
+
},
|
|
31
|
+
"validation": {
|
|
32
|
+
"dangerousFunctions": [
|
|
33
|
+
"eval",
|
|
34
|
+
"Function",
|
|
35
|
+
"setTimeout",
|
|
36
|
+
"setInterval",
|
|
37
|
+
"execScript",
|
|
38
|
+
"setImmediate"
|
|
39
|
+
],
|
|
40
|
+
"dangerousPatterns": [
|
|
41
|
+
"new Function",
|
|
42
|
+
"window.eval",
|
|
43
|
+
"global.eval",
|
|
44
|
+
"globalThis.eval"
|
|
45
|
+
],
|
|
46
|
+
"dynamicCodeIndicators": [
|
|
47
|
+
"code",
|
|
48
|
+
"script",
|
|
49
|
+
"expression",
|
|
50
|
+
"formula",
|
|
51
|
+
"template"
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
}
|