Spaces:
Build error
Build error
File size: 2,279 Bytes
464c149 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | """
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 + ";"
|