@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,777 @@
1
+ const { Project, SyntaxKind } = require("ts-morph");
2
+
3
+ /**
4
+ * S017 Symbol-Based Analyzer - Always use parameterized queries
5
+ * Uses semantic analysis to detect SQL injection vulnerabilities
6
+ */
7
+ class S017SymbolBasedAnalyzer {
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-Symbol] Constructor - databaseLibraries:`,
90
+ this.databaseLibraries.length
91
+ );
92
+ console.log(
93
+ `🔧 [S017-Symbol] 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 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`
111
+ );
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Analyze file using symbol information
117
+ */
118
+ async analyzeFile(filePath, fileContent) {
119
+ if (this.debug) {
120
+ console.log(`🔍 [S017-Symbol] Analyzing: ${filePath}`);
121
+ }
122
+
123
+ const violations = [];
124
+ const violationMap = new Map(); // Track unique violations
125
+
126
+ try {
127
+ const project = new Project({
128
+ useInMemoryFileSystem: true,
129
+ compilerOptions: {
130
+ allowJs: true,
131
+ target: "ES2020",
132
+ },
133
+ });
134
+
135
+ const sourceFile = project.createSourceFile(filePath, fileContent);
136
+
137
+ // Find database-related imports
138
+ const dbImports = this.findDatabaseImports(sourceFile);
139
+
140
+ if (this.debug) {
141
+ console.log(
142
+ `🔍 [S017-Symbol] Found ${dbImports.length} database imports:`,
143
+ dbImports.map((i) => i.module)
144
+ );
145
+ }
146
+
147
+ if (dbImports.length === 0 && this.debug) {
148
+ console.log(
149
+ `ℹ️ [S017-Symbol] No database imports found in ${filePath}`
150
+ );
151
+ }
152
+
153
+ // Analyze method calls in context of database usage
154
+ const violations1 = this.analyzeMethodCallsWithContext(
155
+ sourceFile,
156
+ filePath,
157
+ dbImports
158
+ );
159
+ this.addUniqueViolations(violations1, violationMap);
160
+
161
+ // Analyze variable assignments that might contain SQL
162
+ const violations2 = this.analyzeSqlVariableAssignments(
163
+ sourceFile,
164
+ filePath
165
+ );
166
+ this.addUniqueViolations(violations2, violationMap);
167
+
168
+ // Analyze function parameters that might be SQL queries
169
+ const violations3 = this.analyzeFunctionParameters(sourceFile, filePath);
170
+ this.addUniqueViolations(violations3, violationMap);
171
+
172
+ // Always analyze SQL patterns regardless of imports (catch cases without explicit DB imports)
173
+ const violations4 = this.analyzeUniversalSqlPatterns(
174
+ sourceFile,
175
+ filePath
176
+ );
177
+ this.addUniqueViolations(violations4, violationMap);
178
+
179
+ // Convert map to array
180
+ violations.push(...Array.from(violationMap.values()));
181
+
182
+ if (this.debug) {
183
+ console.log(
184
+ `🔍 [S017-Symbol] Found ${violations.length} unique violations in ${filePath}`
185
+ );
186
+ }
187
+ } catch (error) {
188
+ if (this.debug) {
189
+ console.error(`❌ [S017-Symbol] Error analyzing ${filePath}:`, error);
190
+ }
191
+ }
192
+
193
+ return violations;
194
+ }
195
+
196
+ /**
197
+ * Add violations to map, avoiding duplicates
198
+ */
199
+ addUniqueViolations(newViolations, violationMap) {
200
+ newViolations.forEach((v) => {
201
+ const key = `${v.line}:${v.column}:${v.message}`;
202
+ if (!violationMap.has(key)) {
203
+ violationMap.set(key, v);
204
+ }
205
+ });
206
+ }
207
+
208
+ /**
209
+ * Find database-related imports
210
+ */
211
+ findDatabaseImports(sourceFile) {
212
+ const imports = [];
213
+
214
+ sourceFile.forEachDescendant((node) => {
215
+ // Check for ES6 imports
216
+ if (node.getKind() === SyntaxKind.ImportDeclaration) {
217
+ const importDecl = node;
218
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
219
+
220
+ if (this.databaseLibraries.includes(moduleSpecifier)) {
221
+ imports.push({
222
+ module: moduleSpecifier,
223
+ node: importDecl,
224
+ line: importDecl.getStartLineNumber(),
225
+ });
226
+ }
227
+ }
228
+
229
+ // Check for CommonJS require() calls
230
+ if (node.getKind() === SyntaxKind.CallExpression) {
231
+ const callExpr = node;
232
+ const expression = callExpr.getExpression();
233
+
234
+ if (
235
+ expression.getKind() === SyntaxKind.Identifier &&
236
+ expression.getText() === "require"
237
+ ) {
238
+ const args = callExpr.getArguments();
239
+ if (args.length > 0) {
240
+ const firstArg = args[0];
241
+ if (firstArg.getKind() === SyntaxKind.StringLiteral) {
242
+ const moduleSpecifier = firstArg.getLiteralValue();
243
+
244
+ if (this.databaseLibraries.includes(moduleSpecifier)) {
245
+ imports.push({
246
+ module: moduleSpecifier,
247
+ node: callExpr,
248
+ line: callExpr.getStartLineNumber(),
249
+ });
250
+ }
251
+ }
252
+ }
253
+ }
254
+ }
255
+ });
256
+
257
+ return imports;
258
+ }
259
+
260
+ /**
261
+ * Analyze method calls with database context
262
+ */
263
+ analyzeMethodCallsWithContext(sourceFile, filePath, dbImports) {
264
+ const violations = [];
265
+
266
+ sourceFile.forEachDescendant((node) => {
267
+ if (node.getKind() === SyntaxKind.CallExpression) {
268
+ const callExpr = node;
269
+ const methodName = this.getMethodName(callExpr);
270
+
271
+ if (this.sqlMethods.includes(methodName)) {
272
+ const args = callExpr.getArguments();
273
+
274
+ if (args.length > 0) {
275
+ const sqlArg = args[0];
276
+ const vulnerability = this.analyzeSqlArgument(sqlArg, methodName);
277
+
278
+ if (vulnerability) {
279
+ violations.push({
280
+ ruleId: this.ruleId,
281
+ severity: "error",
282
+ message: vulnerability.message,
283
+ source: this.ruleId,
284
+ file: filePath,
285
+ line: callExpr.getStartLineNumber(),
286
+ column: sqlArg.getStart(),
287
+ evidence: this.getEvidenceText(callExpr),
288
+ suggestion: vulnerability.suggestion,
289
+ category: "security",
290
+ });
291
+
292
+ if (this.debug) {
293
+ console.log(
294
+ `🚨 [S017-Symbol] Vulnerability in ${methodName} at line ${callExpr.getStartLineNumber()}`
295
+ );
296
+ }
297
+ }
298
+ }
299
+ }
300
+ }
301
+ });
302
+
303
+ return violations;
304
+ }
305
+
306
+ /**
307
+ * Analyze SQL variable assignments
308
+ */
309
+ analyzeSqlVariableAssignments(sourceFile, filePath) {
310
+ const violations = [];
311
+
312
+ sourceFile.forEachDescendant((node) => {
313
+ if (node.getKind() === SyntaxKind.VariableDeclaration) {
314
+ const varDecl = node;
315
+ const initializer = varDecl.getInitializer();
316
+
317
+ if (initializer) {
318
+ const vulnerability = this.checkForSqlConstruction(initializer);
319
+
320
+ if (vulnerability) {
321
+ violations.push({
322
+ ruleId: this.ruleId,
323
+ severity: "error",
324
+ message: `SQL injection risk in variable assignment: ${vulnerability.message}`,
325
+ source: this.ruleId,
326
+ file: filePath,
327
+ line: varDecl.getStartLineNumber(),
328
+ column: initializer.getStart(),
329
+ evidence: this.getEvidenceText(varDecl),
330
+ suggestion: vulnerability.suggestion,
331
+ category: "security",
332
+ });
333
+
334
+ if (this.debug) {
335
+ console.log(
336
+ `🚨 [S017-Symbol] SQL variable assignment at line ${varDecl.getStartLineNumber()}`
337
+ );
338
+ }
339
+ }
340
+ }
341
+ }
342
+ });
343
+
344
+ return violations;
345
+ }
346
+
347
+ /**
348
+ * Analyze function parameters for SQL injection
349
+ */
350
+ analyzeFunctionParameters(sourceFile, filePath) {
351
+ const violations = [];
352
+
353
+ sourceFile.forEachDescendant((node) => {
354
+ if (
355
+ node.getKind() === SyntaxKind.FunctionDeclaration ||
356
+ node.getKind() === SyntaxKind.ArrowFunction ||
357
+ node.getKind() === SyntaxKind.FunctionExpression
358
+ ) {
359
+ const func = node;
360
+ const body = func.getBody();
361
+
362
+ if (body) {
363
+ // Look for SQL construction within function body
364
+ body.forEachDescendant((childNode) => {
365
+ if (childNode.getKind() === SyntaxKind.BinaryExpression) {
366
+ const binExpr = childNode;
367
+ const vulnerability = this.analyzeBinaryExpression(binExpr);
368
+
369
+ if (vulnerability) {
370
+ violations.push({
371
+ ruleId: this.ruleId,
372
+ severity: "error",
373
+ message: vulnerability.message,
374
+ source: this.ruleId,
375
+ file: filePath,
376
+ line: binExpr.getStartLineNumber(),
377
+ column: binExpr.getStart(),
378
+ evidence: this.getEvidenceText(binExpr),
379
+ suggestion: vulnerability.suggestion,
380
+ category: "security",
381
+ });
382
+ }
383
+ }
384
+ });
385
+ }
386
+ }
387
+ });
388
+
389
+ return violations;
390
+ }
391
+
392
+ /**
393
+ * Get method name from call expression
394
+ */
395
+ getMethodName(callExpr) {
396
+ const expression = callExpr.getExpression();
397
+
398
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
399
+ return expression.getName();
400
+ } else if (expression.getKind() === SyntaxKind.Identifier) {
401
+ return expression.getText();
402
+ }
403
+
404
+ return "";
405
+ }
406
+
407
+ /**
408
+ * Analyze SQL argument for vulnerabilities
409
+ */
410
+ analyzeSqlArgument(argNode, methodName) {
411
+ const kind = argNode.getKind();
412
+
413
+ // Template expression with interpolation
414
+ if (kind === SyntaxKind.TemplateExpression) {
415
+ const templateSpans = argNode.getTemplateSpans();
416
+ if (templateSpans.length > 0) {
417
+ return {
418
+ message: `Template literal with variable interpolation in ${methodName}() call`,
419
+ suggestion: `Use parameterized queries with ${methodName}() instead of template literals`,
420
+ };
421
+ }
422
+ }
423
+
424
+ // Binary expression (string concatenation)
425
+ if (kind === SyntaxKind.BinaryExpression) {
426
+ const vulnerability = this.analyzeBinaryExpression(argNode);
427
+ if (vulnerability) {
428
+ return {
429
+ message: `String concatenation in ${methodName}() call: ${vulnerability.message}`,
430
+ suggestion: `Use parameterized queries with ${methodName}() instead of string concatenation`,
431
+ };
432
+ }
433
+ }
434
+
435
+ return null;
436
+ }
437
+
438
+ /**
439
+ * Check if text contains SQL keywords in proper SQL context
440
+ */
441
+ containsSqlKeywords(text) {
442
+ // Convert to uppercase for case-insensitive matching
443
+ const upperText = text.toUpperCase();
444
+
445
+ // Check for SQL keywords that should be word-bounded
446
+ return this.sqlKeywords.some((keyword) => {
447
+ const upperKeyword = keyword.toUpperCase();
448
+
449
+ // For multi-word keywords like "ORDER BY", check exact match
450
+ if (upperKeyword.includes(" ")) {
451
+ return upperText.includes(upperKeyword);
452
+ }
453
+
454
+ // For single-word keywords, ensure word boundaries
455
+ // This prevents "FROM" matching "documents from logs" (casual English)
456
+ // but allows "SELECT * FROM users" (SQL context)
457
+ const wordBoundaryRegex = new RegExp(`\\b${upperKeyword}\\b`, "g");
458
+ const matches = upperText.match(wordBoundaryRegex);
459
+
460
+ if (!matches) return false;
461
+
462
+ // Additional context check: if it's a common English word in non-SQL context, be more strict
463
+ if (["FROM", "WHERE", "ORDER", "GROUP", "JOIN"].includes(upperKeyword)) {
464
+ // Check if it's likely SQL context by looking for other SQL indicators
465
+ const sqlIndicators = [
466
+ "SELECT",
467
+ "INSERT",
468
+ "UPDATE",
469
+ "DELETE",
470
+ "TABLE",
471
+ "DATABASE",
472
+ "\\*",
473
+ "SET ",
474
+ "VALUES",
475
+ ];
476
+ const hasSqlContext = sqlIndicators.some((indicator) =>
477
+ upperText.includes(indicator.toUpperCase())
478
+ );
479
+
480
+ // For logging statements, require stronger SQL context
481
+ if (this.isLikelyLoggingStatement(text)) {
482
+ return hasSqlContext && matches.length > 0;
483
+ }
484
+
485
+ return hasSqlContext || matches.length > 1; // Multiple SQL keywords suggest SQL context
486
+ }
487
+
488
+ return matches.length > 0;
489
+ });
490
+ }
491
+
492
+ /**
493
+ * Check if text looks like a logging statement
494
+ */
495
+ isLikelyLoggingStatement(text) {
496
+ const loggingIndicators = [
497
+ "✅",
498
+ "❌",
499
+ "🐝",
500
+ "⚠️",
501
+ "🔧",
502
+ "📊",
503
+ "🔍", // Emoji indicators
504
+ "log:",
505
+ "info:",
506
+ "debug:",
507
+ "warn:",
508
+ "error:", // Log level indicators
509
+ "Step",
510
+ "Start",
511
+ "End",
512
+ "Complete",
513
+ "Success",
514
+ "Failed", // Process indicators
515
+ "We got",
516
+ "We have",
517
+ "Found",
518
+ "Processed",
519
+ "Recovered", // Reporting language
520
+ "[LINE]",
521
+ "[DB]",
522
+ "[Service]",
523
+ "[API]", // System component indicators
524
+ "Delete rich-menu",
525
+ "Create rich-menu",
526
+ "Update rich-menu", // Specific app operations
527
+ "successfully",
528
+ "failed",
529
+ "done",
530
+ "error", // Result indicators
531
+ "Rollback",
532
+ "Upload",
533
+ "Download", // Action verbs in app context
534
+ ".log(",
535
+ ".error(",
536
+ ".warn(",
537
+ ".info(",
538
+ ".debug(", // Method calls
539
+ ];
540
+
541
+ return loggingIndicators.some((indicator) => text.includes(indicator));
542
+ }
543
+
544
+ /**
545
+ * Check if text contains SQL keywords in proper SQL context
546
+ */
547
+ containsSqlKeywords(text) {
548
+ // Convert to uppercase for case-insensitive matching
549
+ const upperText = text.toUpperCase();
550
+
551
+ // Early return if this looks like logging - be more permissive
552
+ if (this.isLikelyLoggingStatement(text)) {
553
+ // For logging statements, require very strong SQL context
554
+ const strongSqlIndicators = [
555
+ "SELECT *",
556
+ "INSERT INTO",
557
+ "UPDATE SET",
558
+ "DELETE FROM",
559
+ "CREATE TABLE",
560
+ "DROP TABLE",
561
+ "ALTER TABLE",
562
+ "WHERE ",
563
+ "JOIN ",
564
+ "UNION ",
565
+ "GROUP BY",
566
+ "ORDER BY",
567
+ ];
568
+
569
+ const hasStrongSqlContext = strongSqlIndicators.some((indicator) =>
570
+ upperText.includes(indicator.toUpperCase())
571
+ );
572
+
573
+ // Only flag logging statements if they contain strong SQL patterns
574
+ return hasStrongSqlContext;
575
+ }
576
+
577
+ // Check for SQL keywords that should be word-bounded
578
+ return this.sqlKeywords.some((keyword) => {
579
+ const upperKeyword = keyword.toUpperCase();
580
+
581
+ // For multi-word keywords like "ORDER BY", check exact match
582
+ if (upperKeyword.includes(" ")) {
583
+ return upperText.includes(upperKeyword);
584
+ }
585
+
586
+ // For single-word keywords, ensure word boundaries
587
+ const wordBoundaryRegex = new RegExp(`\\b${upperKeyword}\\b`, "g");
588
+ const matches = upperText.match(wordBoundaryRegex);
589
+
590
+ if (!matches) return false;
591
+
592
+ // Additional context check: if it's a common English word in non-SQL context, be more strict
593
+ if (
594
+ [
595
+ "FROM",
596
+ "WHERE",
597
+ "ORDER",
598
+ "GROUP",
599
+ "JOIN",
600
+ "CREATE",
601
+ "DELETE",
602
+ "UPDATE",
603
+ ].includes(upperKeyword)
604
+ ) {
605
+ // Check if it's likely SQL context by looking for other SQL indicators
606
+ const sqlIndicators = [
607
+ "TABLE",
608
+ "DATABASE",
609
+ "COLUMN",
610
+ "\\*",
611
+ "SET ",
612
+ "VALUES",
613
+ "INTO ",
614
+ ];
615
+ const hasSqlContext = sqlIndicators.some((indicator) =>
616
+ upperText.includes(indicator.toUpperCase())
617
+ );
618
+
619
+ return hasSqlContext || matches.length > 1; // Multiple SQL keywords suggest SQL context
620
+ }
621
+
622
+ return matches.length > 0;
623
+ });
624
+ }
625
+
626
+ /**
627
+ * Check for SQL construction patterns
628
+ */
629
+ checkForSqlConstruction(node) {
630
+ const kind = node.getKind();
631
+
632
+ if (kind === SyntaxKind.TemplateExpression) {
633
+ const text = node.getText();
634
+ const hasSqlKeyword = this.containsSqlKeywords(text);
635
+
636
+ if (hasSqlKeyword && node.getTemplateSpans().length > 0) {
637
+ return {
638
+ message:
639
+ "template literal with SQL keywords and variable interpolation",
640
+ suggestion: "Use parameterized queries instead of template literals",
641
+ };
642
+ }
643
+ }
644
+
645
+ if (kind === SyntaxKind.BinaryExpression) {
646
+ return this.analyzeBinaryExpression(node);
647
+ }
648
+
649
+ return null;
650
+ }
651
+ /**
652
+ * Analyze binary expression for SQL concatenation
653
+ */
654
+ analyzeBinaryExpression(binExpr) {
655
+ const operator = binExpr.getOperatorToken();
656
+
657
+ if (operator.getKind() === SyntaxKind.PlusToken) {
658
+ const leftText = binExpr.getLeft().getText();
659
+ const rightText = binExpr.getRight().getText();
660
+ const fullText = binExpr.getText();
661
+
662
+ const hasSqlKeyword = this.containsSqlKeywords(fullText);
663
+
664
+ if (hasSqlKeyword) {
665
+ return {
666
+ message: "string concatenation with SQL keywords detected",
667
+ suggestion:
668
+ "Use parameterized queries with placeholders (?, $1, etc.)",
669
+ };
670
+ }
671
+ }
672
+
673
+ return null;
674
+ }
675
+
676
+ /**
677
+ * Get evidence text for violation
678
+ */
679
+ getEvidenceText(node) {
680
+ const text = node.getText();
681
+ return text.length > 100 ? text.substring(0, 100) + "..." : text;
682
+ }
683
+
684
+ /**
685
+ * Analyze universal SQL patterns regardless of imports
686
+ */
687
+ analyzeUniversalSqlPatterns(sourceFile, filePath) {
688
+ const violations = [];
689
+
690
+ sourceFile.forEachDescendant((node) => {
691
+ // Check template literals with SQL keywords
692
+ if (node.getKind() === SyntaxKind.TemplateExpression) {
693
+ const template = node;
694
+ const text = template.getText();
695
+
696
+ // Check if template contains SQL keywords and has interpolation
697
+ const containsSql = this.containsSqlKeywords(text);
698
+
699
+ if (containsSql && template.getTemplateSpans().length > 0) {
700
+ violations.push({
701
+ ruleId: this.ruleId,
702
+ severity: "error",
703
+ message:
704
+ "SQL injection risk: template literal with variable interpolation in SQL query",
705
+ source: this.ruleId,
706
+ file: filePath,
707
+ line: template.getStartLineNumber(),
708
+ column: template.getStart(),
709
+ evidence: this.getEvidenceText(template),
710
+ suggestion:
711
+ "Use parameterized queries instead of template literals for SQL statements",
712
+ category: "security",
713
+ });
714
+
715
+ if (this.debug) {
716
+ console.log(
717
+ `🚨 [S017-Symbol] Universal SQL template at line ${template.getStartLineNumber()}`
718
+ );
719
+ }
720
+ }
721
+ }
722
+
723
+ // Check binary expressions with SQL concatenation
724
+ if (node.getKind() === SyntaxKind.BinaryExpression) {
725
+ const binExpr = node;
726
+ const operator = binExpr.getOperatorToken();
727
+
728
+ if (operator.getKind() === SyntaxKind.PlusToken) {
729
+ const fullText = binExpr.getText();
730
+
731
+ const hasSqlKeyword = this.containsSqlKeywords(fullText);
732
+
733
+ if (hasSqlKeyword) {
734
+ violations.push({
735
+ ruleId: this.ruleId,
736
+ severity: "error",
737
+ message:
738
+ "SQL injection risk: string concatenation with SQL keywords detected",
739
+ source: this.ruleId,
740
+ file: filePath,
741
+ line: binExpr.getStartLineNumber(),
742
+ column: binExpr.getStart(),
743
+ evidence: this.getEvidenceText(binExpr),
744
+ suggestion:
745
+ "Use parameterized queries with placeholders (?, $1, etc.)",
746
+ category: "security",
747
+ });
748
+
749
+ if (this.debug) {
750
+ console.log(
751
+ `🚨 [S017-Symbol] Universal SQL concatenation at line ${binExpr.getStartLineNumber()}`
752
+ );
753
+ }
754
+ }
755
+ }
756
+ }
757
+ });
758
+
759
+ return violations;
760
+ }
761
+
762
+ /**
763
+ * Get analyzer metadata
764
+ */
765
+ getMetadata() {
766
+ return {
767
+ rule: "S017",
768
+ name: "Always use parameterized queries",
769
+ category: "security",
770
+ type: "symbol-based",
771
+ description:
772
+ "Uses semantic analysis to detect SQL injection vulnerabilities",
773
+ };
774
+ }
775
+ }
776
+
777
+ module.exports = S017SymbolBasedAnalyzer;