""" Контракт защиты баз данных (SQL-Guard Contract). Предотвращает деструктивные операции в СУБД: - DROP TABLE / DATABASE / SCHEMA - TRUNCATE - DELETE и UPDATE без строгого ограничивающего условия WHERE (или с фиктивным WHERE 1=1) - Несанкционированное повышение привилегий (GRANT ALL, SUPERUSER) """ import re from typing import Any, Dict, List from .base import BaseSecurityContract, ContractVerdict SQL_ARG_KEYS = {"query", "sql", "statement", "command"} # Запрещенные DDL команды DROP_PATTERN = re.compile(r"\bDROP\s+(TABLE|DATABASE|SCHEMA|VIEW|INDEX|USER|ROLE)\b", re.IGNORECASE) TRUNCATE_PATTERN = re.compile(r"\bTRUNCATE(\s+TABLE)?\b", re.IGNORECASE) ALTER_DROP_PATTERN = re.compile(r"\bALTER\s+TABLE\s+.*\bDROP\s+(COLUMN|CONSTRAINT)\b", re.IGNORECASE) PRIVILEGE_PATTERN = re.compile(r"\b(GRANT\s+ALL|SUPERUSER|LOGIN|CREATEDB)\b", re.IGNORECASE) # Детекция DELETE/UPDATE без условий WHERE DELETE_WITHOUT_WHERE = re.compile(r"^\s*DELETE\s+FROM\s+[a-zA-Z0-9_\.]+\s*(;)?\s*$", re.IGNORECASE) UPDATE_WITHOUT_WHERE = re.compile(r"^\s*UPDATE\s+[a-zA-Z0-9_\.]+\s+SET\s+[^;]+\s*(;)?\s*$", re.IGNORECASE) DUMMY_WHERE = re.compile(r"\bWHERE\s+(1\s*=\s*1|true|'1'\s*=\s*'1')\b", re.IGNORECASE) class SQLGuardContract(BaseSecurityContract): name = "sql_guard" rule_id = "SEC-SQL-001" def __init__(self, allow_ddl: bool = False, strict_where: bool = True): self.allow_ddl = allow_ddl self.strict_where = strict_where def _extract_queries(self, arguments: Dict[str, Any]) -> List[str]: queries = [] for k, v in arguments.items(): if isinstance(v, str): if k.lower() in SQL_ARG_KEYS or any(w in v.upper() for w in ("SELECT", "INSERT", "UPDATE", "DELETE", "DROP")): queries.append(v) elif isinstance(v, dict): queries.extend(self._extract_queries(v)) return queries def check_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> ContractVerdict: queries = self._extract_queries(arguments) for q in queries: clean_q = q.strip() # 1. Запрет DROP if not self.allow_ddl and DROP_PATTERN.search(clean_q): match = DROP_PATTERN.search(clean_q).group(0) return ContractVerdict( allowed=False, rule_id="SEC-SQL-DROP", reason=f"Unsat Core: Деструктивная команда DDL '{match}' запрещена политикой безопасности.", details={"query": clean_q, "violation": match} ) # 2. Запрет TRUNCATE if not self.allow_ddl and TRUNCATE_PATTERN.search(clean_q): return ContractVerdict( allowed=False, rule_id="SEC-SQL-TRUNCATE", reason="Unsat Core: Команда полной очистки таблицы 'TRUNCATE' запрещена.", details={"query": clean_q} ) # 3. Запрет ALTER TABLE DROP if not self.allow_ddl and ALTER_DROP_PATTERN.search(clean_q): return ContractVerdict( allowed=False, rule_id="SEC-SQL-ALTER-DROP", reason="Unsat Core: Удаление столбцов/ограничений через ALTER TABLE запрещено.", details={"query": clean_q} ) # 4. Запрет DELETE без WHERE if self.strict_where and ("DELETE" in clean_q.upper()): # Проверяем на отсутствие WHERE или фиктивный WHERE if not re.search(r"\bWHERE\b", clean_q, re.IGNORECASE) or DUMMY_WHERE.search(clean_q): return ContractVerdict( allowed=False, rule_id="SEC-SQL-DELETE-NO-WHERE", reason="Unsat Core: Команда DELETE без ограничивающего условия WHERE запрещена (риск массового уничтожения данных).", details={"query": clean_q} ) # 5. Запрет UPDATE без WHERE if self.strict_where and ("UPDATE" in clean_q.upper()): if not re.search(r"\bWHERE\b", clean_q, re.IGNORECASE) or DUMMY_WHERE.search(clean_q): return ContractVerdict( allowed=False, rule_id="SEC-SQL-UPDATE-NO-WHERE", reason="Unsat Core: Команда UPDATE без ограничивающего условия WHERE запрещена (риск перезаписи всей таблицы).", details={"query": clean_q} ) # 6. Запрет манипуляций с привилегиями if PRIVILEGE_PATTERN.search(clean_q): return ContractVerdict( allowed=False, rule_id="SEC-SQL-PRIVILEGE", reason="Unsat Core: Попытка изменения привилегий СУБД (GRANT/SUPERUSER) заблокирована.", details={"query": clean_q} ) return ContractVerdict(allowed=True, reason="SQL-Guard check passed", rule_id=self.rule_id)