diff --git a/backend/app/__pycache__/main.cpython-311.pyc b/backend/app/__pycache__/main.cpython-311.pyc index b9e9e61..a24bf96 100644 Binary files a/backend/app/__pycache__/main.cpython-311.pyc and b/backend/app/__pycache__/main.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/__init__.cpython-311.pyc b/backend/app/api/v1/__pycache__/__init__.cpython-311.pyc index b5a5d9f..c27d59f 100644 Binary files a/backend/app/api/v1/__pycache__/__init__.cpython-311.pyc and b/backend/app/api/v1/__pycache__/__init__.cpython-311.pyc differ diff --git a/backend/app/api/v1/__pycache__/snmp.cpython-311.pyc b/backend/app/api/v1/__pycache__/snmp.cpython-311.pyc index 343b624..74542e3 100644 Binary files a/backend/app/api/v1/__pycache__/snmp.cpython-311.pyc and b/backend/app/api/v1/__pycache__/snmp.cpython-311.pyc differ diff --git a/backend/app/api/v1/snmp.py b/backend/app/api/v1/snmp.py index d84d22a..07a484a 100644 --- a/backend/app/api/v1/snmp.py +++ b/backend/app/api/v1/snmp.py @@ -285,7 +285,7 @@ def get_network_device(device_id: int, db: Session = Depends(get_db)): def create_network_device( name: str, ip_address: str, - credential_id: Optional[int] = None, + snmp_credential_id: Optional[int] = None, # 前端字段名(与 ORM 列名一致) port: int = 161, device_type: str = "other", description: Optional[str] = None, @@ -305,7 +305,7 @@ def create_network_device( ip_address=ip_address, port=port, device_type=device_type, - snmp_credential_id=credential_id, + snmp_credential_id=snmp_credential_id, description=description, location=location, is_active=True @@ -337,7 +337,7 @@ def update_network_device( device_id: int, name: Optional[str] = None, ip_address: Optional[str] = None, - credential_id: Optional[int] = None, + snmp_credential_id: Optional[int] = None, port: Optional[int] = None, device_type: Optional[str] = None, description: Optional[str] = None, @@ -363,8 +363,8 @@ def update_network_device( device.name = name if ip_address: device.ip_address = ip_address - if credential_id is not None: - device.snmp_credential_id = credential_id + if snmp_credential_id is not None: + device.snmp_credential_id = snmp_credential_id if port: device.port = port if device_type: diff --git a/backend/app/models/audit.py b/backend/app/models/audit.py index f25dade..95d90eb 100644 --- a/backend/app/models/audit.py +++ b/backend/app/models/audit.py @@ -17,6 +17,33 @@ class AuditAction(str, enum.Enum): EXPORT = "export" OTHER = "other" + # ===== 兼容层:老 AuditAction 字符串字面量别名,值与新枚举对应 ===== + # 旧代码(以及当前 AuditService._ACTION_MAP)里硬编码引用这些枚举属性名 + NETWORK_CREATE = "create" + NETWORK_UPDATE = "update" + NETWORK_DELETE = "delete" + IP_UPDATE = "update" + IP_RESERVE = "update" + IP_UNRESERVE = "update" + SCAN_TRIGGER = "scan" + SNMP_CRED_CREATE = "create" + SNMP_CRED_UPDATE = "update" + SNMP_CRED_DELETE = "delete" + SNMP_DEVICE_CREATE = "create" + SNMP_DEVICE_UPDATE = "update" + SNMP_DEVICE_DELETE = "delete" + ALERT_ACK = "update" + ALERT_RESOLVE = "update" + USER_LOGIN = "login" + USER_LOGIN_FAILED = "login" + USER_LOGOUT = "logout" + USER_CREATE = "create" + USER_UPDATE_STATUS = "update" + USER_UNLOCK = "update" + USER_RESET_PASSWORD = "update" + USER_DELETE = "delete" + USER_CHANGE_PASSWORD = "update" + class AuditResource(str, enum.Enum): """资源类型枚举""" diff --git a/backend/app/services/audit_service.py b/backend/app/services/audit_service.py index 1537c4e..a77d6f2 100644 --- a/backend/app/services/audit_service.py +++ b/backend/app/services/audit_service.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional, List, Any from sqlalchemy.orm import Session from datetime import datetime, timedelta import json @@ -7,9 +7,134 @@ from app.models.audit import AuditLog, AuditAction, AuditResource from app.models.auth import User +# ===== 兼容层:老 API(字符串 action/resource)映射到新枚举 ===== + +# 字符串 action -> 枚举 + 资源类型;用于兼容各 endpoint 里硬编码的 "network.create" 等 +_ACTION_MAP = { + # network + "network.create": (AuditAction.CREATE, AuditResource.NETWORK), + "network.update": (AuditAction.UPDATE, AuditResource.NETWORK), + "network.delete": (AuditAction.DELETE, AuditResource.NETWORK), + # ip + "ip.update": (AuditAction.UPDATE, AuditResource.IP_ADDRESS), + "ip.reserve": (AuditAction.UPDATE, AuditResource.IP_ADDRESS), + "ip.unreserve": (AuditAction.UPDATE, AuditResource.IP_ADDRESS), + # scan + "scan.trigger": (AuditAction.SCAN, AuditResource.SCAN_TASK), + # snmp + "snmp.credential.create": (AuditAction.CREATE, AuditResource.SNMP_CREDENTIAL), + "snmp.credential.update": (AuditAction.UPDATE, AuditResource.SNMP_CREDENTIAL), + "snmp.credential.delete": (AuditAction.DELETE, AuditResource.SNMP_CREDENTIAL), + "snmp.device.create": (AuditAction.CREATE, AuditResource.SNMP_DEVICE), + "snmp.device.update": (AuditAction.UPDATE, AuditResource.SNMP_DEVICE), + "snmp.device.delete": (AuditAction.DELETE, AuditResource.SNMP_DEVICE), + # alert + "alert.acknowledge": (AuditAction.UPDATE, AuditResource.ALERT), + "alert.resolve": (AuditAction.UPDATE, AuditResource.ALERT), + # user + "user.login": (AuditAction.LOGIN, AuditResource.USER), + "user.login.failed": (AuditAction.LOGIN, AuditResource.USER), + "user.logout": (AuditAction.LOGOUT, AuditResource.USER), + "user.create": (AuditAction.CREATE, AuditResource.USER), + "user.update_status": (AuditAction.UPDATE, AuditResource.USER), + "user.unlock": (AuditAction.UPDATE, AuditResource.USER), + "user.reset_password": (AuditAction.UPDATE, AuditResource.USER), + "user.delete": (AuditAction.DELETE, AuditResource.USER), + "user.change_password": (AuditAction.UPDATE, AuditResource.USER), +} + +_RESOURCE_MAP = { + "network": AuditResource.NETWORK, + "ip": AuditResource.IP_ADDRESS, + "snmp_credential": AuditResource.SNMP_CREDENTIAL, + "snmp_device": AuditResource.SNMP_DEVICE, + "user": AuditResource.USER, + "alert": AuditResource.ALERT, + "scan": AuditResource.SCAN_TASK, +} + + class AuditService: """审计日志服务""" - + + @staticmethod + def record( + db: Session, + *, + action: str, + user=None, + resource_type: Optional[str] = None, + resource_id: Optional[Any] = None, + resource_name: Optional[str] = None, + method: Optional[str] = None, + path: Optional[str] = None, + ip_address: Optional[str] = None, + user_agent: Optional[str] = None, + status: str = "success", + detail: Optional[Any] = None, + ) -> Optional["AuditLog"]: + """ + 兼容层:将老版 API(字符串 action/resource)映射到新版 log()。 + 所有 endpoint 都通过本方法写审计,避免直接调用新签名造成 500。 + 任意环节失败都安全降级(不抛异常),审计写入失败不影响主业务。 + """ + try: + # action 字符串 -> (枚举, 资源枚举) ;未知 action 用 OTHER + SYSTEM + enum_action, default_resource = _ACTION_MAP.get( + action, (AuditAction.OTHER, AuditResource.SYSTEM) + ) + resource_enum = _RESOURCE_MAP.get(resource_type, default_resource) + + # 详情拆分:dict 拆出 old/new;其余当 description + old_value = None + new_value = None + description = None + error_message = None + success = status != "failed" + + if isinstance(detail, dict): + old_value = detail.get("before") + new_value = detail.get("after") + # 把 action 原始字符串 + 资源名放到 description 里,保留细节 + desc_parts = [action] + if resource_name: + desc_parts.append(resource_name) + if status == "failed": + error_message = detail.get("error") or detail.get("message") or json.dumps(detail, ensure_ascii=False, default=str) + description = " | ".join(desc_parts) + else: + description = f"{action} | {resource_name or ''}" + if status == "failed" and detail: + error_message = str(detail) + + # user 不能 None 传给 log();fallback 到 User(id=None, username='anonymous') + # 但 log() 会用 user.id / user.username / user.real_name,如果都是 None 就 OK + if user is None: + anon = User(id=None, username="anonymous", real_name=None) + else: + anon = user + + return AuditService.log( + db=db, + user=anon, + action=enum_action, + resource=resource_enum, + resource_id=str(resource_id) if resource_id is not None else None, + description=description[:500] if description else None, + old_value=old_value, + new_value=new_value, + user_ip=ip_address, + user_agent=(user_agent or "")[:500], + request_method=method, + request_path=path, + success=success, + error_message=error_message, + ) + except Exception as e: + db.rollback() + print(f"[audit] record() failed action={action}: {e}") + return None + @staticmethod def log(db: Session, user: User,