feat: 新增脚手架/开发模式/权限白名单/system-monitor插件
- nebula create mod/key/list-templates 模组脚手架 - nebula dev 开发模式热重载 - manifest permissions.imports 权限白名单机制 - system-monitor 系统监控仪表盘插件 - 默认端口统一为 10086 - 修复 _init_nbpf 误读 Ed25519 私钥为 RSA 的 bug - 更新 README.md 文档
This commit is contained in:
@@ -46,7 +46,7 @@ class NIRCompiler:
|
||||
|
||||
# ── 编译 ──
|
||||
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>") -> bytes:
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>", allowed_imports: list[str] = None) -> bytes:
|
||||
"""将 Python 源码编译为序列化的 code object
|
||||
|
||||
Args:
|
||||
@@ -61,7 +61,7 @@ class NIRCompiler:
|
||||
"""
|
||||
try:
|
||||
# 静态安全检查
|
||||
self._static_check(source, filename)
|
||||
self._static_check(source, filename, allowed_imports or [])
|
||||
|
||||
# 编译为 code object
|
||||
code = compile(source, filename, 'exec')
|
||||
@@ -79,11 +79,12 @@ class NIRCompiler:
|
||||
except Exception as e:
|
||||
raise NIRCompileError(f"编译失败: {type(e).__name__}: {e}") from e
|
||||
|
||||
def compile_plugin(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||
def compile_plugin(self, plugin_dir: Path, allowed_imports: list[str] = None) -> dict[str, bytes]:
|
||||
"""编译整个插件目录为 NIR
|
||||
|
||||
Args:
|
||||
plugin_dir: 插件目录路径
|
||||
allowed_imports: 允许导入的系统模块白名单(来自 manifest permissions.imports)
|
||||
|
||||
Returns:
|
||||
{module_name: nir_bytes} 字典
|
||||
@@ -105,7 +106,7 @@ class NIRCompiler:
|
||||
module_name = rel_path.replace(".py", "").replace("/", ".")
|
||||
if module_name.endswith(".__init__"):
|
||||
module_name = module_name[:-9] # 去掉 .__init__
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path))
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path), allowed_imports)
|
||||
|
||||
return nir_data
|
||||
|
||||
@@ -163,7 +164,7 @@ class NIRCompiler:
|
||||
|
||||
# ── 静态安全检查 ──
|
||||
|
||||
def _static_check(self, source: str, filename: str):
|
||||
def _static_check(self, source: str, filename: str, allowed_imports: list[str] = None):
|
||||
"""静态源码安全检查"""
|
||||
try:
|
||||
tree = ast.parse(source, filename=filename)
|
||||
@@ -174,12 +175,12 @@ class NIRCompiler:
|
||||
# 检查 import 语句
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
self._check_module(alias.name, node.lineno)
|
||||
self._check_module(alias.name, node.lineno, allowed_imports)
|
||||
|
||||
# 检查 from ... import 语句
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
self._check_module(node.module, node.lineno)
|
||||
self._check_module(node.module, node.lineno, allowed_imports)
|
||||
|
||||
# 检查 __import__ 调用
|
||||
elif isinstance(node, ast.Call):
|
||||
@@ -196,12 +197,16 @@ class NIRCompiler:
|
||||
f"{filename}:{node.lineno} - 禁止使用 {node.func.id}()"
|
||||
)
|
||||
|
||||
def _check_module(self, module_name: str, lineno: int):
|
||||
"""检查模块是否被禁止"""
|
||||
def _check_module(self, module_name: str, lineno: int, allowed_imports: list[str] = None):
|
||||
"""检查模块是否被禁止(支持白名单豁免)"""
|
||||
base = module_name.split(".")[0]
|
||||
if base in self.FORBIDDEN_MODULES:
|
||||
# 检查是否在白名单中
|
||||
if allowed_imports and base in allowed_imports:
|
||||
return # 白名单放行
|
||||
raise NIRCompileError(
|
||||
f"第 {lineno} 行 - 禁止导入系统模块: '{module_name}'"
|
||||
f"(如需使用请在 manifest.json 的 permissions.imports 中声明)"
|
||||
)
|
||||
|
||||
def _reject_c_extensions(self, plugin_dir: Path):
|
||||
|
||||
@@ -114,9 +114,16 @@ class NBPFPacker:
|
||||
# 1. 读取 manifest
|
||||
manifest = self._read_manifest(plugin_dir)
|
||||
|
||||
# 2. 编译所有 .py 文件为 NIR
|
||||
# 2. 编译所有 .py 文件为 NIR(传入 manifest 权限白名单)
|
||||
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir)
|
||||
perms = manifest.get("permissions", {})
|
||||
if isinstance(perms, dict):
|
||||
allowed_imports = perms.get("imports", [])
|
||||
else:
|
||||
allowed_imports = [] # 旧的数组格式,不开放系统模块
|
||||
if allowed_imports:
|
||||
Log.info("NBPF", f"已授权导入: {allowed_imports}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir, allowed_imports=allowed_imports)
|
||||
|
||||
# 3. 收集资源文件
|
||||
res_files = self._collect_resources(plugin_dir)
|
||||
|
||||
@@ -55,6 +55,7 @@ class NBPFLoader:
|
||||
self.trusted_ed25519_keys = trusted_ed25519_keys or {}
|
||||
self.trusted_rsa_keys = trusted_rsa_keys or {}
|
||||
self.rsa_private_key = rsa_private_key
|
||||
self._current_allowed_imports: list[str] = []
|
||||
|
||||
def load(
|
||||
self,
|
||||
@@ -112,7 +113,12 @@ class NBPFLoader:
|
||||
meta = manifest.get("metadata", {})
|
||||
name = plugin_name or meta.get("name", nbpf_path.stem)
|
||||
|
||||
# 9. 反序列化并执行
|
||||
# 9. 反序列化并执行(传入 imports 白名单)
|
||||
perms = manifest.get("permissions", {})
|
||||
if isinstance(perms, dict):
|
||||
self._current_allowed_imports = perms.get("imports", [])
|
||||
else:
|
||||
self._current_allowed_imports = []
|
||||
instance, module = self._deserialize_and_exec(nir_data, name)
|
||||
|
||||
# 10. 构建插件信息
|
||||
@@ -326,7 +332,7 @@ class NBPFLoader:
|
||||
"""反序列化 NIR 并执行,返回 (instance, module)"""
|
||||
|
||||
# 构建安全的全局命名空间
|
||||
safe_globals = self._build_safe_globals(plugin_name)
|
||||
safe_globals = self._build_safe_globals(plugin_name, self._current_allowed_imports)
|
||||
|
||||
# 按依赖顺序执行模块
|
||||
main_module = None
|
||||
@@ -365,9 +371,12 @@ class NBPFLoader:
|
||||
|
||||
return instance, main_module
|
||||
|
||||
def _build_safe_globals(self, plugin_name: str) -> dict:
|
||||
def _build_safe_globals(self, plugin_name: str, allowed_imports: list[str] = None) -> dict:
|
||||
"""构建安全的全局命名空间
|
||||
|
||||
如果插件在 manifest 中声明了 imports 权限,将 `__import__` 加回内置函数,
|
||||
并用白名单包装器限制只能导入声明的模块。
|
||||
|
||||
注意:Python 沙箱无法完全阻止通过 ()__class__.__bases__[0].__subclasses__()
|
||||
等反射方式逃逸。本沙箱仅用于防止意外访问危险模块,真正的安全隔离
|
||||
需要 OS 级容器化。
|
||||
@@ -390,6 +399,19 @@ class NBPFLoader:
|
||||
'KeyError': KeyError, 'IndexError': IndexError,
|
||||
'Exception': Exception, 'BaseException': BaseException,
|
||||
}
|
||||
|
||||
# 如果插件声明了 imports 权限,添加白名单 __import__
|
||||
if allowed_imports:
|
||||
_allowed_set = set(allowed_imports)
|
||||
def _safe_import(name, *args, **kwargs):
|
||||
base = name.split(".")[0]
|
||||
if base not in _allowed_set:
|
||||
raise ImportError(
|
||||
f"模块 '{name}' 不在权限白名单中。"
|
||||
f"请在 manifest.json 的 permissions.imports 中声明: {sorted(_allowed_set)}"
|
||||
)
|
||||
return __import__(name, *args, **kwargs)
|
||||
safe_builtins['__import__'] = _safe_import
|
||||
return {
|
||||
'__builtins__': safe_builtins,
|
||||
'__name__': f'nbpf.{plugin_name}',
|
||||
|
||||
Reference in New Issue
Block a user