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