"""规则版本来源解析器。""" 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, )