Compare commits

...

7 Commits

Author SHA1 Message Date
yyhuni
20a22f98d0 更新配置说明 2025-12-19 21:19:58 +08:00
yyhuni
a96ab79891 更新文档 2025-12-19 21:12:28 +08:00
github-actions[bot]
3744a724be chore: bump version to v1.0.23 2025-12-19 12:17:31 +00:00
yyhuni
f63e40fbba 优化:agent自动更新逻辑 2025-12-19 20:07:55 +08:00
yyhuni
54573e210a fix: agent更新逻辑 2025-12-19 20:00:36 +08:00
github-actions[bot]
6179dd2ed3 chore: bump version to v1.0.22 2025-12-19 11:56:56 +00:00
github-actions[bot]
34ac706fbc chore: bump version to v1.0.21 2025-12-19 11:49:51 +00:00
8 changed files with 213 additions and 140 deletions

View File

@@ -1 +1 @@
v1.0.20
v1.0.23

View File

@@ -10,6 +10,8 @@ class WorkerNode(models.Model):
('deploying', '部署中'),
('online', '在线'),
('offline', '离线'),
('updating', '更新中'),
('outdated', '版本过低'),
]
name = models.CharField(max_length=100, help_text='节点名称')

View File

@@ -134,6 +134,17 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
"need_update": true/false,
"server_version": "v1.0.19"
}
状态流转:
┌─────────────────────────────────────────────────────────────────────┐
│ 场景 │ 状态变化 │
├─────────────────────────────┼───────────────────────────────────────┤
│ 首次心跳 │ pending/deploying → online │
│ 远程 Worker 版本不匹配 │ online → updating → (更新成功) online │
│ 远程 Worker 更新失败 │ updating → outdated │
│ 本地 Worker 版本不匹配 │ online → outdated (需手动 update.sh) │
│ 版本匹配 │ updating/outdated → online │
└─────────────────────────────┴───────────────────────────────────────┘
"""
from apps.engine.services.worker_load_service import worker_load_service
from django.conf import settings
@@ -165,9 +176,19 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
)
# 远程 Worker服务端主动通过 SSH 触发更新
# 旧版 agent 不会解析 need_update所以需要服务端主动推送
if not worker.is_local and worker.ip_address:
self._trigger_remote_agent_update(worker, server_version)
else:
# 本地 Worker 版本不匹配:标记为 outdated
# 需要用户手动执行 update.sh 更新
if worker.status != 'outdated':
worker.status = 'outdated'
worker.save(update_fields=['status'])
else:
# 版本匹配,确保状态为 online
if worker.status in ('updating', 'outdated'):
worker.status = 'online'
worker.save(update_fields=['status'])
return Response({
'status': 'ok',
@@ -184,7 +205,8 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
import redis
from django.conf import settings as django_settings
redis_client = redis.from_url(django_settings.REDIS_URL)
redis_url = f"redis://{django_settings.REDIS_HOST}:{django_settings.REDIS_PORT}/{django_settings.REDIS_DB}"
redis_client = redis.from_url(redis_url)
lock_key = f"agent_update_lock:{worker.id}"
# 尝试获取锁60秒过期防止重复触发
@@ -192,6 +214,9 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
logger.debug(f"Worker {worker.name} 更新已在进行中,跳过")
return
# 获取锁成功,设置状态为 updating
self._set_worker_status(worker.id, 'updating')
# 提取数据避免后台线程访问 ORM
worker_id = worker.id
worker_name = worker.name
@@ -230,11 +255,15 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
if success:
logger.info(f"Worker {worker_name} 远程更新成功")
# 更新成功后,新 agent 心跳会自动把状态改回 online
else:
logger.warning(f"Worker {worker_name} 远程更新失败: {message}")
# 更新失败,标记为 outdated
self._set_worker_status(worker_id, 'outdated')
except Exception as e:
logger.error(f"Worker {worker_name} 远程更新异常: {e}")
self._set_worker_status(worker_id, 'outdated')
finally:
# 释放锁
redis_client.delete(lock_key)
@@ -242,6 +271,14 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
# 后台执行,不阻塞心跳响应
threading.Thread(target=_async_update, daemon=True).start()
def _set_worker_status(self, worker_id: int, status: str):
"""更新 Worker 状态(用于后台线程)"""
try:
from apps.engine.models import WorkerNode
WorkerNode.objects.filter(id=worker_id).update(status=status)
except Exception as e:
logger.error(f"更新 Worker {worker_id} 状态失败: {e}")
@action(detail=False, methods=['post'])
def register(self, request):
"""

