sqlguard-ninja 0.2.0__tar.gz → 0.3.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.
Files changed (164) hide show
  1. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/CHANGELOG.md +21 -0
  2. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/PKG-INFO +19 -1
  3. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/README.md +18 -0
  4. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/architecture/ARCHITECTURE_DECISIONS.md +19 -0
  5. sqlguard_ninja-0.3.0/docs/development/IMPLEMENTATION_PROGRESS.md +11 -0
  6. sqlguard_ninja-0.3.0/docs/development/NEXT_ACTIONS.md +5 -0
  7. sqlguard_ninja-0.3.0/docs/development/TEST_EVIDENCE.md +12 -0
  8. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/development/progress.json +10 -6
  9. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/guides/AGENT_INTEGRATION.md +3 -0
  10. sqlguard_ninja-0.3.0/docs/guides/POLICY_AS_CODE.md +97 -0
  11. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/guides/POLICY_REFERENCE.md +5 -0
  12. sqlguard_ninja-0.3.0/docs/releases/v0.3.0.md +29 -0
  13. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/security/THREAT_MODEL.md +2 -0
  14. sqlguard_ninja-0.3.0/src/sqlguard_ninja/_version.py +1 -0
  15. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/cli/main.py +128 -1
  16. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/findings.py +4 -0
  17. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/guard.py +4 -0
  18. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/audit.py +8 -0
  19. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/checkpoint.py +37 -4
  20. sqlguard_ninja-0.3.0/src/sqlguard_ninja/policy/inheritance.py +118 -0
  21. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/policy/model.py +47 -3
  22. sqlguard_ninja-0.3.0/src/sqlguard_ninja/policy/templates/analytics_readonly.yaml +31 -0
  23. sqlguard_ninja-0.3.0/src/sqlguard_ninja/policy/templates/minimal.yaml +17 -0
  24. sqlguard_ninja-0.3.0/src/sqlguard_ninja/policy/templates/multi_tenant_saas.yaml +53 -0
  25. sqlguard_ninja-0.3.0/src/sqlguard_ninja/policy/tools.py +410 -0
  26. sqlguard_ninja-0.3.0/src/sqlguard_ninja/tenancy.py +160 -0
  27. sqlguard_ninja-0.3.0/tests/security/test_tenant_rewrite.py +116 -0
  28. sqlguard_ninja-0.3.0/tests/unit/test_policy_inheritance.py +162 -0
  29. sqlguard_ninja-0.3.0/tests/unit/test_policy_tools.py +254 -0
  30. sqlguard_ninja-0.2.0/docs/development/IMPLEMENTATION_PROGRESS.md +0 -7
  31. sqlguard_ninja-0.2.0/docs/development/NEXT_ACTIONS.md +0 -5
  32. sqlguard_ninja-0.2.0/docs/development/TEST_EVIDENCE.md +0 -7
  33. sqlguard_ninja-0.2.0/src/sqlguard_ninja/_version.py +0 -1
  34. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/.gitignore +0 -0
  35. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/LICENSE +0 -0
  36. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/THIRD_PARTY_NOTICES.md +0 -0
  37. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/baselines/native.json +0 -0
  38. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/external/README.md +0 -0
  39. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/LABEL_CHANGES.yaml +0 -0
  40. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/01_destructive.yaml +0 -0
  41. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/02_multi_statement_and_formatting.yaml +0 -0
  42. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/03_table_and_column_access.yaml +0 -0
  43. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/04_privacy.yaml +0 -0
  44. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/05_tenant_isolation.yaml +0 -0
  45. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/06_cte_nested_unicode.yaml +0 -0
  46. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/07_legitimate_lookalike_cost_dialect.yaml +0 -0
  47. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/cases/08_lineage.yaml +0 -0
  48. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/generated/grant_matrix.yaml +0 -0
  49. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/generated/label_matrix.yaml +0 -0
  50. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/generated/tenant_matrix.yaml +0 -0
  51. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/generated/transforms.yaml +0 -0
  52. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/datasets/v1/manifest.yaml +0 -0
  53. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/policies/retail_saas.yaml +0 -0
  54. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/agent-sql-guard.json +0 -0
  55. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/agent-sql-guard.md +0 -0
  56. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/krish2105-sqlguard.json +0 -0
  57. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/krish2105-sqlguard.md +0 -0
  58. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/native.json +0 -0
  59. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/native.md +0 -0
  60. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/sql-guardrail.json +0 -0
  61. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.0.0/sql-guardrail.md +0 -0
  62. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/agent-sql-guard.csv +0 -0
  63. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/agent-sql-guard.json +0 -0
  64. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/agent-sql-guard.md +0 -0
  65. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.csv +0 -0
  66. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.json +0 -0
  67. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/krish2105-sqlguard.md +0 -0
  68. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/native.csv +0 -0
  69. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/native.json +0 -0
  70. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/native.md +0 -0
  71. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/sql-guardrail.csv +0 -0
  72. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/sql-guardrail.json +0 -0
  73. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/benchmarks/results/v1.1.0/sql-guardrail.md +0 -0
  74. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/benchmarks/RESULTS_v1.1.md +0 -0
  75. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/benchmarks/RESULTS_v1.md +0 -0
  76. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/development/ARCHITECTURE_MAP.md +0 -0
  77. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/development/DECISION_LOG.md +0 -0
  78. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/development/UPGRADE_MASTER_PLAN.md +0 -0
  79. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/guides/BENCHMARK_BIAS_REVIEW.md +0 -0
  80. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/guides/BENCHMARK_METHODOLOGY.md +0 -0
  81. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/guides/CI_INTEGRATION.md +0 -0
  82. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/guides/QUERY_COST.md +0 -0
  83. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/release/FINAL_TECHNICAL_AUDIT.md +0 -0
  84. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/releases/v0.1.0.md +0 -0
  85. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/releases/v0.1.1.md +0 -0
  86. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/releases/v0.2.0.md +0 -0
  87. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/research/COMPETITIVE_ANALYSIS.md +0 -0
  88. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/docs/security/STATIC_ANALYSIS_LIMITS.md +0 -0
  89. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/_shared.py +0 -0
  90. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/fastapi/app.py +0 -0
  91. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/github-actions/sql-security.yml +0 -0
  92. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/langchain/guarded_tool_demo.py +0 -0
  93. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/langgraph/real_llm_agent.py +0 -0
  94. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/langgraph/sql_agent_demo.py +0 -0
  95. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/postgresql/plan_inspection_demo.py +0 -0
  96. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/sdk/quickstart.py +0 -0
  97. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/examples/sqlite/text_to_sql_demo.py +0 -0
  98. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/pyproject.toml +0 -0
  99. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/__init__.py +0 -0
  100. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/__init__.py +0 -0
  101. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/adapters.py +0 -0
  102. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/builtin.py +0 -0
  103. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/compare.py +0 -0
  104. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/dataset.py +0 -0
  105. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/fixtures.py +0 -0
  106. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/metrics.py +0 -0
  107. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/runner.py +0 -0
  108. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/translate.py +0 -0
  109. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/workers/_protocol.py +0 -0
  110. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/workers/agent_sql_guard_worker.py +0 -0
  111. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/workers/krish_sqlguard_worker.py +0 -0
  112. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/benchmarking/workers/sql_guardrail_worker.py +0 -0
  113. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/cli/__init__.py +0 -0
  114. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/cli/config.py +0 -0
  115. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/cli/templates.py +0 -0
  116. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/__init__.py +0 -0
  117. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/analysis.py +0 -0
  118. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/lineage.py +0 -0
  119. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/__init__.py +0 -0
  120. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/access.py +0 -0
  121. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/base.py +0 -0
  122. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/catalog.py +0 -0
  123. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/functions.py +0 -0
  124. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/statements.py +0 -0
  125. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/structure.py +0 -0
  126. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/core/rules/tenant.py +0 -0
  127. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/__init__.py +0 -0
  128. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/executors.py +0 -0
  129. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/fastapi.py +0 -0
  130. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/langchain.py +0 -0
  131. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/integrations/langgraph.py +0 -0
  132. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/policy/__init__.py +0 -0
  133. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/privacy/__init__.py +0 -0
  134. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/privacy/rules.py +0 -0
  135. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/privacy/suggest.py +0 -0
  136. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/py.typed +0 -0
  137. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/query_cost/__init__.py +0 -0
  138. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/query_cost/postgres.py +0 -0
  139. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/query_cost/static.py +0 -0
  140. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/reporting/__init__.py +0 -0
  141. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/src/sqlguard_ninja/reporting/markdown.py +0 -0
  142. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/__init__.py +0 -0
  143. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/conftest.py +0 -0
  144. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/integration/__init__.py +0 -0
  145. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/integration/test_corpus_sqlite_validation.py +0 -0
  146. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/integration/test_external_adapters.py +0 -0
  147. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/integration/test_frameworks.py +0 -0
  148. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/integration/test_postgres.py +0 -0
  149. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/pg_support.py +0 -0
  150. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/property/__init__.py +0 -0
  151. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/property/test_guard_properties.py +0 -0
  152. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/security/__init__.py +0 -0
  153. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/security/test_agent_boundaries.py +0 -0
  154. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/security/test_corpus_native.py +0 -0
  155. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/__init__.py +0 -0
  156. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/helpers.py +0 -0
  157. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_benchmark_engine.py +0 -0
  158. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_cli.py +0 -0
  159. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_dataset.py +0 -0
  160. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_docs.py +0 -0
  161. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_function_policy_mechanics.py +0 -0
  162. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_lineage.py +0 -0
  163. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_policy.py +0 -0
  164. {sqlguard_ninja-0.2.0 → sqlguard_ninja-0.3.0}/tests/unit/test_rules.py +0 -0
