readme
This commit is contained in:
@@ -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)
|
||||
|
||||
Binary file not shown.
+128
-127
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user