Files
MeshSate/app/diagnostic/engine.py

229 lines
8.5 KiB
Python
Raw Normal View History

"""诊断规则引擎。
职责:
- 定义 RuleResult 数据结构(每条规则的输出格式)
- 提供 DiagnosticEngine.run_all() 入口
- 规则实现分散在 rules.py 中,engine 负责调度
"""
from __future__ import annotations
import time
from dataclasses import dataclass, field, asdict
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional
@dataclass
class RuleResult:
"""单条规则的输出。"""
id: str # R001, R002 ...
severity: str # high / medium / low / info / ok
message: str # 给用户看的中文描述
hint: str = "" # 建议/下一步操作
evidence: List[str] = field(default_factory=list) # 证据片段(日志行/DB 统计)
run_at: str = "" # ISO 格式时间戳
run_by: str = "page_request" # page_request / timer:xxx / export
skipped: bool = False # 规则是否跳过
skip_reason: Optional[str] = None # 为什么跳过(如 Docker 不可用)
def __post_init__(self) -> None:
if not self.run_at:
# 用北京时间(Asia/Shanghai, UTC+8),避免显示差 8 小时
try:
bj = timezone(timedelta(hours=8))
self.run_at = datetime.now(bj).strftime("%Y-%m-%d %H:%M:%S")
except Exception: # noqa: BLE001
self.run_at = datetime.now(timezone.utc).isoformat()
def to_dict(self) -> Dict[str, Any]:
return asdict(self)
# ---------------------------------------------------------------------------
# 严重性 → 徽章 CSS 类映射(和 _nav_style.html 保持一致)
# ---------------------------------------------------------------------------
SEVERITY_BADGE: Dict[str, str] = {
"high": "tag bad", # 红色
"medium": "tag warn", # 橙色
"low": "tag notice", # 黄色
"info": "tag muted", # 灰色
"ok": "tag ok", # 绿色
}
def badge_for(severity: str) -> str:
return SEVERITY_BADGE.get(severity, "tag plain")
# ---------------------------------------------------------------------------
# DiagnosticEngine — 规则调度入口
# ---------------------------------------------------------------------------
class DiagnosticEngine:
"""诊断规则引擎。
用法::
engine = DiagnosticEngine(db=SessionLocal(), scanner=scanner,
policy_store=policy_store, docker_status="full")
results = engine.run_all(run_by="page_request")
"""
def __init__(
self,
db, # SQLAlchemy Session
scanner, # Scanner 实例
policy_store, # PolicyStore 实例
docker_status: str = "none", # full / partial / none
docker_log_tail: int = 500,
):
self.db = db
self.scanner = scanner
self.policy_store = policy_store
self.docker_status = docker_status
self.docker_log_tail = docker_log_tail
# ------------------------------------------------------------------
# 入口方法
# ------------------------------------------------------------------
def run_all(self, run_by: str = "page_request") -> List[RuleResult]:
"""运行全部 15 条规则,返回结果列表。"""
# 延迟导入,避免循环依赖
from app.diagnostic import rules as _rules
results: List[RuleResult] = []
all_funcs = [
_rules.check_r001_error_ratio,
_rules.check_r002_clamav_db,
_rules.check_r003_yara_mtime,
_rules.check_r004_engine_health,
_rules.check_r005_no_scans,
_rules.check_r006_suspicious_over_malicious,
_rules.check_r007_reject_ratio,
_rules.check_r008_duplicate_sha,
# 以下 7 条需要 Docker 能力
_rules.check_r009_clamav_crash,
_rules.check_r010_postgres_fatal,
_rules.check_r011_redis_oom,
_rules.check_r012_no_space,
_rules.check_r013_nginx_upstream,
_rules.check_r014_api_connection,
_rules.check_r015_container_restart,
_rules.check_r016_restart_history,
]
for fn in all_funcs:
try:
results.append(fn(self, run_by=run_by))
except Exception as e: # noqa: BLE001
# 单条规则失败不影响整体
results.append(RuleResult(
id=getattr(fn, "_rule_id", "UNKNOWN"),
severity="info",
message=f"规则执行异常: {e}",
skipped=True,
skip_reason=str(e),
run_by=run_by,
))
return results
def run_page_request_rules(self) -> List[RuleResult]:
"""页面请求时必跑的轻量规则(R001/R002/R003/R004/R005)。"""
from app.diagnostic import rules as _rules
results: List[RuleResult] = []
for fn in [
_rules.check_r001_error_ratio,
_rules.check_r002_clamav_db,
_rules.check_r003_yara_mtime,
_rules.check_r004_engine_health,
_rules.check_r005_no_scans,
]:
try:
results.append(fn(self, run_by="page_request"))
except Exception as e: # noqa: BLE001
results.append(RuleResult(
id=getattr(fn, "_rule_id", "UNKNOWN"),
severity="info",
message=f"规则执行异常: {e}",
skipped=True,
skip_reason=str(e),
))
return results
def run_timer_error_scan(self) -> List[RuleResult]:
"""定时器 A:错误类日志(30s 周期)。"""
from app.diagnostic import rules as _rules
results: List[RuleResult] = []
for fn in [
_rules.check_r001_error_ratio,
_rules.check_r005_no_scans,
_rules.check_r009_clamav_crash,
_rules.check_r010_postgres_fatal,
_rules.check_r011_redis_oom,
_rules.check_r012_no_space,
_rules.check_r013_nginx_upstream,
_rules.check_r014_api_connection,
]:
try:
results.append(fn(self, run_by="timer:error_scan"))
except Exception as e: # noqa: BLE001
results.append(RuleResult(
id=getattr(fn, "_rule_id", "UNKNOWN"),
severity="info",
message=f"规则执行异常: {e}",
skipped=True,
skip_reason=str(e),
))
return results
def run_timer_db_aggregate(self) -> List[RuleResult]:
"""定时器 B:DB 聚合类(60s 周期)。"""
from app.diagnostic import rules as _rules
results: List[RuleResult] = []
for fn in [
_rules.check_r006_suspicious_over_malicious,
_rules.check_r007_reject_ratio,
_rules.check_r008_duplicate_sha,
]:
try:
results.append(fn(self, run_by="timer:db_aggregate"))
except Exception as e: # noqa: BLE001
results.append(RuleResult(
id=getattr(fn, "_rule_id", "UNKNOWN"),
severity="info",
message=f"规则执行异常: {e}",
skipped=True,
skip_reason=str(e),
))
return results
def run_timer_container(self) -> List[RuleResult]:
"""定时器 C:容器/引擎类(300s 周期)。"""
from app.diagnostic import rules as _rules
results: List[RuleResult] = []
for fn in [
_rules.check_r002_clamav_db,
_rules.check_r003_yara_mtime,
_rules.check_r004_engine_health,
_rules.check_r015_container_restart,
_rules.check_r016_restart_history,
]:
try:
results.append(fn(self, run_by="timer:container_check"))
except Exception as e: # noqa: BLE001
results.append(RuleResult(
id=getattr(fn, "_rule_id", "UNKNOWN"),
severity="info",
message=f"规则执行异常: {e}",
skipped=True,
skip_reason=str(e),
))
return results