This commit is contained in:
Your Name
2026-07-20 13:40:05 +08:00
parent 9e34f6f0f9
commit bca0da7c7d
12 changed files with 1609 additions and 525 deletions
+2 -1
View File
@@ -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)
+128 -127
View File
@@ -1,164 +1,165 @@
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime
import csv
import io
from app.core.database import get_db
from app.core.security import get_current_user
from app.core.security import get_current_user, require_permission
from app.models.auth import User
from app.models.audit import AuditAction, AuditResource
from app.services.audit_service import AuditService
router = APIRouter(
prefix="/audit",
tags=["审计日志"],
dependencies=[Depends(get_current_user)],
)
router = APIRouter(prefix="/audit", tags=["审计日志"])
@router.get("/logs", summary="查询审计日志")
@router.get("/logs", summary="获取审计日志列表")
def get_audit_logs(
user_id: Optional[int] = None,
username: Optional[str] = None,
action: Optional[str] = None,
resource_type: Optional[str] = None,
status: Optional[str] = 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 = Query(0, ge=0),
limit: int = Query(50, ge=1, le=500),
# 当前用户必须是 admin 或 super_admin 才能看审计
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
limit: int = Query(100, ge=1, le=500),
current_user: User = Depends(require_permission("audit:view")),
db: Session = Depends(get_db)
):
# 角色检查:super_admin / admin 可以看所有人的,operator/viewer 只能看自己的
if current_user.role not in ("super_admin", "admin"):
user_id = current_user.id
"""
获取审计日志列表(需要管理员权限)
- **user_id**: 按用户ID过滤
- **action**: 按操作类型过滤 (create/read/update/delete/login/logout/scan/export)
- **resource**: 按资源类型过滤
- **success**: 按操作结果过滤
- **start_time**: 开始时间
- **end_time**: 结束时间
"""
total, items = AuditService.get_logs(
db,
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
status=status,
resource=resource,
success=success,
start_time=start_time,
end_time=end_time,
skip=skip,
limit=limit,
limit=limit
)
serialized = []
for log in items:
serialized.append({
"id": log.id,
"user_id": log.user_id,
"username": log.username,
"action": log.action,
"resource_type": log.resource_type,
"resource_id": log.resource_id,
"resource_name": log.resource_name,
"method": log.method,
"path": log.path,
"ip_address": log.ip_address,
"user_agent": log.user_agent,
"status": log.status,
"detail": log.detail,
"created_at": log.created_at.isoformat() if log.created_at else None,
# 格式化返回
result_items = []
for item in items:
changed = item.changed_fields.split(',') if item.changed_fields else []
result_items.append({
"id": item.id,
"user_id": item.user_id,
"username": item.username,
"real_name": item.real_name,
"user_ip": item.user_ip,
"action": item.action.value,
"resource": item.resource.value,
"resource_id": item.resource_id,
"description": item.description,
"changed_fields": changed,
"request_method": item.request_method,
"request_path": item.request_path,
"success": bool(item.success),
"error_message": item.error_message,
"created_at": item.created_at
})
return {"total": total, "items": serialized}
return {"total": total, "items": result_items}
@router.get("/logs/export", summary="导出审计日志为 CSV")
def export_audit_logs(
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,
@router.get("/logs/my", summary="获取当前用户的操作日志")
def get_my_logs(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
db: Session = Depends(get_db)
):
"""导出 CSV 流。admin/super_admin 看全部;operator/viewer 仅能导出自己的。"""
if current_user.role not in ("super_admin", "admin"):
user_id = current_user.id
# 导出上限 50000 行,避免一次拉太多 OOM
"""获取当前登录用户的操作日志"""
total, items = AuditService.get_logs(
db,
user_id=user_id,
username=username,
action=action,
resource_type=resource_type,
status=status,
start_time=start_time,
end_time=end_time,
skip=0,
limit=50000,
)
output = io.StringIO()
# 写入 UTF-8 BOM 让 Excel 直接打开不乱码
output.write("\ufeff")
writer = csv.writer(output)
writer.writerow([
"ID", "时间", "用户", "操作", "资源类型", "资源ID", "资源名称",
"HTTP方法", "路径", "客户端IP", "状态", "详情"
])
for log in items:
writer.writerow([
log.id,
log.created_at.isoformat() if log.created_at else "",
log.username or "",
log.action or "",
log.resource_type or "",
log.resource_id or "",
log.resource_name or "",
log.method or "",
log.path or "",
log.ip_address or "",
log.status or "",
(log.detail or "").replace("\n", " ")[:500],
])
output.seek(0)
filename = f"audit_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
return StreamingResponse(
iter([output.getvalue()]),
media_type="text/csv; charset=utf-8",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
user_id=current_user.id,
skip=skip,
limit=limit
)
result_items = []
for item in items:
result_items.append({
"id": item.id,
"action": item.action.value,
"resource": item.resource.value,
"resource_id": item.resource_id,
"description": item.description,
"success": bool(item.success),
"created_at": item.created_at
})
return {"total": total, "items": result_items}
@router.get("/stats", summary="审计日志统计")
def get_audit_stats(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
@router.get("/logs/resource/{resource_type}/{resource_id}", summary="获取资源操作历史")
def get_resource_logs(
resource_type: str,
resource_id: str,
limit: int = Query(50, ge=1, le=200),
current_user: User = Depends(require_permission("audit:view")),
db: Session = Depends(get_db)
):
if current_user.role not in ("super_admin", "admin"):
# operator/viewer 只能看自己的简单计数
from app.models.audit import AuditLog
from sqlalchemy import func
my_count = db.query(func.count(AuditLog.id)).filter(AuditLog.user_id == current_user.id).scalar()
return {"recent_24h": 0, "by_action_24h": {}, "my_total": int(my_count or 0)}
return AuditService.get_stats(db)
"""获取指定资源的操作历史"""
try:
resource_enum = AuditResource(resource_type)
except ValueError:
return {"total": 0, "items": [], "error": "无效的资源类型"}
items = AuditService.get_resource_logs(db, resource_enum, resource_id, limit=limit)
result_items = []
for item in items:
changed = item.changed_fields.split(',') if item.changed_fields else []
result_items.append({
"id": item.id,
"username": item.username,
"real_name": item.real_name,
"action": item.action.value,
"description": item.description,
"changed_fields": changed,
"success": bool(item.success),
"created_at": item.created_at
})
return {"total": len(items), "items": result_items}
@router.get("/actions", summary="获取所有审计操作类型(用于前端过滤下拉框)")
def get_action_types():
"""硬编码返回常用 action 列表,避免每次都查 DB"""
from app.services.audit_service import AuditAction
actions = []
for name in dir(AuditAction):
if name.startswith("_") or name.isupper() and not name.startswith("__"):
continue
val = getattr(AuditAction, name, None)
if isinstance(val, str):
actions.append({"code": val, "name": name})
return actions
@router.get("/statistics", summary="获取审计统计信息")
def get_audit_statistics(
days: int = Query(30, ge=1, le=365),
current_user: User = Depends(require_permission("audit:view")),
db: Session = Depends(get_db)
):
"""获取审计统计信息"""
stats = AuditService.get_statistics(db, days=days)
return stats
@router.delete("/clean", summary="清理旧日志")
def clean_old_logs(
days: int = Query(90, ge=7, le=3650, description="保留天数"),
current_user: User = Depends(require_permission("system:admin")),
db: Session = Depends(get_db)
):
"""
清理指定天数之前的审计日志(需要超级管理员权限)
- **days**: 保留日志的天数,默认90天
"""
deleted_count = AuditService.clean_old_logs(db, days=days)
return {
"message": f"清理完成,已删除 {deleted_count} 条旧日志",
"deleted_count": deleted_count,
"kept_days": days
}
+115
View File
@@ -0,0 +1,115 @@
from fastapi import APIRouter, Depends, Query, Response
from sqlalchemy.orm import Session
from typing import Optional
from app.core.database import get_db
from app.core.security import require_permission
from app.models.network import IPStatus
from app.services.report_service import ReportExportService
router = APIRouter(prefix="/reports", tags=["报表导出"])
@router.get("/ip-addresses/csv", summary="导出IP地址CSV")
def export_ip_addresses_csv(
network_id: Optional[int] = None,
status: Optional[IPStatus] = None,
online_only: bool = Query(False, description="仅导出在线IP"),
db: Session = Depends(get_db),
current_user = Depends(require_permission("report:export"))
):
"""导出IP地址表CSV"""
csv_content, filename = ReportExportService.export_ip_addresses_csv(
db,
network_id=network_id,
status=status,
include_online_only=online_only
)
return Response(
content=csv_content,
media_type="text/csv; charset=utf-8-sig",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"
}
)
@router.get("/networks/csv", summary="导出网段汇总CSV")
def export_networks_csv(
db: Session = Depends(get_db),
current_user = Depends(require_permission("report:export"))
):
"""导出网段汇总CSV"""
csv_content, filename = ReportExportService.export_networks_csv(db)
return Response(
content=csv_content,
media_type="text/csv; charset=utf-8-sig",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"
}
)
@router.get("/alerts/csv", summary="导出告警CSV")
def export_alerts_csv(
status: Optional[str] = None,
db: Session = Depends(get_db),
current_user = Depends(require_permission("report:export"))
):
"""导出告警CSV"""
csv_content, filename = ReportExportService.export_alerts_csv(db, status=status)
return Response(
content=csv_content,
media_type="text/csv; charset=utf-8-sig",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"
}
)
@router.get("/snmp-devices/csv", summary="导出SNMP设备CSV")
def export_snmp_devices_csv(
db: Session = Depends(get_db),
current_user = Depends(require_permission("report:export"))
):
"""导出SNMP设备CSV"""
csv_content, filename = ReportExportService.export_snmp_devices_csv(db)
return Response(
content=csv_content,
media_type="text/csv; charset=utf-8-sig",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"
}
)
@router.get("/arp-table/csv", summary="导出ARP表CSV")
def export_arp_table_csv(
device_id: Optional[int] = None,
db: Session = Depends(get_db),
current_user = Depends(require_permission("report:export"))
):
"""导出ARP表CSV"""
csv_content, filename = ReportExportService.export_arp_table_csv(db, device_id=device_id)
return Response(
content=csv_content,
media_type="text/csv; charset=utf-8-sig",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{filename}"
}
)
@router.get("/summary", summary="获取IPAM系统汇总报表")
def get_ipam_summary_report(
db: Session = Depends(get_db),
current_user = Depends(require_permission("report:view"))
):
"""获取IPAM系统汇总统计报表"""
summary = ReportExportService.get_ipam_summary(db)
return summary