mirror of
https://github.com/yyhuni/xingrin.git
synced 2026-01-31 19:53:11 +08:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e60911cb3 | ||
|
|
5de7ea9dbc | ||
|
|
971641cdeb | ||
|
|
e5a74faf9f | ||
|
|
e9a58e89aa | ||
|
|
3d9d520dc7 | ||
|
|
8d814b5864 | ||
|
|
c16b7afabe | ||
|
|
fa55167989 | ||
|
|
55a2762c71 | ||
|
|
5532f1e63a | ||
|
|
948568e950 | ||
|
|
873b6893f1 | ||
|
|
dbb30f7c78 | ||
|
|
38eced3814 | ||
|
|
68fc7cee3b | ||
|
|
6e23824a45 | ||
|
|
a88cceb4f4 | ||
|
|
81164621d2 | ||
|
|
379abaeca7 | ||
|
|
de77057679 | ||
|
|
630747ed2b | ||
|
|
98c418ee8b |
2
.github/workflows/docker-build.yml
vendored
2
.github/workflows/docker-build.yml
vendored
@@ -102,6 +102,8 @@ jobs:
|
||||
${{ steps.version.outputs.IS_RELEASE == 'true' && format('{0}/{1}:latest', env.IMAGE_PREFIX, matrix.image) || '' }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
provenance: false
|
||||
sbom: false
|
||||
|
||||
# 所有镜像构建成功后,更新 VERSION 文件
|
||||
update-version:
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
1.后端网页应该是 8888 端口
|
||||
3.前端所有路由加上末尾斜杠,以匹配 django 的 DRF 规则
|
||||
4.网页测试可以用 curl
|
||||
8.所有前端 api 接口都应该写在@services 中,所有 type 类型都应该写在@types 中
|
||||
10.前端的加载等逻辑用 React Query来实现,自动管理
|
||||
17.所有业务操作的 toast 都放在 hook 中
|
||||
23.前端非必要不要采用window.location.href去跳转,而是用Next.js 客户端路由
|
||||
24.ui相关的都去调用mcp来看看有没有通用组件,美观的组件来实现
|
||||
@@ -1,85 +0,0 @@
|
||||
---
|
||||
trigger: manual
|
||||
description: 进行代码审查的时候,必须调用这个规则
|
||||
---
|
||||
|
||||
### **0. 逻辑正确性 & Bug 排查** *(最高优先级,必须手动推演)*
|
||||
|
||||
**目标**:不依赖测试,主动发现“代码能跑但结果错”的逻辑错误。
|
||||
|
||||
1. **手动推演关键路径**:
|
||||
- 选 2~3 个典型输入(含边界),**在脑中或纸上一步步推演代码执行流程**。
|
||||
- 输出是否符合预期?每一步变量变化是否正确?
|
||||
2. **常见逻辑 bug 检查**:
|
||||
- **off-by-one**:循环、数组索引、分页
|
||||
- **条件逻辑错误**:`and`/`or` 优先级、短路求值误用
|
||||
- **状态混乱**:变量未初始化、被意外覆盖
|
||||
- **算法偏差**:排序、搜索、二分查找的中点处理
|
||||
- **浮点精度**:是否误用 `==` 比较浮点数?
|
||||
3. **控制流审查**:
|
||||
- 所有 `if/else` 分支是否都覆盖?有无“不可达代码”?
|
||||
- `switch`/`match` 是否有 `default`?是否漏 case?
|
||||
- 异常路径会返回什么?是否遗漏 `finally` 清理?
|
||||
4. **业务逻辑一致性**:
|
||||
- 是否符合**业务规则**?(如“订单总额 = 商品价 × 数量 + 运费 - 折扣”)
|
||||
- 是否遗漏隐含约束?(如“用户只能评价已完成的订单”)
|
||||
|
||||
### **一、功能性 & 正确性** *(阻塞性问题必须修复)*
|
||||
|
||||
1. **需求符合度**:是否100%覆盖需求?遗漏/多余功能点?
|
||||
2. **边界条件**:
|
||||
- 输入:`null`、空、极值、非法格式
|
||||
- 集合:空、单元素、超大(如10⁶)
|
||||
- 循环:终止条件、off-by-one
|
||||
3. **错误处理**:
|
||||
- 异常捕获全面?失败路径有降级?
|
||||
- 错误信息清晰?不泄露栈迹?
|
||||
4. **并发安全**:
|
||||
- 竞态/死锁风险?共享资源是否同步?
|
||||
- 使用了`volatile`/`synchronized`/`Lock`/`atomic`?
|
||||
5. **单元测试**:
|
||||
- 覆盖率 ≥80%?包含正向/边界/异常用例?
|
||||
- 测试独立?无外部依赖?
|
||||
|
||||
### **二、代码质量与可读性**
|
||||
|
||||
1. **命名**:见名知意?遵循规范?
|
||||
2. **函数设计**:
|
||||
- **单一职责**?参数 ≤4?建议长度 <50行(视语言调整)
|
||||
- 可提取为工具函数?
|
||||
3. **结构与复杂度**:
|
||||
- 无重复代码?圈复杂度 <10?
|
||||
- 嵌套 ≤3层?使用卫语句提前返回
|
||||
4. **注释**:解释**为什么**而非**是什么**?复杂逻辑必注释
|
||||
5. **风格一致**:通过`Prettier`/`ESLint`/`Spotless`自动格式化
|
||||
|
||||
### **三、架构与设计**
|
||||
|
||||
1. **SOLID**:是否符合单一职责、开闭、依赖倒置?
|
||||
2. **依赖**:是否依赖接口而非实现?无循环依赖?
|
||||
3. **可测试性**:是否支持依赖注入?避免`new`硬编码
|
||||
4. **扩展性**:新增功能是否只需改一处?
|
||||
|
||||
### **四、性能优化**
|
||||
|
||||
- **N+1查询**?循环内IO/日志/分配?
|
||||
- 算法复杂度合理?(如O(n²)是否可优化)
|
||||
- 内存:无泄漏?大对象及时释放?缓存有失效?
|
||||
|
||||
### **五、其他**
|
||||
|
||||
1. **可维护性**:日志带上下文?修改后更干净?
|
||||
2. **兼容性**:API/数据库变更是否向后兼容?
|
||||
3. **依赖管理**:新库必要?许可证合规?
|
||||
|
||||
---
|
||||
|
||||
### **审查最佳实践**
|
||||
|
||||
- **小批次审查**:≤200行/次
|
||||
- **语气建议**:`“建议将函数拆分以提升可读性”` 而非 `“这个函数太长了”`
|
||||
- **自动化先行**:风格/空指针/安全扫描 → CI工具
|
||||
- **重点分级**:
|
||||
- 🛑 **阻塞**:功能错、安全漏洞
|
||||
- ⚠️ **必须改**:设计缺陷、性能瓶颈
|
||||
- 💡 **建议**:风格、命名、可读性
|
||||
@@ -1,195 +0,0 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
## 标准分层架构调用顺序
|
||||
|
||||
按照 **DDD(领域驱动设计)和清洁架构**原则,调用顺序应该是:
|
||||
|
||||
```
|
||||
HTTP请求 → Views → Tasks → Services → Repositories → Models
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📊 完整的调用链路图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ HTTP Request (前端) │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Views (HTTP 层) │
|
||||
│ - 参数验证 │
|
||||
│ - 权限检查 │
|
||||
│ - 调用 Tasks/Services │
|
||||
│ - 返回 HTTP 响应 │
|
||||
└────────────────────────┬────────────────────────────────────┘
|
||||
↓
|
||||
┌────────────────┴────────────────┐
|
||||
↓ (异步) ↓ (同步)
|
||||
┌──────────────────┐ ┌──────────────────┐
|
||||
│ Tasks (任务层) │ │ Services (业务层)│
|
||||
│ - 异步执行 │ │ - 业务逻辑 │
|
||||
│ - 后台作业 │───────>│ - 事务管理 │
|
||||
│ - 通知发送 │ │ - 数据验证 │
|
||||
└──────────────────┘ └────────┬─────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Repositories (存储层) │
|
||||
│ - 数据访问 │
|
||||
│ - 查询封装 │
|
||||
│ - 批量操作 │
|
||||
└────────┬─────────────┘
|
||||
↓
|
||||
┌──────────────────────┐
|
||||
│ Models (模型层) │
|
||||
│ - ORM 定义 │
|
||||
│ - 数据结构 │
|
||||
│ - 关系映射 │
|
||||
└──────────────────────┘
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔄 具体调用示例
|
||||
|
||||
### **场景 1:同步删除(Views → Services → Repositories → Models)**
|
||||
|
||||
```python
|
||||
# 1. Views 层 (views.py)
|
||||
def some_sync_delete(self, request):
|
||||
# 参数验证
|
||||
target_ids = request.data.get('ids')
|
||||
|
||||
# 调用 Service 层
|
||||
service = TargetService()
|
||||
result = service.bulk_delete_targets(target_ids)
|
||||
|
||||
# 返回响应
|
||||
return Response({'message': 'deleted'})
|
||||
|
||||
# 2. Services 层 (services/target_service.py)
|
||||
class TargetService:
|
||||
def bulk_delete_targets(self, target_ids):
|
||||
# 业务逻辑验证
|
||||
logger.info("准备删除...")
|
||||
|
||||
# 调用 Repository 层
|
||||
deleted_count = self.repo.bulk_delete_by_ids(target_ids)
|
||||
|
||||
# 返回结果
|
||||
return deleted_count
|
||||
|
||||
# 3. Repositories 层 (repositories/django_target_repository.py)
|
||||
class DjangoTargetRepository:
|
||||
def bulk_delete_by_ids(self, target_ids):
|
||||
# 数据访问操作
|
||||
return Target.objects.filter(id__in=target_ids).delete()
|
||||
|
||||
# 4. Models 层 (models.py)
|
||||
class Target(models.Model):
|
||||
# ORM 定义
|
||||
name = models.CharField(...)
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **场景 2:异步删除(Views → Tasks → Services → Repositories → Models)**
|
||||
|
||||
```python
|
||||
# 1. Views 层 (views.py)
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
target = self.get_object()
|
||||
|
||||
# 调用 Tasks 层(异步)
|
||||
async_bulk_delete_targets([target.id], [target.name])
|
||||
|
||||
# 立即返回 202
|
||||
return Response(status=202)
|
||||
|
||||
# 2. Tasks 层 (tasks/target_tasks.py)
|
||||
def async_bulk_delete_targets(target_ids, target_names):
|
||||
def _delete():
|
||||
# 发送通知
|
||||
create_notification("删除中...")
|
||||
|
||||
# 调用 Service 层
|
||||
service = TargetService()
|
||||
result = service.bulk_delete_targets(target_ids)
|
||||
|
||||
# 发送完成通知
|
||||
create_notification("删除成功")
|
||||
|
||||
# 后台线程执行
|
||||
threading.Thread(target=_delete).start()
|
||||
|
||||
# 3. Services 层 (services/target_service.py)
|
||||
class TargetService:
|
||||
def bulk_delete_targets(self, target_ids):
|
||||
# 业务逻辑
|
||||
return self.repo.bulk_delete_by_ids(target_ids)
|
||||
|
||||
# 4. Repositories 层 (repositories/django_target_repository.py)
|
||||
class DjangoTargetRepository:
|
||||
def bulk_delete_by_ids(self, target_ids):
|
||||
# 数据访问
|
||||
return Target.objects.filter(id__in=target_ids).delete()
|
||||
|
||||
# 5. Models 层 (models.py)
|
||||
class Target(models.Model):
|
||||
# ORM 定义
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📋 各层职责清单
|
||||
|
||||
| 层级 | 职责 | 不应该做 |
|
||||
| --- | --- | --- |
|
||||
| **Views** | HTTP 请求处理、参数验证、权限检查 | ❌ 直接访问 Models<br>❌ 业务逻辑 |
|
||||
| **Tasks** | 异步执行、后台作业、通知发送 | ❌ 直接访问 Models<br>❌ HTTP 响应 |
|
||||
| **Services** | 业务逻辑、事务管理、数据验证 | ❌ 直接写 SQL<br>❌ HTTP 相关 |
|
||||
| **Repositories** | 数据访问、查询封装、批量操作 | ❌ 业务逻辑<br>❌ 通知发送 |
|
||||
| **Models** | ORM 定义、数据结构、关系映射 | ❌ 业务逻辑<br>❌ 复杂查询 |
|
||||
|
||||
---
|
||||
|
||||
### ✅ 最佳实践原则
|
||||
|
||||
1. **单向依赖**:只能向下调用,不能向上调用
|
||||
|
||||
```
|
||||
Views → Tasks → Services → Repositories → Models
|
||||
(上层) (下层)
|
||||
|
||||
```
|
||||
|
||||
2. **层级隔离**:相邻层交互,禁止跨层
|
||||
- ✅ Views → Services
|
||||
- ✅ Tasks → Services
|
||||
- ✅ Services → Repositories
|
||||
- ❌ Views → Repositories(跨层)
|
||||
- ❌ Tasks → Models(跨层)
|
||||
3. **依赖注入**:通过构造函数注入依赖
|
||||
|
||||
```python
|
||||
class TargetService:
|
||||
def __init__(self):
|
||||
self.repo = DjangoTargetRepository() # 注入
|
||||
|
||||
```
|
||||
|
||||
4. **接口抽象**:使用 Protocol 定义接口
|
||||
|
||||
```python
|
||||
class TargetRepository(Protocol):
|
||||
def bulk_delete_by_ids(self, ids): ...
|
||||
|
||||
```
|
||||
@@ -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) - 字典文件的存储与同步
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -8,13 +8,32 @@
|
||||
2. 选择负载最低的 Worker(可能是本地或远程)
|
||||
3. 本地 Worker:直接执行 docker run
|
||||
4. 远程 Worker:通过 SSH 执行 docker run
|
||||
5. 任务执行完自动销毁容器
|
||||
5. 任务执行完自动销毁容器(--rm)
|
||||
|
||||
镜像版本管理:
|
||||
- 版本锁定:使用 settings.IMAGE_TAG 确保 server 和 worker 版本一致
|
||||
- 预拉取策略:安装时预拉取镜像,执行时使用 --pull=missing
|
||||
- 本地开发:可通过 TASK_EXECUTOR_IMAGE 环境变量指向本地镜像
|
||||
|
||||
环境变量注入:
|
||||
- Worker 容器不使用 env_file,通过 docker run -e 动态注入
|
||||
- 只注入 SERVER_URL,容器启动后从配置中心获取完整配置
|
||||
- 本地 Worker:SERVER_URL = http://server:{port}(Docker 内部网络)
|
||||
- 远程 Worker:SERVER_URL = http://{public_host}:{port}(公网地址)
|
||||
|
||||
任务启动流程:
|
||||
1. Server 调用 execute_scan_flow() 等方法提交任务
|
||||
2. select_best_worker() 从 Redis 读取心跳数据,选择负载最低的节点
|
||||
3. _build_docker_command() 构建完整的 docker run 命令:
|
||||
- 设置网络(本地加入 Docker 网络,远程不指定)
|
||||
- 注入环境变量(-e SERVER_URL=...)
|
||||
- 挂载结果和日志目录(-v)
|
||||
- 指定执行脚本(python -m apps.scan.scripts.xxx)
|
||||
4. _execute_docker_command() 执行命令:
|
||||
- 本地:subprocess.run() 直接执行
|
||||
- 远程:paramiko SSH 执行
|
||||
5. docker run -d 立即返回容器 ID,任务在后台执行
|
||||
|
||||
特点:
|
||||
- 负载感知:任务优先分发到最空闲的机器
|
||||
- 统一调度:本地和远程 Worker 使用相同的选择逻辑
|
||||
@@ -203,7 +222,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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -225,6 +225,13 @@ def _parse_and_validate_line(line: str) -> Optional[PortScanRecord]:
|
||||
ip = line_data.get('ip', '').strip()
|
||||
port = line_data.get('port')
|
||||
|
||||
logger.debug("解析到的主机名: %s, IP: %s, 端口: %s", host, ip, port)
|
||||
|
||||
if not host and ip:
|
||||
host = ip
|
||||
logger.debug("主机名为空,使用 IP 作为 host")
|
||||
|
||||
|
||||
# 步骤 4: 验证字段不为空
|
||||
if not host or not ip or port is None:
|
||||
logger.warning(
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 目前采用github action自动版本构建,用
|
||||
# git tag v1.0.9
|
||||
# git push origin v1.0.9
|
||||
# ============================================
|
||||
# Docker Hub 镜像推送脚本
|
||||
# 用途:构建并推送所有服务镜像到 Docker Hub
|
||||
|
||||
@@ -101,6 +101,18 @@ services:
|
||||
# SSL 证书挂载(方便更新)
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
|
||||
# Worker:扫描任务执行容器(开发模式下构建)
|
||||
worker:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: docker/worker/Dockerfile
|
||||
image: docker-worker:${IMAGE_TAG:-latest}-dev
|
||||
restart: "no"
|
||||
volumes:
|
||||
- /opt/xingrin/results:/app/backend/results
|
||||
- /opt/xingrin/logs:/app/backend/logs
|
||||
command: echo "Worker image built for development"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -79,20 +79,20 @@ ENV GOPATH=/root/go
|
||||
ENV PATH=/usr/local/go/bin:$PATH:$GOPATH/bin
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
|
||||
# 5. 安装 uv(超快的 Python 包管理器)
|
||||
RUN pip install uv --break-system-packages
|
||||
|
||||
# 安装 Python 依赖(使用 uv 并行下载,速度快 10-100 倍)
|
||||
COPY backend/requirements.txt .
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip install --system -r requirements.txt --break-system-packages && \
|
||||
rm -f /usr/local/lib/python3.*/dist-packages/argparse.py && \
|
||||
rm -rf /usr/local/lib/python3.*/dist-packages/__pycache__/argparse*
|
||||
|
||||
COPY --from=go-builder /usr/local/go /usr/local/go
|
||||
COPY --from=go-builder /go/bin/* /usr/local/bin/
|
||||
COPY --from=go-builder /usr/local/bin/massdns /usr/local/bin/massdns
|
||||
|
||||
# 5. 安装 uv( Python 包管理器)并安装 Python 依赖
|
||||
COPY backend/requirements.txt .
|
||||
RUN pip install uv --break-system-packages && \
|
||||
uv pip install --system -r requirements.txt --break-system-packages && \
|
||||
rm -f /usr/local/lib/python3.*/dist-packages/argparse.py && \
|
||||
rm -rf /usr/local/lib/python3.*/dist-packages/__pycache__/argparse* && \
|
||||
rm -rf /root/.cache/uv && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 6. 复制后端代码
|
||||
COPY backend /app/backend
|
||||
ENV PYTHONPATH=/app/backend
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
### 架构设计
|
||||
- [版本管理架构](./version-management.md) - Git Tag 驱动的自动化版本管理系统
|
||||
- [Nuclei 模板架构](./nuclei-template-architecture.md) - 模板仓库的存储、同步、分发机制
|
||||
- [字典文件架构](./wordlist-architecture.md) - 字典文件的存储、同步、分发机制
|
||||
|
||||
### 开发指南
|
||||
- [快速开始](./quick-start.md) - 一键安装和部署指南
|
||||
|
||||
229
docs/nuclei-template-architecture.md
Normal file
229
docs/nuclei-template-architecture.md
Normal 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 不一致并自动同步。
|
||||
@@ -152,13 +152,13 @@ sequenceDiagram
|
||||
|
||||
### 本地开发测试
|
||||
```bash
|
||||
# docker/.env 中添加
|
||||
TASK_EXECUTOR_IMAGE=docker-agent:latest # 指向本地构建镜像
|
||||
# docker/.env 中添加(开发模式会自动设置)
|
||||
TASK_EXECUTOR_IMAGE=docker-worker:v1.1.0-dev # 指向本地构建镜像
|
||||
```
|
||||
|
||||
### 开发模式启动
|
||||
```bash
|
||||
# 使用本地构建镜像
|
||||
# 使用本地构建镜像(自动构建并标记为 ${VERSION}-dev)
|
||||
./install.sh --dev
|
||||
./start.sh --dev
|
||||
```
|
||||
@@ -238,7 +238,8 @@ curl -s https://hub.docker.com/v2/repositories/yyhuni/xingrin-worker/tags/
|
||||
4. ✅ 使用 `docker system prune` 清理旧镜像
|
||||
|
||||
### 开发调试
|
||||
1. ✅ 本地测试使用 `--dev` 模式
|
||||
1. ✅ 本地测试使用 `--dev` 模式(自动构建 `docker-worker:${VERSION}-dev`)
|
||||
2. ✅ 远程测试先推送测试版本到 Hub
|
||||
3. ✅ 生产环境避免使用 `latest` 标签
|
||||
4. ✅ 版本回滚通过修改 `IMAGE_TAG` 实现
|
||||
3. ✅ 生产环境避免使用 `latest` 标签,始终使用明确版本号
|
||||
4. ✅ 开发环境使用 `-dev` 后缀区分开发版本
|
||||
5. ✅ 版本回滚通过修改 `IMAGE_TAG` 实现
|
||||
257
docs/wordlist-architecture.md
Normal file
257
docs/wordlist-architecture.md
Normal 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: 更新字典内容后会重新计算 hash,Worker 下次使用时会检测到 hash 不匹配并重新下载。
|
||||
|
||||
### Q: 远程 Worker 下载文件失败?
|
||||
|
||||
A: 检查:
|
||||
1. `PUBLIC_HOST` 是否配置为 Server 的外网 IP
|
||||
2. Server 端口(默认 8888)是否开放
|
||||
3. Worker 到 Server 的网络是否通畅
|
||||
|
||||
### Q: 如何批量导入字典?
|
||||
|
||||
A: 目前只支持通过前端逐个上传,后续可能支持批量导入功能。
|
||||
@@ -78,21 +78,21 @@ export function createIPAddressColumns(params: {
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
// IP 地址列
|
||||
// IP 列
|
||||
{
|
||||
accessorKey: "ip",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="IP 地址" />
|
||||
<DataTableColumnHeader column={column} title="IP Address" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<TruncatedCell value={row.original.ip} maxLength="ip" mono />
|
||||
),
|
||||
},
|
||||
// 关联主机名列
|
||||
// host 列
|
||||
{
|
||||
accessorKey: "hosts",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="关联主机" />
|
||||
<DataTableColumnHeader column={column} title="Hosts" />
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const hosts = getValue<string[]>()
|
||||
@@ -107,7 +107,7 @@ export function createIPAddressColumns(params: {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{displayHosts.map((host, index) => (
|
||||
<span key={index} className="text-sm font-mono">{host}</span>
|
||||
<TruncatedCell key={index} value={host} maxLength="host" mono />
|
||||
))}
|
||||
{hasMore && (
|
||||
<Badge variant="secondary" className="text-xs w-fit">
|
||||
@@ -118,11 +118,11 @@ export function createIPAddressColumns(params: {
|
||||
)
|
||||
},
|
||||
},
|
||||
// 发现时间列
|
||||
// discoveredAt 列
|
||||
{
|
||||
accessorKey: "discoveredAt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="发现时间" />
|
||||
<DataTableColumnHeader column={column} title="Discovered At" />
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue<string | undefined>()
|
||||
@@ -133,7 +133,7 @@ export function createIPAddressColumns(params: {
|
||||
{
|
||||
accessorKey: "ports",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="开放端口" />
|
||||
<DataTableColumnHeader column={column} title="Open Ports" />
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const ports = getValue<number[]>()
|
||||
@@ -191,7 +191,7 @@ export function createIPAddressColumns(params: {
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-3">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">所有开放端口 ({sortedPorts.length})</h4>
|
||||
<h4 className="font-medium text-sm">All Open Ports ({sortedPorts.length})</h4>
|
||||
<div className="flex flex-wrap gap-1 max-h-32 overflow-y-auto">
|
||||
{sortedPorts.map((port, index) => (
|
||||
<Badge
|
||||
|
||||
@@ -267,7 +267,7 @@ export const createTargetColumns = ({
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="目标名称" />
|
||||
<DataTableColumnHeader column={column} title="Target Name" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<TargetNameCell
|
||||
@@ -282,7 +282,7 @@ export const createTargetColumns = ({
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="类型" />
|
||||
<DataTableColumnHeader column={column} title="Type" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const type = row.getValue("type") as string | null
|
||||
|
||||
@@ -188,7 +188,7 @@ export const createEngineColumns = ({
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="引擎名称" />
|
||||
<DataTableColumnHeader column={column} title="Engine Name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue("name") as string
|
||||
|
||||
@@ -180,7 +180,7 @@ export const createScheduledScanColumns = ({
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="任务名称" />
|
||||
<DataTableColumnHeader column={column} title="Task Name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue("name") as string
|
||||
@@ -216,7 +216,7 @@ export const createScheduledScanColumns = ({
|
||||
{
|
||||
accessorKey: "engineName",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="扫描引擎" />
|
||||
<DataTableColumnHeader column={column} title="Scan Engine" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const engineName = row.getValue("engineName") as string
|
||||
@@ -283,7 +283,7 @@ export const createScheduledScanColumns = ({
|
||||
{
|
||||
accessorKey: "isEnabled",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="状态" />
|
||||
<DataTableColumnHeader column={column} title="Status" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const isEnabled = row.getValue("isEnabled") as boolean
|
||||
@@ -308,7 +308,7 @@ export const createScheduledScanColumns = ({
|
||||
{
|
||||
accessorKey: "nextRunTime",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="下次执行" />
|
||||
<DataTableColumnHeader column={column} title="Next Run" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const nextRunTime = row.getValue("nextRunTime") as string | undefined
|
||||
@@ -324,7 +324,7 @@ export const createScheduledScanColumns = ({
|
||||
{
|
||||
accessorKey: "runCount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="执行次数" />
|
||||
<DataTableColumnHeader column={column} title="Run Count" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const count = row.getValue("runCount") as number
|
||||
@@ -338,7 +338,7 @@ export const createScheduledScanColumns = ({
|
||||
{
|
||||
accessorKey: "lastRunTime",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="上次执行" />
|
||||
<DataTableColumnHeader column={column} title="Last Run" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const lastRunTime = row.getValue("lastRunTime") as string | undefined
|
||||
|
||||
@@ -100,7 +100,7 @@ export const createSubdomainColumns = ({
|
||||
{
|
||||
accessorKey: "discoveredAt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="发现时间" />
|
||||
<DataTableColumnHeader column={column} title="Discovered At" />
|
||||
),
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue<string | undefined>()
|
||||
|
||||
@@ -95,7 +95,7 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
{
|
||||
accessorKey: "displayName",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="名称" />
|
||||
<DataTableColumnHeader column={column} title="Name" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const displayName = row.getValue("displayName") as string
|
||||
@@ -136,7 +136,7 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
{
|
||||
accessorKey: "tool",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="所属工具" />
|
||||
<DataTableColumnHeader column={column} title="Tool" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const tool = row.original.tool
|
||||
@@ -156,7 +156,7 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
{
|
||||
accessorKey: "commandTemplate",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="命令模板" />
|
||||
<DataTableColumnHeader column={column} title="Command Template" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const template = row.getValue("commandTemplate") as string
|
||||
@@ -192,7 +192,7 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="描述" />
|
||||
<DataTableColumnHeader column={column} title="Description" />
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const description = row.getValue("description") as string
|
||||
@@ -217,7 +217,7 @@ export const commandColumns: ColumnDef<Command>[] = [
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title="更新时间" />
|
||||
<DataTableColumnHeader column={column} title="Updated At" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
|
||||
43
install.sh
43
install.sh
@@ -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,14 +139,18 @@ generate_self_signed_cert() {
|
||||
|
||||
info "未检测到 HTTPS 证书,正在生成自签证书(localhost)..."
|
||||
mkdir -p "$ssl_dir"
|
||||
if openssl req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout "$privkey" \
|
||||
-out "$fullchain" \
|
||||
|
||||
# 使用容器生成证书,避免依赖宿主机 openssl 版本
|
||||
if docker run --rm -v "$ssl_dir:/ssl" alpine/openssl \
|
||||
req -x509 -nodes -newkey rsa:2048 -days 365 \
|
||||
-keyout /ssl/privkey.pem \
|
||||
-out /ssl/fullchain.pem \
|
||||
-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
|
||||
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
|
||||
>/dev/null 2>&1; then
|
||||
success "自签证书已生成: $ssl_dir"
|
||||
else
|
||||
warn "自签证书生成失败,请检查 openssl 是否可用,或手动放置证书到 $ssl_dir"
|
||||
warn "自签证书生成失败,请手动放置证书到 $ssl_dir"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -225,7 +229,7 @@ show_summary() {
|
||||
|
||||
step "[1/3] 检查基础命令"
|
||||
MISSING_CMDS=()
|
||||
for cmd in git curl jq openssl; do
|
||||
for cmd in git curl; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
MISSING_CMDS+=("$cmd")
|
||||
warn "未安装: $cmd"
|
||||
@@ -396,11 +400,28 @@ 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 镜像拉取完成"
|
||||
# 开发模式下构建本地 worker 镜像
|
||||
if [ "$DEV_MODE" = true ]; then
|
||||
info "开发模式:构建本地 Worker 镜像..."
|
||||
if docker compose -f "$DOCKER_DIR/docker-compose.dev.yml" build worker; then
|
||||
# 设置 TASK_EXECUTOR_IMAGE 环境变量指向本地构建的镜像(使用版本号-dev标识)
|
||||
update_env_var "$DOCKER_DIR/.env" "TASK_EXECUTOR_IMAGE" "docker-worker:${APP_VERSION}-dev"
|
||||
success "本地 Worker 镜像构建完成: docker-worker:${APP_VERSION}-dev"
|
||||
else
|
||||
error "开发模式下本地 Worker 镜像构建失败!"
|
||||
error "请检查构建错误并修复后重试"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
warn "Worker 镜像拉取失败,扫描时会自动重试拉取"
|
||||
info "正在拉取: $WORKER_IMAGE"
|
||||
if docker pull "$WORKER_IMAGE"; then
|
||||
success "Worker 镜像拉取完成"
|
||||
else
|
||||
error "Worker 镜像拉取失败,无法继续安装"
|
||||
error "请检查网络连接或 Docker Hub 访问权限"
|
||||
error "镜像地址: $WORKER_IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
Reference in New Issue
Block a user