""" Контракт изоляции файловой системы (FS-Jail Contract). Предотвращает Path Traversal (../), несанкционированный доступ к системным файлам и утечку файлов конфигурации/секретов (.env, id_rsa, /etc/shadow). """ import os import re from typing import Any, Dict, List, Optional from .base import BaseSecurityContract, ContractVerdict # Шаблоны чувствительных файлов, чтение/запись которых категорически запрещены FORBIDDEN_FILE_PATTERNS = [ re.compile(r"(^|[/\\])\.env($|\.)", re.IGNORECASE), re.compile(r"(^|[/\\])\.git([/\\]config)?$", re.IGNORECASE), re.compile(r"(^|[/\\])id_(rsa|dsa|ecdsa|ed25519)($|\.)", re.IGNORECASE), re.compile(r"(^|[/\\])(secrets|credentials)\.(json|yaml|yml|toml|xml)$", re.IGNORECASE), re.compile(r"(^|[/\\])etc[/\\](shadow|passwd|sudoers|master\.passwd)$", re.IGNORECASE), re.compile(r"(^|[/\\])(SAM|SYSTEM|SECURITY)$", re.IGNORECASE), re.compile(r"(^|[/\\])\.ssh[/\\]", re.IGNORECASE), re.compile(r"(^|[/\\])\.aws[/\\]credentials", re.IGNORECASE), re.compile(r"(^|[/\\])\.docker[/\\]config\.json", re.IGNORECASE), ] PATH_ARG_KEYS = { "path", "filepath", "target_file", "source_file", "file", "directory", "dir", "dest", "destination", "target", "filename", "folder" } class FSJailContract(BaseSecurityContract): name = "fs_jail" rule_id = "SEC-FS-001" def __init__(self, allowed_roots: Optional[List[str]] = None, allow_readonly_whitelist: bool = True): """ Параметры: - allowed_roots: список разрешенных корневых директорий (если задан, доступ вне их блокируется) """ self.allowed_roots = [os.path.abspath(r) for r in allowed_roots] if allowed_roots else None self.allow_readonly_whitelist = allow_readonly_whitelist def _extract_paths(self, arguments: Dict[str, Any]) -> List[str]: """Извлекает потенциальные файловые пути из аргументов вызова инструмента.""" found_paths = [] for k, v in arguments.items(): if isinstance(v, str): if k.lower() in PATH_ARG_KEYS or "/" in v or "\\" in v: found_paths.append(v) elif isinstance(v, dict): found_paths.extend(self._extract_paths(v)) elif isinstance(v, list): for item in v: if isinstance(item, str) and ("/" in item or "\\" in item): found_paths.append(item) elif isinstance(item, dict): found_paths.extend(self._extract_paths(item)) return found_paths def check_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> ContractVerdict: paths = self._extract_paths(arguments) for p in paths: # 1. Проверка на Path Traversal через относительные переходы (..) normalized = os.path.normpath(p) if ".." in p.split("/") or ".." in p.split("\\") or normalized.startswith(".."): # Если путь выходит за пределы, проверяем абсолютный резолв pass # 2. Проверка по черному списку критических файлов и путей for pattern in FORBIDDEN_FILE_PATTERNS: if pattern.search(p) or pattern.search(normalized): return ContractVerdict( allowed=False, rule_id="SEC-FS-FORBIDDEN", reason=f"Unsat Core: Доступ к критическому файлу/директории запрещен политикой безопасности: '{p}'", details={"path": p, "matched_pattern": pattern.pattern} ) # 3. Проверка на ограничение разрешенными корневыми директориями (если задано) if self.allowed_roots: abs_path = os.path.abspath(p) is_within_allowed = any( abs_path == root or abs_path.startswith(root + os.sep) for root in self.allowed_roots ) if not is_within_allowed: return ContractVerdict( allowed=False, rule_id="SEC-FS-OUT-OF-JAIL", reason=f"Unsat Core: Выход за пределы доверенного контура файловой системы: '{p}'", details={"path": p, "allowed_roots": self.allowed_roots} ) return ContractVerdict(allowed=True, reason="FS-Jail check passed", rule_id=self.rule_id)