@@ -3,6 +3,27 @@
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.3.0] — 2026-10-24
7
+
8
+ ### Added
9
+ * **Role inheritance** (`extends`): grants merge, explicit denies are sticky, and conflicts,
10
+ unknown parents and cycles are load errors.
11
+ * **Policy metadata and identity:** `metadata.revision/owner/change_ticket`, a policy
12
+ fingerprint, and a per-decision `decision_id`. Audit events add `policy_revision`,
13
+ `policy_fingerprint`, `decision_id`, `correlation_id`, a literal-insensitive
14
+ `query_fingerprint`, and `tenant_filter_rewrite`.
15
+ * **Policy CLI:** `policy template`, `policy diff` (effect classification,
16
+ `--fail-on-relaxation`), `policy simulate` (`--against`, `--fail-on-change`),
17
+ `policy explain` (decision or column access, with inheritance source).
18
+ * **Templates:** `minimal`, `multi_tenant_saas`, `analytics_readonly`.
19
+ * **Opt-in AST tenant-filter rewrite** (`sqlguard_ninja.tenancy`,
20
+ `SQLCheckpoint(rewrite_tenant_filters=True)`) with re-validation and refusal rules (ADR-011).
21
+ * Docs: `docs/guides/POLICY_AS_CODE.md`; ADR-011, ADR-012; threat model T6a/T6b.
22
+
23
+ ### Compatibility
24
+ * Policy files without the new fields behave exactly as before. `ValidationResult` gains
25
+ fields with defaults.
26
+
6
27
  ## [0.2.0] — 2026-10-24
