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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ BIN := $(VENV)/bin
PIP := $(BIN)/pip
PY := $(BIN)/python

.PHONY: install seed test demo clean
.PHONY: install seed test demo seal clean

install: $(VENV)/pyvenv.cfg
$(PIP) install -e ".[dev,postgres,mysql]"
Expand All @@ -23,8 +23,12 @@ demo: seed
$(PY) scripts/demo.py
$(PY) scripts/demo_walkthrough.py

# Exactly 4 core cases; exit non-zero on mismatch. See SEAL.md.
seal: seed
$(PY) scripts/seal.py

clean:
rm -rf $(VENV) src/write_gate.egg-info .pytest_cache
rm -f seed/warehouse.duckdb seed/warehouse.duckdb.wal seed/orders.csv
rm -f .logs/*.jsonl
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

一句话:**SQLGuard**(仓库名 `sql-write-gate`)= 面向 AI Agent 的 **SQL 安全执行网关**;简历项目名建议写 **SQLGuard**。

**Seal (3 min):** [SEAL.md](SEAL.md) — `make seal` runs exactly 4 core cases and prints `ALLOW`/`BLOCK` + `rule_id` + evidence.
Expected: legal→ALLOW/ok · PII→BLOCK/pii_column · schema→BLOCK/schema_hallucination · expired→BLOCK/expired_partition.
非生产唯一边界 — not the sole production DB security boundary.


- [GameStream](https://github.com/tangyf07/GameStream)(指标 / ADS)
- [DataPilot](https://github.com/tangyf07/DataPilot)(问数 → Text2SQL → 出站门禁)
- 本仓:SQLGuard 执行前 BLOCK / EXECUTE
Expand Down Expand Up @@ -58,7 +63,7 @@ sql-write-gate check "DELETE FROM users"
## Try it

```bash
make install && make test && make demo
make install && make test && make seal # or: make demo
sql-write-gate check "DELETE FROM orders"
# → BLOCKED delete_without_where
sql-write-gate datapilot --json "SELECT o.order_id FROM orders o CROSS JOIN orders p"
Expand Down
22 changes: 22 additions & 0 deletions SEAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SQLGuard seal (3 minutes)

**What:** Deterministic SQL write gate for AI agents — ALLOW/BLOCK with rule_id + evidence. No LLM.

**One command:**

```bash
make seal
```

**Expected 4 outcomes (stable order):**

| # | Case | Verdict | rule_id |
|---|------|---------|--------|
| 1 | legal write | ALLOW | `ok` |
| 2 | PII write | BLOCK | `pii_column` |
| 3 | schema mismatch | BLOCK | `schema_hallucination` |
| 4 | expired partition | BLOCK | `expired_partition` |

Stdout prints `ALLOW` or `BLOCK`, `rule_id`, and structured evidence JSON. Non-zero exit on mismatch.

**Boundary (honest):** 非生产唯一边界 — not the sole production DB security boundary. Pair with least-privilege DB roles, network isolation, and human review. Broader demo: `make demo`. DataPilot contract wording stays BLOCK / EXECUTE.
82 changes: 82 additions & 0 deletions scripts/seal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""SQLGuard seal: exactly four core cases (ALLOW/BLOCK + rule_id + evidence)."""

from __future__ import annotations

import json
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
SRC = ROOT / "src"
if str(SRC) not in sys.path:
sys.path.insert(0, str(SRC))

from write_gate.cases import ( # noqa: E402
EXPIRED_WRITE_SQL,
LEGAL_WRITE_SQL,
PII_WRITE_SQL,
SCHEMA_MISMATCH_SQL,
)
from write_gate.paths import DB_PATH, DEMO_POLICY_PATH # noqa: E402
from write_gate.wrapper import WriteGate # noqa: E402

# Stable seal order — do not reorder.
CASES = [
("legal_write", LEGAL_WRITE_SQL, True, "ok"),
("pii_write", PII_WRITE_SQL, False, "pii_column"),
("schema_mismatch", SCHEMA_MISMATCH_SQL, False, "schema_hallucination"),
("expired_partition", EXPIRED_WRITE_SQL, False, "expired_partition"),
]


def main() -> int:
if len(CASES) != 4:
print("SEAL_FAIL: expected exactly 4 cases", file=sys.stderr)
return 1

rc = 0
audit_path = ROOT / ".logs" / "seal_audit.jsonl"
if audit_path.exists():
audit_path.unlink()

with WriteGate(
db_path=DB_PATH,
policy_path=DEMO_POLICY_PATH,
audit_path=audit_path,
agent="seal",
actor="sqlguard-seal",
model_id="seal-offline",
prompt_summary="make seal four core cases",
) as gate:
# Idempotent vs prior seal runs (seed rows use ids 1..120).
gate.conn.execute("DELETE FROM orders WHERE order_id IN (900001, 900002, 900003, 900004)")
for name, sql, expect_allow, expect_rule in CASES:
evidence, _result = gate.execute(sql)
verdict = "ALLOW" if evidence.allowed else "BLOCK"
payload = {
"case": name,
"verdict": verdict,
"rule_id": evidence.rule_id,
"evidence": evidence.to_dict(),
}
print(json.dumps(payload, ensure_ascii=False))
print(f"{verdict} rule_id={evidence.rule_id} case={name}")
if evidence.allowed != expect_allow or evidence.rule_id != expect_rule:
print(
f"SEAL_MISMATCH: case={name} expected verdict="
f"{'ALLOW' if expect_allow else 'BLOCK'} rule_id={expect_rule} "
f"got verdict={verdict} rule_id={evidence.rule_id}",
file=sys.stderr,
)
rc = 1

if rc == 0:
print("seal: 4/4 cases matched")
else:
print("seal: FAILED", file=sys.stderr)
return rc


if __name__ == "__main__":
raise SystemExit(main())
Loading