@sun-asterisk/sunlint 1.3.27 → 1.3.29

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 (29) hide show
  1. package/config/rules/enhanced-rules-registry.json +2 -1
  2. package/config/rules/rules-registry-generated.json +22 -22
  3. package/origin-rules/security-en.md +351 -338
  4. package/package.json +1 -1
  5. package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
  6. package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
  7. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
  8. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
  9. package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
  10. package/rules/security/S005_no_origin_auth/config.json +28 -67
  11. package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
  12. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
  13. package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
  14. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
  15. package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
  16. package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
  17. package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
  18. package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
  19. package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +1 -1
  20. package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
  21. package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
  22. package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
  23. package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
  24. package/docs/COMMAND-EXAMPLES.md +0 -390
  25. package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
  26. package/docs/FOLDER_STRUCTURE.md +0 -59
  27. package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
  28. package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
  29. package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
@@ -84,6 +84,76 @@ class S017SymbolBasedAnalyzer {
84
84
  "values",
85
85
  ];
86
86
 
87
+ // HTTP client libraries (NOT SQL - should be excluded)
88
+ this.httpClientLibraries = [
89
+ "axios",
90
+ "fetch",
91
+ "node-fetch",
92
+ "superagent",
93
+ "got",
94
+ "request",
95
+ "http",
96
+ "https",
97
+ "undici",
98
+ "@angular/common/http",
99
+ ];
100
+
101
+ // HTTP client variable/object names (case-insensitive patterns)
102
+ this.httpClientPatterns = [
103
+ /^axios/i,
104
+ /^fetch/i,
105
+ /^http/i,
106
+ /^https/i,
107
+ /client$/i,
108
+ /api$/i,
109
+ /request$/i,
110
+ ];
111
+
112
+ // Styled-components and CSS-in-JS libraries
113
+ this.styledLibraries = [
114
+ "styled-components",
115
+ "@emotion/styled",
116
+ "emotion",
117
+ "@mui/system",
118
+ "goober",
119
+ ];
120
+
121
+ // TypeORM migration file patterns
122
+ this.migrationFilePatterns = [
123
+ /\/migrations?\//,
124
+ /\\migrations?\\/,
125
+ /-\d{13,}\.ts$/, // TypeORM migration timestamp pattern
126
+ /\.migration\.ts$/,
127
+ ];
128
+
129
+ // Swagger/API documentation file patterns
130
+ this.swaggerFilePatterns = [
131
+ /\.swagger\.ts$/,
132
+ /\/swagger\//,
133
+ /\\swagger\\/,
134
+ ];
135
+
136
+ // Validation message patterns (common in DTOs and Swagger docs)
137
+ this.validationMessagePatterns = [
138
+ /must be one of the following values:/i,
139
+ /should be one of:/i,
140
+ /isEnum:/i,
141
+ /constraints:/i,
142
+ /validation error/i,
143
+ ];
144
+
145
+ // DOM manipulation methods that use HTML templates (NOT SQL)
146
+ this.domManipulationMethods = [
147
+ "insertAdjacentHTML",
148
+ "innerHTML",
149
+ "outerHTML",
150
+ "insertAdjacentElement",
151
+ "insertAdjacentText",
152
+ "setHTML",
153
+ "write",
154
+ "writeln",
155
+ ];
156
+
87
157
  if (this.debug) {
88
158
  console.log(
89
159
  `🔧 [S017-Symbol] Constructor - databaseLibraries:`,
@@ -123,6 +193,22 @@ class S017SymbolBasedAnalyzer {
123
193
  const violations = [];
124
194
  const violationMap = new Map(); // Track unique violations
125
195
 
196
+ // IMPROVEMENT 1: Skip test files
197
+ if (this.isTestFile(filePath)) {
198
+ if (this.debug) {
199
+ console.log(`⏭️ [S017-Symbol] Skipping test file: ${filePath}`);
200
+ }
201
+ return violations;
202
+ }
203
+
204
+ // PHASE 3C IMPROVEMENT: Skip migration files
205
+ if (this.isMigrationFile(filePath)) {
206
+ if (this.debug) {
207
+ console.log(`⏭️ [S017-Symbol] Skipping migration file: ${filePath}`);
208
+ }
209
+ return violations;
210
+ }
211
+
126
212
  try {
127
213
  const project = new Project({
128
214
  useInMemoryFileSystem: true,
@@ -195,12 +281,17 @@ class S017SymbolBasedAnalyzer {
195
281
 
196
282
  /**
197
283
  * Add violations to map, avoiding duplicates
284
+ * PHASE 3B: Improved deduplication - use line:column only (ignore message)
198
285
  */
199
286
  addUniqueViolations(newViolations, violationMap) {
200
287
  newViolations.forEach((v) => {
201
- const key = `${v.line}:${v.column}:${v.message}`;
288
+ // Use line:column as key to deduplicate same location violations
289
+ // Ignore message since different analyzers may report same location with different messages
290
+ const key = `${v.line}:${v.column}`;
202
291
  if (!violationMap.has(key)) {
203
292
  violationMap.set(key, v);
293
+ } else if (this.debug) {
294
+ console.log(`⏭️ [S017-Symbol] Skipping duplicate violation at ${key}`);
204
295
  }
205
296
  });
206
297
  }
@@ -269,6 +360,24 @@ class S017SymbolBasedAnalyzer {
269
360
  const methodName = this.getMethodName(callExpr);
270
361
 
271
362
  if (this.sqlMethods.includes(methodName)) {
363
+ // Skip HTTP client calls (e.g., axios.get(), fetch())
364
+ if (this.isHttpClientCall(callExpr)) {
365
+ return; // Skip this call
366
+ }
367
+
368
+ // Skip config getter calls (e.g., appConfig.get(...))
369
+ const expression = callExpr.getExpression();
370
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
371
+ const propAccess = expression;
372
+ const object = propAccess.getExpression().getText();
373
+ if (methodName === 'get' && /config/i.test(object)) {
374
+ if (this.debug) {
375
+ console.log(`⚙️ [S017-Symbol] Skipping config getter in analyzeMethodCallsWithContext: ${object}.${methodName}()`);
376
+ }
377
+ return; // Skip config getter
378
+ }
379
+ }
380
+
272
381
  const args = callExpr.getArguments();
273
382
 
274
383
  if (args.length > 0) {
@@ -309,6 +418,11 @@ class S017SymbolBasedAnalyzer {
309
418
  analyzeSqlVariableAssignments(sourceFile, filePath) {
310
419
  const violations = [];
311
420
 
421
+ // Skip files in /queries/ or /query/ folders - these are SQL query definition files
422
+ if (filePath.includes('/queries/') || filePath.includes('/query/') || filePath.includes('\\queries\\') || filePath.includes('\\query\\')) {
423
+ return violations;
424
+ }
425
+
312
426
  sourceFile.forEachDescendant((node) => {
313
427
  if (node.getKind() === SyntaxKind.VariableDeclaration) {
314
428
  const varDecl = node;
@@ -318,6 +432,56 @@ class S017SymbolBasedAnalyzer {
318
432
  const vulnerability = this.checkForSqlConstruction(initializer);
319
433
 
320
434
  if (vulnerability) {
435
+ // Check if this is safe SQL fragment composition
436
+ if (initializer.getKind() === SyntaxKind.TemplateExpression) {
437
+ // PHASE 3A: Apply isParameterizedQuery check
438
+ if (this.isParameterizedQuery(initializer)) {
439
+ if (this.debug) {
440
+ console.log(`✅ [S017-Symbol] Parameterized query in variable at line ${varDecl.getStartLineNumber()}`);
441
+ }
442
+ return; // Skip - safe parameterized query
443
+ }
444
+
445
+ // PHASE 3C: Check if this is a query fragment variable
446
+ if (this.isQueryFragmentVariable(varDecl)) {
447
+ if (this.debug) {
448
+ console.log(`✅ [S017-Symbol] Query fragment variable at line ${varDecl.getStartLineNumber()}`);
449
+ }
450
+ return; // Skip - safe query fragment
451
+ }
452
+
453
+ if (this.hasOnlySafeSqlFragments(initializer)) {
454
+ if (this.debug) {
455
+ console.log(`✅ [S017-Symbol] Safe SQL fragment in variable at line ${varDecl.getStartLineNumber()}`);
456
+ }
457
+ return; // Skip - safe SQL fragment composition
458
+ }
459
+
460
+ // Check if this is a URL construction (not SQL)
461
+ if (this.isUrlConstruction(initializer)) {
462
+ if (this.debug) {
463
+ console.log(`✅ [S017-Symbol] URL construction in variable at line ${varDecl.getStartLineNumber()}`);
464
+ }
465
+ return; // Skip - URL construction
466
+ }
467
+
468
+ // Check if this is a logger call
469
+ if (this.isLoggerCall(initializer)) {
470
+ if (this.debug) {
471
+ console.log(`✅ [S017-Symbol] Logger call in variable at line ${varDecl.getStartLineNumber()}`);
472
+ }
473
+ return; // Skip - logger call
474
+ }
475
+
476
+ // Check if this is a config getter
477
+ if (this.isConfigGetterCall(initializer)) {
478
+ if (this.debug) {
479
+ console.log(`✅ [S017-Symbol] Config getter in variable at line ${varDecl.getStartLineNumber()}`);
480
+ }
481
+ return; // Skip - config getter
482
+ }
483
+ }
484
+
321
485
  violations.push({
322
486
  ruleId: this.ruleId,
323
487
  severity: "error",
@@ -410,8 +574,24 @@ class S017SymbolBasedAnalyzer {
410
574
  analyzeSqlArgument(argNode, methodName) {
411
575
  const kind = argNode.getKind();
412
576
 
577
+ // PHASE 3C: Skip NoSQL queries (different security concern)
578
+ if (this.isNoSqlQuery(argNode)) {
579
+ if (this.debug) {
580
+ console.log(`✅ [S017-Symbol] NoSQL query detected in ${methodName}()`);
581
+ }
582
+ return null; // Skip - NoSQL query, not SQL injection
583
+ }
584
+
413
585
  // Template expression with interpolation
414
586
  if (kind === SyntaxKind.TemplateExpression) {
587
+ // PHASE 3A: Apply isParameterizedQuery check
588
+ if (this.isParameterizedQuery(argNode)) {
589
+ if (this.debug) {
590
+ console.log(`✅ [S017-Symbol] Parameterized query in ${methodName}() call`);
591
+ }
592
+ return null; // Skip - safe parameterized query
593
+ }
594
+
415
595
  const templateSpans = argNode.getTemplateSpans();
416
596
  if (templateSpans.length > 0) {
417
597
  return {
@@ -681,22 +861,603 @@ class S017SymbolBasedAnalyzer {
681
861
  return text.length > 100 ? text.substring(0, 100) + "..." : text;
682
862
  }
683
863
 
864
+ /**
865
+ * Check if interpolated expression is a safe constant/fragment
866
+ */
867
+ isSafeSqlFragmentInterpolation(expression) {
868
+ const text = expression.getText();
869
+
870
+ // Check if it's an uppercase constant (e.g., SELECT_QUERY, BASE_SQL)
871
+ if (/^[A-Z_][A-Z0-9_]*$/.test(text)) {
872
+ return true;
873
+ }
874
+
875
+ // Check if it's a camelCase constant starting with lowercase (e.g., selectQuery, baseQuery)
876
+ if (/^[a-z][a-zA-Z0-9]*Query$|^[a-z][a-zA-Z0-9]*Sql$|^[a-z][a-zA-Z0-9]*Select$|^[a-z][a-zA-Z0-9]*CTE$/.test(text)) {
877
+ return true;
878
+ }
879
+
880
+ // Check if it's accessing a property that looks like a SQL fragment
881
+ if (text.includes('.') && (text.toLowerCase().includes('query') || text.toLowerCase().includes('sql') || text.toLowerCase().includes('select'))) {
882
+ return true;
883
+ }
884
+
885
+ // Check if it's a conditional expression with SQL fragment variables
886
+ // Pattern: condition ? sqlFragmentVar : '' or condition ? '' : sqlFragmentVar
887
+ if (expression.getKind() === SyntaxKind.ConditionalExpression) {
888
+ const condExpr = expression;
889
+ const whenTrue = condExpr.getWhenTrue();
890
+ const whenFalse = condExpr.getWhenFalse();
891
+
892
+ const trueText = whenTrue.getText();
893
+ const falseText = whenFalse.getText();
894
+
895
+ // Check if either branch is empty string and the other is a SQL fragment variable
896
+ if ((trueText === "''" || trueText === '""' || trueText === '``') && this.isSqlFragmentVariable(falseText)) {
897
+ return true;
898
+ }
899
+ if ((falseText === "''" || falseText === '""' || falseText === '``') && this.isSqlFragmentVariable(trueText)) {
900
+ return true;
901
+ }
902
+
903
+ // Check if both branches are SQL fragment variables
904
+ if (this.isSqlFragmentVariable(trueText) && this.isSqlFragmentVariable(falseText)) {
905
+ return true;
906
+ }
907
+ }
908
+
909
+ return false;
910
+ }
911
+
912
+ /**
913
+ * Check if a variable name looks like a SQL fragment variable
914
+ */
915
+ isSqlFragmentVariable(text) {
916
+ // Pattern: xxxQuery, xxxSql, xxxSelect, xxxWhere, xxxJoin, xxxCondition, etc.
917
+ return /^[a-z][a-zA-Z0-9]*(Query|Sql|Select|Where|Join|Condition|Fragment|Clause)$/i.test(text) ||
918
+ /^[A-Z_][A-Z0-9_]*(QUERY|SQL|SELECT|WHERE|JOIN|CONDITION|FRAGMENT|CLAUSE)$/.test(text);
919
+ }
920
+
921
+ /**
922
+ * Check if a template literal is wrapping a QueryBuilder result
923
+ * Pattern: const [query, params] = qb.getQueryAndParameters(); `...${query}...`
924
+ */
925
+ isQueryBuilderWrapping(template, sourceFile) {
926
+ const spans = template.getTemplateSpans();
927
+
928
+ for (const span of spans) {
929
+ const expression = span.getExpression();
930
+ const exprText = expression.getText();
931
+
932
+ // Check if interpolated variable is 'query' or 'sql' (common names for QueryBuilder results)
933
+ if (exprText === 'query' || exprText === 'sql' || exprText === 'sqlQuery') {
934
+ // Search in broader scope - check multiple levels up for getQueryAndParameters()
935
+ let scope = template.getParent();
936
+ let foundPattern = false;
937
+ let maxDepth = 10; // Limit depth to avoid infinite loops
938
+
939
+ while (scope && maxDepth > 0) {
940
+ const scopeText = scope.getText();
941
+
942
+ // Check if this scope contains getQueryAndParameters() call
943
+ // and destructuring assignment to the variable name
944
+ const hasQueryBuilderPattern =
945
+ scopeText.includes('getQueryAndParameters()') &&
946
+ (scopeText.includes(`[${exprText},`) || scopeText.includes(`[ ${exprText},`) ||
947
+ scopeText.includes(`[${exprText} ,`) || scopeText.includes(`[${exprText}]`) ||
948
+ scopeText.includes(`const ${exprText} =`) || scopeText.includes(`let ${exprText} =`));
949
+
950
+ if (hasQueryBuilderPattern) {
951
+ if (this.debug) {
952
+ console.log(`✅ [S017-Symbol] Detected QueryBuilder wrapping pattern with variable: ${exprText}`);
953
+ }
954
+ foundPattern = true;
955
+ break;
956
+ }
957
+
958
+ scope = scope.getParent();
959
+ maxDepth--;
960
+ }
961
+
962
+ if (foundPattern) {
963
+ return true;
964
+ }
965
+ }
966
+ }
967
+
968
+ return false;
969
+ }
970
+
971
+ /**
972
+ * Check if all interpolations in template are safe SQL fragments
973
+ */
974
+ hasOnlySafeSqlFragments(template) {
975
+ const spans = template.getTemplateSpans();
976
+
977
+ for (const span of spans) {
978
+ const expression = span.getExpression();
979
+ if (!this.isSafeSqlFragmentInterpolation(expression)) {
980
+ return false;
981
+ }
982
+ }
983
+
984
+ return true;
985
+ }
986
+
987
+ /**
988
+ * Check if file is a TypeORM migration file
989
+ */
990
+ isMigrationFile(filePath) {
991
+ return this.migrationFilePatterns.some(pattern => pattern.test(filePath));
992
+ }
993
+
994
+ /**
995
+ * Check if file is a Swagger/API documentation file
996
+ */
997
+ isSwaggerFile(filePath) {
998
+ return this.swaggerFilePatterns.some(pattern => pattern.test(filePath));
999
+ }
1000
+
1001
+ /**
1002
+ * Check if template literal contains validation message pattern
1003
+ */
1004
+ isValidationMessage(template) {
1005
+ const text = template.getText();
1006
+ return this.validationMessagePatterns.some(pattern => pattern.test(text));
1007
+ }
1008
+
1009
+ /**
1010
+ * Check if template literal is used in logger/console call
1011
+ */
1012
+ isLoggerCall(template) {
1013
+ let parent = template.getParent();
1014
+
1015
+ // Walk up to find if we're in a logger/console call
1016
+ while (parent) {
1017
+ if (parent.getKind() === SyntaxKind.CallExpression) {
1018
+ const callExpr = parent;
1019
+ const expression = callExpr.getExpression();
1020
+ const callText = expression.getText();
1021
+
1022
+ // Check for logger methods
1023
+ const loggerPatterns = [
1024
+ /logger\.(log|error|warn|info|debug)/i,
1025
+ /console\.(log|error|warn|info|debug)/i,
1026
+ /log(Debug|Info|Warn|Error)/i,
1027
+ /this\.logger\./i,
1028
+ ];
1029
+
1030
+ if (loggerPatterns.some(pattern => pattern.test(callText))) {
1031
+ if (this.debug) {
1032
+ console.log(`📝 [S017-Symbol] Detected logger call: ${callText}()`);
1033
+ }
1034
+ return true;
1035
+ }
1036
+ }
1037
+ parent = parent.getParent();
1038
+ }
1039
+
1040
+ return false;
1041
+ }
1042
+
1043
+ /**
1044
+ * Check if template literal is used in config getter call
1045
+ * e.g., appConfig.get(`service.vms.${site}`)
1046
+ */
1047
+ isConfigGetterCall(template) {
1048
+ let parent = template.getParent();
1049
+
1050
+ // Walk up to find if we're in a config getter call
1051
+ while (parent) {
1052
+ if (parent.getKind() === SyntaxKind.CallExpression) {
1053
+ const callExpr = parent;
1054
+ const expression = callExpr.getExpression();
1055
+
1056
+ // Check if it's a PropertyAccessExpression (e.g., appConfig.get)
1057
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
1058
+ const propAccess = expression;
1059
+ const method = propAccess.getName();
1060
+ const object = propAccess.getExpression().getText();
1061
+
1062
+ // Check for config getters: appConfig.get, config.get, etc.
1063
+ if (method === 'get' && /config/i.test(object)) {
1064
+ if (this.debug) {
1065
+ console.log(`⚙️ [S017-Symbol] Detected config getter: ${object}.${method}()`);
1066
+ }
1067
+ return true;
1068
+ }
1069
+ }
1070
+ }
1071
+ parent = parent.getParent();
1072
+ }
1073
+
1074
+ return false;
1075
+ }
1076
+
1077
+ /**
1078
+ * Check if template literal is likely a URL construction
1079
+ */
1080
+ isUrlConstruction(template) {
1081
+ const text = template.getText();
1082
+
1083
+ // Strong URL indicators
1084
+ const strongUrlPatterns = [
1085
+ /https?:\/\//i, // Contains http:// or https://
1086
+ /\/(api|endpoint|service)\/[\w-]+/i, // Contains /api/, /endpoint/, /service/
1087
+ /\?[\w]+=[^&\s]+&[\w]+=/, // Multiple query parameters (e.g., ?where=...&limit=...)
1088
+ ];
1089
+
1090
+ // If has strong URL pattern, it's definitely a URL
1091
+ if (strongUrlPatterns.some(pattern => pattern.test(text))) {
1092
+ if (this.debug) {
1093
+ console.log(`🔗 [S017-Symbol] Detected URL construction (strong pattern)`);
1094
+ }
1095
+ return true;
1096
+ }
1097
+
1098
+ // Check for URL variable patterns in interpolation
1099
+ // e.g., ${database_base_url}, ${api_url}, ${baseUrl}
1100
+ const urlVariablePatterns = [
1101
+ /\$\{[^}]*(_url|_base_url|baseUrl|apiUrl|Base[Uu]rl|API[Uu]rl)[^}]*\}/,
1102
+ /\$\{[^}]*database[^}]*url[^}]*\}/i,
1103
+ /\$\{[^}]*service[^}]*url[^}]*\}/i,
1104
+ ];
1105
+
1106
+ if (urlVariablePatterns.some(pattern => pattern.test(text))) {
1107
+ if (this.debug) {
1108
+ console.log(`🔗 [S017-Symbol] Detected URL construction (URL variable pattern)`);
1109
+ }
1110
+ return true;
1111
+ }
1112
+
1113
+ // Weaker indicators - need more checks
1114
+ const weakUrlPatterns = [
1115
+ /\/[\w-]+\/[\w-]+\//, // Contains path segments
1116
+ /\?[\w]+=/, // Contains query parameter
1117
+ ];
1118
+
1119
+ if (weakUrlPatterns.some(pattern => pattern.test(text))) {
1120
+ // Additional check: Make sure it's not actually SQL
1121
+ // URL constructions usually don't have SELECT, INSERT, UPDATE, DELETE (but may have WHERE in query string)
1122
+ const hasSqlDML = /\b(SELECT|INSERT|UPDATE|DELETE|ALTER|CREATE|DROP)\b/i.test(text);
1123
+
1124
+ if (!hasSqlDML) {
1125
+ if (this.debug) {
1126
+ console.log(`🔗 [S017-Symbol] Detected URL construction (weak pattern + no DML)`);
1127
+ }
1128
+ return true;
1129
+ }
1130
+ }
1131
+
1132
+ return false;
1133
+ }
1134
+
1135
+ /**
1136
+ * Check if a call expression is an HTTP client call
1137
+ */
1138
+ isHttpClientCall(callExpr) {
1139
+ const expression = callExpr.getExpression();
1140
+
1141
+ // HTTP methods
1142
+ const httpMethods = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'request'];
1143
+
1144
+ // Check for property access like axios.get(), fetch(), apiClient.post(), this.get()
1145
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
1146
+ const propAccess = expression;
1147
+ const method = propAccess.getName();
1148
+
1149
+ // Check if method is an HTTP method
1150
+ if (!httpMethods.includes(method)) {
1151
+ return false;
1152
+ }
1153
+
1154
+ // Get the full object path (handles nested like apiClient.xxx.get)
1155
+ let fullObject = propAccess.getExpression().getText();
1156
+
1157
+ // Check for config getters (appConfig.get, config.get, etc.) - NOT HTTP clients
1158
+ if (/config/i.test(fullObject) && method === 'get') {
1159
+ if (this.debug) {
1160
+ console.log(`⚙️ [S017-Symbol] Skipping config getter: ${fullObject}.${method}()`);
1161
+ }
1162
+ return false;
1163
+ }
1164
+
1165
+ // Check if the root object (first part) matches HTTP client patterns
1166
+ // For "apiClient.maintenanceCustomerManagement.get()", check "apiClient"
1167
+ const rootObject = fullObject.split('.')[0];
1168
+ const isHttpObject = this.httpClientPatterns.some(pattern => pattern.test(rootObject));
1169
+
1170
+ if (isHttpObject) {
1171
+ if (this.debug) {
1172
+ console.log(`🌐 [S017-Symbol] Detected HTTP client call: ${fullObject}.${method}()`);
1173
+ }
1174
+ return true;
1175
+ }
1176
+
1177
+ // For nested access, also check if any part contains API/client keywords
1178
+ if (fullObject.includes('.')) {
1179
+ const hasApiKeyword = /api|client|http|request|service/i.test(fullObject);
1180
+ if (hasApiKeyword) {
1181
+ if (this.debug) {
1182
+ console.log(`🌐 [S017-Symbol] Detected nested HTTP client call: ${fullObject}.${method}()`);
1183
+ }
1184
+ return true;
1185
+ }
1186
+ }
1187
+
1188
+ // Check for this.get/post/etc() in API class contexts
1189
+ if (rootObject === 'this') {
1190
+ // Look for class that extends BaseApi, ApiClient, HttpClient, etc.
1191
+ const classDecl = callExpr.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
1192
+ if (classDecl) {
1193
+ const extendsClause = classDecl.getExtends();
1194
+ if (extendsClause) {
1195
+ const baseClassName = extendsClause.getExpression().getText();
1196
+ // Check if base class name suggests HTTP/API client
1197
+ if (/Api|Client|Service|Http|Request/i.test(baseClassName)) {
1198
+ if (this.debug) {
1199
+ console.log(`🌐 [S017-Symbol] Detected HTTP client call in API class: this.${method}()`);
1200
+ }
1201
+ return true;
1202
+ }
1203
+ }
1204
+ }
1205
+ }
1206
+ }
1207
+
1208
+ // Check for standalone fetch()
1209
+ if (expression.getKind() === SyntaxKind.Identifier) {
1210
+ const identifier = expression.getText();
1211
+ if (['fetch', 'request'].includes(identifier)) {
1212
+ if (this.debug) {
1213
+ console.log(`🌐 [S017-Symbol] Detected HTTP client call: ${identifier}()`);
1214
+ }
1215
+ return true;
1216
+ }
1217
+ }
1218
+
1219
+ return false;
1220
+ }
1221
+
1222
+ /**
1223
+ * Check if a template literal is used in styled-components
1224
+ */
1225
+ isStyledComponentsCall(node) {
1226
+ // Check if node is inside a tagged template with 'styled' or 'css'
1227
+ let parent = node.getParent();
1228
+
1229
+ while (parent) {
1230
+ if (parent.getKind() === SyntaxKind.TaggedTemplateExpression) {
1231
+ const tag = parent.getTag().getText();
1232
+ // Check for styled.div`...`, css`...`, styled(Component)`...`
1233
+ if (tag.startsWith('styled') || tag === 'css' || tag.includes('styled.')) {
1234
+ if (this.debug) {
1235
+ console.log(`💅 [S017-Symbol] Detected styled-components usage`);
1236
+ }
1237
+ return true;
1238
+ }
1239
+ }
1240
+ parent = parent.getParent();
1241
+ }
1242
+
1243
+ return false;
1244
+ }
1245
+
1246
+ /**
1247
+ * Check if template literal is used in JSX/HTML attribute
1248
+ */
1249
+ isJsxAttribute(node) {
1250
+ let parent = node.getParent();
1251
+
1252
+ // Walk up the tree to find JSX context
1253
+ while (parent) {
1254
+ const kind = parent.getKind();
1255
+
1256
+ // Check if we're inside a JSX attribute
1257
+ if (kind === SyntaxKind.JsxAttribute || kind === SyntaxKind.JsxExpression) {
1258
+ if (this.debug) {
1259
+ console.log(`⚛️ [S017-Symbol] Detected JSX attribute context`);
1260
+ }
1261
+ return true;
1262
+ }
1263
+
1264
+ // Check if we're inside JSX element
1265
+ if (kind === SyntaxKind.JsxElement || kind === SyntaxKind.JsxSelfClosingElement || kind === SyntaxKind.JsxFragment) {
1266
+ if (this.debug) {
1267
+ console.log(`⚛️ [S017-Symbol] Detected JSX element context`);
1268
+ }
1269
+ return true;
1270
+ }
1271
+
1272
+ parent = parent.getParent();
1273
+ }
1274
+
1275
+ return false;
1276
+ }
1277
+
1278
+ /**
1279
+ * Check if template literal is used in DOM manipulation methods
1280
+ * Examples: element.insertAdjacentHTML('afterbegin', `<tr>...</tr>`)
1281
+ */
1282
+ isDomManipulation(node) {
1283
+ let parent = node.getParent();
1284
+
1285
+ // Walk up to find if we're inside a call expression
1286
+ while (parent) {
1287
+ const kind = parent.getKind();
1288
+
1289
+ // Check if parent is a call expression
1290
+ if (kind === SyntaxKind.CallExpression) {
1291
+ const callExpr = parent;
1292
+ const expression = callExpr.getExpression();
1293
+
1294
+ // Check if it's a property access (e.g., element.insertAdjacentHTML)
1295
+ if (expression.getKind() === SyntaxKind.PropertyAccessExpression) {
1296
+ const propertyAccess = expression;
1297
+ const propertyName = propertyAccess.getName();
1298
+
1299
+ // Check if the method name is a DOM manipulation method
1300
+ if (this.domManipulationMethods.includes(propertyName)) {
1301
+ if (this.debug) {
1302
+ console.log(`🌐 [S017-Symbol] Detected DOM manipulation method: ${propertyName}`);
1303
+ }
1304
+ return true;
1305
+ }
1306
+ }
1307
+ }
1308
+
1309
+ parent = parent.getParent();
1310
+ }
1311
+
1312
+ return false;
1313
+ }
1314
+
1315
+ /**
1316
+ * Check if template literal contains only schema/DDL operations with constants
1317
+ * (TypeORM migrations often use template literals for ALTER TABLE with computed column sizes)
1318
+ */
1319
+ isSchemaDDLWithConstants(template) {
1320
+ const text = template.getText();
1321
+ const upperText = text.toUpperCase();
1322
+
1323
+ // DDL keywords
1324
+ const ddlKeywords = ['ALTER TABLE', 'CREATE TABLE', 'CREATE INDEX', 'DROP TABLE', 'ADD COLUMN', 'ALTER COLUMN', 'DROP COLUMN'];
1325
+ const hasDDL = ddlKeywords.some(keyword => upperText.includes(keyword));
1326
+
1327
+ if (!hasDDL) {
1328
+ return false;
1329
+ }
1330
+
1331
+ // Check if interpolations are simple function calls or constants (not user data)
1332
+ const spans = template.getTemplateSpans();
1333
+ for (const span of spans) {
1334
+ const expr = span.getExpression();
1335
+ const exprText = expr.getText();
1336
+
1337
+ // Allow function calls like encryptedLength(128)
1338
+ if (expr.getKind() === SyntaxKind.CallExpression) {
1339
+ continue;
1340
+ }
1341
+
1342
+ // Allow simple constants
1343
+ if (/^[A-Z_][A-Z0-9_]*$/.test(exprText)) {
1344
+ continue;
1345
+ }
1346
+
1347
+ // Otherwise, it might be risky
1348
+ return false;
1349
+ }
1350
+
1351
+ return true;
1352
+ }
1353
+
684
1354
  /**
685
1355
  * Analyze universal SQL patterns regardless of imports
686
1356
  */
687
1357
  analyzeUniversalSqlPatterns(sourceFile, filePath) {
688
1358
  const violations = [];
689
1359
 
1360
+ // Skip files in /queries/ or /query/ folders - these are SQL query definition files
1361
+ if (filePath.includes('/queries/') || filePath.includes('/query/') || filePath.includes('\\queries\\') || filePath.includes('\\query\\')) {
1362
+ if (this.debug) {
1363
+ console.log(`⏭️ [S017-Symbol] Skipping query definition file: ${filePath}`);
1364
+ }
1365
+ return violations;
1366
+ }
1367
+
1368
+ // Skip Swagger/API documentation files (validation messages, not SQL)
1369
+ if (this.isSwaggerFile(filePath)) {
1370
+ if (this.debug) {
1371
+ console.log(`⏭️ [S017-Symbol] Skipping Swagger documentation file: ${filePath}`);
1372
+ }
1373
+ return violations;
1374
+ }
1375
+
690
1376
  sourceFile.forEachDescendant((node) => {
691
1377
  // Check template literals with SQL keywords
692
1378
  if (node.getKind() === SyntaxKind.TemplateExpression) {
693
1379
  const template = node;
694
1380
  const text = template.getText();
695
1381
 
1382
+ // Skip styled-components and CSS-in-JS
1383
+ if (this.isStyledComponentsCall(template)) {
1384
+ return; // Skip - this is styled-components
1385
+ }
1386
+
1387
+ // Skip JSX/HTML attributes (e.g., key={`select-${id}`}, data-testid={`...`})
1388
+ if (this.isJsxAttribute(template)) {
1389
+ return; // Skip - this is JSX attribute
1390
+ }
1391
+
1392
+ // Skip DOM manipulation methods (e.g., element.insertAdjacentHTML(`<tr>...</tr>`))
1393
+ if (this.isDomManipulation(template)) {
1394
+ return; // Skip - this is DOM manipulation with HTML
1395
+ }
1396
+
1397
+ // Skip validation messages (e.g., "must be one of the following values: ...")
1398
+ if (this.isValidationMessage(template)) {
1399
+ return; // Skip - this is validation message
1400
+ }
1401
+
1402
+ // Skip logger/console calls (e.g., logger.error(`Error: ${message}`))
1403
+ if (this.isLoggerCall(template)) {
1404
+ return; // Skip - this is logger call
1405
+ }
1406
+
1407
+ // Skip config getter calls (e.g., appConfig.get(`service.vms.${site}`))
1408
+ if (this.isConfigGetterCall(template)) {
1409
+ return; // Skip - this is config getter
1410
+ }
1411
+
1412
+ // Skip URL construction (e.g., `${baseUrl}/api/${id}?where=${filter}`)
1413
+ if (this.isUrlConstruction(template)) {
1414
+ return; // Skip - this is URL construction
1415
+ }
1416
+
1417
+ // IMPROVEMENT 2: Check if this is a parameterized query with safe conditional fragments
1418
+ if (this.isParameterizedQuery(template)) {
1419
+ if (this.debug) {
1420
+ console.log(`✅ [S017-Symbol] Parameterized query with safe fragments at line ${template.getStartLineNumber()}`);
1421
+ }
1422
+ return; // Skip - this is a safe parameterized query
1423
+ }
1424
+
1425
+ // PHASE 3C: Check if inside static array iteration
1426
+ if (this.isStaticArrayIteration(template)) {
1427
+ if (this.debug) {
1428
+ console.log(`✅ [S017-Symbol] Static array iteration at line ${template.getStartLineNumber()}`);
1429
+ }
1430
+ return; // Skip - safe static array iteration
1431
+ }
1432
+
696
1433
  // Check if template contains SQL keywords and has interpolation
697
1434
  const containsSql = this.containsSqlKeywords(text);
698
1435
 
699
1436
  if (containsSql && template.getTemplateSpans().length > 0) {
1437
+ // Check if all interpolations are safe SQL fragments/constants
1438
+ if (this.hasOnlySafeSqlFragments(template)) {
1439
+ if (this.debug) {
1440
+ console.log(`✅ [S017-Symbol] Safe SQL fragment composition at line ${template.getStartLineNumber()}`);
1441
+ }
1442
+ return; // Skip - this is safe SQL fragment composition
1443
+ }
1444
+
1445
+ // Check if this is QueryBuilder result wrapping
1446
+ if (this.isQueryBuilderWrapping(template, sourceFile)) {
1447
+ if (this.debug) {
1448
+ console.log(`✅ [S017-Symbol] QueryBuilder wrapping at line ${template.getStartLineNumber()}`);
1449
+ }
1450
+ return; // Skip - QueryBuilder result wrapping
1451
+ }
1452
+
1453
+ // Check if this is a schema DDL operation in a migration file with only constants
1454
+ if (this.isMigrationFile(filePath) && this.isSchemaDDLWithConstants(template)) {
1455
+ if (this.debug) {
1456
+ console.log(`✅ [S017-Symbol] Safe DDL operation in migration at line ${template.getStartLineNumber()}`);
1457
+ }
1458
+ return; // Skip - safe DDL with constants in migration
1459
+ }
1460
+
700
1461
  violations.push({
701
1462
  ruleId: this.ruleId,
702
1463
  severity: "error",
@@ -731,6 +1492,14 @@ class S017SymbolBasedAnalyzer {
731
1492
  const hasSqlKeyword = this.containsSqlKeywords(fullText);
732
1493
 
733
1494
  if (hasSqlKeyword) {
1495
+ // IMPROVEMENT 3: Skip validation messages in binary expressions
1496
+ if (this.isValidationMessage(binExpr)) {
1497
+ if (this.debug) {
1498
+ console.log(`✅ [S017-Symbol] Skipping validation message at line ${binExpr.getStartLineNumber()}`);
1499
+ }
1500
+ return; // Skip - this is validation message
1501
+ }
1502
+
734
1503
  violations.push({
735
1504
  ruleId: this.ruleId,
736
1505
  severity: "error",
@@ -759,6 +1528,382 @@ class S017SymbolBasedAnalyzer {
759
1528
  return violations;
760
1529
  }
761
1530
 
1531
+ /**
1532
+ * IMPROVEMENT 1: Check if file is a test file
1533
+ */
1534
+ isTestFile(filePath) {
1535
+ return (
1536
+ filePath.includes('.test.') ||
1537
+ filePath.includes('.spec.') ||
1538
+ filePath.includes('__tests__/') ||
1539
+ filePath.includes('__mocks__/') ||
1540
+ filePath.includes('/test/') ||
1541
+ filePath.includes('/tests/')
1542
+ );
1543
+ }
1544
+
1545
+ /**
1546
+ * PHASE 3C IMPROVEMENT 1: Check if file is a database migration file
1547
+ * Migration files typically contain DDL statements that are safe
1548
+ */
1549
+ isMigrationFile(filePath) {
1550
+ return (
1551
+ filePath.includes('/migrations/') ||
1552
+ filePath.includes('\\migrations\\') ||
1553
+ /\d{13,}-[A-Z]/.test(filePath) // Timestamp pattern like 1721804767710-AddColumn
1554
+ );
1555
+ }
1556
+
1557
+ /**
1558
+ * PHASE 3C IMPROVEMENT 2: Check if template literal is inside a static array iteration
1559
+ * Example: ['col1', 'col2'].forEach(col => query(`DROP COLUMN ${col}`))
1560
+ */
1561
+ isStaticArrayIteration(node) {
1562
+ let parent = node.getParent();
1563
+ let depth = 0;
1564
+ const maxDepth = 5;
1565
+
1566
+ while (parent && depth < maxDepth) {
1567
+ const kind = parent.getKind();
1568
+
1569
+ // Check for forEach/map/filter calls
1570
+ if (kind === SyntaxKind.CallExpression) {
1571
+ const expr = parent.getExpression();
1572
+ const text = expr?.getText() || '';
1573
+
1574
+ // Check if it's .forEach(), .map(), .filter() etc.
1575
+ if (text.includes('.forEach') || text.includes('.map') || text.includes('.filter')) {
1576
+ // Get the object being called on (the array)
1577
+ const arrayExpr = expr.getExpression?.();
1578
+
1579
+ if (arrayExpr) {
1580
+ const arrayKind = arrayExpr.getKind();
1581
+
1582
+ // Check if it's a static array literal: ['a', 'b', 'c']
1583
+ if (arrayKind === SyntaxKind.ArrayLiteralExpression) {
1584
+ if (this.debug) {
1585
+ console.log(`✅ [S017-Symbol] Found static array iteration`);
1586
+ }
1587
+ return true;
1588
+ }
1589
+ }
1590
+ }
1591
+ }
1592
+
1593
+ parent = parent.getParent();
1594
+ depth++;
1595
+ }
1596
+
1597
+ return false;
1598
+ }
1599
+
1600
+ /**
1601
+ * PHASE 3C IMPROVEMENT 3: Check if variable is a query fragment
1602
+ * Query fragments are intermediate variables that build parts of SQL queries
1603
+ */
1604
+ isQueryFragmentVariable(node) {
1605
+ const varName = node.getName?.();
1606
+ if (!varName) return false;
1607
+
1608
+ // Check if variable name suggests it's a query fragment
1609
+ const fragmentPatterns = [
1610
+ /query/i,
1611
+ /clause/i,
1612
+ /condition/i,
1613
+ /join/i,
1614
+ /where/i,
1615
+ /select/i,
1616
+ /from/i,
1617
+ /sql/i
1618
+ ];
1619
+
1620
+ const matchesPattern = fragmentPatterns.some(p => p.test(varName));
1621
+
1622
+ if (matchesPattern) {
1623
+ // Check if the initializer is a parameterized query
1624
+ const initializer = node.getInitializer();
1625
+ if (initializer && initializer.getKind() === SyntaxKind.TemplateExpression) {
1626
+ if (this.isParameterizedQuery(initializer)) {
1627
+ if (this.debug) {
1628
+ console.log(`✅ [S017-Symbol] Query fragment variable detected: ${varName}`);
1629
+ }
1630
+ return true;
1631
+ }
1632
+ }
1633
+ }
1634
+
1635
+ return false;
1636
+ }
1637
+
1638
+ /**
1639
+ * PHASE 3C IMPROVEMENT 4: Check if this is a NoSQL query (CosmosDB, MongoDB, etc.)
1640
+ * NoSQL injection is a different security concern than SQL injection
1641
+ */
1642
+ isNoSqlQuery(node) {
1643
+ // Get the method name being called
1644
+ const methodName = this.getMethodName(node);
1645
+
1646
+ const noSqlMethods = [
1647
+ 'findByRawQueryWithPagination',
1648
+ 'findByStateQueryType',
1649
+ 'findByStateQueryTypeWithPagination',
1650
+ 'executeCosmosQuery',
1651
+ 'queryDocuments',
1652
+ 'cosmosQuery',
1653
+ 'mongoFind',
1654
+ 'mongoAggregate',
1655
+ 'findOne',
1656
+ 'findMany',
1657
+ 'aggregate'
1658
+ ];
1659
+
1660
+ const isNoSql = noSqlMethods.some(m => methodName.includes(m));
1661
+
1662
+ if (isNoSql && this.debug) {
1663
+ console.log(`✅ [S017-Symbol] NoSQL query detected: ${methodName}`);
1664
+ }
1665
+
1666
+ return isNoSql;
1667
+ }
1668
+
1669
+ /**
1670
+ * Helper: Get method name from a call expression
1671
+ */
1672
+ getMethodName(node) {
1673
+ let current = node;
1674
+ let depth = 0;
1675
+ const maxDepth = 3;
1676
+
1677
+ while (current && depth < maxDepth) {
1678
+ if (current.getKind() === SyntaxKind.CallExpression) {
1679
+ const expr = current.getExpression();
1680
+ return expr?.getText() || '';
1681
+ }
1682
+ current = current.getParent();
1683
+ depth++;
1684
+ }
1685
+
1686
+ return '';
1687
+ }
1688
+
1689
+ /**
1690
+ * IMPROVEMENT 2: Check if template literal uses parameterized queries
1691
+ * Detects patterns like :paramName, $1, $2, or ? placeholders
1692
+ */
1693
+ isParameterizedQuery(template) {
1694
+ const text = template.getText();
1695
+
1696
+ // Check for TypeORM-style named parameters (:paramName)
1697
+ const hasNamedParams = /:[\w]+/.test(text);
1698
+
1699
+ // Check for PostgreSQL-style positional parameters ($1, $2, etc.)
1700
+ const hasPositionalParams = /\$\d+/.test(text);
1701
+
1702
+ // Check for MySQL/SQLite-style placeholders (?)
1703
+ const hasQuestionMarks = /\?/.test(text);
1704
+
1705
+ if (!hasNamedParams && !hasPositionalParams && !hasQuestionMarks) {
1706
+ return false; // No parameterization found
1707
+ }
1708
+
1709
+ // If has parameters, check if interpolated expressions are safe
1710
+ const templateSpans = template.getTemplateSpans();
1711
+ if (templateSpans.length === 0) {
1712
+ return true; // No interpolation, just static parameterized query
1713
+ }
1714
+
1715
+ // Check each interpolated expression
1716
+ for (const span of templateSpans) {
1717
+ const expr = span.getExpression();
1718
+ const kind = expr.getKind();
1719
+
1720
+ // Safe pattern 1: Conditional expression with SQL fragments
1721
+ if (kind === SyntaxKind.ConditionalExpression) {
1722
+ const whenTrue = expr.getWhenTrue()?.getText() || '';
1723
+ const whenFalse = expr.getWhenFalse()?.getText() || '';
1724
+
1725
+ // PHASE 2 IMPROVEMENT: Enhanced SQL fragment detection
1726
+ const isSqlFragment = (str) => {
1727
+ const trimmed = str.trim();
1728
+
1729
+ // Empty string is always safe
1730
+ if (trimmed === '' || trimmed === "''" || trimmed === '""' || trimmed === '``') {
1731
+ return true;
1732
+ }
1733
+
1734
+ // Remove surrounding quotes to analyze content (but keep backticks for template literals)
1735
+ let unquoted = trimmed.replace(/^['"]|['"]$/g, '');
1736
+
1737
+ // Special handling for nested template literals (e.g., `AND date >= :date`)
1738
+ // These are common in TypeORM conditional queries
1739
+ if (unquoted.startsWith('`') && unquoted.endsWith('`')) {
1740
+ unquoted = unquoted.slice(1, -1); // Remove outer backticks
1741
+ }
1742
+
1743
+ // Check if contains SQL keywords
1744
+ const hasSqlKeywords = /\b(AND|OR|WHERE|JOIN|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|ORDER BY|GROUP BY|HAVING|LIMIT|OFFSET|UNION|SELECT|FROM|SET|VALUES|INSERT|UPDATE|DELETE|TO_DATE|CAST|CONVERT)\b/i.test(unquoted);
1745
+
1746
+ if (!hasSqlKeywords) {
1747
+ // No SQL keywords, likely safe string
1748
+ return true;
1749
+ }
1750
+
1751
+ // Has SQL keywords - check if parameterized
1752
+ const hasParams = /:[\w]+|\$\d+|\?/.test(unquoted);
1753
+
1754
+ if (!hasParams) {
1755
+ // SQL keywords but no parameters - could be unsafe
1756
+ // However, check if it's just static SQL syntax (no variables at all)
1757
+ // Pattern: pure SQL like 'ORDER BY created_at DESC' or 'AND NOT del_flg'
1758
+ const hasVariablePlaceholders = /\$\{[^}]+\}/.test(str); // Template interpolation like ${var}
1759
+
1760
+ if (hasVariablePlaceholders) {
1761
+ return false; // Unsafe: has SQL keywords and variable interpolation
1762
+ }
1763
+
1764
+ // Check for common safe static SQL patterns
1765
+ const isSafeStaticSql = (
1766
+ /^\s*(AND|OR)\s+(NOT\s+)?\w+\s*$/i.test(unquoted) || // 'AND NOT del_flg'
1767
+ /ORDER\s+BY\s+[\w.]+\s+(ASC|DESC)?/i.test(unquoted) || // 'ORDER BY col ASC'
1768
+ /GROUP\s+BY\s+[\w.,\s]+/i.test(unquoted) || // 'GROUP BY col1, col2'
1769
+ /LIMIT\s+\d+/i.test(unquoted) || // 'LIMIT 10'
1770
+ /OFFSET\s+\d+/i.test(unquoted) // 'OFFSET 20'
1771
+ );
1772
+
1773
+ if (isSafeStaticSql) {
1774
+ return true; // Safe static SQL clause
1775
+ }
1776
+
1777
+ // No variable interpolation in this branch - likely safe static SQL
1778
+ return true;
1779
+ }
1780
+
1781
+ // Has SQL keywords AND parameters - check if safe
1782
+ // Safe patterns:
1783
+ // 1. 'AND column = :param'
1784
+ // 2. 'WHERE id IN (:ids)'
1785
+ // 3. `AND date >= :dateFrom`
1786
+ // 4. `AND date >= TO_DATE(:dateStr, 'YYYY/MM/DD')`
1787
+
1788
+ // Unsafe patterns (has direct variable interpolation):
1789
+ // 1. 'AND column = ' + variable (this would be binary expression, not template)
1790
+ // 2. `AND ${columnName} = :value` (dynamic column name)
1791
+
1792
+ // Check for unsafe dynamic column/table names
1793
+ const hasDynamicIdentifiers = /\$\{[\w.]+\}\s*=/i.test(str) || // ${col} =
1794
+ /=\s*\$\{[\w.]+\}(?!:)/i.test(str) || // = ${val} (not := or ::)
1795
+ /FROM\s+\$\{/i.test(str) || // FROM ${table}
1796
+ /JOIN\s+\$\{/i.test(str); // JOIN ${table}
1797
+
1798
+ if (hasDynamicIdentifiers) {
1799
+ return false; // Unsafe: dynamic column or table names
1800
+ }
1801
+
1802
+ // Passed all checks - this is a safe parameterized SQL fragment
1803
+ return true;
1804
+ };
1805
+
1806
+ if (isSqlFragment(whenTrue) && isSqlFragment(whenFalse)) {
1807
+ continue; // Safe conditional SQL fragment
1808
+ }
1809
+ }
1810
+
1811
+ // Safe pattern 2: Property access (e.g., obj.length, array?.length)
1812
+ if (kind === SyntaxKind.PropertyAccessExpression) {
1813
+ const propAccess = expr.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
1814
+ const propName = propAccess.getName();
1815
+ if (propName === 'length') {
1816
+ continue; // .length check is safe for conditionals
1817
+ }
1818
+ }
1819
+
1820
+ // Safe pattern 3: Optional chaining property access (e.g., obj?.length)
1821
+ if (kind === SyntaxKind.PropertyAccessExpression) {
1822
+ const text = expr.getText();
1823
+ if (text.includes('?.length')) {
1824
+ continue; // Optional chaining .length is safe
1825
+ }
1826
+ }
1827
+
1828
+ // Safe pattern 4: Binary expression for length checks (e.g., arr.length > 0)
1829
+ if (kind === SyntaxKind.BinaryExpression) {
1830
+ const binExpr = expr;
1831
+ const text = binExpr.getText();
1832
+ // Check if it's a length comparison
1833
+ if (/\.length\s*[><=!]+\s*\d+/.test(text) || /\d+\s*[><=!]+\s*\.length/.test(text)) {
1834
+ continue; // Length comparison is safe
1835
+ }
1836
+ }
1837
+
1838
+ // Safe pattern 5: Logical expressions (e.g., defined !== undefined && defined !== null)
1839
+ if (kind === SyntaxKind.BinaryExpression) {
1840
+ const binExpr = expr;
1841
+ const text = binExpr.getText();
1842
+ // Check for null/undefined checks
1843
+ if (/(undefined|null|!==|===)/.test(text) && !/SELECT|INSERT|UPDATE|DELETE|WHERE|FROM/i.test(text)) {
1844
+ continue; // Null/undefined check is safe
1845
+ }
1846
+ }
1847
+
1848
+ // If we get here, the interpolation might be unsafe
1849
+ return false;
1850
+ }
1851
+
1852
+ return true; // All checks passed
1853
+ }
1854
+
1855
+ /**
1856
+ * IMPROVEMENT 3: Check if node is inside a validation message context
1857
+ */
1858
+ isValidationMessage(node) {
1859
+ let parent = node.getParent();
1860
+ let depth = 0;
1861
+ const maxDepth = 10;
1862
+
1863
+ while (parent && depth < maxDepth) {
1864
+ const kind = parent.getKind();
1865
+
1866
+ // Check if inside a validator or constraint class
1867
+ if (kind === SyntaxKind.ClassDeclaration) {
1868
+ const className = parent.getName?.() || '';
1869
+ if (
1870
+ className.includes('Validator') ||
1871
+ className.includes('Constraint') ||
1872
+ className.includes('Decorator') ||
1873
+ className.includes('ValidationRule')
1874
+ ) {
1875
+ return true;
1876
+ }
1877
+ }
1878
+
1879
+ // Check if assigned to 'message' or 'defaultMessage' property
1880
+ if (kind === SyntaxKind.PropertyAssignment) {
1881
+ const propName = parent.getName?.() || '';
1882
+ if (propName === 'message' || propName === 'defaultMessage' || propName === 'description') {
1883
+ return true;
1884
+ }
1885
+ }
1886
+
1887
+ // Check if inside decorator
1888
+ if (kind === SyntaxKind.Decorator) {
1889
+ return true;
1890
+ }
1891
+
1892
+ parent = parent.getParent();
1893
+ depth++;
1894
+ }
1895
+
1896
+ // Check text content for validation message patterns
1897
+ const text = node.getText();
1898
+ for (const pattern of this.validationMessagePatterns) {
1899
+ if (pattern.test(text)) {
1900
+ return true;
1901
+ }
1902
+ }
1903
+
1904
+ return false;
1905
+ }
1906
+
762
1907
  /**
763
1908
  * Get analyzer metadata
764
1909
  */