365ab73e7d
1. 新增 notification_config 模型,飞书配置存数据库(单行id=1) 2. feishu_service 改为从数据库读配置,支持热更新,Web保存即生效 3. alerts.py 新增接口: - GET /alerts/notify/feishu/config 读配置(secret只返回是否已设) - PUT /alerts/notify/feishu/config 保存配置+热更新 (status/test 同步改为走数据库配置) 4. 前端告警中心增加「飞书通知设置」弹窗: - 开关/Webhook/签名密钥/最低级别/类型白名单 - 保存配置、发送测试按钮 5. alert_service.create_alert 推送时按数据库配置判断 配置无需改文件/重启,前端界面直接保存即生效。
272 lines
9.4 KiB
Python
272 lines
9.4 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from typing import Optional
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.services.alert_service import AlertService
|
|
from app.schemas._tz_util import serialize_dt_fields, serialize_dt_list, to_business_iso
|
|
|
|
router = APIRouter(
|
|
prefix="/alerts",
|
|
tags=["告警管理"],
|
|
dependencies=[Depends(get_current_user)],
|
|
)
|
|
|
|
|
|
@router.get("", summary="获取告警列表")
|
|
def get_alerts(
|
|
status: Optional[str] = None,
|
|
severity: Optional[str] = None,
|
|
alert_type: Optional[str] = None,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""获取告警列表"""
|
|
from app.models.alert import Alert
|
|
|
|
query = db.query(Alert)
|
|
|
|
if status:
|
|
query = query.filter(Alert.status == status)
|
|
if severity:
|
|
query = query.filter(Alert.severity == severity)
|
|
if alert_type:
|
|
query = query.filter(Alert.alert_type == alert_type)
|
|
|
|
total = query.count()
|
|
items = query.order_by(Alert.created_at.desc()).offset(skip).limit(limit).all()
|
|
|
|
return {"total": total, "items": serialize_dt_list(items)}
|
|
|
|
|
|
@router.get("/{alert_id}", summary="获取告警详情")
|
|
def get_alert(alert_id: int, db: Session = Depends(get_db)):
|
|
from app.models.alert import Alert
|
|
|
|
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
|
if not alert:
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
return serialize_dt_fields(alert)
|
|
|
|
|
|
@router.post("/{alert_id}/acknowledge", summary="确认告警")
|
|
def acknowledge_alert(alert_id: int, acknowledged_by: str = "system", db: Session = Depends(get_db)):
|
|
"""确认告警"""
|
|
alert = AlertService.acknowledge_alert(db, alert_id, acknowledged_by)
|
|
if not alert:
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
return {"message": "告警已确认", "alert": alert}
|
|
|
|
|
|
@router.post("/{alert_id}/resolve", summary="解决告警")
|
|
def resolve_alert(alert_id: int, resolved_by: str = "system", notes: str = "", db: Session = Depends(get_db)):
|
|
"""标记告警为已解决"""
|
|
alert = AlertService.resolve_alert(db, alert_id, resolved_by, notes)
|
|
if not alert:
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
return {"message": "告警已解决", "alert": alert}
|
|
|
|
|
|
@router.post("/{alert_id}/ignore", summary="忽略告警")
|
|
def ignore_alert(alert_id: int, db: Session = Depends(get_db)):
|
|
"""忽略告警"""
|
|
alert = AlertService.ignore_alert(db, alert_id)
|
|
if not alert:
|
|
raise HTTPException(status_code=404, detail="告警不存在")
|
|
return {"message": "告警已忽略", "alert": alert}
|
|
|
|
|
|
@router.post("/detect/run", summary="运行所有检测")
|
|
def run_detection(db: Session = Depends(get_db)):
|
|
"""立即运行所有告警检测"""
|
|
results = AlertService.run_all_detections(db)
|
|
return {"message": "检测完成", "results": results}
|
|
|
|
|
|
@router.get("/statistics/summary", summary="告警统计摘要")
|
|
def get_alert_statistics(db: Session = Depends(get_db)):
|
|
"""获取告警统计信息"""
|
|
stats = AlertService.get_statistics(db)
|
|
return stats
|
|
|
|
|
|
# ========== MAC 白名单管理 ==========
|
|
|
|
@router.get("/whitelist/macs", summary="获取 MAC 白名单")
|
|
def get_mac_whitelist(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""获取 MAC 地址白名单"""
|
|
total, items = AlertService.get_mac_whitelist(db, skip=skip, limit=limit)
|
|
return {"total": total, "items": items}
|
|
|
|
|
|
@router.post("/whitelist/macs", summary="添加 MAC 到白名单")
|
|
def add_mac_to_whitelist(
|
|
mac_address: str,
|
|
description: Optional[str] = None,
|
|
owner: Optional[str] = None,
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""添加 MAC 地址到白名单"""
|
|
whitelist_mac = AlertService.add_mac_to_whitelist(
|
|
db,
|
|
mac_address=mac_address,
|
|
description=description,
|
|
owner=owner
|
|
)
|
|
return {"message": "MAC 已添加到白名单", "item": whitelist_mac}
|
|
|
|
|
|
@router.delete("/whitelist/macs/{mac_id}", summary="从白名单移除 MAC")
|
|
def remove_mac_from_whitelist(mac_id: int, db: Session = Depends(get_db)):
|
|
"""从白名单移除 MAC 地址"""
|
|
success = AlertService.remove_mac_from_whitelist(db, mac_id)
|
|
if not success:
|
|
raise HTTPException(status_code=404, detail="MAC 不存在")
|
|
return {"message": "MAC 已从白名单移除"}
|
|
|
|
|
|
# ========== 单独检测接口 ==========
|
|
|
|
@router.post("/detect/ip-conflicts", summary="检测 IP 冲突")
|
|
def detect_ip_conflicts(db: Session = Depends(get_db)):
|
|
"""仅检测 IP 冲突"""
|
|
conflicts = AlertService.detect_ip_conflicts(db)
|
|
return {
|
|
"count": len(conflicts),
|
|
"conflicts": conflicts
|
|
}
|
|
|
|
|
|
@router.post("/detect/unauthorized", summary="检测未授权接入")
|
|
def detect_unauthorized_access(db: Session = Depends(get_db)):
|
|
"""仅检测未授权接入"""
|
|
unauthorized = AlertService.detect_unauthorized_access(db)
|
|
return {
|
|
"count": len(unauthorized),
|
|
"devices": unauthorized
|
|
}
|
|
|
|
|
|
@router.post("/detect/subnet-exhaustion", summary="检测网段耗尽")
|
|
def detect_subnet_exhaustion(db: Session = Depends(get_db)):
|
|
"""仅检测网段耗尽"""
|
|
exhaustion = AlertService.detect_subnet_exhaustion(db)
|
|
return {
|
|
"count": len(exhaustion),
|
|
"subnets": exhaustion
|
|
}
|
|
|
|
|
|
@router.post("/detect/new-devices", summary="检测新设备")
|
|
def detect_new_devices(db: Session = Depends(get_db)):
|
|
"""仅检测新发现的设备"""
|
|
new_devices = AlertService.detect_new_devices(db)
|
|
return {
|
|
"count": len(new_devices),
|
|
"devices": new_devices
|
|
}
|
|
|
|
|
|
@router.post("/detect/offline-devices", summary="检测离线设备")
|
|
def detect_offline_devices(db: Session = Depends(get_db)):
|
|
"""仅检测离线设备"""
|
|
offline_devices = AlertService.detect_device_offline(db)
|
|
return {
|
|
"count": len(offline_devices),
|
|
"devices": offline_devices
|
|
}
|
|
|
|
|
|
# ========== 飞书告警通知 ==========
|
|
|
|
@router.get("/notify/feishu/config", summary="获取飞书通知配置")
|
|
def get_feishu_config(db: Session = Depends(get_db)):
|
|
"""读取飞书通知配置(Web 界面显示用,不返回 secret 原文只返回是否已设置)"""
|
|
from app.models.notification import NotificationConfig
|
|
row = NotificationConfig.get_singleton(db)
|
|
return {
|
|
"feishu_enabled": bool(row.feishu_enabled),
|
|
"feishu_webhook_url": row.feishu_webhook_url or "",
|
|
"feishu_secret_set": bool(row.feishu_secret),
|
|
"feishu_min_severity": row.feishu_min_severity or "warning",
|
|
"feishu_alert_types": row.feishu_alert_types or "",
|
|
}
|
|
|
|
|
|
@router.put("/notify/feishu/config", summary="保存飞书通知配置")
|
|
def update_feishu_config(
|
|
feishu_enabled: bool,
|
|
feishu_webhook_url: str = "",
|
|
feishu_secret: str = "",
|
|
feishu_min_severity: str = "warning",
|
|
feishu_alert_types: str = "",
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""保存飞书通知配置到数据库并热更新(无需重启后端)"""
|
|
from app.models.notification import NotificationConfig
|
|
row = NotificationConfig.get_singleton(db)
|
|
row.feishu_enabled = bool(feishu_enabled)
|
|
row.feishu_webhook_url = (feishu_webhook_url or "").strip()
|
|
# 仅当传入新 secret 时才覆盖(避免 UI 回显时清空已有 secret)
|
|
if feishu_secret:
|
|
row.feishu_secret = feishu_secret.strip()
|
|
row.feishu_min_severity = feishu_min_severity or "warning"
|
|
row.feishu_alert_types = (feishu_alert_types or "").strip()
|
|
db.commit()
|
|
|
|
# 热更新运行时的飞书服务
|
|
from app.services.feishu_service import reload_feishu_service
|
|
svc = reload_feishu_service(db)
|
|
|
|
return {
|
|
"message": "飞书通知配置已保存并生效",
|
|
"success": True,
|
|
"configured": svc.is_configured(),
|
|
}
|
|
|
|
|
|
@router.get("/notify/feishu/status", summary="获取飞书通知运行状态")
|
|
def get_feishu_status(db: Session = Depends(get_db)):
|
|
"""查看飞书通知运行状态"""
|
|
from app.services.feishu_service import get_feishu_service
|
|
svc = get_feishu_service(db)
|
|
return {
|
|
"enabled": svc.enabled,
|
|
"configured": bool(svc.webhook_url),
|
|
"webhook_set": bool(svc.webhook_url),
|
|
"secret_set": bool(svc.secret),
|
|
"min_severity": svc.min_severity,
|
|
"alert_types": svc.alert_types,
|
|
"cani_push": svc.is_configured(),
|
|
}
|
|
|
|
|
|
@router.post("/notify/feishu/test", summary="发送飞书测试告警")
|
|
def test_feishu_notification(db: Session = Depends(get_db)):
|
|
"""发送一条测试告警到飞书,验证 webhook 配置是否正确"""
|
|
from app.services.feishu_service import get_feishu_service
|
|
svc = get_feishu_service(db)
|
|
if not svc.is_configured():
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="飞书通知未启用或未配置 webhook。请在告警中心「飞书通知设置」中配置并开启"
|
|
)
|
|
ok = svc.send_alert({
|
|
"alert_type": "new_device_detected",
|
|
"severity": "warning",
|
|
"title": "飞书通知测试",
|
|
"message": "这是一条测试告警,用于验证飞书机器人 webhook 配置是否正常。",
|
|
"ip_address_str": "172.16.0.1",
|
|
"mac_address": "00:11:22:33:44:55",
|
|
})
|
|
if ok:
|
|
return {"message": "飞书测试通知发送成功", "success": True}
|
|
raise HTTPException(status_code=500, detail="飞书测试通知发送失败,请检查 webhook 地址和日志")
|
|
|