@sun-asterisk/sunlint 1.3.7 → 1.3.8
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 +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S056 Symbol-Based Analyzer - Protect against Log Injection attacks
|
|
3
|
+
* Uses TypeScript compiler API for semantic analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const ts = require("typescript");
|
|
7
|
+
|
|
8
|
+
class S056SymbolBasedAnalyzer {
|
|
9
|
+
constructor(semanticEngine = null) {
|
|
10
|
+
this.semanticEngine = semanticEngine;
|
|
11
|
+
this.ruleId = "S056";
|
|
12
|
+
this.category = "security";
|
|
13
|
+
|
|
14
|
+
// Log method names that can be vulnerable
|
|
15
|
+
this.logMethods = [
|
|
16
|
+
"log",
|
|
17
|
+
"info",
|
|
18
|
+
"warn",
|
|
19
|
+
"error",
|
|
20
|
+
"debug",
|
|
21
|
+
"trace",
|
|
22
|
+
"write",
|
|
23
|
+
"writeSync"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// User input sources that could lead to injection
|
|
27
|
+
this.userInputSources = [
|
|
28
|
+
"req",
|
|
29
|
+
"request",
|
|
30
|
+
"params",
|
|
31
|
+
"query",
|
|
32
|
+
"body",
|
|
33
|
+
"headers",
|
|
34
|
+
"cookies",
|
|
35
|
+
"session"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Dangerous characters for log injection
|
|
39
|
+
this.dangerousCharacters = [
|
|
40
|
+
"\\r",
|
|
41
|
+
"\\n",
|
|
42
|
+
"\\r\\n",
|
|
43
|
+
"\\u000a",
|
|
44
|
+
"\\u000d",
|
|
45
|
+
"%0a",
|
|
46
|
+
"%0d",
|
|
47
|
+
"\\x0a",
|
|
48
|
+
"\\x0d"
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Secure log patterns
|
|
52
|
+
this.securePatterns = [
|
|
53
|
+
"sanitize",
|
|
54
|
+
"escape",
|
|
55
|
+
"clean",
|
|
56
|
+
"filter",
|
|
57
|
+
"validate",
|
|
58
|
+
"replace",
|
|
59
|
+
"strip"
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize analyzer with semantic engine
|
|
65
|
+
*/
|
|
66
|
+
async initialize(semanticEngine) {
|
|
67
|
+
this.semanticEngine = semanticEngine;
|
|
68
|
+
if (this.verbose) {
|
|
69
|
+
console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async analyze(filePath) {
|
|
74
|
+
if (this.verbose) {
|
|
75
|
+
console.log(
|
|
76
|
+
`🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.semanticEngine) {
|
|
81
|
+
if (this.verbose) {
|
|
82
|
+
console.log(
|
|
83
|
+
`🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
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
|
+
const violations = [];
|
|
105
|
+
const typeChecker = this.semanticEngine.program?.getTypeChecker();
|
|
106
|
+
|
|
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);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ts.forEachChild(node, visit);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
visit(sourceFile);
|
|
118
|
+
|
|
119
|
+
if (this.verbose) {
|
|
120
|
+
console.log(
|
|
121
|
+
`🔧 [${this.ruleId}] Symbol analysis completed: ${violations.length} violations found`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return violations;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.warn(`⚠ [${this.ruleId}] Symbol analysis failed:`, error.message);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
checkLogMethodCall(node, violations, sourceFile, typeChecker) {
|
|
133
|
+
// Check if this is a logging method call
|
|
134
|
+
const methodName = this.getMethodName(node);
|
|
135
|
+
if (!methodName || !this.logMethods.includes(methodName)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 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());
|
|
144
|
+
violations.push({
|
|
145
|
+
ruleId: this.ruleId,
|
|
146
|
+
message: `Log injection vulnerability: User input directly used in ${methodName}() call without sanitization`,
|
|
147
|
+
line: position.line + 1,
|
|
148
|
+
column: position.character + 1,
|
|
149
|
+
severity: "error",
|
|
150
|
+
category: this.category,
|
|
151
|
+
code: sourceFile.getFullText().slice(node.getStart(), node.getEnd())
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
getMethodName(callExpression) {
|
|
160
|
+
const expression = callExpression.expression;
|
|
161
|
+
|
|
162
|
+
if (ts.isIdentifier(expression)) {
|
|
163
|
+
return expression.text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
167
|
+
return expression.name.text;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
containsUserInput(node, sourceFile) {
|
|
174
|
+
// Check for direct user input references
|
|
175
|
+
if (ts.isIdentifier(node)) {
|
|
176
|
+
return this.userInputSources.includes(node.text);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for property access on user input (e.g., req.body, req.query)
|
|
180
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
181
|
+
const objectName = this.getObjectName(node);
|
|
182
|
+
return this.userInputSources.includes(objectName);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check for element access on user input (e.g., req["body"], headers['user-agent'])
|
|
186
|
+
if (ts.isElementAccessExpression(node)) {
|
|
187
|
+
const objectName = this.getObjectName(node);
|
|
188
|
+
return this.userInputSources.includes(objectName);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Check for binary expressions (concatenation)
|
|
192
|
+
if (ts.isBinaryExpression(node)) {
|
|
193
|
+
return this.containsUserInput(node.left, sourceFile) ||
|
|
194
|
+
this.containsUserInput(node.right, sourceFile);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check for template literals
|
|
198
|
+
if (ts.isTemplateExpression(node)) {
|
|
199
|
+
return node.templateSpans.some(span =>
|
|
200
|
+
this.containsUserInput(span.expression, sourceFile)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check for function calls that might return user input
|
|
205
|
+
if (ts.isCallExpression(node)) {
|
|
206
|
+
// Check if it's JSON.stringify with user input
|
|
207
|
+
const methodName = this.getMethodName(node);
|
|
208
|
+
if (methodName === "stringify" && node.arguments.length > 0) {
|
|
209
|
+
return this.containsUserInput(node.arguments[0], sourceFile);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getObjectName(node) {
|
|
217
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
218
|
+
if (ts.isIdentifier(node.expression)) {
|
|
219
|
+
return node.expression.text;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Fallback analysis using ts-morph when semantic engine is not available
|
|
227
|
+
*/
|
|
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
|
+
cleanup() {
|
|
283
|
+
// Cleanup resources if needed
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = S056SymbolBasedAnalyzer;
|