chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 火绒终端安全集成模块包
|
||||
# =============================================================================
|
||||
@@ -0,0 +1,658 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 火绒终端安全 API 客户端
|
||||
# =============================================================================
|
||||
# 说明:封装火绒API的签名、请求、响应处理
|
||||
# 核心功能:
|
||||
# 1. HRESS 签名实现(Authorization Header 方式)
|
||||
# 2. 统一请求封装(超时、重试、异常处理)
|
||||
# 3. P0 接口:终端列表 _list / 终端详情 _info2 / 高危漏洞 _leak / 病毒事件 _virus_events
|
||||
# 4. P1 接口:终端隔离/解除 _create(netctrl) / 快速扫描 / 在线终端查询
|
||||
# 签名算法(来自火绒官方API文档 v1):
|
||||
# Authorization = "HRESS" + AccessKeyId + ":" + Expires + ":" + Signature
|
||||
# Signature = urlencode(base64(hmac-sha1(AccessKeySecret,
|
||||
# AccessKeyId + "\n" + Expires + "\n" + HTTP-METHOD + "\n"
|
||||
# + Content-MD5 + "\n" + CanonicalizedResource)))
|
||||
# CanonicalizedResource = API路径(无前导/),含排序后的查询参数
|
||||
# Content-MD5 = base64(md5_digest(body_bytes)) — RFC2616
|
||||
# 使用方式:
|
||||
# client = HuorongClient(access_key_id="...", access_key_secret="...", base_url="...")
|
||||
# terminals = await client.list_terminals()
|
||||
# =============================================================================
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
import httpx
|
||||
|
||||
from .exceptions import (
|
||||
HuorongApiError,
|
||||
HuorongAuthError,
|
||||
HuorongConnectionError,
|
||||
HuorongError,
|
||||
)
|
||||
from .models import (
|
||||
HuorongApiResponse,
|
||||
TerminalBasicInfo,
|
||||
TerminalDetailV2,
|
||||
TerminalLeakInfo,
|
||||
VirusEventStats,
|
||||
VirusHandleResult,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 默认请求超时(秒)— 火绒内网响应通常在1秒内,3秒足够兜底
|
||||
# 注意:_virus_events 查询全部终端时可能较慢,需要更长超时
|
||||
DEFAULT_TIMEOUT = 10.0
|
||||
|
||||
# 默认分页大小
|
||||
DEFAULT_PAGE_SIZE = 20
|
||||
|
||||
# 签名有效期(秒)— 请求签名中的 Expires 字段
|
||||
SIGN_EXPIRES_SECONDS = 300
|
||||
|
||||
|
||||
class HuorongClient:
|
||||
"""火绒终端安全 API 客户端。
|
||||
|
||||
封装了火绒API的签名认证、请求发送和响应解析。
|
||||
所有方法均为异步(async),使用 httpx.AsyncClient 发送请求。
|
||||
|
||||
签名方式:HRESS Authorization Header
|
||||
参考:火绒终端安全管理系统API说明文档 v1
|
||||
|
||||
Attributes:
|
||||
access_key_id: 火绒 AccessKey ID(控制中心显示为 Secret ID)
|
||||
access_key_secret: 火绒 AccessKey Secret(控制中心显示为 Secret Key)
|
||||
base_url: 火绒API内网地址(如 http://huorong.oa.servyou-it.com:8080)
|
||||
timeout: 请求超时秒数
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
access_key_id: str,
|
||||
access_key_secret: str,
|
||||
base_url: str,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
):
|
||||
"""初始化火绒API客户端。
|
||||
|
||||
Args:
|
||||
access_key_id: 火绒 AccessKey ID(控制中心显示为 Secret ID)
|
||||
access_key_secret: 火绒 AccessKey Secret(控制中心显示为 Secret Key)
|
||||
base_url: 火绒API内网地址(不含尾部斜杠)
|
||||
timeout: 请求超时秒数,默认3秒
|
||||
"""
|
||||
self.access_key_id = access_key_id
|
||||
self.access_key_secret = access_key_secret
|
||||
# 确保 base_url 不以 / 结尾,拼接路径时统一加 /
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout
|
||||
|
||||
# ======================================================================
|
||||
# 签名实现(HRESS Authorization Header 方式)
|
||||
# ======================================================================
|
||||
|
||||
def _compute_content_md5(self, body_bytes: bytes) -> str:
|
||||
"""计算请求体的 Content-MD5(RFC2616)。
|
||||
|
||||
算法步骤:
|
||||
1. 计算请求体的 MD5 二进制摘要(128位)
|
||||
2. 对二进制摘要进行 base64 编码
|
||||
|
||||
注意:不是对32位十六进制字符串编码,而是对原始二进制摘要编码。
|
||||
|
||||
Args:
|
||||
body_bytes: 请求体的字节内容
|
||||
|
||||
Returns:
|
||||
str: base64 编码的 MD5 摘要
|
||||
"""
|
||||
md5_digest = hashlib.md5(body_bytes).digest()
|
||||
return base64.b64encode(md5_digest).decode("utf-8")
|
||||
|
||||
def _build_canonicalized_resource(self, path: str) -> str:
|
||||
"""构建 CanonicalizedResource。
|
||||
|
||||
根据火绒API文档:
|
||||
1. 将 CanonicalizedResource 置为空字符串
|
||||
2. 设置要访问的资源路径(去掉前导 /),如 "api/clnts/_list"
|
||||
3. 如果请求包含子资源(查询参数),按字典序排列,
|
||||
以 & 为分隔符生成子资源字符串,末尾添加 ? 和子资源字符串
|
||||
|
||||
示例:
|
||||
- /api/clnts/_list → "api/clnts/_list"
|
||||
- /api/group/_info?group_id=1 → "api/group/_info?group_id=1"
|
||||
|
||||
Args:
|
||||
path: 请求路径(如 /api/clnts/_list)
|
||||
|
||||
Returns:
|
||||
str: CanonicalizedResource 字符串
|
||||
"""
|
||||
# 去掉前导 /
|
||||
return path.lstrip("/")
|
||||
|
||||
def _sign_request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
body_bytes: bytes = b"",
|
||||
) -> Dict[str, str]:
|
||||
"""生成火绒API请求签名(HRESS Authorization Header 方式)。
|
||||
|
||||
签名算法(来自火绒官方API文档):
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Authorization = "HRESS" + AccessKeyId + ":" + Expires + ":" + Signature │
|
||||
│ │
|
||||
│ Signature = urlencode(base64(hmac-sha1(AccessKeySecret, │
|
||||
│ AccessKeyId + "\\n" │
|
||||
│ + Expires + "\\n" │
|
||||
│ + HTTP-METHOD + "\\n" │
|
||||
│ + Content-MD5 + "\\n" │
|
||||
│ + CanonicalizedResource))) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
|
||||
其中:
|
||||
- AccessKeyId: 标识用户身份
|
||||
- Expires: Unix 时间戳,签名过期时间(当前时间 + 300秒)
|
||||
- HTTP-METHOD: POST(火绒API统一使用 POST)
|
||||
- Content-MD5: 请求体的 RFC2616 MD5(base64编码)
|
||||
- CanonicalizedResource: API资源路径(去掉前导/)
|
||||
|
||||
Args:
|
||||
method: HTTP方法(POST)
|
||||
path: 请求路径(如 /api/clnts/_list)
|
||||
body_bytes: 请求体字节内容
|
||||
|
||||
Returns:
|
||||
Dict[str, str]: 包含 Authorization 和 Content-Type 的 Header 字典
|
||||
"""
|
||||
# 1. 计算过期时间(Unix时间戳)
|
||||
expires = str(int(time.time()) + SIGN_EXPIRES_SECONDS)
|
||||
|
||||
# 2. 计算 Content-MD5(RFC2616: MD5 二进制摘要 → base64)
|
||||
content_md5 = self._compute_content_md5(body_bytes) if body_bytes else ""
|
||||
|
||||
# 3. 构建 CanonicalizedResource(去掉前导 /)
|
||||
canonicalized_resource = self._build_canonicalized_resource(path)
|
||||
|
||||
# 4. 构建签名字符串
|
||||
string_to_sign = (
|
||||
self.access_key_id + "\n"
|
||||
+ expires + "\n"
|
||||
+ method + "\n"
|
||||
+ content_md5 + "\n"
|
||||
+ canonicalized_resource
|
||||
)
|
||||
|
||||
# 5. HMAC-SHA1 签名 → base64 编码 → URL 编码
|
||||
signature_raw = hmac.new(
|
||||
self.access_key_secret.encode("utf-8"), # 密钥
|
||||
string_to_sign.encode("utf-8"), # 待签名字符串
|
||||
hashlib.sha1, # 算法
|
||||
).digest()
|
||||
signature_b64 = base64.b64encode(signature_raw).decode("utf-8")
|
||||
signature_encoded = quote(signature_b64, safe="")
|
||||
|
||||
# 6. 拼接 Authorization Header
|
||||
# 格式: "HRESS" + AccessKeyId + ":" + Expires + ":" + Signature
|
||||
authorization = f"HRESS{self.access_key_id}:{expires}:{signature_encoded}"
|
||||
|
||||
return {
|
||||
"Authorization": authorization,
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# 通用请求方法
|
||||
# ======================================================================
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
path: str,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
) -> HuorongApiResponse:
|
||||
"""发送签名请求到火绒API。
|
||||
|
||||
统一处理:
|
||||
1. HRESS 签名 Authorization Header 生成
|
||||
2. HTTP请求发送(POST,超时控制)
|
||||
3. 响应解析和错误码处理
|
||||
4. 异常分类(认证/连接/API业务错误)
|
||||
|
||||
Args:
|
||||
path: API路径(如 /api/clnts/_list)
|
||||
body: 请求体字典(可选)
|
||||
|
||||
Returns:
|
||||
HuorongApiResponse: 火绒API响应
|
||||
|
||||
Raises:
|
||||
HuorongConnectionError: 网络不通或超时
|
||||
HuorongAuthError: 签名验证失败
|
||||
HuorongApiError: 火绒API返回业务错误
|
||||
"""
|
||||
# 构建完整URL
|
||||
url = f"{self.base_url}{path}"
|
||||
|
||||
# 序列化请求体为字节(签名基于字节内容)
|
||||
body_bytes = json.dumps(body, separators=(",", ":")).encode("utf-8") if body else b"{}"
|
||||
|
||||
# 生成签名 Header
|
||||
headers = self._sign_request("POST", path, body_bytes)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
logger.debug(
|
||||
f"火绒API请求: POST {url}\n"
|
||||
f" AccessKeyID: {self.access_key_id}\n"
|
||||
f" Path: {path}\n"
|
||||
f" Body: {body_bytes[:200].decode('utf-8', errors='replace')}\n"
|
||||
f" Authorization: {headers.get('Authorization', 'N/A')[:60]}..."
|
||||
)
|
||||
response = await client.post(url, headers=headers, content=body_bytes)
|
||||
|
||||
# HTTP层面错误
|
||||
if response.status_code == 401:
|
||||
raise HuorongAuthError()
|
||||
if response.status_code != 200:
|
||||
raise HuorongApiError(
|
||||
code=response.status_code,
|
||||
message=f"HTTP {response.status_code}: {response.text[:200]}",
|
||||
)
|
||||
|
||||
# 解析JSON响应
|
||||
resp_data = response.json()
|
||||
api_resp = HuorongApiResponse(**resp_data)
|
||||
|
||||
# 火绒业务错误码处理
|
||||
# 官方文档定义的错误码:
|
||||
# - errno=0: 成功
|
||||
# - errno=1: 认证失败
|
||||
# - errno=2: 参数错误
|
||||
# - errno=3: 服务器内部错误
|
||||
# - errno=4: API未授权
|
||||
if api_resp.errcode != 0:
|
||||
if api_resp.errcode == 1 or api_resp.errcode in (401, 403):
|
||||
raise HuorongAuthError(f"认证/权限失败: {api_resp.errmsg}")
|
||||
if api_resp.errcode == 4:
|
||||
raise HuorongApiError(
|
||||
code=api_resp.errcode,
|
||||
message=f"API未授权: {api_resp.errmsg}",
|
||||
)
|
||||
raise HuorongApiError(
|
||||
code=api_resp.errcode,
|
||||
message=api_resp.errmsg,
|
||||
)
|
||||
|
||||
return api_resp
|
||||
|
||||
except httpx.TimeoutException:
|
||||
raise HuorongConnectionError(f"火绒API请求超时({self.timeout}秒): {url}")
|
||||
except httpx.ConnectError:
|
||||
raise HuorongConnectionError(f"无法连接火绒服务器: {url}")
|
||||
except (HuorongAuthError, HuorongApiError, HuorongConnectionError):
|
||||
# 已分类异常,直接向上抛出
|
||||
raise
|
||||
except Exception as e:
|
||||
# 未预期异常,包装为通用错误
|
||||
logger.error(f"火绒API未预期异常: {type(e).__name__}: {e}")
|
||||
raise HuorongError(code=-1, message=f"火绒API调用异常: {e}")
|
||||
|
||||
# ======================================================================
|
||||
# P0 接口:查询能力
|
||||
# ======================================================================
|
||||
|
||||
async def list_terminals(
|
||||
self,
|
||||
group_id: Optional[str] = None,
|
||||
page: int = 1,
|
||||
per_page: int = DEFAULT_PAGE_SIZE,
|
||||
) -> Dict[str, Any]:
|
||||
"""查询终端基本信息列表。
|
||||
|
||||
火绒API: POST /api/clnts/_list
|
||||
官方参数: limit(每页条数, 默认15, 最大200) + offset(起始索引, 默认0)
|
||||
本方法将 page/per_page 转换为 limit/offset,保持外部接口一致。
|
||||
|
||||
Args:
|
||||
group_id: 分组ID(可选,不传则查全部分组)
|
||||
page: 页码(从1开始,内部转换为offset)
|
||||
per_page: 每页条数(内部转换为limit)
|
||||
|
||||
Returns:
|
||||
Dict: 包含 total(总数) 和 items(TerminalBasicInfo列表)
|
||||
"""
|
||||
# 火绒API使用 limit/offset 分页,不是 page/per_page
|
||||
limit = min(per_page, 200) # 火绒限制最大200
|
||||
offset = (page - 1) * limit
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
if group_id:
|
||||
body["group_id"] = int(group_id)
|
||||
|
||||
resp = await self._request("/api/clnts/_list", body)
|
||||
|
||||
# 解析响应数据
|
||||
data = resp.data or {}
|
||||
raw_items = data.get("list", [])
|
||||
total = data.get("total", 0)
|
||||
|
||||
items = [TerminalBasicInfo(**item) for item in raw_items]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
async def get_terminal_detail(
|
||||
self,
|
||||
client_id: str,
|
||||
optional_fields: Optional[List[str]] = None,
|
||||
) -> TerminalDetailV2:
|
||||
"""获取终端详细信息v2。
|
||||
|
||||
火绒API: POST /api/clnts/_info2
|
||||
用途:获取终端硬件/软件/资产/网络配置等详细信息
|
||||
|
||||
Args:
|
||||
client_id: 终端唯一ID
|
||||
optional_fields: 需要返回的可选信息块
|
||||
可选值: hardware, software, assets, netconf
|
||||
默认全部返回
|
||||
|
||||
Returns:
|
||||
TerminalDetailV2: 终端详细信息
|
||||
"""
|
||||
if optional_fields is None:
|
||||
optional_fields = ["hardware", "software", "assets", "netconf"]
|
||||
|
||||
body = {
|
||||
"client_id": client_id,
|
||||
"optional_fields": optional_fields,
|
||||
}
|
||||
|
||||
resp = await self._request("/api/clnts/_info2", body)
|
||||
data = resp.data or {}
|
||||
|
||||
return TerminalDetailV2(**data)
|
||||
|
||||
async def list_terminal_leaks(
|
||||
self,
|
||||
group_id: Optional[str] = None,
|
||||
page: int = 1,
|
||||
per_page: int = DEFAULT_PAGE_SIZE,
|
||||
) -> Dict[str, Any]:
|
||||
"""查询存在高危漏洞未修复的终端。
|
||||
|
||||
火绒API: POST /api/clnts/_leak
|
||||
官方参数: limit(每页条数, 默认15, 最大200) + offset(起始索引, 默认0)
|
||||
说明:返回的是"存在高危漏洞的终端列表",不是漏洞详情。
|
||||
每条记录是终端信息,字段名与 _list 不同:
|
||||
- cid (非 client_id)
|
||||
- hostname (非 computer_name)
|
||||
- ip_addr (非 local_ip)
|
||||
- stat (1=离线,2=在线,3=异常,非 is_online 布尔值)
|
||||
外层有 all_client(终端总数)和 risk_client(高危终端数)统计。
|
||||
|
||||
Args:
|
||||
group_id: 分组ID(可选,不传则查全部分组)
|
||||
page: 页码(从1开始,内部转换为offset)
|
||||
per_page: 每页条数(内部转换为limit)
|
||||
|
||||
Returns:
|
||||
Dict: 包含 total(高危终端总数), risk_client(高危终端数),
|
||||
all_client(全部终端数) 和 items(TerminalLeakInfo列表)
|
||||
"""
|
||||
# 火绒API使用 limit/offset 分页
|
||||
limit = min(per_page, 200)
|
||||
offset = (page - 1) * limit
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
if group_id:
|
||||
body["group_id"] = int(group_id)
|
||||
|
||||
resp = await self._request("/api/clnts/_leak", body)
|
||||
|
||||
# 解析响应数据
|
||||
data = resp.data or {}
|
||||
raw_items = data.get("list", [])
|
||||
# _leak 不返回 total,但有 all_client 和 risk_client 统计
|
||||
all_client = data.get("all_client", 0)
|
||||
risk_client = data.get("risk_client", 0)
|
||||
|
||||
items = [TerminalLeakInfo(**item) for item in raw_items]
|
||||
|
||||
return {
|
||||
"total": risk_client, # 高危终端总数 = risk_client
|
||||
"all_client": all_client, # 全部终端数
|
||||
"risk_client": risk_client, # 高危终端数
|
||||
"items": items,
|
||||
}
|
||||
|
||||
async def get_virus_events(
|
||||
self,
|
||||
client_id: Optional[str] = None,
|
||||
group_id: Optional[str] = None,
|
||||
query_type: int = 2,
|
||||
begin_time: Optional[int] = None,
|
||||
end_time: Optional[int] = None,
|
||||
page: int = 1,
|
||||
per_page: int = DEFAULT_PAGE_SIZE,
|
||||
) -> Dict[str, Any]:
|
||||
"""查询终端病毒事件统计。
|
||||
|
||||
火绒API: POST /api/clnts/_virus_events
|
||||
官方参数:
|
||||
- type: 查询类型(必填)
|
||||
0=使用终端唯一标识查询(client_id字段必填)
|
||||
1=使用分组ID查询(group_id字段必填)
|
||||
2=查询全部终端日志(client_id和group_id字段可忽略)
|
||||
- client_id: 终端唯一标识
|
||||
- group_id: 分组ID
|
||||
- begin_time/end_time: 日志范围时间(Unix时间戳,默认全部时间)
|
||||
- limit/offset: 分页参数
|
||||
说明:返回终端维度的病毒日志统计,含 count(总数) 和
|
||||
result{success/fail/ignored/trusted}(处理结果明细)。
|
||||
|
||||
Args:
|
||||
client_id: 终端唯一ID(type=0时必填)
|
||||
group_id: 分组ID(type=1时必填)
|
||||
query_type: 查询类型,默认2(查全部)
|
||||
begin_time: 日志开始时间(Unix时间戳,可选)
|
||||
end_time: 日志结束时间(Unix时间戳,可选)
|
||||
page: 页码(从1开始,内部转换为offset)
|
||||
per_page: 每页条数(内部转换为limit)
|
||||
|
||||
Returns:
|
||||
Dict: 包含 total(总数) 和 items(VirusEventStats列表)
|
||||
"""
|
||||
# 火绒API使用 limit/offset 分页
|
||||
limit = min(per_page, 200)
|
||||
offset = (page - 1) * limit
|
||||
|
||||
body: Dict[str, Any] = {
|
||||
"type": query_type,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
}
|
||||
# 根据查询类型添加可选参数
|
||||
if query_type == 0 and client_id:
|
||||
body["client_id"] = client_id
|
||||
if query_type in (0, 1) and group_id:
|
||||
body["group_id"] = int(group_id)
|
||||
# 时间范围过滤
|
||||
if begin_time:
|
||||
body["begin_time"] = begin_time
|
||||
if end_time:
|
||||
body["end_time"] = end_time
|
||||
|
||||
resp = await self._request("/api/clnts/_virus_events", body)
|
||||
|
||||
data = resp.data or {}
|
||||
raw_items = data.get("list", [])
|
||||
total = data.get("total", 0)
|
||||
|
||||
items = [VirusEventStats(**item) for item in raw_items]
|
||||
|
||||
return {
|
||||
"total": total,
|
||||
"items": items,
|
||||
}
|
||||
|
||||
# ======================================================================
|
||||
# P1 接口:控制能力
|
||||
# ======================================================================
|
||||
|
||||
async def isolate_terminal(
|
||||
self,
|
||||
client_ids: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
"""隔离终端(断网)。
|
||||
|
||||
火绒API: POST /api/task/_create (type=netctrl, net_isolation=true)
|
||||
安全等级: 🔴 高危操作,调用方必须确保:
|
||||
1. 仅 admin 角色可调用
|
||||
2. 已完成二次确认
|
||||
3. 已记录操作原因
|
||||
|
||||
Args:
|
||||
client_ids: 目标终端ID列表
|
||||
|
||||
Returns:
|
||||
Dict: 火绒API响应的data部分
|
||||
"""
|
||||
body = {
|
||||
"type": "netctrl",
|
||||
"net_isolation": True,
|
||||
"clients": client_ids,
|
||||
}
|
||||
|
||||
resp = await self._request("/api/task/_create", body)
|
||||
logger.warning(f"火绒终端隔离操作: client_ids={client_ids}")
|
||||
return resp.data or {}
|
||||
|
||||
async def unisolate_terminal(
|
||||
self,
|
||||
client_ids: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
"""解除终端隔离(恢复网络)。
|
||||
|
||||
火绒API: POST /api/task/_create (type=netctrl, net_isolation=false)
|
||||
|
||||
Args:
|
||||
client_ids: 目标终端ID列表
|
||||
|
||||
Returns:
|
||||
Dict: 火绒API响应的data部分
|
||||
"""
|
||||
body = {
|
||||
"type": "netctrl",
|
||||
"net_isolation": False,
|
||||
"clients": client_ids,
|
||||
}
|
||||
|
||||
resp = await self._request("/api/task/_create", body)
|
||||
logger.info(f"火绒终端解除隔离: client_ids={client_ids}")
|
||||
return resp.data or {}
|
||||
|
||||
async def create_scan_task(
|
||||
self,
|
||||
client_ids: List[str],
|
||||
scan_type: str = "quick_scan",
|
||||
) -> Dict[str, Any]:
|
||||
"""创建终端扫描任务。
|
||||
|
||||
火绒API: POST /api/task/_create
|
||||
扫描类型: quick_scan(快速扫描) / full_scan(全盘扫描) / custom_scan(自定义扫描)
|
||||
|
||||
Args:
|
||||
client_ids: 目标终端ID列表
|
||||
scan_type: 扫描类型,默认快速扫描
|
||||
|
||||
Returns:
|
||||
Dict: 火绒API响应的data部分
|
||||
"""
|
||||
body = {
|
||||
"type": scan_type,
|
||||
"clients": client_ids,
|
||||
}
|
||||
|
||||
resp = await self._request("/api/task/_create", body)
|
||||
logger.info(f"火绒终端扫描任务: type={scan_type}, client_ids={client_ids}")
|
||||
return resp.data or {}
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
client_ids: List[str],
|
||||
content: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""向终端发送通知。
|
||||
|
||||
火绒API: POST /api/task/_create (type=message)
|
||||
|
||||
Args:
|
||||
client_ids: 目标终端ID列表
|
||||
content: 通知内容
|
||||
|
||||
Returns:
|
||||
Dict: 火绒API响应的data部分
|
||||
"""
|
||||
body = {
|
||||
"type": "message",
|
||||
"clients": client_ids,
|
||||
"content": content,
|
||||
}
|
||||
|
||||
resp = await self._request("/api/task/_create", body)
|
||||
logger.info(f"火绒终端通知: client_ids={client_ids}, content={content[:50]}")
|
||||
return resp.data or {}
|
||||
|
||||
# ======================================================================
|
||||
# 测试连接
|
||||
# ======================================================================
|
||||
|
||||
async def test_connection(self) -> Dict[str, Any]:
|
||||
"""测试火绒API连接是否正常。
|
||||
|
||||
使用 _list 接口(page=1, per_page=1)进行轻量级连接测试,
|
||||
验证签名是否正确、网络是否可达。
|
||||
|
||||
Returns:
|
||||
Dict: 包含 success(bool) 和 message(str)
|
||||
"""
|
||||
try:
|
||||
result = await self.list_terminals(page=1, per_page=1)
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"连接成功,共 {result.get('total', 0)} 个终端",
|
||||
"total_terminals": result.get("total", 0),
|
||||
}
|
||||
except HuorongAuthError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"认证失败: {e.message}",
|
||||
}
|
||||
except HuorongConnectionError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"连接失败: {e.message}",
|
||||
}
|
||||
except HuorongError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"测试失败: {e.message}",
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 火绒集成配置管理
|
||||
# =============================================================================
|
||||
# 说明:从系统配置表(system_configs)读取火绒 AccessKey/Secret/BaseUrl,
|
||||
# 构建火绒API客户端实例
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.system_config import SystemConfig
|
||||
from .client import HuorongClient
|
||||
from .exceptions import HuorongConfigError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 火绒配置在 system_configs 表中的 key 前缀
|
||||
HUORONG_CONFIG_PREFIX = "integration_huorong_"
|
||||
|
||||
|
||||
async def get_huorong_client(db: AsyncSession) -> HuorongClient:
|
||||
"""从系统配置表构建火绒API客户端。
|
||||
|
||||
读取 integration_huorong_ 前缀的配置项,构建 HuorongClient 实例。
|
||||
如果任何必填配置缺失,抛出 HuorongConfigError。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
HuorongClient: 已配置的火绒API客户端实例
|
||||
|
||||
Raises:
|
||||
HuorongConfigError: AccessKey ID/Secret/Base URL 任一缺失
|
||||
"""
|
||||
# 读取三个必填配置
|
||||
result = await db.execute(
|
||||
select(SystemConfig).where(
|
||||
SystemConfig.config_key.startswith(HUORONG_CONFIG_PREFIX)
|
||||
)
|
||||
)
|
||||
configs = list(result.scalars().all())
|
||||
|
||||
# 构建 key→value 映射
|
||||
config_map = {cfg.config_key: cfg.config_value for cfg in configs}
|
||||
|
||||
access_key_id = config_map.get(f"{HUORONG_CONFIG_PREFIX}access_key_id", "")
|
||||
access_key_secret = config_map.get(f"{HUORONG_CONFIG_PREFIX}access_key_secret", "")
|
||||
base_url = config_map.get(f"{HUORONG_CONFIG_PREFIX}base_url", "")
|
||||
|
||||
# 校验必填项
|
||||
if not access_key_id or not access_key_secret or not base_url:
|
||||
missing = []
|
||||
if not access_key_id:
|
||||
missing.append("AccessKey ID")
|
||||
if not access_key_secret:
|
||||
missing.append("AccessKey Secret")
|
||||
if not base_url:
|
||||
missing.append("Base URL")
|
||||
raise HuorongConfigError(
|
||||
f"火绒集成配置不完整,缺失: {', '.join(missing)},请先在管理后台完成配置"
|
||||
)
|
||||
|
||||
return HuorongClient(
|
||||
access_key_id=access_key_id,
|
||||
access_key_secret=access_key_secret,
|
||||
base_url=base_url,
|
||||
)
|
||||
|
||||
|
||||
async def is_huorong_configured(db: AsyncSession) -> bool:
|
||||
"""检查火绒集成是否已完整配置。
|
||||
|
||||
Args:
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
bool: 三项配置均存在且非空时返回 True
|
||||
"""
|
||||
try:
|
||||
client = await get_huorong_client(db)
|
||||
return bool(client.access_key_id and client.access_key_secret and client.base_url)
|
||||
except HuorongConfigError:
|
||||
return False
|
||||
@@ -0,0 +1,63 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 火绒集成自定义异常
|
||||
# =============================================================================
|
||||
# 说明:火绒API调用中可能抛出的各种异常类型
|
||||
# 包含:认证错误、连接超时、API错误码等
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class HuorongError(Exception):
|
||||
"""火绒集成基础异常。
|
||||
|
||||
所有火绒相关异常的父类,便于统一捕获处理。
|
||||
|
||||
Attributes:
|
||||
code: 错误码(火绒API返回的errcode,或自定义错误码)
|
||||
message: 错误描述
|
||||
"""
|
||||
|
||||
def __init__(self, code: int = -1, message: str = "火绒API调用失败"):
|
||||
self.code = code
|
||||
self.message = message
|
||||
super().__init__(f"[HuorongError:{code}] {message}")
|
||||
|
||||
|
||||
class HuorongAuthError(HuorongError):
|
||||
"""火绒认证失败异常。
|
||||
|
||||
场景:AccessKey ID/Secret 无效、签名校验失败、权限不足
|
||||
火绒API返回 errcode=401 或签名相关错误时抛出。
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "火绒API认证失败,请检查AccessKey配置"):
|
||||
super().__init__(code=401, message=message)
|
||||
|
||||
|
||||
class HuorongConnectionError(HuorongError):
|
||||
"""火绒连接失败异常。
|
||||
|
||||
场景:内网地址不通、超时、DNS解析失败
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "无法连接火绒服务器,请检查网络和Base URL配置"):
|
||||
super().__init__(code=502, message=message)
|
||||
|
||||
|
||||
class HuorongConfigError(HuorongError):
|
||||
"""火绒配置缺失异常。
|
||||
|
||||
场景:AccessKey ID/Secret/Base URL 未在系统配置中设置
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = "火绒集成未配置,请先在管理后台设置AccessKey和Base URL"):
|
||||
super().__init__(code=400, message=message)
|
||||
|
||||
|
||||
class HuorongApiError(HuorongError):
|
||||
"""火绒API业务错误。
|
||||
|
||||
场景:火绒API返回非0 errcode(如参数错误、终端不存在等)
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, message: str):
|
||||
super().__init__(code=code, message=message)
|
||||
@@ -0,0 +1,373 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 火绒集成数据模型
|
||||
# =============================================================================
|
||||
# 说明:火绒API请求/响应的 Pydantic 数据模型
|
||||
# 包含:终端信息、漏洞信息、病毒事件、任务下发等
|
||||
# =============================================================================
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 通用响应模型
|
||||
# ==========================================================================
|
||||
|
||||
class HuorongApiResponse(BaseModel):
|
||||
"""火绒API统一响应模型。
|
||||
|
||||
火绒所有API返回格式一致(官方API文档 v1):
|
||||
成功时: { "errno": 0, "errmsg": "", "data": { ... } }
|
||||
失败时: { "errno": 1, "errmsg": "Authentication failed" }
|
||||
|
||||
官方错误码定义:
|
||||
- errno=0: 成功
|
||||
- errno=1: 认证失败
|
||||
- errno=2: 参数错误
|
||||
- errno=3: 服务器内部错误
|
||||
- errno=4: API未授权
|
||||
|
||||
注意:火绒API始终使用 errno(不是 errcode)。
|
||||
使用 model_validator 在验证前将 errno 归一化为 errcode,
|
||||
保持内部代码统一使用 errcode 字段。
|
||||
|
||||
Attributes:
|
||||
errcode: 错误码,0表示成功(从 errno 归一化而来)
|
||||
errmsg: 错误描述(成功时为空字符串)
|
||||
data: 业务数据(成功时非None)
|
||||
"""
|
||||
|
||||
@model_validator(mode='before')
|
||||
@classmethod
|
||||
def normalize_error_fields(cls, data: Any) -> Any:
|
||||
"""将火绒API返回的 errno 字段归一化为 errcode。
|
||||
|
||||
火绒API在认证失败等错误场景下返回 errno 而非 errcode,
|
||||
此验证器在 Pydantic 字段校验前将 errno 转换为 errcode,
|
||||
统一后续处理逻辑。
|
||||
|
||||
Args:
|
||||
data: 原始输入数据(通常为dict)
|
||||
|
||||
Returns:
|
||||
归一化后的数据
|
||||
"""
|
||||
if isinstance(data, dict) and 'errno' in data and 'errcode' not in data:
|
||||
data['errcode'] = data.pop('errno')
|
||||
return data
|
||||
|
||||
errcode: int = Field(..., description="错误码,0=成功")
|
||||
errmsg: str = Field(default="ok", description="错误描述")
|
||||
data: Optional[Any] = Field(default=None, description="业务数据")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端基本信息 — /api/clnts/_list 返回
|
||||
# ==========================================================================
|
||||
|
||||
class TerminalBasicInfo(BaseModel):
|
||||
"""终端基本信息(_list 接口返回的每条记录)。
|
||||
|
||||
字段名严格按照火绒API文档实际返回值定义。
|
||||
注意:API返回的字段名与之前猜测不同,已根据官方文档修正。
|
||||
|
||||
Attributes:
|
||||
id: 内部数据库ID
|
||||
client_id: 终端唯一ID(40位十六进制字符串,用于所有任务下发)
|
||||
client_name: 客户端名称
|
||||
computer_name: 计算机名
|
||||
local_ip: 本地IP
|
||||
connect_ip: 连接IP(客户端连接控制中心使用的IP)
|
||||
mac: MAC地址
|
||||
group_id: 分组ID
|
||||
os_version: 操作系统版本
|
||||
version: 火绒客户端版本
|
||||
definitions: 病毒库更新时间
|
||||
is_online: 在线状态
|
||||
last_connect_time: 最后连接时间(Unix时间戳)
|
||||
last_seen_time: 最后可见时间(Unix时间戳)
|
||||
first_appear_time: 首次出现时间(Unix时间戳)
|
||||
"""
|
||||
id: Optional[int] = Field(default=None, description="内部数据库ID")
|
||||
client_id: str = Field(..., description="终端唯一ID")
|
||||
client_name: str = Field(default="", description="客户端名称")
|
||||
computer_name: str = Field(default="", description="计算机名")
|
||||
local_ip: str = Field(default="", description="本地IP")
|
||||
connect_ip: str = Field(default="", description="连接IP")
|
||||
mac: str = Field(default="", description="MAC地址")
|
||||
group_id: Optional[Any] = Field(default=None, description="分组ID(int或str)")
|
||||
os_version: str = Field(default="", description="操作系统版本")
|
||||
version: str = Field(default="", description="火绒客户端版本")
|
||||
definitions: str = Field(default="", description="病毒库更新时间")
|
||||
is_online: bool = Field(default=False, description="在线状态")
|
||||
last_connect_time: Optional[int] = Field(default=None, description="最后连接时间")
|
||||
last_seen_time: Optional[int] = Field(default=None, description="最后可见时间")
|
||||
first_appear_time: Optional[int] = Field(default=None, description="首次出现时间")
|
||||
|
||||
|
||||
class TerminalListRequest(BaseModel):
|
||||
"""终端列表查询请求。
|
||||
|
||||
Attributes:
|
||||
group_id: 分组ID(可选,不传则查全部分组)
|
||||
page: 页码(从1开始)
|
||||
per_page: 每页条数
|
||||
"""
|
||||
group_id: Optional[str] = Field(default=None, description="分组ID")
|
||||
page: int = Field(default=1, ge=1, description="页码")
|
||||
per_page: int = Field(default=20, ge=1, le=100, description="每页条数")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端详细信息v2 — /api/clnts/_info2 返回
|
||||
# ==========================================================================
|
||||
|
||||
class HardwareInfo(BaseModel):
|
||||
"""终端硬件信息。
|
||||
|
||||
Attributes:
|
||||
cpu: CPU信息
|
||||
memory: 内存信息
|
||||
disk: 磁盘信息
|
||||
motherboard: 主板信息
|
||||
network_card: 网卡信息
|
||||
"""
|
||||
cpu: str = Field(default="", description="CPU信息")
|
||||
memory: str = Field(default="", description="内存信息")
|
||||
disk: str = Field(default="", description="磁盘信息")
|
||||
motherboard: str = Field(default="", description="主板信息")
|
||||
network_card: str = Field(default="", description="网卡信息")
|
||||
|
||||
|
||||
class SoftwareInfo(BaseModel):
|
||||
"""已安装软件条目。
|
||||
|
||||
Attributes:
|
||||
name: 软件名称
|
||||
version: 版本号
|
||||
publisher: 发布者
|
||||
"""
|
||||
name: str = Field(default="", description="软件名称")
|
||||
version: str = Field(default="", description="版本号")
|
||||
publisher: str = Field(default="", description="发布者")
|
||||
|
||||
|
||||
class AssetInfo(BaseModel):
|
||||
"""资产信息。
|
||||
|
||||
Attributes:
|
||||
asset_tag: 资产标签
|
||||
serial_number: 序列号
|
||||
"""
|
||||
asset_tag: str = Field(default="", description="资产标签")
|
||||
serial_number: str = Field(default="", description="序列号")
|
||||
|
||||
|
||||
class NetworkConfig(BaseModel):
|
||||
"""网络配置信息。
|
||||
|
||||
Attributes:
|
||||
ip: IP地址
|
||||
gateway: 网关
|
||||
dns: DNS服务器
|
||||
adapter_info: 网卡适配器信息
|
||||
"""
|
||||
ip: str = Field(default="", description="IP地址")
|
||||
gateway: str = Field(default="", description="网关")
|
||||
dns: str = Field(default="", description="DNS服务器")
|
||||
adapter_info: str = Field(default="", description="网卡适配器信息")
|
||||
|
||||
|
||||
class TerminalDetailV2(BaseModel):
|
||||
"""终端详细信息v2(_info2 接口返回)。
|
||||
|
||||
通过 optional_fields 参数指定需要返回的信息块:
|
||||
- hardware: 硬件信息
|
||||
- software: 已安装软件
|
||||
- assets: 资产信息
|
||||
- netconf: 网络配置
|
||||
|
||||
Attributes:
|
||||
client_id: 终端唯一ID
|
||||
computer_name: 计算机名
|
||||
hardware: 硬件信息(可选)
|
||||
software: 已安装软件列表(可选)
|
||||
assets: 资产信息(可选)
|
||||
netconf: 网络配置(可选)
|
||||
"""
|
||||
client_id: str = Field(..., description="终端唯一ID")
|
||||
computer_name: str = Field(default="", description="计算机名")
|
||||
hardware: Optional[HardwareInfo] = Field(default=None, description="硬件信息")
|
||||
software: Optional[List[SoftwareInfo]] = Field(default=None, description="已安装软件")
|
||||
assets: Optional[AssetInfo] = Field(default=None, description="资产信息")
|
||||
netconf: Optional[NetworkConfig] = Field(default=None, description="网络配置")
|
||||
|
||||
|
||||
class TerminalDetailRequest(BaseModel):
|
||||
"""终端详细信息查询请求。
|
||||
|
||||
Attributes:
|
||||
client_id: 终端唯一ID
|
||||
optional_fields: 需要返回的可选信息块列表
|
||||
"""
|
||||
client_id: str = Field(..., description="终端唯一ID")
|
||||
optional_fields: List[str] = Field(
|
||||
default_factory=lambda: ["hardware", "software", "assets", "netconf"],
|
||||
description="可选信息块: hardware/software/assets/netconf",
|
||||
)
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 漏洞信息 — /api/clnts/_leak 返回
|
||||
# 说明:_leak 接口返回的是"存在高危漏洞未修复的终端列表",
|
||||
# 每条记录是终端信息(非漏洞详情),API不返回具体漏洞CVE列表。
|
||||
# 外层还有 all_client(终端总数)和 risk_client(高危终端数)统计。
|
||||
# ==========================================================================
|
||||
|
||||
class TerminalLeakInfo(BaseModel):
|
||||
"""存在高危漏洞的终端信息(_leak 接口返回的每条记录)。
|
||||
|
||||
注意:_leak 返回的是终端维度数据,不是漏洞维度。
|
||||
字段名严格按照火绒API文档实际返回值定义。
|
||||
与 _list 接口的字段名不同!
|
||||
|
||||
Attributes:
|
||||
cid: 终端唯一ID(_leak 中叫 cid,_list 中叫 client_id)
|
||||
hostname: 计算机名(_leak 中叫 hostname,_list 中叫 computer_name)
|
||||
client_name: 终端名称
|
||||
group_name: 分组名称
|
||||
group_id: 分组ID
|
||||
ip_addr: 本地IP(_leak 中叫 ip_addr,_list 中叫 local_ip)
|
||||
call_ip: 连接IP(_leak 中叫 call_ip,_list 中叫 connect_ip)
|
||||
mac: MAC地址
|
||||
osver: 操作系统版本(_leak 中叫 osver,_list 中叫 os_version)
|
||||
os_type: 终端类型(如 Windows)
|
||||
prodver: 火绒客户端版本(_leak 中叫 prodver,_list 中叫 version)
|
||||
virdb: 病毒库版本(Unix时间戳,_leak 中叫 virdb,_list 中叫 definitions)
|
||||
stat: 在线状态码(1=离线, 2=在线, 3=异常,_list 中是 is_online 布尔值)
|
||||
"""
|
||||
cid: str = Field(..., description="终端唯一ID")
|
||||
hostname: str = Field(default="", description="计算机名")
|
||||
client_name: str = Field(default="", description="终端名称")
|
||||
group_name: str = Field(default="", description="分组名称")
|
||||
group_id: Optional[Any] = Field(default=None, description="分组ID")
|
||||
ip_addr: str = Field(default="", description="本地IP")
|
||||
call_ip: str = Field(default="", description="连接IP")
|
||||
mac: str = Field(default="", description="MAC地址")
|
||||
osver: str = Field(default="", description="操作系统版本")
|
||||
os_type: str = Field(default="", description="终端类型")
|
||||
prodver: str = Field(default="", description="火绒客户端版本")
|
||||
virdb: Optional[Any] = Field(default=None, description="病毒库版本(Unix时间戳)")
|
||||
stat: int = Field(default=1, description="在线状态码: 1=离线 2=在线 3=异常")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 病毒事件 — /api/clnts/_virus_events 返回
|
||||
# 说明:_virus_events 返回终端维度的病毒日志统计,
|
||||
# 含总数(count)和4种处理结果(result)的明细。
|
||||
# 请求需指定 type: 0=按client_id查, 1=按group_id查, 2=查全部
|
||||
# ==========================================================================
|
||||
|
||||
class VirusHandleResult(BaseModel):
|
||||
"""病毒事件处理结果统计。
|
||||
|
||||
Attributes:
|
||||
success: 处理成功数
|
||||
fail: 处理失败数
|
||||
ignored: 暂不处理数
|
||||
trusted: 已信任数
|
||||
"""
|
||||
success: int = Field(default=0, description="处理成功数")
|
||||
fail: int = Field(default=0, description="处理失败数")
|
||||
ignored: int = Field(default=0, description="暂不处理数")
|
||||
trusted: int = Field(default=0, description="已信任数")
|
||||
|
||||
|
||||
class VirusEventStats(BaseModel):
|
||||
"""终端病毒事件统计(_virus_events 接口返回的每条记录)。
|
||||
|
||||
字段名严格按照火绒API文档实际返回值定义。
|
||||
与 _list 接口的字段名基本一致。
|
||||
|
||||
Attributes:
|
||||
group_id: 分组ID
|
||||
client_id: 终端唯一ID
|
||||
client_name: 终端名称
|
||||
computer_name: 计算机名
|
||||
local_ip: 本地IP
|
||||
connect_ip: 连接IP
|
||||
mac: MAC地址
|
||||
count: 病毒日志总数
|
||||
result: 处理结果统计(success/fail/ignored/trusted)
|
||||
"""
|
||||
group_id: Optional[Any] = Field(default=None, description="分组ID")
|
||||
client_id: str = Field(..., description="终端唯一ID")
|
||||
client_name: str = Field(default="", description="终端名称")
|
||||
computer_name: str = Field(default="", description="计算机名")
|
||||
local_ip: str = Field(default="", description="本地IP")
|
||||
connect_ip: str = Field(default="", description="连接IP")
|
||||
mac: str = Field(default="", description="MAC地址")
|
||||
count: int = Field(default=0, description="病毒日志总数")
|
||||
result: Optional[VirusHandleResult] = Field(default=None, description="处理结果统计")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端任务 — /api/task/_create
|
||||
# ==========================================================================
|
||||
|
||||
class TaskCreateRequest(BaseModel):
|
||||
"""终端任务创建请求。
|
||||
|
||||
支持的任务类型:
|
||||
- quick_scan: 快速扫描
|
||||
- full_scan: 全盘扫描
|
||||
- custom_scan: 自定义扫描
|
||||
- netctrl: 终端隔离/解除
|
||||
- message: 发送通知
|
||||
|
||||
Attributes:
|
||||
task_type: 任务类型
|
||||
client_ids: 目标终端ID列表
|
||||
net_isolation: 是否隔离(仅 netctrl 类型有效)
|
||||
message_content: 通知内容(仅 message 类型有效)
|
||||
"""
|
||||
task_type: str = Field(..., description="任务类型: quick_scan/full_scan/custom_scan/netctrl/message")
|
||||
client_ids: List[str] = Field(..., min_length=1, description="目标终端ID列表")
|
||||
net_isolation: Optional[bool] = Field(default=None, description="是否隔离(仅netctrl类型)")
|
||||
message_content: Optional[str] = Field(default=None, description="通知内容(仅message类型)")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 终端安全画像(聚合模型,供前端直接使用)
|
||||
# ==========================================================================
|
||||
|
||||
class TerminalSecurityProfile(BaseModel):
|
||||
"""终端安全画像(聚合模型)。
|
||||
|
||||
将终端基本信息+安全状态聚合成一个模型,供坐席端直接展示。
|
||||
|
||||
Attributes:
|
||||
client_id: 终端唯一ID
|
||||
computer_name: 计算机名
|
||||
ip: 本地IP
|
||||
mac: MAC地址
|
||||
os_version: 操作系统版本
|
||||
is_online: 在线状态
|
||||
group_name: 分组名称
|
||||
hardware: 硬件概要
|
||||
high_risk_leaks: 高危漏洞数
|
||||
uncleaned_virus: 未处理病毒事件数
|
||||
security_score: 安全评分(0-100,综合漏洞+病毒+在线状态)
|
||||
"""
|
||||
client_id: str = Field(..., description="终端唯一ID")
|
||||
computer_name: str = Field(default="", description="计算机名")
|
||||
ip: str = Field(default="", description="本地IP")
|
||||
mac: str = Field(default="", description="MAC地址")
|
||||
os_version: str = Field(default="", description="操作系统版本")
|
||||
is_online: bool = Field(default=False, description="在线状态")
|
||||
group_name: str = Field(default="", description="分组名称")
|
||||
hardware: Optional[HardwareInfo] = Field(default=None, description="硬件概要")
|
||||
high_risk_leaks: int = Field(default=0, description="高危漏洞数")
|
||||
uncleaned_virus: int = Field(default=0, description="未处理病毒事件数")
|
||||
security_score: int = Field(default=100, description="安全评分(0-100)")
|
||||
Reference in New Issue
Block a user