View File

@@ -1,28 +1,14 @@
# 引擎配置
#
# ==================== 参数命名规范 ====================
# 所有参数统一用中划线,如 rate-limit, request-timeout, wordlist-name
# - 贴近 CLI 参数风格,用户更直观
# - 系统会自动转换为下划线供代码使用
#
# ==================== 必需参数 ====================
# - enabled: 是否启用工具true/false
# - timeout: 超时时间(秒),工具执行超过此时间会被强制终止
#
# 使用方式:
# - 在前端创建扫描引擎时,将此配置保存到数据库
# - 执行扫描时,从数据库读取配置并传递给 Flow
# - 取消注释可选参数即可启用
# 参数命名:统一用中划线(如 rate-limit系统自动转换为下划线
# 必需参数enabled是否启用、timeout超时秒数auto 表示自动计算)
# ==================== 子域名发现 ====================
#
# 流程说明:
# Stage 1: 被动收集(并行) - 必选,至少启用一个工具
# Stage 2: 字典爆破(可选) - 使用字典暴力枚举子域名
# Stage 3: 变异生成 + 验证(可选) - 基于已发现域名生成变异,流式验证存活
# Stage 4: DNS 存活验证(可选) - 验证所有候选域名是否能解析
#
# 灵活组合:可以关闭 2/3/4 中的任意阶段,最终结果会根据实际执行的阶段动态决定
# Stage 1: 被动收集(并行) - 必选,至少启用一个工具
# Stage 2: 字典爆破(可选) - 使用字典暴力枚举子域名
# Stage 3: 变异生成 + 验证(可选) - 基于已发现域名生成变异,流式验证存活
# Stage 4: DNS 存活验证(可选) - 验证所有候选域名是否能解析
#
subdomain_discovery:
# === Stage 1: 被动收集工具(并行执行)===
@@ -30,11 +16,11 @@ subdomain_discovery:
subfinder:
enabled: true
timeout: 7200 # 2小时
# threads: 10 # 可选,并发 goroutine 数
# threads: 10 # 并发 goroutine 数
amass_passive:
enabled: true
timeout: 7200 # 2小时
timeout: 7200
amass_active:
enabled: true # 主动枚举 + 爆破
@@ -43,7 +29,7 @@ subdomain_discovery:
sublist3r:
enabled: true
timeout: 7200
# threads: 50 # 可选,线程数
# threads: 50 # 线程数
assetfinder:
enabled: true
@@ -51,174 +37,123 @@ subdomain_discovery:
# === Stage 2: 主动字典爆破(可选)===
bruteforce:
enabled: false # 是否启用字典爆破
enabled: false
subdomain_bruteforce:
timeout: auto # 自动根据字典行数计算(后续代码中按行数 * 3 秒实现)
wordlist-name: subdomains-top1million-110000.txt # 字典名称,对应「字典管理」中的 Wordlist.name
timeout: auto # 自动根据字典行数计算
wordlist-name: subdomains-top1million-110000.txt # 对应「字典管理」中的 Wordlist.name
# === Stage 3: 变异生成 + 存活验证(可选,流式管道避免 OOM===
# === Stage 3: 变异生成 + 存活验证(可选)===
permutation:
enabled: true # 是否启用变异生成
enabled: true
subdomain_permutation_resolve:
timeout: 7200 # 2小时变异量大时需要更长时间
timeout: 7200
# === Stage 4: DNS 存活验证(可选)===
resolve:
enabled: true # 是否启用存活验证
enabled: true
subdomain_resolve:
timeout: auto # 自动根据候选子域数量计算(在 Flow 中按行数 * 3 秒实现)
timeout: auto # 自动根据候选子域数量计算
# ==================== 端口扫描 ====================
port_scan:
tools:
naabu_active:
enabled: true
timeout: auto # 自动计算(根据:目标数 × 端口数 × 0.5秒)
# 例如100个域名 × 100个端口 × 0.5 = 5000秒
# 10个域名 × 1000个端口 × 0.5 = 5000秒
# 超时范围60秒 ~ 2天172800秒
# 或者手动指定timeout: 3600
threads: 200 # 可选,并发连接数(默认 5
# ports: 1-65535 # 可选,扫描端口范围(默认 1-65535
top-ports: 100 # 可选Scan for nmap top 100 ports影响 timeout 计算)
rate: 10 # 可选,扫描速率(默认 10
timeout: auto # 自动计算(目标数 × 端口数 × 0.5秒),范围 60秒 ~ 2天
threads: 200 # 并发连接数(默认 5
# ports: 1-65535 # 扫描端口范围(默认 1-65535
top-ports: 100 # 扫描 nmap top 100 端口
rate: 10 # 扫描速率(默认 10
naabu_passive:
enabled: true
timeout: auto # 自动计算(被动扫描通常较快,端口数默认为 100
# 被动扫描,使用被动数据源,无需额外配置
timeout: auto # 被动扫描通常较快
# ==================== 站点扫描 ====================
site_scan:
tools:
httpx:
enabled: true
timeout: auto # 自动计算(根据URL数量每个URL 1秒)
# 或者手动指定timeout: 3600
# threads: 50 # 可选并发线程数httpx 默认 50
# rate-limit: 150 # 可选每秒发送的请求数量httpx 默认 150
# request-timeout: 10 # 可选单个请求的超时时间httpx 默认 10
# retries: 2 # 可选,请求失败重试次数
timeout: auto # 自动计算(每个 URL 约 1 秒)
# threads: 50 # 并发线程数(默认 50
# rate-limit: 150 # 每秒请求数(默认 150
# request-timeout: 10 # 单个请求超时秒数(默认 10
# retries: 2 # 请求失败重试次数
# ==================== 目录扫描 ====================
directory_scan:
tools:
ffuf:
enabled: true
timeout: auto # 自动计算超时时间(根据字典行数)
# 计算公式:字典行数 × 0.02秒/词
# 超时范围60秒 ~ 7200秒2小时
# 也可以手动指定固定超时(如 300
wordlist-name: dir_default.txt # 字典名称(必需),对应「字典管理」中唯一的 Wordlist.name
# 安装时会自动初始化名为 dir_default.txt 的默认目录字典
# ffuf 会逐行读取字典文件,将每行作为 FUZZ 关键字的替换值
delay: 0.1-2.0 # Seconds of delay between requests, or a range of random delay
# For example "0.1" or "0.1-2.0"
threads: 10 # Number of concurrent threads (default: 40)
request-timeout: 10 # HTTP request timeout in seconds (default: 10)
match-codes: 200,201,301,302,401,403 # Match HTTP status codes, comma separated
# rate: 0 # Rate of requests per second (default: 0)
timeout: auto # 自动计算(字典行数 × 0.02秒),范围 60秒 ~ 2小时
wordlist-name: dir_default.txt # 对应「字典管理」中的 Wordlist.name
delay: 0.1-2.0 # 请求间隔,支持范围随机(如 "0.1-2.0"
threads: 10 # 并发线程数(默认 40
request-timeout: 10 # HTTP 请求超时秒数(默认 10
match-codes: 200,201,301,302,401,403 # 匹配的 HTTP 状态码
# rate: 0 # 每秒请求数(默认 0 不限制)
# ==================== URL 获取 ====================
url_fetch:
tools:
waymore:
enabled: true
timeout: 3600 # 工具级别总超时:固定 3600 秒(按域名 target_name 输入)
# 如果目标较大或希望更快/更慢,可根据需要手动调整秒数
# 输入类型domain_name域名级别自动去重同域名站点
timeout: 3600 # 固定 1 小时(按域名输入)
katana:
enabled: true
timeout: auto # 工具级别总超时:自动计算(根据站点数量)
# 或手动指定timeout: 300
# ========== 核心功能参数(已在命令中固定开启) ==========
# -jc: JavaScript 爬取 + 自动解析 .js 文件里的所有端点(最重要)
# -xhr: 从 JS 中提取 XHR/Fetch 请求的 API 路径(再多挖 10-20% 隐藏接口)
# -kf all: 自动 fuzz 所有已知敏感文件(.env、.git、backup、config 等 5000+ 条)
# -fs rdn: 智能过滤重复+噪声路径(分页、?id=1/2/3 全干掉,输出极干净)
# ========== 可选参数(推荐配置) ==========
depth: 5 # 爬取最大深度(平衡深度与时间,默认 3推荐 5
threads: 10 # 全局并发数(极低并发最像真人,推荐 10
rate-limit: 30 # 全局硬限速:每秒最多 30 个请求WAF 几乎不报警)
random-delay: 1 # 每次请求之间随机延迟 0.5~1.5 秒(再加一层人性化)
retry: 2 # 失败请求自动重试 2 次(网络抖动不丢包)
request-timeout: 12 # 单请求超时 12 秒防卡死katana 参数名是 -timeout
# 输入类型url站点级别每个站点单独爬取
timeout: auto # 自动计算(根据站点数量)
depth: 5 # 爬取最大深度(默认 3
threads: 10 # 全局并发数
rate-limit: 30 # 每秒最多请求数
random-delay: 1 # 请求间随机延迟秒数
retry: 2 # 失败重试次数
request-timeout: 12 # 单请求超时秒数
uro:
enabled: true
timeout: auto # 自动计算(根据 URL 数量,每 100 个约 1 秒)
# 范围30 秒 ~ 300 秒
# 或手动指定timeout: 60
# ========== 可选参数 ==========
# whitelist: # 只保留指定扩展名的 URLphp,asp,jsp
timeout: auto # 自动计算(每 100 个 URL 约 1 秒),范围 30 ~ 300 秒
# whitelist: # 只保留指定扩展名
# - php
# - asp
# blacklist: # 排除指定扩展名的 URL(静态资源)
# blacklist: # 排除指定扩展名(静态资源)
# - jpg
# - jpeg
# - png
# - gif
# - svg
# - ico
# - css
# - woff
# - woff2
# - ttf
# - eot
# - mp4
# - mp3
# - pdf
# filters: # 额外的过滤规则,参考 uro 文档
# - hasparams # 只保留有参数的 URL
# - hasext # 只保留有扩展名的 URL
# - vuln # 只保留可能有漏洞的 URL
# 用途:清理合并后的 URL 列表,去除冗余和无效 URL
# 输入类型merged_file合并后的 URL 文件)
# 输出:清理后的 URL 列表
# filters: # 额外过滤规则
# - hasparams # 只保留有参数的 URL
# - vuln # 只保留可能有漏洞的 URL
httpx:
enabled: true
timeout: auto # 自动计算(根据 URL 数量,每个 URL 1 秒)
# 或手动指定timeout: 600
# threads: 50 # 可选并发线程数httpx 默认 50
# rate-limit: 150 # 可选每秒发送的请求数量httpx 默认 150
# request-timeout: 10 # 可选单个请求的超时时间httpx 默认 10
# retries: 2 # 可选,请求失败重试次数
# 用途:判断 URL 存活,过滤无效 URL
# 输入类型url_fileURL 列表文件)
# 输出:存活的 URL 及其响应信息status, title, server, tech 等)
timeout: auto # 自动计算(每个 URL 1 秒)
# threads: 50 # 并发线程数(默认 50
# rate-limit: 150 # 每秒请求数(默认 150
# request-timeout: 10 # 单个请求超时秒数(默认 10
# retries: 2 # 请求失败重试次数
# ==================== 漏洞扫描 ====================
vuln_scan:
tools:
dalfox_xss:
enabled: true
timeout: auto # 自动计算(根据 endpoints 行数 × 100 秒),或手动指定秒数如 timeout: 600
request-timeout: 10 # Dalfox 单个请求超时时间,对应命令行 --timeout
timeout: auto # 自动计算endpoints 行数 × 100 秒)
request-timeout: 10 # 单个请求超时秒数
only-poc: r # 只输出 POC 结果r: 反射型)
ignore-return: "302,404,403" # 忽略这些返回码
# blind-xss-server: xxx # 可选:盲打 XSS 回连服务地址,需要时再开启
delay: 100 # Dalfox 扫描内部延迟参数
worker: 10 # Dalfox worker 数量
user-agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" # 默认 UA可根据需要修改
ignore-return: "302,404,403" # 忽略返回码
delay: 100 # 扫描内部延迟
worker: 10 # worker 数量
user-agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
# blind-xss-server: xxx # 盲打 XSS 回连服务地址
nuclei:
enabled: true
timeout: auto # 自动计算(根据 endpoints 行数),或手动指定秒数
template-repo-names: # 模板仓库列表(必填,数组写法)对应「Nuclei 模板」中的仓库名
- nuclei-templates # Worker 会自动同步到与 Server 一致的 commit 版本
# - nuclei-custom # 可追加自定义仓库,按顺序依次 -t 传入
concurrency: 25 # 并发数(默认 25
rate-limit: 150 # 每秒请求数限制(默认 150
request-timeout: 5 # 单个请求超时秒数(默认 5
severity: medium,high,critical # 只扫描中高危,降低噪音(逗号分隔)
# tags: cve,rce # 可选:只使用指定标签的模板
timeout: auto # 自动计算(根据 endpoints 行数)
template-repo-names: # 模板仓库列表对应「Nuclei 模板」中的仓库名
- nuclei-templates
# - nuclei-custom # 可追加自定义仓库
concurrency: 25 # 并发数(默认 25
rate-limit: 150 # 每秒请求数限制(默认 150
request-timeout: 5 # 单个请求超时秒数(默认 5
severity: medium,high,critical # 只扫描中高危
# tags: cve,rce # 只使用指定标签的模板

