sqlspec 0.12.2__py3-none-any.whl → 0.13.1__py3-none-any.whl
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.
Potentially problematic release.
This version of sqlspec might be problematic. Click here for more details.
- sqlspec/_sql.py +21 -180
- sqlspec/adapters/adbc/config.py +10 -12
- sqlspec/adapters/adbc/driver.py +120 -118
- sqlspec/adapters/aiosqlite/config.py +16 -3
- sqlspec/adapters/aiosqlite/driver.py +100 -130
- sqlspec/adapters/asyncmy/config.py +17 -4
- sqlspec/adapters/asyncmy/driver.py +123 -135
- sqlspec/adapters/asyncpg/config.py +17 -29
- sqlspec/adapters/asyncpg/driver.py +98 -140
- sqlspec/adapters/bigquery/config.py +4 -5
- sqlspec/adapters/bigquery/driver.py +125 -167
- sqlspec/adapters/duckdb/config.py +3 -6
- sqlspec/adapters/duckdb/driver.py +114 -111
- sqlspec/adapters/oracledb/config.py +32 -5
- sqlspec/adapters/oracledb/driver.py +242 -259
- sqlspec/adapters/psqlpy/config.py +18 -9
- sqlspec/adapters/psqlpy/driver.py +118 -93
- sqlspec/adapters/psycopg/config.py +44 -31
- sqlspec/adapters/psycopg/driver.py +283 -236
- sqlspec/adapters/sqlite/config.py +3 -3
- sqlspec/adapters/sqlite/driver.py +103 -97
- sqlspec/config.py +0 -4
- sqlspec/driver/_async.py +89 -98
- sqlspec/driver/_common.py +52 -17
- sqlspec/driver/_sync.py +81 -105
- sqlspec/driver/connection.py +207 -0
- sqlspec/driver/mixins/_csv_writer.py +91 -0
- sqlspec/driver/mixins/_pipeline.py +38 -49
- sqlspec/driver/mixins/_result_utils.py +27 -9
- sqlspec/driver/mixins/_storage.py +67 -181
- sqlspec/driver/mixins/_type_coercion.py +3 -4
- sqlspec/driver/parameters.py +138 -0
- sqlspec/exceptions.py +10 -2
- sqlspec/extensions/aiosql/adapter.py +0 -10
- sqlspec/extensions/litestar/handlers.py +0 -1
- sqlspec/extensions/litestar/plugin.py +0 -3
- sqlspec/extensions/litestar/providers.py +0 -14
- sqlspec/loader.py +25 -90
- sqlspec/protocols.py +542 -0
- sqlspec/service/__init__.py +3 -2
- sqlspec/service/_util.py +147 -0
- sqlspec/service/base.py +1116 -9
- sqlspec/statement/builder/__init__.py +42 -32
- sqlspec/statement/builder/_ddl_utils.py +0 -10
- sqlspec/statement/builder/_parsing_utils.py +10 -4
- sqlspec/statement/builder/base.py +67 -22
- sqlspec/statement/builder/column.py +283 -0
- sqlspec/statement/builder/ddl.py +91 -67
- sqlspec/statement/builder/delete.py +23 -7
- sqlspec/statement/builder/insert.py +29 -15
- sqlspec/statement/builder/merge.py +4 -4
- sqlspec/statement/builder/mixins/_aggregate_functions.py +113 -14
- sqlspec/statement/builder/mixins/_common_table_expr.py +0 -1
- sqlspec/statement/builder/mixins/_delete_from.py +1 -1
- sqlspec/statement/builder/mixins/_from.py +10 -8
- sqlspec/statement/builder/mixins/_group_by.py +0 -1
- sqlspec/statement/builder/mixins/_insert_from_select.py +0 -1
- sqlspec/statement/builder/mixins/_insert_values.py +0 -2
- sqlspec/statement/builder/mixins/_join.py +20 -13
- sqlspec/statement/builder/mixins/_limit_offset.py +3 -3
- sqlspec/statement/builder/mixins/_merge_clauses.py +3 -4
- sqlspec/statement/builder/mixins/_order_by.py +2 -2
- sqlspec/statement/builder/mixins/_pivot.py +4 -7
- sqlspec/statement/builder/mixins/_select_columns.py +6 -5
- sqlspec/statement/builder/mixins/_unpivot.py +6 -9
- sqlspec/statement/builder/mixins/_update_from.py +2 -1
- sqlspec/statement/builder/mixins/_update_set.py +11 -8
- sqlspec/statement/builder/mixins/_where.py +61 -34
- sqlspec/statement/builder/select.py +32 -17
- sqlspec/statement/builder/update.py +25 -11
- sqlspec/statement/filters.py +39 -14
- sqlspec/statement/parameter_manager.py +220 -0
- sqlspec/statement/parameters.py +210 -79
- sqlspec/statement/pipelines/__init__.py +166 -23
- sqlspec/statement/pipelines/analyzers/_analyzer.py +21 -20
- sqlspec/statement/pipelines/context.py +35 -39
- sqlspec/statement/pipelines/transformers/__init__.py +2 -3
- sqlspec/statement/pipelines/transformers/_expression_simplifier.py +19 -187
- sqlspec/statement/pipelines/transformers/_literal_parameterizer.py +628 -58
- sqlspec/statement/pipelines/transformers/_remove_comments_and_hints.py +76 -0
- sqlspec/statement/pipelines/validators/_dml_safety.py +33 -18
- sqlspec/statement/pipelines/validators/_parameter_style.py +87 -14
- sqlspec/statement/pipelines/validators/_performance.py +38 -23
- sqlspec/statement/pipelines/validators/_security.py +39 -62
- sqlspec/statement/result.py +37 -129
- sqlspec/statement/splitter.py +0 -12
- sqlspec/statement/sql.py +863 -391
- sqlspec/statement/sql_compiler.py +140 -0
- sqlspec/storage/__init__.py +10 -2
- sqlspec/storage/backends/fsspec.py +53 -8
- sqlspec/storage/backends/obstore.py +15 -19
- sqlspec/storage/capabilities.py +101 -0
- sqlspec/storage/registry.py +56 -83
- sqlspec/typing.py +6 -434
- sqlspec/utils/cached_property.py +25 -0
- sqlspec/utils/correlation.py +0 -2
- sqlspec/utils/logging.py +0 -6
- sqlspec/utils/sync_tools.py +0 -4
- sqlspec/utils/text.py +0 -5
- sqlspec/utils/type_guards.py +892 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/METADATA +1 -1
- sqlspec-0.13.1.dist-info/RECORD +150 -0
- sqlspec/statement/builder/protocols.py +0 -20
- sqlspec/statement/pipelines/base.py +0 -315
- sqlspec/statement/pipelines/result_types.py +0 -41
- sqlspec/statement/pipelines/transformers/_remove_comments.py +0 -66
- sqlspec/statement/pipelines/transformers/_remove_hints.py +0 -81
- sqlspec/statement/pipelines/validators/base.py +0 -67
- sqlspec/storage/protocol.py +0 -173
- sqlspec-0.12.2.dist-info/RECORD +0 -145
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/WHEEL +0 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/licenses/LICENSE +0 -0
- {sqlspec-0.12.2.dist-info → sqlspec-0.13.1.dist-info}/licenses/NOTICE +0 -0
|
@@ -11,8 +11,9 @@ from sqlglot import exp
|
|
|
11
11
|
from sqlglot.expressions import EQ, Binary, Func, Literal, Or, Subquery, Union
|
|
12
12
|
|
|
13
13
|
from sqlspec.exceptions import RiskLevel
|
|
14
|
-
from sqlspec.
|
|
15
|
-
from sqlspec.statement.pipelines.
|
|
14
|
+
from sqlspec.protocols import ProcessorProtocol
|
|
15
|
+
from sqlspec.statement.pipelines.context import ValidationError
|
|
16
|
+
from sqlspec.utils.type_guards import has_expressions, has_sql_method
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from sqlspec.statement.pipelines.context import SQLProcessingContext
|
|
@@ -178,10 +179,26 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
178
179
|
with contextlib.suppress(re.error):
|
|
179
180
|
self._compiled_patterns[f"custom_suspicious_{i}"] = re.compile(pattern, re.IGNORECASE)
|
|
180
181
|
|
|
181
|
-
def
|
|
182
|
+
def add_error(
|
|
183
|
+
self,
|
|
184
|
+
context: "SQLProcessingContext",
|
|
185
|
+
message: str,
|
|
186
|
+
code: str,
|
|
187
|
+
risk_level: RiskLevel,
|
|
188
|
+
expression: "Optional[exp.Expression]" = None,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""Add a validation error to the context."""
|
|
191
|
+
error = ValidationError(
|
|
192
|
+
message=message, code=code, risk_level=risk_level, processor=self.__class__.__name__, expression=expression
|
|
193
|
+
)
|
|
194
|
+
context.validation_errors.append(error)
|
|
195
|
+
|
|
196
|
+
def process(
|
|
197
|
+
self, expression: Optional[exp.Expression], context: "SQLProcessingContext"
|
|
198
|
+
) -> Optional[exp.Expression]:
|
|
182
199
|
"""Process the SQL expression and detect security issues in a single pass."""
|
|
183
200
|
if not context.current_expression:
|
|
184
|
-
return
|
|
201
|
+
return None
|
|
185
202
|
|
|
186
203
|
security_issues: list[SecurityIssue] = []
|
|
187
204
|
visited_nodes: set[int] = set()
|
|
@@ -198,17 +215,14 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
198
215
|
if isinstance(node, (Subquery, exp.Select)):
|
|
199
216
|
nesting_depth += 1
|
|
200
217
|
|
|
201
|
-
# Check injection patterns (enhanced AST-based)
|
|
202
218
|
if self.config.check_injection:
|
|
203
219
|
injection_issues = self._check_injection_patterns(node, context)
|
|
204
220
|
security_issues.extend(injection_issues)
|
|
205
221
|
|
|
206
|
-
# Check tautology conditions (enhanced)
|
|
207
222
|
if self.config.check_tautology:
|
|
208
223
|
tautology_issues = self._check_tautology_patterns(node, context)
|
|
209
224
|
security_issues.extend(tautology_issues)
|
|
210
225
|
|
|
211
|
-
# Check suspicious keywords/functions
|
|
212
226
|
if self.config.check_keywords:
|
|
213
227
|
keyword_issues = self._check_suspicious_keywords(node, context)
|
|
214
228
|
security_issues.extend(keyword_issues)
|
|
@@ -223,7 +237,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
223
237
|
structural_issues = self._check_structural_attacks(node, context)
|
|
224
238
|
security_issues.extend(structural_issues)
|
|
225
239
|
|
|
226
|
-
# Check combined attack patterns
|
|
227
240
|
if self.config.check_combined_patterns and security_issues:
|
|
228
241
|
combined_issues = self._check_combined_patterns(context.current_expression, security_issues)
|
|
229
242
|
security_issues.extend(combined_issues)
|
|
@@ -242,11 +255,9 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
242
255
|
)
|
|
243
256
|
)
|
|
244
257
|
|
|
245
|
-
# Determine overall risk level
|
|
246
258
|
if security_issues:
|
|
247
259
|
max(issue.risk_level for issue in security_issues)
|
|
248
260
|
|
|
249
|
-
# Create validation errors
|
|
250
261
|
for issue in security_issues:
|
|
251
262
|
error = ValidationError(
|
|
252
263
|
message=issue.description,
|
|
@@ -278,9 +289,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
278
289
|
issue for issue in security_issues if issue.confidence >= self.config.min_confidence_threshold
|
|
279
290
|
]
|
|
280
291
|
|
|
281
|
-
# Update validation result with filtered issues
|
|
282
292
|
if filtered_issues != security_issues:
|
|
283
|
-
# Clear previous errors and add filtered ones
|
|
284
293
|
context.validation_errors = []
|
|
285
294
|
for issue in filtered_issues:
|
|
286
295
|
error = ValidationError(
|
|
@@ -292,7 +301,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
292
301
|
)
|
|
293
302
|
context.validation_errors.append(error)
|
|
294
303
|
|
|
295
|
-
# Update metadata with filtered issues
|
|
296
304
|
context.metadata["security_validator"] = {
|
|
297
305
|
"security_issues": filtered_issues,
|
|
298
306
|
"total_issues_found": len(security_issues),
|
|
@@ -312,13 +320,14 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
312
320
|
},
|
|
313
321
|
}
|
|
314
322
|
|
|
323
|
+
return expression
|
|
324
|
+
|
|
315
325
|
def _check_injection_patterns(
|
|
316
326
|
self, node: "exp.Expression", context: "SQLProcessingContext"
|
|
317
327
|
) -> "list[SecurityIssue]":
|
|
318
328
|
"""Check for SQL injection patterns in the node."""
|
|
319
329
|
issues: list[SecurityIssue] = []
|
|
320
330
|
|
|
321
|
-
# Check UNION-based injection
|
|
322
331
|
if isinstance(node, exp.Union):
|
|
323
332
|
union_issues = self._check_union_injection(node, context)
|
|
324
333
|
issues.extend(union_issues)
|
|
@@ -336,7 +345,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
336
345
|
)
|
|
337
346
|
)
|
|
338
347
|
|
|
339
|
-
# Check for encoded characters
|
|
340
348
|
if PATTERNS["encoded_chars"].search(sql_text) or PATTERNS["hex_encoding"].search(sql_text):
|
|
341
349
|
issues.append(
|
|
342
350
|
SecurityIssue(
|
|
@@ -349,7 +357,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
349
357
|
)
|
|
350
358
|
)
|
|
351
359
|
|
|
352
|
-
# Check for system schema access
|
|
353
360
|
if isinstance(node, exp.Table):
|
|
354
361
|
system_access = self._check_system_schema_access(node)
|
|
355
362
|
if system_access:
|
|
@@ -391,8 +398,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
391
398
|
)
|
|
392
399
|
)
|
|
393
400
|
|
|
394
|
-
|
|
395
|
-
if hasattr(union_node, "right") and isinstance(union_node.right, exp.Select):
|
|
401
|
+
if isinstance(union_node, exp.Union) and isinstance(union_node.right, exp.Select):
|
|
396
402
|
select_expr = union_node.right
|
|
397
403
|
if select_expr.expressions:
|
|
398
404
|
null_count = sum(1 for expr in select_expr.expressions if isinstance(expr, exp.Null))
|
|
@@ -417,7 +423,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
417
423
|
schema_name = table_node.db.lower() if table_node.db else ""
|
|
418
424
|
table_node.catalog.lower() if table_node.catalog else ""
|
|
419
425
|
|
|
420
|
-
# Check if schema is in allowed list
|
|
421
426
|
if schema_name in self.config.allowed_system_schemas:
|
|
422
427
|
return None
|
|
423
428
|
|
|
@@ -442,7 +447,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
442
447
|
"""Check for tautology conditions that are always true."""
|
|
443
448
|
issues: list[SecurityIssue] = []
|
|
444
449
|
|
|
445
|
-
# Check for boolean literals in WHERE conditions
|
|
446
450
|
if isinstance(node, exp.Boolean) and node.this is True:
|
|
447
451
|
issues.append(
|
|
448
452
|
SecurityIssue(
|
|
@@ -455,7 +459,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
455
459
|
)
|
|
456
460
|
)
|
|
457
461
|
|
|
458
|
-
# Check for tautological conditions
|
|
459
462
|
if isinstance(node, (exp.EQ, exp.NEQ, exp.GT, exp.LT, exp.GTE, exp.LTE)) and self._is_tautology(node):
|
|
460
463
|
issues.append(
|
|
461
464
|
SecurityIssue(
|
|
@@ -468,7 +471,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
468
471
|
)
|
|
469
472
|
)
|
|
470
473
|
|
|
471
|
-
# Check for OR 1=1 patterns
|
|
472
474
|
if isinstance(node, exp.Or):
|
|
473
475
|
or_sql = node.sql()
|
|
474
476
|
if PATTERNS["or_patterns"].search(or_sql) or PATTERNS["always_true"].search(or_sql):
|
|
@@ -494,14 +496,12 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
494
496
|
left = comparison.this
|
|
495
497
|
right = comparison.expression
|
|
496
498
|
|
|
497
|
-
# Check if comparing identical expressions
|
|
498
499
|
if self._expressions_identical(left, right):
|
|
499
500
|
if isinstance(comparison, (exp.EQ, exp.GTE, exp.LTE)):
|
|
500
501
|
return True
|
|
501
502
|
if isinstance(comparison, (exp.NEQ, exp.GT, exp.LT)):
|
|
502
503
|
return False
|
|
503
504
|
|
|
504
|
-
# Check for literal comparisons
|
|
505
505
|
if isinstance(left, exp.Literal) and isinstance(right, exp.Literal):
|
|
506
506
|
try:
|
|
507
507
|
left_val = left.this
|
|
@@ -511,7 +511,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
511
511
|
return bool(left_val == right_val)
|
|
512
512
|
if isinstance(comparison, exp.NEQ):
|
|
513
513
|
return bool(left_val != right_val)
|
|
514
|
-
# Add more comparison logic as needed
|
|
515
514
|
except Exception:
|
|
516
515
|
# Value extraction failed, can't evaluate the condition
|
|
517
516
|
logger.debug("Failed to extract values for comparison evaluation")
|
|
@@ -539,11 +538,9 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
539
538
|
"""Check for suspicious functions and keywords."""
|
|
540
539
|
issues: list[SecurityIssue] = []
|
|
541
540
|
|
|
542
|
-
# Check function calls
|
|
543
541
|
if isinstance(node, exp.Func):
|
|
544
542
|
func_name = node.name.lower() if node.name else ""
|
|
545
543
|
|
|
546
|
-
# Check if function is explicitly blocked
|
|
547
544
|
if func_name in self.config.blocked_functions:
|
|
548
545
|
issues.append(
|
|
549
546
|
SecurityIssue(
|
|
@@ -555,7 +552,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
555
552
|
recommendation=f"Function {func_name} is not allowed",
|
|
556
553
|
)
|
|
557
554
|
)
|
|
558
|
-
# Check if function is suspicious but not explicitly allowed
|
|
559
555
|
elif func_name in SUSPICIOUS_FUNCTIONS and func_name not in self.config.allowed_functions:
|
|
560
556
|
issues.append(
|
|
561
557
|
SecurityIssue(
|
|
@@ -569,7 +565,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
569
565
|
)
|
|
570
566
|
)
|
|
571
567
|
|
|
572
|
-
# Special handling for Command nodes (e.g., EXECUTE statements)
|
|
573
568
|
if isinstance(node, exp.Command):
|
|
574
569
|
# Commands are often used for dynamic SQL execution
|
|
575
570
|
command_text = str(node)
|
|
@@ -587,8 +582,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
587
582
|
)
|
|
588
583
|
)
|
|
589
584
|
|
|
590
|
-
|
|
591
|
-
if hasattr(node, "sql"):
|
|
585
|
+
if has_sql_method(node):
|
|
592
586
|
sql_text = node.sql()
|
|
593
587
|
|
|
594
588
|
# File operations
|
|
@@ -725,14 +719,13 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
725
719
|
"""
|
|
726
720
|
issues: list[SecurityIssue] = []
|
|
727
721
|
|
|
728
|
-
# Check for excessive nesting (potential injection)
|
|
729
722
|
if nesting_depth > self.config.max_nesting_depth:
|
|
730
723
|
issues.append(
|
|
731
724
|
SecurityIssue(
|
|
732
725
|
issue_type=SecurityIssueType.AST_ANOMALY,
|
|
733
726
|
risk_level=self.config.ast_anomaly_risk_level,
|
|
734
727
|
description=f"Excessive query nesting detected (depth: {nesting_depth})",
|
|
735
|
-
location=node.sql()[:100] if
|
|
728
|
+
location=node.sql()[:100] if has_sql_method(node) else str(node)[:100],
|
|
736
729
|
pattern_matched="excessive_nesting",
|
|
737
730
|
recommendation="Review query structure for potential injection",
|
|
738
731
|
ast_node_type=type(node).__name__,
|
|
@@ -741,7 +734,6 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
741
734
|
)
|
|
742
735
|
)
|
|
743
736
|
|
|
744
|
-
# Check for suspiciously long literals (potential injection payload)
|
|
745
737
|
if isinstance(node, Literal) and isinstance(node.this, str):
|
|
746
738
|
literal_length = len(str(node.this))
|
|
747
739
|
if literal_length > self.config.max_literal_length:
|
|
@@ -759,12 +751,10 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
759
751
|
)
|
|
760
752
|
)
|
|
761
753
|
|
|
762
|
-
# Check for unusual function call patterns
|
|
763
754
|
if isinstance(node, Func):
|
|
764
755
|
func_issues = self._analyze_function_anomalies(node)
|
|
765
756
|
issues.extend(func_issues)
|
|
766
757
|
|
|
767
|
-
# Check for suspicious binary operations (potential injection)
|
|
768
758
|
if isinstance(node, Binary):
|
|
769
759
|
binary_issues = self._analyze_binary_anomalies(node)
|
|
770
760
|
issues.extend(binary_issues)
|
|
@@ -777,17 +767,14 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
777
767
|
"""Check for structural attack patterns using AST analysis."""
|
|
778
768
|
issues: list[SecurityIssue] = []
|
|
779
769
|
|
|
780
|
-
# Check for UNION-based injection using AST structure
|
|
781
770
|
if isinstance(node, Union):
|
|
782
771
|
union_issues = self._analyze_union_structure(node)
|
|
783
772
|
issues.extend(union_issues)
|
|
784
773
|
|
|
785
|
-
# Check for subquery injection patterns
|
|
786
774
|
if isinstance(node, Subquery):
|
|
787
775
|
subquery_issues = self._analyze_subquery_structure(node)
|
|
788
776
|
issues.extend(subquery_issues)
|
|
789
777
|
|
|
790
|
-
# Check for OR-based injection using AST structure
|
|
791
778
|
if isinstance(node, Or):
|
|
792
779
|
or_issues = self._analyze_or_structure(node)
|
|
793
780
|
issues.extend(or_issues)
|
|
@@ -804,8 +791,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
804
791
|
|
|
805
792
|
func_name = func_node.name.lower()
|
|
806
793
|
|
|
807
|
-
|
|
808
|
-
if hasattr(func_node, "this") and isinstance(func_node.this, Func):
|
|
794
|
+
if func_node.this and isinstance(func_node.this, Func):
|
|
809
795
|
nested_func = func_node.this
|
|
810
796
|
if nested_func.name and nested_func.name.lower() in SUSPICIOUS_FUNCTIONS:
|
|
811
797
|
issues.append(
|
|
@@ -813,7 +799,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
813
799
|
issue_type=SecurityIssueType.AST_ANOMALY,
|
|
814
800
|
risk_level=RiskLevel.MEDIUM,
|
|
815
801
|
description=f"Nested suspicious function call: {nested_func.name.lower()} inside {func_name}",
|
|
816
|
-
location=func_node.sql()[:100],
|
|
802
|
+
location=func_node.sql()[:100] if has_sql_method(func_node) else str(func_node)[:100],
|
|
817
803
|
pattern_matched="nested_suspicious_function",
|
|
818
804
|
recommendation="Review nested function calls for evasion attempts",
|
|
819
805
|
ast_node_type="Func",
|
|
@@ -822,8 +808,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
822
808
|
)
|
|
823
809
|
)
|
|
824
810
|
|
|
825
|
-
|
|
826
|
-
if hasattr(func_node, "expressions") and func_node.expressions:
|
|
811
|
+
if has_expressions(func_node) and func_node.expressions:
|
|
827
812
|
arg_count = len(func_node.expressions)
|
|
828
813
|
if func_name in {"concat", "concat_ws"} and arg_count > MAX_FUNCTION_ARGS:
|
|
829
814
|
issues.append(
|
|
@@ -831,7 +816,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
831
816
|
issue_type=SecurityIssueType.AST_ANOMALY,
|
|
832
817
|
risk_level=RiskLevel.MEDIUM,
|
|
833
818
|
description=f"Excessive arguments to {func_name} function ({arg_count} args)",
|
|
834
|
-
location=func_node.sql()[:100],
|
|
819
|
+
location=func_node.sql()[:100] if has_sql_method(func_node) else str(func_node)[:100],
|
|
835
820
|
pattern_matched="excessive_function_args",
|
|
836
821
|
recommendation="Review function arguments for potential injection",
|
|
837
822
|
ast_node_type="Func",
|
|
@@ -869,8 +854,7 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
869
854
|
"""Analyze UNION structure for injection patterns."""
|
|
870
855
|
issues: list[SecurityIssue] = []
|
|
871
856
|
|
|
872
|
-
|
|
873
|
-
if hasattr(union_node, "left") and hasattr(union_node, "right"):
|
|
857
|
+
if isinstance(union_node, exp.Union):
|
|
874
858
|
left_cols = self._count_select_columns(union_node.left)
|
|
875
859
|
right_cols = self._count_select_columns(union_node.right)
|
|
876
860
|
|
|
@@ -896,12 +880,10 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
896
880
|
"""Analyze subquery structure for injection patterns."""
|
|
897
881
|
issues: list[SecurityIssue] = []
|
|
898
882
|
|
|
899
|
-
|
|
900
|
-
if hasattr(subquery_node, "this") and isinstance(subquery_node.this, exp.Select):
|
|
883
|
+
if subquery_node.this and isinstance(subquery_node.this, exp.Select):
|
|
901
884
|
select_expr = subquery_node.this
|
|
902
885
|
|
|
903
|
-
|
|
904
|
-
if hasattr(select_expr, "expressions") and select_expr.expressions:
|
|
886
|
+
if has_expressions(select_expr) and select_expr.expressions:
|
|
905
887
|
literal_count = sum(1 for expr in select_expr.expressions if isinstance(expr, Literal))
|
|
906
888
|
total_expressions = len(select_expr.expressions)
|
|
907
889
|
|
|
@@ -926,11 +908,8 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
926
908
|
"""Analyze OR conditions for tautology patterns."""
|
|
927
909
|
issues: list[SecurityIssue] = []
|
|
928
910
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
hasattr(or_node, "left")
|
|
932
|
-
and hasattr(or_node, "right")
|
|
933
|
-
and (self._is_always_true_condition(or_node.left) or self._is_always_true_condition(or_node.right))
|
|
911
|
+
if isinstance(or_node, exp.Binary) and (
|
|
912
|
+
self._is_always_true_condition(or_node.left) or self._is_always_true_condition(or_node.right)
|
|
934
913
|
):
|
|
935
914
|
issues.append(
|
|
936
915
|
SecurityIssue(
|
|
@@ -955,10 +934,10 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
955
934
|
"""Calculate the depth of nested binary operations."""
|
|
956
935
|
max_depth = depth
|
|
957
936
|
|
|
958
|
-
if
|
|
937
|
+
if isinstance(node, exp.Binary) and isinstance(node.left, Binary):
|
|
959
938
|
max_depth = max(max_depth, self._calculate_binary_depth(node.left, depth + 1))
|
|
960
939
|
|
|
961
|
-
if
|
|
940
|
+
if isinstance(node, exp.Binary) and isinstance(node.right, Binary):
|
|
962
941
|
max_depth = max(max_depth, self._calculate_binary_depth(node.right, depth + 1))
|
|
963
942
|
|
|
964
943
|
return max_depth
|
|
@@ -966,22 +945,20 @@ class SecurityValidator(ProcessorProtocol):
|
|
|
966
945
|
@staticmethod
|
|
967
946
|
def _count_select_columns(node: "exp.Expression") -> int:
|
|
968
947
|
"""Count the number of columns in a SELECT statement."""
|
|
969
|
-
if isinstance(node, exp.Select) and
|
|
948
|
+
if isinstance(node, exp.Select) and has_expressions(node):
|
|
970
949
|
return len(node.expressions) if node.expressions else 0
|
|
971
950
|
return 0
|
|
972
951
|
|
|
973
952
|
@staticmethod
|
|
974
953
|
def _is_always_true_condition(node: "exp.Expression") -> bool:
|
|
975
954
|
"""Check if a condition is always true using AST analysis."""
|
|
976
|
-
# Check for literal true
|
|
977
955
|
if isinstance(node, Literal) and str(node.this).upper() in {"TRUE", "1"}:
|
|
978
956
|
return True
|
|
979
957
|
|
|
980
958
|
# Check for 1=1 or similar tautologies
|
|
981
959
|
return bool(
|
|
982
960
|
isinstance(node, EQ)
|
|
983
|
-
and
|
|
984
|
-
and hasattr(node, "right")
|
|
961
|
+
and isinstance(node, exp.Binary)
|
|
985
962
|
and (
|
|
986
963
|
isinstance(node.left, Literal)
|
|
987
964
|
and isinstance(node.right, Literal)
|