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