-
Notifications
You must be signed in to change notification settings - Fork 1
feat(sql_sanitizer): add Rust-backed SQL sanitizer plugin #133
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
328d148
feat(sql_sanitizer): add Rust-backed SQL sanitizer plugin
madhu-mohan-jaishankar c008a81
fix(sql_sanitizer): add README and register plugin in catalog tests
madhu-mohan-jaishankar 025c373
fix(sql_sanitizer): strip field-name prefix from issue messages; add β¦
madhu-mohan-jaishankar cfa2ab5
fix(sql_sanitizer): add PyPI maintainers to pyproject.toml
madhu-mohan-jaishankar ef26ae0
revert(sql_sanitizer): remove maintainers from pyproject.toml
madhu-mohan-jaishankar 1a3e68d
fix(sql_sanitizer): address Copilot review comments
madhu-mohan-jaishankar 9ccdaad
fix(sql_sanitizer): address review comments
madhu-mohan-jaishankar a7d4038
test(sql_sanitizer): add mutation-testing coverage for new helpers
madhu-mohan-jaishankar 95f6a8c
fix(sql_sanitizer): address further review comments
madhu-mohan-jaishankar 7139478
Merge remote-tracking branch 'origin/main' into feat/sql-sanitizer-plβ¦
madhu-mohan-jaishankar 21dec48
fix(sql_sanitizer): use workspace deps for mutants and once_cell
madhu-mohan-jaishankar 4f359fb
fix(sql_sanitizer): avoid timeout mutant in strip_sql_comments
madhu-mohan-jaishankar dc3385c
fix(sql_sanitizer): close MySQL DELETE bypasses and add package stubs
madhu-mohan-jaishankar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| [package] | ||
| name = "sql_sanitizer" | ||
| version = "0.1.0" | ||
| edition.workspace = true | ||
| authors.workspace = true | ||
| license.workspace = true | ||
| repository.workspace = true | ||
| description = "Rust-backed SQL sanitizer plugin for MCP Gateway" | ||
|
|
||
| [lib] | ||
| name = "sql_sanitizer_rust" | ||
| crate-type = ["cdylib", "rlib"] | ||
|
|
||
| [[bin]] | ||
| name = "stub_gen" | ||
| path = "src/bin/stub_gen.rs" | ||
| required-features = ["stub-gen"] | ||
|
|
||
| [features] | ||
| default = [] | ||
| stub-gen = ["dep:pyo3-stub-gen"] | ||
|
|
||
| [dependencies] | ||
| cpex_framework_bridge = { workspace = true } | ||
| log = { workspace = true } | ||
| pyo3 = { workspace = true } | ||
| pyo3-log = { workspace = true } | ||
| pyo3-stub-gen = { workspace = true, optional = true } | ||
| regex = { workspace = true } | ||
| mutants = { workspace = true } | ||
| once_cell = { workspace = true } | ||
| serde = { workspace = true } | ||
| serde_json = { workspace = true } | ||
| thiserror = { workspace = true } | ||
|
|
||
| [dev-dependencies] | ||
| criterion = { workspace = true } | ||
|
|
||
| [[bench]] | ||
| name = "sql_sanitizer" | ||
| harness = false |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| .PHONY: help | ||
| help: | ||
| @grep '^# help\:' $(firstword $(MAKEFILE_LIST)) | sed 's/^# help\: //' | ||
|
|
||
| PACKAGE_NAME := cpex-sql-sanitizer | ||
| WHEEL_PREFIX := cpex_sql_sanitizer | ||
| CARGO := cargo | ||
| CARGO_PACKAGE := sql_sanitizer | ||
| NEXTEST_PROFILE ?= default | ||
| WHEEL_DIR := ../../../../target/wheels | ||
|
|
||
| GREEN := \033[0;32m | ||
| YELLOW := \033[0;33m | ||
| NC := \033[0m | ||
|
|
||
| # help: fmt - Format Rust code with rustfmt | ||
| # help: fmt-check - Check Rust code formatting (CI) | ||
| # help: clippy - Run clippy lints | ||
| .PHONY: fmt fmt-check clippy | ||
|
|
||
| fmt: | ||
| $(CARGO) fmt | ||
|
|
||
| fmt-check: | ||
| $(CARGO) fmt -- --check | ||
|
|
||
| clippy: | ||
| $(CARGO) clippy -- -D warnings | ||
|
|
||
| # help: sync - Install plugin development dependencies | ||
| # help: test - Run Rust unit tests and Python integration tests | ||
| # help: test-unit - Run Rust unit tests | ||
| # help: test-verbose - Run Rust tests with verbose output | ||
| # help: test-integration - Run repo-level integration tests for sql_sanitizer | ||
| # help: test-all - Alias for test | ||
| .PHONY: sync test test-unit test-verbose test-python test-integration test-all | ||
|
|
||
| sync: | ||
| uv sync --dev | ||
|
|
||
| test-unit: | ||
| @echo "$(GREEN)Running sql_sanitizer Rust tests...$(NC)" | ||
| $(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) | ||
|
|
||
| test: test-unit test-integration | ||
|
|
||
| test-verbose: | ||
| @echo "$(GREEN)Running sql_sanitizer Rust tests (verbose)...$(NC)" | ||
| $(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --no-capture | ||
|
|
||
| test-python: | ||
| $(MAKE) test-integration | ||
|
|
||
| test-integration: | ||
| @echo "$(GREEN)Running Python tests...$(NC)" | ||
| CPEX_TEST_PLUGIN_HOOKS=1 uv run pytest ../../../tests/sql_sanitizer/test_integration.py -v -rs | ||
|
|
||
| test-all: test | ||
|
|
||
| # help: build - Build release wheel (no install) | ||
| # help: install - Build and install editable extension into project venv | ||
| # help: install-wheel - Install the previously built wheel into project venv | ||
| .PHONY: build install install-wheel uninstall | ||
|
|
||
| build: | ||
| @echo "$(GREEN)Building $(PACKAGE_NAME)...$(NC)" | ||
| uv run maturin build --release | ||
| @echo "$(GREEN)Build complete$(NC)" | ||
|
|
||
| install: | ||
| @echo "$(GREEN)Installing $(PACKAGE_NAME)...$(NC)" | ||
| uv run maturin develop --release | ||
| @echo "$(GREEN)Installation complete$(NC)" | ||
|
|
||
| install-wheel: build | ||
| @echo "$(GREEN)Installing built wheel for $(PACKAGE_NAME)...$(NC)" | ||
| python3 ../../../../tools/install_built_wheel.py --wheel-dir "$(WHEEL_DIR)" --wheel-prefix "$(WHEEL_PREFIX)" --package-name "$(PACKAGE_NAME)" --venv-dir .venv | ||
| @echo "$(GREEN)Wheel installation complete$(NC)" | ||
|
|
||
| uninstall: | ||
| @echo "$(YELLOW)Uninstalling $(PACKAGE_NAME)...$(NC)" | ||
| @uv pip uninstall -y $(PACKAGE_NAME) 2>/dev/null || true | ||
|
|
||
| # help: bench - Run Criterion benchmarks | ||
| # help: bench-no-run - Compile Criterion benchmark targets with nextest | ||
| .PHONY: bench bench-no-run | ||
|
|
||
| bench: | ||
| @echo "$(GREEN)Running benchmarks...$(NC)" | ||
| $(CARGO) bench | ||
|
|
||
| bench-no-run: | ||
| @echo "$(GREEN)Compiling Criterion benchmark targets with nextest...$(NC)" | ||
| $(CARGO) nextest run --profile $(NEXTEST_PROFILE) -p $(CARGO_PACKAGE) --benches -E 'kind(bench)' --no-run | ||
|
|
||
| .PHONY: clean clean-all | ||
|
|
||
| clean: | ||
| $(CARGO) clean | ||
| rm -rf target/ coverage/ | ||
| find . -name "*.whl" -delete | ||
|
|
||
| clean-all: clean | ||
|
|
||
| # help: doc - Generate Rust documentation | ||
| .PHONY: doc doc-open | ||
|
|
||
| doc: | ||
| $(CARGO) doc --no-deps --document-private-items | ||
|
|
||
| doc-open: doc | ||
| $(CARGO) doc --no-deps --document-private-items --open | ||
|
|
||
| # help: verify - Verify plugin installation | ||
| # help: check-all - Run fmt-check + clippy + Rust tests | ||
| # help: ci-build - Run CI build/static verification without integration tests | ||
| # help: ci - Run the full CI-equivalent plugin verification flow | ||
| .PHONY: verify check-all ci-build ci | ||
|
|
||
| verify: | ||
| @uv run python -c "from cpex_sql_sanitizer import sql_sanitizer_rust; print('sql_sanitizer_rust available')" || echo "sql_sanitizer_rust not installed β run: make install" | ||
|
|
||
| check-all: fmt-check clippy test-unit | ||
|
|
||
| ci-build: fmt-check clippy test-unit build | ||
|
|
||
| ci: fmt-check clippy test-unit install test-integration |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| # SQL Sanitizer (Rust) | ||
|
|
||
| SQL security analysis plugin for ContextForge. | ||
|
|
||
| ## Features | ||
|
|
||
| - Per-statement analysis: SQL payloads are split on `;` and each statement is | ||
| checked independently so a `WHERE` clause in one statement cannot suppress a | ||
| violation in another | ||
| - Blocked statement patterns: `DROP`, `TRUNCATE`, `ALTER`, `GRANT`, `REVOKE` | ||
| (configurable) | ||
| - `DELETE FROM` and `UPDATE` without a `WHERE` clause detection | ||
| - Comment stripping: `--` line comments and `/* */` block comments are removed | ||
| before analysis | ||
| - Field filtering: scan only named argument keys, or all string values when | ||
| `fields` is unset | ||
| - Monitoring mode: pass through with `metadata.sql_issues` populated instead of | ||
| blocking | ||
| - Interpolation heuristic: optional detection of `+`, `%.`, and `{β¦}` patterns | ||
|
madhu-mohan-jaishankar marked this conversation as resolved.
madhu-mohan-jaishankar marked this conversation as resolved.
|
||
|
|
||
|
madhu-mohan-jaishankar marked this conversation as resolved.
|
||
| ## Build | ||
|
|
||
| ```bash | ||
| make install | ||
| ``` | ||
|
|
||
| ## Runtime Requirements | ||
|
|
||
| This plugin depends on `cpex>=0.1.0,<0.2` and imports hook models from | ||
| `cpex.framework`. The compiled Rust extension is mandatory; there is no Python | ||
| fallback implementation. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```python | ||
| from cpex_sql_sanitizer import SQLSanitizerPlugin | ||
| ``` | ||
|
|
||
| The plugin is automatically discovered by the gateway via the | ||
| `cpex.plugins` entry point registered in `pyproject.toml`. | ||
|
|
||
| ### Configuration | ||
|
|
||
| | Key | Type | Default | Description | | ||
| |---|---|---|---| | ||
| | `fields` | `list[str] \| null` | `null` | Field names to scan; `null` scans all strings | | ||
| | `blocked_statements` | `list[str]` | `["\\bDROP\\b", "\\bTRUNCATE\\b", "\\bALTER\\b", "\\bGRANT\\b", "\\bREVOKE\\b"]` | **Replaces** the default blocked-pattern set; `[]` disables this category | | ||
|
lucarlig marked this conversation as resolved.
|
||
| | `block_delete_without_where` | `bool` | `true` | Block `DELETE FROM` without `WHERE` | | ||
| | `block_update_without_where` | `bool` | `true` | Block `UPDATE` without `WHERE` | | ||
| | `strip_comments` | `bool` | `true` | Strip SQL comments before analysis | | ||
| | `require_parameterization` | `bool` | `false` | Flag non-parameterized SQL interpolation | | ||
| | `block_on_violation` | `bool` | `true` | Block request on violation; `false` = monitoring mode | | ||
|
|
||
| ### Hook Signatures | ||
|
|
||
| ```python | ||
| async def prompt_pre_fetch( | ||
| self, | ||
| payload: typing.Any, | ||
| context: typing.Any, | ||
| extensions: typing.Any = None, | ||
| ) -> typing.Any: ... | ||
|
|
||
| async def tool_pre_invoke( | ||
| self, | ||
| payload: typing.Any, | ||
| context: typing.Any, | ||
| extensions: typing.Any = None, | ||
| ) -> typing.Any: ... | ||
| ``` | ||
41 changes: 41 additions & 0 deletions
41
plugins/rust/python-package/sql_sanitizer/benches/sql_sanitizer.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Copyright 2026 | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
| // | ||
| // Benchmarks for SQL sanitizer hot paths. | ||
|
|
||
| use criterion::{Criterion, criterion_group, criterion_main}; | ||
| use sql_sanitizer_rust::config::SqlSanitizerConfig; | ||
| use sql_sanitizer_rust::issues::find_issues; | ||
| use std::hint::black_box; | ||
|
|
||
| fn bench_safe_select(c: &mut Criterion) { | ||
| let cfg = SqlSanitizerConfig::default(); | ||
| let sql = "SELECT id, name, email FROM users WHERE id = 1 AND active = true"; | ||
| c.bench_function("find_issues_safe_select", |b| { | ||
| b.iter(|| find_issues(black_box(sql), &cfg)) | ||
| }); | ||
| } | ||
|
|
||
| fn bench_multi_statement_violation(c: &mut Criterion) { | ||
| let cfg = SqlSanitizerConfig::default(); | ||
| let sql = "UPDATE a SET x=1; UPDATE b SET x=2; UPDATE c SET x=3; UPDATE d SET x=4; SELECT * FROM e WHERE id=1"; | ||
| c.bench_function("find_issues_multi_stmt_violation", |b| { | ||
| b.iter(|| find_issues(black_box(sql), &cfg)) | ||
| }); | ||
| } | ||
|
|
||
| fn bench_comment_stripping(c: &mut Criterion) { | ||
| let cfg = SqlSanitizerConfig::default(); | ||
| let sql = "SELECT /* secret */ id -- inline comment\nFROM users WHERE id = 1"; | ||
| c.bench_function("find_issues_with_comments", |b| { | ||
| b.iter(|| find_issues(black_box(sql), &cfg)) | ||
| }); | ||
| } | ||
|
|
||
| criterion_group!( | ||
| benches, | ||
| bench_safe_select, | ||
| bench_multi_statement_violation, | ||
| bench_comment_stripping | ||
| ); | ||
| criterion_main!(benches); |
20 changes: 20 additions & 0 deletions
20
plugins/rust/python-package/sql_sanitizer/cpex_sql_sanitizer/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| # -*- coding: utf-8 -*- | ||
| # Copyright 2026 | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| """SQL sanitizer plugin package.""" | ||
|
Copilot marked this conversation as resolved.
|
||
|
|
||
| from __future__ import annotations | ||
|
|
||
|
|
||
| def __getattr__(name: str): | ||
| if name == "SqlSanitizerPluginCore": | ||
| from cpex_sql_sanitizer.sql_sanitizer_rust import SqlSanitizerPluginCore | ||
|
|
||
| return SqlSanitizerPluginCore | ||
| if name == "SQLSanitizerPlugin": | ||
| from cpex_sql_sanitizer.sql_sanitizer import SQLSanitizerPlugin | ||
|
|
||
| return SQLSanitizerPlugin | ||
| raise AttributeError(f"module 'cpex_sql_sanitizer' has no attribute {name!r}") | ||
|
|
||
| __all__ = ["SqlSanitizerPluginCore", "SQLSanitizerPlugin"] | ||
10 changes: 10 additions & 0 deletions
10
plugins/rust/python-package/sql_sanitizer/cpex_sql_sanitizer/__init__.pyi
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # This file is automatically generated by pyo3_stub_gen | ||
| # ruff: noqa: E501, F401, F403, F405 | ||
|
|
||
| from .sql_sanitizer import SQLSanitizerPlugin | ||
| from .sql_sanitizer_rust import SqlSanitizerPluginCore | ||
|
|
||
| __all__ = [ | ||
| "SqlSanitizerPluginCore", | ||
| "SQLSanitizerPlugin", | ||
| ] |
20 changes: 20 additions & 0 deletions
20
plugins/rust/python-package/sql_sanitizer/cpex_sql_sanitizer/plugin-manifest.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| description: "Rust-backed SQL statement sanitizer β blocks risky DDL/DML and WHERE-clause-less mutations" | ||
| author: "ContextForge Contributors" | ||
| version: "0.1.0" | ||
| kind: "cpex_sql_sanitizer.sql_sanitizer.SQLSanitizerPlugin" | ||
| available_hooks: | ||
| - "prompt_pre_fetch" | ||
| - "tool_pre_invoke" | ||
| default_configs: | ||
| fields: null | ||
| blocked_statements: | ||
| - "\\bDROP\\b" | ||
| - "\\bTRUNCATE\\b" | ||
| - "\\bALTER\\b" | ||
| - "\\bGRANT\\b" | ||
| - "\\bREVOKE\\b" | ||
| block_delete_without_where: true | ||
| block_update_without_where: true | ||
| strip_comments: true | ||
| require_parameterization: false | ||
| block_on_violation: true |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.