@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.
@@ -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 TypeScript compiler API for semantic analysis
3
+ * Uses ts-morph for semantic analysis (consistent with SunLint architecture)
4
4
  */
5
5
 
6
- const ts = require("typescript");
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 TypeScript compiler API for semantic analysis
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 ts = require("typescript");
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(