246c0e5ded
- 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
126 lines
4.0 KiB
Python
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,
|
|
)
|