7
28
 
8
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlguard-ninja
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -246,6 +246,23 @@ Labels also apply to paths inside JSON columns (`profile->'health'->>'allergies'
246
246
  suggestions are never enforced. This is a technical control, not a certification of GDPR
247
247
  compliance. Full semantics: [POLICY_REFERENCE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md).
248
248
 
249
+ ## Policy as code
250
+
251
+ Roles can inherit (`extends`), and explicit denies cannot be relaxed by a child. Every decision
252
+ carries the policy revision, the policy fingerprint and a decision id. Review changes before
253
+ activating them:
254
+
255
+ ```bash
256
+ sqlninja policy template multi_tenant_saas --output policy.yaml
257
+ sqlninja policy diff old.yaml policy.yaml --fail-on-relaxation
258
+ sqlninja policy simulate --policy old.yaml --against policy.yaml --cases cases.yaml
259
+ sqlninja policy explain --policy policy.yaml --role support --column customers.email
260
+ ```
261
+
262
+ Optional: `SQLCheckpoint(..., rewrite_tenant_filters=True)` adds missing tenant filters
263
+ through the AST and executes only the re-validated rewrite. See
264
+ [POLICY_AS_CODE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md).
265
+
249
266
  ## Query cost analysis
250
267
 
251
268
  Every validation includes an explainable static score that uses the catalog's
@@ -340,6 +357,7 @@ writes, more tested dialects, and SARIF output. Details in [ROADMAP.md](https://
340
357
  ## Documentation
341
358
 
342
359
  * [Policy reference](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md)
360
+ * [Policy as code](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md)
343
361
  * [Agent integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/AGENT_INTEGRATION.md)
344
362
  * [Query cost](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/QUERY_COST.md)
345
363
  * [CI integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/CI_INTEGRATION.md)
@@ -190,6 +190,23 @@ Labels also apply to paths inside JSON columns (`profile->'health'->>'allergies'
190
190
  suggestions are never enforced. This is a technical control, not a certification of GDPR
191
191
  compliance. Full semantics: [POLICY_REFERENCE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md).
192
192
 
193
+ ## Policy as code
194
+
195
+ Roles can inherit (`extends`), and explicit denies cannot be relaxed by a child. Every decision
196
+ carries the policy revision, the policy fingerprint and a decision id. Review changes before
197
+ activating them:
198
+
199
+ ```bash
200
+ sqlninja policy template multi_tenant_saas --output policy.yaml
201
+ sqlninja policy diff old.yaml policy.yaml --fail-on-relaxation
202
+ sqlninja policy simulate --policy old.yaml --against policy.yaml --cases cases.yaml
203
+ sqlninja policy explain --policy policy.yaml --role support --column customers.email
204
+ ```
205
+
206
+ Optional: `SQLCheckpoint(..., rewrite_tenant_filters=True)` adds missing tenant filters
207
+ through the AST and executes only the re-validated rewrite. See
208
+ [POLICY_AS_CODE.md](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md).
209
+
193
210
  ## Query cost analysis
194
211
 
195
212
  Every validation includes an explainable static score that uses the catalog's
@@ -284,6 +301,7 @@ writes, more tested dialects, and SARIF output. Details in [ROADMAP.md](https://
284
301
  ## Documentation
285
302
 
286
303
  * [Policy reference](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_REFERENCE.md)
304
+ * [Policy as code](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/POLICY_AS_CODE.md)
287
305
  * [Agent integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/AGENT_INTEGRATION.md)
288
306
  * [Query cost](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/QUERY_COST.md)
289
307
  * [CI integration](https://github.com/Pratik653/SQLGuard_Ninjapy/blob/main/docs/guides/CI_INTEGRATION.md)
@@ -122,6 +122,25 @@ Extras: `langchain`, `langgraph`, `fastapi`, `postgres` (`psycopg`), `dev`.
122
122
  Apache-2.0 provides an explicit patent grant, which matters to enterprise adopters. It is
123
123
  compatible with all dependencies we use (MIT, BSD, Apache-2.0). No third-party code is vendored.
124
124
 
125
+ ## ADR-011 — Opt-in tenant-filter rewrite (refines ADR-005)
126
+
127
+ **Context.** Enterprise users want missing tenant filters added rather than rejected.
128
+
129
+ **Decision.** `sqlguard_ninja.tenancy.apply_tenant_filters` may add tenant predicates through
130
+ the AST. It is opt-in (`rewrite_tenant_filters=True`), uses bind parameters only, refuses on any
131
+ non-tenant problem or ambiguous ownership, and **re-validates** the rewritten SQL, which is the
132
+ exact string executed. ADR-005's invariant still holds: the validated string and the executed
133
+ string are identical.
134
+
135
+ **Consequences.** The rewritten SQL is the qualified form produced by sqlglot. It is equivalent
136
+ but may look different from the input. Database RLS remains the authoritative control.
137
+
138
+ ## ADR-012 — Role inheritance with sticky explicit denies
139
+
140
+ **Decision.** `extends` merges grants. A child cannot relax an inherited explicit deny, and an
141
+ attempt is a load error. `policy diff` works on the effective (inherited) policy and classifies
142
+ relaxations so reviewers and CI can gate them.
143
+
125
144
  ## ADR-010 — Read-only scope for v0.1
126
145
 
127
146
  **Decision.** v0.1 validates read-only workloads only. All writes are denied. Supporting governed
@@ -0,0 +1,11 @@
1
+ # Implementation progress
2
+
3
+ | Phase | Status | Commit | Tests | Notes |
4
+ |---|---|---|---|---|
5
+ | 0 Audit and checkpoint files | IMPLEMENTED | 375c52f^ | — | baseline v0.1.1 verified: clean tree, main@5206e96, PyPI 0.1.1 |
6
+ | 1a Benchmark tiers, basis, F1, Wilson CIs, throughput, CSV, hashes | RELEASED v0.2.0 | 375c52f | focused + full gate | |
7
+ | 1b Lineage: aggregate-only through CTEs, tenant push-down, small-cohort through derived scopes | RELEASED v0.2.0 | 8b59a38 | corpus 1.1.0 (21 boundary cases), 11 unit tests | LIN-012 found a real bypass of the small-cohort rule before release; fixed |
8
+ | 2a Role inheritance (sticky denies), revision/fingerprint, decision ids, audit fields | VERIFIED (local) | 2f1919c | test_policy_inheritance (11) | |
9
+ | 2b Policy diff/simulate/explain/templates CLI | VERIFIED (local) | e64859a | test_policy_tools (21) | |
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 | VERIFIED (local) | see PR | full gate 1,804 passed, 94% | |
@@ -0,0 +1,5 @@
1
+ # Next actions (development program)
2
+
3
+ 1. Merge the v0.3.0 PR, tag v0.3.0, release, verify PyPI and a fresh install.
4
+ 2. Phase 3 (v0.3.x or v0.4.0): MCP stdio server with validation-only tools (`validate_sql`, `explain_policy_decision`, `inspect_query_cost`, `get_policy_capabilities`), trusted role and tenant from server config, tested with a real MCP client (the official Python SDK client over stdio).
5
+ 3. Workstream 1 open items: independent dataset (#6), function/catalog cases (#5).
@@ -0,0 +1,12 @@
1
+ # Test evidence
2
+
3
+ | Date | Scope | Command | Result |
4
+ |---|---|---|---|
5
+ | 2026-10-24 | Baseline (v0.1.1) | `pytest --cov` | 1,655 passed, 95% (local, Python 3.11) |
6
+ | 2026-10-24 | v0.2.0 candidate | full gate (`ruff`, `mypy`, corpus `--check`, `pytest --cov`, build, `twine check`) | 1,738 passed, 94% branch coverage; twine PASSED |
7
+ | 2026-10-24 | Benchmark, corpus 1.1.0 | `sqlninja benchmark run --dataset builtin:v1` (4 adapters) | see docs/benchmarks/RESULTS_v1.1.md |
8
+ | 2026-10-24 | v0.2.0 PR #17 | CI run on PR | 12/12 jobs green; gate: baseline from main, 4 approved relabels, 0 regressions |
9
+ | 2026-10-24 | v0.2.0 main | CI run 40107938223 on 8b59a38 | success |
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
+ | 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
+ | 2026-10-24 | v0.3.0 candidate | full gate | 1,804 passed, 94%; build + twine PASSED; 3 templates in wheel |
@@ -13,7 +13,8 @@
13
13
  ],
14
14
  "evidence": [
15
15
  "tiers, basis, F1, Wilson CIs, CSV, hashes: VERIFIED locally",
16
- "no independent dataset yet (#6); function/catalog cases absent (#5)"
16
+ "no independent dataset yet (#6); function/catalog cases absent (#5)",
17
+ "v0.2.0 released: tiers/CIs/CSV/hashes; still no independent data"
17
18
  ],
18
19
  "blockers": [
19
20
  "external dataset needs licensed independent source (#6)"
@@ -22,24 +23,27 @@
22
23
  {
23
24
  "id": 2,
24
25
  "name": "Lineage-aware privacy",
25
- "status": "VERIFIED",
26
+ "status": "RELEASED",
26
27
  "release": "v0.2.0",
27
28
  "issues": [
28
29
  8,
29
30
  9
30
31
  ],
31
32
  "evidence": [
32
- "corpus 1.1.0 lineage cases + tests/unit/test_lineage.py; full gate 1,738 passed"
33
+ "corpus 1.1.0 lineage cases + tests/unit/test_lineage.py; full gate 1,738 passed",
34
+ "v0.2.0 on PyPI; fresh install verified"
33
35
  ],
34
36
  "blockers": []
35
37
  },
36
38
  {
37
39
  "id": 3,
38
40
  "name": "Policy-as-code and multitenancy",
39
- "status": "NOT_STARTED",
41
+ "status": "VERIFIED",
40
42
  "release": "v0.3.0",
41
43
  "issues": [],
42
- "evidence": [],
44
+ "evidence": [
45
+ "inheritance, diff/simulate/explain/templates, tenant rewrite, audit metadata; 1,804 tests pass locally"
46
+ ],
43
47
  "blockers": []
44
48
  },
45
49
  {
@@ -103,4 +107,4 @@
103
107
  ]
104
108
  }
105
109
  ]
106
- }
110
+ }
@@ -19,6 +19,9 @@ LLM ──SQL──▶ SQLCheckpoint.evaluate(sql, RequestContext) ──▶ aut
19
19
  | `review` | not run | run only with an approval bound to the SQL hash, trace and principal (single use, TTL, no self-approval) | run |
