@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.
- package/config/rules/enhanced-rules-registry.json +2 -1
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +1 -1
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- 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
|
-
|
|
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
|
*/
|