| | |
| | from __future__ import annotations |
| |
|
| | import ast |
| | import io |
| | import textwrap |
| | from contextlib import redirect_stdout |
| | from typing import Any |
| |
|
| | from config import PY_UNSAFE_DEFAULT |
| | from utils.audit import audit_event |
| |
|
| |
|
| | SAFE_BUILTINS: dict[str, Any] = { |
| | "print": print, |
| | "len": len, |
| | "range": range, |
| | "min": min, |
| | "max": max, |
| | "sum": sum, |
| | "enumerate": enumerate, |
| | "sorted": sorted, |
| | "int": int, |
| | "float": float, |
| | "str": str, |
| | "bool": bool, |
| | "list": list, |
| | "dict": dict, |
| | "set": set, |
| | "tuple": tuple, |
| | "abs": abs, |
| | "round": round, |
| | "zip": zip, |
| | "any": any, |
| | "all": all, |
| | } |
| |
|
| |
|
| | def _has_forbidden_nodes(tree: ast.AST) -> tuple[bool, str]: |
| | """Detecta importações e acessos perigosos no modo SAFE.""" |
| | for node in ast.walk(tree): |
| | if isinstance(node, (ast.Import, ast.ImportFrom)): |
| | return True, "Uso de import não é permitido no modo SAFE." |
| | if isinstance(node, ast.Call) and isinstance(node.func, ast.Name): |
| | if node.func.id in {"eval", "exec", "__import__", "open"}: |
| | return True, f"Uso de {node.func.id} não é permitido no modo SAFE." |
| | if isinstance(node, ast.Attribute) and isinstance(node.value, ast.Name): |
| | |
| | if node.value.id in {"os", "sys", "subprocess", "pathlib"}: |
| | return True, f"Acesso a {node.value.id} não é permitido no modo SAFE." |
| | return False, "" |
| |
|
| |
|
| | def _parse_flags(args: str) -> dict[str, Any]: |
| | """ |
| | Interpreta flags: |
| | --unsafe (exige token CONFIRMO) |
| | --preview (não executa; apenas mostra o código detectado) |
| | """ |
| | tokens = args.split() |
| | flags = {"unsafe": False, "preview": False, "confirmed": False, "rest": ""} |
| |
|
| | |
| | rest = [] |
| | for t in tokens: |
| | tl = t.lower() |
| | if tl == "--unsafe": |
| | flags["unsafe"] = True |
| | elif tl == "--preview": |
| | flags["preview"] = True |
| | elif tl == "confirmo": |
| | flags["confirmed"] = True |
| | else: |
| | rest.append(t) |
| | flags["rest"] = " ".join(rest).strip() |
| | return flags |
| |
|
| |
|
| | def _normalize_block(block: str) -> str: |
| | if not block: |
| | return "" |
| | |
| | return textwrap.dedent(block).strip("\n\r ") |
| |
|
| |
|
| | async def handle_py(args: str, block: str) -> str: |
| | """ |
| | /py <<<codigo>>> -> Modo SAFE |
| | /py --unsafe CONFIRMO <<<...>>> -> Modo UNSAFE (permite import) |
| | |
| | Regras: |
| | - SAFE: sem import, sem eval/exec/__import__/open/os/sys/subprocess. |
| | - UNSAFE: requer --unsafe e CONFIRMO. |
| | - --preview: apenas retorna o código detectado (sem executar). |
| | """ |
| | flags = _parse_flags(args) |
| | code = _normalize_block(block) |
| |
|
| | if flags["preview"]: |
| | preview = code if code else "(vazio)" |
| | return "**Preview de código (/py)**\n```\n" + preview[:6000] + "\n```" |
| |
|
| | if not code: |
| | return "Uso: /py [--unsafe CONFIRMO] [--preview] <<<código Python>>>" |
| |
|
| | try: |
| | if flags["unsafe"] or PY_UNSAFE_DEFAULT: |
| | |
| | if not flags["confirmed"]: |
| | return ( |
| | "⚠️ Modo UNSAFE requer confirmação explícita.\n" |
| | "Use: `/py --unsafe CONFIRMO <<<seu código>>>`" |
| | ) |
| | audit_event("pyexec_unsafe", {"preview": False}) |
| |
|
| | |
| | buf = io.StringIO() |
| | glb: dict[str, Any] = {} |
| | loc: dict[str, Any] = {} |
| | with redirect_stdout(buf): |
| | exec(code, glb, loc) |
| | output = buf.getvalue() |
| | if not output.strip(): |
| | output = "_(sem saída)_" |
| | if len(output) > 6000: |
| | output = output[:6000] + "\n...[truncado]..." |
| | return "**/py (UNSAFE)**\n```\n" + output + "\n```" |
| |
|
| | |
| | audit_event("pyexec_safe", {"preview": False}) |
| | |
| | tree = ast.parse(code, mode="exec") |
| | bad, reason = _has_forbidden_nodes(tree) |
| | if bad: |
| | return f"🚫 Bloqueado no modo SAFE: {reason}" |
| |
|
| | buf = io.StringIO() |
| | safe_globals = {"__builtins__": SAFE_BUILTINS} |
| | safe_locals: dict[str, Any] = {} |
| | with redirect_stdout(buf): |
| | exec(compile(tree, "<safe>", "exec"), safe_globals, safe_locals) |
| | output = buf.getvalue() |
| | if not output.strip(): |
| | output = "_(sem saída)_" |
| | if len(output) > 6000: |
| | output = output[:6000] + "\n...[truncado]..." |
| | return "**/py (SAFE)**\n```\n" + output + "\n```" |
| |
|
| | except SyntaxError as e: |
| | return f"❌ Erro de sintaxe em /py: {e}" |
| | except Exception as e: |
| | return f"❌ Erro em /py: {e}" |
| |
|
| |
|
| | |
| | handle_pyexec = handle_py |
| |
|