2025-05-16 15:18:02 +08:00

314 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Python代码规则执行器
负责安全地执行规则中包含的Python代码确保代码在隔离的环境中运行
并对可执行的操作进行限制,以防恶意代码。
"""
import ast
import logging
import importlib
import inspect
import time
import threading
import os
from typing import Dict, Any, Optional, List, Callable, Tuple
from concurrent.futures import ThreadPoolExecutor
from ..models.rule_models import PythonCodeRule
from ..models.rule_models import RuleCategory
logger = logging.getLogger(__name__)
class CodeExecutionError(Exception):
"""执行Python代码时发生的错误"""
pass
class TimeoutError(CodeExecutionError):
"""代码执行超时错误"""
pass
class ImportError(CodeExecutionError):
"""非法导入模块错误"""
pass
class ValidationResult:
"""Python代码验证的结果"""
def __init__(self,
is_valid: bool,
message: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
exception: Optional[Exception] = None):
self.is_valid = is_valid
self.message = message
self.details = details or {}
self.exception = exception
def __bool__(self):
return self.is_valid
class PythonRuleExecutor:
"""
Python代码规则执行器
负责安全地执行规则中的Python代码并返回验证结果。
"""
def __init__(self, rules_base_path: str = "./rules"):
self.logger = logging.getLogger(__name__)
self.rules_base_path = os.path.abspath(rules_base_path)
def _load_code_from_file(self, rule: PythonCodeRule) -> str:
"""
从文件加载代码
Args:
rule: Python代码规则对象包含code_file属性
Returns:
加载的代码内容
Raises:
CodeExecutionError: 如果加载代码失败
"""
if not rule.code_file:
raise CodeExecutionError("规则未指定code_file属性")
# 构建代码文件的绝对路径
# 如果规则ID和版本都存在则在python_code目录下查找
if rule.id and rule.version:
# 代码文件路径格式: rules/python_code/{rule_id}/{version}.py
file_path = os.path.join(self.rules_base_path, "python_code", rule.id, f"{rule.version}.py")
else:
# 否则使用传入的相对路径
file_path = os.path.join(self.rules_base_path, rule.code_file)
# 检查文件是否存在
if not os.path.isfile(file_path):
raise CodeExecutionError(f"代码文件不存在: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
except Exception as e:
raise CodeExecutionError(f"读取代码文件失败: {e}")
def analyze_code(self, code: str) -> List[str]:
"""
分析代码,检查其中使用的导入模块
Args:
code: 要分析的Python代码
Returns:
代码中导入的模块列表
Raises:
SyntaxError: 如果代码存在语法错误
"""
try:
tree = ast.parse(code)
except SyntaxError as e:
raise CodeExecutionError(f"代码语法错误: {e}")
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for name in node.names:
imports.append(name.name)
elif isinstance(node, ast.ImportFrom):
imports.append(node.module)
return imports
def _execute_with_timeout(self,
func: Callable,
args: Tuple,
kwargs: Dict[str, Any],
timeout: int) -> Any:
"""
使用超时执行函数
Args:
func: 要执行的函数
args: 函数的位置参数
kwargs: 函数的关键字参数
timeout: 超时时间(秒)
Returns:
函数的返回值
Raises:
TimeoutError: 如果函数执行超过指定的超时时间
"""
result = [None]
exception = [None]
def target():
try:
result[0] = func(*args, **kwargs)
except Exception as e:
exception[0] = e
thread = threading.Thread(target=target)
thread.daemon = True
thread.start()
thread.join(timeout)
if thread.is_alive():
# 超时
raise TimeoutError(f"代码执行超时(超过{timeout}秒)")
if exception[0]:
raise exception[0]
return result[0]
def execute_rule(self, rule: PythonCodeRule, context: Dict[str, Any]) -> ValidationResult:
"""
执行Python代码规则
Args:
rule: Python代码规则定义
context: 验证上下文(包含规则执行所需的参数和数据)
Returns:
ValidationResult: 验证结果
"""
# 检查是否提供了所有必需的参数
if rule.expected_parameters:
missing_params = [p for p in rule.expected_parameters if p not in context]
if missing_params:
return ValidationResult(
is_valid=False,
message=f"缺少必需的参数: {', '.join(missing_params)}"
)
# 获取代码内容
try:
if rule.code:
code = rule.code
elif rule.code_file:
code = self._load_code_from_file(rule)
else:
return ValidationResult(
is_valid=False,
message="规则既没有code也没有code_file属性"
)
except CodeExecutionError as e:
return ValidationResult(
is_valid=False,
message=str(e),
exception=e
)
# 分析代码中的导入
try:
imports = self.analyze_code(code)
except CodeExecutionError as e:
return ValidationResult(
is_valid=False,
message=str(e),
exception=e
)
# 检查导入是否被允许
if imports and not rule.allow_imports:
return ValidationResult(
is_valid=False,
message=f"规则不允许导入模块,但代码尝试导入: {', '.join(imports)}"
)
# 如果允许导入,检查是否所有导入都在允许列表中
if imports and rule.allow_imports and rule.allowed_modules:
unauthorized_imports = [imp for imp in imports if imp not in rule.allowed_modules]
if unauthorized_imports:
return ValidationResult(
is_valid=False,
message=f"代码尝试导入未授权的模块: {', '.join(unauthorized_imports)}"
)
# 准备执行环境
# 创建一个隔离的命名空间
namespace = {'__builtins__': __builtins__}
# 添加上下文变量
namespace.update(context)
# 如果允许导入,预先导入允许的模块
if rule.allow_imports and imports:
for module_name in imports:
if not rule.allowed_modules or module_name in rule.allowed_modules:
try:
module = importlib.import_module(module_name)
namespace[module_name] = module
except Exception as e:
return ValidationResult(
is_valid=False,
message=f"导入模块 '{module_name}' 失败: {e}",
exception=e
)
# 执行代码
try:
# 编译代码
compiled_code = compile(code, f"<rule:{rule.id}>", 'exec')
# 执行代码
self._execute_with_timeout(
exec,
(compiled_code, namespace),
{},
rule.timeout
)
# 获取入口函数
if rule.entry_function not in namespace:
return ValidationResult(
is_valid=False,
message=f"代码未定义指定的入口函数 '{rule.entry_function}'"
)
entry_func = namespace[rule.entry_function]
if not callable(entry_func):
return ValidationResult(
is_valid=False,
message=f"'{rule.entry_function}' 不是一个可调用的函数"
)
# 调用入口函数
result = self._execute_with_timeout(
entry_func,
tuple(),
{},
rule.timeout
)
# 处理结果
if isinstance(result, dict):
# 如果函数返回一个字典将它转换为ValidationResult
return ValidationResult(
is_valid=bool(result.get('is_valid', False)),
message=result.get('message'),
details=result.get('details', {})
)
else:
# 如果返回其他类型,将其解释为布尔值
return ValidationResult(
is_valid=bool(result),
message=str(result) if result is not True else "验证通过"
)
except TimeoutError as e:
return ValidationResult(
is_valid=False,
message=str(e),
exception=e
)
except Exception as e:
return ValidationResult(
is_valid=False,
message=f"执行代码时发生错误: {e}",
exception=e
)