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