@sun-asterisk/sunlint 1.3.2 → 1.3.4

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 (60) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +5 -3
  3. package/config/rules/enhanced-rules-registry.json +144 -33
  4. package/core/analysis-orchestrator.js +173 -42
  5. package/core/auto-performance-manager.js +243 -0
  6. package/core/cli-action-handler.js +24 -2
  7. package/core/cli-program.js +19 -5
  8. package/core/constants/defaults.js +56 -0
  9. package/core/performance-optimizer.js +271 -0
  10. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
  11. package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
  12. package/docs/PERFORMANCE.md +311 -0
  13. package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
  14. package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
  15. package/docs/QUICK_FILE_LIMITS.md +64 -0
  16. package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
  17. package/engines/engine-factory.js +7 -0
  18. package/engines/heuristic-engine.js +182 -5
  19. package/package.json +2 -1
  20. package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
  21. package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
  22. package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
  23. package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
  24. package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
  25. package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
  26. package/rules/index.js +2 -0
  27. package/rules/security/S017_use_parameterized_queries/README.md +128 -0
  28. package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
  29. package/rules/security/S017_use_parameterized_queries/config.json +109 -0
  30. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
  31. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
  32. package/rules/security/S031_secure_session_cookies/README.md +127 -0
  33. package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
  34. package/rules/security/S031_secure_session_cookies/config.json +86 -0
  35. package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
  36. package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
  37. package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
  38. package/rules/security/S032_httponly_session_cookies/README.md +184 -0
  39. package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
  40. package/rules/security/S032_httponly_session_cookies/config.json +96 -0
  41. package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
  42. package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
  43. package/rules/security/S033_samesite_session_cookies/README.md +227 -0
  44. package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
  45. package/rules/security/S033_samesite_session_cookies/config.json +87 -0
  46. package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
  47. package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
  48. package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
  49. package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
  50. package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
  51. package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
  52. package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
  53. package/rules/security/S035_path_session_cookies/README.md +257 -0
  54. package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
  55. package/rules/security/S035_path_session_cookies/config.json +99 -0
  56. package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
  57. package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
  58. package/scripts/batch-processing-demo.js +334 -0
  59. package/scripts/performance-test.js +541 -0
  60. package/scripts/quick-performance-test.js +108 -0
