@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.
Files changed (27) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/config/rules/enhanced-rules-registry.json +79 -22
  3. package/core/cli-program.js +1 -1
  4. package/core/file-targeting-service.js +15 -0
  5. package/core/semantic-engine.js +4 -2
  6. package/package.json +1 -1
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +116 -10
  8. package/rules/common/C060_no_override_superclass/analyzer.js +180 -0
  9. package/rules/common/C060_no_override_superclass/config.json +50 -0
  10. package/rules/common/C060_no_override_superclass/symbol-based-analyzer.js +220 -0
  11. package/rules/index.js +1 -0
  12. package/rules/security/S020_no_eval_dynamic_code/README.md +136 -0
  13. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +263 -0
  14. package/rules/security/S020_no_eval_dynamic_code/config.json +54 -0
  15. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +307 -0
  16. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +280 -0
  17. package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +3 -3
  18. package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +3 -4
  19. package/rules/security/S030_directory_browsing_protection/README.md +128 -0
  20. package/rules/security/S030_directory_browsing_protection/analyzer.js +264 -0
  21. package/rules/security/S030_directory_browsing_protection/config.json +63 -0
  22. package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +483 -0
  23. package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +539 -0
  24. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +8 -9
  25. package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +33 -26
  26. package/rules/security/S056_log_injection_protection/analyzer.js +2 -2
  27. package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +77 -118