View File

@@ -148,6 +148,71 @@ sequenceDiagram
2. **远程 Worker**:按需拉取对应版本
3. **自动同步**update.sh 统一更新版本号
## Agent 自动更新机制
### 概述
Agent 是运行在每个 Worker 节点上的轻量级心跳服务(~10MB负责上报节点状态和负载信息。当主服务器更新后Agent 需要同步更新以保持版本一致。
### 版本检测流程
```mermaid
sequenceDiagram
participant A as Agent
participant S as Server
participant H as Docker Hub
A->>S: POST /api/workers/{id}/heartbeat/
Note right of A: {"cpu": 50, "mem": 60, "version": "v1.0.8"}
S->>S: 比较 agent_version vs IMAGE_TAG
alt 版本匹配
S->>A: {"status": "ok", "need_update": false}
else 版本不匹配 (远程 Worker)
S->>S: 设置状态为 updating
S->>A: {"status": "ok", "need_update": true}
S-->>H: SSH: docker pull agent:v1.0.19
S-->>A: SSH: 重启 agent 容器
else 版本不匹配 (本地 Worker)
S->>S: 设置状态为 outdated
S->>A: {"status": "ok", "need_update": true}
Note over S: 需用户手动 ./update.sh
end
```
### Worker 状态流转
| 场景 | 状态变化 | 说明 |
|------|---------|------|
| 首次心跳 | `pending/deploying``online` | Agent 启动成功 |
| 远程 Worker 版本不匹配 | `online``updating``online` | 服务端自动 SSH 更新 |
| 远程 Worker 更新失败 | `updating``outdated` | SSH 执行失败 |
| 本地 Worker 版本不匹配 | `online``outdated` | 需手动 update.sh |
| 版本匹配 | `updating/outdated``online` | 恢复正常 |
### 更新触发条件
1. **远程 Worker**:服务端检测到版本不匹配时,自动通过 SSH 执行更新
2. **本地 Worker**:用户执行 `./update.sh`docker-compose 会拉取新镜像并重启
### 防重复机制
使用 Redis 锁防止同一 Worker 在 60 秒内重复触发更新:
```
lock_key = f"agent_update_lock:{worker_id}"
redis.set(lock_key, "1", nx=True, ex=60)
```
### 相关文件
| 文件 | 作用 |
|------|------|
| `backend/apps/engine/views/worker_views.py` | 心跳 API版本检测和更新触发 |
| `backend/scripts/worker-deploy/agent.sh` | Agent 心跳脚本,上报版本号 |
| `backend/scripts/worker-deploy/start-agent.sh` | Agent 启动脚本 |
| `docker/agent/Dockerfile` | Agent 镜像构建,注入 IMAGE_TAG |
## 开发环境配置
### 本地开发测试
@@ -188,7 +253,13 @@ else:
TASK_EXECUTOR_IMAGE = ''
```
## 故障排查
## Agent 自动更新机制
### 概述
Agent 是运行在每个 Worker 节点上的轻量级心跳服务负责上报节点状态和负载信息。当主服务器更新后Agent 需要同步更新以保持版本一致。
### 版本检测流程
### 版本不一致问题
**症状**:任务执行失败,兼容性错误

View File

@@ -297,6 +297,8 @@ export function DeployTerminalDialog({
{isConnected && currentStatus === 'deploying' && '正在部署中,点击查看进度'}
{isConnected && currentStatus === 'online' && '节点运行正常'}
{isConnected && currentStatus === 'offline' && '节点离线,可尝试重新部署'}
{isConnected && currentStatus === 'updating' && '正在自动更新 Agent...'}
{isConnected && currentStatus === 'outdated' && '版本过低,需要更新'}
</div>
{/* 右侧:操作按钮 */}
@@ -334,6 +336,28 @@ export function DeployTerminalDialog({
</button>
)}
{/* 更新中 -> 显示"查看进度" */}
{currentStatus === 'updating' && (
<button
onClick={handleAttach}
className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#e0af68] text-[#1a1b26] hover:bg-[#e0af68]/80 transition-colors"
>
<IconEye className="mr-1.5 h-4 w-4" />
</button>
)}
{/* 版本过低 -> 显示"重新部署" */}
{currentStatus === 'outdated' && (
<button
onClick={handleDeploy}
className="inline-flex items-center px-3 py-1.5 text-sm rounded-md bg-[#f7768e] text-[#1a1b26] hover:bg-[#f7768e]/80 transition-colors"
>
<IconRocket className="mr-1.5 h-4 w-4" />
</button>
)}
{/* 已部署(online/offline) -> 显示"重新部署"和"卸载" */}
{(currentStatus === 'online' || currentStatus === 'offline') && (
<>

View File

@@ -51,6 +51,8 @@ const STATUS_MAP: Record<WorkerStatus, 'online' | 'offline' | 'maintenance' | 'd
offline: 'offline',
pending: 'maintenance',
deploying: 'degraded',
updating: 'degraded',
outdated: 'offline',
}
// 状态中文标签
@@ -59,6 +61,8 @@ const STATUS_LABEL: Record<WorkerStatus, string> = {
offline: '离线',
pending: '等待部署',
deploying: '部署中',
updating: '更新中',
outdated: '版本过低',
}
// 统计卡片组件

View File

@@ -3,7 +3,7 @@
*/
// Worker 状态枚举(前后端统一)
export type WorkerStatus = 'pending' | 'deploying' | 'online' | 'offline'
export type WorkerStatus = 'pending' | 'deploying' | 'online' | 'offline' | 'updating' | 'outdated'
// Worker 节点
export interface WorkerNode {