@sun-asterisk/sunlint 1.3.18 → 1.3.19

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 (34) hide show
  1. package/config/rules/enhanced-rules-registry.json +77 -18
  2. package/core/cli-program.js +2 -1
  3. package/core/github-annotate-service.js +89 -0
  4. package/core/output-service.js +25 -0
  5. package/core/summary-report-service.js +30 -30
  6. package/package.json +3 -2
  7. package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
  8. package/rules/common/C017_constructor_logic/analyzer.js +137 -503
  9. package/rules/common/C017_constructor_logic/config.json +50 -0
  10. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
  11. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
  12. package/rules/security/S011_secure_guid_generation/README.md +255 -0
  13. package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
  14. package/rules/security/S011_secure_guid_generation/config.json +56 -0
  15. package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
  16. package/rules/security/S028_file_upload_size_limits/README.md +537 -0
  17. package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
  18. package/rules/security/S028_file_upload_size_limits/config.json +186 -0
  19. package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
  20. package/rules/security/S041_session_token_invalidation/README.md +303 -0
  21. package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
  22. package/rules/security/S041_session_token_invalidation/config.json +175 -0
  23. package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
  24. package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
  25. package/rules/security/S044_re_authentication_required/README.md +136 -0
  26. package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
  27. package/rules/security/S044_re_authentication_required/config.json +161 -0
  28. package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
  29. package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
  30. package/rules/security/S045_brute_force_protection/README.md +345 -0
  31. package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
  32. package/rules/security/S045_brute_force_protection/config.json +139 -0
  33. package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
  34. package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
