mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 19:53:11 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba1ba427e | ||
|
|
6019555729 | ||
|
|
750f52c515 | ||
|
|
bb5ce66a31 | ||
|
|
ac958571a5 | ||
|
|
bcb321f883 | ||
|
|
fd3cdf8033 | ||
|
|
f3f9718df2 | ||
|
|
984c34dbca | ||
|
|
e9dcbf510d | ||
|
|
65deb8c5d0 | ||
|
|
5a93ad878c | ||
|
|
51f25d0976 | ||
|
|
fe1579e7fb | ||
|
|
ef117d2245 | ||
|
|
39cea5a918 | ||
|
|
0d477ce269 | ||
|
|
1bb6e90c3d | ||
|
|
9004c77031 | ||
|
|
71de0b4b1b | ||
|
|
1ef1f9709e | ||
|
|
3323bd2a4f | ||
|
|
df602dd1ae |
14
.github/workflows/docker-build.yml
vendored
14
.github/workflows/docker-build.yml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
IMAGE_PREFIX: yyhuni
|
||||
|
||||
permissions:
|
||||
contents: write # 允许修改仓库内容
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -27,18 +27,23 @@ jobs:
|
||||
- image: xingrin-server
|
||||
dockerfile: docker/server/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- image: xingrin-frontend
|
||||
dockerfile: docker/frontend/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64 # ARM64 构建时 Next.js 在 QEMU 下会崩溃
|
||||
- image: xingrin-worker
|
||||
dockerfile: docker/worker/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- image: xingrin-nginx
|
||||
dockerfile: docker/nginx/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- image: xingrin-agent
|
||||
dockerfile: docker/agent/Dockerfile
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -48,7 +53,6 @@ jobs:
|
||||
run: |
|
||||
echo "=== Before cleanup ==="
|
||||
df -h
|
||||
# 删除不需要的大型软件包
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /usr/local/lib/android
|
||||
sudo rm -rf /opt/ghc
|
||||
@@ -95,11 +99,13 @@ jobs:
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platforms }}
|
||||
push: true
|
||||
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
|
||||
@@ -108,7 +114,7 @@ jobs:
|
||||
# 所有镜像构建成功后,更新 VERSION 文件
|
||||
update-version:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build # 等待所有 build job 完成
|
||||
needs: build
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
137
README.md
137
README.md
@@ -31,6 +31,7 @@
|
||||
- [🔄 版本管理](./docs/version-management.md) - Git Tag 驱动的自动化版本管理系统
|
||||
- [📦 Nuclei 模板架构](./docs/nuclei-template-architecture.md) - 模板仓库的存储与同步
|
||||
- [📖 字典文件架构](./docs/wordlist-architecture.md) - 字典文件的存储与同步
|
||||
- [🔍 扫描流程架构](./docs/scan-flow-architecture.md) - 完整扫描流程与工具编排
|
||||
|
||||
|
||||
---
|
||||
@@ -48,6 +49,54 @@
|
||||
- **自定义流程** - YAML 配置扫描流程,灵活编排
|
||||
- **定时扫描** - Cron 表达式配置,自动化周期扫描
|
||||
|
||||
#### 扫描流程架构
|
||||
|
||||
完整的扫描流程包括:子域名发现、端口扫描、站点发现、URL 收集、目录扫描、漏洞扫描等阶段
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
START["开始扫描"]
|
||||
|
||||
subgraph STAGE1["阶段 1: 资产发现"]
|
||||
direction TB
|
||||
SUB["子域名发现<br/>subfinder, amass, puredns"]
|
||||
PORT["端口扫描<br/>naabu"]
|
||||
SITE["站点识别<br/>httpx"]
|
||||
SUB --> PORT --> SITE
|
||||
end
|
||||
|
||||
subgraph STAGE2["阶段 2: 深度分析"]
|
||||
direction TB
|
||||
URL["URL 收集<br/>waymore, katana"]
|
||||
DIR["目录扫描<br/>ffuf"]
|
||||
end
|
||||
|
||||
subgraph STAGE3["阶段 3: 漏洞检测"]
|
||||
VULN["漏洞扫描<br/>nuclei, dalfox"]
|
||||
end
|
||||
|
||||
FINISH["扫描完成"]
|
||||
|
||||
START --> STAGE1
|
||||
SITE --> STAGE2
|
||||
STAGE2 --> STAGE3
|
||||
STAGE3 --> FINISH
|
||||
|
||||
style START fill:#34495e,stroke:#2c3e50,stroke-width:2px,color:#fff
|
||||
style FINISH fill:#27ae60,stroke:#229954,stroke-width:2px,color:#fff
|
||||
style STAGE1 fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
|
||||
style STAGE2 fill:#9b59b6,stroke:#8e44ad,stroke-width:2px,color:#fff
|
||||
style STAGE3 fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff
|
||||
style SUB fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
|
||||
style PORT fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
|
||||
style SITE fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
|
||||
style URL fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
|
||||
style DIR fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
|
||||
style VULN fill:#f0b27a,stroke:#e67e22,stroke-width:1px,color:#fff
|
||||
```
|
||||
|
||||
详细说明请查看 [扫描流程架构文档](./docs/scan-flow-architecture.md)
|
||||
|
||||
### 🖥️ 分布式架构
|
||||
- **多节点扫描** - 支持部署多个 Worker 节点,横向扩展扫描能力
|
||||
- **本地节点** - 零配置,安装即自动注册本地 Docker Worker
|
||||
@@ -56,34 +105,41 @@
|
||||
- **节点监控** - 实时心跳检测,CPU/内存/磁盘状态监控
|
||||
- **断线重连** - 节点离线自动检测,恢复后自动重新接入
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 主服务器 (Master) │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||
│ │ Next.js │ │ Django │ │ Postgres│ │ Redis │ │
|
||||
│ │ 前端 │ │ 后端 │ │ 数据库 │ │ 缓存 │ │
|
||||
│ └─────────┘ └────┬────┘ └─────────┘ └─────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────┴─────┐ │
|
||||
│ │ 任务调度器 │ │
|
||||
│ │ Scheduler │ │
|
||||
│ └─────┬─────┘ │
|
||||
└────────────────────┼────────────────────────────────────────────┘
|
||||
│
|
||||
┌────────────┼────────────┐
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────┐ ┌───────────┐ ┌───────────┐
|
||||
│ Worker 1 │ │ Worker 2 │ │ Worker N │
|
||||
│ (本地) │ │ (远程) │ │ (远程) │
|
||||
├───────────┤ ├───────────┤ ├───────────┤
|
||||
│ • Nuclei │ │ • Nuclei │ │ • Nuclei │
|
||||
│ • httpx │ │ • httpx │ │ • httpx │
|
||||
│ • naabu │ │ • naabu │ │ • naabu │
|
||||
│ • ... │ │ • ... │ │ • ... │
|
||||
├───────────┤ ├───────────┤ ├───────────┤
|
||||
│ 心跳上报 │ │ 心跳上报 │ │ 心跳上报 │
|
||||
└───────────┘ └───────────┘ └───────────┘
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph MASTER["主服务器 (Master Server)"]
|
||||
direction TB
|
||||
|
||||
REDIS["Redis 负载缓存"]
|
||||
|
||||
subgraph SCHEDULER["任务调度器 (Task Distributor)"]
|
||||
direction TB
|
||||
SUBMIT["接收扫描任务"]
|
||||
SELECT["负载感知选择"]
|
||||
DISPATCH["智能分发"]
|
||||
|
||||
SUBMIT --> SELECT
|
||||
SELECT --> DISPATCH
|
||||
end
|
||||
|
||||
REDIS -.负载数据.-> SELECT
|
||||
end
|
||||
|
||||
subgraph WORKERS["Worker 节点集群"]
|
||||
direction TB
|
||||
|
||||
W1["Worker 1 (本地)<br/>CPU: 45% | MEM: 60%"]
|
||||
W2["Worker 2 (远程)<br/>CPU: 30% | MEM: 40%"]
|
||||
W3["Worker N (远程)<br/>CPU: 90% | MEM: 85%"]
|
||||
end
|
||||
|
||||
DISPATCH -->|任务分发| W1
|
||||
DISPATCH -->|任务分发| W2
|
||||
DISPATCH -->|高负载跳过| W3
|
||||
|
||||
W1 -.心跳上报.-> REDIS
|
||||
W2 -.心跳上报.-> REDIS
|
||||
W3 -.心跳上报.-> REDIS
|
||||
```
|
||||
|
||||
### 📊 可视化界面
|
||||
@@ -100,27 +156,12 @@
|
||||
- **数据库**: PostgreSQL + Redis
|
||||
- **部署**: Docker + Nginx
|
||||
|
||||
### 🔧 内置扫描工具
|
||||
|
||||
| 类别 | 工具 |
|
||||
|------|------|
|
||||
| 子域名爆破 | puredns, massdns, dnsgen |
|
||||
| 被动发现 | subfinder, amass, assetfinder, Sublist3r |
|
||||
| 端口扫描 | naabu |
|
||||
| 站点发现 | httpx |
|
||||
| 目录扫描 | ffuf |
|
||||
| 爬虫 | katana |
|
||||
| 被动URL收集 | waymore, uro |
|
||||
| 漏洞扫描 | nuclei, dalfox |
|
||||
|
||||
---
|
||||
|
||||
## 📦 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **操作系统**: Ubuntu 20.04+ / Debian 11+ (推荐)
|
||||
- **硬件**: 2核 4G 内存起步,10GB+ 磁盘空间
|
||||
- **硬件**: 2核 4G 内存起步,20GB+ 磁盘空间
|
||||
|
||||
### 一键安装
|
||||
|
||||
@@ -131,14 +172,11 @@ cd xingrin
|
||||
|
||||
# 安装并启动(生产模式)
|
||||
sudo ./install.sh
|
||||
|
||||
# 开发模式
|
||||
sudo ./install.sh --dev
|
||||
```
|
||||
|
||||
### 访问服务
|
||||
|
||||
- **Web 界面**: `https://localhost` 或 `http://localhost`
|
||||
- **Web 界面**: `https://localhost`
|
||||
|
||||
### 常用命令
|
||||
|
||||
@@ -158,9 +196,6 @@ sudo ./uninstall.sh
|
||||
# 更新
|
||||
sudo ./update.sh
|
||||
```
|
||||
## 日志
|
||||
- 项目日志:/opt/xingrin/logs 下存储了这个项目的运行日志信息,error文件存储了错误相关信息,xingrin.log存储了包括错误在内的所有项目日志
|
||||
- 工具调用日志:/opt/xingrin/results 下存储了工具的运行结果日志,比如naabu,httpx等的结果调用日志
|
||||
|
||||
## 🤝 反馈与贡献
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -27,3 +27,10 @@ vulnerabilities_saved = Signal()
|
||||
# - worker_name: str Worker 名称
|
||||
# - message: str 失败原因
|
||||
worker_delete_failed = Signal()
|
||||
|
||||
# 所有 Worker 高负载信号
|
||||
# 参数:
|
||||
# - worker_name: str 被选中的 Worker 名称
|
||||
# - cpu: float CPU 使用率
|
||||
# - mem: float 内存使用率
|
||||
all_workers_high_load = Signal()
|
||||
|
||||
@@ -241,8 +241,9 @@ class WorkerDeployConsumer(AsyncWebsocketConsumer):
|
||||
}))
|
||||
return
|
||||
|
||||
django_host = f"{public_host}:{server_port}" # Django / 心跳上报使用
|
||||
heartbeat_api_url = f"http://{django_host}" # 基础 URL,agent 会加 /api/...
|
||||
# 远程 Worker 通过 nginx HTTPS 访问(nginx 反代到后端 8888)
|
||||
# 使用 https://{PUBLIC_HOST} 而不是直连 8888 端口
|
||||
heartbeat_api_url = f"https://{public_host}" # 基础 URL,agent 会加 /api/...
|
||||
|
||||
session_name = f'xingrin_deploy_{self.worker_id}'
|
||||
remote_script_path = '/tmp/xingrin_deploy.sh'
|
||||
|
||||
@@ -153,11 +153,30 @@ class TaskDistributor:
|
||||
else:
|
||||
scored_workers.append((worker, score, cpu, mem))
|
||||
|
||||
# 降级策略:如果没有正常负载的,使用高负载中最低的
|
||||
# 降级策略:如果没有正常负载的,等待后重新选择
|
||||
if not scored_workers:
|
||||
if high_load_workers:
|
||||
logger.warning("所有 Worker 高负载,降级选择负载最低的")
|
||||
scored_workers = high_load_workers
|
||||
# 高负载时先等待,给系统喘息时间(默认 60 秒)
|
||||
high_load_wait = getattr(settings, 'HIGH_LOAD_WAIT_SECONDS', 60)
|
||||
logger.warning("所有 Worker 高负载,等待 %d 秒后重试...", high_load_wait)
|
||||
time.sleep(high_load_wait)
|
||||
|
||||
# 重新选择(递归调用,可能负载已降下来)
|
||||
# 为避免无限递归,这里直接使用高负载中最低的
|
||||
high_load_workers.sort(key=lambda x: x[1])
|
||||
best_worker, _, cpu, mem = high_load_workers[0]
|
||||
|
||||
# 发送高负载通知
|
||||
from apps.common.signals import all_workers_high_load
|
||||
all_workers_high_load.send(
|
||||
sender=self.__class__,
|
||||
worker_name=best_worker.name,
|
||||
cpu=cpu,
|
||||
mem=mem
|
||||
)
|
||||
|
||||
logger.info("选择 Worker: %s (CPU: %.1f%%, MEM: %.1f%%)", best_worker.name, cpu, mem)
|
||||
return best_worker
|
||||
else:
|
||||
logger.warning("没有可用的 Worker")
|
||||
return None
|
||||
@@ -213,9 +232,9 @@ class TaskDistributor:
|
||||
network_arg = f"--network {settings.DOCKER_NETWORK_NAME}"
|
||||
server_url = f"http://server:{settings.SERVER_PORT}"
|
||||
else:
|
||||
# 远程:无需指定网络,使用公网地址
|
||||
# 远程:通过 Nginx 反向代理访问(HTTPS,不直连 8888 端口)
|
||||
network_arg = ""
|
||||
server_url = f"http://{settings.PUBLIC_HOST}:{settings.SERVER_PORT}"
|
||||
server_url = f"https://{settings.PUBLIC_HOST}"
|
||||
|
||||
# 挂载路径(所有节点统一使用固定路径)
|
||||
host_results_dir = settings.HOST_RESULTS_DIR # /opt/xingrin/results
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -116,7 +116,7 @@ class NucleiTemplateRepoViewSet(viewsets.ModelViewSet):
|
||||
return Response({"message": str(exc)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.error("刷新 Nuclei 模板仓库失败: %s", exc, exc_info=True)
|
||||
return Response({"message": "刷新仓库失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
return Response({"message": f"刷新仓库失败: {exc}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
return Response({"message": "刷新成功", "result": result}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -118,8 +118,25 @@ 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"
|
||||
}
|
||||
"""
|
||||
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 +151,96 @@ 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 触发更新
|
||||
# 旧版 agent 不会解析 need_update,所以需要服务端主动推送
|
||||
if not worker.is_local and worker.ip_address:
|
||||
self._trigger_remote_agent_update(worker, server_version)
|
||||
|
||||
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_client = redis.from_url(django_settings.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
|
||||
|
||||
# 提取数据避免后台线程访问 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} 远程更新成功")
|
||||
else:
|
||||
logger.warning(f"Worker {worker_name} 远程更新失败: {message}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Worker {worker_name} 远程更新异常: {e}")
|
||||
finally:
|
||||
# 释放锁
|
||||
redis_client.delete(lock_key)
|
||||
|
||||
# 后台执行,不阻塞心跳响应
|
||||
threading.Thread(target=_async_update, daemon=True).start()
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def register(self, request):
|
||||
|
||||
@@ -372,19 +372,17 @@ def port_scan_flow(
|
||||
端口扫描 Flow
|
||||
|
||||
主要功能:
|
||||
1. 扫描目标域名的开放端口(核心目标)
|
||||
2. 发现域名对应的 IP 地址(附带产物)
|
||||
3. 保存 IP 和端口的关联关系
|
||||
1. 扫描目标域名/IP 的开放端口
|
||||
2. 保存 host + ip + port 三元映射到 HostPortMapping 表
|
||||
|
||||
输出资产:
|
||||
- Port:开放的端口列表(主要资产)
|
||||
- IPAddress:域名对应的 IP 地址(附带资产)
|
||||
- HostPortMapping:主机端口映射(host + ip + port 三元组)
|
||||
|
||||
工作流程:
|
||||
Step 0: 创建工作目录
|
||||
Step 1: 导出域名列表到文件(供扫描工具使用)
|
||||
Step 2: 解析配置,获取启用的工具
|
||||
Step 3: 串行执行扫描工具,运行端口扫描工具并实时解析输出到数据库(Subdomain → IPAddress → Port)
|
||||
Step 3: 串行执行扫描工具,运行端口扫描工具并实时解析输出到数据库(→ HostPortMapping)
|
||||
|
||||
Args:
|
||||
scan_id: 扫描任务 ID
|
||||
@@ -418,10 +416,8 @@ def port_scan_flow(
|
||||
RuntimeError: 执行失败
|
||||
|
||||
Note:
|
||||
端口扫描的输出必然包含 IP 信息,因为:
|
||||
- 扫描工具需要解析域名 → IP
|
||||
- 端口属于 IP,而不是直接属于域名
|
||||
- 同一域名可能对应多个 IP(CDN、负载均衡)
|
||||
端口扫描工具(如 naabu)会解析域名获取 IP,输出 host + ip + port 三元组。
|
||||
同一 host 可能对应多个 IP(CDN、负载均衡),因此使用三元映射表存储。
|
||||
"""
|
||||
try:
|
||||
# 参数验证
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
子域名发现扫描 Flow(增强版)
|
||||
子域名发现扫描 Flow
|
||||
|
||||
负责编排子域名发现扫描的完整流程
|
||||
|
||||
@@ -343,7 +343,7 @@ def subdomain_discovery_flow(
|
||||
scan_workspace_dir: str,
|
||||
enabled_tools: dict
|
||||
) -> dict:
|
||||
"""子域名发现扫描流程(增强版)
|
||||
"""子域名发现扫描流程
|
||||
|
||||
工作流程(4 阶段):
|
||||
Stage 1: 被动收集(并行) - 必选
|
||||
@@ -410,7 +410,7 @@ def subdomain_discovery_flow(
|
||||
# 验证成功后打印日志
|
||||
logger.info(
|
||||
"="*60 + "\n" +
|
||||
"开始子域名发现扫描(增强版)\n" +
|
||||
"开始子域名发现扫描\n" +
|
||||
f" Scan ID: {scan_id}\n" +
|
||||
f" Domain: {domain_name}\n" +
|
||||
f" Workspace: {scan_workspace_dir}\n" +
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
from django.dispatch import receiver
|
||||
|
||||
from apps.common.signals import vulnerabilities_saved, worker_delete_failed
|
||||
from apps.common.signals import vulnerabilities_saved, worker_delete_failed, all_workers_high_load
|
||||
from apps.scan.notifications import create_notification, NotificationLevel, NotificationCategory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -80,3 +80,15 @@ def on_worker_delete_failed(sender, worker_name, message, **kwargs):
|
||||
category=NotificationCategory.SYSTEM
|
||||
)
|
||||
logger.warning("Worker 删除失败通知已发送 - worker=%s, message=%s", worker_name, message)
|
||||
|
||||
|
||||
@receiver(all_workers_high_load)
|
||||
def on_all_workers_high_load(sender, worker_name, cpu, mem, **kwargs):
|
||||
"""所有 Worker 高负载时的通知处理"""
|
||||
create_notification(
|
||||
title="系统负载较高",
|
||||
message=f"所有节点负载较高,已选择负载最低的节点 {worker_name}(CPU: {cpu:.1f}%, 内存: {mem:.1f}%)执行任务,扫描速度可能受影响",
|
||||
level=NotificationLevel.MEDIUM,
|
||||
category=NotificationCategory.SYSTEM
|
||||
)
|
||||
logger.warning("高负载通知已发送 - worker=%s, cpu=%.1f%%, mem=%.1f%%", worker_name, cpu, mem)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -157,7 +157,7 @@ class ScanService:
|
||||
"""取消所有正在运行的阶段(委托给 ScanStateService)"""
|
||||
return self.state_service.cancel_running_stages(scan_id, final_status)
|
||||
|
||||
# todo:待接入
|
||||
# TODO:待接入
|
||||
def add_command_to_scan(self, scan_id: int, stage_name: str, tool_name: str, command: str) -> bool:
|
||||
"""
|
||||
增量添加命令到指定扫描阶段
|
||||
|
||||
@@ -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
|
||||
@@ -81,15 +82,20 @@ def ensure_wordlist_local(wordlist_name: str) -> str:
|
||||
raise RuntimeError(
|
||||
"无法确定 Django API 地址:请配置 SERVER_URL 或 PUBLIC_HOST 环境变量"
|
||||
)
|
||||
server_port = getattr(settings, 'SERVER_PORT', '8888')
|
||||
api_base = f"http://{public_host}:{server_port}/api"
|
||||
# 远程 Worker 通过 nginx HTTPS 访问,不再直连 8888
|
||||
api_base = f"https://{public_host}/api"
|
||||
query = urllib_parse.urlencode({'wordlist': wordlist_name})
|
||||
download_url = f"{api_base.rstrip('/')}/wordlists/download/?{query}"
|
||||
|
||||
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'
|
||||
@@ -52,7 +55,7 @@ if [ "$RUN_MODE" = "remote" ] && [ -f "$ENV_FILE" ]; then
|
||||
fi
|
||||
|
||||
# 获取配置
|
||||
# SERVER_URL: 后端 API 地址(容器内用 http://server:8888,远程用公网地址)
|
||||
# SERVER_URL: 后端 API 地址(容器内用 http://server:8888,远程用 https://{PUBLIC_HOST})
|
||||
API_URL="${HEARTBEAT_API_URL:-${SERVER_URL:-}}"
|
||||
WORKER_NAME="${WORKER_NAME:-}"
|
||||
IS_LOCAL="${IS_LOCAL:-false}"
|
||||
@@ -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)
|
||||
@@ -113,7 +116,7 @@ if [ -z "$WORKER_ID" ]; then
|
||||
# 等待 Server 就绪
|
||||
log "等待 Server 就绪..."
|
||||
for i in $(seq 1 30); do
|
||||
if curl -s "${API_URL}/api/" > /dev/null 2>&1; then
|
||||
if curl -k -s "${API_URL}/api/" > /dev/null 2>&1; then
|
||||
log "${GREEN}Server 已就绪${NC}"
|
||||
break
|
||||
fi
|
||||
@@ -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 -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}
|
||||
|
||||
|
||||
@@ -15,14 +15,14 @@ REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
# ==================== 服务端口配置 ====================
|
||||
# SERVER_PORT 为 Django / uvicorn 对外端口
|
||||
# SERVER_PORT 为 Django / uvicorn 容器内部端口(由 nginx 反代,对公网不直接暴露)
|
||||
SERVER_PORT=8888
|
||||
|
||||
# ==================== 远程 Worker 配置 ====================
|
||||
# 供远程 Worker 访问主服务器的地址:
|
||||
# - 仅本地部署:server(Docker 内部服务名)
|
||||
# - 有远程 Worker:改为主服务器外网 IP(如 192.168.1.100)
|
||||
# 注意:远程 Worker 访问数据库/Redis 也会使用此地址(除非配置了远程 PostgreSQL)
|
||||
# - 有远程 Worker:改为主服务器外网 IP 或域名(如 192.168.1.100 或 xingrin.example.com)
|
||||
# 注意:远程 Worker 会通过 https://{PUBLIC_HOST} 访问(nginx 反代到后端 8888)
|
||||
PUBLIC_HOST=server
|
||||
|
||||
# ==================== Django 核心配置 ====================
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -37,8 +37,6 @@ services:
|
||||
context: ..
|
||||
dockerfile: docker/server/Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- "${SERVER_PORT}:8888"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
@@ -56,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
|
||||
|
||||
@@ -41,8 +41,6 @@ services:
|
||||
server:
|
||||
image: ${DOCKER_USER:-yyhuni}/xingrin-server:${IMAGE_TAG:?IMAGE_TAG is required}
|
||||
restart: always
|
||||
ports:
|
||||
- "${SERVER_PORT}:8888"
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
@@ -74,6 +72,7 @@ services:
|
||||
- SERVER_URL=http://server:8888
|
||||
- WORKER_NAME=本地节点
|
||||
- IS_LOCAL=true
|
||||
- IMAGE_TAG=${IMAGE_TAG}
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
|
||||
97
docker/scripts/setup-swap.sh
Executable file
97
docker/scripts/setup-swap.sh
Executable file
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Ubuntu/Debian 一键开启交换分区脚本
|
||||
# 用法: sudo ./setup-swap.sh [大小GB]
|
||||
# 示例: sudo ./setup-swap.sh 4 # 创建 4GB 交换分区
|
||||
# sudo ./setup-swap.sh # 默认创建与内存相同大小的交换分区
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# 检查 root 权限
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
log_error "请使用 sudo 运行此脚本"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查是否已有交换分区
|
||||
CURRENT_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
|
||||
CURRENT_SWAP_GB=$(awk "BEGIN {printf \"%.0f\", $CURRENT_SWAP_KB / 1024 / 1024}")
|
||||
if [ "$CURRENT_SWAP_GB" -gt 0 ]; then
|
||||
log_warn "系统已有 ${CURRENT_SWAP_GB}GB 交换分区"
|
||||
swapon --show
|
||||
read -p "是否继续添加新的交换分区?(y/N) " -r
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "已取消"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# 获取系统内存大小(GB,四舍五入)
|
||||
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
TOTAL_MEM_GB=$(awk "BEGIN {printf \"%.0f\", $TOTAL_MEM_KB / 1024 / 1024}")
|
||||
|
||||
# 确定交换分区大小
|
||||
if [ -n "$1" ]; then
|
||||
SWAP_SIZE_GB=$1
|
||||
else
|
||||
# 默认与内存相同,最小 1GB,最大 8GB
|
||||
SWAP_SIZE_GB=$TOTAL_MEM_GB
|
||||
[ "$SWAP_SIZE_GB" -lt 1 ] && SWAP_SIZE_GB=1
|
||||
[ "$SWAP_SIZE_GB" -gt 8 ] && SWAP_SIZE_GB=8
|
||||
fi
|
||||
|
||||
SWAP_FILE="/swapfile_xingrin"
|
||||
|
||||
log_info "系统内存: ${TOTAL_MEM_GB}GB"
|
||||
log_info "将创建 ${SWAP_SIZE_GB}GB 交换分区: $SWAP_FILE"
|
||||
|
||||
# 检查磁盘空间(向下取整,保守估计)
|
||||
AVAILABLE_GB=$(df / | tail -1 | awk '{printf "%.0f", $4/1024/1024}')
|
||||
if [ "$AVAILABLE_GB" -lt "$SWAP_SIZE_GB" ]; then
|
||||
log_error "磁盘空间不足!可用: ${AVAILABLE_GB}GB,需要: ${SWAP_SIZE_GB}GB"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建交换文件
|
||||
log_info "正在创建交换文件(可能需要几分钟)..."
|
||||
dd if=/dev/zero of=$SWAP_FILE bs=1G count=$SWAP_SIZE_GB status=progress
|
||||
|
||||
# 设置权限
|
||||
chmod 600 $SWAP_FILE
|
||||
|
||||
# 格式化为交换分区
|
||||
mkswap $SWAP_FILE
|
||||
|
||||
# 启用交换分区
|
||||
swapon $SWAP_FILE
|
||||
|
||||
# 添加到 fstab(开机自动挂载)
|
||||
if ! grep -q "$SWAP_FILE" /etc/fstab; then
|
||||
echo "$SWAP_FILE none swap sw 0 0" >> /etc/fstab
|
||||
log_info "已添加到 /etc/fstab,开机自动启用"
|
||||
fi
|
||||
|
||||
# 优化 swappiness(降低交换倾向,优先使用内存)
|
||||
SWAPPINESS=10
|
||||
if ! grep -q "vm.swappiness" /etc/sysctl.conf; then
|
||||
echo "vm.swappiness=$SWAPPINESS" >> /etc/sysctl.conf
|
||||
fi
|
||||
sysctl vm.swappiness=$SWAPPINESS >/dev/null
|
||||
|
||||
log_info "交换分区创建成功!"
|
||||
echo ""
|
||||
echo "当前交换分区状态:"
|
||||
swapon --show
|
||||
echo ""
|
||||
free -h
|
||||
@@ -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
|
||||
@@ -173,7 +174,7 @@ if [ "$WITH_FRONTEND" = true ]; then
|
||||
echo -e " XingRin: ${CYAN}https://${ACCESS_HOST}/${NC}"
|
||||
echo -e " ${YELLOW}(HTTP 会自动跳转到 HTTPS)${NC}"
|
||||
else
|
||||
echo -e " API: ${CYAN}http://${ACCESS_HOST}:8888${NC}"
|
||||
echo -e " API: ${CYAN}通过前端或 nginx 访问(后端未暴露 8888)${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}[TIP]${NC} 前端未启动,请手动运行:"
|
||||
echo " cd frontend && pnpm dev"
|
||||
|
||||
@@ -14,10 +14,9 @@
|
||||
- **端口要求**: 需要开放以下端口
|
||||
- `80` - HTTP 访问(自动跳转到 HTTPS)
|
||||
- `443` - HTTPS 访问(主要访问端口)
|
||||
- `3000` - 前端开发服务(开发模式)
|
||||
- `8888` - 后端 API 服务
|
||||
- `5432` - PostgreSQL 数据库(如使用本地数据库)
|
||||
- `6379` - Redis 缓存服务
|
||||
- 后端 API 仅容器内监听 8888,由 nginx 反代到 80/443,对公网无需放行 8888
|
||||
|
||||
## 一键安装
|
||||
|
||||
@@ -64,10 +63,10 @@ sudo ./install.sh --no-frontend
|
||||
80 - HTTP 访问
|
||||
443 - HTTPS 访问
|
||||
3000 - 前端服务(开发模式)
|
||||
8888 - 后端 API
|
||||
5432 - PostgreSQL(如使用本地数据库)
|
||||
6379 - Redis 缓存
|
||||
```
|
||||
> 后端 API 默认仅在容器内 8888 监听,由 nginx 反代到 80/443,对公网无需放行 8888。
|
||||
|
||||
#### 推荐方案
|
||||
- **国外 VPS**:如 Vultr、DigitalOcean、Linode 等,默认开放所有端口,无需额外配置
|
||||
@@ -157,8 +156,8 @@ DB_USER=postgres # 数据库用户
|
||||
DB_PASSWORD=随机生成 # 数据库密码
|
||||
|
||||
# 服务配置
|
||||
SERVER_PORT=8888 # 后端服务端口
|
||||
PUBLIC_HOST=server # 对外访问地址
|
||||
SERVER_PORT=8888 # 后端容器内部端口(仅 Docker 内网监听)
|
||||
PUBLIC_HOST=server # 对外访问地址(远程 Worker 用,配置外网 IP 或域名)
|
||||
DEBUG=False # 调试模式
|
||||
|
||||
# 版本配置
|
||||
|
||||
123
docs/scan-flow-architecture.md
Normal file
123
docs/scan-flow-architecture.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 扫描流程架构
|
||||
|
||||
## 完整扫描流程
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
START[Start Scan]
|
||||
TARGET[Input Target]
|
||||
|
||||
START --> TARGET
|
||||
|
||||
subgraph STAGE1["Stage 1: Discovery Sequential"]
|
||||
direction TB
|
||||
|
||||
subgraph SUB["Subdomain Discovery"]
|
||||
direction TB
|
||||
SUBFINDER[subfinder]
|
||||
AMASS[amass]
|
||||
SUBLIST3R[sublist3r]
|
||||
ASSETFINDER[assetfinder]
|
||||
MERGE[Merge & Deduplicate]
|
||||
BRUTEFORCE[puredns bruteforce<br/>Dictionary Attack]
|
||||
MUTATE[dnsgen + puredns<br/>Mutation Generation]
|
||||
RESOLVE[puredns resolve<br/>Alive Verification]
|
||||
|
||||
SUBFINDER --> MERGE
|
||||
AMASS --> MERGE
|
||||
SUBLIST3R --> MERGE
|
||||
ASSETFINDER --> MERGE
|
||||
MERGE --> BRUTEFORCE
|
||||
BRUTEFORCE --> MUTATE
|
||||
MUTATE --> RESOLVE
|
||||
end
|
||||
|
||||
subgraph PORT["Port Scan"]
|
||||
NAABU[naabu<br/>Port Discovery]
|
||||
end
|
||||
|
||||
subgraph SITE["Site Scan"]
|
||||
HTTPX1[httpx<br/>Web Service Detection]
|
||||
end
|
||||
|
||||
RESOLVE --> NAABU
|
||||
NAABU --> HTTPX1
|
||||
end
|
||||
|
||||
TARGET --> SUBFINDER
|
||||
TARGET --> AMASS
|
||||
TARGET --> SUBLIST3R
|
||||
TARGET --> ASSETFINDER
|
||||
|
||||
subgraph STAGE2["Stage 2: Analysis Parallel"]
|
||||
direction TB
|
||||
|
||||
subgraph URL["URL Collection"]
|
||||
direction TB
|
||||
WAYMORE[waymore<br/>Historical URLs]
|
||||
KATANA[katana<br/>Crawler]
|
||||
URO[uro<br/>URL Deduplication]
|
||||
HTTPX2[httpx<br/>Alive Verification]
|
||||
|
||||
WAYMORE --> URO
|
||||
KATANA --> URO
|
||||
URO --> HTTPX2
|
||||
end
|
||||
|
||||
subgraph DIR["Directory Scan"]
|
||||
FFUF[ffuf<br/>Directory Bruteforce]
|
||||
end
|
||||
end
|
||||
|
||||
HTTPX1 --> WAYMORE
|
||||
HTTPX1 --> KATANA
|
||||
HTTPX1 --> FFUF
|
||||
|
||||
subgraph STAGE3["Stage 3: Vulnerability Sequential"]
|
||||
direction TB
|
||||
|
||||
subgraph VULN["Vulnerability Scan"]
|
||||
direction LR
|
||||
DALFOX[dalfox<br/>XSS Scan]
|
||||
NUCLEI[nuclei<br/>Vulnerability Scan]
|
||||
end
|
||||
end
|
||||
|
||||
HTTPX2 --> DALFOX
|
||||
HTTPX2 --> NUCLEI
|
||||
|
||||
DALFOX --> FINISH
|
||||
NUCLEI --> FINISH
|
||||
FFUF --> FINISH
|
||||
|
||||
FINISH[Scan Complete]
|
||||
|
||||
style START fill:#ff9999
|
||||
style FINISH fill:#99ff99
|
||||
style TARGET fill:#ffcc99
|
||||
style STAGE1 fill:#e6f3ff
|
||||
style STAGE2 fill:#fff4e6
|
||||
style STAGE3 fill:#ffe6f0
|
||||
```
|
||||
|
||||
## 执行阶段定义
|
||||
|
||||
```python
|
||||
# backend/apps/scan/configs/command_templates.py
|
||||
EXECUTION_STAGES = [
|
||||
{'mode': 'sequential', 'flows': ['subdomain_discovery', 'port_scan', 'site_scan']},
|
||||
{'mode': 'parallel', 'flows': ['url_fetch', 'directory_scan']},
|
||||
{'mode': 'sequential', 'flows': ['vuln_scan']},
|
||||
]
|
||||
```
|
||||
|
||||
## 各阶段输出
|
||||
|
||||
| Flow | 工具 | 输出表 |
|
||||
|------|------|--------|
|
||||
| subdomain_discovery | subfinder, amass, sublist3r, assetfinder, puredns | Subdomain |
|
||||
| port_scan | naabu | HostPortMapping |
|
||||
| site_scan | httpx | WebSite |
|
||||
| url_fetch | waymore, katana, uro, httpx | Endpoint |
|
||||
| directory_scan | ffuf | Directory |
|
||||
| vuln_scan | dalfox, nuclei | Vulnerability |
|
||||
@@ -40,62 +40,66 @@ Wordlist
|
||||
- 统计文件大小和行数
|
||||
- 创建数据库记录
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Server 容器 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 前端 UI │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ 上传字典 │ │ 编辑内容 │ │ 删除字典 │ │ │
|
||||
│ │ │ 选择文件 │ │ 在线修改 │ │ │ │ │
|
||||
│ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │ │
|
||||
│ └─────────┼───────────────────┼───────────────────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WordlistViewSet │ │
|
||||
│ │ POST /api/wordlists/ | PUT .../content/ │ │
|
||||
│ └─────────────────────────────┬───────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ WordlistService │ │
|
||||
│ │ │ │
|
||||
│ │ ┌────────────────────┐ ┌────────────────────────────────┐ │ │
|
||||
│ │ │ create_wordlist() │ │ update_wordlist_content() │ │ │
|
||||
│ │ │ 创建字典 │ │ 更新字典内容 │ │ │
|
||||
│ │ └────────┬───────────┘ └───────────────┬────────────────┘ │ │
|
||||
│ └───────────┼────────────────────────────────┼────────────────────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 处理流程 │ │
|
||||
│ │ │ │
|
||||
│ │ 1. 保存文件到 /opt/xingrin/wordlists/<filename> │ │
|
||||
│ │ 2. 计算 SHA256 哈希值 │ │
|
||||
│ │ 3. 统计文件大小和行数 │ │
|
||||
│ │ 4. 创建/更新数据库记录 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ PostgreSQL 数据库 │ │
|
||||
│ │ │ │
|
||||
│ │ INSERT INTO wordlist (name, file_path, file_size, │ │
|
||||
│ │ line_count, file_hash) │ │
|
||||
│ │ VALUES ('subdomains', '/opt/xingrin/wordlists/subdomains.txt', │ │
|
||||
│ │ 1024000, 50000, 'sha256...') │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 文件系统 │ │
|
||||
│ │ /opt/xingrin/wordlists/ │ │
|
||||
│ │ ├── common.txt │ │
|
||||
│ │ ├── subdomains.txt │ │
|
||||
│ │ └── directories.txt │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph SERVER["🖥️ Server 容器"]
|
||||
direction TB
|
||||
|
||||
subgraph UI["前端 UI"]
|
||||
direction LR
|
||||
UPLOAD["📤 上传字典<br/>选择文件"]
|
||||
EDIT["✏️ 编辑内容<br/>在线修改"]
|
||||
DELETE["🗑️ 删除字典"]
|
||||
end
|
||||
|
||||
UPLOAD --> API
|
||||
EDIT --> API
|
||||
|
||||
subgraph API["API 层"]
|
||||
VIEWSET["WordlistViewSet<br/>POST /api/wordlists/<br/>PUT .../content/"]
|
||||
end
|
||||
|
||||
API --> SERVICE
|
||||
|
||||
subgraph SERVICE["业务逻辑层"]
|
||||
CREATE["create_wordlist()<br/>创建字典"]
|
||||
UPDATE["update_wordlist_content()<br/>更新字典内容"]
|
||||
end
|
||||
|
||||
CREATE --> PROCESS
|
||||
UPDATE --> PROCESS
|
||||
|
||||
subgraph PROCESS["处理流程"]
|
||||
direction TB
|
||||
STEP1["1️⃣ 保存文件到<br/>/opt/xingrin/wordlists/"]
|
||||
STEP2["2️⃣ 计算 SHA256 哈希值"]
|
||||
STEP3["3️⃣ 统计文件大小和行数"]
|
||||
STEP4["4️⃣ 创建/更新数据库记录"]
|
||||
|
||||
STEP1 --> STEP2
|
||||
STEP2 --> STEP3
|
||||
STEP3 --> STEP4
|
||||
end
|
||||
|
||||
STEP4 --> DB
|
||||
STEP1 --> FS
|
||||
|
||||
subgraph DB["💾 PostgreSQL 数据库"]
|
||||
DBRECORD["INSERT INTO wordlist<br/>name: 'subdomains'<br/>file_path: '/opt/xingrin/wordlists/subdomains.txt'<br/>file_size: 1024000<br/>line_count: 50000<br/>file_hash: 'sha256...'"]
|
||||
end
|
||||
|
||||
subgraph FS["📁 文件系统"]
|
||||
FILES["/opt/xingrin/wordlists/<br/>├── common.txt<br/>├── subdomains.txt<br/>└── directories.txt"]
|
||||
end
|
||||
end
|
||||
|
||||
style SERVER fill:#e6f3ff
|
||||
style UI fill:#fff4e6
|
||||
style API fill:#f0f0f0
|
||||
style SERVICE fill:#d4edda
|
||||
style PROCESS fill:#ffe6f0
|
||||
style DB fill:#cce5ff
|
||||
style FS fill:#e2e3e5
|
||||
```
|
||||
|
||||
## 四、Worker 端获取流程
|
||||
@@ -110,76 +114,65 @@ Worker 执行扫描任务时,通过 `ensure_wordlist_local()` 获取字典:
|
||||
3. 下载地址:`GET /api/wordlists/download/?wordlist=<name>`
|
||||
4. 返回本地字典文件路径
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ Worker 容器 │
|
||||
│ │
|
||||
│ ┌─────────────┐ │
|
||||
│ │ 扫描任务 │ │
|
||||
│ │ 需要字典 │ │
|
||||
│ └──────┬──────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
|
||||
│ │ ensure_wordlist_local() │ │ PostgreSQL │ │
|
||||
│ │ 参数: wordlist_name │─────▶│ 查询 Wordlist 表 │ │
|
||||
│ │ │ │ 获取 file_path, file_hash │ │
|
||||
│ └───────────┬─────────────┘ └─────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────┐ │
|
||||
│ │ 检查本地文件是否存在 │ │
|
||||
│ │ /opt/xingrin/wordlists/ │ │
|
||||
│ └───────────┬─────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────┴───────┐ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌────────┐ ┌────────────┐ │
|
||||
│ │ 不存在 │ │ 存在 │ │
|
||||
│ └───┬────┘ └─────┬──────┘ │
|
||||
│ │ │ │
|
||||
│ │ ▼ │
|
||||
│ │ ┌─────────────────────┐ │
|
||||
│ │ │ 计算本地文件 SHA256 │ │
|
||||
│ │ │ 与数据库 hash 比较 │ │
|
||||
│ │ └──────────┬──────────┘ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────┴───────┐ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ ▼ │
|
||||
│ │ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │ │ 一致 │ │ 不一致 │ │
|
||||
│ │ │ 直接使用 │ │ 需重新下载 │ │
|
||||
│ │ └────┬─────┘ └───────┬──────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ ▼ │ ▼ │
|
||||
│ ┌─────────────┴─────────────────────────────────────────────────────┐ │
|
||||
│ │ 从 Server API 下载 │ │
|
||||
│ │ GET /api/wordlists/download/?wordlist=<name> │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────┐ HTTP Request ┌──────────────────────┐ │ │
|
||||
│ │ │ Worker │ ───────────────────────▶│ Server (Django) │ │ │
|
||||
│ │ │ │◀─────────────────────── │ 返回文件内容 │ │ │
|
||||
│ │ └──────────┘ File Content └──────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ │ 保存到: /opt/xingrin/wordlists/<filename> │ │
|
||||
│ └───────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 返回本地字典文件路径 │ │
|
||||
│ │ /opt/xingrin/wordlists/subdomains.txt │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ 执行扫描工具 │ │
|
||||
│ │ puredns bruteforce -w /opt/xingrin/wordlists/xxx.txt │ │
|
||||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph WORKER["🔧 Worker 容器"]
|
||||
direction TB
|
||||
|
||||
START["🎯 扫描任务<br/>需要字典"]
|
||||
|
||||
START --> ENSURE
|
||||
|
||||
ENSURE["ensure_wordlist_local()<br/>参数: wordlist_name"]
|
||||
|
||||
ENSURE --> QUERY
|
||||
|
||||
QUERY["📊 查询 PostgreSQL<br/>获取 file_path, file_hash"]
|
||||
|
||||
QUERY --> CHECK
|
||||
|
||||
CHECK{"🔍 检查本地文件<br/>/opt/xingrin/wordlists/"}
|
||||
|
||||
CHECK -->|不存在| DOWNLOAD
|
||||
CHECK -->|存在| HASH
|
||||
|
||||
HASH["🔐 计算本地文件 SHA256<br/>与数据库 hash 比较"]
|
||||
|
||||
HASH -->|一致| USE
|
||||
HASH -->|不一致| DOWNLOAD
|
||||
|
||||
DOWNLOAD["📥 从 Server API 下载<br/>GET /api/wordlists/download/?wordlist=name"]
|
||||
|
||||
DOWNLOAD --> SERVER
|
||||
|
||||
SERVER["🌐 HTTP Request"]
|
||||
|
||||
SERVER -.请求.-> API["Server (Django)<br/>返回文件内容"]
|
||||
API -.响应.-> SERVER
|
||||
|
||||
SERVER --> SAVE
|
||||
|
||||
SAVE["💾 保存到本地<br/>/opt/xingrin/wordlists/filename"]
|
||||
|
||||
SAVE --> RETURN
|
||||
|
||||
USE["✅ 直接使用"] --> RETURN
|
||||
|
||||
RETURN["📂 返回本地字典文件路径<br/>/opt/xingrin/wordlists/subdomains.txt"]
|
||||
|
||||
RETURN --> EXEC
|
||||
|
||||
EXEC["🚀 执行扫描工具<br/>puredns bruteforce -w /opt/xingrin/wordlists/xxx.txt"]
|
||||
end
|
||||
|
||||
style WORKER fill:#e6f3ff
|
||||
style START fill:#fff4e6
|
||||
style CHECK fill:#ffe6f0
|
||||
style HASH fill:#ffe6f0
|
||||
style USE fill:#d4edda
|
||||
style DOWNLOAD fill:#f8d7da
|
||||
style RETURN fill:#d4edda
|
||||
style EXEC fill:#cce5ff
|
||||
```
|
||||
|
||||
## 五、Hash 校验机制
|
||||
@@ -199,25 +192,28 @@ Worker 执行扫描任务时,通过 `ensure_wordlist_local()` 获取字典:
|
||||
|
||||
**注意**:Worker 容器只挂载了 `results` 和 `logs` 目录,没有挂载 `wordlists` 目录,所以字典文件需要通过 API 下载。
|
||||
|
||||
```
|
||||
Worker(本地/远程) Server
|
||||
│ │
|
||||
│ 1. 查询数据库获取 file_hash │
|
||||
│─────────────────────────────────▶│
|
||||
│ │
|
||||
│ 2. 检查本地缓存 │
|
||||
│ - 存在且 hash 匹配 → 直接使用│
|
||||
│ - 不存在或不匹配 → 继续下载 │
|
||||
│ │
|
||||
│ 3. GET /api/wordlists/download/ │
|
||||
│─────────────────────────────────▶│
|
||||
│ │
|
||||
│ 4. 返回文件内容 │
|
||||
│◀─────────────────────────────────│
|
||||
│ │
|
||||
│ 5. 保存到本地缓存 │
|
||||
│ /opt/xingrin/wordlists/ │
|
||||
│ │
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant W as Worker (本地/远程)
|
||||
participant DB as PostgreSQL
|
||||
participant S as Server API
|
||||
participant FS as 本地缓存
|
||||
|
||||
W->>DB: 1️⃣ 查询数据库获取 file_hash
|
||||
DB-->>W: 返回 file_hash
|
||||
|
||||
W->>FS: 2️⃣ 检查本地缓存
|
||||
|
||||
alt 存在且 hash 匹配
|
||||
FS-->>W: ✅ 直接使用
|
||||
else 不存在或不匹配
|
||||
W->>S: 3️⃣ GET /api/wordlists/download/
|
||||
S-->>W: 4️⃣ 返回文件内容
|
||||
W->>FS: 5️⃣ 保存到本地缓存<br/>/opt/xingrin/wordlists/
|
||||
FS-->>W: ✅ 使用缓存文件
|
||||
end
|
||||
|
||||
Note over W,FS: 本地 Worker 优势:<br/>• 网络延迟更低(容器内网络)<br/>• 缓存可复用(同一宿主机多次任务)
|
||||
```
|
||||
|
||||
### 本地 Worker 的优势
|
||||
@@ -235,8 +231,8 @@ Worker(本地/远程) Server
|
||||
WORDLISTS_PATH=/opt/xingrin/wordlists
|
||||
|
||||
# Server 地址(Worker 用于下载文件)
|
||||
PUBLIC_HOST=your-server-ip
|
||||
SERVER_PORT=8888
|
||||
PUBLIC_HOST=your-server-ip # 远程 Worker 会通过 https://{PUBLIC_HOST}/api 访问
|
||||
SERVER_PORT=8888 # 后端容器内部端口,仅 Docker 内网监听
|
||||
```
|
||||
|
||||
## 八、常见问题
|
||||
@@ -248,8 +244,8 @@ A: 更新字典内容后会重新计算 hash,Worker 下次使用时会检测
|
||||
### Q: 远程 Worker 下载文件失败?
|
||||
|
||||
A: 检查:
|
||||
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP
|
||||
2. Server 端口(默认 8888)是否开放
|
||||
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP 或域名
|
||||
2. Nginx 443 (HTTPS) 是否可达(远程 Worker 通过 nginx 访问后端)
|
||||
3. Worker 到 Server 的网络是否通畅
|
||||
|
||||
### Q: 如何批量导入字典?
|
||||
|
||||
@@ -231,7 +231,7 @@ export const createScheduledScanColumns = ({
|
||||
// Cron 表达式列
|
||||
{
|
||||
accessorKey: "cronExpression",
|
||||
header: "调度时间",
|
||||
header: "Cron Expression",
|
||||
cell: ({ row }) => {
|
||||
const cron = row.original.cronExpression
|
||||
return (
|
||||
@@ -251,7 +251,7 @@ export const createScheduledScanColumns = ({
|
||||
// 目标列(根据 scanMode 显示组织或目标)
|
||||
{
|
||||
accessorKey: "scanMode",
|
||||
header: "目标",
|
||||
header: "Target",
|
||||
cell: ({ row }) => {
|
||||
const scanMode = row.original.scanMode
|
||||
const organizationName = row.original.organizationName
|
||||
|
||||
@@ -81,7 +81,7 @@ export function createVulnerabilityColumns({
|
||||
},
|
||||
{
|
||||
accessorKey: "vulnType",
|
||||
header: "类型",
|
||||
header: "Vuln Type",
|
||||
cell: ({ row }) => {
|
||||
const vulnType = row.getValue("vulnType") as string
|
||||
const vulnerability = row.original
|
||||
@@ -143,7 +143,7 @@ export function createVulnerabilityColumns({
|
||||
},
|
||||
{
|
||||
accessorKey: "discoveredAt",
|
||||
header: "发现时间",
|
||||
header: "Discovered At",
|
||||
cell: ({ row }) => {
|
||||
const discoveredAt = row.getValue("discoveredAt") as string
|
||||
return (
|
||||
|
||||
42
install.sh
42
install.sh
@@ -233,7 +233,7 @@ show_summary() {
|
||||
|
||||
echo -e "${YELLOW}[!] 云服务器某些厂商默认开启了安全策略(阿里云/腾讯云/华为云等):${RESET}"
|
||||
echo -e " 端口未放行可能导致无法访问或无法扫描,强烈推荐用国外vps,或者在云控制台放行:"
|
||||
echo -e " ${RESET}80, 443, 3000,8888, 5432, 6379"
|
||||
echo -e " ${RESET}80, 443, 5432, 6379"
|
||||
echo
|
||||
}
|
||||
|
||||
@@ -278,6 +278,46 @@ else
|
||||
success "docker compose 安装完成"
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# 交换分区配置(仅 Linux)
|
||||
# ==============================================================================
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
# 获取当前内存大小(GB,四舍五入)
|
||||
TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
TOTAL_MEM_GB=$(awk "BEGIN {printf \"%.0f\", $TOTAL_MEM_KB / 1024 / 1024}")
|
||||
|
||||
# 获取当前交换分区大小(GB,四舍五入)
|
||||
CURRENT_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
|
||||
CURRENT_SWAP_GB=$(awk "BEGIN {printf \"%.0f\", $CURRENT_SWAP_KB / 1024 / 1024}")
|
||||
|
||||
# 推荐交换分区大小(与内存相同,最小1G,最大8G)
|
||||
RECOMMENDED_SWAP=$TOTAL_MEM_GB
|
||||
[ "$RECOMMENDED_SWAP" -lt 1 ] && RECOMMENDED_SWAP=1
|
||||
[ "$RECOMMENDED_SWAP" -gt 8 ] && RECOMMENDED_SWAP=8
|
||||
|
||||
echo ""
|
||||
info "系统内存: ${TOTAL_MEM_GB}GB,当前交换分区: ${CURRENT_SWAP_GB}GB"
|
||||
|
||||
# 如果交换分区小于推荐值,提示用户
|
||||
if [ "$CURRENT_SWAP_GB" -lt "$RECOMMENDED_SWAP" ]; then
|
||||
echo -n -e "${BOLD}${CYAN}[?] 是否开启 ${RECOMMENDED_SWAP}GB 交换分区?可提升扫描稳定性 (Y/n) ${RESET}"
|
||||
read -r setup_swap
|
||||
echo
|
||||
if [[ ! $setup_swap =~ ^[Nn]$ ]]; then
|
||||
info "正在配置 ${RECOMMENDED_SWAP}GB 交换分区..."
|
||||
if bash "$ROOT_DIR/docker/scripts/setup-swap.sh" "$RECOMMENDED_SWAP"; then
|
||||
success "交换分区配置完成"
|
||||
else
|
||||
warn "交换分区配置失败,继续安装..."
|
||||
fi
|
||||
else
|
||||
info "跳过交换分区配置"
|
||||
fi
|
||||
else
|
||||
success "交换分区已足够: ${CURRENT_SWAP_GB}GB"
|
||||
fi
|
||||
fi
|
||||
|
||||
step "[3/3] 初始化配置"
|
||||
DOCKER_DIR="$ROOT_DIR/docker"
|
||||
if [ ! -d "$DOCKER_DIR" ]; then
|
||||
|
||||
Reference in New Issue
Block a user