Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"plugins/rust/python-package/retry_with_backoff",
"plugins/rust/python-package/url_reputation",
"plugins/rust/python-package/secrets_detection",
"plugins/rust/python-package/sql_sanitizer",
]
resolver = "3"

Expand All @@ -22,6 +23,7 @@ cpex_framework_bridge = { path = "crates/framework_bridge" }
criterion = { version = "0.8.2", features = ["html_reports"] }
log = "0.4.29"
mutants = "0.0.4"
once_cell = "1.21.4"
pyo3 = { version = "0.29.0", features = ["abi3-py311"] }
pyo3-log = "0.13.4"
pyo3-stub-gen = "0.23.0"
Expand Down
4 changes: 1 addition & 3 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
# See https://embarkstudios.github.io/cargo-deny/

[advisories]
# pyo3-stub-gen 0.22.1 still pulls rustpython-parser's unmaintained unic-* parser crates.
# pyo3-stub-gen still pulls rustpython-parser's unmaintained unic-* parser crates.
# Keep this scoped to the known transitive advisories so new unmaintained dependencies fail CI.
ignore = [
{ id = "RUSTSEC-2026-0176", reason = "pyo3-stub-gen blocks PyO3 0.29 until upstream compatibility release; expires 2026-08-01" },
{ id = "RUSTSEC-2026-0177", reason = "pyo3-stub-gen blocks PyO3 0.29 until upstream compatibility release; expires 2026-08-01" },
"RUSTSEC-2025-0075", # unic-char-range via rustpython-parser
"RUSTSEC-2025-0080", # unic-common via rustpython-parser
"RUSTSEC-2025-0081", # unic-char-property via rustpython-parser
Expand Down
2 changes: 1 addition & 1 deletion plugins/rust/python-package/pii_filter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pyo3 = { workspace = true }
pyo3-log = { workspace = true }
pyo3-stub-gen = { workspace = true, optional = true }
regex = { workspace = true }
once_cell = "1.21.4"
once_cell = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
Expand Down
41 changes: 41 additions & 0 deletions plugins/rust/python-package/sql_sanitizer/Cargo.toml
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
127 changes: 127 additions & 0 deletions plugins/rust/python-package/sql_sanitizer/Makefile
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
70 changes: 70 additions & 0 deletions plugins/rust/python-package/sql_sanitizer/README.md
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
Comment thread
madhu-mohan-jaishankar marked this conversation as resolved.
Comment thread
madhu-mohan-jaishankar marked this conversation as resolved.
Comment thread
madhu-mohan-jaishankar marked this conversation as resolved.

Comment thread
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 |
Comment thread
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: ...
```
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);
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."""
Comment thread
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"]
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",
]
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
Loading