asknetworkstate / guardrail.py
EYEDOL's picture
Upload 10 files
464c149 verified
Raw History Blame Contribute Delete
2.28 kB
"""
Validates LLM-generated SQL before it touches the database.
Rules enforced:
- Exactly one statement, and it must be a SELECT.
- No DDL/DML keywords anywhere (belt and suspenders alongside the read-only
DB connection used in app.py).
- Every table referenced must be in the whitelist.
- A LIMIT is added if the query doesn't already have one and isn't an
aggregate-only query.
"""
import re
ALLOWED_TABLES = {
"devices",
"interfaces",
"traffic_flows",
"alerts",
"metrics_timeseries",
}
FORBIDDEN_KEYWORDS = {
"insert", "update", "delete", "drop", "alter", "create",
"truncate", "replace", "attach", "detach", "pragma", "vacuum",
}
DEFAULT_LIMIT = 50
class SQLGuardrailError(Exception):
pass
def _extract_tables(sql: str) -> set:
# crude but effective for this schema: look for tokens after FROM/JOIN
tables = set()
for m in re.finditer(r"\b(?:from|join)\s+([a-zA-Z_][a-zA-Z0-9_]*)", sql, re.IGNORECASE):
tables.add(m.group(1).lower())
return tables
def validate_and_sanitize(sql: str) -> str:
"""Raises SQLGuardrailError on anything unsafe. Returns a safe SQL string."""
if sql is None:
raise SQLGuardrailError("Model returned no SQL.")
cleaned = sql.strip().strip("`").strip()
# strip a leading "sql" language tag if the model added a fenced block anyway
cleaned = re.sub(r"^sql\s*", "", cleaned, flags=re.IGNORECASE)
cleaned = cleaned.rstrip(";").strip()
if not cleaned:
raise SQLGuardrailError("Empty SQL after cleaning.")
if ";" in cleaned:
raise SQLGuardrailError("Multiple statements are not allowed.")
lowered = cleaned.lower()
if not lowered.startswith("select"):
raise SQLGuardrailError("Only SELECT statements are allowed.")
tokens = set(re.findall(r"[a-zA-Z_]+", lowered))
forbidden_hit = tokens & FORBIDDEN_KEYWORDS
if forbidden_hit:
raise SQLGuardrailError(f"Forbidden keyword(s) detected: {forbidden_hit}")
referenced = _extract_tables(cleaned)
unknown = referenced - ALLOWED_TABLES
if unknown:
raise SQLGuardrailError(f"Unknown table(s) referenced: {unknown}")
if "limit" not in lowered:
cleaned = f"{cleaned} LIMIT {DEFAULT_LIMIT}"
return cleaned + ";"