@@ -101,21 +101,26 @@ class S039SymbolBasedAnalyzer {
101
101
 
102
102
  for (const call of callExpressions) {
103
103
  const expression = call.getExpression();
104
- if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
105
- const methodName = expression.getNameNode().getText();
106
- if (/^(get|post|put|delete|patch|all|use)$/.test(methodName)) {
107
- const args = call.getArguments();
108
- const lastArg = args[args.length - 1];
109
- // The last argument should be the handler function
110
- if (
111
- lastArg.getKind() === SyntaxKind.ArrowFunction ||
112
- lastArg.getKind() === SyntaxKind.FunctionExpression
113
- ) {
114
- routeHandlers.push({
115
- handler: lastArg,
116
- routeCall: call,
117
- type: "express",
118
- });
104
+ if (expression && expression.getKind() === SyntaxKind.PropertyAccessExpression) {
105
+ const nameNode = expression.getNameNode();
106
+ if (nameNode) {
107
+ const methodName = nameNode.getText();
108
+ if (/^(get|post|put|delete|patch|all|use)$/.test(methodName)) {
109
+ const args = call.getArguments();
110
+ const lastArg = args[args.length - 1];
111
+ // The last argument should be the handler function
112
+ if (
113
+ lastArg && (
114
+ lastArg.getKind() === SyntaxKind.ArrowFunction ||
115
+ lastArg.getKind() === SyntaxKind.FunctionExpression
116
+ )
117
+ ) {
118
+ routeHandlers.push({
119
+ handler: lastArg,
120
+ routeCall: call,
121
+ type: "express",
122
+ });
123
+ }
119
124
  }
120
125
  }
121
126
  }
@@ -165,7 +170,7 @@ class S039SymbolBasedAnalyzer {
165
170
  // Nuxt.js defineEventHandler patterns
166
171
  for (const call of callExpressions) {
167
172
  const expression = call.getExpression();
168
- if (expression.getKind() === SyntaxKind.Identifier) {
173
+ if (expression && expression.getKind() === SyntaxKind.Identifier) {
169
174
  const identifier = expression.asKindOrThrow(SyntaxKind.Identifier);
170
175
  if (identifier.getText() === "defineEventHandler") {
171
176
  // Find the arrow function or function parameter
@@ -173,8 +178,10 @@ class S039SymbolBasedAnalyzer {
173
178
  if (args.length > 0) {
174
179
  const firstArg = args[0];
175
180
  if (
176
- firstArg.getKind() === SyntaxKind.ArrowFunction ||
177
- firstArg.getKind() === SyntaxKind.FunctionExpression
181
+ firstArg && (
182
+ firstArg.getKind() === SyntaxKind.ArrowFunction ||
183
+ firstArg.getKind() === SyntaxKind.FunctionExpression
184
+ )
178
185
  ) {
179
186
  routeHandlers.push({
180
187
  handler: firstArg,
@@ -247,7 +254,7 @@ class S039SymbolBasedAnalyzer {
247
254
  const { SyntaxKind } = require("ts-morph");
248
255
 
249
256
  // For NestJS, check decorator parameters first
250
- if (node.getKind() === SyntaxKind.MethodDeclaration) {
257
+ if (node && node.getKind() === SyntaxKind.MethodDeclaration) {
251
258
  const parameters = node.getParameters();
252
259
  for (const param of parameters) {
253
260
  const decorators = param.getDecorators();
@@ -257,7 +264,7 @@ class S039SymbolBasedAnalyzer {
257
264
  const args = decorator.getArguments();
258
265
  if (args.length > 0) {
259
266
  const firstArg = args[0];
260
- if (firstArg.getKind() === SyntaxKind.StringLiteral) {
267
+ if (firstArg && firstArg.getKind() === SyntaxKind.StringLiteral) {
261
268
  const paramName = firstArg.getLiteralValue();
262
269
  if (this.isSessionTokenParam(paramName)) {
263
270
  exposures.push({
@@ -283,7 +290,7 @@ class S039SymbolBasedAnalyzer {
283
290
  const property = propAccess.getName();
284
291
 
285
292
  // Check for req.query.paramName patterns
286
- if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
293
+ if (expression && expression.getKind() === SyntaxKind.PropertyAccessExpression) {
287
294
  const parentExpression = expression.getExpression();
288
295
  const parentProperty = expression.getName();
289
296
 
@@ -314,7 +321,7 @@ class S039SymbolBasedAnalyzer {
314
321
  const argumentExpression = elemAccess.getArgumentExpression();
315
322
 
316
323
  if (
317
- expression.getKind() === SyntaxKind.PropertyAccessExpression &&
324
+ expression && expression.getKind() === SyntaxKind.PropertyAccessExpression &&
318
325
  argumentExpression &&
319
326
  argumentExpression.getKind() === SyntaxKind.StringLiteral
320
327
  ) {
@@ -344,19 +351,19 @@ class S039SymbolBasedAnalyzer {
344
351
 
345
352
  for (const call of callExpressions) {
346
353
  const callExpression = call.getExpression();
347
- if (callExpression.getKind() === SyntaxKind.PropertyAccessExpression) {
354
+ if (callExpression && callExpression.getKind() === SyntaxKind.PropertyAccessExpression) {
348
355
  const methodName = callExpression.getName();
349
356
  const objectExpression = callExpression.getExpression();
350
357
 
351
358
  // searchParams.get("sessionToken"), URLSearchParams.get("token")
352
359
  if (
353
360
  methodName === "get" &&
354
- objectExpression.getText().includes("searchParams")
361
+ objectExpression && objectExpression.getText().includes("searchParams")
355
362
  ) {
356
363
  const args = call.getArguments();
357
364
  if (args.length > 0) {
358
365
  const firstArg = args[0];
359
- if (firstArg.getKind() === SyntaxKind.StringLiteral) {
366
+ if (firstArg && firstArg.getKind() === SyntaxKind.StringLiteral) {
360
367
  const paramName = firstArg.getLiteralValue();
361
368
  if (this.isSessionTokenParam(paramName)) {
362
369
  exposures.push({
@@ -378,7 +385,7 @@ class S039SymbolBasedAnalyzer {
378
385
 
379
386
  for (const varDecl of variableDeclarations) {
380
387
  const nameNode = varDecl.getNameNode();
381
- if (nameNode.getKind() === SyntaxKind.ObjectBindingPattern) {
388
+ if (nameNode && nameNode.getKind() === SyntaxKind.ObjectBindingPattern) {
382
389
  const bindingPattern = nameNode.asKindOrThrow(
383
390
  SyntaxKind.ObjectBindingPattern
384
391
  );
@@ -28,9 +28,9 @@ class S056Analyzer {
28
28
 
29
29
  // Configuration
30
30
  this.config = {
31
- useSymbolBased: true, // Primary approach
31
+ useSymbolBased: true, // Re-enabled with ts-morph API fixes
32
32
  fallbackToRegex: true, // Secondary approach
33
- regexBasedOnly: false, // Can be set to true for pure mode
33
+ regexBasedOnly: false, // Now we can use symbol analysis again
34
34
  };
35
35
 
36
36
  // Initialize analyzers
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * S056 Symbol-Based Analyzer - Protect against Log Injection attacks
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 S056SymbolBasedAnalyzer {
9
9
  constructor(semanticEngine = null) {
@@ -70,51 +70,54 @@ class S056SymbolBasedAnalyzer {
70
70
  }
71
71
  }
72
72
 
73
- async analyze(filePath) {
73
+ async analyze(sourceFileOrPath) {
74
+ // Handle both sourceFile object and file path
75
+ let sourceFile;
76
+ let filePath;
77
+
78
+ if (typeof sourceFileOrPath === 'string') {
79
+ filePath = sourceFileOrPath;
80
+ // Try to get from semantic engine first
81
+ if (this.semanticEngine?.project) {
82
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
83
+ }
84
+
85
+ // If not found, create new ts-morph project
86
+ if (!sourceFile) {
87
+ const { Project } = require("ts-morph");
88
+ const project = new Project();
89
+ sourceFile = project.addSourceFileAtPath(filePath);
90
+ }
91
+ } else {
92
+ // Assume it's already a ts-morph SourceFile
93
+ sourceFile = sourceFileOrPath;
94
+ filePath = sourceFile.getFilePath();
95
+ }
96
+
74
97
  if (this.verbose) {
75
98
  console.log(
76
99
  `🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
77
100
  );
78
101
  }
79
102
 
80
- if (!this.semanticEngine) {
103
+ if (!sourceFile) {
81
104
  if (this.verbose) {
82
105
  console.log(
83
- `🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
106
+ `� [${this.ruleId}] Symbol: Could not create source file`
84
107
  );
85
108
  }
86
109
  return [];
87
110
  }
88
111
 
89
112
  try {
90
- const sourceFile = this.semanticEngine.getSourceFile(filePath);
91
- if (!sourceFile) {
92
- if (this.verbose) {
93
- console.log(
94
- `🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback`
95
- );
96
- }
97
- return await this.analyzeTsMorph(filePath);
98
- }
99
-
100
- if (this.verbose) {
101
- console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`);
102
- }
103
-
104
113
  const violations = [];
105
- const typeChecker = this.semanticEngine.program?.getTypeChecker();
106
114
 
107
- // Visit all nodes in the source file
108
- const visit = (node) => {
109
- // Check for log method calls
110
- if (ts.isCallExpression(node)) {
111
- this.checkLogMethodCall(node, violations, sourceFile, typeChecker);
115
+ // Find all call expressions using ts-morph API
116
+ sourceFile.forEachDescendant((node) => {
117
+ if (node.getKind() === SyntaxKind.CallExpression) {
118
+ this.checkLogMethodCall(node, violations, sourceFile);
112
119
  }
113
-
114
- ts.forEachChild(node, visit);
115
- };
116
-
117
- visit(sourceFile);
120
+ });
118
121
 
119
122
  if (this.verbose) {
120
123
  console.log(
@@ -129,26 +132,28 @@ class S056SymbolBasedAnalyzer {
129
132
  }
130
133
  }
131
134
 
132
- checkLogMethodCall(node, violations, sourceFile, typeChecker) {
133
- // Check if this is a logging method call
134
- const methodName = this.getMethodName(node);
135
+ checkLogMethodCall(callExprNode, violations, sourceFile) {
136
+ // Check if this is a logging method call using ts-morph API
137
+ const methodName = this.getMethodName(callExprNode);
135
138
  if (!methodName || !this.logMethods.includes(methodName)) {
136
139
  return;
137
140
  }
138
141
 
139
142
  // Check arguments for user input
140
- if (node.arguments && node.arguments.length > 0) {
141
- for (const arg of node.arguments) {
142
- if (this.containsUserInput(arg, sourceFile)) {
143
- const position = sourceFile.getLineAndCharacterOfPosition(node.getStart());
143
+ const args = callExprNode.getArguments();
144
+ if (args && args.length > 0) {
145
+ for (const arg of args) {
146
+ if (this.containsUserInput(arg)) {
147
+ const lineAndCol = sourceFile.getLineAndColumnAtPos(callExprNode.getStart());
148
+
144
149
  violations.push({
145
150
  ruleId: this.ruleId,
146
151
  message: `Log injection vulnerability: User input directly used in ${methodName}() call without sanitization`,
147
- line: position.line + 1,
148
- column: position.character + 1,
152
+ line: lineAndCol.line,
153
+ column: lineAndCol.column,
149
154
  severity: "error",
150
155
  category: this.category,
151
- code: sourceFile.getFullText().slice(node.getStart(), node.getEnd())
156
+ code: callExprNode.getText()
152
157
  });
153
158
  break;
154
159
  }
@@ -157,56 +162,62 @@ class S056SymbolBasedAnalyzer {
157
162
  }
158
163
 
159
164
  getMethodName(callExpression) {
160
- const expression = callExpression.expression;
165
+ // Use ts-morph API instead of TypeScript compiler API
166
+ const expression = callExpression.getExpression();
161
167
 
162
- if (ts.isIdentifier(expression)) {
163
- return expression.text;
168
+ if (expression.getKind() === SyntaxKind.Identifier) {
169
+ return expression.getText();
164
170
  }
165
171
 
166
- if (ts.isPropertyAccessExpression(expression)) {
167
- return expression.name.text;
172
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
173
+ return expression.getName();
168
174
  }
169
175
 
170
176
  return null;
171
177
  }
172
178
 
173
- containsUserInput(node, sourceFile) {
174
- // Check for direct user input references
175
- if (ts.isIdentifier(node)) {
176
- return this.userInputSources.includes(node.text);
179
+ containsUserInput(node) {
180
+ // Check for direct user input references using ts-morph API
181
+ if (node.getKind() === SyntaxKind.Identifier) {
182
+ return this.userInputSources.includes(node.getText());
177
183
  }
178
184
 
179
185
  // Check for property access on user input (e.g., req.body, req.query)
180
- if (ts.isPropertyAccessExpression(node)) {
186
+ if (node.getKind() === SyntaxKind.PropertyAccessExpression) {
181
187
  const objectName = this.getObjectName(node);
182
188
  return this.userInputSources.includes(objectName);
183
189
  }
184
190
 
185
191
  // Check for element access on user input (e.g., req["body"], headers['user-agent'])
186
- if (ts.isElementAccessExpression(node)) {
192
+ if (node.getKind() === SyntaxKind.ElementAccessExpression) {
187
193
  const objectName = this.getObjectName(node);
188
194
  return this.userInputSources.includes(objectName);
189
195
  }
190
196
 
191
197
  // Check for binary expressions (concatenation)
192
- if (ts.isBinaryExpression(node)) {
193
- return this.containsUserInput(node.left, sourceFile) ||
194
- this.containsUserInput(node.right, sourceFile);
198
+ if (node.getKind() === SyntaxKind.BinaryExpression) {
199
+ const left = node.getLeft();
200
+ const right = node.getRight();
201
+ return this.containsUserInput(left) || this.containsUserInput(right);
195
202
  }
196
203
 
197
204
  // Check for template literals
198
- if (ts.isTemplateExpression(node)) {
199
- return node.templateSpans.some(span =>
200
- this.containsUserInput(span.expression, sourceFile)
205
+ if (node.getKind() === SyntaxKind.TemplateExpression) {
206
+ const templateSpans = node.getTemplateSpans();
207
+ return templateSpans.some(span =>
208
+ this.containsUserInput(span.getExpression())
201
209
  );
202
210
  }
203
211
 
204
212
  // Check for function calls that might return user input
205
- if (ts.isCallExpression(node)) {
213
+ if (node.getKind() === SyntaxKind.CallExpression) {
206
214
  // Check if it's JSON.stringify with user input
207
215
  const methodName = this.getMethodName(node);
208
- if (methodName === "stringify" && node.arguments.length > 0) {
209
- return this.containsUserInput(node.arguments[0], sourceFile);
216
+ if (methodName === "stringify") {
217
+ const args = node.getArguments();
218
+ if (args.length > 0) {
219
+ return this.containsUserInput(args[0]);
220
+ }
210
221
  }
211
222
  }
212
223
 
@@ -214,71 +225,19 @@ class S056SymbolBasedAnalyzer {
214
225
  }
215
226
 
216
227
  getObjectName(node) {
217
- if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
218
- if (ts.isIdentifier(node.expression)) {
219
- return node.expression.text;
228
+ if (node.getKind() === SyntaxKind.PropertyAccessExpression ||
229
+ node.getKind() === SyntaxKind.ElementAccessExpression) {
230
+ const expression = node.getExpression();
231
+ if (expression.getKind() === SyntaxKind.Identifier) {
232
+ return expression.getText();
220
233
  }
221
234
  }
222
235
  return null;
223
236
  }
224
237
 
225
238
  /**
226
- * Fallback analysis using ts-morph when semantic engine is not available
239
+ * Clean up resources
227
240
  */
228
- async analyzeTsMorph(filePath) {
229
- try {
230
- const fs = require("fs");
231
- const { Project } = require("ts-morph");
232
-
233
- const project = new Project();
234
- const sourceFile = project.addSourceFileAtPath(filePath);
235
- const violations = [];
236
-
237
- // Find all call expressions
238
- sourceFile.forEachDescendant((node) => {
239
- if (node.getKind() === ts.SyntaxKind.CallExpression) {
240
- const callExpr = node;
241
- const methodName = this.extractMethodName(callExpr.getText());
242
-
243
- if (this.logMethods.includes(methodName)) {
244
- const args = callExpr.getArguments();
245
- for (const arg of args) {
246
- if (this.containsUserInputText(arg.getText())) {
247
- const line = sourceFile.getLineAndColumnAtPos(node.getStart()).line;
248
- const column = sourceFile.getLineAndColumnAtPos(node.getStart()).column;
249
-
250
- violations.push({
251
- ruleId: this.ruleId,
252
- message: `Log injection vulnerability: User input directly used in ${methodName}() call without sanitization`,
253
- line: line,
254
- column: column,
255
- severity: "error",
256
- category: this.category,
257
- code: node.getText()
258
- });
259
- break;
260
- }
261
- }
262
- }
263
- }
264
- });
265
-
266
- return violations;
267
- } catch (error) {
268
- console.warn(`⚠ [${this.ruleId}] ts-morph analysis failed:`, error.message);
269
- return [];
270
- }
271
- }
272
-
273
- extractMethodName(callText) {
274
- const match = callText.match(/(\w+)\s*\(/);
275
- return match ? match[1] : null;
276
- }
277
-
278
- containsUserInputText(text) {
279
- return this.userInputSources.some(source => text.includes(source));
280
- }
281
-
282
241
  cleanup() {
283
242
  // Cleanup resources if needed
284
243
  }