Compare commits

...

18 Commits

Author SHA1 Message Date
yyhuni
873b6893f1 fix version 2025-12-18 18:32:20 +08:00
yyhuni
dbb30f7c78 chore: bump version to v1.0.9 2025-12-18 18:29:35 +08:00
yyhuni
38eced3814 fix: Prefect 本地模式配置:禁用 API server 和事件系统 2025-12-18 18:28:19 +08:00
yyhuni
68fc7cee3b fix: 兼容旧版本证书安装,兼容旧版本docker compose 2025-12-18 17:44:02 +08:00
yyhuni
6e23824a45 todo:待接入add_command_to_scan 2025-12-17 21:40:07 +08:00
github-actions[bot]
a88cceb4f4 chore: bump version to v1.0.8 2025-12-17 11:09:39 +00:00
yyhuni
81164621d2 fix: fix bug 扫描任务删除接口 2025-12-17 18:50:18 +08:00
yyhuni
379abaeca7 更新架构文档 2025-12-17 18:20:50 +08:00
github-actions[bot]
de77057679 chore: bump version to v1.0.7 2025-12-17 09:58:03 +00:00
github-actions[bot]
630747ed2b chore: bump version to v1.0.6 2025-12-17 09:55:10 +00:00
github-actions[bot]
98c418ee8b chore: bump version to v1.0.5 2025-12-17 09:37:54 +00:00
yyhuni
cd54089c34 fix: 前端日志显示排序问题 2025-12-17 17:35:08 +08:00
yyhuni
8fcda537a3 优化,预拉取docker镜像 2025-12-17 17:14:50 +08:00
github-actions[bot]
3ca94be7b7 chore: bump version to v1.0.4 2025-12-17 08:50:19 +00:00
yyhuni
eb70692843 更新注释 2025-12-17 16:40:07 +08:00
yyhuni
3d20623b41 fix: 卸载日志逻辑,安装启动逻辑优化
增加:日志系统的前端显示,简略版本
2025-12-17 16:27:58 +08:00
yyhuni
2c45b3baa8 设计日志ui 2025-12-17 15:43:35 +08:00
github-actions[bot]
0cd2215f9d chore: bump version to v1.0.3 2025-12-17 03:53:19 +00:00
23 changed files with 956 additions and 28 deletions

View File

@@ -29,6 +29,8 @@
- [📖 技术文档](./docs/README.md) - 技术文档导航(🚧 持续完善中)
- [🚀 快速开始](./docs/quick-start.md) - 一键安装和部署指南
- [🔄 版本管理](./docs/version-management.md) - Git Tag 驱动的自动化版本管理系统
- [📦 Nuclei 模板架构](./docs/nuclei-template-architecture.md) - 模板仓库的存储与同步
- [📖 字典文件架构](./docs/wordlist-architecture.md) - 字典文件的存储与同步
---

View File

@@ -1 +1 @@
v1.0.2
v1.0.8

View File

@@ -0,0 +1,10 @@
"""
通用服务模块
提供系统级别的公共服务,包括:
- SystemLogService: 系统日志读取服务
"""
from .system_log_service import SystemLogService
__all__ = ['SystemLogService']

View File

@@ -0,0 +1,69 @@
"""
系统日志服务模块
提供系统日志的读取功能,支持:
- 从日志目录读取日志文件
- 限制返回行数,防止内存溢出
"""
import logging
import subprocess
logger = logging.getLogger(__name__)
class SystemLogService:
"""
系统日志服务类
负责读取系统日志文件,支持从容器内路径或宿主机挂载路径读取日志。
"""
def __init__(self):
# 日志文件路径(容器内路径,通过 volume 挂载到宿主机 /opt/xingrin/logs
self.log_file = "/app/backend/logs/xingrin.log"
self.default_lines = 200 # 默认返回行数
self.max_lines = 10000 # 最大返回行数限制
self.timeout_seconds = 3 # tail 命令超时时间
def get_logs_content(self, lines: int | None = None) -> str:
"""
获取系统日志内容
Args:
lines: 返回的日志行数,默认 200 行,最大 10000 行
Returns:
str: 日志内容,每行以换行符分隔,保持原始顺序
"""
# 参数校验和默认值处理
if lines is None:
lines = self.default_lines
lines = int(lines)
if lines < 1:
lines = 1
if lines > self.max_lines:
lines = self.max_lines
# 使用 tail 命令读取日志文件末尾内容
cmd = ["tail", "-n", str(lines), self.log_file]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=self.timeout_seconds,
check=False,
)
if result.returncode != 0:
logger.warning(
"tail command failed: returncode=%s stderr=%s",
result.returncode,
(result.stderr or "").strip(),
)
# 直接返回原始内容,保持文件中的顺序
return result.stdout or ""