20
20
  | `audit` | run, recorded `enforced=False` | run, recorded | run |
21
21
 
22
+ `rewrite_tenant_filters=True` (off by default) adds missing tenant filters through the AST and
23
+ executes the re-validated rewrite. See [POLICY_AS_CODE.md](POLICY_AS_CODE.md).
24
+
22
25
  `max_denied_attempts` (default 5) locks a trace after repeated denials, so a retry loop
23
26
  cannot grind through rephrasings.
24
27
 
@@ -0,0 +1,97 @@
1
+ # Policy as code
2
+
3
+ Policies are versioned files reviewed like code. This guide covers inheritance, metadata,
4
+ templates and the review workflow. Field reference:
5
+ [POLICY_REFERENCE.md](POLICY_REFERENCE.md).
6
+
7
+ ## Metadata and identity
8
+
9
+ ```yaml
10
+ version: 1 # schema version
11
+ name: shop
12
+ metadata:
13
+ revision: "2026.10.2" # your revision; recorded in every decision and audit event
14
+ owner: data-governance
15
+ change_ticket: SEC-1234
16
+ ```
17
+
18
+ Every `ValidationResult` and audit event records:
19
+ - `decision_id`: unique per decision
20
+ - `policy_revision`
21
+ - `policy_fingerprint`: a 16-hex-character content hash that changes with any semantic change
22
+ Audit events also record the `correlation_id` (the request trace id) and a `query_fingerprint`,
23
+ which is the SHA-256 of the literal-redacted SQL. Queries that differ only in literal values
24
+ share a fingerprint, and literal values themselves are never logged.
25
+
26
+ ## Role inheritance
27
+
28
+ ```yaml
29
+ roles:
30
+ tenant_base:
31
+ tenant_scoped: true
32
+ labels: {payment_card: deny}
33
+ support:
34
+ extends: [tenant_base]
35
+ tables: {customers: "*", orders: [id, tenant_id, status]}
36
+ labels: {contact: allow}
37
+ ```
38
+
39
+ * Grants are merged. Column lists are unioned, and `"*"` wins.
40
+ * Label access and `column_access` override per key, and `limits` come from the nearest role
41
+ that sets them.
42
+ * **Explicit denies are sticky.** A child cannot relax an inherited deny label, deny column
43
+ rule, `deny_columns` entry, or `tenant_scoped: true`. The attempt is a load error (a
44
+ *conflict*), not a silent override.
45
+ * Unknown parents and cycles are load errors. Grants inherited from parents are validated
46
+ against the catalog just like direct grants.
47
+
48
+ `sqlninja policy explain --policy P --role R --column table.column` shows the effective access
49
+ and which role in the chain set each label.
50
+
51
+ ## Templates
52
+
53
+ ```bash
54
+ sqlninja policy template # list: minimal, multi_tenant_saas, analytics_readonly
55
+ sqlninja policy template multi_tenant_saas --output policies/policy.yaml
56
+ ```
57
+
58
+ ## Reviewing a change before activation
59
+
60
+ ```bash
61
+ sqlninja policy diff policies/old.yaml policies/new.yaml # every change, classified
62
+ sqlninja policy diff old.yaml new.yaml --fail-on-relaxation # CI: block silent relaxations
63
+ sqlninja policy simulate --policy old.yaml --against new.yaml --cases examples.yaml
64
+ sqlninja policy explain --policy new.yaml --role support --tenant 1 --sql "SELECT ..."
65
+ ```
66
+
67
+ * `diff` compares the **effective** policies, after inheritance. It classifies each change as
68
+ `relaxes` (can only allow more), `restricts` or `neutral`. Covered areas: roles, table and
69
+ column grants, `deny_columns`, column rules, label access, `tenant_scoped`, catalog labels
70
+ and tenant columns, function policy, limits, defaults, cost thresholds and disclosure.
71
+ * `simulate` evaluates example queries (`{id, sql, role, context, expect}`) under the current
72
+ policy and, optionally, a candidate. It reports decision changes and expectation mismatches,
73
+ and exits 1 on mismatch, or with `--fail-on-change` when any decision changes.
74
+ * `explain` prints the structured decision: rule ids, messages, internal detail and lineage.
75
+ Use it for operators. Agent-facing messages remain the redacted `agent_message`.
76
+
77
+ Suggested CI step for policy repositories:
78
+
79
+ ```yaml
80
+ - run: sqlninja policy diff "$BASE_POLICY" policies/policy.yaml --fail-on-relaxation
81
+ - run: sqlninja policy simulate --policy policies/policy.yaml --cases policies/cases.yaml
82
+ ```
83
+
84
+ ## Tenant isolation: inspection, rewrite, and the database
85
+
86
+ 1. **Inspection (default).** Every scope reading a tenant table must pin it to the context
87
+ tenant. Otherwise the query is denied (see the methodology for the exact semantics).
88
+ 2. **Opt-in rewrite.** `SQLCheckpoint(..., rewrite_tenant_filters=True)` or
89
+ `sqlguard_ninja.tenancy.apply_tenant_filters()` adds the missing
90
+ `alias.tenant_column = <bind parameter>` predicates through the AST, then **re-validates**.
91
+ The rewritten string is what gets executed. It refuses when the query has any other
92
+ problem (a forbidden column, another tenant's id, a write), when ownership is ambiguous
93
+ (the preserved side of a RIGHT/FULL join), or when the rewritten SQL fails validation. The
94
+ tenant value is always a bind parameter, never inlined.
95
+ 3. **The database (authoritative).** Use PostgreSQL row-level security, or per-tenant
96
+ credentials, as the real boundary. SQL inspection and rewriting are defence in depth. They
97
+ cannot see views, functions or data an attacker reaches by other means.
@@ -9,6 +9,7 @@ start-up instead of silently changing behaviour. Run `sqlninja policy check --po
9
9
  ```yaml
10
10
  version: 1
11
11
  name: shop
12
+ metadata: {revision: "2026.10.1", owner: data-governance} # recorded in every decision
12
13
  dialect: postgres # default dialect for validate()
13
14
  disclosure: names # or "generic": agent messages never name tables/columns
14
15
 
@@ -35,6 +36,7 @@ catalog:
35
36
 
36
37
  roles:
37
38
  support:
39
+ # extends: [base_role] # inherit grants; inherited explicit denies cannot be relaxed
38
40
  tables:
39
41
  customers: [id, tenant_id, email, email_domain] # or "*", or {columns, deny_columns, column_access}
40
42
  labels: {contact: allow, health: deny}
@@ -83,6 +85,9 @@ cost: {enabled: true, review_score: 50, deny_score: 80, large_table_rows: 100000
83
85
 
84
86
  Without roles, an implicit role grants every catalog table and denies every labelled column.
85
87
 
88
+ Inheritance, templates, `policy diff` / `simulate` / `explain` and the tenant-filter rewrite
89
+ are described in [POLICY_AS_CODE.md](POLICY_AS_CODE.md).
90
+
86
91
  ## Decisions
87
92
 
88
93
  Each rule emits findings with an action (`deny`, `review`, `warn`). Any deny → **DENY**,
@@ -0,0 +1,29 @@
1
+ # SQLGuard Ninja v0.3.0
2
+
3
+ Enterprise policy-as-code and tenant enforcement.
4
+
5
+ ## Highlights
6
+
7
+ * **Role inheritance** with sticky explicit denies: a child role can add permissions, but cannot undo an inherited deny. Conflicts, unknown parents and cycles fail at load time.
8
+ * **Review before activation:** `sqlninja policy diff` classifies every change as relaxes / restricts / neutral (`--fail-on-relaxation` for CI). `policy simulate --against` shows decision changes on example queries, and `policy explain` traces effective access through the inheritance chain.
9
+ * **Auditability:** every decision has a `decision_id`, the policy revision and fingerprint. Audit events add a correlation id, a literal-insensitive query fingerprint and a tenant-rewrite flag.
10
+ * **Templates:** `minimal`, `multi_tenant_saas`, `analytics_readonly`.
11
+ * **Opt-in AST tenant-filter rewrite:** missing tenant pins are added as bind-parameter predicates and the rewrite is re-validated. It is refused on any other problem or on RIGHT/FULL-join ownership ambiguity.
12
+
13
+ ## Compatibility
14
+
15
+ Existing policies behave as before; the new fields are optional.
16
+
17
+ ## Known limitations
18
+
19
+ * All benchmark results are internal. There is no independent dataset yet (issue #6), and no restricted-function or system-catalog cases (issue #5).
20
+ * The tenant rewrite emits sqlglot-normalised SQL. Database row-level security remains the authoritative tenant control.
21
+ * Only PostgreSQL and SQLite are supported; writes are denied; no guarantee against SQL injection or data leakage.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ python -m pip install sqlguard-ninja==0.3.0
27
+ ```
28
+
29
+ PyPI: https://pypi.org/project/sqlguard-ninja/0.3.0/
@@ -57,6 +57,8 @@ 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
+ | 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
+ | 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 |
60
62
  | 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. |
61
63
  | T7 | Unauthorized columns | `SELECT *`, whole-row references, alias laundering, columns in filters | Schema-based star expansion. Per-scope base-column attribution. Ungranted columns are denied in any clause. | Views and UDFs hide columns. |
62
64
  | T8 | Sensitive-data exposure | card numbers, health data in JSON, salary per person | Explicit labels. Deny / aggregate-only / review per role. JSON path labels. Window aggregates count as value-revealing. Small-cohort review. | Inference attacks across queries. |
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -36,6 +36,15 @@ from sqlguard_ninja.cli.templates import FILES
36
36
  from sqlguard_ninja.core.findings import Decision
37
37
  from sqlguard_ninja.core.guard import Guard
38
38
  from sqlguard_ninja.policy.model import Policy, PolicyError
39
+ from sqlguard_ninja.policy.tools import (
40
+ TEMPLATES,
41
+ diff_policies,
42
+ explain_column,
43
+ explain_result,
44
+ load_sim_cases,
45
+ simulate,
46
+ template_text,
47
+ )
39
48
  from sqlguard_ninja.privacy.suggest import suggest_labels
40
49
  from sqlguard_ninja.reporting.markdown import benchmark_report, comparison_report
41
50
 
@@ -154,7 +163,9 @@ def cmd_policy_check(args: argparse.Namespace) -> int:
154
163
  warnings = []
155
164
  if policy.functions.mode == "denylist":
156
165
  warnings.append("functions.mode is 'denylist'; 'allowlist' is safer for production")
157
- for name, role in policy.roles.items():
166
+ for name in policy.roles:
167
+ role = policy.effective_role(name)
168
+ assert role is not None
158
169
  if not role.tenant_scoped and tenant_tables:
159
170
  warnings.append(f"role '{name}' is not tenant-scoped")
160
171
  for w in warnings:
@@ -174,6 +185,94 @@ def cmd_policy_suggest(args: argparse.Namespace) -> int:
174
185
  return EXIT_OK
175
186
 
176
187
 
188
+ def cmd_policy_template(args: argparse.Namespace) -> int:
189
+ if not args.kind:
190
+ _out("available templates: " + ", ".join(TEMPLATES))
191
+ return EXIT_OK
192
+ try:
193
+ text = template_text(args.kind)
194
+ except KeyError as exc:
195
+ raise CLIError(str(exc.args[0])) from exc
196
+ if args.output:
197
+ path = Path(args.output)
198
+ if path.exists() and not args.force:
199
+ raise CLIError(f"refusing to overwrite {path}; use --force")
200
+ path.parent.mkdir(parents=True, exist_ok=True)
201
+ path.write_text(text, encoding="utf-8")
202
+ _out(f"wrote {path}")
203
+ else:
204
+ _out(text)
205
+ return EXIT_OK
206
+
207
+
208
+ def cmd_policy_diff(args: argparse.Namespace) -> int:
209
+ old, new = _load_policy(args.old), _load_policy(args.new)
210
+ diff = diff_policies(old, new)
211
+ if args.format == "json":
212
+ _out(json.dumps(diff.as_dict(), indent=2, default=str))
213
+ else:
214
+ _out(
215
+ f"policy diff {old.revision} ({old.fingerprint}) -> {new.revision} "
216
+ f"({new.fingerprint}): {len(diff.changes)} change(s), "
217
+ f"{len(diff.relaxations)} relaxation(s)"
218
+ )
219
+ for c in diff.changes:
220
+ _out(f" [{c.effect.value:9}] {c.path}: {c.before!s} -> {c.after!s}")
221
+ if args.fail_on_relaxation and diff.relaxations:
222
+ return EXIT_FAIL
223
+ return EXIT_OK
224
+
225
+
226
+ def cmd_policy_simulate(args: argparse.Namespace) -> int:
227
+ policy = _load_policy(args.policy)
228
+ against = _load_policy(args.against) if args.against else None
229
+ try:
230
+ cases = load_sim_cases(args.cases)
231
+ except (OSError, ValueError, yaml.YAMLError) as exc:
232
+ raise CLIError(f"cannot load cases {args.cases}: {exc}") from exc
233
+ rows = simulate(policy, cases, against)
234
+ if args.format == "json":
235
+ _out(json.dumps(rows, indent=2))
236
+ else:
237
+ for r in rows:
238
+ line = f"{r['id']:>6} {r['role'] or '-':18} {r['decision']:6}"
239
+ if against is not None:
240
+ marker = " CHANGED" if r["changed"] else ""
241
+ line += f" -> {r['candidate_decision']:6}{marker}"
242
+ if r["expect"] is not None:
243
+ line += f" expect={r['expect']}"
244
+ _out(line)
245
+ key = "candidate_matches_expectation" if against is not None else "matches_expectation"
246
+ mismatches = sum(1 for r in rows if r.get(key) is False)
247
+ changed = sum(1 for r in rows if r.get("changed"))
248
+ _out(f"{len(rows)} case(s); {mismatches} expectation mismatch(es); {changed} changed")
249
+ if mismatches or (args.fail_on_change and changed):
250
+ return EXIT_FAIL
251
+ return EXIT_OK
252
+
253
+
254
+ def cmd_policy_explain(args: argparse.Namespace) -> int:
255
+ policy = _load_policy(args.policy)
256
+ if bool(args.sql) == bool(args.column):
257
+ raise CLIError("give exactly one of --sql or --column TABLE.COLUMN")
258
+ if args.column:
259
+ table, _, column = args.column.partition(".")
260
+ try:
261
+ payload = explain_column(policy, args.role, table.lower(), column.lower())
262
+ except KeyError as exc:
263
+ raise CLIError(str(exc.args[0])) from exc
264
+ else:
265
+ context: dict[str, Any] = {}
266
+ if args.tenant is not None:
267
+ context[policy.tenant.context_key] = args.tenant
268
+ result = Guard(policy).validate(
269
+ args.sql, role=args.role, dialect=args.dialect, context=context
270
+ )
271
+ payload = explain_result(result)
272
+ _out(json.dumps(payload, indent=2, default=str))
273
+ return EXIT_OK
274
+
275
+
177
276
  def cmd_init(args: argparse.Namespace) -> int:
178
277
  root = Path(args.directory)
179
278
  existing = [name for name in FILES if (root / name).exists()]
@@ -390,6 +489,34 @@ def build_parser() -> argparse.ArgumentParser:
390
489
  common(ps)
391
490
  ps.add_argument("--policy")
392
491
  ps.set_defaults(func=cmd_policy_suggest)
492
+ pt = pol.add_parser("template", help="print or write a policy template")
493
+ pt.add_argument("kind", nargs="?", help="template name (omit to list)")
494
+ pt.add_argument("--output")
495
+ pt.add_argument("--force", action="store_true")
496
+ pt.set_defaults(func=cmd_policy_template)
497
+ pd = pol.add_parser("diff", help="semantic diff of two policies, flagging relaxations")
498
+ pd.add_argument("old")
499
+ pd.add_argument("new")
500
+ pd.add_argument("--format", choices=["text", "json"], default="text")
501
+ pd.add_argument("--fail-on-relaxation", action="store_true")
502
+ pd.set_defaults(func=cmd_policy_diff)
503
+ psim = pol.add_parser(
504
+ "simulate", help="evaluate example queries; optionally against a candidate"
505
+ )
506
+ psim.add_argument("--policy", required=True)
507
+ psim.add_argument("--cases", required=True, help="YAML list of {sql, role, context, expect}")
508
+ psim.add_argument("--against", help="candidate policy to compare with")
509
+ psim.add_argument("--format", choices=["text", "json"], default="text")
510
+ psim.add_argument("--fail-on-change", action="store_true")
511
+ psim.set_defaults(func=cmd_policy_simulate)
512
+ pe = pol.add_parser("explain", help="explain a decision or a column's effective access")
513
+ pe.add_argument("--policy", required=True)
514
+ pe.add_argument("--role", required=True)
515
+ pe.add_argument("--sql")
516
+ pe.add_argument("--column", help="TABLE.COLUMN")
517
+ pe.add_argument("--dialect")
518
+ pe.add_argument("--tenant")
519
+ pe.set_defaults(func=cmd_policy_explain)
393
520
 
394
521
  bench = sub.add_parser("benchmark", help="run and compare benchmarks").add_subparsers(
395
522
  dest="bench_cmd", required=True
@@ -97,6 +97,10 @@ class ValidationResult(BaseModel):
97
97
  role: str | None
98
98
  policy_name: str
99
99
  policy_version: int
100
+ policy_revision: str = "unversioned"
101
+ policy_fingerprint: str = ""
102
+ decision_id: str = ""
103
+ """Unique id of this decision (correlates the result, audit event and logs)."""
100
104
  tables: tuple[str, ...] = ()
101
105
  columns: tuple[str, ...] = ()
102
106
  elapsed_ms: float = 0.0
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import hashlib
6
6
  import time
7
+ import uuid
7
8
  from collections.abc import Iterable, Mapping, Sequence
8
9
  from typing import Any
9
10
 
@@ -176,6 +177,9 @@ class Guard:
176
177
  role=role_name,
177
178
  policy_name=self.policy.name,
178
179
  policy_version=self.policy.version,
180
+ policy_revision=self.policy.revision,
181
+ policy_fingerprint=self.policy.fingerprint,
182
+ decision_id=uuid.uuid4().hex,
179
183
  tables=tuple(analysis.base_table_names) if analysis else (),
180
184
  columns=tuple(sorted({f"{c.table}.{c.column}" for c in analysis.columns}))
181
185
  if analysis
@@ -60,8 +60,16 @@ class AuditEvent(BaseModel):
60
60
  would_block: bool
61
61
  sql_sha256: str
62
62
  sql_redacted: str | None
63
+ query_fingerprint: str | None = None
64
+ """SHA-256 of the redacted SQL: equal for queries that differ only in literals."""
63
65
  dialect: str
64
66
  policy: str
67
+ policy_revision: str = "unversioned"
68
+ policy_fingerprint: str = ""
69
+ decision_id: str = ""
70
+ correlation_id: str | None = None
71
+ tenant_filter_rewrite: bool = False
72
+ """True when the executed SQL is the AST-rewritten form with tenant filters added."""
65
73
  findings: list[dict[str, Any]]
66
74
  approval: dict[str, Any] | None = None
67
75
  attempt: int = 1