@sun-asterisk/sunlint 1.3.4 → 1.3.6
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 +62 -0
- package/config/presets/all.json +49 -48
- package/config/presets/beginner.json +7 -18
- package/config/presets/ci.json +63 -27
- package/config/presets/maintainability.json +6 -4
- package/config/presets/performance.json +4 -3
- package/config/presets/quality.json +11 -50
- package/config/presets/recommended.json +83 -10
- package/config/presets/security.json +20 -19
- package/config/presets/strict.json +6 -13
- package/config/rule-analysis-strategies.js +5 -0
- package/config/rules/enhanced-rules-registry.json +87 -7
- package/core/config-preset-resolver.js +7 -2
- package/package.json +1 -1
- package/rules/common/C067_no_hardcoded_config/analyzer.js +95 -0
- package/rules/common/C067_no_hardcoded_config/config.json +81 -0
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +1034 -0
- package/rules/common/C070_no_real_time_tests/analyzer.js +320 -0
- package/rules/common/C070_no_real_time_tests/config.json +78 -0
- package/rules/common/C070_no_real_time_tests/regex-analyzer.js +424 -0
- package/rules/security/S024_xpath_xxe_protection/analyzer.js +242 -0
- package/rules/security/S024_xpath_xxe_protection/config.json +152 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +338 -0
- package/rules/security/S024_xpath_xxe_protection/symbol-based-analyzer.js +474 -0
- package/rules/security/S025_server_side_validation/README.md +179 -0
- package/rules/security/S025_server_side_validation/analyzer.js +242 -0
- package/rules/security/S025_server_side_validation/config.json +111 -0
- package/rules/security/S025_server_side_validation/regex-based-analyzer.js +388 -0
- package/rules/security/S025_server_side_validation/symbol-based-analyzer.js +523 -0
- package/scripts/README.md +83 -0
- package/scripts/analyze-core-rules.js +151 -0
- package/scripts/generate-presets.js +202 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { CommentDetector } = require('../../utils/rule-helpers');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* C070 - Tests Should Not Depend on Real Time
|
|
7
|
+
* Detects real-time sleeps/timeouts in test files and suggests fake timers
|
|
8
|
+
*
|
|
9
|
+
* Focus: Improve test reliability by avoiding time-dependent flaky tests
|
|
10
|
+
*/
|
|
11
|
+
class C070TestRealTimeAnalyzer {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.ruleId = 'C070';
|
|
14
|
+
this.configPath = path.join(__dirname, 'config.json');
|
|
15
|
+
this.config = this.loadConfig();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
loadConfig() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.warn(`Failed to load config for ${this.ruleId}:`, error.message);
|
|
23
|
+
return this.getDefaultConfig();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
getDefaultConfig() {
|
|
28
|
+
return {
|
|
29
|
+
options: {
|
|
30
|
+
timerApis: {
|
|
31
|
+
ts_js: [
|
|
32
|
+
"setTimeout\\s*\\(",
|
|
33
|
+
"setInterval\\s*\\(",
|
|
34
|
+
"\\.sleep\\s*\\(",
|
|
35
|
+
"\\.delay\\s*\\(",
|
|
36
|
+
"\\.wait\\s*\\(",
|
|
37
|
+
"new\\s+Promise.*setTimeout"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
fakeTimerDetectors: {
|
|
41
|
+
jest_vitest: [
|
|
42
|
+
"jest\\.useFakeTimers\\(\\)",
|
|
43
|
+
"vi\\.useFakeTimers\\(\\)",
|
|
44
|
+
"jest\\.advanceTimersByTime",
|
|
45
|
+
"vi\\.advanceTimersByTime"
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
busyPollingDetectors: {
|
|
49
|
+
ts_js: ["Date\\.now\\(\\)", "new\\s+Date\\(\\)"]
|
|
50
|
+
},
|
|
51
|
+
allowAnnotations: ["@perf", "@benchmark", "@allow-real-time", "// @allow-real-time"]
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Check if file is a test file
|
|
58
|
+
*/
|
|
59
|
+
isTestFile(filePath) {
|
|
60
|
+
const testPatterns = [
|
|
61
|
+
/\.test\.(js|ts|jsx|tsx)$/,
|
|
62
|
+
/\.spec\.(js|ts|jsx|tsx)$/,
|
|
63
|
+
/__tests__\//,
|
|
64
|
+
/\/tests?\//,
|
|
65
|
+
/test-cases\.(js|ts)$/ // Add pattern for our test cases
|
|
66
|
+
];
|
|
67
|
+
return testPatterns.some(pattern => pattern.test(filePath));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if line has annotation allowing real-time
|
|
72
|
+
*/
|
|
73
|
+
hasAllowAnnotation(content, lineIndex) {
|
|
74
|
+
const allowAnnotations = this.config.options.allowAnnotations || [];
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
|
|
77
|
+
// Check current line and 2 lines above for annotations
|
|
78
|
+
for (let i = Math.max(0, lineIndex - 2); i <= lineIndex; i++) {
|
|
79
|
+
const line = lines[i] || '';
|
|
80
|
+
for (const annotation of allowAnnotations) {
|
|
81
|
+
if (line.includes(annotation)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if fake timers are used in the file
|
|
91
|
+
*/
|
|
92
|
+
hasFakeTimers(content) {
|
|
93
|
+
const fakeTimerPatterns = this.config.options.fakeTimerDetectors.jest_vitest || [];
|
|
94
|
+
return fakeTimerPatterns.some(pattern => {
|
|
95
|
+
const regex = new RegExp(pattern, 'g');
|
|
96
|
+
return regex.test(content);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Detect timer API violations
|
|
102
|
+
*/
|
|
103
|
+
detectTimerViolations(content, filePath) {
|
|
104
|
+
const violations = [];
|
|
105
|
+
const lines = content.split('\n');
|
|
106
|
+
const timerPatterns = this.config.options.timerApis.ts_js || this.getDefaultConfig().options.timerApis.ts_js;
|
|
107
|
+
const hasFakeTimersInFile = this.hasFakeTimers(content);
|
|
108
|
+
|
|
109
|
+
timerPatterns.forEach(pattern => {
|
|
110
|
+
// Convert config patterns (* wildcards) to proper regex
|
|
111
|
+
let regexPattern = pattern;
|
|
112
|
+
if (pattern.includes('(*)')) {
|
|
113
|
+
regexPattern = pattern.replace(/\(\*\)/g, '\\([^)]*\\)');
|
|
114
|
+
}
|
|
115
|
+
if (pattern.includes('*')) {
|
|
116
|
+
regexPattern = pattern.replace(/\*/g, '[^)]*');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const regex = new RegExp(regexPattern, 'g');
|
|
120
|
+
|
|
121
|
+
lines.forEach((line, index) => {
|
|
122
|
+
const trimmedLine = line.trim();
|
|
123
|
+
|
|
124
|
+
// Skip empty lines
|
|
125
|
+
if (!trimmedLine) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip if entire line is in block comment
|
|
130
|
+
if (CommentDetector.isLineInBlockComment(lines, index)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Skip if has allow annotation
|
|
135
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const matches = [...line.matchAll(regex)];
|
|
140
|
+
if (matches.length > 0) {
|
|
141
|
+
matches.forEach(match => {
|
|
142
|
+
const column = match.index + 1;
|
|
143
|
+
|
|
144
|
+
// Skip if match position is inside a comment
|
|
145
|
+
if (CommentDetector.isPositionInComment(line, match.index)) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let suggestion = "Use fake timers instead of real-time delays in tests.";
|
|
150
|
+
let severity = "error";
|
|
151
|
+
|
|
152
|
+
// Specific suggestions based on pattern
|
|
153
|
+
if (pattern.includes('setTimeout') || pattern.includes('setInterval')) {
|
|
154
|
+
if (!hasFakeTimersInFile) {
|
|
155
|
+
suggestion = "Use jest.useFakeTimers() and jest.advanceTimersByTime() instead of setTimeout/setInterval.";
|
|
156
|
+
} else {
|
|
157
|
+
suggestion = "You have fake timers setup. Use jest.advanceTimersByTime() to control time instead of real setTimeout.";
|
|
158
|
+
}
|
|
159
|
+
} else if (pattern.includes('sleep') || pattern.includes('delay')) {
|
|
160
|
+
suggestion = "Replace sleep/delay with fake timers or condition-based waiting.";
|
|
161
|
+
} else if (pattern.includes('Promise.*setTimeout')) {
|
|
162
|
+
suggestion = "Replace Promise+setTimeout with fake timers: await jest.advanceTimersByTimeAsync().";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
violations.push({
|
|
166
|
+
file: filePath,
|
|
167
|
+
line: index + 1,
|
|
168
|
+
column: column,
|
|
169
|
+
message: `Avoid real-time ${match[0]} in tests. ${suggestion}`,
|
|
170
|
+
severity: severity,
|
|
171
|
+
ruleId: this.ruleId,
|
|
172
|
+
evidence: line.trim(),
|
|
173
|
+
suggestion: suggestion
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return violations;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Detect busy polling violations (Date.now(), new Date() in loops)
|
|
185
|
+
*/
|
|
186
|
+
detectBusyPollingViolations(content, filePath) {
|
|
187
|
+
const violations = [];
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
const pollingPatterns = this.config.options.busyPollingDetectors.ts_js || [];
|
|
190
|
+
|
|
191
|
+
pollingPatterns.forEach(pattern => {
|
|
192
|
+
const regex = new RegExp(pattern, 'g');
|
|
193
|
+
|
|
194
|
+
lines.forEach((line, index) => {
|
|
195
|
+
const trimmedLine = line.trim();
|
|
196
|
+
|
|
197
|
+
// Skip empty lines
|
|
198
|
+
if (!trimmedLine) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Skip if entire line is in block comment
|
|
203
|
+
if (CommentDetector.isLineInBlockComment(lines, index)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Skip if has allow annotation
|
|
208
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Look for Date.now()/new Date() in potential polling contexts
|
|
213
|
+
const matches = line.match(regex);
|
|
214
|
+
if (matches && this.isLikelyPolling(lines, index)) {
|
|
215
|
+
matches.forEach(match => {
|
|
216
|
+
const column = line.indexOf(match) + 1;
|
|
217
|
+
|
|
218
|
+
// Skip if match position is inside a comment
|
|
219
|
+
if (CommentDetector.isPositionInComment(line, line.indexOf(match))) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
violations.push({
|
|
224
|
+
file: filePath,
|
|
225
|
+
line: index + 1,
|
|
226
|
+
column: column,
|
|
227
|
+
message: `Avoid busy polling with ${match} in tests. Use condition-based waiting instead.`,
|
|
228
|
+
severity: "warning",
|
|
229
|
+
ruleId: this.ruleId,
|
|
230
|
+
evidence: line.trim(),
|
|
231
|
+
suggestion: "Use waitFor conditions or fake timers instead of polling Date.now()."
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
return violations;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check if Date.now()/new Date() usage looks like polling
|
|
243
|
+
*/
|
|
244
|
+
isLikelyPolling(lines, currentIndex) {
|
|
245
|
+
const currentLine = lines[currentIndex];
|
|
246
|
+
|
|
247
|
+
// Skip if new Date() has static parameters (test data)
|
|
248
|
+
if (currentLine.includes('new Date(') && /new Date\(\s*\d/.test(currentLine)) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Look for polling patterns around this line
|
|
253
|
+
const contextRange = 5;
|
|
254
|
+
const start = Math.max(0, currentIndex - contextRange);
|
|
255
|
+
const end = Math.min(lines.length - 1, currentIndex + contextRange);
|
|
256
|
+
|
|
257
|
+
let hasLoop = false;
|
|
258
|
+
let hasTimeCheck = false;
|
|
259
|
+
|
|
260
|
+
for (let i = start; i <= end; i++) {
|
|
261
|
+
const line = lines[i].trim().toLowerCase();
|
|
262
|
+
|
|
263
|
+
// Check for loop patterns
|
|
264
|
+
if (line.includes('while') && (line.includes('date.now') || line.includes('new date'))) {
|
|
265
|
+
hasLoop = true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Check for time-based conditions
|
|
269
|
+
if ((line.includes('date.now') || line.includes('new date')) &&
|
|
270
|
+
(line.includes(' - ') || line.includes(' < ') || line.includes(' > ')) &&
|
|
271
|
+
(line.includes('start') || line.includes('time') || line.includes('duration'))) {
|
|
272
|
+
hasTimeCheck = true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for explicit polling patterns
|
|
276
|
+
if (line.includes('setinterval') && (line.includes('date.now') || line.includes('new date'))) {
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Only flag as polling if both loop and time check are present
|
|
282
|
+
return hasLoop && hasTimeCheck;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Detect E2E specific violations
|
|
287
|
+
*/
|
|
288
|
+
detectE2EViolations(content, filePath) {
|
|
289
|
+
const violations = [];
|
|
290
|
+
const lines = content.split('\n');
|
|
291
|
+
const e2ePatterns = this.config.options.timerApis.e2e || [];
|
|
292
|
+
|
|
293
|
+
e2ePatterns.forEach(pattern => {
|
|
294
|
+
const regex = new RegExp(pattern, 'g');
|
|
295
|
+
|
|
296
|
+
lines.forEach((line, index) => {
|
|
297
|
+
const trimmedLine = line.trim();
|
|
298
|
+
|
|
299
|
+
if (!trimmedLine) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Skip if entire line is in block comment
|
|
304
|
+
if (CommentDetector.isLineInBlockComment(lines, index)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (this.hasAllowAnnotation(content, index)) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const matches = line.match(regex);
|
|
313
|
+
if (matches) {
|
|
314
|
+
matches.forEach(match => {
|
|
315
|
+
const column = line.indexOf(match) + 1;
|
|
316
|
+
|
|
317
|
+
// Skip if match position is inside a comment
|
|
318
|
+
if (CommentDetector.isPositionInComment(line, line.indexOf(match))) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let suggestion = "Use element-based waiting instead of fixed timeouts.";
|
|
323
|
+
if (match.includes('page.waitForTimeout')) {
|
|
324
|
+
suggestion = "Use page.waitForSelector() or page.waitForFunction() instead of waitForTimeout().";
|
|
325
|
+
} else if (match.includes('cy.wait')) {
|
|
326
|
+
suggestion = "Use cy.get().should() or cy.intercept() instead of cy.wait() with fixed time.";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
violations.push({
|
|
330
|
+
file: filePath,
|
|
331
|
+
line: index + 1,
|
|
332
|
+
column: column,
|
|
333
|
+
message: `Avoid fixed timeout ${match} in E2E tests. ${suggestion}`,
|
|
334
|
+
severity: "warning",
|
|
335
|
+
ruleId: this.ruleId,
|
|
336
|
+
evidence: line.trim(),
|
|
337
|
+
suggestion: suggestion
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return violations;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Main analysis function - Engine interface
|
|
349
|
+
* Expected signature: analyze(files, language, options)
|
|
350
|
+
*/
|
|
351
|
+
async analyze(files, language, options = {}) {
|
|
352
|
+
const allViolations = [];
|
|
353
|
+
|
|
354
|
+
for (const filePath of files) {
|
|
355
|
+
try {
|
|
356
|
+
// Only analyze test files
|
|
357
|
+
if (!this.isTestFile(filePath)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Read file content
|
|
362
|
+
const content = require('fs').readFileSync(filePath, 'utf8');
|
|
363
|
+
|
|
364
|
+
// Skip if file has explicit allow annotation
|
|
365
|
+
if (this.hasAllowAnnotation(content)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let violations = [];
|
|
370
|
+
|
|
371
|
+
// Detect timer API violations
|
|
372
|
+
violations = violations.concat(this.detectTimerViolations(content, filePath));
|
|
373
|
+
|
|
374
|
+
// Detect busy polling violations
|
|
375
|
+
violations = violations.concat(this.detectBusyPollingViolations(content, filePath));
|
|
376
|
+
|
|
377
|
+
// Detect E2E violations (if file looks like E2E test)
|
|
378
|
+
if (filePath.includes('e2e') || content.includes('playwright') || content.includes('cypress')) {
|
|
379
|
+
violations = violations.concat(this.detectE2EViolations(content, filePath));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
allViolations.push(...violations);
|
|
383
|
+
|
|
384
|
+
} catch (error) {
|
|
385
|
+
console.warn(`C070 analysis error for ${filePath}:`, error.message);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return allViolations;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Legacy analysis function for single file
|
|
394
|
+
* @deprecated Use analyze(files, language, options) instead
|
|
395
|
+
*/
|
|
396
|
+
analyzeSingleFile(content, filePath) {
|
|
397
|
+
// Only analyze test files
|
|
398
|
+
if (!this.isTestFile(filePath)) {
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
let violations = [];
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
// Detect timer API violations
|
|
406
|
+
violations = violations.concat(this.detectTimerViolations(content, filePath));
|
|
407
|
+
|
|
408
|
+
// Detect busy polling violations
|
|
409
|
+
violations = violations.concat(this.detectBusyPollingViolations(content, filePath));
|
|
410
|
+
|
|
411
|
+
// Detect E2E violations (if file looks like E2E test)
|
|
412
|
+
if (filePath.includes('e2e') || content.includes('playwright') || content.includes('cypress')) {
|
|
413
|
+
violations = violations.concat(this.detectE2EViolations(content, filePath));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.warn(`C070 analysis error for ${filePath}:`, error.message);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return violations;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
module.exports = C070TestRealTimeAnalyzer;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S024 Main Analyzer - Protect against XPath Injection and XML External Entity (XXE)
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S024 --input=examples/rule-test-fixtures/rules/S024_xpath_xxe_protection --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S024SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S024RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S024Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S024] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S024] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S024";
|
|
23
|
+
this.ruleName = "Protect against XPath Injection and XML External Entity (XXE)";
|
|
24
|
+
this.description =
|
|
25
|
+
"Protect against XPath Injection and XML External Entity (XXE) attacks. XPath injection occurs when user input is used to construct XPath queries without proper sanitization. XXE attacks exploit XML parsers that process external entities, potentially leading to data disclosure, server-side request forgery, or denial of service.";
|
|
26
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
27
|
+
this.verbose = options.verbose || false;
|
|
28
|
+
|
|
29
|
+
// Configuration
|
|
30
|
+
this.config = {
|
|
31
|
+
useSymbolBased: true, // Primary approach
|
|
32
|
+
fallbackToRegex: true, // Secondary approach
|
|
33
|
+
regexBasedOnly: false, // Can be set to true for pure mode
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Initialize analyzers
|
|
37
|
+
try {
|
|
38
|
+
this.symbolAnalyzer = new S024SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
40
|
+
console.log(`🔧 [S024] Symbol analyzer created successfully`);
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`🔧 [S024] Error creating symbol analyzer:`, error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.regexAnalyzer = new S024RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
+
console.log(`🔧 [S024] Regex analyzer created successfully`);
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error(`🔧 [S024] Error creating regex analyzer:`, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Initialize analyzer with semantic engine
|
|
58
|
+
*/
|
|
59
|
+
async initialize(semanticEngine) {
|
|
60
|
+
this.semanticEngine = semanticEngine;
|
|
61
|
+
|
|
62
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
63
|
+
console.log(`🔧 [S024] Main analyzer initializing...`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Initialize both analyzers
|
|
67
|
+
if (this.symbolAnalyzer) {
|
|
68
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
69
|
+
}
|
|
70
|
+
if (this.regexAnalyzer) {
|
|
71
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Clean up if needed
|
|
75
|
+
if (this.regexAnalyzer) {
|
|
76
|
+
this.regexAnalyzer.cleanup?.();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
80
|
+
console.log(`🔧 [S024] Main analyzer initialized successfully`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Single file analysis method for testing
|
|
86
|
+
*/
|
|
87
|
+
analyzeSingle(filePath, options = {}) {
|
|
88
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
89
|
+
console.log(`📊 [S024] analyzeSingle() called for: ${filePath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Return result using same format as analyze method
|
|
93
|
+
return this.analyze([filePath], "typescript", options);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async analyze(files, language, options = {}) {
|
|
97
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
+
console.log(
|
|
99
|
+
`🔧 [S024] analyze() method called with ${files.length} files, language: ${language}`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
for (const filePath of files) {
|
|
106
|
+
try {
|
|
107
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
108
|
+
console.log(`🔧 [S024] Processing file: ${filePath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
112
|
+
violations.push(...fileViolations);
|
|
113
|
+
|
|
114
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
115
|
+
console.log(
|
|
116
|
+
`🔧 [S024] File ${filePath}: Found ${fileViolations.length} violations`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.warn(
|
|
121
|
+
`⚠ [S024] Analysis failed for ${filePath}:`,
|
|
122
|
+
error.message
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
128
|
+
console.log(`🔧 [S024] Total violations found: ${violations.length}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return violations;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async analyzeFile(filePath, options = {}) {
|
|
135
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
136
|
+
console.log(`🔍 [S024] analyzeFile() called for: ${filePath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create a Map to track unique violations and prevent duplicates
|
|
140
|
+
const violationMap = new Map();
|
|
141
|
+
|
|
142
|
+
// 1. Try Symbol-based analysis first (primary)
|
|
143
|
+
if (
|
|
144
|
+
this.config.useSymbolBased &&
|
|
145
|
+
this.semanticEngine?.project &&
|
|
146
|
+
this.semanticEngine?.initialized
|
|
147
|
+
) {
|
|
148
|
+
try {
|
|
149
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
150
|
+
console.log(`🔧 [S024] Trying symbol-based analysis...`);
|
|
151
|
+
}
|
|
152
|
+
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
153
|
+
if (sourceFile) {
|
|
154
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
155
|
+
console.log(`🔧 [S024] Source file found, analyzing...`);
|
|
156
|
+
}
|
|
157
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
158
|
+
sourceFile,
|
|
159
|
+
filePath
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Add to violation map with deduplication
|
|
163
|
+
symbolViolations.forEach((violation) => {
|
|
164
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
165
|
+
if (!violationMap.has(key)) {
|
|
166
|
+
violationMap.set(key, violation);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
171
|
+
console.log(
|
|
172
|
+
`🔧 [S024] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
177
|
+
console.log(`🔧 [S024] Source file not found, falling back...`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠ [S024] Symbol analysis failed:`, error.message);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 2. Try Regex-based analysis (fallback or additional)
|
|
186
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
187
|
+
try {
|
|
188
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
189
|
+
console.log(`🔧 [S024] Trying regex-based analysis...`);
|
|
190
|
+
}
|
|
191
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
192
|
+
|
|
193
|
+
// Add to violation map with deduplication
|
|
194
|
+
regexViolations.forEach((violation) => {
|
|
195
|
+
const key = `${violation.line}:${violation.column}:${violation.message}`;
|
|
196
|
+
if (!violationMap.has(key)) {
|
|
197
|
+
violationMap.set(key, violation);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
202
|
+
console.log(
|
|
203
|
+
`🔧 [S024] Regex analysis completed: ${regexViolations.length} violations`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.warn(`⚠ [S024] Regex analysis failed:`, error.message);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convert Map values to array and add filePath to each violation
|
|
212
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
213
|
+
(violation) => ({
|
|
214
|
+
...violation,
|
|
215
|
+
filePath: filePath,
|
|
216
|
+
file: filePath, // Also add 'file' for compatibility
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
221
|
+
console.log(
|
|
222
|
+
`🔧 [S024] File analysis completed: ${finalViolations.length} unique violations`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return finalViolations;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Clean up resources
|
|
231
|
+
*/
|
|
232
|
+
cleanup() {
|
|
233
|
+
if (this.symbolAnalyzer?.cleanup) {
|
|
234
|
+
this.symbolAnalyzer.cleanup();
|
|
235
|
+
}
|
|
236
|
+
if (this.regexAnalyzer?.cleanup) {
|
|
237
|
+
this.regexAnalyzer.cleanup();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = S024Analyzer;
|