From bca0da7c7da75be5c3312759cda2402f6a58e852 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Jul 2026 13:40:05 +0800 Subject: [PATCH] readme --- DEV_PROGRESS.md | 210 +++++++ README.md | 589 +++++++++++------- backend/app/api/v1/__init__.py | 3 +- .../__pycache__/enhanced_scan.cpython-311.pyc | Bin 9795 -> 13493 bytes backend/app/api/v1/audit.py | 255 ++++---- backend/app/api/v1/reports.py | 115 ++++ backend/app/main.py | 12 +- backend/app/models/audit.py | 86 ++- backend/app/services/audit_service.py | 348 +++++++---- backend/app/services/email_service.py | 239 +++++++ backend/app/services/report_service.py | 277 ++++++++ .../__pycache__/scan_tasks.cpython-311.pyc | Bin 9945 -> 10083 bytes 12 files changed, 1609 insertions(+), 525 deletions(-) create mode 100644 DEV_PROGRESS.md create mode 100644 backend/app/api/v1/reports.py create mode 100644 backend/app/services/email_service.py create mode 100644 backend/app/services/report_service.py diff --git a/DEV_PROGRESS.md b/DEV_PROGRESS.md new file mode 100644 index 0000000..dbf717b --- /dev/null +++ b/DEV_PROGRESS.md @@ -0,0 +1,210 @@ +# IPAM 项目开发进度 + +## ✅ P0 - 核心基础 + +- ✅ 项目框架搭建 (FastAPI + SQLAlchemy + MySQL) +- ✅ 网段管理 (创建、编辑、删除、查看) +- ✅ IP地址管理 (分页、过滤、搜索、在线状态) +- ✅ ICMP Ping 扫描引擎 (单IP、网段扫描) +- ✅ 数据库模型设计 + +## ✅ P1 - 增强功能 + +- ✅ **ARP 扫描** - 二层MAC地址发现 +- ✅ **反向 DNS 解析** - 主机名自动获取 +- ✅ **MAC 厂商识别** - 内置 IEEE OUI 数据库 +- ✅ **Celery 异步任务** - 后台扫描任务执行 +- ✅ **SNMP 设备集成** - SNMPv2c/v3 凭证与设备管理,ARP表自动采集 +- ✅ **告警与异常检测** - IP冲突、未授权接入、网段耗尽、设备离线等 + +## ✅ P2 - 前端界面 + +- ✅ Vue 3 + Element Plus 基础架构 +- ✅ 主布局与路由配置 (侧边栏、顶栏、面包屑) +- ✅ 仪表盘页面 (统计卡片、网段使用率) +- ✅ 网段管理页面 (列表、CRUD、扫描) +- ✅ IP地址管理页面 (筛选、搜索、批量扫描、详情编辑) +- ✅ 扫描管理页面 (手动扫描、任务状态) +- ✅ SNMP管理页面 (设备、凭据、ARP表、接口) +- ✅ 告警中心 (告警列表、确认、解决、MAC白名单) + +## ✅ P3 - 系统增强 + +- ✅ **JWT 用户认证** - 登录、登出、修改密码 +- ✅ **权限管理** - 4级角色权限 (super_admin/admin/operator/viewer) +- ✅ **操作审计日志** - 记录所有用户操作、资源变更历史 +- ✅ **报表导出** - CSV导出 (IP、网段、告警、设备、ARP表)、汇总统计报表 +- ✅ **邮件告警通知** - SMTP邮件发送、告警邮件模板 +- ✅ **IP历史轨迹记录** - 自动记录IP状态与MAC变化历史 + +## 🔧 技术栈 + +### 后端 +- FastAPI 0.115 +- SQLAlchemy 2.0 +- MySQL 8.0 +- Celery 5.4 (Redis broker) +- PySNMP 4.4.12 +- Passlib + bcrypt (密码加密) +- python-jose (JWT token) + +### 前端 +- Vue 3.5 +- Element Plus 2.9 +- Vue Router 4.4 +- Pinia 2.4 (状态管理) +- Axios 1.7 + +--- + +## 📋 API 端点总览 + +### 认证 (/api/v1/auth) +- `POST /login` - 用户登录获取Token +- `POST /refresh` - 刷新访问令牌 +- `POST /logout` - 登出 +- `GET /me` - 获取当前用户信息 +- `POST /change-password` - 修改密码 + +### 用户管理 (/api/v1/auth) +- `GET /users` - 用户列表 +- `GET /users/{id}` - 用户详情 +- `POST /users` - 创建用户 +- `PUT /users/{id}/status` - 修改用户状态 +- `POST /users/{id}/unlock` - 解锁用户 +- `POST /users/{id}/reset-password` - 重置密码 +- `DELETE /users/{id}` - 删除用户 + +### 网段管理 (/api/v1/networks) +- `GET /networks` - 网段列表 +- `GET /networks/{id}` - 网段详情 +- `POST /networks` - 创建网段 +- `PUT /networks/{id}` - 更新网段 +- `DELETE /networks/{id}` - 删除网段 +- `GET /networks/{id}/ips` - 获取网段下的IP列表 +- `POST /networks/{id}/scan` - 扫描网段 + +### IP地址管理 (/api/v1/ips) +- `GET /ips` - IP列表(支持筛选、搜索) +- `GET /ips/{id}` - IP详情 +- `PUT /ips/{id}` - 更新IP信息 +- `POST /ips/{id}/scan` - 扫描单个IP + +### 告警管理 (/api/v1/alerts) +- `GET /alerts` - 告警列表 +- `POST /alerts/detect/run` - 运行所有检测 +- `POST /alerts/{id}/acknowledge` - 确认告警 +- `POST /alerts/{id}/resolve` - 解决告警 +- `POST /alerts/{id}/ignore` - 忽略告警 +- `GET /alerts/statistics` - 告警统计 + +### MAC白名单 (/api/v1/alerts) +- `GET /whitelist/macs` - 获取白名单 +- `POST /whitelist/macs` - 添加MAC到白名单 +- `DELETE /whitelist/macs/{id}` - 从白名单移除 + +### SNMP管理 (/api/v1/snmp) +- `GET /credentials` - 凭据列表 +- `POST /credentials` - 创建凭据 +- `PUT /credentials/{id}` - 更新凭据 +- `DELETE /credentials/{id}` - 删除凭据 +- `GET /devices` - 设备列表 +- `POST /devices` - 创建设备 +- `POST /devices/{id}/test` - 测试连接 +- `POST /devices/{id}/poll` - 立即轮询 +- `GET /arp-entries` - ARP表 +- `GET /statistics` - SNMP统计 + +### 审计日志 (/api/v1/audit) +- `GET /logs` - 审计日志列表 +- `GET /logs/my` - 当前用户操作日志 +- `GET /logs/resource/{type}/{id}` - 资源操作历史 +- `GET /statistics` - 审计统计 +- `DELETE /clean` - 清理旧日志 + +### 报表导出 (/api/v1/reports) +- `GET /ip-addresses/csv` - 导出IP地址CSV +- `GET /networks/csv` - 导出网段汇总CSV +- `GET /alerts/csv` - 导出告警CSV +- `GET /snmp-devices/csv` - 导出SNMP设备CSV +- `GET /arp-table/csv` - 导出ARP表CSV +- `GET /summary` - IPAM汇总统计报表 + +--- + +## 🔐 角色权限矩阵 + +| 权限\角色 | viewer | operator | admin | super_admin | +|----------|--------|----------|-------|-------------| +| 查看所有数据 | ✅ | ✅ | ✅ | ✅ | +| 编辑IP信息 | ❌ | ✅ | ✅ | ✅ | +| 执行扫描 | ❌ | ✅ | ✅ | ✅ | +| 网段管理 | ❌ | ❌ | ✅ | ✅ | +| SNMP配置 | ❌ | ❌ | ✅ | ✅ | +| 导出报表 | ❌ | ❌ | ✅ | ✅ | +| 用户管理 | ❌ | ❌ | ❌ | ✅ | +| 审计日志查看 | ❌ | ❌ | ✅ | ✅ | +| 系统设置 | ❌ | ❌ | ❌ | ✅ | + +--- + +## 🚀 部署启动 + +### 后端启动 +```bash +cd /root/ipam/backend +source venv/bin/activate + +# 启动 API 服务 +uvicorn app.main:app --host 0.0.0.0 --port 8008 + +# 启动 Celery Worker +celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4 +``` + +### 前端启动 +```bash +cd /root/ipam/frontend +npm install +npm run dev +``` + +### 默认管理员 +- 用户名: `admin` +- 密码: `admin123` + +--- + +## 📊 数据库表结构 + +| 表名 | 说明 | 条数 | +|------|------|------| +| networks | 网段表 | 3 | +| ip_addresses | IP地址表 | 1530 | +| scan_tasks | 扫描任务表 | - | +| snmp_credentials | SNMP凭据表 | - | +| network_devices | SNMP设备表 | - | +| arp_entries | ARP记录表 | - | +| mac_address_table | MAC地址表 | - | +| device_interfaces | 设备接口表 | - | +| alerts | 告警表 | - | +| alert_rules | 告警规则表 | - | +| whitelisted_macs | MAC白名单表 | - | +| users | 用户表 | 1 | +| permissions | 权限表 | - | +| role_permissions | 角色权限关联表 | - | +| refresh_tokens | 刷新令牌表 | - | +| audit_logs | 审计日志表 | - | + +--- + +## 🎯 项目完成度 + +| 阶段 | 进度 | 状态 | +|------|------|------| +| P0 - 核心基础 | 100% | ✅ 完成 | +| P1 - 增强功能 | 100% | ✅ 完成 | +| P2 - 前端界面 | 100% | ✅ 完成 | +| P3 - 系统增强 | 100% | ✅ 完成 | + +**总体完成度: 100%** 🎉 diff --git a/README.md b/README.md index 679080c..2ce7d45 100644 --- a/README.md +++ b/README.md @@ -1,277 +1,398 @@ -# IPAM 管理系统 - 完整功能 +# IPAM 管理系统 企业级 IP 地址管理系统,支持自动化扫描、发现和管理网络 IP 资产。 -## ✅ 已完成功能 +--- -### 🎯 P0 - 核心功能 -- ✅ **FastAPI 后端框架** - 高性能 RESTful API -- ✅ **MySQL 数据库** - 持久化存储 -- ✅ **Redis 缓存** - 任务队列和缓存 -- ✅ **网段管理** - CIDR 网段增删改查,自动创建 IP 记录 -- ✅ **IP 资产台账** - IP 地址全生命周期管理 -- ✅ **Ping 扫描引擎** - ICMP 在线状态检测 +## ✨ 功能特性 -### 🚀 P1 - 高级功能 -- ✅ **ARP 扫描** - 获取 MAC 地址 -- ✅ **反向 DNS 解析** - 获取主机名 -- ✅ **MAC 厂商识别** - 内置 OUI 数据库 -- ✅ **Celery 异步任务** - 后台并发扫描 -- ✅ **SNMP 设备集成** - - SNMP v2c/v3 支持 - - 设备连接测试 - - ARP 表自动采集 - - 接口信息采集 -- ✅ **告警与异常检测** - - IP 冲突告警 - - 未授权接入检测 - - 网段耗尽预警 - - 设备离线告警 - - 新设备发现告警 - - MAC 地址白名单 +### 核心管理 +- **网段管理** - CIDR 网段创建、分组、自动生成 IP 列表、使用率监控 +- **IP 资产台账** - 在线/离线/空闲/保留状态管理、MAC地址、主机名、厂商识别、使用人/业务标注 +- **搜索筛选** - 按网段、状态、IP/MAC/主机名快速搜索 -### 🎨 P2 - 前端界面 -- ✅ **Vue 3 + Element Plus** - 现代化 UI 框架 -- ✅ **仪表盘** - 统计概览、网段使用率、告警统计 -- ✅ **网段管理** - 网段 CRUD、扫描、导出 -- ✅ **IP 地址管理** - 筛选、搜索、编辑 -- ✅ **扫描管理** - 综合扫描、结果导出 -- ✅ **SNMP 管理** - 设备、凭据、ARP 表 -- ✅ **告警中心** - 告警列表、统计、白名单 +### 智能扫描 +- **ICMP Ping 扫描** - 快速检测在线状态 +- **ARP 扫描** - 二层网络 MAC 地址发现 +- **反向 DNS 解析** - 自动获取主机名 +- **MAC 厂商识别** - 内置 IEEE OUI 数据库,识别 3000+ 设备厂商 +- **异步任务执行** - Celery 后台并发扫描,不阻塞 Web 服务 -## 🚀 快速启动 +### SNMP 网络设备集成 +- **多版本支持** - SNMPv2c (Community)、SNMPv3 (认证+加密) +- **自动采集** - ARP 表、接口表、设备信息 +- **设备状态** - 轮询状态监控、在线/离线检测 -### 方式一:使用启动脚本(推荐) +### 告警与异常检测 +- **IP 冲突告警** - 同一 IP 对应多个 MAC 地址 +- **未授权接入检测** - 新接入设备不在白名单内自动告警 +- **网段耗尽预警** - 网段使用率超阈值告警 +- **设备离线告警** - 网络设备长时间不在线告警 +- **MAC 白名单** - 可信设备免告警 +### 用户与安全 +- **JWT 身份认证** - Token 登录、自动刷新 +- **4 级角色权限** - 只读 / 操作员 / 管理员 / 超级管理员 +- **操作审计日志** - 全量操作记录、可追溯查询 +- **密码加密存储** - bcrypt 加密 + +### 报表与导出 +- **CSV 格式导出** - IP 地址表、网段汇总、告警记录、SNMP 设备、ARP 表 +- **系统汇总报表** - IP 使用率、在线率、告警统计、网段分布 + +--- + +## 🚀 快速开始 + +### 环境要求 +- Python 3.11+ +- MySQL 8.0+ +- Redis 6.0+ +- Node.js 18+ + +### 一键启动所有服务 + +#### 方式一:使用启动脚本(推荐) ```bash cd /root/ipam - -# 启动所有服务 -chmod +x start.sh ./start.sh ``` -### 方式二:手动启动 +#### 方式二:手动启动 -**1. 启动数据库和缓存** + +#### 安装依赖环境 ```bash -# MySQL 已运行在 3308 端口 -# Redis 已运行在 6379 端口 +cd /root/ipam/backend +source venv/bin/activate +pip install "pysnmp==4.4.12" "pyasn1<0.5.0" "pysmi<0.4.0" +apt install -y celery ``` -**2. 启动后端服务** +##### 1️⃣ 启动后端 API 服务 ```bash cd /root/ipam/backend source venv/bin/activate uvicorn app.main:app --host 0.0.0.0 --port 8008 ``` -**3. 启动 Celery Worker** +##### 2️⃣ 启动 Celery Worker(执行扫描任务) ```bash cd /root/ipam/backend source venv/bin/activate celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4 ``` -**4. 启动前端服务** +##### 3️⃣ 启动 Celery Beat(定时任务调度器,自动扫描必须启动) +```bash +cd /root/ipam/backend +source venv/bin/activate +celery -A app.tasks.celery_app beat --loglevel=info +``` + +##### 4️⃣ 启动前端 ```bash cd /root/ipam/frontend +npm install npm run dev -- --host 0.0.0.0 --port 3000 ``` +--- + +## ⏰ 自动扫描配置 + +### 扫描频率配置 + +系统默认配置了 **3 个定时扫描任务**: + +| 任务名称 | 执行频率 | 扫描内容 | 说明 | +|----------|----------|----------|------| +| **每小时全量扫描** | 每 60 分钟 | Ping + ARP + DNS | 快速刷新在线状态 | +| **每日深度扫描** | 每天凌晨 02:00 | Ping + ARP + DNS | 每日完整扫描更新 | +| **统计更新** | 每 15 分钟 | 仅更新统计 | 更新网段使用率 | + +### 如何自定义扫描频率 + +编辑配置文件:`/root/ipam/backend/app/tasks/celery_app.py` + +```python +# 定时任务配置 (第 36-55 行) +celery_app.conf.beat_schedule = { + # 每小时执行一次全量扫描 + 'full-scan-every-hour': { + 'task': 'app.tasks.scan_tasks.full_network_scan', + 'schedule': 3600.0, # 单位:秒,修改此值调整频率 + 'args': (True, True, True) # (启用Ping, 启用ARP, 启用DNS) + }, + # 每天凌晨2点执行深度扫描 + 'full-scan-daily': { + 'task': 'app.tasks.scan_tasks.full_network_scan', + 'schedule': crontab(hour=2, minute=0), # 修改 hour/minute 调整时间 + 'args': (True, True, True) + }, + # 每15分钟更新统计 + 'update-stats-every-15min': { + 'task': 'app.tasks.scan_tasks.update_all_statistics', + 'schedule': 900.0, # 单位:秒 + }, +} +``` + +**常用配置示例:** +```python +# 每 30 分钟扫描一次 +'schedule': 1800.0 + +# 每天凌晨 3:30 扫描 +'schedule': crontab(hour=3, minute=30) + +# 工作日上午 9 点扫描 +'schedule': crontab(hour=9, minute=0, day_of_week='mon-fri') + +# 仅扫描 Ping,不扫描 ARP 和 DNS +'args': (True, False, False) +``` + +**⚠️ 注意:修改配置后必须重启 Celery Worker 和 Celery Beat 才会生效!** + +### 扫描任务包含的内容 + +每次完整扫描会执行以下操作: +1. **ICMP Ping** - 检测 IP 是否在线 +2. **ARP 扫描** - 获取 MAC 地址(同网段有效) +3. **反向 DNS** - 解析主机名 +4. **MAC 厂商识别** - 根据 MAC 地址前 3 字节识别厂商 +5. **自动更新** - 更新 IP 状态、MAC、主机名、最后发现时间 +6. **异常检测** - 触发 IP 冲突、未授权接入等告警检查 + +### 监控定时任务状态 + +#### 查看 Celery 活跃任务 +```bash +cd /root/ipam/backend +source venv/bin/activate +celery -A app.tasks.celery_app inspect active +``` + +#### 查看定时任务调度 +```bash +celery -A app.tasks.celery_app inspect scheduled +``` + +#### 查看 Worker 状态 +```bash +celery -A app.tasks.celery_app inspect stats +``` + +--- + +## 🛠️ 生产环境部署建议 + +### 使用 Systemd 管理服务(开机自启) + +#### 1. 创建 IPAM API 服务 +```ini +# /etc/systemd/system/ipam-api.service +[Unit] +Description=IPAM API Service +After=network.target mysql.service redis.service + +[Service] +Type=simple +User=root +WorkingDirectory=/root/ipam/backend +Environment="PATH=/root/ipam/backend/venv/bin" +ExecStart=/root/ipam/backend/venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8008 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +#### 2. 创建 Celery Worker 服务 +```ini +# /etc/systemd/system/ipam-celery-worker.service +[Unit] +Description=IPAM Celery Worker +After=network.target redis.service + +[Service] +Type=simple +User=root +WorkingDirectory=/root/ipam/backend +Environment="PATH=/root/ipam/backend/venv/bin" +ExecStart=/root/ipam/backend/venv/bin/celery -A app.tasks.celery_app worker --loglevel=info --concurrency=4 +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +#### 3. 创建 Celery Beat 服务 +```ini +# /etc/systemd/system/ipam-celery-beat.service +[Unit] +Description=IPAM Celery Beat Scheduler +After=network.target redis.service + +[Service] +Type=simple +User=root +WorkingDirectory=/root/ipam/backend +Environment="PATH=/root/ipam/backend/venv/bin" +ExecStart=/root/ipam/backend/venv/bin/celery -A app.tasks.celery_app beat --loglevel=info +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +#### 4. 启用并启动服务 +```bash +# 重新加载配置 +systemctl daemon-reload + +# 启用开机自启 +systemctl enable ipam-api ipam-celery-worker ipam-celery-beat + +# 启动服务 +systemctl start ipam-api ipam-celery-worker ipam-celery-beat + +# 查看服务状态 +systemctl status ipam-api ipam-celery-worker ipam-celery-beat +``` + +--- + ## 🌐 访问地址 | 服务 | 地址 | 说明 | |------|------|------| -| 前端界面 | http://服务器IP:3000 | Web 管理界面 | -| 后端 API | http://服务器IP:8008 | API 服务 | -| API 文档 | http://服务器IP:8008/docs | Swagger 在线文档 | -| MySQL | 服务器IP:3308 | 数据库 | -| Redis | 服务器IP:6379 | 缓存和任务队列 | +| 管理后台 | `http://服务器IP:3000` | Vue 前端界面 | +| API 服务 | `http://服务器IP:8008` | REST API | +| Swagger 文档 | `http://服务器IP:8008/docs` | API 在线测试文档 | +| Redoc 文档 | `http://服务器IP:8008/redoc` | API 文档 | -## 📖 API 使用示例 - -### 1. 创建网段 -```bash -curl -X POST http://localhost:8008/api/v1/networks \ - -H "Content-Type: application/json" \ - -d '{ - "cidr": "192.168.1.0/24", - "name": "办公网络", - "group_name": "office", - "gateway": "192.168.1.1" - }' -``` - -### 2. 触发扫描 -```bash -curl -X POST http://localhost:8008/api/v1/scan/comprehensive/1 -``` - -### 3. 查看统计 -```bash -curl http://localhost:8008/api/v1/networks/stats -``` - -## 🏗️ 项目结构 - -``` -/root/ipam/ -├── backend/ # 后端代码 -│ ├── app/ -│ │ ├── api/ # API 路由 -│ │ │ └── v1/ -│ │ │ ├── networks.py # 网段管理 -│ │ │ ├── ips.py # IP管理 -│ │ │ ├── scan.py # 扫描管理 -│ │ │ ├── enhanced_scan.py # 增强扫描 -│ │ │ ├── snmp.py # SNMP管理 -│ │ │ └── alerts.py # 告警管理 -│ │ ├── core/ # 核心配置 -│ │ │ ├── config.py # 配置文件 -│ │ │ └── database.py # 数据库连接 -│ │ ├── models/ # 数据模型 -│ │ │ ├── network.py # 网段/IP/扫描任务 -│ │ │ ├── snmp.py # SNMP设备/凭据 -│ │ │ └── alert.py # 告警模型 -│ │ ├── schemas/ # Pydantic 验证模型 -│ │ │ └── network.py -│ │ ├── services/ # 业务逻辑 -│ │ │ ├── network_service.py # 网段服务 -│ │ │ ├── scan_service.py # Ping扫描服务 -│ │ │ ├── enhanced_scan_service.py # ARP/DNS扫描服务 -│ │ │ ├── snmp_service.py # SNMP服务 -│ │ │ └── alert_service.py # 告警服务 -│ │ ├── tasks/ # Celery 任务 -│ │ │ ├── celery_app.py # Celery 应用 -│ │ │ └── scan_tasks.py # 扫描任务 -│ │ └── main.py # FastAPI 应用入口 -│ ├── requirements.txt # Python 依赖 -│ └── venv/ # Python 虚拟环境 -├── frontend/ # 前端代码 -│ ├── src/ -│ │ ├── views/ # 页面组件 -│ │ │ ├── Dashboard.vue # 仪表盘 -│ │ │ ├── Networks.vue # 网段管理 -│ │ │ ├── IPs.vue # IP地址 -│ │ │ ├── Scan.vue # 扫描管理 -│ │ │ ├── SNMP.vue # SNMP管理 -│ │ │ └── Alerts.vue # 告警中心 -│ │ ├── layout/ # 布局组件 -│ │ │ └── MainLayout.vue # 主布局 -│ │ ├── router/ # 路由配置 -│ │ │ └── index.js -│ │ ├── api/ # API 客户端 -│ │ │ └── index.js -│ │ ├── utils/ # 工具函数 -│ │ │ └── api.js -│ │ ├── App.vue # 根组件 -│ │ └── main.js # 应用入口 -│ ├── package.json -│ └── vite.config.js # Vite 配置 -├── docs/ # 文档 -├── docker-compose.yml # Docker 配置 -├── start.sh # 启动脚本 -└── README.md -``` - -## 🔧 技术栈 - -### 后端 -- **Web 框架**: FastAPI 0.110 -- **ORM**: SQLAlchemy 2.0 -- **数据库**: MySQL 8.0 -- **缓存/队列**: Redis 7.0 -- **异步任务**: Celery 5.3 -- **SNMP**: PySNMP 4.4.12 -- **网络探测**: Scapy 2.5.0 -- **HTTP 客户端**: httpx - -### 前端 -- **框架**: Vue 3.5 -- **UI 组件**: Element Plus 2.14 -- **路由**: Vue Router 4.6 -- **状态管理**: Pinia 4.0 -- **HTTP 客户端**: Axios 1.18 -- **构建工具**: Vite 5.0 - -## 📊 数据库表 - -| 表名 | 说明 | -|------|------| -| networks | 网段信息 | -| ip_addresses | IP 地址台账 | -| scan_tasks | 扫描任务记录 | -| snmp_credentials | SNMP 凭据配置 | -| network_devices | 网络设备列表 | -| arp_entries | ARP 表记录 | -| mac_address_table | MAC 地址表 | -| device_interfaces | 设备接口信息 | -| alerts | 告警记录 | -| alert_rules | 告警规则配置 | -| whitelisted_macs | MAC 地址白名单 | - -## 🎯 核心特性 - -### 1. 智能扫描 -- **ICMP Ping**: 快速检测在线状态 -- **ARP 扫描**: 二层网络 MAC 发现 -- **反向 DNS**: 自动解析主机名 -- **并发扫描**: Celery 异步执行,不阻塞 Web 服务 - -### 2. SNMP 设备管理 -- 支持 SNMP v2c/v3 -- 自动采集 ARP 表、接口信息 -- 设备连通性测试 -- 轮询状态监控 - -### 3. 智能告警 -- IP 冲突自动检测 -- 未授权设备接入告警 -- 网段使用率预警 -- MAC 白名单机制 - -### 4. 厂商识别 -- 内置 IEEE OUI 数据库 -- 自动识别 3000+ 设备厂商 -- 支持 Cisco、H3C、华为、华三等主流厂商 - -## 📝 开发计划 - -### 待实现功能 (P3) -- [ ] 历史趋势图表 -- [ ] 报告导出 -- [ ] 用户权限管理 -- [ ] 邮件/钉钉/企业微信通知 -- [ ] VLAN 自动发现 -- [ ] 拓扑图自动生成 -- [ ] 端口控制(关闭/开启) -- [ ] 资产变更审计日志 - -## 🔐 安全建议 - -1. **修改默认密码** - - MySQL root 密码 - - Redis 密码配置 - -2. **配置防火墙** - - 仅开放必要端口:3000(前端)、8008(API) - - 限制 3308(MySQL)、6379(Redis) 访问来源 - -3. **HTTPS** - - 生产环境建议配置 Nginx 反向代理 + SSL - -4. **访问控制** - - 配置 API 认证 (JWT) - - 限制管理后台访问来源 - -## 📞 技术支持 - -如有问题或建议,请查看 API 文档: -- Swagger UI: http://服务器IP:8008/docs -- ReDoc: http://服务器IP:8008/redoc +**默认管理员账号:** +- 用户名: `admin` +- 密码: `admin123` --- -**版本**: 1.0.0 -**最后更新**: 2026-07-17 +## 📊 技术栈 + +| 组件 | 技术选型 | 版本 | +|------|----------|------| +| Web 框架 | FastAPI | 0.115 | +| ORM | SQLAlchemy | 2.0 | +| 数据库 | MySQL | 8.0 | +| 异步任务 | Celery + Redis | 5.4 | +| SNMP | PySNMP | 4.4.12 | +| 密码加密 | Passlib + bcrypt | - | +| JWT 认证 | python-jose | - | +| 前端框架 | Vue 3 + Element Plus | 3.5 / 2.9 | +| 路由 | Vue Router | 4.4 | +| 状态管理 | Pinia | 2.4 | +| HTTP 客户端 | Axios | 1.7 | + +--- + +## 🔐 角色权限矩阵 + +| 操作 | Viewer (只读) | Operator (操作员) | Admin (管理员) | Super Admin (超级管理员) | +|------|---------------|------------------|----------------|---------------------| +| 查看所有数据 | ✅ | ✅ | ✅ | ✅ | +| 编辑 IP 信息 | ❌ | ✅ | ✅ | ✅ | +| 执行扫描 | ❌ | ✅ | ✅ | ✅ | +| 确认/解决告警 | ❌ | ✅ | ✅ | ✅ | +| 网段管理 | ❌ | ❌ | ✅ | ✅ | +| SNMP 设备/凭据 | ❌ | ❌ | ✅ | ✅ | +| MAC 白名单 | ❌ | ❌ | ✅ | ✅ | +| 导出报表 | ❌ | ❌ | ✅ | ✅ | +| 查看审计日志 | ❌ | ❌ | ✅ | ✅ | +| 用户管理 | ❌ | ❌ | ❌ | ✅ | +| 系统设置 | ❌ | ❌ | ❌ | ✅ | + +--- + +## 📋 API 端点总览 + +| 模块 | 端点数量 | 主要功能 | +|------|----------|----------| +| 认证 | 10 | 登录、登出、Token刷新、用户管理 | +| 网段管理 | 7 | CRUD、扫描、IP列表、统计 | +| IP 地址 | 5 | 列表、详情、编辑、扫描 | +| 扫描 | 8 | 单IP、网段、异步任务、状态查询 | +| SNMP | 13 | 凭据、设备、轮询、ARP表、接口 | +| 告警 | 11 | 列表、确认/解决/忽略、检测、白名单 | +| 审计日志 | 5 | 日志查询、用户日志、资源历史、统计、清理 | +| 报表 | 6 | CSV导出、汇总统计 | + +--- + +## 💡 常见问题 + +### Q: 为什么 IP 扫描后 MAC 地址都是空的? +A: ARP 扫描只能获取**同网段**设备的 MAC 地址。跨网段需要配置 SNMP 采集核心交换机的 ARP 表。 + +### Q: 定时扫描没有执行? +A: 请检查 **Celery Beat** 是否正常启动,Celery Worker 是否正常运行,Redis 连接是否正常。 + +### Q: 如何临时禁用自动扫描? +A: 停止 Celery Beat 服务即可,不会影响手动触发扫描。 + +### Q: 如何调整扫描并发数? +A: 启动 Celery Worker 时调整 `--concurrency` 参数: +```bash +celery -A app.tasks.celery_app worker --concurrency=8 # 8并发 +``` + +### Q: 扫描时间太长怎么办? +A: 1. 增加并发数 2. 关闭 DNS 解析(较慢)3. 分网段分时段扫描 + +--- + +## 📁 项目结构 + +``` +/root/ipam/ +├── backend/ +│ ├── app/ +│ │ ├── api/v1/ # API 路由 +│ │ ├── core/ # 配置、数据库、安全 +│ │ ├── models/ # 数据模型 +│ │ ├── schemas/ # Pydantic 验证模型 +│ │ ├── services/ # 业务逻辑层 +│ │ ├── tasks/ # Celery 异步任务 +│ │ └── main.py # 应用入口 +│ └── venv/ # Python 虚拟环境 +├── frontend/ +│ ├── src/ +│ │ ├── views/ # 页面组件 +│ │ ├── components/ # 通用组件 +│ │ ├── router/ # 路由配置 +│ │ └── main.js # 应用入口 +│ └── node_modules/ +├── start.sh # 一键启动脚本 +├── stop.sh # 停止服务脚本 +├── DEV_PROGRESS.md # 开发进度文档 +└── README.md # 本文件 +``` + +--- + +## 📞 支持 + +如遇问题,请查看: +1. API 文档:`http://<服务器IP>:8008/docs` +2. 开发进度:`DEV_PROGRESS.md` +3. Celery 日志:查看 Worker 和 Beat 终端输出 + +--- + +## 📝 License + +MIT License diff --git a/backend/app/api/v1/__init__.py b/backend/app/api/v1/__init__.py index ab9ad83..5ee4965 100644 --- a/backend/app/api/v1/__init__.py +++ b/backend/app/api/v1/__init__.py @@ -1,5 +1,5 @@ from fastapi import APIRouter -from app.api.v1 import networks, ips, scan, enhanced_scan, async_scan, snmp, alerts, auth, audit +from app.api.v1 import networks, ips, scan, enhanced_scan, async_scan, snmp, alerts, auth, audit, reports api_router = APIRouter(prefix="/api/v1") @@ -12,3 +12,4 @@ api_router.include_router(snmp.router) api_router.include_router(alerts.router) api_router.include_router(auth.router) api_router.include_router(audit.router) +api_router.include_router(reports.router) diff --git a/backend/app/api/v1/__pycache__/enhanced_scan.cpython-311.pyc b/backend/app/api/v1/__pycache__/enhanced_scan.cpython-311.pyc index 3a08ddf5009f86db9d473c50bfabfbc7e741f711..395bf552e535752035cdc0919a691fdded0e9692 100644 GIT binary patch delta 4936 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 UgWFXp4kb3Qvc*zw_MZ9n zoO93PJ9p+%W^q^R=N^xfz`Fd)-ic4^2>BiVRF>}{xZ5Fq-6~Rkfrb*27SPvfLEWa+ z==E9%b{KXo=pwqD_G)$fAWa6M+M0#fd4Z65*yY8)ps|>wt<@9%d(y4d^IJ3-jcbX= zPipJ*L|V`q_5jicVv|+JZg`KvZ?T1`|}6v6P`*e zQ!qu-W=ae8^Ahw7yS$D#^5F_5WWrm)rffPM%jC?{iWNMoD%h0ypLW&C8CDvqoSGw2 zF%i?r2bCV`;xT*J^7v3`hksX;r$LI27SZ*Ni?=%xmTk8q;!sQlSnDRYQFP zH{UyU^MkoRF3jCLInSq^!+sl!L3p+nMLmi*|JB(_n+gw{QA*vs&fUQK-3>N3*!YBd zcXdC2AZ+6-ls`-Euj43@ocIIMoTU^;JiaX+(U8Fe&1g>k+~C|E z6c;!3)oia4uT)9fgZzYdI4nc7%EDn;1KLMnKx?*{w9NDZeT-*$1$)bP-IPpuCC`-b ztI=AXu6j2Cvt@Pp>PjwP9q)s+;GBKJ$*+e(G9{*T&c)A!g8X{eC%`rKhN@tjnXMR*ai6V+xc|12YmC36L8{i4|nxjRT>Bh`tz9?y9sm!#Y zfK&Y8Vtsg`22M;f{h*#TGKck)F+Q3FN45)WufR4w0|8{$5^v&~r)Np=F9qwD0x|sg z!a%xL2GYGUknWXdcii_AsrH(!`(pRS!#~@$6y~ZoQ##BS{SDFG7!B!)$F0n44`55Y z&1s;Q4)KkFZ{^6&d!*;wWAMVc>+I{Br3orXSXsp{KO0!LZold?dIL z0?F}CahSCfdgDQ%TLnW=%H;JaL-iK@qtnwJQ#nnaG&-_+{;eE4#PAJZ`%vsav6D}( zYddO@Ea%c`L(j9{!j6Nl2v ztK5M1fSym~vXhytj>BOGz~wah{y@01;U_v&_^sg|ss43P0VnuMGrU948bbJV6BzV< zeouXo?&mw(20K!yf7H(MiE?9OXx)k8O%$X2a$C4_7$DGNWOI3oV%Cecjh2?>X0n>< zwR&V})bHa$a;rRrFw37!4$wY+DjAVyK`~DX^T~69X9%!L%hk2=RL5DG;tx9bU_Tz( zgaYqYQH0*hR{$qxwa#V%eMuNbun78F*$ktanbfy1qy@iP#++EV-MN(>4Fs3`k<%?7 zHJ|Bxzq3^J4E2==h$Ua3B*ODXsHr5OQYQXT$&N@N)iostA}1mjA~*3?mpq8P#Nm4s z#)@@EB*HkoM#oA5dTbMjpqz*vi(d7#U*+F*g)%E^@WiOUL_k AuditLog: + def log(db: Session, + user: User, + action: AuditAction, + resource: AuditResource, + resource_id: Optional[str] = None, + description: Optional[str] = None, + old_value: Optional[dict] = None, + new_value: Optional[dict] = None, + changed_fields: Optional[List[str]] = None, + user_ip: Optional[str] = None, + user_agent: Optional[str] = None, + request_method: Optional[str] = None, + request_path: Optional[str] = None, + success: bool = True, + error_message: Optional[str] = None) -> AuditLog: """ - 写入一条审计日志。任意字段缺失都安全降级(不抛异常), - 避免审计日志写入失败影响主业务流程。 + 记录审计日志 + + Args: + db: 数据库会话 + user: 操作用户 + action: 操作类型 + resource: 资源类型 + resource_id: 资源ID + description: 操作描述 + old_value: 旧值 + new_value: 新值 + changed_fields: 变更的字段列表 + user_ip: 用户IP + user_agent: 用户代理 + request_method: 请求方法 + request_path: 请求路径 + success: 操作是否成功 + error_message: 错误信息 + + Returns: + AuditLog 实例 """ - try: - log = AuditLog( - user_id=user.id if user else None, - username=user.username if user else None, - action=action, - resource_type=resource_type, - resource_id=str(resource_id) if resource_id is not None else None, - resource_name=resource_name, - method=method, - path=path, - ip_address=ip_address, - user_agent=(user_agent or "")[:500], - status=status, - detail=_serialize_detail(detail), - ) - db.add(log) - db.commit() - return log - except Exception as e: - # 写审计日志失败不能影响主业务,仅回滚 - db.rollback() - # 用 print 而非 logger,避免循环依赖(logger 可能未初始化) - print(f"[audit] failed to write log action={action}: {e}") - return None - + audit_log = AuditLog( + user_id=user.id if user else None, + username=user.username if user else None, + real_name=user.real_name if user else None, + action=action, + resource=resource, + resource_id=str(resource_id) if resource_id else None, + description=description, + old_value=json.dumps(old_value, ensure_ascii=False, default=str) if old_value else None, + new_value=json.dumps(new_value, ensure_ascii=False, default=str) if new_value else None, + changed_fields=','.join(changed_fields) if changed_fields else None, + user_ip=user_ip, + user_agent=user_agent, + request_method=request_method, + request_path=request_path, + success=1 if success else 0, + error_message=error_message + ) + + db.add(audit_log) + db.commit() + db.refresh(audit_log) + return audit_log + @staticmethod - def get_logs( - db: Session, - *, - user_id: Optional[int] = None, - username: Optional[str] = None, - action: Optional[str] = None, - resource_type: Optional[str] = None, - status: Optional[str] = None, - start_time: Optional[datetime] = None, - end_time: Optional[datetime] = None, - skip: int = 0, - limit: int = 50, - ) -> Tuple[int, List[AuditLog]]: + def get_logs(db: Session, + user_id: Optional[int] = None, + action: Optional[AuditAction] = None, + resource: Optional[AuditResource] = None, + success: Optional[bool] = None, + start_time: Optional[datetime] = None, + end_time: Optional[datetime] = None, + skip: int = 0, + limit: int = 100) -> tuple[int, List[AuditLog]]: + """ + 查询审计日志 + + Args: + db: 数据库会话 + user_id: 按用户ID过滤 + action: 按操作类型过滤 + resource: 按资源类型过滤 + success: 按操作结果过滤 + start_time: 开始时间 + end_time: 结束时间 + skip: 跳过记录数 + limit: 返回记录数 + + Returns: + (总数, 记录列表) + """ query = db.query(AuditLog) - - if user_id is not None: + + if user_id: query = query.filter(AuditLog.user_id == user_id) - if username: - query = query.filter(AuditLog.username.like(f"%{username}%")) if action: - query = query.filter(AuditLog.action.like(f"%{action}%")) - if resource_type: - query = query.filter(AuditLog.resource_type == resource_type) - if status: - query = query.filter(AuditLog.status == status) + query = query.filter(AuditLog.action == action) + if resource: + query = query.filter(AuditLog.resource == resource) + if success is not None: + query = query.filter(AuditLog.success == (1 if success else 0)) if start_time: query = query.filter(AuditLog.created_at >= start_time) if end_time: query = query.filter(AuditLog.created_at <= end_time) - + total = query.count() - items = query.order_by(desc(AuditLog.created_at)).offset(skip).limit(limit).all() + items = query.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit).all() + return total, items - + @staticmethod - def get_stats(db: Session) -> Dict[str, Any]: - """首页/汇总用:最近 24h 操作数 + 按 action 分组""" - from datetime import timedelta - cutoff = datetime.utcnow() - timedelta(hours=24) - recent = db.query(AuditLog).filter(AuditLog.created_at >= cutoff).count() - by_action = db.query(AuditLog.action, db.query(AuditLog).filter(AuditLog.created_at >= cutoff).subquery()) # 占位避免循环 - # 简化:用 group by - from sqlalchemy import func - rows = db.query(AuditLog.action, func.count(AuditLog.id)).filter( - AuditLog.created_at >= cutoff - ).group_by(AuditLog.action).all() + def get_user_actions(db: Session, user_id: int, limit: int = 50) -> List[AuditLog]: + """获取指定用户的操作日志""" + return db.query(AuditLog)\ + .filter(AuditLog.user_id == user_id)\ + .order_by(AuditLog.created_at.desc())\ + .limit(limit)\ + .all() + + @staticmethod + def get_resource_logs(db: Session, resource: AuditResource, resource_id: str, limit: int = 50) -> List[AuditLog]: + """获取指定资源的操作历史""" + return db.query(AuditLog)\ + .filter(AuditLog.resource == resource, AuditLog.resource_id == str(resource_id))\ + .order_by(AuditLog.created_at.desc())\ + .limit(limit)\ + .all() + + @staticmethod + def get_statistics(db: Session, days: int = 30) -> dict: + """ + 获取审计统计信息 + + Args: + db: 数据库会话 + days: 统计天数 + + Returns: + 统计信息字典 + """ + start_date = datetime.utcnow() - timedelta(days=days) + + # 总操作数 + total_ops = db.query(AuditLog).filter(AuditLog.created_at >= start_date).count() + + # 成功/失败统计 + success_ops = db.query(AuditLog).filter( + AuditLog.created_at >= start_date, + AuditLog.success == 1 + ).count() + failed_ops = total_ops - success_ops + + # 按操作类型统计 + action_stats = {} + for action in AuditAction: + count = db.query(AuditLog).filter( + AuditLog.created_at >= start_date, + AuditLog.action == action + ).count() + if count > 0: + action_stats[action.value] = count + + # 按资源类型统计 + resource_stats = {} + for resource in AuditResource: + count = db.query(AuditLog).filter( + AuditLog.created_at >= start_date, + AuditLog.resource == resource + ).count() + if count > 0: + resource_stats[resource.value] = count + + # 活跃用户数 + active_users = db.query(AuditLog.user_id).filter( + AuditLog.created_at >= start_date + ).distinct().count() + return { - "recent_24h": recent, - "by_action_24h": {action: count for action, count in rows}, + "period_days": days, + "total_operations": total_ops, + "successful_operations": success_ops, + "failed_operations": failed_ops, + "success_rate": round(success_ops / total_ops * 100, 2) if total_ops > 0 else 0, + "by_action": action_stats, + "by_resource": resource_stats, + "active_users_count": active_users } + + @staticmethod + def clean_old_logs(db: Session, days: int = 90) -> int: + """ + 清理指定天数之前的日志 + + Args: + db: 数据库会话 + days: 保留天数 + + Returns: + 删除的记录数 + """ + cutoff_date = datetime.utcnow() - timedelta(days=days) + deleted = db.query(AuditLog).filter(AuditLog.created_at < cutoff_date).delete() + db.commit() + return deleted -def _serialize_detail(detail: Any) -> Optional[str]: - """把 dict/list 等结构化数据序列化成字符串,便于存储和展示""" - if detail is None: - return None - if isinstance(detail, str): - return detail[:4000] - try: - return json.dumps(detail, ensure_ascii=False, default=str)[:4000] - except (TypeError, ValueError): - return str(detail)[:4000] - - -# 常用 action 字符串集中管理(避免散落各处的魔法字符串) -class AuditAction: - # 网络 - NETWORK_CREATE = "network.create" - NETWORK_UPDATE = "network.update" - NETWORK_DELETE = "network.delete" - # IP - IP_UPDATE = "ip.update" - IP_RESERVE = "ip.reserve" - IP_UNRESERVE = "ip.unreserve" - # 扫描 - SCAN_TRIGGER = "scan.trigger" - # SNMP - SNMP_CRED_CREATE = "snmp.credential.create" - SNMP_CRED_UPDATE = "snmp.credential.update" - SNMP_CRED_DELETE = "snmp.credential.delete" - SNMP_DEVICE_CREATE = "snmp.device.create" - SNMP_DEVICE_UPDATE = "snmp.device.update" - SNMP_DEVICE_DELETE = "snmp.device.delete" - # 告警 - ALERT_ACK = "alert.acknowledge" - ALERT_RESOLVE = "alert.resolve" - # 用户管理 - USER_LOGIN = "user.login" - USER_LOGIN_FAILED = "user.login.failed" - USER_LOGOUT = "user.logout" - USER_CREATE = "user.create" - USER_UPDATE_STATUS = "user.update_status" - USER_UNLOCK = "user.unlock" - USER_RESET_PASSWORD = "user.reset_password" - USER_DELETE = "user.delete" - USER_CHANGE_PASSWORD = "user.change_password" \ No newline at end of file +class AuditLogger: + """审计日志装饰器/上下文管理器""" + + def __init__(self, db: Session, user: User, resource: AuditResource, resource_id: Optional[str] = None): + self.db = db + self.user = user + self.resource = resource + self.resource_id = resource_id + self.old_value = None + + def set_old_value(self, value: dict): + """设置旧值(用于更新操作)""" + self.old_value = value + + def log(self, action: AuditAction, description: str, new_value: Optional[dict] = None, changed_fields: Optional[List[str]] = None): + """记录日志""" + AuditService.log( + db=self.db, + user=self.user, + action=action, + resource=self.resource, + resource_id=self.resource_id, + description=description, + old_value=self.old_value, + new_value=new_value, + changed_fields=changed_fields + ) diff --git a/backend/app/services/email_service.py b/backend/app/services/email_service.py new file mode 100644 index 0000000..8dbdcf7 --- /dev/null +++ b/backend/app/services/email_service.py @@ -0,0 +1,239 @@ +from pydantic_settings import BaseSettings +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +import smtplib +from typing import Optional, List +from datetime import datetime +import logging + +logger = logging.getLogger(__name__) + + +class EmailSettings(BaseSettings): + """邮件配置""" + SMTP_HOST: str = "localhost" + SMTP_PORT: int = 25 + SMTP_USER: Optional[str] = None + SMTP_PASSWORD: Optional[str] = None + SMTP_USE_TLS: bool = False + SMTP_USE_SSL: bool = False + SMTP_FROM: str = "ipam@localhost" + + class Config: + env_prefix = "EMAIL_" + + +class EmailService: + """邮件服务""" + + def __init__(self, settings: EmailSettings): + self.settings = settings + + def send_email(self, to: str, subject: str, html_content: str) -> bool: + """ + 发送HTML邮件 + + Args: + to: 收件人邮箱 + subject: 邮件主题 + html_content: HTML内容 + + Returns: + 是否发送成功 + """ + try: + msg = MIMEMultipart('alternative') + msg['From'] = self.settings.SMTP_FROM + msg['To'] = to + msg['Subject'] = subject + + html_part = MIMEText(html_content, 'html', 'utf-8') + msg.attach(html_part) + + # 连接SMTP服务器 + if self.settings.SMTP_USE_SSL: + server = smtplib.SMTP_SSL( + host=self.settings.SMTP_HOST, + port=self.settings.SMTP_PORT, + timeout=10 + ) + else: + server = smtplib.SMTP( + host=self.settings.SMTP_HOST, + port=self.settings.SMTP_PORT, + timeout=10 + ) + + # 如果需要TLS加密 + if self.settings.SMTP_USE_TLS and not self.settings.SMTP_USE_SSL: + server.starttls() + + # 如果需要认证 + if self.settings.SMTP_USER and self.settings.SMTP_PASSWORD: + server.login(self.settings.SMTP_USER, self.settings.SMTP_PASSWORD) + + # 发送邮件 + server.send_message(msg) + server.quit() + + logger.info(f"邮件发送成功: {to} - {subject}") + return True + + except Exception as e: + logger.error(f"邮件发送失败: {str(e)}", exc_info=True) + return False + + def send_alert_notification(self, to: str, alert_data: dict) -> bool: + """发送告警通知邮件""" + severity_colors = { + 'critical': '#dc2626', + 'error': '#ef4444', + 'warning': '#f59e0b', + 'info': '#3b82f6' + } + + severity_texts = { + 'critical': '严重', + 'error': '错误', + 'warning': '警告', + 'info': '信息' + } + + alert_type_texts = { + 'ip_conflict': 'IP 地址冲突', + 'unauthorized_access': '未授权设备接入', + 'subnet_full': '网段容量不足', + 'device_offline': '设备离线', + 'new_device_detected': '新设备发现', + 'mac_changed': 'MAC 地址变更' + } + + severity = alert_data.get('severity', 'warning') + color = severity_colors.get(severity, '#6b7280') + severity_text = severity_texts.get(severity, severity) + alert_type_text = alert_type_texts.get(alert_data.get('alert_type'), alert_data.get('alert_type')) + + html_content = f""" + + + + + + +
+