@@ -0,0 +1,530 @@
1
+ /**
2
+ * S028 - Limit upload file size and number of files (Symbol-based Analyzer)
3
+ *
4
+ * Detects file upload configurations without proper size and quantity limits.
5
+ * Prevents DoS attacks and resource exhaustion.
6
+ *
7
+ * Based on:
8
+ * - OWASP A04:2021 - Insecure Design
9
+ * - CWE-400: Uncontrolled Resource Consumption
10
+ */
11
+
12
+ const { Project, SyntaxKind } = require("ts-morph");
13
+
14
+ class S028SymbolBasedAnalyzer {
15
+ constructor(semanticEngine = null) {
16
+ this.ruleId = "S028";
17
+ this.semanticEngine = semanticEngine;
18
+ this.verbose = process.env.SUNLINT_DEBUG || false;
19
+
20
+ // Recommended limits
21
+ this.maxFileSize = 10 * 1024 * 1024; // 10MB
22
+ this.maxFiles = 10;
23
+ this.highRiskThreshold = 50 * 1024 * 1024; // 50MB
24
+ this.mediumRiskThreshold = 20 * 1024 * 1024; // 20MB
25
+
26
+ // Framework patterns
27
+ this.multerPatterns = ["multer(", "multer({"];
28
+ this.fileInterceptorPatterns = [
29
+ "FileInterceptor",
30
+ "FilesInterceptor",
31
+ "FileFieldsInterceptor",
32
+ ];
33
+ this.expressPatterns = ["express.json", "express.urlencoded"];
34
+ }
35
+
36
+ async initialize(semanticEngine = null) {
37
+ if (semanticEngine) {
38
+ this.semanticEngine = semanticEngine;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Main analyze method
44
+ */
45
+ async analyze(sourceFile, filePath) {
46
+ const violations = [];
47
+ const reportedLines = new Set(); // Prevent duplicates
48
+
49
+ try {
50
+ // Create ts-morph project if not already a SourceFile
51
+ let tsSourceFile = sourceFile;
52
+ if (typeof sourceFile === "string") {
53
+ const project = new Project({ useInMemoryFileSystem: true });
54
+ tsSourceFile = project.createSourceFile(filePath, sourceFile);
55
+ }
56
+
57
+ // Check different upload patterns
58
+ this.checkMulterConfiguration(
59
+ tsSourceFile,
60
+ filePath,
61
+ violations,
62
+ reportedLines
63
+ );
64
+ this.checkFileInterceptor(
65
+ tsSourceFile,
66
+ filePath,
67
+ violations,
68
+ reportedLines
69
+ );
70
+ this.checkExpressMiddleware(
71
+ tsSourceFile,
72
+ filePath,
73
+ violations,
74
+ reportedLines
75
+ );
76
+
77
+ if (this.verbose) {
78
+ console.log(
79
+ `🔍 [${this.ruleId}] File: ${filePath} - Found ${violations.length} violations`
80
+ );
81
+ }
82
+ } catch (error) {
83
+ if (this.verbose) {
84
+ console.error(
85
+ `❌ [${this.ruleId}] Error analyzing ${filePath}:`,
86
+ error.message
87
+ );
88
+ }
89
+ }
90
+
91
+ return violations;
92
+ }
93
+
94
+ /**
95
+ * Check multer() configuration
96
+ * Pattern: multer({ dest, storage, limits, fileFilter })
97
+ */
98
+ checkMulterConfiguration(sourceFile, filePath, violations, reportedLines) {
99
+ try {
100
+ const callExpressions = sourceFile.getDescendantsOfKind(
101
+ SyntaxKind.CallExpression
102
+ );
103
+
104
+ callExpressions.forEach((callExpr) => {
105
+ const exprText = callExpr.getExpression().getText();
106
+
107
+ // Check if it's multer call
108
+ if (!exprText.includes("multer")) {
109
+ return;
110
+ }
111
+
112
+ const args = callExpr.getArguments();
113
+ if (args.length === 0) {
114
+ // multer() without arguments
115
+ const line = callExpr.getStartLineNumber();
116
+ if (reportedLines.has(line)) return;
117
+
118
+ violations.push({
119
+ ruleId: this.ruleId,
120
+ source: filePath,
121
+ filePath: filePath,
122
+ file: filePath,
123
+ line: line,
124
+ column: callExpr.getStartLinePos(),
125
+ message:
126
+ "Multer configuration missing size limits - add limits.fileSize and limits.files to prevent DoS attacks",
127
+ severity: "error",
128
+ category: "security",
129
+ });
130
+ reportedLines.add(line);
131
+ return;
132
+ }
133
+
134
+ // Check if config object has limits
135
+ const configArg = args[0];
136
+ if (!configArg) return;
137
+
138
+ const configText = configArg.getText();
139
+
140
+ // Check if limits object exists
141
+ if (!configText.includes("limits")) {
142
+ const line = callExpr.getStartLineNumber();
143
+ if (reportedLines.has(line)) return;
144
+
145
+ violations.push({
146
+ ruleId: this.ruleId,
147
+ source: filePath,
148
+ filePath: filePath,
149
+ file: filePath,
150
+ line: line,
151
+ column: callExpr.getStartLinePos(),
152
+ message:
153
+ "Multer configuration missing 'limits' object - add { limits: { fileSize: 10485760, files: 10 } }",
154
+ severity: "error",
155
+ category: "security",
156
+ });
157
+ reportedLines.add(line);
158
+ return;
159
+ }
160
+
161
+ // Check if limits.fileSize exists
162
+ if (!configText.includes("fileSize")) {
163
+ const line = callExpr.getStartLineNumber();
164
+ if (reportedLines.has(line)) return;
165
+
166
+ violations.push({
167
+ ruleId: this.ruleId,
168
+ source: filePath,
169
+ filePath: filePath,
170
+ file: filePath,
171
+ line: line,
172
+ column: callExpr.getStartLinePos(),
173
+ message:
174
+ "Multer limits missing 'fileSize' - add limits.fileSize to prevent large file uploads (recommend ≤ 10MB)",
175
+ severity: "error",
176
+ category: "security",
177
+ });
178
+ reportedLines.add(line);
179
+ return;
180
+ }
181
+
182
+ if (process.env.SUNLINT_DEBUG) {
183
+ console.log(
184
+ `🔍 [S028] Line ${callExpr.getStartLineNumber()}: Has fileSize, checking threshold...`
185
+ );
186
+ }
187
+
188
+ // Validate fileSize threshold - try to evaluate expression
189
+ let fileSize = null;
190
+
191
+ // Try to evaluate expression first (handles both "100" and "100 * 1024 * 1024")
192
+ const expressionMatch = configText.match(/fileSize\s*:\s*([^,}]+)/);
193
+ if (expressionMatch) {
194
+ const expression = expressionMatch[1]
195
+ .trim()
196
+ .replace(/\/\/.*/g, "")
197
+ .trim();
198
+ if (process.env.SUNLINT_DEBUG) {
199
+ console.log(
200
+ `🔍 [S028] Line ${callExpr.getStartLineNumber()}: Evaluating expression: "${expression}"`
201
+ );
202
+ }
203
+ try {
204
+ // Safe evaluation of numeric expressions only
205
+ fileSize = eval(expression);
206
+ if (process.env.SUNLINT_DEBUG) {
207
+ console.log(
208
+ `🔍 [S028] Evaluated fileSize: ${fileSize} bytes = ${(
209
+ fileSize /
210
+ (1024 * 1024)
211
+ ).toFixed(0)}MB`
212
+ );
213
+ }
214
+ } catch (e) {
215
+ // If eval fails, skip threshold check
216
+ if (process.env.SUNLINT_DEBUG) {
217
+ console.log(`🔍 [S028] Eval failed: ${e.message}`);
218
+ }
219
+ fileSize = null;
220
+ }
221
+ }
222
+
223
+ if (fileSize && !isNaN(fileSize)) {
224
+ if (fileSize > this.highRiskThreshold) {
225
+ const line = callExpr.getStartLineNumber();
226
+ if (reportedLines.has(line)) return;
227
+
228
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
229
+ violations.push({
230
+ ruleId: this.ruleId,
231
+ source: filePath,
232
+ filePath: filePath,
233
+ file: filePath,
234
+ line: line,
235
+ column: callExpr.getStartLinePos(),
236
+ message: `File size limit too high (${fileSizeMB}MB) - recommend ≤ 10MB to prevent DoS attacks`,
237
+ severity: "error",
238
+ category: "security",
239
+ });
240
+ reportedLines.add(line);
241
+ } else if (fileSize > this.mediumRiskThreshold) {
242
+ const line = callExpr.getStartLineNumber();
243
+ if (reportedLines.has(line)) return;
244
+
245
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
246
+ violations.push({
247
+ ruleId: this.ruleId,
248
+ source: filePath,
249
+ filePath: filePath,
250
+ file: filePath,
251
+ line: line,
252
+ column: callExpr.getStartLinePos(),
253
+ message: `File size limit (${fileSizeMB}MB) exceeds recommended threshold - consider reducing to ≤ 10MB`,
254
+ severity: "warning",
255
+ category: "security",
256
+ });
257
+ reportedLines.add(line);
258
+ }
259
+ }
260
+
261
+ // Check if limits.files exists (optional but recommended)
262
+ if (!configText.includes("files")) {
263
+ const line = callExpr.getStartLineNumber();
264
+ // Don't report if already reported fileSize issue
265
+ if (reportedLines.has(line)) return;
266
+
267
+ // This is a warning, not error (files limit is optional)
268
+ violations.push({
269
+ ruleId: this.ruleId,
270
+ source: filePath,
271
+ filePath: filePath,
272
+ file: filePath,
273
+ line: line,
274
+ column: callExpr.getStartLinePos(),
275
+ message:
276
+ "Multer limits missing 'files' count - consider adding limits.files to prevent excessive uploads (recommend ≤ 10)",
277
+ severity: "warning",
278
+ category: "security",
279
+ });
280
+ reportedLines.add(line);
281
+ }
282
+ });
283
+ } catch (error) {
284
+ if (this.verbose) {
285
+ console.error(
286
+ `❌ [${this.ruleId}] Error checking multer:`,
287
+ error.message
288
+ );
289
+ }
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Check @UseInterceptors(FileInterceptor(...))
295
+ * Pattern: @UseInterceptors(FileInterceptor('file', { limits: {...} }))
296
+ */
297
+ checkFileInterceptor(sourceFile, filePath, violations, reportedLines) {
298
+ try {
299
+ const decorators = sourceFile.getDescendantsOfKind(SyntaxKind.Decorator);
300
+
301
+ decorators.forEach((decorator) => {
302
+ const decoratorText = decorator.getText();
303
+
304
+ // Check if it's FileInterceptor decorator
305
+ const isFileInterceptor = this.fileInterceptorPatterns.some((pattern) =>
306
+ decoratorText.includes(pattern)
307
+ );
308
+
309
+ if (!isFileInterceptor) {
310
+ return;
311
+ }
312
+
313
+ // Check if it has limits configuration
314
+ if (!decoratorText.includes("limits")) {
315
+ const line = decorator.getStartLineNumber();
316
+ if (reportedLines.has(line)) return;
317
+
318
+ const interceptorType = decoratorText.includes("FilesInterceptor")
319
+ ? "FilesInterceptor"
320
+ : decoratorText.includes("FileFieldsInterceptor")
321
+ ? "FileFieldsInterceptor"
322
+ : "FileInterceptor";
323
+
324
+ violations.push({
325
+ ruleId: this.ruleId,
326
+ source: filePath,
327
+ filePath: filePath,
328
+ file: filePath,
329
+ line: line,
330
+ column: decorator.getStartLinePos(),
331
+ message: `${interceptorType} missing 'limits' configuration - add { limits: { fileSize: 10485760 } } to prevent large uploads`,
332
+ severity: "error",
333
+ category: "security",
334
+ });
335
+ reportedLines.add(line);
336
+ return;
337
+ }
338
+
339
+ // Check if limits.fileSize exists
340
+ if (!decoratorText.includes("fileSize")) {
341
+ const line = decorator.getStartLineNumber();
342
+ if (reportedLines.has(line)) return;
343
+
344
+ violations.push({
345
+ ruleId: this.ruleId,
346
+ source: filePath,
347
+ filePath: filePath,
348
+ file: filePath,
349
+ line: line,
350
+ column: decorator.getStartLinePos(),
351
+ message:
352
+ "FileInterceptor limits missing 'fileSize' - add limits.fileSize to prevent large file uploads (recommend ≤ 10MB)",
353
+ severity: "error",
354
+ category: "security",
355
+ });
356
+ reportedLines.add(line);
357
+ return;
358
+ }
359
+
360
+ // Validate fileSize threshold - try to evaluate expression
361
+ let fileSize = null;
362
+
363
+ // Try to evaluate expression first (handles both "100" and "100 * 1024 * 1024")
364
+ const expressionMatch = decoratorText.match(/fileSize\s*:\s*([^,}]+)/);
365
+ if (expressionMatch) {
366
+ const expression = expressionMatch[1]
367
+ .trim()
368
+ .replace(/\/\/.*/g, "")
369
+ .trim();
370
+ try {
371
+ // Safe evaluation of numeric expressions only
372
+ fileSize = eval(expression);
373
+ } catch (e) {
374
+ // If eval fails, skip threshold check
375
+ fileSize = null;
376
+ }
377
+ }
378
+
379
+ if (fileSize && !isNaN(fileSize)) {
380
+ if (fileSize > this.highRiskThreshold) {
381
+ const line = decorator.getStartLineNumber();
382
+ if (reportedLines.has(line)) return;
383
+
384
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
385
+ violations.push({
386
+ ruleId: this.ruleId,
387
+ source: filePath,
388
+ filePath: filePath,
389
+ file: filePath,
390
+ line: line,
391
+ column: decorator.getStartLinePos(),
392
+ message: `File size limit too high (${fileSizeMB}MB) - recommend ≤ 10MB to prevent DoS attacks`,
393
+ severity: "error",
394
+ category: "security",
395
+ });
396
+ reportedLines.add(line);
397
+ } else if (fileSize > this.mediumRiskThreshold) {
398
+ const line = decorator.getStartLineNumber();
399
+ if (reportedLines.has(line)) return;
400
+
401
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(0);
402
+ violations.push({
403
+ ruleId: this.ruleId,
404
+ source: filePath,
405
+ filePath: filePath,
406
+ file: filePath,
407
+ line: line,
408
+ column: decorator.getStartLinePos(),
409
+ message: `File size limit (${fileSizeMB}MB) exceeds recommended threshold - consider reducing to ≤ 10MB`,
410
+ severity: "warning",
411
+ category: "security",
412
+ });
413
+ reportedLines.add(line);
414
+ }
415
+ }
416
+ });
417
+ } catch (error) {
418
+ if (this.verbose) {
419
+ console.error(
420
+ `❌ [${this.ruleId}] Error checking FileInterceptor:`,
421
+ error.message
422
+ );
423
+ }
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Check Express middleware: express.json() and express.urlencoded()
429
+ * Pattern: app.use(express.json({ limit: '10mb' }))
430
+ */
431
+ checkExpressMiddleware(sourceFile, filePath, violations, reportedLines) {
432
+ try {
433
+ const callExpressions = sourceFile.getDescendantsOfKind(
434
+ SyntaxKind.CallExpression
435
+ );
436
+
437
+ callExpressions.forEach((callExpr) => {
438
+ const exprText = callExpr.getExpression().getText();
439
+
440
+ // Check if it's express.json or express.urlencoded
441
+ const isExpressJson = exprText.includes("express.json");
442
+ const isExpressUrlencoded = exprText.includes("express.urlencoded");
443
+
444
+ if (!isExpressJson && !isExpressUrlencoded) {
445
+ return;
446
+ }
447
+
448
+ const args = callExpr.getArguments();
449
+
450
+ // Check if it has limit configuration
451
+ const hasLimitConfig =
452
+ args.length > 0 && args[0].getText().includes("limit");
453
+
454
+ if (!hasLimitConfig) {
455
+ const line = callExpr.getStartLineNumber();
456
+ if (reportedLines.has(line)) return;
457
+
458
+ const methodName = isExpressJson
459
+ ? "express.json()"
460
+ : "express.urlencoded()";
461
+ violations.push({
462
+ ruleId: this.ruleId,
463
+ source: filePath,
464
+ filePath: filePath,
465
+ file: filePath,
466
+ line: line,
467
+ column: callExpr.getStartLinePos(),
468
+ message: `${methodName} missing body size limit - add { limit: '10mb' } to prevent large payload attacks`,
469
+ severity: "warning",
470
+ category: "security",
471
+ });
472
+ reportedLines.add(line);
473
+ return;
474
+ }
475
+
476
+ // Validate limit threshold
477
+ const configText = args[0].getText();
478
+ const limitMatch = configText.match(
479
+ /limit\s*:\s*['"](\d+)(mb|kb|gb)?['"]/i
480
+ );
481
+
482
+ if (limitMatch) {
483
+ const limitValue = parseInt(limitMatch[1], 10);
484
+ const limitUnit = (limitMatch[2] || "").toLowerCase();
485
+
486
+ let limitBytes = limitValue;
487
+ if (limitUnit === "kb") {
488
+ limitBytes = limitValue * 1024;
489
+ } else if (limitUnit === "mb") {
490
+ limitBytes = limitValue * 1024 * 1024;
491
+ } else if (limitUnit === "gb") {
492
+ limitBytes = limitValue * 1024 * 1024 * 1024;
493
+ }
494
+
495
+ if (limitBytes > this.mediumRiskThreshold) {
496
+ const line = callExpr.getStartLineNumber();
497
+ if (reportedLines.has(line)) return;
498
+
499
+ const limitMB = (limitBytes / (1024 * 1024)).toFixed(0);
500
+ violations.push({
501
+ ruleId: this.ruleId,
502
+ source: filePath,
503
+ filePath: filePath,
504
+ file: filePath,
505
+ line: line,
506
+ column: callExpr.getStartLinePos(),
507
+ message: `Body size limit (${limitMB}MB) exceeds recommended threshold - consider reducing to ≤ 10MB`,
508
+ severity: "warning",
509
+ category: "security",
510
+ });
511
+ reportedLines.add(line);
512
+ }
513
+ }
514
+ });
515
+ } catch (error) {
516
+ if (this.verbose) {
517
+ console.error(
518
+ `❌ [${this.ruleId}] Error checking Express middleware:`,
519
+ error.message
520
+ );
521
+ }
522
+ }
523
+ }
524
+
525
+ async cleanup() {
526
+ // Cleanup if needed
527
+ }
528
+ }
529
+
530
+ module.exports = S028SymbolBasedAnalyzer;