113 lines
3.5 KiB
Python
113 lines
3.5 KiB
Python
"""安全评分服务
|
||
|
||
基于 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:
|
||
"""取每个检查项的最新一条"""
|
||
stmt = (
|
||
select(SecurityCheck)
|
||
.where(SecurityCheck.asset_id == asset_id)
|
||
.order_by(SecurityCheck.ts.desc())
|
||
.limit(50)
|
||
)
|
||
checks = session.exec(stmt).all()
|
||
latest = {}
|
||
for check in checks:
|
||
if check.check_item not in latest:
|
||
latest[check.check_item] = check
|
||
return list(latest.values())
|
||
|
||
|
||
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
|