View File

@@ -1,12 +1,21 @@
"""
通用模块 URL 配置
路由说明:
- /api/auth/* 认证相关接口(登录、登出、用户信息)
- /api/system/* 系统管理接口(日志查看等)
"""
from django.urls import path
from .views import LoginView, LogoutView, MeView, ChangePasswordView
from .views import LoginView, LogoutView, MeView, ChangePasswordView, SystemLogsView
urlpatterns = [
# 认证相关
path('auth/login/', LoginView.as_view(), name='auth-login'),
path('auth/logout/', LogoutView.as_view(), name='auth-logout'),
path('auth/me/', MeView.as_view(), name='auth-me'),
path('auth/change-password/', ChangePasswordView.as_view(), name='auth-change-password'),
# 系统管理
path('system/logs/', SystemLogsView.as_view(), name='system-logs'),
]

View File

@@ -1,3 +1,12 @@
from .auth_views import LoginView, LogoutView, MeView, ChangePasswordView
"""
通用模块视图导出
__all__ = ['LoginView', 'LogoutView', 'MeView', 'ChangePasswordView']
包含:
- 认证相关视图:登录、登出、用户信息、修改密码
- 系统日志视图:实时日志查看
"""
from .auth_views import LoginView, LogoutView, MeView, ChangePasswordView
from .system_log_views import SystemLogsView
__all__ = ['LoginView', 'LogoutView', 'MeView', 'ChangePasswordView', 'SystemLogsView']

View File