@@ -0,0 +1,541 @@
1
+ const { Project, SyntaxKind } = require("ts-morph");
2
+
3
+ /**
4
+ * S017 Regex-Based Analyzer - Always use parameterized queries
5
+ * Uses regex patterns and TypeScript AST to detect SQL injection vulnerabilities
6
+ */
7
+ class S017RegexBasedAnalyzer {
8
+ constructor(semanticEngine = null) {
9
+ this.ruleId = "S017";
10
+ this.ruleName = "Always use parameterized queries";
11
+ this.semanticEngine = semanticEngine;
12
+ this.verbose = false;
13
+ this.debug = process.env.SUNLINT_DEBUG === "1";
14
+
15
+ // SQL execution methods
16
+ this.sqlMethods = [
17
+ "query",
18
+ "execute",
19
+ "exec",
20
+ "run",
21
+ "all",
22
+ "get",
23
+ "prepare",
24
+ "createQuery",
25
+ "executeQuery",
26
+ "executeSql",
27
+ "rawQuery",
28
+ ];
29
+
30
+ // SQL keywords that indicate SQL operations
31
+ this.sqlKeywords = [
32
+ "SELECT",
33
+ "INSERT",
34
+ "UPDATE",
35
+ "DELETE",
36
+ "DROP",
37
+ "CREATE",
38
+ "ALTER",
39
+ "UNION",
40
+ "WHERE",
41
+ "ORDER BY",
42
+ "GROUP BY",
43
+ "HAVING",
44
+ "FROM",
45
+ "JOIN",
46
+ "INNER JOIN",
47
+ "LEFT JOIN",
48
+ "RIGHT JOIN",
49
+ "FULL JOIN",
50
+ ];
51
+
52
+ // Database libraries to look for
53
+ this.databaseLibraries = [
54
+ "mysql",
55
+ "mysql2",
56
+ "pg",
57
+ "postgres",
58
+ "sqlite3",
59
+ "sqlite",
60
+ "mssql",
61
+ "tedious",
62
+ "oracle",
63
+ "mongodb",
64
+ "mongoose",
65
+ "sequelize",
66
+ "typeorm",
67
+ "prisma",
68
+ "knex",
69
+ "objection",
70
+ ];
71
+
72
+ // Safe patterns that indicate parameterized queries
73
+ this.safePatterns = [
74
+ "\\?",
75
+ "\\$1",
76
+ "\\$2",
77
+ "\\$3",
78
+ "\\$4",
79
+ "\\$5",
80
+ "prepare",
81
+ "bind",
82
+ "params",
83
+ "parameters",
84
+ "values",
85
+ ];
86
+
87
+ if (this.debug) {
88
+ console.log(
89
+ `🔧 [S017-Regex] Constructor - databaseLibraries:`,
90
+ this.databaseLibraries.length
91
+ );
92
+ console.log(
93
+ `🔧 [S017-Regex] Constructor - sqlMethods:`,
94
+ this.sqlMethods.length
95
+ );
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Initialize with semantic engine
101
+ */
102
+ async initialize(semanticEngine = null) {
103
+ if (semanticEngine) {
104
+ this.semanticEngine = semanticEngine;
105
+ this.verbose = semanticEngine.verbose || false;
106
+ }
107
+
108
+ if (this.verbose) {
109
+ console.log(
110
+ `🔧 [S017 Regex-Based] Analyzer initialized, verbose: ${this.verbose}`
111
+ );
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Analyze file using AST
117
+ */
118
+ async analyzeFile(filePath, fileContent) {
119
+ if (this.debug) {
120
+ console.log(`🔍 [S017-AST] Analyzing: ${filePath}`);
121
+ }
122
+
123
+ const violations = [];
124
+
125
+ try {
126
+ const project = new Project({
127
+ useInMemoryFileSystem: true,
128
+ compilerOptions: {
129
+ allowJs: true,
130
+ target: "ES2020",
131
+ },
132
+ });
133
+
134
+ const sourceFile = project.createSourceFile(filePath, fileContent);
135
+
136
+ // Find SQL-related method calls
137
+ const sqlMethodCalls = this.findSqlMethodCalls(sourceFile);
138
+
139
+ for (const methodCall of sqlMethodCalls) {
140
+ const sqlViolations = this.analyzeSqlMethodCall(methodCall, filePath);
141
+ violations.push(...sqlViolations);
142
+ }
143
+
144
+ // Find template literals with SQL content
145
+ const templateLiterals = this.findSqlTemplateLiterals(sourceFile);
146
+
147
+ for (const template of templateLiterals) {
148
+ const templateViolations = this.analyzeTemplateLiteral(
149
+ template,
150
+ filePath
151
+ );
152
+ violations.push(...templateViolations);
153
+ }
154
+
155
+ if (this.debug) {
156
+ console.log(
157
+ `🔍 [S017-AST] Found ${violations.length} violations in ${filePath}`
158
+ );
159
+ }
160
+ } catch (error) {
161
+ if (this.debug) {
162
+ console.error(`❌ [S017-AST] Error analyzing ${filePath}:`, error);
163
+ }
164
+ }
165
+
166
+ return violations;
167
+ }
168
+
169
+ /**
170
+ * Find method calls that might execute SQL
171
+ */
172
+ findSqlMethodCalls(sourceFile) {
173
+ const methodCalls = [];
174
+
175
+ sourceFile.forEachDescendant((node) => {
176
+ if (node.getKind() === SyntaxKind.CallExpression) {
177
+ const callExpr = node;
178
+ const expression = callExpr.getExpression();
179
+
180
+ let methodName = "";
181
+
182
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
183
+ const propAccess = expression;
184
+ methodName = propAccess.getName();
185
+ } else if (expression.getKind() === SyntaxKind.Identifier) {
186
+ methodName = expression.getText();
187
+ }
188
+
189
+ // Check if method name matches SQL execution methods
190
+ if (this.sqlMethods.includes(methodName)) {
191
+ methodCalls.push({
192
+ node: callExpr,
193
+ methodName,
194
+ line: callExpr.getStartLineNumber(),
195
+ column: callExpr.getStart(),
196
+ });
197
+ }
198
+ }
199
+ });
200
+
201
+ return methodCalls;
202
+ }
203
+
204
+ /**
205
+ * Check if text contains SQL keywords in proper SQL context
206
+ */
207
+ containsSqlKeywords(text) {
208
+ // Convert to uppercase for case-insensitive matching
209
+ const upperText = text.toUpperCase();
210
+
211
+ // Check for SQL keywords that should be word-bounded
212
+ return this.sqlKeywords.some((keyword) => {
213
+ const upperKeyword = keyword.toUpperCase();
214
+
215
+ // For multi-word keywords like "ORDER BY", check exact match
216
+ if (upperKeyword.includes(" ")) {
217
+ return upperText.includes(upperKeyword);
218
+ }
219
+
220
+ // For single-word keywords, ensure word boundaries
221
+ // This prevents "FROM" matching "documents from logs" (casual English)
222
+ // but allows "SELECT * FROM users" (SQL context)
223
+ const wordBoundaryRegex = new RegExp(`\\b${upperKeyword}\\b`, "g");
224
+ const matches = upperText.match(wordBoundaryRegex);
225
+
226
+ if (!matches) return false;
227
+
228
+ // Additional context check: if it's a common English word in non-SQL context, be more strict
229
+ if (["FROM", "WHERE", "ORDER", "GROUP", "JOIN"].includes(upperKeyword)) {
230
+ // Check if it's likely SQL context by looking for other SQL indicators
231
+ const sqlIndicators = [
232
+ "SELECT",
233
+ "INSERT",
234
+ "UPDATE",
235
+ "DELETE",
236
+ "TABLE",
237
+ "DATABASE",
238
+ "\\*",
239
+ "SET ",
240
+ "VALUES",
241
+ ];
242
+ const hasSqlContext = sqlIndicators.some((indicator) =>
243
+ upperText.includes(indicator.toUpperCase())
244
+ );
245
+
246
+ // For logging statements, require stronger SQL context
247
+ if (this.isLikelyLoggingStatement(text)) {
248
+ return hasSqlContext && matches.length > 0;
249
+ }
250
+
251
+ return hasSqlContext || matches.length > 1; // Multiple SQL keywords suggest SQL context
252
+ }
253
+
254
+ return matches.length > 0;
255
+ });
256
+ }
257
+
258
+ /**
259
+ * Check if text looks like a logging statement
260
+ */
261
+ isLikelyLoggingStatement(text) {
262
+ const loggingIndicators = [
263
+ "✅",
264
+ "❌",
265
+ "🐝",
266
+ "⚠️",
267
+ "🔧",
268
+ "📊",
269
+ "🔍", // Emoji indicators
270
+ "log:",
271
+ "info:",
272
+ "debug:",
273
+ "warn:",
274
+ "error:", // Log level indicators
275
+ "Step",
276
+ "Start",
277
+ "End",
278
+ "Complete",
279
+ "Success",
280
+ "Failed", // Process indicators
281
+ "We got",
282
+ "We have",
283
+ "Found",
284
+ "Processed",
285
+ "Recovered", // Reporting language
286
+ "[LINE]",
287
+ "[DB]",
288
+ "[Service]",
289
+ "[API]", // System component indicators
290
+ "Delete rich-menu",
291
+ "Create rich-menu",
292
+ "Update rich-menu", // Specific app operations
293
+ "successfully",
294
+ "failed",
295
+ "done",
296
+ "error", // Result indicators
297
+ "Rollback",
298
+ "Upload",
299
+ "Download", // Action verbs in app context
300
+ ".log(",
301
+ ".error(",
302
+ ".warn(",
303
+ ".info(",
304
+ ".debug(", // Method calls
305
+ ];
306
+
307
+ return loggingIndicators.some((indicator) => text.includes(indicator));
308
+ }
309
+
310
+ /**
311
+ * Check if text contains SQL keywords in proper SQL context
312
+ */
313
+ containsSqlKeywords(text) {
314
+ // Convert to uppercase for case-insensitive matching
315
+ const upperText = text.toUpperCase();
316
+
317
+ // Early return if this looks like logging - be more permissive
318
+ if (this.isLikelyLoggingStatement(text)) {
319
+ // For logging statements, require very strong SQL context
320
+ const strongSqlIndicators = [
321
+ "SELECT *",
322
+ "INSERT INTO",
323
+ "UPDATE SET",
324
+ "DELETE FROM",
325
+ "CREATE TABLE",
326
+ "DROP TABLE",
327
+ "ALTER TABLE",
328
+ "WHERE ",
329
+ "JOIN ",
330
+ "UNION ",
331
+ "GROUP BY",
332
+ "ORDER BY",
333
+ ];
334
+
335
+ const hasStrongSqlContext = strongSqlIndicators.some((indicator) =>
336
+ upperText.includes(indicator.toUpperCase())
337
+ );
338
+
339
+ // Only flag logging statements if they contain strong SQL patterns
340
+ return hasStrongSqlContext;
341
+ }
342
+
343
+ // Check for SQL keywords that should be word-bounded
344
+ return this.sqlKeywords.some((keyword) => {
345
+ const upperKeyword = keyword.toUpperCase();
346
+
347
+ // For multi-word keywords like "ORDER BY", check exact match
348
+ if (upperKeyword.includes(" ")) {
349
+ return upperText.includes(upperKeyword);
350
+ }
351
+
352
+ // For single-word keywords, ensure word boundaries
353
+ const wordBoundaryRegex = new RegExp(`\\b${upperKeyword}\\b`, "g");
354
+ const matches = upperText.match(wordBoundaryRegex);
355
+
356
+ if (!matches) return false;
357
+
358
+ // Additional context check: if it's a common English word in non-SQL context, be more strict
359
+ if (
360
+ [
361
+ "FROM",
362
+ "WHERE",
363
+ "ORDER",
364
+ "GROUP",
365
+ "JOIN",
366
+ "CREATE",
367
+ "DELETE",
368
+ "UPDATE",
369
+ ].includes(upperKeyword)
370
+ ) {
371
+ // Check if it's likely SQL context by looking for other SQL indicators
372
+ const sqlIndicators = [
373
+ "TABLE",
374
+ "DATABASE",
375
+ "COLUMN",
376
+ "\\*",
377
+ "SET ",
378
+ "VALUES",
379
+ "INTO ",
380
+ ];
381
+ const hasSqlContext = sqlIndicators.some((indicator) =>
382
+ upperText.includes(indicator.toUpperCase())
383
+ );
384
+
385
+ return hasSqlContext || matches.length > 1; // Multiple SQL keywords suggest SQL context
386
+ }
387
+
388
+ return matches.length > 0;
389
+ });
390
+ }
391
+
392
+ /**
393
+ * Find template literals that might contain SQL
394
+ */
395
+ findSqlTemplateLiterals(sourceFile) {
396
+ const templateLiterals = [];
397
+
398
+ sourceFile.forEachDescendant((node) => {
399
+ if (node.getKind() === SyntaxKind.TemplateExpression) {
400
+ const template = node;
401
+ const text = template.getText();
402
+
403
+ // Check if template contains SQL keywords using improved logic
404
+ const containsSql = this.containsSqlKeywords(text);
405
+
406
+ if (containsSql) {
407
+ templateLiterals.push({
408
+ node: template,
409
+ text,
410
+ line: template.getStartLineNumber(),
411
+ column: template.getStart(),
412
+ });
413
+ }
414
+ }
415
+ });
416
+
417
+ return templateLiterals;
418
+ }
419
+
420
+ /**
421
+ * Analyze SQL method call for vulnerabilities
422
+ */
423
+ analyzeSqlMethodCall(methodCall, filePath) {
424
+ const violations = [];
425
+ const { node, methodName, line } = methodCall;
426
+ const args = node.getArguments();
427
+
428
+ if (args.length === 0) return violations;
429
+
430
+ const firstArg = args[0];
431
+
432
+ // Check if first argument is a string concatenation or template literal
433
+ if (this.isSuspiciousSqlArgument(firstArg)) {
434
+ const argText = firstArg.getText();
435
+ const evidence =
436
+ node.getText().length > 100
437
+ ? node.getText().substring(0, 100) + "..."
438
+ : node.getText();
439
+
440
+ violations.push({
441
+ ruleId: this.ruleId,
442
+ severity: "error",
443
+ message: `SQL injection risk in ${methodName}(): avoid string concatenation or template literals in SQL queries`,
444
+ source: this.ruleId,
445
+ file: filePath,
446
+ line: line,
447
+ column: firstArg.getStart(),
448
+ evidence: evidence,
449
+ suggestion: `Use parameterized queries with ${methodName}() method instead of string concatenation`,
450
+ category: "security",
451
+ });
452
+
453
+ if (this.debug) {
454
+ console.log(
455
+ `🚨 [S017-AST] Unsafe SQL method call at line ${line}: ${methodName}`
456
+ );
457
+ }
458
+ }
459
+
460
+ return violations;
461
+ }
462
+
463
+ /**
464
+ * Analyze template literal for SQL injection risks
465
+ */
466
+ analyzeTemplateLiteral(template, filePath) {
467
+ const violations = [];
468
+ const { node, text, line } = template;
469
+
470
+ // Check if template has variable interpolation
471
+ if (node.getTemplateSpans().length > 0) {
472
+ const evidence =
473
+ text.length > 100 ? text.substring(0, 100) + "..." : text;
474
+
475
+ violations.push({
476
+ ruleId: this.ruleId,
477
+ severity: "error",
478
+ message:
479
+ "SQL injection risk: template literal with variable interpolation in SQL query",
480
+ source: this.ruleId,
481
+ file: filePath,
482
+ line: line,
483
+ column: node.getStart(),
484
+ evidence: evidence,
485
+ suggestion:
486
+ "Use parameterized queries instead of template literals for SQL statements",
487
+ category: "security",
488
+ });
489
+
490
+ if (this.debug) {
491
+ console.log(`🚨 [S017-AST] Unsafe SQL template at line ${line}`);
492
+ }
493
+ }
494
+
495
+ return violations;
496
+ }
497
+
498
+ /**
499
+ * Check if argument is suspicious for SQL injection
500
+ */
501
+ isSuspiciousSqlArgument(argNode) {
502
+ const kind = argNode.getKind();
503
+
504
+ // Template expressions with interpolation
505
+ if (kind === SyntaxKind.TemplateExpression) {
506
+ return argNode.getTemplateSpans().length > 0;
507
+ }
508
+
509
+ // Binary expressions (string concatenation)
510
+ if (kind === SyntaxKind.BinaryExpression) {
511
+ const binExpr = argNode;
512
+ return binExpr.getOperatorToken().getKind() === SyntaxKind.PlusToken;
513
+ }
514
+
515
+ // Check if it's a template literal with SQL keywords
516
+ if (kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
517
+ const text = argNode.getText();
518
+ return this.sqlKeywords.some((keyword) =>
519
+ text.toUpperCase().includes(keyword.toUpperCase())
520
+ );
521
+ }
522
+
523
+ return false;
524
+ }
525
+
526
+ /**
527
+ * Get analyzer metadata
528
+ */
529
+ getMetadata() {
530
+ return {
531
+ rule: "S017",
532
+ name: "Always use parameterized queries",
533
+ category: "security",
534
+ type: "regex-based",
535
+ description:
536
+ "Uses regex patterns and AST analysis to detect SQL injection vulnerabilities",
537
+ };
538
+ }
539
+ }
540
+
541
+ module.exports = S017RegexBasedAnalyzer;