@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
|
@@ -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
|
|
106
|
-
if (
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
177
|
-
|
|
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, //
|
|
31
|
+
useSymbolBased: true, // Re-enabled with ts-morph API fixes
|
|
32
32
|
fallbackToRegex: true, // Secondary approach
|
|
33
|
-
regexBasedOnly: false, //
|
|
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
|
|
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 S056SymbolBasedAnalyzer {
|
|
9
9
|
constructor(semanticEngine = null) {
|
|
@@ -70,51 +70,54 @@ class S056SymbolBasedAnalyzer {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async analyze(
|
|
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 (!
|
|
103
|
+
if (!sourceFile) {
|
|
81
104
|
if (this.verbose) {
|
|
82
105
|
console.log(
|
|
83
|
-
|
|
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
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
133
|
-
// Check if this is a logging method call
|
|
134
|
-
const methodName = this.getMethodName(
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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:
|
|
148
|
-
column:
|
|
152
|
+
line: lineAndCol.line,
|
|
153
|
+
column: lineAndCol.column,
|
|
149
154
|
severity: "error",
|
|
150
155
|
category: this.category,
|
|
151
|
-
code:
|
|
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
|
-
|
|
165
|
+
// Use ts-morph API instead of TypeScript compiler API
|
|
166
|
+
const expression = callExpression.getExpression();
|
|
161
167
|
|
|
162
|
-
if (
|
|
163
|
-
return expression.
|
|
168
|
+
if (expression.getKind() === SyntaxKind.Identifier) {
|
|
169
|
+
return expression.getText();
|
|
164
170
|
}
|
|
165
171
|
|
|
166
|
-
if (
|
|
167
|
-
return expression.
|
|
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
|
|
174
|
-
// Check for direct user input references
|
|
175
|
-
if (
|
|
176
|
-
return this.userInputSources.includes(node.
|
|
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 (
|
|
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 (
|
|
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 (
|
|
193
|
-
|
|
194
|
-
|
|
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 (
|
|
199
|
-
|
|
200
|
-
|
|
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 (
|
|
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"
|
|
209
|
-
|
|
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 (
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
*
|
|
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
|
}
|