@@ -0,0 +1,69 @@
"""
系统日志视图模块
提供系统日志的 REST API 接口,供前端实时查看系统运行日志。
"""
import logging
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.common.services.system_log_service import SystemLogService
logger = logging.getLogger(__name__)
@method_decorator(csrf_exempt, name="dispatch")
class SystemLogsView(APIView):
"""
系统日志 API 视图
GET /api/system/logs/
获取系统日志内容
Query Parameters:
lines (int, optional): 返回的日志行数,默认 200最大 10000
Response:
{
"content": "日志内容字符串..."
}
Note:
- 当前为开发阶段,暂时允许匿名访问
- 生产环境应添加管理员权限验证
"""
# TODO: 生产环境应改为 IsAdminUser 权限
authentication_classes = []
permission_classes = [AllowAny]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.service = SystemLogService()
def get(self, request):
"""
获取系统日志
支持通过 lines 参数控制返回行数,用于前端分页或实时刷新场景。
"""
try:
# 解析 lines 参数
lines_raw = request.query_params.get("lines")
lines = int(lines_raw) if lines_raw is not None else None
# 调用服务获取日志内容
content = self.service.get_logs_content(lines=lines)
return Response({"content": content})
except ValueError:
return Response({"error": "lines 参数必须是整数"}, status=status.HTTP_400_BAD_REQUEST)
except Exception:
logger.exception("获取系统日志失败")
return Response({"error": "获取系统日志失败"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

View File

@@ -203,7 +203,12 @@ class TaskDistributor:
host_logs_dir = settings.HOST_LOGS_DIR # /opt/xingrin/logs
# 环境变量:只需 SERVER_URL其他配置容器启动时从配置中心获取
env_vars = [f"-e SERVER_URL={shlex.quote(server_url)}"]
# Prefect 本地模式配置:禁用 API server 和事件系统
env_vars = [
f"-e SERVER_URL={shlex.quote(server_url)}",
"-e PREFECT_API_URL=", # 禁用 API server
"-e PREFECT_LOGGING_EXTRA_LOGGERS=", # 禁用 Prefect 的额外内部日志器
]
# 挂载卷
volumes = [

View File

@@ -157,6 +157,51 @@ class ScanService:
"""取消所有正在运行的阶段(委托给 ScanStateService"""
return self.state_service.cancel_running_stages(scan_id, final_status)
# todo待接入
def add_command_to_scan(self, scan_id: int, stage_name: str, tool_name: str, command: str) -> bool:
"""
增量添加命令到指定扫描阶段
Args:
scan_id: 扫描任务ID
stage_name: 阶段名称(如 'subdomain_discovery', 'port_scan'
tool_name: 工具名称
command: 执行命令
Returns:
bool: 是否成功添加
"""
try:
scan = self.get_scan(scan_id, prefetch_relations=False)
if not scan:
logger.error(f"扫描任务不存在: {scan_id}")
return False
stage_progress = scan.stage_progress or {}
# 确保指定阶段存在
if stage_name not in stage_progress:
stage_progress[stage_name] = {'status': 'running', 'commands': []}
# 确保 commands 列表存在
if 'commands' not in stage_progress[stage_name]:
stage_progress[stage_name]['commands'] = []
# 增量添加命令
command_entry = f"{tool_name}: {command}"
stage_progress[stage_name]['commands'].append(command_entry)
scan.stage_progress = stage_progress
scan.save(update_fields=['stage_progress'])
command_count = len(stage_progress[stage_name]['commands'])
logger.info(f"✓ 记录命令: {stage_name}.{tool_name} (总计: {command_count})")
return True
except Exception as e:
logger.error(f"记录命令失败: {e}")
return False
# ==================== 删除和控制方法(委托给 ScanControlService ====================
def delete_scans_two_phase(self, scan_ids: List[int]) -> dict:
@@ -167,6 +212,20 @@ class ScanService:
"""停止扫描任务(委托给 ScanControlService"""
return self.control_service.stop_scan(scan_id)
def hard_delete_scans(self, scan_ids: List[int]) -> tuple[int, Dict[str, int]]:
"""
硬删除扫描任务(真正删除数据)
用于 Worker 容器中执行,删除已软删除的扫描及其关联数据。
Args:
scan_ids: 扫描任务 ID 列表
Returns:
(删除数量, 详情字典)
"""
return self.scan_repo.hard_delete_by_ids(scan_ids)
# ==================== 统计方法(委托给 ScanStatsService ====================
def get_statistics(self) -> dict:

View File

@@ -27,10 +27,10 @@ check_docker() {
# ==================== Docker Compose 命令检测 ====================
detect_compose_cmd() {
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
elif docker compose version >/dev/null 2>&1; then
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
else
log_error "未检测到 docker-compose 或 docker compose。"
exit 1

View File

@@ -42,10 +42,10 @@ if ! docker info >/dev/null 2>&1; then
exit 1
fi
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
elif docker compose version >/dev/null 2>&1; then
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD="docker-compose"
else
echo -e "${RED}[ERROR]${NC} 未检测到 docker compose请先安装"
exit 1
@@ -85,38 +85,68 @@ export BUILDKIT_INLINE_CACHE=1
# 使用指定的 compose 文件
COMPOSE_ARGS="-f ${COMPOSE_FILE} ${PROFILE_ARG}"
SERVICES="$(${COMPOSE_CMD} ${COMPOSE_ARGS} config --services)"
service_exists() {
echo "$SERVICES" | grep -qx "$1"
}
BACKEND_SERVICES=()
for s in redis server agent; do
if service_exists "$s"; then
BACKEND_SERVICES+=("$s")
fi
done
# 如果使用本地数据库,先启动 postgres 并等待健康
start_postgres_first() {
if [ -n "$PROFILE_ARG" ]; then
echo -e "${CYAN}[DB]${NC} 启动 PostgreSQL 容器..."
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d postgres
echo -e "${CYAN}[DB]${NC} 等待 PostgreSQL 就绪..."
local max_wait=30
local count=0
while [ $count -lt $max_wait ]; do
if ${COMPOSE_CMD} ${COMPOSE_ARGS} exec -T postgres pg_isready -U postgres >/dev/null 2>&1; then
echo -e "${GREEN}[DB]${NC} PostgreSQL 已就绪"
return 0
fi
sleep 1
count=$((count + 1))
done
echo -e "${YELLOW}[WARN]${NC} PostgreSQL 等待超时,继续启动其他服务..."
fi
}
echo ""
if [ "$DEV_MODE" = true ]; then
# 开发模式:本地构建
if [ "$WITH_FRONTEND" = true ]; then
echo -e "${CYAN}[BUILD]${NC} 并行构建镜像..."
${COMPOSE_CMD} ${COMPOSE_ARGS} build --parallel
start_postgres_first
echo -e "${CYAN}[START]${NC} 启动全部服务..."
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d
else
echo -e "${CYAN}[BUILD]${NC} 并行构建后端镜像..."
${COMPOSE_CMD} ${COMPOSE_ARGS} build --parallel server scan-worker maintenance-worker
${COMPOSE_CMD} ${COMPOSE_ARGS} build --parallel "${BACKEND_SERVICES[@]}"
start_postgres_first
echo -e "${CYAN}[START]${NC} 启动后端服务..."
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d redis server scan-worker maintenance-worker
if [ -n "$PROFILE_ARG" ]; then
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d postgres
fi
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d "${BACKEND_SERVICES[@]}"
fi
else
# 生产模式:拉取 Docker Hub 镜像
if [ "$WITH_FRONTEND" = true ]; then
echo -e "${CYAN}[PULL]${NC} 拉取最新镜像..."
${COMPOSE_CMD} ${COMPOSE_ARGS} pull
start_postgres_first
echo -e "${CYAN}[START]${NC} 启动全部服务..."
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d
else
echo -e "${CYAN}[PULL]${NC} 拉取后端镜像..."
${COMPOSE_CMD} ${COMPOSE_ARGS} pull redis server scan-worker maintenance-worker
${COMPOSE_CMD} ${COMPOSE_ARGS} pull "${BACKEND_SERVICES[@]}"
start_postgres_first
echo -e "${CYAN}[START]${NC} 启动后端服务..."
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d redis server scan-worker maintenance-worker
if [ -n "$PROFILE_ARG" ]; then
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d postgres
fi
${COMPOSE_CMD} ${COMPOSE_ARGS} up -d "${BACKEND_SERVICES[@]}"
fi
fi
echo -e "${GREEN}[OK]${NC} 服务已启动"

View File

@@ -4,6 +4,8 @@
### 架构设计
- [版本管理架构](./version-management.md) - Git Tag 驱动的自动化版本管理系统
- [Nuclei 模板架构](./nuclei-template-architecture.md) - 模板仓库的存储、同步、分发机制
- [字典文件架构](./wordlist-architecture.md) - 字典文件的存储、同步、分发机制
### 开发指南
- [快速开始](./quick-start.md) - 一键安装和部署指南

View File

@@ -0,0 +1,229 @@
# Nuclei 模板管理架构
本文档介绍 XingRin 中 Nuclei 模板的存储、同步和使用机制。
## 目录结构
```
/opt/xingrin/nuclei-repos/
├── nuclei-templates/ # 官方模板仓库(按仓库名命名)
│ ├── .git/
│ ├── http/
│ ├── network/
│ └── ...
└── custom-repo/ # 自定义模板仓库
```
## 一、存储位置
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `NUCLEI_TEMPLATES_REPOS_BASE_DIR` | `/opt/xingrin/nuclei-repos` | 模板仓库根目录 |
每个模板仓库会在根目录下创建独立子目录,目录名由仓库名称 slugify 生成。
## 二、数据模型
```
NucleiTemplateRepo
├── id # 仓库 ID
├── name # 仓库名称(用于前端展示和 Worker 查询)
├── repo_url # Git 仓库地址
├── local_path # 本地克隆路径(自动生成)
├── commit_hash # 当前同步的 commit hash
└── last_synced_at # 最后同步时间
```
## 三、Server 端同步流程
1. 用户在前端添加模板仓库(填写名称和 Git URL
2. 点击「同步」触发 `NucleiTemplateRepoService.refresh_repo()`
3. 首次同步:`git clone --depth 1`(浅克隆,节省空间)
4. 后续同步:`git pull --ff-only`(快进合并)
5. 同步成功后更新数据库:`commit_hash``last_synced_at`
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Server 容器 │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 前端 UI │ │
│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ 添加仓库 │ │ 同步仓库 │ │ 浏览模板 │ │ │
│ │ │ name + url │ │ 点击刷新 │ │ 目录树 │ │ │
│ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │ │
│ └─────────┼───────────────────┼───────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ NucleiTemplateRepoViewSet │ │
│ │ POST /api/nuclei/repos/ | POST .../refresh/ │ │
│ └─────────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ NucleiTemplateRepoService │ │
│ │ │ │
│ │ ┌────────────────────┐ ┌────────────────────────────────┐ │ │
│ │ │ ensure_local_path()│ │ refresh_repo() │ │ │
│ │ │ 生成本地目录路径 │ │ 执行 Git 同步 │ │ │
│ │ └────────────────────┘ └───────────────┬────────────────┘ │ │
│ └────────────────────────────────────────────┼────────────────────┘ │
│ │ │
│ ┌───────────────┴───────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ 首次同步(无 .git │ │ 后续同步(有 .git │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ git clone --depth 1 │ │ git pull --ff-only │ │
│ │ <repo_url> │ │ │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ └──────────────┬─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ git rev-parse HEAD │ │
│ │ 获取当前 commit hash │ │
│ └──────────────────────────┬──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL 数据库 │ │
│ │ │ │
│ │ UPDATE nuclei_template_repo SET │ │
│ │ local_path = '/opt/xingrin/nuclei-repos/xxx', │ │
│ │ commit_hash = 'abc123...', │ │
│ │ last_synced_at = NOW() │ │
│ │ WHERE id = ? │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 文件系统 │ │
│ │ /opt/xingrin/nuclei-repos/ │ │
│ │ ├── nuclei-templates/ # 官方模板 │ │
│ │ │ ├── .git/ │ │
│ │ │ ├── http/ │ │
│ │ │ ├── network/ │ │
│ │ │ └── ... │ │
│ │ └── custom-repo/ # 自定义模板 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
│ git clone / pull
┌─────────────────────┐
│ GitHub / GitLab │
│ 远程 Git 仓库 │
└─────────────────────┘
```
## 四、Worker 端同步流程
Worker 执行扫描任务时,通过 `ensure_nuclei_templates_local()` 确保本地模板与 Server 版本一致:
1. 从数据库查询仓库记录,获取 `repo_url``commit_hash`
2. 检查本地是否存在仓库目录
- 不存在:`git clone --depth 1`
- 存在:比较本地 commit hash 与 Server 的 `commit_hash`
3. 如果 commit 不一致:`git fetch` + `git checkout <commit_hash>`
4. 返回本地模板目录路径,供 nuclei 命令使用
```
┌──────────────────────────────────────────────────────────────────────────┐
│ Worker 容器 │
│ │
│ ┌─────────────┐ │
│ │ 扫描任务 │ │
│ │ 开始执行 │ │
│ └──────┬──────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ ensure_nuclei_ │ │ PostgreSQL │ │
│ │ templates_local() │─────▶│ 查询 NucleiTemplateRepo 表 │ │
│ │ │ │ 获取 repo_url, commit_hash │ │
│ └───────────┬─────────────┘ └─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ 检查本地 .git 目录 │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ┌───────┴───────┐ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────┐ ┌────────────┐ │
│ │ 不存在 │ │ 存在 │ │
│ └───┬────┘ └─────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────┐ ┌─────────────────────┐ │
│ │ git clone │ │ 比较 commit hash │ │
│ │ --depth 1 │ │ local vs server │ │
│ └─────┬──────┘ └──────────┬──────────┘ │
│ │ │ │
│ │ ┌───────┴───────┐ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────┐ ┌──────────────┐ │
│ │ │ 一致 │ │ 不一致 │ │
│ │ │ 直接使用 │ │ │ │
│ │ └────┬─────┘ └───────┬──────┘ │
│ │ │ │ │
│ │ │ ▼ │
│ │ │ ┌──────────────────┐ │
│ │ │ │ git fetch origin │ │
│ │ │ │ git checkout │ │
│ │ │ │ <commit_hash> │ │
│ │ │ └────────┬─────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 返回本地模板目录路径 │ │
│ │ /opt/xingrin/nuclei-repos/<repo-name>/ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 执行 nuclei 扫描 │ │
│ │ nuclei -t /opt/xingrin/nuclei-repos/xxx/ -l targets.txt │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
```
## 五、版本一致性保证
- Server 同步时记录 `commit_hash`
- Worker 使用前检查本地 hash 是否与 Server 一致
- 不一致时自动同步到指定 commit
- 确保所有节点使用相同版本的模板
## 六、配置项
`docker/.env` 或环境变量中配置:
```bash
# Nuclei 模板仓库根目录
NUCLEI_TEMPLATES_REPOS_BASE_DIR=/opt/xingrin/nuclei-repos
```
## 七、常见问题
### Q: Worker 报错「未找到模板仓库」?
A: 需要先在 Server 端添加并同步模板仓库Worker 通过数据库查询仓库信息。
### Q: 如何添加自定义模板仓库?
A: 在前端「Nuclei 模板」页面点击添加,填写仓库名称和 Git URL然后点击同步即可。
### Q: 模板更新后 Worker 如何获取最新版本?
A: 在 Server 端点击「同步」更新模板Worker 下次执行扫描时会检测到 commit hash 不一致并自动同步。

View File

@@ -0,0 +1,257 @@
# 字典文件管理架构
本文档介绍 XingRin 中字典文件的存储、同步和使用机制。
## 目录结构
```
/opt/xingrin/wordlists/
├── common.txt # 通用字典
├── subdomains.txt # 子域名字典
├── directories.txt # 目录字典
└── ...
```
## 一、存储位置
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `WORDLISTS_BASE_PATH` | `/opt/xingrin/wordlists` | 字典文件存储目录 |
## 二、数据模型
```
Wordlist
├── id # 字典 ID
├── name # 字典名称(唯一,用于查询)
├── description # 描述
├── file_path # 文件绝对路径
├── file_size # 文件大小(字节)
├── line_count # 行数
└── file_hash # SHA256 哈希值(用于校验)
```
## 三、Server 端上传流程
1. 用户在前端上传字典文件
2. `WordlistService.create_wordlist()` 处理:
- 保存文件到 `WORDLISTS_BASE_PATH` 目录
- 计算 SHA256 哈希值
- 统计文件大小和行数
- 创建数据库记录
```
┌──────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
```
## 四、Worker 端获取流程
Worker 执行扫描任务时,通过 `ensure_wordlist_local()` 获取字典:
1. 根据字典名称查询数据库,获取 `file_path``file_hash`
2. 检查本地是否存在字典文件
- 存在且 hash 匹配:直接使用
- 存在但 hash 不匹配:重新下载
- 不存在:从 Server API 下载
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 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘
```
## 五、Hash 校验机制
- 上传时计算 SHA256 并存入数据库
- Worker 使用前校验本地文件 hash
- 不匹配时自动重新下载
- 确保所有节点使用相同内容的字典
## 六、本地 Worker vs 远程 Worker
本地 Worker 和远程 Worker 获取字典的方式相同:
1. 从数据库查询字典元数据file_hash
2. 检查本地缓存是否存在且 hash 匹配
3. 不匹配则通过 HTTP API 下载
**注意**Worker 容器只挂载了 `results``logs` 目录,没有挂载 `wordlists` 目录,所以字典文件需要通过 API 下载。
```
Worker本地/远程) Server
│ │
│ 1. 查询数据库获取 file_hash │
│─────────────────────────────────▶│
│ │
│ 2. 检查本地缓存 │
│ - 存在且 hash 匹配 → 直接使用│
│ - 不存在或不匹配 → 继续下载 │
│ │
│ 3. GET /api/wordlists/download/ │
│─────────────────────────────────▶│
│ │
│ 4. 返回文件内容 │
│◀─────────────────────────────────│
│ │
│ 5. 保存到本地缓存 │
│ /opt/xingrin/wordlists/ │
│ │
```
### 本地 Worker 的优势
虽然获取方式相同,但本地 Worker 有以下优势:
- 网络延迟更低(容器内网络)
- 下载后的缓存可复用(同一宿主机上的多次任务)
## 七、配置项
`docker/.env` 或环境变量中配置:
```bash
# 字典文件存储目录
WORDLISTS_PATH=/opt/xingrin/wordlists
# Server 地址Worker 用于下载文件)
PUBLIC_HOST=your-server-ip
SERVER_PORT=8888
```
## 八、常见问题
### Q: 字典文件更新后 Worker 没有使用新版本?
A: 更新字典内容后会重新计算 hashWorker 下次使用时会检测到 hash 不匹配并重新下载。
### Q: 远程 Worker 下载文件失败?
A: 检查:
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP
2. Server 端口(默认 8888是否开放
3. Worker 到 Server 的网络是否通畅
### Q: 如何批量导入字典?
A: 目前只支持通过前端逐个上传,后续可能支持批量导入功能。

View File

@@ -0,0 +1,15 @@
"use client"
import { SystemLogsView } from "@/components/settings/system-logs"
export default function SystemLogsPage() {
return (
<div className="flex flex-1 flex-col gap-4 p-4">
<div>
<h1 className="text-2xl font-bold tracking-tight"></h1>
<p className="text-muted-foreground"></p>
</div>
<SystemLogsView />
</div>
)
}

View File

@@ -14,6 +14,7 @@ import {
IconTool, // 工具图标
IconFlask, // 实验瓶图标
IconServer, // 服务器图标
IconTerminal2, // 终端图标
IconBug, // 漏洞图标
} from "@tabler/icons-react"
// 导入路径名 hook
@@ -130,6 +131,11 @@ const data = {
url: "/settings/workers/",
icon: IconServer,
},
{
name: "系统日志",
url: "/settings/system-logs/",
icon: IconTerminal2,
},
{
name: "通知设置", // 通知设置
url: "/settings/notifications/",

View File

@@ -0,0 +1 @@
export { SystemLogsView } from "./system-logs-view"

View File

@@ -0,0 +1,58 @@
"use client"
import { useEffect, useMemo, useRef } from "react"
import Editor from "@monaco-editor/react"
import { useTheme } from "next-themes"
import { Card, CardContent } from "@/components/ui/card"
import { useSystemLogs } from "@/hooks/use-system-logs"
export function SystemLogsView() {
const { theme } = useTheme()
const { data } = useSystemLogs({ lines: 200 })
const content = useMemo(() => data?.content ?? "", [data?.content])
const editorRef = useRef<any>(null)
useEffect(() => {
const editor = editorRef.current
if (!editor) return
const model = editor.getModel?.()
if (!model) return
const lastLine = model.getLineCount?.() ?? 1
editor.revealLine?.(lastLine)
}, [content])
return (
<Card>
<CardContent>
<div className="h-[calc(100vh-240px)] min-h-[360px] rounded-lg border overflow-hidden">
<Editor
height="100%"
defaultLanguage="log"
value={content || "(暂无日志内容)"}
theme={theme === "dark" ? "vs-dark" : "light"}
onMount={(editor) => {
editorRef.current = editor
}}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "off",
scrollBeyondLastLine: false,
automaticLayout: true,
folding: false,
wordWrap: "off",
renderLineHighlight: "none",
padding: { top: 12, bottom: 12 },
}}
/>
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,32 @@
import { useEffect, useRef } from "react"
import { useQuery } from "@tanstack/react-query"
import { toast } from "sonner"
import { systemLogService } from "@/services/system-log.service"
export function useSystemLogs(options?: { lines?: number; enabled?: boolean }) {
const hadErrorRef = useRef(false)
const query = useQuery({
queryKey: ["system", "logs", { lines: options?.lines ?? null }],
queryFn: () => systemLogService.getSystemLogs({ lines: options?.lines }),
enabled: options?.enabled ?? true,
refetchInterval: 1000,
refetchIntervalInBackground: true,
retry: false,
})
useEffect(() => {
if (query.isError && !hadErrorRef.current) {
hadErrorRef.current = true
toast.error("系统日志获取失败,请检查后端接口")
}
if (query.isSuccess && hadErrorRef.current) {
hadErrorRef.current = false
toast.success("系统日志连接已恢复")
}
}, [query.isError, query.isSuccess])
return query
}

View File

@@ -0,0 +1,20 @@
import apiClient from "@/lib/api-client"
import type { SystemLogResponse } from "@/types/system-log.types"
const BASE_URL = "/system/logs"
export const systemLogService = {
async getSystemLogs(params?: { lines?: number }): Promise<SystemLogResponse> {
const searchParams = new URLSearchParams()
if (params?.lines != null) {
searchParams.set("lines", String(params.lines))
}
const query = searchParams.toString()
const url = query ? `${BASE_URL}/?${query}` : `${BASE_URL}/`
const response = await apiClient.get<SystemLogResponse>(url)
return response.data
},
}

View File

@@ -0,0 +1,3 @@
export interface SystemLogResponse {
content: string
}

View File

@@ -126,7 +126,7 @@ update_env_var() {
GENERATED_DB_PASSWORD=""
GENERATED_DJANGO_KEY=""
# 生成自签 HTTPS 证书(无域名场景)
# 生成自签 HTTPS 证书(无域名场景)——兼容旧版 OpenSSL
generate_self_signed_cert() {
local ssl_dir="$DOCKER_DIR/nginx/ssl"
local fullchain="$ssl_dir/fullchain.pem"
@@ -139,15 +139,42 @@ generate_self_signed_cert() {
info "未检测到 HTTPS 证书正在生成自签证书localhost..."
mkdir -p "$ssl_dir"
# 创建临时配置文件(兼容 OpenSSL 1.0.2
local config_file="/tmp/openssl-selfsigned.cnf"
cat > "$config_file" << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = CN
ST = NA
L = NA
O = XingRin
CN = localhost
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName = DNS:localhost,IP:127.0.0.1
EOF
if openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
-keyout "$privkey" \
-out "$fullchain" \
-subj "/C=CN/ST=NA/L=NA/O=XingRin/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" >/dev/null 2>&1; then
-config "$config_file" \
-extensions v3_req >/dev/null 2>&1; then
success "自签证书已生成: $ssl_dir"
else
warn "自签证书生成失败,请检查 openssl 是否可用,或手动放置证书到 $ssl_dir"
warn "自签证书生成失败(可能是 OpenSSL 版本过旧),请手动放置证书到 $ssl_dir"
warn "或者升级系统 OpenSSL或使用 Let's Encrypt 证书"
fi
# 清理临时配置文件
rm -f "$config_file"
}
# 自动为 docker/.env 填充敏感变量
@@ -388,6 +415,21 @@ fi
# 准备 HTTPS 证书(无域名也可使用自签)
generate_self_signed_cert
# ==============================================================================
# 预拉取 Worker 镜像(避免扫描时等待)
# ==============================================================================
step "预拉取 Worker 镜像..."
DOCKER_USER=$(grep "^DOCKER_USER=" "$DOCKER_DIR/.env" 2>/dev/null | cut -d= -f2)
DOCKER_USER=${DOCKER_USER:-yyhuni}
WORKER_IMAGE="${DOCKER_USER}/xingrin-worker:${APP_VERSION}"
info "正在拉取: $WORKER_IMAGE"
if docker pull "$WORKER_IMAGE"; then
success "Worker 镜像拉取完成"
else
warn "Worker 镜像拉取失败,扫描时会自动重试拉取"
fi
# ==============================================================================
# 启动服务
# ==============================================================================

View File

@@ -95,16 +95,17 @@ fi
# ==============================================================================
# 2. 删除扫描日志和结果目录
# ==============================================================================
LOGS_DIR="$ROOT_DIR/backend/logs"
RESULTS_DIR="$ROOT_DIR/backend/results"
step "[2/6] 是否删除扫描日志和结果目录 ($LOGS_DIR, $RESULTS_DIR)(Y/n)"
OPT_LOGS_DIR="/opt/xingrin/logs"
OPT_RESULTS_DIR="/opt/xingrin/results"
step "[2/6] 是否删除扫描日志和结果目录 ($OPT_LOGS_DIR, $OPT_RESULTS_DIR)(Y/n)"
read -r ans_logs
ans_logs=${ans_logs:-Y}
if [[ $ans_logs =~ ^[Yy]$ ]]; then
info "正在删除日志和结果目录..."
rm -rf "$LOGS_DIR" "$RESULTS_DIR"
rm -rf "$OPT_LOGS_DIR" "$OPT_RESULTS_DIR"
success "已删除日志和结果目录。"
else
warn "已保留日志和结果目录。"