sqlguard-ninja 0.3.0__tar.gz → 0.4.0__tar.gz
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.
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/CHANGELOG.md +11 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/PKG-INFO +20 -1
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/README.md +15 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/IMPLEMENTATION_PROGRESS.md +2 -1
- sqlguard_ninja-0.4.0/docs/development/NEXT_ACTIONS.md +6 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/TEST_EVIDENCE.md +3 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/progress.json +8 -5
- sqlguard_ninja-0.4.0/docs/guides/MCP_INTEGRATION.md +77 -0
- sqlguard_ninja-0.4.0/docs/releases/v0.4.0.md +20 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/security/THREAT_MODEL.md +1 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/pyproject.toml +4 -2
- sqlguard_ninja-0.4.0/src/sqlguard_ninja/_version.py +1 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/main.py +20 -0
- sqlguard_ninja-0.4.0/src/sqlguard_ninja/mcp_server.py +287 -0
- sqlguard_ninja-0.4.0/tests/integration/test_mcp_server.py +142 -0
- sqlguard_ninja-0.3.0/docs/development/NEXT_ACTIONS.md +0 -5
- sqlguard_ninja-0.3.0/src/sqlguard_ninja/_version.py +0 -1
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/.gitignore +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/LICENSE +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/THIRD_PARTY_NOTICES.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/baselines/native.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/external/README.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/LABEL_CHANGES.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/01_destructive.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/02_multi_statement_and_formatting.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/03_table_and_column_access.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/04_privacy.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/05_tenant_isolation.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/06_cte_nested_unicode.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/07_legitimate_lookalike_cost_dialect.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/cases/08_lineage.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/grant_matrix.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/label_matrix.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/tenant_matrix.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/generated/transforms.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/datasets/v1/manifest.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/policies/retail_saas.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/agent-sql-guard.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/agent-sql-guard.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/krish2105-sqlguard.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/krish2105-sqlguard.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/native.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/native.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/sql-guardrail.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.0.0/sql-guardrail.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/agent-sql-guard.csv +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/agent-sql-guard.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/agent-sql-guard.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.csv +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/native.csv +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/native.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/native.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/sql-guardrail.csv +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/sql-guardrail.json +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/benchmarks/results/v1.1.0/sql-guardrail.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/architecture/ARCHITECTURE_DECISIONS.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/benchmarks/RESULTS_v1.1.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/benchmarks/RESULTS_v1.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/ARCHITECTURE_MAP.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/DECISION_LOG.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/development/UPGRADE_MASTER_PLAN.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/AGENT_INTEGRATION.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/BENCHMARK_BIAS_REVIEW.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/BENCHMARK_METHODOLOGY.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/CI_INTEGRATION.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/POLICY_AS_CODE.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/POLICY_REFERENCE.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/guides/QUERY_COST.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/release/FINAL_TECHNICAL_AUDIT.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.1.0.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.1.1.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.2.0.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/releases/v0.3.0.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/research/COMPETITIVE_ANALYSIS.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/docs/security/STATIC_ANALYSIS_LIMITS.md +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/_shared.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/fastapi/app.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/github-actions/sql-security.yml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/langchain/guarded_tool_demo.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/langgraph/real_llm_agent.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/langgraph/sql_agent_demo.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/postgresql/plan_inspection_demo.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/sdk/quickstart.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/examples/sqlite/text_to_sql_demo.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/adapters.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/builtin.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/compare.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/dataset.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/fixtures.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/metrics.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/runner.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/translate.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/_protocol.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/agent_sql_guard_worker.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/krish_sqlguard_worker.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/benchmarking/workers/sql_guardrail_worker.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/config.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/cli/templates.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/analysis.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/findings.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/guard.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/lineage.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/access.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/base.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/catalog.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/functions.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/statements.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/structure.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/core/rules/tenant.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/audit.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/checkpoint.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/executors.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/fastapi.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/langchain.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/integrations/langgraph.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/inheritance.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/model.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/templates/analytics_readonly.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/templates/minimal.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/templates/multi_tenant_saas.yaml +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/policy/tools.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/privacy/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/privacy/rules.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/privacy/suggest.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/py.typed +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/query_cost/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/query_cost/postgres.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/query_cost/static.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/reporting/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/reporting/markdown.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/src/sqlguard_ninja/tenancy.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/conftest.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_corpus_sqlite_validation.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_external_adapters.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_frameworks.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/integration/test_postgres.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/pg_support.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/property/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/property/test_guard_properties.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/test_agent_boundaries.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/test_corpus_native.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/security/test_tenant_rewrite.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/__init__.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/helpers.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_benchmark_engine.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_cli.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_dataset.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_docs.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_function_policy_mechanics.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_lineage.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_policy.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_policy_inheritance.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_policy_tools.py +0 -0
- {sqlguard_ninja-0.3.0 → sqlguard_ninja-0.4.0}/tests/unit/test_rules.py +0 -0
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes are documented here. The project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/); before 1.0, minor versions may change the API.
|
|
5
5
|
|
|
6
|
+
## [0.4.0] — 2026-10-24
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
* **MCP server** (`sqlninja mcp`, `sqlguard_ninja.mcp_server`, extra `[mcp]` on the official
|
|
10
|
+
MCP Python SDK 2.x). Tools: `validate_sql`, `explain_policy_decision`,
|
|
11
|
+
`inspect_query_cost`, `get_policy_capabilities`. The role and tenant are fixed server-side,
|
|
12
|
+
there is no execution and no database connection, internal detail is withheld, ungranted
|
|
13
|
+
tables and columns are not disclosed, input is size-capped, and calls are rate-limited.
|
|
14
|
+
* An end-to-end test drives the real server over stdio with the SDK's `ClientSession`.
|
|
15
|
+
* Docs: `docs/guides/MCP_INTEGRATION.md`; threat model T17.
|
|
16
|
+
|
|
6
17
|
## [0.3.0] — 2026-10-24
|
|
7
18
|
|
|
8
19
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlguard-ninja
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Security testing, policy enforcement, agent integration, privacy analysis and CI benchmarking for LLM-generated SQL.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Pratik653/SQLGuard_Ninjapy
|
|
6
6
|
Project-URL: Documentation, https://github.com/Pratik653/SQLGuard_Ninjapy/tree/main/docs
|
|
@@ -29,6 +29,7 @@ Provides-Extra: all
|
|
|
29
29
|
Requires-Dist: fastapi>=0.110; extra == 'all'
|
|
30
30
|
Requires-Dist: langchain-core>=0.3; extra == 'all'
|
|
31
31
|
Requires-Dist: langgraph>=0.2; extra == 'all'
|
|
32
|
+
Requires-Dist: mcp<3,>=2.3; extra == 'all'
|
|
32
33
|
Requires-Dist: psycopg[binary]>=3.1; extra == 'all'
|
|
33
34
|
Provides-Extra: dev
|
|
34
35
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
@@ -37,6 +38,7 @@ Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
|
37
38
|
Requires-Dist: hypothesis>=6.100; extra == 'dev'
|
|
38
39
|
Requires-Dist: langchain-core>=0.3; extra == 'dev'
|
|
39
40
|
Requires-Dist: langgraph>=0.2; extra == 'dev'
|
|
41
|
+
Requires-Dist: mcp<3,>=2.3; extra == 'dev'
|
|
40
42
|
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
41
43
|
Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
|
|
42
44
|
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
@@ -50,6 +52,8 @@ Requires-Dist: langchain-core>=0.3; extra == 'langchain'
|
|
|
50
52
|
Provides-Extra: langgraph
|
|
51
53
|
Requires-Dist: langchain-core>=0.3; extra == 'langgraph'
|
|
52
54
|
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
|
|
55
|
+
Provides-Extra: mcp
|
|
56
|
+
Requires-Dist: mcp<3,>=2.3; extra == 'mcp'
|
|
53
57
|
Provides-Extra: postgres
|
|
54
58
|
Requires-Dist: psycopg[binary]>=3.1; extra == 'postgres'
|
|
55
59
|
Description-Content-Type: text/markdown
|
|
@@ -263,6 +267,20 @@ Optional: `SQLCheckpoint(..., rewrite_tenant_filters=True)` adds missing tenant
|
|
|
263
267
|
through the AST and executes only the re-validated rewrite. See
|
|
264
268
|
[POLICY_AS_CODE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md).
|
|
265
269
|
|
|
270
|
+
## MCP server
|
|
271
|
+
|
|
272
|
+
Let MCP-compatible agents check SQL against your policy before running it anywhere:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
python -m pip install "sqlguard-ninja[mcp]"
|
|
276
|
+
sqlninja mcp --policy builtin:retail_saas --role support_analyst --tenant 1
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
The four tools are `validate_sql`, `explain_policy_decision`, `inspect_query_cost` and
|
|
280
|
+
`get_policy_capabilities`. The role and tenant are fixed server-side, the server never connects
|
|
281
|
+
to a database, and it exposes no execution tool. Tested with the official MCP Python SDK
|
|
282
|
+
client over stdio. See [MCP_INTEGRATION.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md).
|
|
283
|
+
|
|
266
284
|
## Query cost analysis
|
|
267
285
|
|
|
268
286
|
Every validation includes an explainable static score that uses the catalog's
|
|
@@ -358,6 +376,7 @@ writes, more tested dialects, and SARIF output. Details in [ROADMAP.md](https://
|
|
|
358
376
|
|
|
359
377
|
* [Policy reference](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md)
|
|
360
378
|
* [Policy as code](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md)
|
|
379
|
+
* [MCP integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md)
|
|
361
380
|
* [Agent integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/AGENT_INTEGRATION.md)
|
|
362
381
|
* [Query cost](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/QUERY_COST.md)
|
|
363
382
|
* [CI integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/CI_INTEGRATION.md)
|
|
@@ -207,6 +207,20 @@ Optional: `SQLCheckpoint(..., rewrite_tenant_filters=True)` adds missing tenant
|
|
|
207
207
|
through the AST and executes only the re-validated rewrite. See
|
|
208
208
|
[POLICY_AS_CODE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md).
|
|
209
209
|
|
|
210
|
+
## MCP server
|
|
211
|
+
|
|
212
|
+
Let MCP-compatible agents check SQL against your policy before running it anywhere:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
python -m pip install "sqlguard-ninja[mcp]"
|
|
216
|
+
sqlninja mcp --policy builtin:retail_saas --role support_analyst --tenant 1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The four tools are `validate_sql`, `explain_policy_decision`, `inspect_query_cost` and
|
|
220
|
+
`get_policy_capabilities`. The role and tenant are fixed server-side, the server never connects
|
|
221
|
+
to a database, and it exposes no execution tool. Tested with the official MCP Python SDK
|
|
222
|
+
client over stdio. See [MCP_INTEGRATION.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md).
|
|
223
|
+
|
|
210
224
|
## Query cost analysis
|
|
211
225
|
|
|
212
226
|
Every validation includes an explainable static score that uses the catalog's
|
|
@@ -302,6 +316,7 @@ writes, more tested dialects, and SARIF output. Details in [ROADMAP.md](https://
|
|
|
302
316
|
|
|
303
317
|
* [Policy reference](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md)
|
|
304
318
|
* [Policy as code](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md)
|
|
319
|
+
* [MCP integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/MCP_INTEGRATION.md)
|
|
305
320
|
* [Agent integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/AGENT_INTEGRATION.md)
|
|
306
321
|
* [Query cost](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/QUERY_COST.md)
|
|
307
322
|
* [CI integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/CI_INTEGRATION.md)
|
|
@@ -8,4 +8,5 @@
|
|
|
8
8
|
| 2a Role inheritance (sticky denies), revision/fingerprint, decision ids, audit fields | VERIFIED (local) | 2f1919c | test_policy_inheritance (11) | |
|
|
9
9
|
| 2b Policy diff/simulate/explain/templates CLI | VERIFIED (local) | e64859a | test_policy_tools (21) | |
|
|
10
10
|
| 2c Opt-in AST tenant-filter rewrite + checkpoint | VERIFIED (local) | b0b5ade | test_tenant_rewrite (32, executes on SQLite, tenants 1-3) | |
|
|
11
|
-
| 2d Docs, ADR-011/012, v0.3.0 bump |
|
|
11
|
+
| 2d Docs, ADR-011/012, v0.3.0 bump | RELEASED v0.3.0 | 198a308 | full gate 1,804 passed, 94% | |
|
|
12
|
+
| 3 MCP server (stdio, validation-only) | VERIFIED (local) | see PR | 9 tests incl. real SDK client | released as v0.4.0 (version targets adapted) |
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Next actions (development program)
|
|
2
|
+
|
|
3
|
+
1. Merge the v0.4.0 (MCP) PR, tag, release, verify PyPI and a fresh install including the `[mcp]` extra and a stdio session.
|
|
4
|
+
2. Phase 4 (v0.5.0): make MySQL a tested dialect against a MySQL 8 service container in CI (#11); SQL Server and Oracle parser-level experimental only; publish a compatibility matrix.
|
|
5
|
+
3. Phase 5: OpenTelemetry/Prometheus extras, SARIF output (#14), Grafana dashboard JSON.
|
|
6
|
+
4. Phases 6-8: playground UI on the FastAPI backend, Docker/Kubernetes reference, public demo (hosting needs maintainer authorization).
|
|
@@ -10,3 +10,6 @@
|
|
|
10
10
|
| 2026-10-24 | v0.2.0 publish | release.yml run 40108113801 | build + publish success; wheel and sdist with PEP 740 provenance; 0.1.0 and 0.1.1 unchanged |
|
|
11
11
|
| 2026-10-24 | v0.2.0 fresh install | `pip install --no-cache-dir --index-url https://pypi.org/simple sqlguard-ninja==0.2.0` (Python 3.13, outside repo) | import from site-packages; lineage allow/deny and tenant push-down via CLI; bundled benchmark 1,044 evaluations with CSV, tiers, 14 file hashes |
|
|
12
12
|
| 2026-10-24 | v0.3.0 candidate | full gate | 1,804 passed, 94%; build + twine PASSED; 3 templates in wheel |
|
|
13
|
+
| 2026-10-24 | v0.3.0 release | PR #19 12/12; main CI on 198a308 success; release.yml run 40115952065 success | PyPI files with provenance |
|
|
14
|
+
| 2026-10-24 | v0.3.0 fresh install | pip from PyPI, Python 3.13, outside repo (3rd attempt; CDN lag) | templates, policy check/explain (inheritance), tenant rewrite, decision metadata OK |
|
|
15
|
+
| 2026-10-24 | v0.4.0 candidate (MCP) | tests/integration/test_mcp_server.py | 9 passed, including a real stdio ClientSession |
|
|
@@ -38,21 +38,24 @@
|
|
|
38
38
|
{
|
|
39
39
|
"id": 3,
|
|
40
40
|
"name": "Policy-as-code and multitenancy",
|
|
41
|
-
"status": "
|
|
41
|
+
"status": "RELEASED",
|
|
42
42
|
"release": "v0.3.0",
|
|
43
43
|
"issues": [],
|
|
44
44
|
"evidence": [
|
|
45
|
-
"inheritance, diff/simulate/explain/templates, tenant rewrite, audit metadata; 1,804 tests pass locally"
|
|
45
|
+
"inheritance, diff/simulate/explain/templates, tenant rewrite, audit metadata; 1,804 tests pass locally",
|
|
46
|
+
"v0.3.0 on PyPI; fresh install verified"
|
|
46
47
|
],
|
|
47
48
|
"blockers": []
|
|
48
49
|
},
|
|
49
50
|
{
|
|
50
51
|
"id": 4,
|
|
51
52
|
"name": "MCP server",
|
|
52
|
-
"status": "
|
|
53
|
-
"release": "v0.
|
|
53
|
+
"status": "VERIFIED",
|
|
54
|
+
"release": "v0.4.0",
|
|
54
55
|
"issues": [],
|
|
55
|
-
"evidence": [
|
|
56
|
+
"evidence": [
|
|
57
|
+
"stdio server; 9 tests incl. official SDK ClientSession"
|
|
58
|
+
],
|
|
56
59
|
"blockers": []
|
|
57
60
|
},
|
|
58
61
|
{
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# MCP integration
|
|
2
|
+
|
|
3
|
+
SQLGuard Ninja ships a [Model Context Protocol](https://modelcontextprotocol.io/) server that
|
|
4
|
+
lets MCP-compatible agents check SQL against your policy **before** they run it anywhere.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
python -m pip install "sqlguard-ninja[mcp]"
|
|
8
|
+
sqlninja mcp --policy policy.yaml --role support --tenant 42 # stdio transport
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Built on the official MCP Python SDK (`mcp` 2.x, `MCPServer`). The bundled reference policy
|
|
12
|
+
works out of the box: `--policy builtin:retail_saas --role support_analyst --tenant 1`.
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | Arguments | Returns |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| `validate_sql` | `sql`, optional `dialect` (`postgres` or `sqlite`) | `decision` (allow/review/deny), findings (rule id, action, message, remediation), `decision_id`, policy revision |
|
|
19
|
+
| `explain_policy_decision` | `sql`, optional `dialect` | the decision plus per-finding lineage (base column, JSON path, labels) and how-to-fix hints |
|
|
20
|
+
| `inspect_query_cost` | `sql`, optional `dialect` | static cost score, thresholds and factors (no execution plan, no database) |
|
|
21
|
+
| `get_policy_capabilities` | — | the tables and columns this identity may use, their access mode (`allow`, `aggregate_only`, `review`), required tenant filter, limits, supported dialects |
|
|
22
|
+
|
|
23
|
+
No tool accepts a role, a tenant, credentials or a connection string. The server has **no
|
|
24
|
+
execution tool** and never connects to a database.
|
|
25
|
+
|
|
26
|
+
## Security model
|
|
27
|
+
|
|
28
|
+
* **Identity is server-side.** `--role` and `--tenant` are fixed when the server starts, by
|
|
29
|
+
whoever writes the client configuration. A model cannot choose its own permissions. Extra
|
|
30
|
+
arguments such as `role` are rejected or ignored, and the end-to-end test checks this.
|
|
31
|
+
* **Least disclosure.** `get_policy_capabilities` lists only tables and columns the role may
|
|
32
|
+
use. Denied tables and columns are not revealed. Internal finding detail is never returned,
|
|
33
|
+
and with `disclosure: generic` lineage is omitted as well.
|
|
34
|
+
* **Limits.** A hard cap of 100,000 characters applies before parsing, the policy's own length
|
|
35
|
+
and AST-size limits still apply, and a per-process rate limit (120 calls per minute by
|
|
36
|
+
default) is enforced. Errors are structured (`{"error": ..., "message": ...}`) and carry no
|
|
37
|
+
stack traces.
|
|
38
|
+
* **Fail closed.** A tenant-scoped role started without `--tenant` gets every tenant-table
|
|
39
|
+
query denied.
|
|
40
|
+
* The server is a validation service. Running the SQL remains your application's job, with a
|
|
41
|
+
least-privilege, read-only database role and row-level security.
|
|
42
|
+
|
|
43
|
+
## Client configuration (stdio)
|
|
44
|
+
|
|
45
|
+
Most MCP clients accept a JSON server definition of this shape. Adapt the command path to
|
|
46
|
+
your environment:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"sqlguard-ninja": {
|
|
52
|
+
"command": "sqlninja",
|
|
53
|
+
"args": ["mcp", "--policy", "/absolute/path/policy.yaml", "--role", "support", "--tenant", "42"]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
One server process serves one identity. To give different users different permissions, start
|
|
60
|
+
one server per identity from your trusted configuration. Never let the model write the client
|
|
61
|
+
configuration.
|
|
62
|
+
|
|
63
|
+
## Remote transports
|
|
64
|
+
|
|
65
|
+
The SDK also supports Streamable HTTP. SQLGuard Ninja does **not** enable it by default,
|
|
66
|
+
because a remote endpoint needs authentication that binds each caller to a role and tenant.
|
|
67
|
+
If you expose it remotely, put it behind your own authenticated gateway, derive the identity
|
|
68
|
+
from the authenticated caller, and keep one identity per server process or route. This setup
|
|
69
|
+
is not covered by the test suite.
|
|
70
|
+
|
|
71
|
+
## Verification
|
|
72
|
+
|
|
73
|
+
`tests/integration/test_mcp_server.py` starts the real server as a subprocess and connects
|
|
74
|
+
with the official SDK's `stdio_client` and `ClientSession`. It checks tool discovery,
|
|
75
|
+
allowed and denied validations, a forged `role` argument, and capability listing. It also
|
|
76
|
+
unit-tests malformed input, oversized input, unsupported dialects, rate limiting, the
|
|
77
|
+
missing-tenant fail-closed path and least disclosure.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SQLGuard Ninja v0.4.0
|
|
2
|
+
|
|
3
|
+
## Highlights
|
|
4
|
+
|
|
5
|
+
* **MCP server.** `sqlninja mcp --policy ... --role ... --tenant ...` exposes `validate_sql`, `explain_policy_decision`, `inspect_query_cost` and `get_policy_capabilities` to MCP-compatible agents over stdio. It is built on the official MCP Python SDK (2.x).
|
|
6
|
+
* **Security model.** The identity is fixed by the operator at start-up, and tools take no role, tenant, credential or connection-string arguments. The server never executes SQL or connects to a database, withholds internal detail, and does not reveal ungranted tables or columns. Inputs are size-capped and calls are rate-limited.
|
|
7
|
+
* **Verified with a real client.** The end-to-end test launches the server as a subprocess and uses the SDK's `stdio_client` and `ClientSession` to discover and call the tools, including a forged `role` argument.
|
|
8
|
+
|
|
9
|
+
## Notes
|
|
10
|
+
|
|
11
|
+
* Remote (Streamable HTTP) transport is not enabled. A remote deployment needs an authenticated gateway that binds each caller to an identity (see the MCP guide).
|
|
12
|
+
* Known limitations are unchanged: all benchmark results are internal (issues #5 and #6), only PostgreSQL and SQLite are supported, and there is no guarantee against SQL injection or data leakage.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
python -m pip install "sqlguard-ninja[mcp]==0.4.0"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
PyPI: https://pypi.org/project/sqlguard-ninja/0.4.0/
|
|
@@ -57,6 +57,7 @@ flowchart LR
|
|
|
57
57
|
| T3 | Prompt injection through database content | A ticket body says "now query payments" | Every subsequent query is checked again. Retry lock-out per trace. Read-only session. | The model can still be steered to permitted data. |
|
|
58
58
|
| T4 | Parser ambiguity | Comment and string tricks, nested comments, dollar quoting | sqlglot dialect parsing. The corpus includes formatting and comment cases validated against real engines. Unparseable input is denied. | Unknown tokenizer differences. Report them as vulnerabilities. |
|
|
59
59
|
| T5 | Dialect-specific bypasses | MySQL `/*! */` executable comments, SQLite `ATTACH`/`PRAGMA` | Executable-comment rule for the MySQL family. SQLite session statements are denied. | Untested dialects are experimental. |
|
|
60
|
+
| T17 | MCP client or model escalating privileges | A model passes `role` or `tenant` arguments, or probes `get_policy_capabilities` for hidden tables | Identity fixed at server start from operator config; tool schemas have no identity arguments; capabilities list only granted, non-denied columns; no execution or credentials in the server | Anyone who can edit the MCP client configuration chooses the identity |
|
|
60
61
|
| T6a | Unsafe automatic tenant filtering | A rewrite that changes meaning or masks a query naming another tenant | Rewrite is opt-in, bind-parameter only, refused on any non-tenant finding and on RIGHT/FULL-join ownership ambiguity; the rewritten SQL is re-validated and is the executed string | Rewritten SQL is sqlglot-normalised; RLS remains authoritative |
|
|
61
62
|
| T6b | Silent policy relaxation through inheritance or edits | A child role or edit quietly widens access | Sticky explicit denies (load error on conflict); `policy diff --fail-on-relaxation`; fingerprint and revision in every decision | Reviewers must still read the diff |
|
|
62
63
|
| T6 | Tenant data leakage | missing filter, `OR 1=1`, other tenant id, unpinned join or subquery | Every scope reading a tenant table must pin it with an equality conjunct to the context value or an approved bind parameter. `OR`/`NOT` don't count, and casts of the column don't count. | Apply RLS in the database as the authoritative control. |
|
|
@@ -36,8 +36,10 @@ dependencies = [
|
|
|
36
36
|
langchain = ["langchain-core>=0.3"]
|
|
37
37
|
langgraph = ["langgraph>=0.2", "langchain-core>=0.3"]
|
|
38
38
|
fastapi = ["fastapi>=0.110"]
|
|
39
|
+
# Official MCP Python SDK; 2.x renamed FastMCP to MCPServer.
|
|
40
|
+
mcp = ["mcp>=2.3,<3"]
|
|
39
41
|
postgres = ["psycopg[binary]>=3.1"]
|
|
40
|
-
all = ["sqlguard-ninja[langchain,langgraph,fastapi,postgres]"]
|
|
42
|
+
all = ["sqlguard-ninja[langchain,langgraph,fastapi,postgres,mcp]"]
|
|
41
43
|
dev = [
|
|
42
44
|
"sqlguard-ninja[all]",
|
|
43
45
|
"pytest>=8.0",
|
|
@@ -118,5 +120,5 @@ exclude = ["src/sqlguard_ninja/benchmarking/workers/"]
|
|
|
118
120
|
plugins = ["pydantic.mypy"]
|
|
119
121
|
|
|
120
122
|
[[tool.mypy.overrides]]
|
|
121
|
-
module = ["langgraph.*", "langchain_core.*", "psycopg.*", "pgserver.*"]
|
|
123
|
+
module = ["langgraph.*", "langchain_core.*", "psycopg.*", "pgserver.*", "mcp.*"]
|
|
122
124
|
ignore_missing_imports = true
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -273,6 +273,19 @@ def cmd_policy_explain(args: argparse.Namespace) -> int:
|
|
|
273
273
|
return EXIT_OK
|
|
274
274
|
|
|
275
275
|
|
|
276
|
+
def cmd_mcp(args: argparse.Namespace) -> int: # pragma: no cover - blocks on stdio
|
|
277
|
+
try:
|
|
278
|
+
from sqlguard_ninja.mcp_server import ServerIdentity, serve # noqa: PLC0415
|
|
279
|
+
except ImportError as exc:
|
|
280
|
+
raise CLIError('the MCP server needs: pip install "sqlguard-ninja[mcp]"') from exc
|
|
281
|
+
policy = _load_policy(args.policy)
|
|
282
|
+
try:
|
|
283
|
+
serve(policy, ServerIdentity(role=args.role, tenant=args.tenant), args.dialect)
|
|
284
|
+
except ValueError as exc:
|
|
285
|
+
raise CLIError(str(exc)) from exc
|
|
286
|
+
return EXIT_OK
|
|
287
|
+
|
|
288
|
+
|
|
276
289
|
def cmd_init(args: argparse.Namespace) -> int:
|
|
277
290
|
root = Path(args.directory)
|
|
278
291
|
existing = [name for name in FILES if (root / name).exists()]
|
|
@@ -558,6 +571,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
558
571
|
bb.add_argument("--i-understand-this-is-ci", action="store_true", help=argparse.SUPPRESS)
|
|
559
572
|
bb.set_defaults(func=cmd_benchmark_baseline)
|
|
560
573
|
|
|
574
|
+
mcp = sub.add_parser("mcp", help="run the MCP server (stdio) for a fixed role and tenant")
|
|
575
|
+
mcp.add_argument("--policy", required=True, help="policy file or builtin:<name>")
|
|
576
|
+
mcp.add_argument("--role", required=True, help="trusted role for every request")
|
|
577
|
+
mcp.add_argument("--tenant", help="trusted tenant for every request")
|
|
578
|
+
mcp.add_argument("--dialect")
|
|
579
|
+
mcp.set_defaults(func=cmd_mcp)
|
|
580
|
+
|
|
561
581
|
rep = sub.add_parser("report", help="reports").add_subparsers(dest="report_cmd", required=True)
|
|
562
582
|
rg = rep.add_parser("generate", help="render a Markdown report from a result JSON")
|
|
563
583
|
rg.add_argument("--result", required=True)
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Model Context Protocol (MCP) server exposing SQLGuard Ninja as a validation service.
|
|
2
|
+
|
|
3
|
+
Security model:
|
|
4
|
+
|
|
5
|
+
* **Identity is server-side.** The role and tenant are fixed when the server starts, from the
|
|
6
|
+
operator's configuration (for example an MCP client config). No tool accepts a role, a
|
|
7
|
+
tenant or an approval, so a model cannot choose its own permissions.
|
|
8
|
+
* **No execution, no credentials.** The server never connects to a database. It has no
|
|
9
|
+
connection-string arguments and no filesystem tools. It only validates and explains SQL.
|
|
10
|
+
* **Agent-safe output.** Tools return the agent-facing messages and remediations. Internal
|
|
11
|
+
detail is omitted, and lineage only names columns the role is allowed to know about.
|
|
12
|
+
* **Resource limits.** A hard input-size cap applies before parsing, plus a per-process rate
|
|
13
|
+
limit.
|
|
14
|
+
|
|
15
|
+
Run: ``sqlninja mcp --policy policy.yaml --role support --tenant 42`` (stdio transport).
|
|
16
|
+
Requires ``pip install "sqlguard-ninja[mcp]"``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from collections import deque
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from sqlguard_ninja._version import __version__
|
|
28
|
+
from sqlguard_ninja.core.analysis import SUPPORTED_DIALECTS, analyse
|
|
29
|
+
from sqlguard_ninja.core.findings import ValidationResult
|
|
30
|
+
from sqlguard_ninja.core.guard import Guard
|
|
31
|
+
from sqlguard_ninja.policy.model import Access, Disclosure, Policy
|
|
32
|
+
from sqlguard_ninja.privacy.rules import resolve_column_access
|
|
33
|
+
from sqlguard_ninja.query_cost.static import analyse_static_cost
|
|
34
|
+
|
|
35
|
+
HARD_MAX_SQL_CHARS = 100_000
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class ServerIdentity:
|
|
40
|
+
"""Trusted request identity, configured by the operator, never by the model."""
|
|
41
|
+
|
|
42
|
+
role: str
|
|
43
|
+
tenant: str | None = None
|
|
44
|
+
principal: str = "mcp-client"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RateLimiter:
|
|
48
|
+
"""Sliding-window limiter: at most ``limit`` calls per ``window_s`` seconds."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, limit: int = 120, window_s: float = 60.0) -> None:
|
|
51
|
+
self.limit = limit
|
|
52
|
+
self.window_s = window_s
|
|
53
|
+
self._calls: deque[float] = deque()
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
def allow(self) -> bool:
|
|
57
|
+
now = time.monotonic()
|
|
58
|
+
with self._lock:
|
|
59
|
+
while self._calls and now - self._calls[0] > self.window_s:
|
|
60
|
+
self._calls.popleft()
|
|
61
|
+
if len(self._calls) >= self.limit:
|
|
62
|
+
return False
|
|
63
|
+
self._calls.append(now)
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SQLGuardService:
|
|
68
|
+
"""Transport-independent tool implementations (unit-testable without MCP)."""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
policy: Policy,
|
|
73
|
+
identity: ServerIdentity,
|
|
74
|
+
*,
|
|
75
|
+
dialect: str | None = None,
|
|
76
|
+
rate_limiter: RateLimiter | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
if policy.roles and policy.effective_role(identity.role) is None:
|
|
79
|
+
raise ValueError(f"role {identity.role!r} is not defined in the policy")
|
|
80
|
+
self.policy = policy
|
|
81
|
+
self.guard = Guard(policy)
|
|
82
|
+
self.identity = identity
|
|
83
|
+
self.dialect = (dialect or policy.dialect).lower()
|
|
84
|
+
self.rate_limiter = rate_limiter or RateLimiter()
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------ helpers
|
|
87
|
+
def _context(self) -> dict[str, Any]:
|
|
88
|
+
if self.identity.tenant is None:
|
|
89
|
+
return {}
|
|
90
|
+
return {self.policy.tenant.context_key: self.identity.tenant}
|
|
91
|
+
|
|
92
|
+
def _precheck(self, sql: Any, dialect: str | None) -> tuple[str, str] | dict[str, Any]:
|
|
93
|
+
if not self.rate_limiter.allow():
|
|
94
|
+
return {"error": "rate_limited", "message": "Too many requests; try again shortly."}
|
|
95
|
+
if not isinstance(sql, str):
|
|
96
|
+
return {"error": "invalid_input", "message": "sql must be a string."}
|
|
97
|
+
if len(sql) > HARD_MAX_SQL_CHARS:
|
|
98
|
+
return {"error": "invalid_input", "message": "sql is too large."}
|
|
99
|
+
chosen = (dialect or self.dialect).lower()
|
|
100
|
+
if chosen not in SUPPORTED_DIALECTS:
|
|
101
|
+
return {
|
|
102
|
+
"error": "unsupported_dialect",
|
|
103
|
+
"message": f"Supported dialects: {', '.join(SUPPORTED_DIALECTS)}.",
|
|
104
|
+
}
|
|
105
|
+
return sql, chosen
|
|
106
|
+
|
|
107
|
+
def _validate(self, sql: str, dialect: str) -> ValidationResult:
|
|
108
|
+
return self.guard.validate(
|
|
109
|
+
sql, role=self.identity.role, dialect=dialect, context=self._context()
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _public_findings(
|
|
113
|
+
self, result: ValidationResult, with_lineage: bool
|
|
114
|
+
) -> list[dict[str, Any]]:
|
|
115
|
+
out = []
|
|
116
|
+
generic = self.policy.disclosure == Disclosure.GENERIC
|
|
117
|
+
for f in result.findings:
|
|
118
|
+
item: dict[str, Any] = {
|
|
119
|
+
"rule_id": f.rule_id,
|
|
120
|
+
"action": f.action.value,
|
|
121
|
+
"category": f.category.value,
|
|
122
|
+
"message": f.message,
|
|
123
|
+
}
|
|
124
|
+
if f.remediation:
|
|
125
|
+
item["remediation"] = f.remediation
|
|
126
|
+
if with_lineage and not generic and f.metadata.get("lineage"):
|
|
127
|
+
lineage = f.metadata["lineage"]
|
|
128
|
+
item["lineage"] = {
|
|
129
|
+
"base_column": lineage.get("base_column"),
|
|
130
|
+
"json_path": lineage.get("json_path"),
|
|
131
|
+
"labels": lineage.get("labels"),
|
|
132
|
+
}
|
|
133
|
+
out.append(item)
|
|
134
|
+
return out
|
|
135
|
+
|
|
136
|
+
# -------------------------------------------------------------------- tools
|
|
137
|
+
def validate_sql(self, sql: Any, dialect: str | None = None) -> dict[str, Any]:
|
|
138
|
+
checked = self._precheck(sql, dialect)
|
|
139
|
+
if isinstance(checked, dict):
|
|
140
|
+
return checked
|
|
141
|
+
result = self._validate(*checked)
|
|
142
|
+
return {
|
|
143
|
+
"decision": result.decision.value,
|
|
144
|
+
"allowed": result.allowed,
|
|
145
|
+
"findings": self._public_findings(result, with_lineage=False),
|
|
146
|
+
"decision_id": result.decision_id,
|
|
147
|
+
"policy_revision": result.policy_revision,
|
|
148
|
+
"note": "Validation only: this server never executes SQL. Database permissions "
|
|
149
|
+
"remain authoritative.",
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
def explain_policy_decision(self, sql: Any, dialect: str | None = None) -> dict[str, Any]:
|
|
153
|
+
checked = self._precheck(sql, dialect)
|
|
154
|
+
if isinstance(checked, dict):
|
|
155
|
+
return checked
|
|
156
|
+
result = self._validate(*checked)
|
|
157
|
+
return {
|
|
158
|
+
"decision": result.decision.value,
|
|
159
|
+
"decision_id": result.decision_id,
|
|
160
|
+
"role": self.identity.role,
|
|
161
|
+
"tenant_scoped": bool(self.identity.tenant),
|
|
162
|
+
"policy": {"name": result.policy_name, "revision": result.policy_revision},
|
|
163
|
+
"findings": self._public_findings(result, with_lineage=True),
|
|
164
|
+
"how_to_fix": [f.remediation for f in result.findings if f.remediation],
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
def inspect_query_cost(self, sql: Any, dialect: str | None = None) -> dict[str, Any]:
|
|
168
|
+
checked = self._precheck(sql, dialect)
|
|
169
|
+
if isinstance(checked, dict):
|
|
170
|
+
return checked
|
|
171
|
+
text, chosen = checked
|
|
172
|
+
limits = self.policy.effective_limits(self.policy.role(self.identity.role)[1])
|
|
173
|
+
analysis = analyse(text, chosen, self.policy.catalog, max_nodes=limits.max_ast_nodes)
|
|
174
|
+
if not analysis.ok or analysis.qualified is None:
|
|
175
|
+
return {"error": "unanalysable", "message": "The query could not be analysed."}
|
|
176
|
+
report = analyse_static_cost(analysis, self.policy)
|
|
177
|
+
return {
|
|
178
|
+
"static_score": report.score,
|
|
179
|
+
"review_threshold": self.policy.cost.review_score,
|
|
180
|
+
"deny_threshold": self.policy.cost.deny_score,
|
|
181
|
+
"factors": [
|
|
182
|
+
{"code": f.code, "points": f.points, "explanation": f.explanation}
|
|
183
|
+
for f in report.factors
|
|
184
|
+
],
|
|
185
|
+
"note": "Static heuristic only; no execution plan is requested from any database.",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
def get_policy_capabilities(self) -> dict[str, Any]:
|
|
189
|
+
"""What this identity may query: permitted tables/columns and how they may be used."""
|
|
190
|
+
if not self.rate_limiter.allow():
|
|
191
|
+
return {"error": "rate_limited", "message": "Too many requests; try again shortly."}
|
|
192
|
+
role = self.policy.effective_role(self.identity.role) if self.policy.roles else None
|
|
193
|
+
tables: dict[str, Any] = {}
|
|
194
|
+
for tname, tdef in self.policy.catalog.tables.items():
|
|
195
|
+
grant = role.tables.get(tname) if role is not None else None
|
|
196
|
+
if role is not None and grant is None:
|
|
197
|
+
continue # do not reveal tables the role cannot use
|
|
198
|
+
columns = {}
|
|
199
|
+
for cname in tdef.columns:
|
|
200
|
+
listed = grant is None or grant.columns == "*" or cname in grant.columns
|
|
201
|
+
if not listed or (grant is not None and cname in grant.deny_columns):
|
|
202
|
+
continue
|
|
203
|
+
access = resolve_column_access(self.policy, role, tname, cname).strongest
|
|
204
|
+
if access is Access.DENY:
|
|
205
|
+
continue
|
|
206
|
+
columns[cname] = access.value
|
|
207
|
+
needs_pin = bool(tdef.tenant_column) and (role is None or role.tenant_scoped)
|
|
208
|
+
tables[tname] = {
|
|
209
|
+
"columns": columns,
|
|
210
|
+
"tenant_filter_required": tdef.tenant_column if needs_pin else None,
|
|
211
|
+
}
|
|
212
|
+
limits = self.policy.effective_limits(role)
|
|
213
|
+
return {
|
|
214
|
+
"role": self.identity.role,
|
|
215
|
+
"policy": {"name": self.policy.name, "revision": self.policy.revision},
|
|
216
|
+
"dialects": list(SUPPORTED_DIALECTS),
|
|
217
|
+
"default_dialect": self.dialect,
|
|
218
|
+
"read_only": True,
|
|
219
|
+
"tenant_bind_parameter": self.policy.tenant.parameter_names[0]
|
|
220
|
+
if self.policy.tenant.parameter_names
|
|
221
|
+
else None,
|
|
222
|
+
"limits": {"max_limit": limits.max_limit, "max_joins": limits.max_joins},
|
|
223
|
+
"tables": tables,
|
|
224
|
+
"column_access_values": {
|
|
225
|
+
"allow": "may be selected and filtered",
|
|
226
|
+
"aggregate_only": "only inside COUNT/SUM/AVG-style aggregates",
|
|
227
|
+
"review": "requires human approval",
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def build_server(service: SQLGuardService) -> Any:
|
|
233
|
+
"""Create the MCP server with the four tools bound to ``service``."""
|
|
234
|
+
from mcp.server.mcpserver import MCPServer # noqa: PLC0415 - optional dependency
|
|
235
|
+
|
|
236
|
+
server = MCPServer(
|
|
237
|
+
name="sqlguard-ninja",
|
|
238
|
+
version=__version__,
|
|
239
|
+
instructions=(
|
|
240
|
+
"Validate SQL before running it. The server enforces a fixed role and tenant "
|
|
241
|
+
"chosen by the operator; you cannot change them. It never executes SQL."
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
@server.tool(description="Validate one SQL statement against the configured policy.")
|
|
246
|
+
def validate_sql(sql: str, dialect: str | None = None) -> dict[str, Any]:
|
|
247
|
+
return service.validate_sql(sql, dialect)
|
|
248
|
+
|
|
249
|
+
@server.tool(description="Explain why a SQL statement is allowed, denied or needs review.")
|
|
250
|
+
def explain_policy_decision(sql: str, dialect: str | None = None) -> dict[str, Any]:
|
|
251
|
+
return service.explain_policy_decision(sql, dialect)
|
|
252
|
+
|
|
253
|
+
@server.tool(description="Estimate query cost risk with static heuristics (no execution).")
|
|
254
|
+
def inspect_query_cost(sql: str, dialect: str | None = None) -> dict[str, Any]:
|
|
255
|
+
return service.inspect_query_cost(sql, dialect)
|
|
256
|
+
|
|
257
|
+
@server.tool(description="List the tables, columns and rules available to this identity.")
|
|
258
|
+
def get_policy_capabilities() -> dict[str, Any]:
|
|
259
|
+
return service.get_policy_capabilities()
|
|
260
|
+
|
|
261
|
+
return server
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def serve(
|
|
265
|
+
policy: Policy, identity: ServerIdentity, dialect: str | None = None
|
|
266
|
+
) -> None: # pragma: no cover - exercised through the stdio integration test
|
|
267
|
+
build_server(SQLGuardService(policy, identity, dialect=dialect)).run("stdio")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def main(argv: list[str] | None = None) -> int: # pragma: no cover - thin CLI wrapper
|
|
271
|
+
import argparse # noqa: PLC0415
|
|
272
|
+
|
|
273
|
+
from sqlguard_ninja.benchmarking.builtin import resolve # noqa: PLC0415
|
|
274
|
+
|
|
275
|
+
p = argparse.ArgumentParser(prog="sqlguard-ninja-mcp")
|
|
276
|
+
p.add_argument("--policy", required=True)
|
|
277
|
+
p.add_argument("--role", required=True)
|
|
278
|
+
p.add_argument("--tenant")
|
|
279
|
+
p.add_argument("--dialect")
|
|
280
|
+
args = p.parse_args(argv)
|
|
281
|
+
policy = Policy.from_file(resolve(args.policy, "policy"))
|
|
282
|
+
serve(policy, ServerIdentity(role=args.role, tenant=args.tenant), args.dialect)
|
|
283
|
+
return 0
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
if __name__ == "__main__": # pragma: no cover
|
|
287
|
+
raise SystemExit(main())
|