mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 19:53:11 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bce310a4b0 | ||
|
|
8502daf8a0 | ||
|
|
d0066dd9fc | ||
|
|
3407a98cac | ||
|
|
3d189431fc | ||
|
|
1cbb6350c4 | ||
|
|
20a22f98d0 | ||
|
|
a96ab79891 | ||
|
|
3744a724be | ||
|
|
f63e40fbba | ||
|
|
54573e210a | ||
|
|
6179dd2ed3 | ||
|
|
34ac706fbc | ||
|
|
3ba1ba427e | ||
|
|
6019555729 | ||
|
|
750f52c515 | ||
|
|
bb5ce66a31 | ||
|
|
ac958571a5 |
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -104,6 +104,8 @@ jobs:
|
||||
tags: |
|
||||
${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }}
|
||||
${{ steps.version.outputs.IS_RELEASE == 'true' && format('{0}/{1}:latest', env.IMAGE_PREFIX, matrix.image) || '' }}
|
||||
build-args: |
|
||||
IMAGE_TAG=${{ steps.version.outputs.VERSION }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
|
||||
30
README.md
30
README.md
@@ -1,10 +1,30 @@
|
||||
<h1 align="center">Xingrin - 星环</h1>
|
||||
|
||||
<p align="center">
|
||||
<b>一款现代化的企业级漏洞扫描与资产管理平台</b><br>
|
||||
提供自动化安全检测、资产发现、漏洞管理等功能
|
||||
<b>🛡️ 开源攻击面管理平台 (ASM) | 自动化资产发现与漏洞扫描系统</b>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/yyhuni/xingrin/stargazers"><img src="https://img.shields.io/github/stars/yyhuni/xingrin?style=flat-square&logo=github" alt="GitHub stars"></a>
|
||||
<a href="https://github.com/yyhuni/xingrin/network/members"><img src="https://img.shields.io/github/forks/yyhuni/xingrin?style=flat-square&logo=github" alt="GitHub forks"></a>
|
||||
<a href="https://github.com/yyhuni/xingrin/issues"><img src="https://img.shields.io/github/issues/yyhuni/xingrin?style=flat-square&logo=github" alt="GitHub issues"></a>
|
||||
<a href="https://github.com/yyhuni/xingrin/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-PolyForm%20NC-blue?style=flat-square" alt="License"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#-功能特性">功能特性</a> •
|
||||
<a href="#-快速开始">快速开始</a> •
|
||||
<a href="#-文档">文档</a> •
|
||||
<a href="#-技术栈">技术栈</a> •
|
||||
<a href="#-反馈与贡献">反馈与贡献</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<sub>🔍 关键词: ASM | 攻击面管理 | 漏洞扫描 | 资产发现 | Bug Bounty | 渗透测试 | Nuclei | 子域名枚举 | EASM</sub>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
<b>🌗 明暗模式切换</b>
|
||||
</p>
|
||||
@@ -227,6 +247,12 @@ sudo ./update.sh
|
||||
- 遵守所在地区的法律法规
|
||||
- 承担因滥用产生的一切后果
|
||||
|
||||
## 🌟 Star History
|
||||
|
||||
如果这个项目对你有帮助,请给一个 ⭐ Star 支持一下!
|
||||
|
||||
[](https://star-history.com/#yyhuni/xingrin&Date)
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [PolyForm Noncommercial License 1.0.0](LICENSE) 许可证。
|
||||
|
||||
@@ -14,6 +14,10 @@ import os
|
||||
import sys
|
||||
import requests
|
||||
import logging
|
||||
import urllib3
|
||||
|
||||
# 禁用自签名证书的 SSL 警告(远程 Worker 场景)
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,7 +40,8 @@ def fetch_config_and_setup_django():
|
||||
print(f"[CONFIG] 正在从配置中心获取配置: {config_url}")
|
||||
print(f"[CONFIG] IS_LOCAL={is_local}")
|
||||
try:
|
||||
resp = requests.get(config_url, timeout=10)
|
||||
# verify=False: 远程 Worker 通过 HTTPS 访问时可能使用自签名证书
|
||||
resp = requests.get(config_url, timeout=10, verify=False)
|
||||
resp.raise_for_status()
|
||||
config = resp.json()
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ class WorkerNode(models.Model):
|
||||
('deploying', '部署中'),
|
||||
('online', '在线'),
|
||||
('offline', '离线'),
|
||||
('updating', '更新中'),
|
||||
('outdated', '版本过低'),
|
||||
]
|
||||
|
||||
name = models.CharField(max_length=100, help_text='节点名称')
|
||||
|
||||
@@ -134,5 +134,57 @@ class WorkerService:
|
||||
logger.warning(f"[卸载] Worker {worker_id} 远程卸载异常: {e}")
|
||||
return False, f"远程卸载异常: {str(e)}"
|
||||
|
||||
def execute_remote_command(
|
||||
self,
|
||||
ip_address: str,
|
||||
ssh_port: int,
|
||||
username: str,
|
||||
password: str | None,
|
||||
command: str
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
在远程主机上执行命令
|
||||
|
||||
Args:
|
||||
ip_address: SSH 主机地址
|
||||
ssh_port: SSH 端口
|
||||
username: SSH 用户名
|
||||
password: SSH 密码
|
||||
command: 要执行的命令
|
||||
|
||||
Returns:
|
||||
(success, message) 元组
|
||||
"""
|
||||
if not password:
|
||||
return False, "未配置 SSH 密码"
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
ssh.connect(
|
||||
ip_address,
|
||||
port=ssh_port,
|
||||
username=username,
|
||||
password=password,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
stdin, stdout, stderr = ssh.exec_command(command, timeout=120)
|
||||
exit_status = stdout.channel.recv_exit_status()
|
||||
|
||||
ssh.close()
|
||||
|
||||
if exit_status == 0:
|
||||
return True, stdout.read().decode().strip()
|
||||
else:
|
||||
error = stderr.read().decode().strip()
|
||||
return False, error
|
||||
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
__all__ = ["WorkerService"]
|
||||
|
||||
@@ -118,8 +118,36 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def heartbeat(self, request, pk=None):
|
||||
"""接收心跳上报(写 Redis,首次心跳更新部署状态)"""
|
||||
"""
|
||||
接收心跳上报(写 Redis,首次心跳更新部署状态,检查版本)
|
||||
|
||||
请求体:
|
||||
{
|
||||
"cpu_percent": 50.0,
|
||||
"memory_percent": 60.0,
|
||||
"version": "v1.0.9"
|
||||
}
|
||||
|
||||
返回:
|
||||
{
|
||||
"status": "ok",
|
||||
"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
|
||||
|
||||
worker = self.get_object()
|
||||
info = request.data if request.data else {}
|
||||
@@ -134,7 +162,122 @@ class WorkerNodeViewSet(viewsets.ModelViewSet):
|
||||
worker.status = 'online'
|
||||
worker.save(update_fields=['status'])
|
||||
|
||||
return Response({'status': 'ok'})
|
||||
# 3. 版本检查:比较 agent 版本与 server 版本
|
||||
agent_version = info.get('version', '')
|
||||
server_version = settings.IMAGE_TAG # Server 当前版本
|
||||
need_update = False
|
||||
|
||||
if agent_version and agent_version != 'unknown':
|
||||
# 版本不匹配时通知 agent 更新
|
||||
need_update = agent_version != server_version
|
||||
if need_update:
|
||||
logger.info(
|
||||
f"Worker {worker.name} 版本不匹配: agent={agent_version}, server={server_version}"
|
||||
)
|
||||
|
||||
# 远程 Worker:服务端主动通过 SSH 触发更新
|
||||
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',
|
||||
'need_update': need_update,
|
||||
'server_version': server_version
|
||||
})
|
||||
|
||||
def _trigger_remote_agent_update(self, worker, target_version: str):
|
||||
"""
|
||||
通过 SSH 触发远程 agent 更新(后台执行,不阻塞心跳响应)
|
||||
|
||||
使用 Redis 锁防止重复触发(同一 worker 60秒内只触发一次)
|
||||
"""
|
||||
import redis
|
||||
from django.conf import settings as django_settings
|
||||
|
||||
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秒过期,防止重复触发)
|
||||
if not redis_client.set(lock_key, "1", nx=True, ex=60):
|
||||
logger.debug(f"Worker {worker.name} 更新已在进行中,跳过")
|
||||
return
|
||||
|
||||
# 获取锁成功,设置状态为 updating
|
||||
self._set_worker_status(worker.id, 'updating')
|
||||
|
||||
# 提取数据避免后台线程访问 ORM
|
||||
worker_id = worker.id
|
||||
worker_name = worker.name
|
||||
ip_address = worker.ip_address
|
||||
ssh_port = worker.ssh_port
|
||||
username = worker.username
|
||||
password = worker.password
|
||||
|
||||
def _async_update():
|
||||
try:
|
||||
logger.info(f"开始远程更新 Worker {worker_name} 到 {target_version}")
|
||||
|
||||
# 构建更新命令:拉取新镜像并重启 agent
|
||||
docker_user = getattr(django_settings, 'DOCKER_USER', 'yyhuni')
|
||||
update_cmd = f'''
|
||||
docker pull {docker_user}/xingrin-agent:{target_version} && \
|
||||
docker stop xingrin-agent 2>/dev/null || true && \
|
||||
docker rm xingrin-agent 2>/dev/null || true && \
|
||||
docker run -d --pull=always \
|
||||
--name xingrin-agent \
|
||||
--restart always \
|
||||
-e HEARTBEAT_API_URL="https://{django_settings.PUBLIC_HOST}" \
|
||||
-e WORKER_ID="{worker_id}" \
|
||||
-e IMAGE_TAG="{target_version}" \
|
||||
-v /proc:/host/proc:ro \
|
||||
{docker_user}/xingrin-agent:{target_version}
|
||||
'''
|
||||
|
||||
success, message = self.worker_service.execute_remote_command(
|
||||
ip_address=ip_address,
|
||||
ssh_port=ssh_port,
|
||||
username=username,
|
||||
password=password,
|
||||
command=update_cmd
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
# 后台执行,不阻塞心跳响应
|
||||
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):
|
||||
|
||||
@@ -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: # 只保留指定扩展名的 URL(如:php,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_file(URL 列表文件)
|
||||
# 输出:存活的 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 # 只使用指定标签的模板
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
import logging
|
||||
import time
|
||||
import requests
|
||||
import urllib3
|
||||
from .models import Notification, NotificationSettings
|
||||
from .types import NotificationLevel, NotificationCategory
|
||||
from .repositories import DjangoNotificationRepository, NotificationSettingsRepository
|
||||
|
||||
# 禁用自签名证书的 SSL 警告(远程 Worker 回调场景)
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -314,7 +318,8 @@ def _push_via_api_callback(notification: Notification, server_url: str) -> None:
|
||||
'created_at': notification.created_at.isoformat()
|
||||
}
|
||||
|
||||
resp = requests.post(callback_url, json=data, timeout=5)
|
||||
# verify=False: 远程 Worker 回调 Server 时可能使用自签名证书
|
||||
resp = requests.post(callback_url, json=data, timeout=5, verify=False)
|
||||
resp.raise_for_status()
|
||||
|
||||
logger.debug(f"通知回调推送成功 - ID: {notification.id}")
|
||||
|
||||
@@ -51,6 +51,18 @@ class ServiceSet:
|
||||
)
|
||||
|
||||
|
||||
def _sanitize_string(value: str) -> str:
|
||||
"""
|
||||
清理字符串中的 NUL 字符和其他不可打印字符
|
||||
|
||||
PostgreSQL 不允许字符串字段包含 NUL (0x00) 字符
|
||||
"""
|
||||
if not value:
|
||||
return value
|
||||
# 移除 NUL 字符
|
||||
return value.replace('\x00', '')
|
||||
|
||||
|
||||
def _parse_and_validate_line(line: str) -> Optional[dict]:
|
||||
"""
|
||||
解析并验证单行 httpx JSON 输出
|
||||
@@ -64,6 +76,9 @@ def _parse_and_validate_line(line: str) -> Optional[dict]:
|
||||
只返回存活的 URL(2xx/3xx 状态码)
|
||||
"""
|
||||
try:
|
||||
# 清理 NUL 字符后再解析 JSON
|
||||
line = _sanitize_string(line)
|
||||
|
||||
# 解析 JSON
|
||||
try:
|
||||
line_data = json.loads(line)
|
||||
@@ -87,16 +102,16 @@ def _parse_and_validate_line(line: str) -> Optional[dict]:
|
||||
# 只保存存活的 URL(2xx 或 3xx)
|
||||
if status_code and (200 <= status_code < 400):
|
||||
return {
|
||||
'url': url,
|
||||
'host': line_data.get('host', ''), # 从 httpx 输出中提取 host
|
||||
'url': _sanitize_string(url),
|
||||
'host': _sanitize_string(line_data.get('host', '')),
|
||||
'status_code': status_code,
|
||||
'title': line_data.get('title', ''),
|
||||
'title': _sanitize_string(line_data.get('title', '')),
|
||||
'content_length': line_data.get('content_length', 0),
|
||||
'content_type': line_data.get('content_type', ''),
|
||||
'webserver': line_data.get('webserver', ''),
|
||||
'location': line_data.get('location', ''),
|
||||
'content_type': _sanitize_string(line_data.get('content_type', '')),
|
||||
'webserver': _sanitize_string(line_data.get('webserver', '')),
|
||||
'location': _sanitize_string(line_data.get('location', '')),
|
||||
'tech': line_data.get('tech', []),
|
||||
'body_preview': line_data.get('body_preview', ''),
|
||||
'body_preview': _sanitize_string(line_data.get('body_preview', '')),
|
||||
'vhost': line_data.get('vhost', False),
|
||||
}
|
||||
else:
|
||||
@@ -104,7 +119,7 @@ def _parse_and_validate_line(line: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error("解析行数据异常: %s - 数据: %s", e, line[:100])
|
||||
logger.error("解析行数据异常: %s - 数据: %s", e, line[:100] if line else 'empty')
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
from urllib import request as urllib_request
|
||||
from urllib import parse as urllib_parse
|
||||
@@ -89,7 +90,12 @@ def ensure_wordlist_local(wordlist_name: str) -> str:
|
||||
logger.info("从后端下载字典: %s -> %s", download_url, local_path)
|
||||
|
||||
try:
|
||||
with urllib_request.urlopen(download_url) as resp:
|
||||
# 创建不验证 SSL 的上下文(远程 Worker 可能使用自签名证书)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
with urllib_request.urlopen(download_url, context=ssl_context) as resp:
|
||||
if resp.status != 200:
|
||||
raise RuntimeError(f"下载字典失败,HTTP {resp.status}")
|
||||
data = resp.read()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
# ============================================
|
||||
# XingRin Agent
|
||||
# 用途:心跳上报 + 负载监控
|
||||
# 用途:心跳上报 + 负载监控 + 版本检查
|
||||
# 适用:远程 VPS 或 Docker 容器内
|
||||
# ============================================
|
||||
|
||||
@@ -17,6 +17,9 @@ SRC_DIR="${MARKER_DIR}/src"
|
||||
ENV_FILE="${SRC_DIR}/backend/.env"
|
||||
INTERVAL=${AGENT_INTERVAL:-3}
|
||||
|
||||
# Agent 版本(从环境变量获取,由 Docker 镜像构建时注入)
|
||||
AGENT_VERSION="${IMAGE_TAG:-unknown}"
|
||||
|
||||
# 颜色定义
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
@@ -90,7 +93,7 @@ register_worker() {
|
||||
EOF
|
||||
)
|
||||
|
||||
RESPONSE=$(curl -s -X POST \
|
||||
RESPONSE=$(curl -k -s -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$REGISTER_DATA" \
|
||||
"${API_URL}/api/workers/register/" 2>/dev/null)
|
||||
@@ -172,22 +175,72 @@ while true; do
|
||||
fi
|
||||
|
||||
# 构建 JSON 数据(使用数值而非字符串,便于比较和排序)
|
||||
# 包含版本号,供 Server 端检查版本一致性
|
||||
JSON_DATA=$(cat <<EOF
|
||||
{
|
||||
"cpu_percent": $CPU_PERCENT,
|
||||
"memory_percent": $MEM_PERCENT
|
||||
"memory_percent": $MEM_PERCENT,
|
||||
"version": "$AGENT_VERSION"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
|
||||
# 发送心跳
|
||||
RESPONSE=$(curl -k -s -o /dev/null -w "%{http_code}" -X POST \
|
||||
# 发送心跳,获取响应内容
|
||||
RESPONSE_FILE=$(mktemp)
|
||||
HTTP_CODE=$(curl -k -s -o "$RESPONSE_FILE" -w "%{http_code}" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$JSON_DATA" \
|
||||
"${API_URL}/api/workers/${WORKER_ID}/heartbeat/" 2>/dev/null || echo "000")
|
||||
RESPONSE_BODY=$(cat "$RESPONSE_FILE" 2>/dev/null)
|
||||
rm -f "$RESPONSE_FILE"
|
||||
|
||||
if [ "$RESPONSE" != "200" ] && [ "$RESPONSE" != "201" ]; then
|
||||
log "${YELLOW}心跳发送失败 (HTTP $RESPONSE)${NC}"
|
||||
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
|
||||
log "${YELLOW}心跳发送失败 (HTTP $HTTP_CODE)${NC}"
|
||||
else
|
||||
# 检查是否需要更新
|
||||
NEED_UPDATE=$(echo "$RESPONSE_BODY" | grep -oE '"need_update":\s*(true|false)' | grep -oE '(true|false)')
|
||||
if [ "$NEED_UPDATE" = "true" ]; then
|
||||
SERVER_VERSION=$(echo "$RESPONSE_BODY" | grep -oE '"server_version":\s*"[^"]+"' | sed 's/.*"\([^"]*\)"$/\1/')
|
||||
log "${YELLOW}检测到版本不匹配: Agent=$AGENT_VERSION, Server=$SERVER_VERSION${NC}"
|
||||
log "${GREEN}正在自动更新...${NC}"
|
||||
|
||||
# 执行自动更新
|
||||
if [ "$RUN_MODE" = "container" ]; then
|
||||
# 容器模式:通知外部重启(退出后由 docker-compose restart policy 重启)
|
||||
log "容器模式:退出以触发重启更新"
|
||||
exit 0
|
||||
else
|
||||
# 远程模式:拉取新镜像并重启 agent 容器
|
||||
log "远程模式:更新 agent 镜像..."
|
||||
DOCKER_USER="${DOCKER_USER:-yyhuni}"
|
||||
NEW_IMAGE="${DOCKER_USER}/xingrin-agent:${SERVER_VERSION}"
|
||||
|
||||
# 拉取新镜像
|
||||
if $DOCKER_CMD pull "$NEW_IMAGE" 2>/dev/null; then
|
||||
log "${GREEN}镜像拉取成功: $NEW_IMAGE${NC}"
|
||||
|
||||
# 停止当前容器并用新镜像重启
|
||||
CONTAINER_NAME="xingrin-agent"
|
||||
$DOCKER_CMD stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
$DOCKER_CMD rm "$CONTAINER_NAME" 2>/dev/null || true
|
||||
|
||||
# 重新启动(使用相同的环境变量)
|
||||
$DOCKER_CMD run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart unless-stopped \
|
||||
-e HEARTBEAT_API_URL="$API_URL" \
|
||||
-e WORKER_ID="$WORKER_ID" \
|
||||
-e IMAGE_TAG="$SERVER_VERSION" \
|
||||
-v /proc:/host/proc:ro \
|
||||
"$NEW_IMAGE"
|
||||
|
||||
log "${GREEN}Agent 已更新到 $SERVER_VERSION${NC}"
|
||||
exit 0
|
||||
else
|
||||
log "${RED}镜像拉取失败: $NEW_IMAGE${NC}"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 休眠
|
||||
|
||||
@@ -60,12 +60,14 @@ start_agent() {
|
||||
log_info "=========================================="
|
||||
|
||||
log_info "启动 agent 容器..."
|
||||
# --pull=missing 只在本地没有镜像时才拉取,避免意外更新
|
||||
# --pull=missing: 本地没有镜像时才拉取
|
||||
# 版本更新由服务端通过 SSH 显式 docker pull 触发
|
||||
docker run -d --pull=missing \
|
||||
--name ${CONTAINER_NAME} \
|
||||
--restart always \
|
||||
-e SERVER_URL="${PRESET_SERVER_URL}" \
|
||||
-e WORKER_ID="${PRESET_WORKER_ID}" \
|
||||
-e IMAGE_TAG="${IMAGE_TAG}" \
|
||||
-v /proc:/host/proc:ro \
|
||||
${IMAGE}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
# ============================================
|
||||
# XingRin Agent - 轻量心跳上报镜像
|
||||
# 用途:心跳上报 + 负载监控
|
||||
# 用途:心跳上报 + 负载监控 + 版本检查
|
||||
# 基础镜像:Alpine Linux (~5MB)
|
||||
# 最终大小:~10MB
|
||||
# ============================================
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
# 构建参数:版本号
|
||||
ARG IMAGE_TAG=unknown
|
||||
|
||||
# 安装必要工具
|
||||
RUN apk add --no-cache \
|
||||
bash \
|
||||
@@ -17,6 +20,9 @@ RUN apk add --no-cache \
|
||||
COPY backend/scripts/worker-deploy/agent.sh /app/agent.sh
|
||||
RUN chmod +x /app/agent.sh
|
||||
|
||||
# 将版本号写入环境变量(运行时可用)
|
||||
ENV IMAGE_TAG=${IMAGE_TAG}
|
||||
|
||||
# 工作目录
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -54,19 +54,19 @@ services:
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
# Agent:心跳上报 + 负载监控
|
||||
# Agent:心跳上报 + 负载监控 + 版本检查
|
||||
agent:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/worker/Dockerfile
|
||||
dockerfile: docker/agent/Dockerfile
|
||||
args:
|
||||
IMAGE_TAG: ${IMAGE_TAG:-dev}
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- SERVER_URL=http://server:8888
|
||||
- WORKER_NAME=本地节点
|
||||
- IS_LOCAL=true
|
||||
command: bash /app/backend/scripts/worker-deploy/agent.sh
|
||||
- IMAGE_TAG=${IMAGE_TAG:-dev}
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -72,6 +72,7 @@ services:
|
||||
- SERVER_URL=http://server:8888
|
||||
- WORKER_NAME=本地节点
|
||||
- IS_LOCAL=true
|
||||
- IMAGE_TAG=${IMAGE_TAG}
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -135,6 +135,7 @@ if [ "$DEV_MODE" = true ]; then
|
||||
fi
|
||||
else
|
||||
# 生产模式:拉取 Docker Hub 镜像
|
||||
# pull 后 up -d 会自动检测镜像变化并重建容器
|
||||
if [ "$WITH_FRONTEND" = true ]; then
|
||||
echo -e "${CYAN}[PULL]${NC} 拉取最新镜像..."
|
||||
${COMPOSE_CMD} ${COMPOSE_ARGS} pull
|
||||
|
||||
@@ -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 需要同步更新以保持版本一致。
|
||||
|
||||
### 版本检测流程
|
||||
|
||||
### 版本不一致问题
|
||||
**症状**:任务执行失败,兼容性错误
|
||||
|
||||
@@ -29,9 +29,10 @@ import { AuthLayout } from "@/components/auth/auth-layout"
|
||||
|
||||
// 定义页面的元数据信息,用于 SEO 优化
|
||||
export const metadata: Metadata = {
|
||||
title: "XingRin - 星环", // 页面标题
|
||||
description: "XingRin - 星环", // 页面描述
|
||||
generator: "XingRin", // 生成器标识
|
||||
title: "星环 (Xingrin) - 攻击面管理平台 | ASM",
|
||||
description: "星环 - 攻击面管理平台 (ASM),提供自动化资产发现、漏洞扫描、子域名枚举、端口扫描等功能",
|
||||
keywords: ["ASM", "攻击面管理", "漏洞扫描", "资产发现", "Bug Bounty", "渗透测试", "Nuclei", "子域名枚举", "安全工具"],
|
||||
generator: "Xingrin",
|
||||
}
|
||||
|
||||
// 使用思源黑体 + 系统字体回退,完全本地加载
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Metadata } from "next"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "登录 - XingRin - 星环",
|
||||
description: "登录到 XingRin - 星环",
|
||||
title: "登录 - 星环 | 攻击面管理平台",
|
||||
description: "星环 (Xingrin) - 攻击面管理平台 (ASM),提供自动化资产发现与漏洞扫描",
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function LoginPage() {
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">XingRin - 星环</h1>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
一站式安全扫描平台
|
||||
攻击面管理平台
|
||||
</p>
|
||||
</div>
|
||||
<Field>
|
||||
|
||||
@@ -88,9 +88,9 @@ export function createDirectoryColumns({
|
||||
// URL 列
|
||||
{
|
||||
accessorKey: "url",
|
||||
size: 300,
|
||||
size: 400,
|
||||
minSize: 200,
|
||||
maxSize: 400,
|
||||
maxSize: 500,
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -110,9 +110,25 @@ export function createIPAddressColumns(params: {
|
||||
<TruncatedCell key={index} value={host} maxLength="host" mono />
|
||||
))}
|
||||
{hasMore && (
|
||||
<Badge variant="secondary" className="text-xs w-fit">
|
||||
+{hosts.length - 3} more
|
||||
</Badge>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Badge variant="secondary" className="text-xs w-fit cursor-pointer hover:bg-muted">
|
||||
+{hosts.length - 3} more
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">All Hosts ({hosts.length})</h4>
|
||||
<div className="flex flex-col gap-1 max-h-48 overflow-y-auto">
|
||||
{hosts.map((host, index) => (
|
||||
<span key={index} className="text-sm font-mono break-all">
|
||||
{host}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -186,7 +202,7 @@ export function createIPAddressColumns(params: {
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Badge variant="outline" className="text-xs cursor-pointer hover:bg-muted">
|
||||
+{ports.length - 8}
|
||||
+{ports.length - 8} more
|
||||
</Badge>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-3">
|
||||
|
||||
@@ -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') && (
|
||||
<>
|
||||
|
||||
@@ -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: '版本过低',
|
||||
}
|
||||
|
||||
// 统计卡片组件
|
||||
|
||||
@@ -14,7 +14,7 @@ import { cn } from "@/lib/utils"
|
||||
* 预设的截断长度配置
|
||||
*/
|
||||
export const TRUNCATE_LENGTHS = {
|
||||
url: 35,
|
||||
url: 50,
|
||||
title: 25,
|
||||
location: 20,
|
||||
webServer: 20,
|
||||
@@ -142,7 +142,7 @@ export function TruncatedUrlCell({
|
||||
: value
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 w-[280px] min-w-[280px]">
|
||||
<div className="flex items-center gap-1 w-[380px] min-w-[380px]">
|
||||
<span className={cn("text-sm font-mono truncate", className)}>
|
||||
{displayText}
|
||||
</span>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user