Files
vps-manager/app/services/security_service.py
T

120 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""安全评分服务
基于 Agent 上报的安全检查项(SecurityCheck)计算安全评分与加固建议。
评分按检查项加权:pass 满分、warn 半分、fail/unknown 零分。
"""
from sqlalchemy import func
from sqlmodel import Session, select
from app.models.monitor import SecurityCheck
# 检查项权重(总和约 100
CHECK_WEIGHTS = {"ssh_config": 40, "firewall": 35, "listening_ports": 25}
DEFAULT_WEIGHT = 20
def latest_checks(session: Session, asset_id: int) -> list:
"""取每个检查项的最新一条
用 GROUP BY max(ts) 子查询精确取每项最新记录:若用 limit(50) 后去重,
当某个检查项连续上报超过 50 次时会把其他项的最新记录挤出,导致评分失真。
"""
latest_ts = (
select(
SecurityCheck.check_item,
func.max(SecurityCheck.ts).label("max_ts"),
)
.where(SecurityCheck.asset_id == asset_id)
.group_by(SecurityCheck.check_item)
.subquery()
)
stmt = select(SecurityCheck).where(SecurityCheck.asset_id == asset_id).join(
latest_ts,
(SecurityCheck.check_item == latest_ts.c.check_item)
& (SecurityCheck.ts == latest_ts.c.max_ts),
)
return list(session.exec(stmt).all())
def compute_security_score(checks: list):
"""计算安全评分(0-100),无检查数据返回 None"""
if not checks:
return None
total = 0
earned = 0.0
for check in checks:
weight = CHECK_WEIGHTS.get(check.check_item, DEFAULT_WEIGHT)
total += weight
if check.status == "pass":
earned += weight
elif check.status == "warn":
earned += weight * 0.5
return round(earned / total * 100) if total else None
def score_level(score):
"""评分分级:good(>=80) / warning(>=50) / risk(<50) / unknown"""
if score is None:
return "unknown"
if score >= 80:
return "good"
if score >= 50:
return "warning"
return "risk"
def get_asset_security(session: Session, asset_id: int) -> dict:
"""单个服务器的安全评分与加固建议"""
checks = latest_checks(session, asset_id)
score = compute_security_score(checks)
return {
"asset_id": asset_id,
"score": score,
"level": score_level(score),
"checks_count": len(checks),
"suggestions": [c.suggestion for c in checks if c.suggestion],
}
def get_security_overview(session: Session) -> list:
"""所有有安全检查数据的服务器评分总览(按评分升序,风险高的在前)
性能优化:用子查询取每个 (asset_id, check_item) 的最新 ts,仅拉取最新记录,
避免全表扫描(历史数据量大时内存可控)。
"""
latest_ts = (
select(
SecurityCheck.asset_id,
SecurityCheck.check_item,
func.max(SecurityCheck.ts).label("max_ts"),
)
.group_by(SecurityCheck.asset_id, SecurityCheck.check_item)
.subquery()
)
stmt = select(SecurityCheck).join(
latest_ts,
(SecurityCheck.asset_id == latest_ts.c.asset_id)
& (SecurityCheck.check_item == latest_ts.c.check_item)
& (SecurityCheck.ts == latest_ts.c.max_ts),
)
all_checks = session.exec(stmt).all()
# 按 asset_id 分组
latest_by_asset: dict = {}
for check in all_checks:
latest_by_asset.setdefault(check.asset_id, []).append(check)
result = []
for asset_id, checks in latest_by_asset.items():
score = compute_security_score(checks)
result.append({
"asset_id": asset_id,
"score": score,
"level": score_level(score),
"checks_count": len(checks),
"suggestions": [c.suggestion for c in checks if c.suggestion],
})
result.sort(key=lambda x: (x["score"] is None, x["score"] if x["score"] is not None else 0))
return result