Files
leaudit-platform-backend/fastapi_modules/fastapi_leaudit/leaudit_bridge/ruleVersionResolver.py
T
wren 246c0e5ded feat: complete M1-M3 infrastructure — OSS client, native execution chain, rule lifecycle API, system docs
- M1: unified OSS client (upload/download/presign) + path utils + config
- M2: rule service with validate/create/publish/rollback + binding CRUD endpoints
- M3: native AuditCtx runner, file/rule resolvers, storage adapter with full persistence
- docs: SYSTEM_OVERVIEW.md as comprehensive architecture reference
- fix: double finalize — terminal state now written once by finalize_run
2026-04-28 11:49:55 +08:00

126 lines
4.0 KiB
Python

"""规则版本来源解析器。"""
from __future__ import annotations
import hashlib
from dataclasses import dataclass
from pathlib import Path
from fastapi_common.fastapi_common_logger import logger
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
from fastapi_common.fastapi_common_storage.oss_client import OssClient
from sqlalchemy import text
@dataclass(frozen=True)
class RuleVersionPayload:
"""规则文件解析结果。"""
localPath: str
sourceType: str
sourcePath: str
ruleVersionId: int | None = None
ruleTypeId: str | None = None
fileSha256: str | None = None
class RuleVersionResolver:
"""按运行记录解析规则 YAML 文件来源。"""
def __init__(self, Oss: OssClient | None = None) -> None:
self.Oss = Oss or OssClient()
async def ResolveForRun(self, RunId: int) -> RuleVersionPayload | None:
"""根据运行记录解析规则文件来源。"""
RunInfo = await self._LoadRunInfo(RunId)
if not RunInfo:
return None
LocalCachePath = RunInfo["rule_local_cache_path"]
if LocalCachePath:
CachePath = Path(LocalCachePath)
if CachePath.is_file():
return RuleVersionPayload(
localPath=str(CachePath),
sourceType="local_cache",
sourcePath=str(CachePath),
ruleVersionId=RunInfo["rule_version_id"],
ruleTypeId=RunInfo["rule_type_id"],
fileSha256=RunInfo["rule_source_sha256"],
)
SourceUrl = RunInfo["rule_source_oss_url"]
if not SourceUrl:
return None
return await self._DownloadFromUrl(
Url=SourceUrl,
RuleVersionId=RunInfo["rule_version_id"],
RuleTypeId=RunInfo["rule_type_id"],
ExpectedSha256=RunInfo["rule_source_sha256"],
)
async def _LoadRunInfo(self, RunId: int) -> dict[str, object] | None:
"""读取运行记录中的规则来源信息。"""
async with GetAsyncSession() as Session:
Result = await Session.execute(
text(
"""
SELECT
rule_version_id,
rule_type_id,
rule_source_oss_url,
rule_source_sha256,
rule_local_cache_path
FROM leaudit_audit_runs
WHERE id = :run_id
LIMIT 1
"""
),
{"run_id": RunId},
)
Row = Result.mappings().first()
return dict(Row) if Row else None
async def _DownloadFromUrl(
self,
*,
Url: str,
RuleVersionId: int | None,
RuleTypeId: str | None,
ExpectedSha256: str | None,
) -> RuleVersionPayload:
"""从 OSS 下载规则 YAML 到本地临时文件。"""
try:
Content = self.Oss.DownloadBytes(Url)
except Exception as Error:
logger.error(f"下载规则 YAML 失败: url={Url}, error={Error}")
raise
ActualSha256 = hashlib.sha256(Content).hexdigest()
if ExpectedSha256 and ActualSha256.lower() != ExpectedSha256.lower():
raise ValueError(
"规则 YAML SHA256 校验失败: "
f"expected={ExpectedSha256}, actual={ActualSha256}"
)
FilePrefix = "leaudit-rule-"
if RuleTypeId:
SafeTypeId = RuleTypeId.replace("/", "_").replace(".", "_")
FilePrefix = f"{FilePrefix}{SafeTypeId}-"
LocalPath = self.Oss.WriteTempBytes(
Content=Content,
Suffix=".yaml",
Prefix=FilePrefix,
)
return RuleVersionPayload(
localPath=LocalPath,
sourceType="oss",
sourcePath=Url,
ruleVersionId=RuleVersionId,
ruleTypeId=RuleTypeId,
fileSha256=ActualSha256,
)