@sun-asterisk/sunlint 1.3.8 → 1.3.9
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 +61 -22
- package/core/file-targeting-service.js +15 -0
- package/package.json +1 -1
- 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,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S020 Regex-Based Analyzer - Avoid using eval() or executing dynamic code
|
|
3
|
+
* Detects dangerous dynamic code execution patterns across different contexts
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
|
|
7
|
+
class S020RegexBasedAnalyzer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.ruleId = "S020";
|
|
10
|
+
|
|
11
|
+
// Dangerous function patterns
|
|
12
|
+
this.dangerousPatterns = {
|
|
13
|
+
// Direct eval calls
|
|
14
|
+
eval: /\beval\s*\(/g,
|
|
15
|
+
// Function constructor
|
|
16
|
+
functionConstructor: /\bnew\s+Function\s*\(/g,
|
|
17
|
+
// Global eval access
|
|
18
|
+
globalEval: /\b(window|global|globalThis|self)\.eval\s*\(/g,
|
|
19
|
+
// setTimeout/setInterval with strings (but not function references)
|
|
20
|
+
timerWithString: /(setTimeout|setInterval)\s*\(\s*['"`]/g,
|
|
21
|
+
// execScript (IE legacy) - including type casting
|
|
22
|
+
execScript: /(\bexecScript\s*\(|\(\w+\s+as\s+any\)\.execScript\s*\()/g,
|
|
23
|
+
// setImmediate with strings (but not function references) - including type casting
|
|
24
|
+
setImmediateString:
|
|
25
|
+
/(\bsetImmediate\s*\(\s*['"`]|\(\s*setImmediate\s+as\s+any\)\s*\(\s*['"`])/g,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Dynamic code indicators in variable names
|
|
29
|
+
this.dynamicCodeVariables =
|
|
30
|
+
/\b(code|script|expression|formula|template|eval)\w*\s*=/gi;
|
|
31
|
+
|
|
32
|
+
// Execution context patterns
|
|
33
|
+
this.executionPatterns = {
|
|
34
|
+
// Function.prototype.call/apply with dynamic strings
|
|
35
|
+
functionCall: /Function\.prototype\.(call|apply)/g,
|
|
36
|
+
// Code generation patterns
|
|
37
|
+
codeGeneration: /(generate|build|create)\w*\s*(code|script|function)/gi,
|
|
38
|
+
// Template execution
|
|
39
|
+
templateExecution: /(execute|run|eval)\w*\s*(template|expression)/gi,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async analyze(filePath) {
|
|
44
|
+
// Skip files that are unlikely to contain dynamic code execution
|
|
45
|
+
const skipPatterns = [
|
|
46
|
+
/\.d\.ts$/,
|
|
47
|
+
/\.types\.ts$/,
|
|
48
|
+
/\.interface\.ts$/,
|
|
49
|
+
/\.constants?\.ts$/,
|
|
50
|
+
/\.config\.ts$/,
|
|
51
|
+
/\.spec\.ts$/,
|
|
52
|
+
/\.test\.ts$/,
|
|
53
|
+
/\.min\.js$/,
|
|
54
|
+
/\.bundle\.js$/,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
58
|
+
if (shouldSkip) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
63
|
+
const lines = content.split(/\r?\n/);
|
|
64
|
+
const violations = [];
|
|
65
|
+
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i];
|
|
68
|
+
const lineNumber = i + 1;
|
|
69
|
+
|
|
70
|
+
// Skip comments and imports
|
|
71
|
+
if (this.shouldSkipLine(line)) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for dangerous function patterns
|
|
76
|
+
this.checkDangerousPatterns(line, lineNumber, violations);
|
|
77
|
+
|
|
78
|
+
// Check for dynamic code variable patterns
|
|
79
|
+
this.checkDynamicCodeVariables(line, lineNumber, violations);
|
|
80
|
+
|
|
81
|
+
// Check for execution context patterns
|
|
82
|
+
this.checkExecutionPatterns(line, lineNumber, violations);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return violations;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
shouldSkipLine(line) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
|
|
91
|
+
// Skip empty lines
|
|
92
|
+
if (!trimmed) return true;
|
|
93
|
+
|
|
94
|
+
// Skip single-line comments
|
|
95
|
+
if (trimmed.startsWith("//")) return true;
|
|
96
|
+
|
|
97
|
+
// Skip import/export statements
|
|
98
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("export "))
|
|
99
|
+
return true;
|
|
100
|
+
|
|
101
|
+
// Skip require statements
|
|
102
|
+
if (trimmed.startsWith("const ") && trimmed.includes("require("))
|
|
103
|
+
return true;
|
|
104
|
+
|
|
105
|
+
// Skip JSDoc comments
|
|
106
|
+
if (
|
|
107
|
+
trimmed.startsWith("*") ||
|
|
108
|
+
trimmed.startsWith("/**") ||
|
|
109
|
+
trimmed.startsWith("*/")
|
|
110
|
+
)
|
|
111
|
+
return true;
|
|
112
|
+
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
checkDangerousPatterns(line, lineNumber, violations) {
|
|
117
|
+
// Debug log for setImmediate lines specifically
|
|
118
|
+
if (line.includes("setImmediate") && process.env.SUNLINT_DEBUG) {
|
|
119
|
+
console.log(
|
|
120
|
+
`🔍 [S020-Regex] Checking setImmediate line ${lineNumber}: ${line.trim()}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const [patternName, pattern] of Object.entries(
|
|
125
|
+
this.dangerousPatterns
|
|
126
|
+
)) {
|
|
127
|
+
const matches = [...line.matchAll(pattern)];
|
|
128
|
+
|
|
129
|
+
// Debug log for setImmediate pattern specifically
|
|
130
|
+
if (
|
|
131
|
+
patternName === "setImmediateString" &&
|
|
132
|
+
line.includes("setImmediate") &&
|
|
133
|
+
process.env.SUNLINT_DEBUG
|
|
134
|
+
) {
|
|
135
|
+
console.log(
|
|
136
|
+
`🔍 [S020-Regex] Testing setImmediateString pattern on line ${lineNumber}`
|
|
137
|
+
);
|
|
138
|
+
console.log(`🔍 [S020-Regex] Pattern: ${pattern}`);
|
|
139
|
+
console.log(`🔍 [S020-Regex] Matches found: ${matches.length}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const match of matches) {
|
|
143
|
+
let message;
|
|
144
|
+
let severity = "error";
|
|
145
|
+
|
|
146
|
+
switch (patternName) {
|
|
147
|
+
case "eval":
|
|
148
|
+
message =
|
|
149
|
+
"Direct eval() call can execute arbitrary code and poses security risks";
|
|
150
|
+
break;
|
|
151
|
+
case "functionConstructor":
|
|
152
|
+
message =
|
|
153
|
+
"Function constructor can create functions from strings and execute dynamic code";
|
|
154
|
+
break;
|
|
155
|
+
case "globalEval":
|
|
156
|
+
message = `Global eval access '${match[0]}' can execute arbitrary code and poses security risks`;
|
|
157
|
+
break;
|
|
158
|
+
case "timerWithString":
|
|
159
|
+
message = `${match[1]}() with string argument can execute dynamic code - use function reference instead`;
|
|
160
|
+
severity = "warning";
|
|
161
|
+
break;
|
|
162
|
+
case "execScript":
|
|
163
|
+
message =
|
|
164
|
+
"execScript() can execute arbitrary code and poses security risks";
|
|
165
|
+
break;
|
|
166
|
+
case "setImmediateString":
|
|
167
|
+
message =
|
|
168
|
+
"setImmediate() with string argument can execute dynamic code - use function reference instead";
|
|
169
|
+
severity = "warning";
|
|
170
|
+
break;
|
|
171
|
+
default:
|
|
172
|
+
message = "Potential dynamic code execution detected";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
violations.push({
|
|
176
|
+
ruleId: this.ruleId,
|
|
177
|
+
message: message,
|
|
178
|
+
severity: severity,
|
|
179
|
+
line: lineNumber,
|
|
180
|
+
column: match.index + 1,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
184
|
+
console.log(
|
|
185
|
+
`🔧 [S020-Regex] Found ${patternName} at line ${lineNumber}: ${match[0]}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
checkDynamicCodeVariables(line, lineNumber, violations) {
|
|
193
|
+
const matches = [...line.matchAll(this.dynamicCodeVariables)];
|
|
194
|
+
|
|
195
|
+
for (const match of matches) {
|
|
196
|
+
// Additional context checking to reduce false positives
|
|
197
|
+
const context = line.toLowerCase();
|
|
198
|
+
|
|
199
|
+
// Check if it's actually related to code execution
|
|
200
|
+
if (this.isLikelyDynamicCode(context)) {
|
|
201
|
+
violations.push({
|
|
202
|
+
ruleId: this.ruleId,
|
|
203
|
+
message: `Variable '${match[0].trim()}' suggests dynamic code handling - review for potential code execution`,
|
|
204
|
+
severity: "warning",
|
|
205
|
+
line: lineNumber,
|
|
206
|
+
column: match.index + 1,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
210
|
+
console.log(
|
|
211
|
+
`🔧 [S020-Regex] Found dynamic code variable at line ${lineNumber}: ${match[0]}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
checkExecutionPatterns(line, lineNumber, violations) {
|
|
219
|
+
for (const [patternName, pattern] of Object.entries(
|
|
220
|
+
this.executionPatterns
|
|
221
|
+
)) {
|
|
222
|
+
const matches = [...line.matchAll(pattern)];
|
|
223
|
+
|
|
224
|
+
for (const match of matches) {
|
|
225
|
+
let message;
|
|
226
|
+
|
|
227
|
+
switch (patternName) {
|
|
228
|
+
case "functionCall":
|
|
229
|
+
message = `Function.prototype.${match[1]} may be used for dynamic code execution - review implementation`;
|
|
230
|
+
break;
|
|
231
|
+
case "codeGeneration":
|
|
232
|
+
message = `Code generation pattern '${match[0]}' detected - ensure no dynamic code execution`;
|
|
233
|
+
break;
|
|
234
|
+
case "templateExecution":
|
|
235
|
+
message = `Template execution pattern '${match[0]}' detected - ensure safe template handling`;
|
|
236
|
+
break;
|
|
237
|
+
default:
|
|
238
|
+
message = "Potential code execution pattern detected";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
violations.push({
|
|
242
|
+
ruleId: this.ruleId,
|
|
243
|
+
message: message,
|
|
244
|
+
severity: "warning",
|
|
245
|
+
line: lineNumber,
|
|
246
|
+
column: match.index + 1,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
250
|
+
console.log(
|
|
251
|
+
`🔧 [S020-Regex] Found execution pattern ${patternName} at line ${lineNumber}: ${match[0]}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
isLikelyDynamicCode(context) {
|
|
259
|
+
// Keywords that suggest actual code execution
|
|
260
|
+
const codeExecutionKeywords = [
|
|
261
|
+
"execute",
|
|
262
|
+
"run",
|
|
263
|
+
"eval",
|
|
264
|
+
"compile",
|
|
265
|
+
"interpret",
|
|
266
|
+
"function",
|
|
267
|
+
"method",
|
|
268
|
+
"call",
|
|
269
|
+
"invoke",
|
|
270
|
+
"dynamic",
|
|
271
|
+
"runtime",
|
|
272
|
+
"generated",
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
// Keywords that suggest benign usage (reduce false positives)
|
|
276
|
+
const benignKeywords = [
|
|
277
|
+
"html",
|
|
278
|
+
"css",
|
|
279
|
+
"style",
|
|
280
|
+
"markup",
|
|
281
|
+
"tag",
|
|
282
|
+
"error",
|
|
283
|
+
"message",
|
|
284
|
+
"text",
|
|
285
|
+
"string",
|
|
286
|
+
"config",
|
|
287
|
+
"setting",
|
|
288
|
+
"option",
|
|
289
|
+
"parameter",
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const hasExecutionKeyword = codeExecutionKeywords.some((keyword) =>
|
|
293
|
+
context.includes(keyword)
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const hasBenignKeyword = benignKeywords.some((keyword) =>
|
|
297
|
+
context.includes(keyword)
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
// Only flag if there's execution context and no benign indicators
|
|
301
|
+
return hasExecutionKeyword && !hasBenignKeyword;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
cleanup() {}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = S020RegexBasedAnalyzer;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S020 Symbol-Based Analyzer - Avoid using eval() or executing dynamic code
|
|
3
|
+
* Enhanced to analyze function calls, constructor patterns, and dynamic code execution
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class S020SymbolBasedAnalyzer {
|
|
7
|
+
constructor(semanticEngine) {
|
|
8
|
+
this.ruleId = "S020";
|
|
9
|
+
this.semanticEngine = semanticEngine;
|
|
10
|
+
|
|
11
|
+
// Dangerous functions that execute dynamic code
|
|
12
|
+
this.dangerousFunctions = ["eval", "Function", "execScript"];
|
|
13
|
+
|
|
14
|
+
// Functions that can execute code when used with strings
|
|
15
|
+
this.conditionallyDangerous = ["setTimeout", "setInterval", "setImmediate"];
|
|
16
|
+
|
|
17
|
+
// Constructor patterns to detect
|
|
18
|
+
this.dangerousConstructors = ["Function"];
|
|
19
|
+
|
|
20
|
+
// Global object access patterns
|
|
21
|
+
this.globalAccessPatterns = [
|
|
22
|
+
"window.eval",
|
|
23
|
+
"global.eval",
|
|
24
|
+
"globalThis.eval",
|
|
25
|
+
"self.eval",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Dynamic code indicators in variable names/strings
|
|
29
|
+
this.dynamicCodeIndicators = [
|
|
30
|
+
"code",
|
|
31
|
+
"script",
|
|
32
|
+
"expression",
|
|
33
|
+
"formula",
|
|
34
|
+
"template",
|
|
35
|
+
"eval",
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initialize() {}
|
|
40
|
+
|
|
41
|
+
analyze(sourceFile, filePath) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
|
|
44
|
+
// Skip files that are unlikely to contain dynamic code execution
|
|
45
|
+
const skipPatterns = [
|
|
46
|
+
/\.d\.ts$/,
|
|
47
|
+
/\.types\.ts$/,
|
|
48
|
+
/\.interface\.ts$/,
|
|
49
|
+
/\.constants?\.ts$/,
|
|
50
|
+
/\.config\.ts$/,
|
|
51
|
+
/\.spec\.ts$/,
|
|
52
|
+
/\.test\.ts$/,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const shouldSkip = skipPatterns.some((pattern) => pattern.test(filePath));
|
|
56
|
+
if (shouldSkip) {
|
|
57
|
+
return violations;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const { SyntaxKind } = require("ts-morph");
|
|
62
|
+
|
|
63
|
+
// Find all call expressions for function calls
|
|
64
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
65
|
+
SyntaxKind.CallExpression
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
for (const call of callExpressions) {
|
|
69
|
+
try {
|
|
70
|
+
const callViolations = this.analyzeCallExpression(call, filePath);
|
|
71
|
+
violations.push(...callViolations);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.warn(
|
|
74
|
+
`⚠ [S020] Call expression analysis failed:`,
|
|
75
|
+
error.message
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Find all new expressions for constructor calls
|
|
81
|
+
const newExpressions = sourceFile.getDescendantsOfKind(
|
|
82
|
+
SyntaxKind.NewExpression
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
for (const newExpr of newExpressions) {
|
|
86
|
+
try {
|
|
87
|
+
const newViolations = this.analyzeNewExpression(newExpr, filePath);
|
|
88
|
+
violations.push(...newViolations);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.warn(
|
|
91
|
+
`⚠ [S020] New expression analysis failed:`,
|
|
92
|
+
error.message
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Find property access expressions for global object access
|
|
98
|
+
const propertyAccesses = sourceFile.getDescendantsOfKind(
|
|
99
|
+
SyntaxKind.PropertyAccessExpression
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
for (const propAccess of propertyAccesses) {
|
|
103
|
+
try {
|
|
104
|
+
const propViolations = this.analyzePropertyAccess(
|
|
105
|
+
propAccess,
|
|
106
|
+
filePath
|
|
107
|
+
);
|
|
108
|
+
violations.push(...propViolations);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.warn(
|
|
111
|
+
`⚠ [S020] Property access analysis failed:`,
|
|
112
|
+
error.message
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`⚠ [S020] Symbol analysis failed for ${filePath}:`,
|
|
119
|
+
error.message
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return violations;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
analyzeCallExpression(call, filePath) {
|
|
127
|
+
const violations = [];
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const { SyntaxKind } = require("ts-morph");
|
|
131
|
+
|
|
132
|
+
const expression = call.getExpression();
|
|
133
|
+
let functionName = null;
|
|
134
|
+
|
|
135
|
+
// Direct function call: eval(), Function()
|
|
136
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
137
|
+
functionName = expression.getText();
|
|
138
|
+
}
|
|
139
|
+
// Property access: window.eval(), global.Function()
|
|
140
|
+
else if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
|
|
141
|
+
const fullExpression = expression.getText();
|
|
142
|
+
const propertyName = expression.getName();
|
|
143
|
+
|
|
144
|
+
// Check for global access patterns
|
|
145
|
+
if (
|
|
146
|
+
this.globalAccessPatterns.some(
|
|
147
|
+
(pattern) => fullExpression === pattern
|
|
148
|
+
)
|
|
149
|
+
) {
|
|
150
|
+
functionName = propertyName;
|
|
151
|
+
} else if (this.dangerousFunctions.includes(propertyName)) {
|
|
152
|
+
functionName = propertyName;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Type assertion / type casting: (setImmediate as any)("string")
|
|
156
|
+
else if (expression.getKind() === SyntaxKind.ParenthesizedExpression) {
|
|
157
|
+
const innerExpression = expression.getExpression();
|
|
158
|
+
if (innerExpression.getKind() === SyntaxKind.AsExpression) {
|
|
159
|
+
const asExpression = innerExpression;
|
|
160
|
+
const leftExpression = asExpression.getExpression();
|
|
161
|
+
if (leftExpression.getKind() === SyntaxKind.Identifier) {
|
|
162
|
+
functionName = leftExpression.getText();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (functionName) {
|
|
168
|
+
// Check for dangerous functions
|
|
169
|
+
if (this.dangerousFunctions.includes(functionName)) {
|
|
170
|
+
const startLine = call.getStartLineNumber();
|
|
171
|
+
violations.push({
|
|
172
|
+
ruleId: this.ruleId,
|
|
173
|
+
message: `Dangerous function '${functionName}()' can execute arbitrary code and poses security risks`,
|
|
174
|
+
severity: "error",
|
|
175
|
+
line: startLine,
|
|
176
|
+
column: 1,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// Check for conditionally dangerous functions (setTimeout/setInterval with string)
|
|
180
|
+
else if (this.conditionallyDangerous.includes(functionName)) {
|
|
181
|
+
const args = call.getArguments();
|
|
182
|
+
if (args.length > 0) {
|
|
183
|
+
const firstArg = args[0];
|
|
184
|
+
|
|
185
|
+
// Check if first argument is a string literal
|
|
186
|
+
if (firstArg.getKind() === SyntaxKind.StringLiteral) {
|
|
187
|
+
const startLine = call.getStartLineNumber();
|
|
188
|
+
violations.push({
|
|
189
|
+
ruleId: this.ruleId,
|
|
190
|
+
message: `Function '${functionName}()' with string argument can execute dynamic code - use function reference instead`,
|
|
191
|
+
severity: "warning",
|
|
192
|
+
line: startLine,
|
|
193
|
+
column: 1,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Check if first argument contains dynamic code indicators
|
|
197
|
+
else {
|
|
198
|
+
const argText = firstArg.getText().toLowerCase();
|
|
199
|
+
const hasDynamicIndicator = this.dynamicCodeIndicators.some(
|
|
200
|
+
(indicator) => argText.includes(indicator)
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (hasDynamicIndicator) {
|
|
204
|
+
const startLine = call.getStartLineNumber();
|
|
205
|
+
violations.push({
|
|
206
|
+
ruleId: this.ruleId,
|
|
207
|
+
message: `Function '${functionName}()' may be executing dynamic code based on variable naming`,
|
|
208
|
+
severity: "warning",
|
|
209
|
+
line: startLine,
|
|
210
|
+
column: 1,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.warn(`⚠ [S020] Call expression analysis failed:`, error.message);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return violations;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
analyzeNewExpression(newExpr, filePath) {
|
|
225
|
+
const violations = [];
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const { SyntaxKind } = require("ts-morph");
|
|
229
|
+
|
|
230
|
+
const expression = newExpr.getExpression();
|
|
231
|
+
|
|
232
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
233
|
+
const constructorName = expression.getText();
|
|
234
|
+
|
|
235
|
+
if (this.dangerousConstructors.includes(constructorName)) {
|
|
236
|
+
const startLine = newExpr.getStartLineNumber();
|
|
237
|
+
violations.push({
|
|
238
|
+
ruleId: this.ruleId,
|
|
239
|
+
message: `Constructor 'new ${constructorName}()' can create functions from strings and execute dynamic code`,
|
|
240
|
+
severity: "error",
|
|
241
|
+
line: startLine,
|
|
242
|
+
column: 1,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
console.warn(`⚠ [S020] New expression analysis failed:`, error.message);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return violations;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
analyzePropertyAccess(propAccess, filePath) {
|
|
254
|
+
const violations = [];
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const fullExpression = propAccess.getText();
|
|
258
|
+
|
|
259
|
+
// Check for global eval access patterns
|
|
260
|
+
if (this.globalAccessPatterns.includes(fullExpression)) {
|
|
261
|
+
const startLine = propAccess.getStartLineNumber();
|
|
262
|
+
violations.push({
|
|
263
|
+
ruleId: this.ruleId,
|
|
264
|
+
message: `Global eval access '${fullExpression}' can execute arbitrary code and poses security risks`,
|
|
265
|
+
severity: "error",
|
|
266
|
+
line: startLine,
|
|
267
|
+
column: 1,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
console.warn(`⚠ [S020] Property access analysis failed:`, error.message);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return violations;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
cleanup() {}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = S020SymbolBasedAnalyzer;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S024 Symbol-Based Analyzer - Protect against XPath Injection and XML External Entity (XXE)
|
|
3
|
-
* Uses
|
|
3
|
+
* Uses ts-morph for semantic analysis (consistent with SunLint architecture)
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const { SyntaxKind } = require("ts-morph");
|
|
7
7
|
|
|
8
8
|
class S024SymbolBasedAnalyzer {
|
|
9
9
|
constructor(semanticEngine = null) {
|
|
@@ -85,7 +85,7 @@ class S024SymbolBasedAnalyzer {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
try {
|
|
88
|
-
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
88
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
89
89
|
if (!sourceFile) {
|
|
90
90
|
if (this.verbose) {
|
|
91
91
|
console.log(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S025 Symbol-Based Analyzer - Always validate client-side data on the server
|
|
3
|
-
* Uses
|
|
3
|
+
* Uses ts-morph for semantic analysis (consistent with SunLint architecture)
|
|
4
4
|
*
|
|
5
5
|
* Detects patterns where client data is used without server-side validation:
|
|
6
6
|
* 1. Using @Body() without ValidationPipe or DTO validation
|
|
@@ -8,10 +8,9 @@
|
|
|
8
8
|
* 3. Direct use of req.body, req.query, req.params without validation
|
|
9
9
|
* 4. Missing ValidationPipe configuration
|
|
10
10
|
* 5. SQL injection via string concatenation
|
|
11
|
-
* 6. File upload without server-side validation
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
const
|
|
13
|
+
const { SyntaxKind } = require("ts-morph");
|
|
15
14
|
|
|
16
15
|
class S025SymbolBasedAnalyzer {
|
|
17
16
|
constructor(semanticEngine = null) {
|
|
@@ -81,7 +80,7 @@ class S025SymbolBasedAnalyzer {
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
try {
|
|
84
|
-
const sourceFile = this.semanticEngine.getSourceFile(filePath);
|
|
83
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
85
84
|
if (!sourceFile) {
|
|
86
85
|
if (this.verbose) {
|
|
87
86
|
console.log(
|