新增一系列安全相关功能: 1. 新增二维码生成工具,支持自定义参数导出图片/Base64/字节流 2. 新增HTML内容安全处理工具,包括标签剥离、转义、URL校验 3. 新增日志敏感信息脱敏工具,支持配置、字典、日志记录脱敏 4. 新增密钥管理运行时,支持从环境变量/文件加载密钥 5. 新增身份链接管理,支持多渠道身份绑定与解析 6. 新增SSRF防护工具,支持域名/IP校验与固定主机 7. 新增安全权限修复工具,修复文件目录权限与配置项 8. 新增外部内容安全处理,支持LLM特殊令牌剥离与注入检测 9. 新增认证限流工具,支持多维度限流与本地回环豁免 10. 新增配对管理工具,支持安全配对码生成与校验 11. 新增白名单管理工具,支持DM/群组/来源白名单校验
116 lines
3.6 KiB
Python
116 lines
3.6 KiB
Python
import logging
|
|
import os
|
|
import threading
|
|
from collections.abc import Callable
|
|
from enum import StrEnum
|
|
from pathlib import Path
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SecretSource(StrEnum):
|
|
ENV = "env"
|
|
FILE = "file"
|
|
|
|
|
|
class SecretsRuntime:
|
|
def __init__(self):
|
|
self._secrets: dict[str, str] = {}
|
|
self._snapshots: list[dict[str, str]] = []
|
|
self._resolvers: list[Callable[[str], str | None]] = []
|
|
self._lock = threading.Lock()
|
|
|
|
def register_resolver(self, resolver: Callable[[str], str | None]) -> None:
|
|
with self._lock:
|
|
self._resolvers.append(resolver)
|
|
|
|
def load_from_env(self, var_name: str, secret_name: str | None = None) -> None:
|
|
value = os.getenv(var_name)
|
|
if value is not None:
|
|
name = secret_name or var_name
|
|
with self._lock:
|
|
self._secrets[name] = value
|
|
logger.debug("Loaded secret '%s' from env var %s", name, var_name)
|
|
|
|
def load_from_file(self, file_path: str | Path, secret_name: str | None = None) -> None:
|
|
path = Path(file_path)
|
|
if not path.exists():
|
|
logger.warning("Secret file not found: %s", path)
|
|
return
|
|
try:
|
|
value = path.read_text(encoding="utf-8").strip()
|
|
except Exception:
|
|
logger.exception("Failed to read secret file: %s", path)
|
|
return
|
|
name = secret_name or path.stem
|
|
with self._lock:
|
|
self._secrets[name] = value
|
|
logger.debug("Loaded secret '%s' from file %s", name, path)
|
|
|
|
def snapshot(self) -> None:
|
|
with self._lock:
|
|
self._snapshots.append(dict(self._secrets))
|
|
|
|
def rollback(self) -> None:
|
|
with self._lock:
|
|
if not self._snapshots:
|
|
raise RuntimeError("No snapshot available for rollback")
|
|
self._secrets = self._snapshots.pop()
|
|
|
|
def plan(self) -> list[str]:
|
|
with self._lock:
|
|
return sorted(self._secrets.keys())
|
|
|
|
def validate(self, required: list[str]) -> list[str]:
|
|
with self._lock:
|
|
return [name for name in required if name not in self._secrets]
|
|
|
|
def apply(self, updates: dict[str, str]) -> None:
|
|
with self._lock:
|
|
self._snapshots.append(dict(self._secrets))
|
|
try:
|
|
self._secrets.update(updates)
|
|
except Exception:
|
|
self._secrets = self._snapshots.pop()
|
|
raise
|
|
|
|
def get(self, name: str) -> str | None:
|
|
with self._lock:
|
|
return self._secrets.get(name)
|
|
|
|
def resolve(self, value: str) -> str:
|
|
with self._lock:
|
|
resolvers = list(self._resolvers)
|
|
for resolver in resolvers:
|
|
result = resolver(value)
|
|
if result is not None:
|
|
return result
|
|
|
|
if value.startswith("$env:"):
|
|
var_name = value[5:]
|
|
env_val = os.getenv(var_name)
|
|
if env_val is not None:
|
|
return env_val
|
|
if value.startswith("$secret:"):
|
|
secret_name = value[8:]
|
|
with self._lock:
|
|
secret_val = self._secrets.get(secret_name)
|
|
if secret_val is not None:
|
|
return secret_val
|
|
return value
|
|
|
|
def trim_credential(self, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
if value.startswith("${") and value.endswith("}"):
|
|
return None
|
|
return value
|
|
|
|
@property
|
|
def secret_names(self) -> list[str]:
|
|
with self._lock:
|
|
return list(self._secrets.keys())
|
|
|
|
|
|
secrets_runtime = SecretsRuntime()
|