1e1e5957e8
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄 - Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致) - Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效 - enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
497 lines
17 KiB
Python
497 lines
17 KiB
Python
from typing import List, Dict, Any, Optional
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
import json
|
|
|
|
from app.models.alert import (
|
|
Alert, AlertRule, WhitelistedMAC,
|
|
AlertType, AlertSeverity, AlertStatus
|
|
)
|
|
from app.models.network import Network, IPAddress
|
|
from app.models.snmp import NetworkDevice, ARPEntry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AlertService:
|
|
"""告警检测与管理服务"""
|
|
|
|
@staticmethod
|
|
def create_alert(db: Session,
|
|
alert_type: AlertType,
|
|
title: str,
|
|
message: str = "",
|
|
severity: AlertSeverity = AlertSeverity.WARNING,
|
|
network_id: Optional[int] = None,
|
|
ip_address_id: Optional[int] = None,
|
|
device_id: Optional[int] = None,
|
|
ip_address_str: Optional[str] = None,
|
|
mac_address: Optional[str] = None,
|
|
conflicting_mac: Optional[str] = None,
|
|
usage_percent: Optional[float] = None) -> Alert:
|
|
"""创建告警"""
|
|
# 检查是否已有相同的活跃告警,避免重复告警
|
|
existing = db.query(Alert).filter(
|
|
Alert.alert_type == alert_type,
|
|
Alert.status == AlertStatus.ACTIVE
|
|
)
|
|
|
|
if ip_address_str:
|
|
existing = existing.filter(Alert.ip_address_str == ip_address_str)
|
|
if network_id:
|
|
existing = existing.filter(Alert.network_id == network_id)
|
|
if device_id:
|
|
existing = existing.filter(Alert.device_id == device_id)
|
|
|
|
existing = existing.first()
|
|
|
|
if existing:
|
|
logger.debug(f"已有相同的活跃告警: {alert_type} - {ip_address_str or network_id or device_id}")
|
|
return existing
|
|
|
|
alert = Alert(
|
|
alert_type=alert_type,
|
|
title=title,
|
|
message=message,
|
|
severity=severity,
|
|
network_id=network_id,
|
|
ip_address_id=ip_address_id,
|
|
device_id=device_id,
|
|
ip_address_str=ip_address_str,
|
|
mac_address=mac_address,
|
|
conflicting_mac=conflicting_mac,
|
|
usage_percent=usage_percent
|
|
)
|
|
|
|
db.add(alert)
|
|
db.commit()
|
|
db.refresh(alert)
|
|
|
|
logger.info(f"创建告警: {alert_type} - {title}")
|
|
return alert
|
|
|
|
@staticmethod
|
|
def detect_ip_conflicts(db: Session) -> List[Dict[str, Any]]:
|
|
"""检测 IP 冲突"""
|
|
conflicts = []
|
|
|
|
# 检查 ARP 表中同一个 IP 对应多个不同 MAC 的情况
|
|
subquery = db.query(
|
|
ARPEntry.ip_address
|
|
).group_by(ARPEntry.ip_address).having(
|
|
db.func.count(db.func.distinct(ARPEntry.mac_address)) > 1
|
|
).subquery()
|
|
|
|
conflict_entries = db.query(ARPEntry).filter(
|
|
ARPEntry.ip_address.in_(subquery)
|
|
).order_by(ARPEntry.ip_address).all()
|
|
|
|
# 按 IP 分组
|
|
ip_macs = {}
|
|
for entry in conflict_entries:
|
|
if entry.ip_address not in ip_macs:
|
|
ip_macs[entry.ip_address] = set()
|
|
ip_macs[entry.ip_address].add(entry.mac_address)
|
|
|
|
# 为每个冲突创建告警
|
|
for ip, macs in ip_macs.items():
|
|
if len(macs) > 1:
|
|
mac_list = list(macs)
|
|
conflict_info = {
|
|
'ip_address': ip,
|
|
'mac_addresses': mac_list
|
|
}
|
|
conflicts.append(conflict_info)
|
|
|
|
# 创建告警
|
|
AlertService.create_alert(
|
|
db=db,
|
|
alert_type=AlertType.IP_CONFLICT,
|
|
title=f"IP 冲突检测: {ip}",
|
|
message=f"IP 地址 {ip} 检测到多个 MAC 地址: {', '.join(mac_list)}",
|
|
severity=AlertSeverity.ERROR,
|
|
ip_address_str=ip,
|
|
mac_address=mac_list[0],
|
|
conflicting_mac=mac_list[1] if len(mac_list) > 1 else None
|
|
)
|
|
|
|
return conflicts
|
|
|
|
@staticmethod
|
|
def detect_unauthorized_access(db: Session) -> List[Dict[str, Any]]:
|
|
"""检测未授权接入(新 MAC 地址不在白名单中)"""
|
|
unauthorized = []
|
|
|
|
# 获取白名单 MAC
|
|
whitelist = set()
|
|
whitelist_entries = db.query(WhitelistedMAC).filter(
|
|
WhitelistedMAC.is_active == True
|
|
).all()
|
|
for entry in whitelist_entries:
|
|
whitelist.add(entry.mac_address.upper())
|
|
|
|
# 获取所有 ARP 表中的 MAC 地址
|
|
arp_entries = db.query(ARPEntry).all()
|
|
|
|
for entry in arp_entries:
|
|
if not entry.mac_address:
|
|
continue
|
|
|
|
mac = entry.mac_address.upper()
|
|
if mac and mac not in whitelist:
|
|
# 检查是否是最近发现的(1小时内)
|
|
is_recent = entry.discovered_at >= datetime.utcnow() - timedelta(hours=1)
|
|
|
|
if is_recent:
|
|
unauth_info = {
|
|
'ip_address': entry.ip_address,
|
|
'mac_address': mac,
|
|
'discovered_at': entry.discovered_at
|
|
}
|
|
unauthorized.append(unauth_info)
|
|
|
|
# 创建告警
|
|
AlertService.create_alert(
|
|
db=db,
|
|
alert_type=AlertType.UNAUTHORIZED_ACCESS,
|
|
title=f"未授权接入: {mac}",
|
|
message=f"检测到未授权设备接入: IP {entry.ip_address}, MAC {mac}",
|
|
severity=AlertSeverity.WARNING,
|
|
ip_address_str=entry.ip_address,
|
|
mac_address=mac
|
|
)
|
|
|
|
return unauthorized
|
|
|
|
@staticmethod
|
|
def detect_subnet_exhaustion(db: Session) -> List[Dict[str, Any]]:
|
|
"""检测网段耗尽"""
|
|
exhausted = []
|
|
|
|
# 获取所有网段的统计信息
|
|
networks = db.query(Network).all()
|
|
|
|
for network in networks:
|
|
if network.total_ips == 0:
|
|
continue
|
|
|
|
# 计算使用中的 IP 数量(在线或已分配)
|
|
used_count = db.query(IPAddress).filter(
|
|
IPAddress.network_id == network.id
|
|
).filter(
|
|
(IPAddress.mac_address.isnot(None)) |
|
|
(IPAddress.hostname.isnot(None))
|
|
).count()
|
|
|
|
usage_percent = (used_count / network.total_ips) * 100
|
|
|
|
# 默认阈值 90%
|
|
threshold = 90.0
|
|
|
|
if usage_percent >= threshold:
|
|
exhaustion_info = {
|
|
'network_id': network.id,
|
|
'cidr': network.cidr,
|
|
'name': network.name,
|
|
'total_ips': network.total_ips,
|
|
'used_ips': used_count,
|
|
'usage_percent': round(usage_percent, 2)
|
|
}
|
|
exhausted.append(exhaustion_info)
|
|
|
|
# 创建告警
|
|
AlertService.create_alert(
|
|
db=db,
|
|
alert_type=AlertType.SUBNET_FULL,
|
|
title=f"网段使用率告警: {network.cidr}",
|
|
message=f"网段 {network.cidr} ({network.name}) 使用率达到 {round(usage_percent, 2)}%,总IP {network.total_ips},已使用 {used_count}",
|
|
severity=AlertSeverity.WARNING if usage_percent < 95 else AlertSeverity.CRITICAL,
|
|
network_id=network.id,
|
|
usage_percent=usage_percent
|
|
)
|
|
|
|
return exhausted
|
|
|
|
@staticmethod
|
|
def detect_new_devices(db: Session) -> List[Dict[str, Any]]:
|
|
"""检测新发现的设备"""
|
|
new_devices = []
|
|
|
|
# 查找最近 1 小时内发现的新 MAC 地址
|
|
one_hour_ago = datetime.utcnow() - timedelta(hours=1)
|
|
|
|
recent_entries = db.query(ARPEntry).filter(
|
|
ARPEntry.discovered_at >= one_hour_ago
|
|
).all()
|
|
|
|
for entry in recent_entries:
|
|
# 检查此 MAC 是否是新出现的
|
|
older_entries = db.query(ARPEntry).filter(
|
|
ARPEntry.mac_address == entry.mac_address,
|
|
ARPEntry.discovered_at < one_hour_ago
|
|
).count()
|
|
|
|
if older_entries == 0 and entry.mac_address:
|
|
device_info = {
|
|
'ip_address': entry.ip_address,
|
|
'mac_address': entry.mac_address,
|
|
'discovered_at': entry.discovered_at
|
|
}
|
|
new_devices.append(device_info)
|
|
|
|
# 创建告警
|
|
AlertService.create_alert(
|
|
db=db,
|
|
alert_type=AlertType.NEW_DEVICE_DETECTED,
|
|
title=f"新设备发现: {entry.ip_address}",
|
|
message=f"检测到新设备: IP {entry.ip_address}, MAC {entry.mac_address}",
|
|
severity=AlertSeverity.INFO,
|
|
ip_address_str=entry.ip_address,
|
|
mac_address=entry.mac_address
|
|
)
|
|
|
|
return new_devices
|
|
|
|
@staticmethod
|
|
def detect_device_offline(db: Session) -> List[Dict[str, Any]]:
|
|
"""检测设备离线"""
|
|
offline_devices = []
|
|
|
|
# 检查网络设备是否长时间没有成功轮询
|
|
threshold_time = datetime.utcnow() - timedelta(hours=2)
|
|
|
|
offline_network_devices = db.query(NetworkDevice).filter(
|
|
NetworkDevice.is_active == True,
|
|
(NetworkDevice.last_successful_poll < threshold_time) |
|
|
(NetworkDevice.last_successful_poll == None)
|
|
).all()
|
|
|
|
for device in offline_network_devices:
|
|
offline_info = {
|
|
'device_id': device.id,
|
|
'device_name': device.name,
|
|
'ip_address': device.ip_address,
|
|
'last_polled_at': device.last_polled_at,
|
|
'last_successful_poll': device.last_successful_poll
|
|
}
|
|
offline_devices.append(offline_info)
|
|
|
|
# 创建告警
|
|
AlertService.create_alert(
|
|
db=db,
|
|
alert_type=AlertType.DEVICE_OFFLINE,
|
|
title=f"设备离线: {device.name}",
|
|
message=f"网络设备 {device.name} ({device.ip_address}) 已超过 2 小时未成功轮询",
|
|
severity=AlertSeverity.ERROR,
|
|
device_id=device.id
|
|
)
|
|
|
|
return offline_devices
|
|
|
|
@staticmethod
|
|
def run_all_detections(db: Session) -> Dict[str, Any]:
|
|
"""运行所有检测"""
|
|
results = {}
|
|
|
|
logger.info("开始运行告警检测...")
|
|
|
|
# 检测 IP 冲突
|
|
conflicts = AlertService.detect_ip_conflicts(db)
|
|
results['ip_conflicts'] = {
|
|
'count': len(conflicts),
|
|
'items': conflicts
|
|
}
|
|
|
|
# 检测未授权接入
|
|
unauthorized = AlertService.detect_unauthorized_access(db)
|
|
results['unauthorized_access'] = {
|
|
'count': len(unauthorized),
|
|
'items': unauthorized
|
|
}
|
|
|
|
# 检测网段耗尽
|
|
subnet_exhaustion = AlertService.detect_subnet_exhaustion(db)
|
|
results['subnet_exhaustion'] = {
|
|
'count': len(subnet_exhaustion),
|
|
'items': subnet_exhaustion
|
|
}
|
|
|
|
# 检测新设备
|
|
new_devices = AlertService.detect_new_devices(db)
|
|
results['new_devices'] = {
|
|
'count': len(new_devices),
|
|
'items': new_devices
|
|
}
|
|
|
|
# 检测设备离线
|
|
offline_devices = AlertService.detect_device_offline(db)
|
|
results['offline_devices'] = {
|
|
'count': len(offline_devices),
|
|
'items': offline_devices
|
|
}
|
|
|
|
total_alerts = sum(v['count'] for v in results.values())
|
|
logger.info(f"告警检测完成,共发现 {total_alerts} 个问题")
|
|
|
|
return results
|
|
|
|
@staticmethod
|
|
def get_alerts(db: Session,
|
|
status: Optional[AlertStatus] = None,
|
|
severity: Optional[AlertSeverity] = None,
|
|
alert_type: Optional[AlertType] = None,
|
|
skip: int = 0,
|
|
limit: int = 100) -> tuple[int, List[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, items
|
|
|
|
@staticmethod
|
|
def acknowledge_alert(db: Session, alert_id: int, acknowledged_by: str = "system") -> Optional[Alert]:
|
|
"""确认告警"""
|
|
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
|
if not alert:
|
|
return None
|
|
|
|
alert.status = AlertStatus.ACKNOWLEDGED
|
|
alert.acknowledged_by = acknowledged_by
|
|
alert.acknowledged_at = datetime.utcnow()
|
|
db.commit()
|
|
db.refresh(alert)
|
|
|
|
return alert
|
|
|
|
@staticmethod
|
|
def resolve_alert(db: Session, alert_id: int, resolved_by: str = "system", notes: str = "") -> Optional[Alert]:
|
|
"""解决告警"""
|
|
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
|
if not alert:
|
|
return None
|
|
|
|
alert.status = AlertStatus.RESOLVED
|
|
alert.resolved_by = resolved_by
|
|
alert.resolved_at = datetime.utcnow()
|
|
alert.resolution_notes = notes
|
|
db.commit()
|
|
db.refresh(alert)
|
|
|
|
return alert
|
|
|
|
@staticmethod
|
|
def ignore_alert(db: Session, alert_id: int) -> Optional[Alert]:
|
|
"""忽略告警"""
|
|
alert = db.query(Alert).filter(Alert.id == alert_id).first()
|
|
if not alert:
|
|
return None
|
|
|
|
alert.status = AlertStatus.IGNORED
|
|
db.commit()
|
|
db.refresh(alert)
|
|
|
|
return alert
|
|
|
|
@staticmethod
|
|
def get_statistics(db: Session) -> Dict[str, Any]:
|
|
"""获取告警统计"""
|
|
active_count = db.query(Alert).filter(Alert.status == AlertStatus.ACTIVE).count()
|
|
acknowledged_count = db.query(Alert).filter(Alert.status == AlertStatus.ACKNOWLEDGED).count()
|
|
resolved_count = db.query(Alert).filter(Alert.status == AlertStatus.RESOLVED).count()
|
|
|
|
# 按严重级别统计
|
|
critical_count = db.query(Alert).filter(
|
|
Alert.status == AlertStatus.ACTIVE,
|
|
Alert.severity == AlertSeverity.CRITICAL
|
|
).count()
|
|
error_count = db.query(Alert).filter(
|
|
Alert.status == AlertStatus.ACTIVE,
|
|
Alert.severity == AlertSeverity.ERROR
|
|
).count()
|
|
warning_count = db.query(Alert).filter(
|
|
Alert.status == AlertStatus.ACTIVE,
|
|
Alert.severity == AlertSeverity.WARNING
|
|
).count()
|
|
info_count = db.query(Alert).filter(
|
|
Alert.status == AlertStatus.ACTIVE,
|
|
Alert.severity == AlertSeverity.INFO
|
|
).count()
|
|
|
|
# 按类型统计
|
|
type_stats = {}
|
|
for alert_type in AlertType:
|
|
count = db.query(Alert).filter(
|
|
Alert.status == AlertStatus.ACTIVE,
|
|
Alert.alert_type == alert_type
|
|
).count()
|
|
type_stats[alert_type.value] = count
|
|
|
|
return {
|
|
'by_status': {
|
|
'active': active_count,
|
|
'acknowledged': acknowledged_count,
|
|
'resolved': resolved_count
|
|
},
|
|
'by_severity': {
|
|
'critical': critical_count,
|
|
'error': error_count,
|
|
'warning': warning_count,
|
|
'info': info_count
|
|
},
|
|
'by_type': type_stats
|
|
}
|
|
|
|
@staticmethod
|
|
def add_mac_to_whitelist(db: Session, mac_address: str, description: str = "", owner: str = "") -> WhitelistedMAC:
|
|
"""添加 MAC 地址到白名单"""
|
|
existing = db.query(WhitelistedMAC).filter(WhitelistedMAC.mac_address == mac_address.upper()).first()
|
|
if existing:
|
|
existing.is_active = True
|
|
if description:
|
|
existing.description = description
|
|
if owner:
|
|
existing.owner = owner
|
|
db.commit()
|
|
db.refresh(existing)
|
|
return existing
|
|
|
|
whitelisted_mac = WhitelistedMAC(
|
|
mac_address=mac_address.upper(),
|
|
description=description,
|
|
owner=owner
|
|
)
|
|
db.add(whitelisted_mac)
|
|
db.commit()
|
|
db.refresh(whitelisted_mac)
|
|
|
|
return whitelisted_mac
|
|
|
|
@staticmethod
|
|
def get_mac_whitelist(db: Session, skip: int = 0, limit: int = 100) -> tuple[int, List[WhitelistedMAC]]:
|
|
"""获取 MAC 白名单"""
|
|
query = db.query(WhitelistedMAC).filter(WhitelistedMAC.is_active == True)
|
|
total = query.count()
|
|
items = query.order_by(WhitelistedMAC.id.desc()).offset(skip).limit(limit).all()
|
|
return total, items
|
|
|
|
@staticmethod
|
|
def remove_mac_from_whitelist(db: Session, mac_id: int) -> bool:
|
|
"""从白名单移除 MAC 地址"""
|
|
mac = db.query(WhitelistedMAC).filter(WhitelistedMAC.id == mac_id).first()
|
|
if not mac:
|
|
return False
|
|
|
|
mac.is_active = False
|
|
db.commit()
|
|
return True
|