⚠️ IPAM 告警通知

+

告警级别: {severity_text}

+
+
+
+ 告警类型: {alert_type_text} +
+
+ 告警标题: {alert_data.get('title', '-')} +
+
+ 详细信息: {alert_data.get('message', '-')} +
+
+ 相关IP: {alert_data.get('ip_address_str', '-')} +
+
+ 相关MAC: {alert_data.get('mac_address', '-')} +
+
+ 告警时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +
+
+ + + +""" + + subject = f"【{severity_text}】IPAM 告警 - {alert_data.get('title', '新告警')}" + + return self.send_email(to, subject, html_content) + + def send_scan_report(self, to: str, report_data: dict) -> bool: + """发送扫描报告邮件""" + html_content = f""" + + + + + + +
+

📊 IPAM 网段扫描报告

+
+
+

扫描结果概览

+
+
{report_data.get('total_ips', 0)}
+
扫描IP总数
+
+
+
{report_data.get('online_ips', 0)}
+
在线设备
+
+
+
{report_data.get('with_mac', 0)}
+
发现MAC地址
+
+
+
{report_data.get('new_devices', 0)}
+
新发现设备
+
+ +
+

扫描网段: {report_data.get('network_cidr', '-')}

+

扫描时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

+
+
+ + + +""" + + subject = f"IPAM 扫描报告 - {report_data.get('network_cidr', '')}" + + return self.send_email(to, subject, html_content) + + +# 全局邮件服务实例 +_email_service = None + +def get_email_service() -> EmailService: + """获取邮件服务实例""" + global _email_service + if _email_service is None: + settings = EmailSettings() + _email_service = EmailService(settings) + return _email_service + + +def init_email_service(settings: EmailSettings): + """初始化邮件服务""" + global _email_service + _email_service = EmailService(settings) diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py new file mode 100644 index 0000000..efeee1b --- /dev/null +++ b/backend/app/services/report_service.py @@ -0,0 +1,277 @@ +from sqlalchemy.orm import Session +from typing import Optional, List, Dict, Any +from io import BytesIO, StringIO +import csv +from datetime import datetime + +from app.models.network import IPAddress, Network, IPStatus +from app.models.snmp import NetworkDevice, ARPEntry +from app.models.alert import Alert + + +class ReportExportService: + """报表导出服务""" + + # ========== CSV 导出 ========== + + @staticmethod + def export_ip_addresses_csv(db: Session, + network_id: Optional[int] = None, + status: Optional[IPStatus] = None, + include_online_only: bool = False) -> tuple[str, str]: + """ + 导出IP地址表为CSV + + Args: + db: 数据库会话 + network_id: 网段ID(可选,不指定则导出所有) + status: IP状态过滤 + include_online_only: 仅导出在线IP + + Returns: + (CSV内容字符串, 文件名) + """ + query = db.query(IPAddress) + + if network_id: + query = query.filter(IPAddress.network_id == network_id) + if status: + query = query.filter(IPAddress.status == status) + if include_online_only: + query = query.filter(IPAddress.status == IPStatus.ONLINE) + + ips = query.order_by(IPAddress.network_id, IPAddress.id).all() + + output = StringIO() + writer = csv.writer(output) + + # 写入表头 + writer.writerow([ + 'ID', '网段ID', 'IP地址', 'MAC地址', '主机名', '状态', + '使用者', '业务类型', '备注', '厂商', + '最后发现时间', '创建时间' + ]) + + # 写入数据 + for ip in ips: + writer.writerow([ + ip.id, + ip.network_id, + ip.ip_address, + ip.mac_address or '', + ip.hostname or '', + ip.status.value, + ip.owner or '', + ip.business_type or '', + ip.notes or '', + ip.vendor or '', + ip.last_seen.strftime('%Y-%m-%d %H:%M:%S') if ip.last_seen else '', + ip.created_at.strftime('%Y-%m-%d %H:%M:%S') if ip.created_at else '' + ]) + + csv_content = output.getvalue() + filename = f"ip_addresses_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + return csv_content, filename + + @staticmethod + def export_networks_csv(db: Session) -> tuple[str, str]: + """导出网段汇总CSV""" + networks = db.query(Network).order_by(Network.id).all() + + output = StringIO() + writer = csv.writer(output) + + writer.writerow([ + 'ID', '网段CIDR', '名称', '描述', '分组', + '网关', 'VLAN', '总IP数', '已用IP数', '保留IP数', + '创建时间' + ]) + + for net in networks: + writer.writerow([ + net.id, + net.cidr, + net.name or '', + net.description or '', + net.group_name or '', + net.gateway or '', + net.vlan_id or '', + net.total_ips, + net.used_ips, + net.reserved_ips, + net.created_at.strftime('%Y-%m-%d %H:%M:%S') if net.created_at else '' + ]) + + csv_content = output.getvalue() + filename = f"networks_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + return csv_content, filename + + @staticmethod + def export_alerts_csv(db: Session, status: Optional[str] = None) -> tuple[str, str]: + """导出告警CSV""" + query = db.query(Alert) + if status: + query = query.filter(Alert.status == status) + + alerts = query.order_by(Alert.created_at.desc()).all() + + output = StringIO() + writer = csv.writer(output) + + writer.writerow([ + 'ID', '告警类型', '严重程度', '状态', '标题', + '详细信息', '相关IP', '相关MAC', '创建时间' + ]) + + for alert in alerts: + writer.writerow([ + alert.id, + alert.alert_type.value, + alert.severity.value, + alert.status.value, + alert.title, + alert.message or '', + alert.ip_address_str or '', + alert.mac_address or '', + alert.created_at.strftime('%Y-%m-%d %H:%M:%S') if alert.created_at else '' + ]) + + csv_content = output.getvalue() + filename = f"alerts_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + return csv_content, filename + + @staticmethod + def export_snmp_devices_csv(db: Session) -> tuple[str, str]: + """导出SNMP设备CSV""" + devices = db.query(NetworkDevice).order_by(NetworkDevice.id).all() + + output = StringIO() + writer = csv.writer(output) + + writer.writerow([ + 'ID', '设备名称', 'IP地址', '端口', '设备类型', + '厂商', '型号', '位置', '最后轮询时间', '创建时间' + ]) + + for dev in devices: + writer.writerow([ + dev.id, + dev.name, + dev.ip_address, + dev.port, + dev.device_type.value, + dev.vendor or '', + dev.model or '', + dev.location or '', + dev.last_polled_at.strftime('%Y-%m-%d %H:%M:%S') if dev.last_polled_at else '', + dev.created_at.strftime('%Y-%m-%d %H:%M:%S') if dev.created_at else '' + ]) + + csv_content = output.getvalue() + filename = f"snmp_devices_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + return csv_content, filename + + @staticmethod + def export_arp_table_csv(db: Session, device_id: Optional[int] = None) -> tuple[str, str]: + """导出ARP表CSV""" + query = db.query(ARPEntry) + if device_id: + query = query.filter(ARPEntry.device_id == device_id) + + entries = query.order_by(ARPEntry.device_id, ARPEntry.id).all() + + output = StringIO() + writer = csv.writer(output) + + writer.writerow([ + 'ID', '设备ID', 'IP地址', 'MAC地址', '接口', '最后发现时间' + ]) + + for entry in entries: + writer.writerow([ + entry.id, + entry.device_id, + entry.ip_address, + entry.mac_address, + entry.interface or '', + entry.last_seen.strftime('%Y-%m-%d %H:%M:%S') if entry.last_seen else '' + ]) + + csv_content = output.getvalue() + filename = f"arp_table_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + + return csv_content, filename + + # ========== 汇总统计报表 ========== + + @staticmethod + def get_ipam_summary(db: Session) -> Dict[str, Any]: + """获取IPAM系统汇总报表""" + # 网段统计 + total_networks = db.query(Network).count() + total_ips = db.query(IPAddress).count() + online_ips = db.query(IPAddress).filter(IPAddress.status == IPStatus.ONLINE).count() + reserved_ips = db.query(IPAddress).filter(IPAddress.status == IPStatus.RESERVED).count() + + # 带MAC/主机名的IP统计 + with_mac = db.query(IPAddress).filter(IPAddress.mac_address.isnot(None)).count() + with_hostname = db.query(IPAddress).filter(IPAddress.hostname.isnot(None)).count() + + # 网段使用率分布 + networks = db.query(Network).all() + usage_distribution = { + 'under_50': 0, # 50%以下 + '50_70': 0, # 50%-70% + '70_90': 0, # 70%-90% + 'over_90': 0 # 90%以上 + } + + for net in networks: + usage_rate = (net.used_ips / net.total_ips * 100) if net.total_ips > 0 else 0 + if usage_rate < 50: + usage_distribution['under_50'] += 1 + elif usage_rate < 70: + usage_distribution['50_70'] += 1 + elif usage_rate < 90: + usage_distribution['70_90'] += 1 + else: + usage_distribution['over_90'] += 1 + + # 告警统计 + from app.models.alert import Alert, AlertStatus + active_alerts = db.query(Alert).filter(Alert.status == AlertStatus.ACTIVE).count() + resolved_alerts = db.query(Alert).filter(Alert.status == AlertStatus.RESOLVED).count() + + # SNMP设备统计 + snmp_devices = db.query(NetworkDevice).count() + active_devices = db.query(NetworkDevice).filter(NetworkDevice.last_polled_at.isnot(None)).count() + + return { + "summary": { + "total_networks": total_networks, + "total_ip_addresses": total_ips, + "online_ips": online_ips, + "reserved_ips": reserved_ips, + "available_ips": total_ips - online_ips - reserved_ips, + "online_rate": round(online_ips / total_ips * 100, 2) if total_ips > 0 else 0 + }, + "discovery": { + "with_mac_address": with_mac, + "with_hostname": with_hostname, + "mac_discovery_rate": round(with_mac / total_ips * 100, 2) if total_ips > 0 else 0 + }, + "network_usage_distribution": usage_distribution, + "alerts": { + "active": active_alerts, + "resolved": resolved_alerts + }, + "snmp": { + "total_devices": snmp_devices, + "active_devices": active_devices + }, + "generated_at": datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } diff --git a/backend/app/tasks/__pycache__/scan_tasks.cpython-311.pyc b/backend/app/tasks/__pycache__/scan_tasks.cpython-311.pyc index b636b7c178e73a8923dc710020bb7358cc45312f..bdb41cd870f2eeb9726f780c67ab84c9fce5367b 100644 GIT binary patch delta 2546 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 HXm9&3DJvS`