Remove .kiro directory from version control

This commit is contained in:
yyhuni
2026-01-23 09:28:09 +08:00
parent 64bcd9a6f5
commit ca6c0eb082
55 changed files with 0 additions and 19149 deletions

View File

@@ -1,13 +0,0 @@
{
"enabled": true,
"name": "Code Quality Analyzer",
"description": "Analyzes modified source code for potential improvements, including code smells, design patterns, and best practices. Generates suggestions for improving code quality while maintaining existing functionality, focusing on readability, maintainability, and performance optimizations.",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "Review the code changes I just made and analyze for potential improvements. Please check for:\n\n1. **Code Smells**: Long methods, duplicate code, dead code, magic numbers, complex conditionals\n2. **Design Patterns**: Opportunities to apply appropriate design patterns, or misuse of existing patterns\n3. **Best Practices**: Naming conventions, error handling, logging, type hints/annotations\n4. **Readability**: Code clarity, comments, function/variable naming, code organization\n5. **Maintainability**: Single responsibility, coupling, cohesion, testability\n6. **Performance**: Inefficient algorithms, unnecessary computations, memory leaks, N+1 queries\n\nProvide specific, actionable suggestions while ensuring the existing functionality is preserved. Prioritize the most impactful improvements."
}
}

View File

@@ -1,15 +0,0 @@
{
"enabled": true,
"name": "Code Simplifier",
"description": "Simplifies and refines code for clarity, consistency, and maintainability while preserving all functionality. Focuses on recently modified code unless instructed otherwise.",
"version": "1",
"when": {
"type": "userTriggered"
},
"then": {
"type": "askAgent",
"prompt": "You are an expert code simplification specialist. Analyze the recently modified code and apply refinements that:\n\n1. **Preserve Functionality**: Never change what the code does - only how it does it. All original features, outputs, and behaviors must remain intact.\n\n2. **Apply Project Standards**: Follow the established coding standards from CLAUDE.md including:\n - Use ES modules with proper import sorting and extensions\n - Prefer `function` keyword over arrow functions\n - Use explicit return type annotations for top-level functions\n - Follow proper React component patterns with explicit Props types\n - Use proper error handling patterns (avoid try/catch when possible)\n - Maintain consistent naming conventions\n\n3. **Enhance Clarity**: Simplify code structure by:\n - Reducing unnecessary complexity and nesting\n - Eliminating redundant code and abstractions\n - Improving readability through clear variable and function names\n - Consolidating related logic\n - Removing unnecessary comments that describe obvious code\n - IMPORTANT: Avoid nested ternary operators - prefer switch statements or if/else chains for multiple conditions\n - Choose clarity over brevity - explicit code is often better than overly compact code\n\n4. **Maintain Balance**: Avoid over-simplification that could:\n - Reduce code clarity or maintainability\n - Create overly clever solutions that are hard to understand\n - Combine too many concerns into single functions or components\n - Remove helpful abstractions that improve code organization\n - Prioritize \"fewer lines\" over readability (e.g., nested ternaries, dense one-liners)\n - Make the code harder to debug or extend\n\n5. **Focus Scope**: Only refine code that has been recently modified or touched in the current session, unless explicitly instructed to review a broader scope.\n\nYour refinement process:\n1. Identify the recently modified code sections\n2. Analyze for opportunities to improve elegance and consistency\n3. Apply project-specific best practices and coding standards\n4. Ensure all functionality remains unchanged\n5. Verify the refined code is simpler and more maintainable\n6. Document only significant changes that affect understanding\n\nApply these refinements now to the modified file, ensuring all functionality is preserved while improving code quality."
},
"workspaceFolderName": "xingrin",
"shortName": "code-simplifier"
}

View File

@@ -1,190 +0,0 @@
# 扫描架构重构设计文档
## 1. 架构模式管道模式Pipeline Pattern
### 1.1 现有管道架构
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 扫描管道 (Scan Pipeline) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 子域名 │ → │ 端口 │ → │ 网站 │ → │ 指纹 │ → │ 爬虫 │ → ... │
│ │ 发现 │ │ 扫描 │ │ 识别 │ │ 识别 │ │ URL获取 │ │
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
│ ↓ ↓ ↓ ↓ ↓ │
│ 保存DB 保存DB 保存DB 保存DB 保存DB │
│ │
│ 特点:固定顺序 → 单向流动 → 可预测 → 易调试 │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 1.2 管道模式的优势
| 特点 | 说明 |
|------|------|
| **固定顺序** | 阶段顺序预先定义好,执行路径确定 |
| **单向流动** | 数据从前一阶段流向后一阶段 |
| **可预测** | 知道会执行哪些步骤,资源消耗可估算 |
| **易调试** | 每个阶段独立,问题容易定位 |
---
## 2. 核心问题:数据源硬编码
### 2.1 问题描述
```
┌─────────────────────────────────────────────────────────────────┐
│ 现有问题 │
│ │
│ 用户输入: https://api.example.com/v1/users │
│ │
│ 期望: 只扫描这一个 URL │
│ │
│ 实际: 扫描 example.com 下所有 500+ 个网站 │
│ 因为每个 Task 都用 target_id 查数据库 │
│ │
│ ┌─────────┐ │
│ │ Task │ → SELECT * FROM subdomains WHERE target_id = 123 │
│ └─────────┘ 返回 500+ 条记录,全部扫描 │
└─────────────────────────────────────────────────────────────────┘
```
### 2.2 根本原因
| 问题 | 描述 | 影响 |
|------|------|------|
| **数据源硬编码** | 每个 Flow/Task 都用 `target_id` 查数据库 | 无法精准扫描用户输入的特定目标 |
---
## 3. 解决方案:策略模式
### 3.1 设计思路
```
┌─────────────────────────────────────────────────────────────────┐
│ 数据源抽象(策略模式) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ DatabaseProvider│ │ ListProvider │ │ SnapshotProvider│ │
│ │ (target_id查询) │ │ (用户输入列表) │ │ (快照表查询) │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ ↓ ↓ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 统一的 TargetProvider 接口 │ │
│ │ get_targets() → List[Target] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 扫描管道 │ │
│ │ 只扫描 Provider 返回的目标 │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### 3.2 使用场景对比
| 场景 | 使用的 Provider | 数据来源 | 扫描范围 |
|------|-----------------|----------|----------|
| **完整扫描** | DatabaseProvider | target_id 查数据库 | 目标下所有资产 |
| **快速扫描** | ListProvider | 用户输入的列表 | 只扫描指定的目标 |
| **阶段间传递** | SnapshotProvider | 上阶段保存的快照 | 上阶段发现的目标 |
### 3.3 快速扫描流程
```
┌─────────────────────────────────────────────────────────────────┐
│ 快速扫描流程 │
│ │
│ 用户输入: ["api.example.com", "admin.example.com"] │
│ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ ListProvider │ ← 直接使用用户输入的列表 │
│ │ get_targets() │ │
│ └─────────────────┘ │
│ ↓ │
│ 返回: ["api.example.com", "admin.example.com"] │
│ ↓ │
│ ┌─────────────────┐ │
│ │ 扫描管道 │ ← 只扫描这 2 个目标 │
│ └─────────────────┘ │
│ ↓ │
│ 结果: 只扫描了用户指定的 2 个目标,而不是 500+ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 4. 实施状态
### 4.1 Phase 1策略模式 ✅ 已完成
**目标**:数据源可插拔,支持精准扫描
**已实现的 Provider**
| Provider | 用途 | 状态 |
|----------|------|------|
| `DatabaseTargetProvider` | 从数据库查询(完整扫描) | ✅ 已完成 |
| `ListTargetProvider` | 直接使用内存列表(快速扫描) | ✅ 已完成 |
| `SnapshotTargetProvider` | 从快照表查询(阶段间传递) | ✅ 已完成 |
| `PipelineTargetProvider` | 使用上阶段输出 | ✅ 已完成 |
**改动范围**`backend/apps/scan/providers/`
**验收标准**
- [x] 现有扫描功能不受影响(向后兼容)
- [x] 单元测试覆盖所有 Provider
- [x] 属性测试验证正确性
### 4.2 待完成工作
| 任务 | 描述 | 状态 |
|------|------|------|
| 集成到更多 Task | 将 TargetProvider 集成到所有需要的 Task | 进行中 |
| API 支持 | 前端 API 支持传入目标列表 | 待实现 |
| 文档更新 | 更新使用文档 | 待实现 |
---
## 5. 管道模式 vs 动态路由
| 维度 | 管道模式(当前架构) | 动态路由 |
|------|----------------------|----------|
| **数据流向** | ✅ 单向、固定 | ❌ 多向、动态 |
| **执行顺序** | ✅ 预定义、可预测 | ❌ 运行时决定 |
| **复杂度** | ✅ 低 | ❌ 高(队列、规则、决策) |
| **可预测性** | ✅ 确定 | ❌ 不确定 |
| **调试难度** | ✅ 易定位 | ❌ 需要追溯决策链路 |
| **资源控制** | ✅ 可估算 | ❌ 可能递归爆炸 |
| **维护成本** | ✅ 简单直观 | ❌ 规则越来越复杂 |
| **适用场景** | SRC 挖掘、定向扫描 | 探索性扫描 |
**结论**:管道模式 + 策略模式完全满足 SRC 挖掘需求,通过 TargetProvider 抽象解决精准扫描问题。
---
## 6. 总结
### 架构模式
本项目采用**管道模式Pipeline Pattern**
- 数据按固定顺序流经各个处理阶段
- 每个阶段独立处理,结果传递给下一阶段
- 流程可预测、易调试、资源可控
### 核心改进
通过**策略模式**抽象数据源,解决"快速扫描时扫描所有资产"的问题:
- `DatabaseProvider`:完整扫描,查询目标下所有资产
- `ListProvider`:快速扫描,只扫描用户指定的目标
- `SnapshotProvider`:阶段间传递,使用上阶段发现的目标
### 设计原则
- **保持管道模式**:不引入动态路由的复杂性
- **策略模式**:数据源可插拔,支持多种扫描场景
- **向后兼容**:默认使用 DatabaseProvider不影响现有功能

View File

@@ -1,28 +0,0 @@
{
"mcpServers": {
"shadcn-ui": {
"command": "npx",
"args": [
"@jpisnice/shadcn-ui-mcp-server",
"--github-api-key",
"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"
],
"autoApprove": [
"list_components",
"get_component",
"get_component_demo",
"list_blocks"
]
},
"shadcn": {
"command": "npx",
"args": [
"shadcn@latest",
"mcp"
],
"autoApprove": [
"search_items_in_registries"
]
}
}
}

View File

@@ -1,644 +0,0 @@
# Design Document
## Overview
本文档描述基于 API 的种子数据生成器的设计。该生成器是一个 Python 脚本,通过调用 Go 后端的 REST API 来创建测试数据。设计重点是独立性、可靠性和易用性。
## Architecture
### 整体架构
```
┌─────────────────────────────────────────────────────┐
│ Python Seed Generator │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ CLI Parser │ │ API Client │ │ Data │ │
│ │ │ │ │ │ Generator │ │
│ └──────┬───────┘ └──────┬───────┘ └─────┬─────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
└───────────────────────────┼─────────────────────────┘
│ HTTP/JSON
┌──────────────────┐
│ Go Backend │
│ (Port 8888) │
└──────────────────┘
```
### 模块划分
| 模块 | 职责 | 文件 |
|------|------|------|
| CLI Parser | 解析命令行参数 | `seed_generator.py` (main) |
| API Client | HTTP 请求封装、认证管理 | `api_client.py` |
| Data Generator | 生成随机测试数据 | `data_generator.py` |
| Progress Tracker | 进度显示和统计 | `progress.py` |
| Error Handler | 错误处理和重试 | `error_handler.py` |
## Components and Interfaces
### 1. API Client
**职责:** 封装所有 HTTP 请求,管理认证 token
**接口:**
```python
class APIClient:
def __init__(self, base_url: str, username: str, password: str):
"""初始化 API 客户端"""
def login(self) -> str:
"""登录并获取 JWT token"""
def refresh_token(self) -> str:
"""刷新过期的 token"""
def post(self, endpoint: str, data: dict) -> dict:
"""发送 POST 请求"""
def get(self, endpoint: str, params: dict = None) -> dict:
"""发送 GET 请求"""
def delete(self, endpoint: str) -> None:
"""发送 DELETE 请求"""
```
**实现细节:**
- 使用 `requests.Session` 保持连接
- 自动在请求头中添加 `Authorization: Bearer {token}`
- Token 过期时自动调用 `refresh_token()`
- 所有请求使用 30 秒超时
- 返回解析后的 JSON 数据
### 2. Data Generator
**职责:** 生成随机但合理的测试数据
**接口:**
```python
class DataGenerator:
@staticmethod
def generate_organization(index: int) -> dict:
"""生成组织数据"""
@staticmethod
def generate_targets(count: int, target_type_ratios: dict) -> list[dict]:
"""生成目标数据域名70%、IP20%、CIDR10%"""
@staticmethod
def generate_websites(target: dict, count: int) -> list[dict]:
"""为目标生成 Website 数据"""
@staticmethod
def generate_subdomains(target: dict, count: int) -> list[dict]:
"""为域名目标生成 Subdomain 数据"""
@staticmethod
def generate_endpoints(target: dict, count: int) -> list[dict]:
"""为目标生成 Endpoint 数据"""
@staticmethod
def generate_directories(target: dict, count: int) -> list[dict]:
"""为目标生成 Directory 数据"""
@staticmethod
def generate_host_ports(target: dict, count: int) -> list[dict]:
"""为目标生成 HostPort 数据"""
@staticmethod
def generate_vulnerabilities(target: dict, count: int) -> list[dict]:
"""为目标生成 Vulnerability 数据"""
```
**数据模板:**
- 组织名称:从预定义列表中选择 + 随机后缀
- 域名:`{env}.{company}-{suffix}.{tld}` 格式
- IP随机生成合法的 IPv4 地址
- CIDR随机生成 /8、/16、/24 网段
- URL根据目标类型生成合理的 URL
- 技术栈:从常见技术中随机选择
### 3. Progress Tracker
**职责:** 显示生成进度和统计信息
**接口:**
```python
class ProgressTracker:
def __init__(self):
"""初始化进度跟踪器"""
def start_phase(self, phase_name: str, total: int):
"""开始新阶段"""
def update(self, count: int):
"""更新进度"""
def add_success(self, count: int):
"""记录成功数量"""
def add_error(self, error: str):
"""记录错误"""
def finish_phase(self):
"""完成当前阶段"""
def print_summary(self):
"""打印总结"""
```
**显示格式:**
```
🏢 Creating organizations... [15/15] ✓ 15 created
🎯 Creating targets... [225/225] ✓ 225 created (domains: 157, IPs: 45, CIDRs: 23)
🔗 Linking targets to organizations... [225/225] ✓ 225 links created
🌐 Creating websites... [3375/3375] ✓ 3375 created
📝 Creating subdomains... [2355/2355] ✓ 2355 created (157 domain targets)
...
✅ Test data generation completed!
Total time: 45.2s
Success: 12,000 records
Errors: 3 records
```
### 4. Error Handler
**职责:** 处理 API 错误和重试逻辑
**接口:**
```python
class ErrorHandler:
def __init__(self, max_retries: int = 3, retry_delay: float = 1.0):
"""初始化错误处理器"""
def should_retry(self, status_code: int) -> bool:
"""判断是否应该重试"""
def handle_error(self, error: Exception, context: dict) -> bool:
"""处理错误,返回是否应该继续"""
def log_error(self, error: str, request_data: dict = None, response_data: dict = None):
"""记录错误详情"""
```
**重试策略:**
| 状态码 | 行为 | 重试次数 |
|--------|------|----------|
| 5xx | 自动重试 | 3 次 |
| 429 | 等待后重试 | 3 次 |
| 401 | 刷新 token 后重试 | 1 次 |
| 4xx (其他) | 记录错误,跳过 | 0 次 |
| 网络超时 | 重试 | 3 次 |
## Data Models
### JSON 请求格式
所有 JSON 使用 **camelCase** 字段名(符合前端规范):
**创建组织:**
```python
{
"name": "Acme Corporation - Global (5123-0)",
"description": "A leading technology company..."
}
```
**批量创建目标:**
```python
{
"targets": [
{"name": "example.com", "type": "domain"},
{"name": "192.168.1.1", "type": "ip"},
{"name": "10.0.0.0/8", "type": "cidr"}
]
}
```
**关联目标到组织:**
```python
{
"targetIds": [1, 2, 3, 4, 5]
}
```
**批量创建 Website**
```python
{
"websites": [
{
"url": "https://www.example.com",
"title": "Welcome - Dashboard",
"statusCode": 200,
"contentLength": 1500,
"contentType": "text/html; charset=utf-8",
"webserver": "nginx/1.24.0",
"tech": ["nginx", "PHP", "MySQL"],
"vhost": false
}
]
}
```
**批量创建 Subdomain**
```python
{
"subdomains": [
{"name": "www.example.com"},
{"name": "api.example.com"}
]
}
```
**批量创建 Endpoint**
```python
{
"endpoints": [
{
"url": "https://api.example.com/v1/users",
"title": "User Service",
"statusCode": 200,
"contentLength": 500,
"contentType": "application/json",
"webserver": "nginx/1.24.0",
"tech": ["nginx", "Node.js", "Express"],
"matchedGfPatterns": ["cors", "ssrf"],
"vhost": false
}
]
}
```
**批量创建 Directory**
```python
{
"directories": [
{
"url": "https://www.example.com/admin/",
"status": 403,
"contentLength": 1200,
"contentType": "text/html",
"duration": 55
}
]
}
```
**批量创建 HostPort**
```python
{
"hostPorts": [
{
"host": "www.example.com",
"ip": "192.168.1.10",
"port": 443
}
]
}
```
**批量创建 Vulnerability**
```python
{
"vulnerabilities": [
{
"url": "https://www.example.com/login",
"vulnType": "SQL Injection",
"severity": "critical",
"source": "nuclei",
"cvssScore": 9.8,
"description": "A SQL injection vulnerability was found..."
}
]
}
```
## Correctness Properties
*属性是一个特征或行为,应该在系统的所有有效执行中保持为真——本质上是关于系统应该做什么的正式陈述。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*
### Property 1: 认证 Token 有效性
*对于任何* API 请求,如果 token 有效,则请求应该成功;如果 token 过期,则应该自动刷新后重试成功
**Validates: Requirements 1.2, 1.3**
### Property 2: 批量创建幂等性
*对于任何* 批量创建请求,重复发送相同的数据应该不会创建重复记录(由于 API 的 ON CONFLICT DO NOTHING
**Validates: Requirements 3.4, 5.4, 6.4, 7.4, 8.4, 9.4, 10.4**
### Property 3: 目标类型分布
*对于任何* 目标生成请求生成的目标类型分布应该接近指定的比例域名70%、IP20%、CIDR10%误差±5%
**Validates: Requirements 3.2**
### Property 4: 资产归属验证
*对于任何* 资产Website、Subdomain、Endpoint 等),其 URL/名称应该与所属目标匹配域名匹配、IP 匹配、CIDR 范围内)
**Validates: Requirements 5.2, 6.2, 7.2, 8.2, 9.2**
### Property 5: JSON 字段命名一致性
*对于任何* API 响应,所有字段名应该使用 camelCase 格式,不应该出现 snake_case
**Validates: Requirements 15.4**
### Property 6: 错误重试收敛性
*对于任何* 5xx 错误或网络超时,重试次数应该不超过 3 次,且最终要么成功要么记录失败
**Validates: Requirements 12.1, 12.3**
### Property 7: 进度显示单调性
*对于任何* 生成阶段,显示的进度数字应该单调递增,且最终等于总数
**Validates: Requirements 13.1, 13.2**
### Property 8: 批量操作分批一致性
*对于任何* 批量操作,如果总数超过批次大小,则应该分批发送,且所有批次的总和等于原始总数
**Validates: Requirements 3.4, 5.4, 6.4, 7.4, 8.4, 9.4, 10.4**
### Property 9: 数据清理顺序正确性
*对于任何* 清理操作,删除顺序应该遵循外键约束(先删除子表,再删除父表),不应该出现外键冲突错误
**Validates: Requirements 14.2**
### Property 10: 组织目标分配均匀性
*对于任何* 目标关联操作,每个组织分配的目标数量应该大致相等(误差不超过 1
**Validates: Requirements 4.2**
## Error Handling
### 错误分类
| 错误类型 | HTTP 状态码 | 处理策略 |
|----------|-------------|----------|
| 认证失败 | 401 | 刷新 token 后重试 1 次 |
| 权限不足 | 403 | 记录错误,终止程序 |
| 资源不存在 | 404 | 记录错误,跳过该记录 |
| 请求格式错误 | 400 | 记录详细错误(包含请求 JSON跳过该记录 |
| 资源冲突 | 409 | 记录警告,跳过该记录(可能是重复数据) |
| 限流 | 429 | 等待 5 秒后重试,最多 3 次 |
| 服务器错误 | 5xx | 等待 1 秒后重试,最多 3 次 |
| 网络超时 | Timeout | 等待 1 秒后重试,最多 3 次 |
| 连接失败 | ConnectionError | 等待 2 秒后重试,最多 3 次 |
### 错误日志格式
```python
{
"timestamp": "2026-01-14T10:30:45Z",
"error_type": "API_ERROR",
"status_code": 400,
"endpoint": "/api/targets/1/websites/bulk-create",
"request": {
"websites": [...]
},
"response": {
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid URL format"
}
},
"retry_count": 0
}
```
### 错误恢复
- **部分失败策略:** 批量操作中,单条记录失败不影响其他记录
- **断点续传:** 记录已成功创建的数据 ID失败后可以从断点继续
- **回滚机制:** 提供 `--clear` 参数清空所有数据,重新开始
## Testing Strategy
### 单元测试
**测试范围:**
- Data Generator 的数据生成逻辑
- API Client 的请求构造
- Error Handler 的重试逻辑
- Progress Tracker 的统计计算
**测试工具:** `pytest`
**示例测试:**
```python
def test_generate_domain_target():
"""测试域名目标生成"""
target = DataGenerator.generate_target("domain", 0)
assert target["type"] == "domain"
assert "." in target["name"]
assert not target["name"].startswith(".")
def test_api_client_auto_refresh_token(mock_api):
"""测试 token 自动刷新"""
client = APIClient("http://localhost:8888", "admin", "admin")
# 模拟 token 过期
mock_api.set_token_expired()
# 应该自动刷新 token 并重试
response = client.post("/api/targets", {"name": "test.com", "type": "domain"})
assert response["id"] > 0
assert mock_api.refresh_called
def test_error_handler_retry_on_5xx():
"""测试 5xx 错误重试"""
handler = ErrorHandler(max_retries=3)
assert handler.should_retry(500) == True
assert handler.should_retry(503) == True
assert handler.should_retry(400) == False
```
### 集成测试
**测试范围:**
- 完整的数据生成流程
- API 调用的正确性
- 错误处理和重试
- 进度显示
**测试环境:** 本地 Go 后端(端口 8888
**测试步骤:**
1. 启动 Go 后端
2. 运行种子生成器小规模2 个组织10 个目标)
3. 验证数据是否正确创建
4. 验证 JSON 字段命名camelCase
5. 验证错误处理(模拟网络错误)
### 手动测试
**测试场景:**
| 场景 | 命令 | 预期结果 |
|------|------|----------|
| 小规模生成 | `python seed_generator.py --orgs 2` | 快速完成,数据正确 |
| 大规模生成 | `python seed_generator.py --orgs 50` | 进度显示正常,无内存问题 |
| 清空数据 | `python seed_generator.py --clear` | 所有数据被删除 |
| 网络中断 | 生成过程中断开网络 | 自动重试,显示错误 |
| 认证失败 | 使用错误的密码 | 显示认证错误,退出 |
| API 不可用 | 后端未启动 | 显示连接错误,退出 |
## Implementation Notes
### 项目位置
脚本放在 `tools/seed-api/` 目录下,与 Go 后端分离:
```
项目根目录/
├── go-backend/ # Go 后端
├── backend/ # Python 后端(旧)
├── frontend/ # 前端
└── tools/ # 工具脚本
└── seed-api/ # API 种子数据生成器 ⭐
├── seed_generator.py # 主程序入口
├── api_client.py # API 客户端
├── data_generator.py # 数据生成器
├── progress.py # 进度跟踪
├── error_handler.py # 错误处理
├── requirements.txt # Python 依赖
└── README.md # 使用说明
```
**选择 `tools/seed-api/` 的原因:**
1. ✅ 独立于后端代码,不会被误认为是后端的一部分
2. ✅ 与其他工具脚本放在一起,便于管理
3. ✅ 可以独立运行,不依赖 Go 后端的构建
4. ✅ 便于版本控制和分发
### 代码组织
**模块化设计 - 每个文件一个职责:**
| 文件 | 行数估计 | 职责 |
|------|----------|------|
| `seed_generator.py` | ~150 行 | 主程序CLI 参数解析、流程编排 |
| `api_client.py` | ~200 行 | API 客户端HTTP 请求、认证管理 |
| `data_generator.py` | ~400 行 | 数据生成:生成各类测试数据 |
| `progress.py` | ~100 行 | 进度跟踪:显示进度和统计 |
| `error_handler.py` | ~150 行 | 错误处理:重试逻辑、错误日志 |
**总计:** ~1000 行代码,分 5 个文件
**不要写成单文件的原因:**
- ❌ 单文件 1000+ 行难以维护
- ❌ 职责不清晰,难以测试
- ❌ 难以复用(比如 API Client 可以用于其他脚本)
- ✅ 模块化便于单元测试
- ✅ 每个文件可以独立理解和修改
### 命令行参数
```bash
python seed_generator.py [OPTIONS]
Options:
--api-url URL API 地址 (默认: http://localhost:8888)
--username USER 用户名 (默认: admin)
--password PASS 密码 (默认: admin)
--orgs N 组织数量 (默认: 15)
--targets-per-org N 每个组织的目标数量 (默认: 15)
--assets-per-target N 每个目标的资产数量 (默认: 15)
--clear 清空现有数据
--batch-size N 批量操作的批次大小 (默认: 100)
--verbose 显示详细日志
--help 显示帮助信息
```
### 性能优化
1. **批量操作:** 使用批量 APIbulk-create、bulk-upsert减少请求次数
2. **连接复用:** 使用 `requests.Session` 复用 HTTP 连接
3. **并发控制:** 单线程顺序执行(避免复杂性,性能已足够)
4. **内存优化:** 分批生成数据,避免一次性加载所有数据到内存
### 依赖管理
**requirements.txt:**
```
requests>=2.31.0
```
**安装命令:**
```bash
pip install -r requirements.txt
```
### 使用示例
```bash
# 1. 进入工具目录
cd tools/seed-api
# 2. 安装依赖
pip install -r requirements.txt
# 3. 启动 Go 后端(另一个终端)
cd ../../go-backend
make run
# 4. 生成测试数据(默认配置)
python seed_generator.py
# 5. 生成大规模测试数据
python seed_generator.py --orgs 50 --targets-per-org 20
# 6. 清空数据后重新生成
python seed_generator.py --clear --orgs 10
# 7. 使用自定义 API 地址
python seed_generator.py --api-url http://192.168.1.100:8888
```
### 模块导入关系
```python
# seed_generator.py (主程序)
from api_client import APIClient
from data_generator import DataGenerator
from progress import ProgressTracker
from error_handler import ErrorHandler
# api_client.py (独立模块)
import requests
# data_generator.py (独立模块)
import random
from typing import List, Dict
# progress.py (独立模块)
from datetime import datetime
# error_handler.py (独立模块)
import json
from typing import Optional
```
**模块间依赖:**
- `seed_generator.py` 依赖所有其他模块
- 其他模块相互独立,便于测试和复用

View File

@@ -1,205 +0,0 @@
# Requirements Document
## Introduction
本文档定义了基于 API 的种子数据生成器的需求。该生成器将通过调用 Go 后端的 REST API 来创建测试数据,而不是直接操作数据库。这种方式能够测试完整的 API 流程,包括路由、中间件、验证、序列化等,更接近真实的生产环境。
**实现语言Python**
选择 Python 的原因:
1. 最适合脚本任务,代码简洁
2. JSON 操作自然Python 字典 = JSON
3. 完全独立于 Go 后端代码,真正测试 API
4. requests 库成熟,错误处理简单
5. 调试方便,易于维护
## Glossary
- **Seed_Generator**: 种子数据生成器,用于创建测试数据的 Python 脚本
- **API_Client**: HTTP 客户端,使用 Python requests 库发送 API 请求
- **JWT_Token**: JSON Web Token用于 API 认证
- **Batch_API**: 批量操作 API支持一次创建多条记录
- **Test_Data**: 测试数据,包括组织、目标、资产等
- **Asset**: 资产,包括 Website、Subdomain、Endpoint、Directory、HostPort 等
- **Independent_JSON**: 独立构造的 JSON不依赖后端 DTO 结构体
## Requirements
### Requirement 1: 认证管理
**User Story:** 作为种子数据生成器,我需要通过 API 进行身份认证,以便访问受保护的 API 端点。
#### Acceptance Criteria
1. WHEN 生成器启动时THE Seed_Generator SHALL 调用 `/api/auth/login` 获取 JWT token
2. WHEN token 获取成功后THE Seed_Generator SHALL 在所有后续请求中携带 Authorization header
3. IF token 过期THEN THE Seed_Generator SHALL 自动调用 `/api/auth/refresh` 刷新 token
4. WHEN 认证失败时THE Seed_Generator SHALL 返回明确的错误信息并退出
### Requirement 2: 组织数据生成
**User Story:** 作为测试人员,我需要生成多个组织数据,以便测试多租户场景。
#### Acceptance Criteria
1. WHEN 用户指定组织数量时THE Seed_Generator SHALL 调用 `/api/organizations` POST 接口创建组织
2. WHEN 创建组织时THE Seed_Generator SHALL 生成随机但合理的组织名称和描述
3. WHEN 组织创建成功后THE Seed_Generator SHALL 保存组织 ID 用于后续关联
4. WHEN API 返回错误时THE Seed_Generator SHALL 记录错误详情并继续处理其他数据
### Requirement 3: 目标数据生成
**User Story:** 作为测试人员我需要生成不同类型的目标域名、IP、CIDR以便测试各种扫描场景。
#### Acceptance Criteria
1. WHEN 用户指定目标数量时THE Seed_Generator SHALL 调用 `/api/targets/batch_create` 批量创建目标
2. WHEN 生成目标时THE Seed_Generator SHALL 按比例生成域名70%、IP20%、CIDR10%
3. WHEN 目标创建成功后THE Seed_Generator SHALL 保存目标 ID 用于后续资产创建
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 4: 目标与组织关联
**User Story:** 作为测试人员,我需要将目标关联到组织,以便测试组织的目标管理功能。
#### Acceptance Criteria
1. WHEN 目标和组织都创建完成后THE Seed_Generator SHALL 调用 `/api/organizations/:id/link_targets` 关联目标
2. WHEN 关联目标时THE Seed_Generator SHALL 平均分配目标到各个组织
3. WHEN 关联失败时THE Seed_Generator SHALL 记录失败的组织和目标 ID
4. WHEN 批量关联时THE Seed_Generator SHALL 每批次不超过 50 个目标
### Requirement 5: Website 资产生成
**User Story:** 作为测试人员,我需要为每个目标生成 Website 资产,以便测试 Website 列表和导出功能。
#### Acceptance Criteria
1. WHEN 目标创建完成后THE Seed_Generator SHALL 调用 `/api/targets/:id/websites/bulk-upsert` 创建 Website
2. WHEN 生成 Website 时THE Seed_Generator SHALL 根据目标类型生成合理的 URL
3. WHEN 生成 Website 时THE Seed_Generator SHALL 包含 title、statusCode、tech 等字段
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 6: Subdomain 资产生成
**User Story:** 作为测试人员,我需要为域名类型的目标生成 Subdomain 资产,以便测试子域名发现功能。
#### Acceptance Criteria
1. WHEN 目标类型为 domain 时THE Seed_Generator SHALL 调用 `/api/targets/:id/subdomains/bulk-create` 创建 Subdomain
2. WHEN 目标类型不是 domain 时THE Seed_Generator SHALL 跳过该目标的 Subdomain 生成
3. WHEN 生成 Subdomain 时THE Seed_Generator SHALL 使用常见的子域名前缀www、api、admin 等)
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 7: Endpoint 资产生成
**User Story:** 作为测试人员,我需要为每个目标生成 Endpoint 资产,以便测试端点发现和分析功能。
#### Acceptance Criteria
1. WHEN 目标创建完成后THE Seed_Generator SHALL 调用 `/api/targets/:id/endpoints/bulk-upsert` 创建 Endpoint
2. WHEN 生成 Endpoint 时THE Seed_Generator SHALL 包含 URL、statusCode、tech、matchedGFPatterns 等字段
3. WHEN 生成 Endpoint 时THE Seed_Generator SHALL 使用常见的 API 路径(/api/v1/users、/login 等)
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 8: Directory 资产生成
**User Story:** 作为测试人员,我需要为每个目标生成 Directory 资产,以便测试目录扫描功能。
#### Acceptance Criteria
1. WHEN 目标创建完成后THE Seed_Generator SHALL 调用 `/api/targets/:id/directories/bulk-upsert` 创建 Directory
2. WHEN 生成 Directory 时THE Seed_Generator SHALL 包含 URL、status、contentLength 等字段
3. WHEN 生成 Directory 时THE Seed_Generator SHALL 使用常见的目录路径(/admin/、/backup/ 等)
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 9: HostPort 资产生成
**User Story:** 作为测试人员,我需要为每个目标生成 HostPort 映射,以便测试端口扫描功能。
#### Acceptance Criteria
1. WHEN 目标创建完成后THE Seed_Generator SHALL 调用 `/api/targets/:id/host-ports/bulk-upsert` 创建 HostPort
2. WHEN 生成 HostPort 时THE Seed_Generator SHALL 包含 host、ip、port 字段
3. WHEN 生成 HostPort 时THE Seed_Generator SHALL 使用常见的端口80、443、22、3306 等)
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 10: Vulnerability 数据生成
**User Story:** 作为测试人员,我需要为每个目标生成漏洞数据,以便测试漏洞管理功能。
#### Acceptance Criteria
1. WHEN 目标创建完成后THE Seed_Generator SHALL 调用 `/api/targets/:id/vulnerabilities/bulk-create` 创建漏洞
2. WHEN 生成漏洞时THE Seed_Generator SHALL 包含 vulnType、severity、cvssScore 等字段
3. WHEN 生成漏洞时THE Seed_Generator SHALL 按比例生成不同严重级别的漏洞
4. WHEN 批量创建时THE Seed_Generator SHALL 每批次不超过 100 条记录
### Requirement 11: 命令行参数
**User Story:** 作为测试人员,我需要通过命令行参数控制生成的数据量,以便灵活调整测试规模。
#### Acceptance Criteria
1. THE Seed_Generator SHALL 支持 `-orgs` 参数指定组织数量
2. THE Seed_Generator SHALL 支持 `-targets-per-org` 参数指定每个组织的目标数量
3. THE Seed_Generator SHALL 支持 `-assets-per-target` 参数指定每个目标的资产数量
4. THE Seed_Generator SHALL 支持 `-clear` 参数清空现有数据
5. THE Seed_Generator SHALL 支持 `-api-url` 参数指定 API 地址
6. THE Seed_Generator SHALL 支持 `-username``-password` 参数指定登录凭据
### Requirement 12: 错误处理和重试
**User Story:** 作为测试人员,我需要生成器能够处理 API 错误并重试,以便在网络不稳定时也能完成数据生成。
#### Acceptance Criteria
1. WHEN API 返回 5xx 错误时THE Seed_Generator SHALL 自动重试最多 3 次
2. WHEN API 返回 4xx 错误时THE Seed_Generator SHALL 记录错误并跳过该条记录
3. WHEN 重试次数耗尽时THE Seed_Generator SHALL 记录失败详情并继续处理其他数据
4. WHEN 遇到网络超时时THE Seed_Generator SHALL 等待 1 秒后重试
### Requirement 13: 进度显示
**User Story:** 作为测试人员,我需要看到数据生成的进度,以便了解生成过程的状态。
#### Acceptance Criteria
1. WHEN 生成器运行时THE Seed_Generator SHALL 显示当前正在处理的数据类型
2. WHEN 每个阶段完成时THE Seed_Generator SHALL 显示成功创建的记录数量
3. WHEN 发生错误时THE Seed_Generator SHALL 显示错误数量和详情
4. WHEN 生成完成时THE Seed_Generator SHALL 显示总耗时和统计信息
### Requirement 14: 数据清理
**User Story:** 作为测试人员,我需要在生成新数据前清空旧数据,以便从干净的状态开始测试。
#### Acceptance Criteria
1. WHEN 用户指定 `-clear` 参数时THE Seed_Generator SHALL 调用批量删除 API 清空数据
2. WHEN 清理数据时THE Seed_Generator SHALL 按正确的顺序删除(先删除资产,再删除目标,最后删除组织)
3. WHEN 清理失败时THE Seed_Generator SHALL 显示错误信息并询问是否继续
4. WHEN 清理完成时THE Seed_Generator SHALL 显示清理的记录数量
### Requirement 15: 独立 JSON 构造
**User Story:** 作为开发人员,我需要种子生成器独立构造 JSON 请求,而不是复用后端 DTO 结构体,以便发现 API 序列化问题。
#### Acceptance Criteria
1. THE Seed_Generator SHALL 使用 Python 字典独立构造请求 JSON
2. THE Seed_Generator SHALL NOT 导入或依赖 Go 后端的任何代码
3. WHEN API 返回错误时THE Seed_Generator SHALL 显示原始 JSON 请求和响应
4. THE Seed_Generator SHALL 验证响应字段是否符合预期的 camelCase 格式
5. WHEN 字段命名不一致时THE Seed_Generator SHALL 记录详细的错误信息
### Requirement 16: Python 环境依赖
**User Story:** 作为测试人员,我需要简单的环境配置,以便快速运行种子生成器。
#### Acceptance Criteria
1. THE Seed_Generator SHALL 使用 Python 3.8+ 版本
2. THE Seed_Generator SHALL 仅依赖 `requests` 库(通过 pip 安装)
3. THE Seed_Generator SHALL 提供 `requirements.txt` 文件列出依赖
4. THE Seed_Generator SHALL 在脚本开头检查依赖是否已安装

View File

@@ -1,222 +0,0 @@
# Implementation Plan: API-Based Seed Generator
## Overview
实现一个基于 API 的种子数据生成器,使用 Python 通过 HTTP 请求调用 Go 后端 API 来创建测试数据。项目采用模块化设计,分为 5 个独立文件,总计约 1000 行代码。
## Tasks
- [x] 1. 创建项目结构和基础文件
- 创建 `tools/seed-api/` 目录
- 创建 `requirements.txt` 文件(依赖 requests>=2.31.0
- 创建 `README.md` 文件(使用说明)
- 创建空的 Python 模块文件5 个 .py 文件)
- _Requirements: 16.3_
- [x] 2. 实现 API Client 模块
- [x] 2.1 实现 APIClient 类基础结构
- 实现 `__init__` 方法(初始化 base_url、username、password、Session
- 实现 `login` 方法POST /api/auth/login获取 JWT token
- 实现 `_get_headers` 方法(返回带 Authorization 的请求头)
- _Requirements: 1.1, 1.2_
- [x] 2.2 实现 HTTP 请求方法
- 实现 `post` 方法(发送 POST 请求,自动添加认证头)
- 实现 `get` 方法(发送 GET 请求,自动添加认证头)
- 实现 `delete` 方法(发送 DELETE 请求,自动添加认证头)
- 所有方法使用 30 秒超时
- _Requirements: 1.2_
- [x] 2.3 实现 Token 自动刷新
- 实现 `refresh_token` 方法POST /api/auth/refresh
- 在请求方法中捕获 401 错误,自动调用 refresh_token 后重试
- _Requirements: 1.3_
- [x] 2.4 实现错误处理
- 捕获 requests 异常Timeout、ConnectionError
- 解析 API 错误响应JSON 格式)
- 返回统一的错误信息
- _Requirements: 1.4_
- [x] 3. 实现 Error Handler 模块
- [x] 3.1 实现 ErrorHandler 类基础结构
- 实现 `__init__` 方法(初始化 max_retries、retry_delay
- 实现 `should_retry` 方法(判断状态码是否应该重试)
- _Requirements: 12.1, 12.2_
- [x] 3.2 实现重试逻辑
- 实现 `handle_error` 方法(根据错误类型决定是否重试)
- 5xx 错误重试 3 次,每次等待 1 秒
- 429 错误重试 3 次,每次等待 5 秒
- 网络超时重试 3 次,每次等待 1 秒
- _Requirements: 12.1, 12.4_
- [x] 3.3 实现错误日志
- 实现 `log_error` 方法(记录错误详情到文件)
- 日志包含时间戳、错误类型、请求数据、响应数据
- 日志文件:`seed_errors.log`
- _Requirements: 12.2, 15.3_
- [x] 4. 实现 Progress Tracker 模块
- [x] 4.1 实现 ProgressTracker 类基础结构
- 实现 `__init__` 方法(初始化统计变量)
- 实现 `start_phase` 方法(开始新阶段,记录阶段名和总数)
- 实现 `update` 方法(更新当前进度)
- _Requirements: 13.1_
- [x] 4.2 实现统计功能
- 实现 `add_success` 方法(记录成功数量)
- 实现 `add_error` 方法(记录错误信息)
- 实现 `finish_phase` 方法(完成当前阶段,显示总结)
- _Requirements: 13.2, 13.3_
- [x] 4.3 实现进度显示
- 使用 emoji 图标显示不同阶段(🏢 🎯 🔗 🌐 📝 等)
- 显示进度条格式:`[当前/总数]`
- 显示成功数量和错误数量
- _Requirements: 13.1, 13.2_
- [x] 4.4 实现总结报告
- 实现 `print_summary` 方法(打印最终统计)
- 显示总耗时、成功记录数、错误记录数
- _Requirements: 13.4_
- [x] 5. 实现 Data Generator 模块
- [x] 5.1 实现组织数据生成
- 实现 `generate_organization` 方法
- 使用预定义的组织名称列表 + 随机后缀
- 生成合理的描述文本
- 返回 Python 字典camelCase 字段名)
- _Requirements: 2.2, 15.1_
- [x] 5.2 实现目标数据生成
- 实现 `generate_targets` 方法
- 按比例生成域名70%、IP20%、CIDR10%
- 域名格式:`{env}.{company}-{suffix}.{tld}`
- IP 格式:随机合法 IPv4
- CIDR 格式:随机 /8、/16、/24 网段
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 3.2, 15.1_
- [x] 5.3 实现 Website 数据生成
- 实现 `generate_websites` 方法
- 根据目标类型生成合理的 URL
- 生成 title、statusCode、contentLength、tech 等字段
- tech 字段为数组(如 ["nginx", "PHP", "MySQL"]
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 5.2, 5.3, 15.1_
- [x] 5.4 实现 Subdomain 数据生成
- 实现 `generate_subdomains` 方法
- 仅为域名类型目标生成
- 使用常见子域名前缀www、api、admin 等)
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 6.2, 6.3, 15.1_
- [x] 5.5 实现 Endpoint 数据生成
- 实现 `generate_endpoints` 方法
- 生成 API 路径(/api/v1/users、/login 等)
- 生成 tech、matchedGfPatterns 数组字段
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 7.2, 7.3, 15.1_
- [x] 5.6 实现 Directory 数据生成
- 实现 `generate_directories` 方法
- 生成常见目录路径(/admin/、/backup/ 等)
- 生成 status、contentLength、duration 字段
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 8.2, 8.3, 15.1_
- [x] 5.7 实现 HostPort 数据生成
- 实现 `generate_host_ports` 方法
- 生成 host、ip、port 字段
- 使用常见端口80、443、22、3306 等)
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 9.2, 9.3, 15.1_
- [x] 5.8 实现 Vulnerability 数据生成
- 实现 `generate_vulnerabilities` 方法
- 生成 vulnType、severity、cvssScore 等字段
- 按比例生成不同严重级别
- 返回 Python 字典列表camelCase 字段名)
- _Requirements: 10.2, 10.3, 15.1_
- [x] 6. 实现主程序seed_generator.py
- [x] 6.1 实现命令行参数解析
- 使用 argparse 解析参数
- 支持 --api-url、--username、--password
- 支持 --orgs、--targets-per-org、--assets-per-target
- 支持 --clear、--batch-size、--verbose
- _Requirements: 11.1, 11.2, 11.3, 11.4, 11.5, 11.6_
- [x] 6.2 实现数据清理功能
- 调用批量删除 API 清空数据
- 按正确顺序删除(先资产,再目标,最后组织)
- 显示清理进度和结果
- _Requirements: 14.1, 14.2, 14.4_
- [x] 6.3 实现组织和目标创建流程
- 调用 API Client 登录获取 token
- 调用 POST /api/organizations 创建组织
- 调用 POST /api/targets/batch_create 批量创建目标
- 调用 POST /api/organizations/:id/link_targets 关联目标
- 使用 Progress Tracker 显示进度
- 使用 Error Handler 处理错误和重试
- _Requirements: 2.1, 2.3, 3.1, 3.4, 4.1, 4.2, 4.4_
- [x] 6.4 实现资产创建流程
- 调用 POST /api/targets/:id/websites/bulk-upsert 创建 Website
- 调用 POST /api/targets/:id/subdomains/bulk-create 创建 Subdomain
- 调用 POST /api/targets/:id/endpoints/bulk-upsert 创建 Endpoint
- 调用 POST /api/targets/:id/directories/bulk-upsert 创建 Directory
- 调用 POST /api/targets/:id/host-ports/bulk-upsert 创建 HostPort
- 调用 POST /api/targets/:id/vulnerabilities/bulk-create 创建 Vulnerability
- 每个资产类型分批发送batch_size=100
- 使用 Progress Tracker 显示进度
- 使用 Error Handler 处理错误和重试
- _Requirements: 5.1, 5.4, 6.1, 6.4, 7.1, 7.4, 8.1, 8.4, 9.1, 9.4, 10.1, 10.4_
- [x] 6.5 实现主流程编排
- 检查 Python 版本(需要 3.8+
- 检查 requests 库是否已安装
- 按顺序执行:清理 → 组织 → 目标 → 关联 → 资产
- 捕获所有异常,显示友好的错误信息
- 最后显示总结报告
- _Requirements: 16.1, 16.4_
- [x] 7. 编写文档和测试
- [x] 7.1 编写 README.md
- 说明项目用途和功能
- 列出依赖和安装步骤
- 提供使用示例和命令行参数说明
- 说明常见问题和解决方法
- _Requirements: 16.3_
- [x] 7.2 编写单元测试
- 测试 Data Generator 的数据生成逻辑
- 测试 API Client 的请求构造
- 测试 Error Handler 的重试逻辑
- 使用 pytest 框架
- _Requirements: 所有需求_
- [x] 7.3 执行集成测试
- 启动 Go 后端
- 运行种子生成器小规模2 个组织10 个目标)
- 验证数据是否正确创建
- 验证 JSON 字段命名camelCase
- _Requirements: 15.4_
- [x] 8. Checkpoint - 确保所有功能正常
- 运行完整的数据生成流程
- 验证所有 API 调用成功
- 验证错误处理和重试机制
- 验证进度显示和统计
- 如有问题,询问用户
## Notes
- 所有任务都是必做的,确保从一开始就保证质量
- 每个任务引用了具体的需求编号,便于追溯
- Checkpoint 任务确保增量验证
- Python 代码使用 camelCase 构造 JSON但变量名使用 snake_casePython 规范)
- 所有 API 请求使用独立构造的 Python 字典,不依赖 Go 后端代码

View File

@@ -1,701 +0,0 @@
# Design Document: Go Asset APIs
## Overview
本设计文档描述了 Go 后端资产 API 的架构设计,包括 Subdomain、Endpoint 和 Directory 三类资产的 CRUD 操作。设计遵循现有 Go 后端的分层架构模式Handler → Service → Repository确保与前端 API 的兼容性。
## Architecture
### 分层架构
```
┌─────────────────────────────────────────────────────────────┐
│ HTTP Layer (Gin) │
├─────────────────────────────────────────────────────────────┤
│ SubdomainHandler │ EndpointHandler │ DirectoryHandler │
├─────────────────────────────────────────────────────────────┤
│ SubdomainService │ EndpointService │ DirectoryService │
├─────────────────────────────────────────────────────────────┤
│ SubdomainRepo │ EndpointRepo │ DirectoryRepo │
├─────────────────────────────────────────────────────────────┤
│ PostgreSQL (GORM) │
└─────────────────────────────────────────────────────────────┘
```
### API 路由设计
```
# Subdomain APIs
GET /api/targets/:id/subdomains # 列表查询
POST /api/targets/:id/subdomains/bulk-create # 批量创建
GET /api/targets/:id/subdomains/export # 导出
POST /api/assets/subdomains/bulk-delete # 批量删除
# Endpoint APIs
GET /api/targets/:id/endpoints # 列表查询
POST /api/targets/:id/endpoints/bulk-create # 批量创建
GET /api/targets/:id/endpoints/export # 导出
GET /api/endpoints/:id # 详情查询
DELETE /api/endpoints/:id # 单个删除
POST /api/assets/endpoints/bulk-delete # 批量删除
# Directory APIs
GET /api/targets/:id/directories # 列表查询
POST /api/targets/:id/directories/bulk-create # 批量创建
GET /api/targets/:id/directories/export # 导出
POST /api/assets/directories/bulk-delete # 批量删除
```
## Components and Interfaces
### Handler Layer
```go
// SubdomainHandler handles subdomain HTTP endpoints
type SubdomainHandler struct {
svc *service.SubdomainService
}
func (h *SubdomainHandler) List(c *gin.Context) // GET /targets/:id/subdomains
func (h *SubdomainHandler) BulkCreate(c *gin.Context) // POST /targets/:id/subdomains/bulk-create
func (h *SubdomainHandler) Export(c *gin.Context) // GET /targets/:id/subdomains/export
func (h *SubdomainHandler) BulkDelete(c *gin.Context) // POST /assets/subdomains/bulk-delete
// EndpointHandler handles endpoint HTTP endpoints
type EndpointHandler struct {
svc *service.EndpointService
}
func (h *EndpointHandler) List(c *gin.Context) // GET /targets/:id/endpoints
func (h *EndpointHandler) GetByID(c *gin.Context) // GET /endpoints/:id
func (h *EndpointHandler) BulkCreate(c *gin.Context) // POST /targets/:id/endpoints/bulk-create
func (h *EndpointHandler) Delete(c *gin.Context) // DELETE /endpoints/:id
func (h *EndpointHandler) Export(c *gin.Context) // GET /targets/:id/endpoints/export
func (h *EndpointHandler) BulkDelete(c *gin.Context) // POST /assets/endpoints/bulk-delete
// DirectoryHandler handles directory HTTP endpoints
type DirectoryHandler struct {
svc *service.DirectoryService
}
func (h *DirectoryHandler) List(c *gin.Context) // GET /targets/:id/directories
func (h *DirectoryHandler) BulkCreate(c *gin.Context) // POST /targets/:id/directories/bulk-create
func (h *DirectoryHandler) Export(c *gin.Context) // GET /targets/:id/directories/export
func (h *DirectoryHandler) BulkDelete(c *gin.Context) // POST /assets/directories/bulk-delete
```
### Service Layer
```go
// SubdomainService handles subdomain business logic
type SubdomainService struct {
repo *repository.SubdomainRepository
targetRepo *repository.TargetRepository
}
func (s *SubdomainService) ListByTarget(targetID int, query *dto.SubdomainListQuery) ([]model.Subdomain, int64, error)
func (s *SubdomainService) BulkCreate(targetID int, names []string) (int, error)
func (s *SubdomainService) BulkDelete(ids []int) (int64, error)
func (s *SubdomainService) StreamByTarget(targetID int) (*sql.Rows, error)
func (s *SubdomainService) CountByTarget(targetID int) (int64, error)
// EndpointService handles endpoint business logic
type EndpointService struct {
repo *repository.EndpointRepository
targetRepo *repository.TargetRepository
}
func (s *EndpointService) ListByTarget(targetID int, query *dto.EndpointListQuery) ([]model.Endpoint, int64, error)
func (s *EndpointService) GetByID(id int) (*model.Endpoint, error)
func (s *EndpointService) BulkCreate(targetID int, urls []string) (int, error)
func (s *EndpointService) Delete(id int) error
func (s *EndpointService) BulkDelete(ids []int) (int64, error)
func (s *EndpointService) StreamByTarget(targetID int) (*sql.Rows, error)
func (s *EndpointService) CountByTarget(targetID int) (int64, error)
// DirectoryService handles directory business logic
type DirectoryService struct {
repo *repository.DirectoryRepository
targetRepo *repository.TargetRepository
}
func (s *DirectoryService) ListByTarget(targetID int, query *dto.DirectoryListQuery) ([]model.Directory, int64, error)
func (s *DirectoryService) BulkCreate(targetID int, urls []string) (int, error)
func (s *DirectoryService) BulkDelete(ids []int) (int64, error)
func (s *DirectoryService) StreamByTarget(targetID int) (*sql.Rows, error)
func (s *DirectoryService) CountByTarget(targetID int) (int64, error)
```
### Repository Layer
```go
// SubdomainRepository handles subdomain database operations
type SubdomainRepository struct {
db *gorm.DB
}
func (r *SubdomainRepository) FindByTargetID(targetID, page, pageSize int, filter string) ([]model.Subdomain, int64, error)
func (r *SubdomainRepository) BulkCreate(subdomains []model.Subdomain) (int, error)
func (r *SubdomainRepository) BulkDelete(ids []int) (int64, error)
func (r *SubdomainRepository) StreamByTargetID(targetID int) (*sql.Rows, error)
func (r *SubdomainRepository) CountByTargetID(targetID int) (int64, error)
// EndpointRepository handles endpoint database operations
type EndpointRepository struct {
db *gorm.DB
}
func (r *EndpointRepository) FindByTargetID(targetID, page, pageSize int, filter string) ([]model.Endpoint, int64, error)
func (r *EndpointRepository) FindByID(id int) (*model.Endpoint, error)
func (r *EndpointRepository) BulkCreate(endpoints []model.Endpoint) (int, error)
func (r *EndpointRepository) Delete(id int) error
func (r *EndpointRepository) BulkDelete(ids []int) (int64, error)
func (r *EndpointRepository) StreamByTargetID(targetID int) (*sql.Rows, error)
func (r *EndpointRepository) CountByTargetID(targetID int) (int64, error)
// DirectoryRepository handles directory database operations
type DirectoryRepository struct {
db *gorm.DB
}
func (r *DirectoryRepository) FindByTargetID(targetID, page, pageSize int, filter string) ([]model.Directory, int64, error)
func (r *DirectoryRepository) BulkCreate(directories []model.Directory) (int, error)
func (r *DirectoryRepository) BulkDelete(ids []int) (int64, error)
func (r *DirectoryRepository) StreamByTargetID(targetID int) (*sql.Rows, error)
func (r *DirectoryRepository) CountByTargetID(targetID int) (int64, error)
```
## Data Models
### 现有模型(已定义)
模型已在 `go-backend/internal/model/` 中定义,无需修改:
- `Subdomain`: id, target_id, name, created_at
- `Endpoint`: id, target_id, url, host, location, title, status_code, content_length, content_type, tech, webserver, response_body, response_headers, vhost, matched_gf_patterns, created_at
- `Directory`: id, target_id, url, status, content_length, words, lines, content_type, duration, created_at
## Asset-Target Matching Validation
资产创建时必须验证资产是否属于目标,确保数据一致性。
### 验证规则
| 资产类型 | Target 类型限制 | 匹配规则 |
|---------|----------------|---------|
| Subdomain | 仅 `domain` | subdomain == target 或 subdomain 以 `.target` 结尾 |
| Website | 无限制 | URL hostname 匹配 target |
| Endpoint | 无限制 | URL hostname 匹配 target |
| Directory | 无限制 | URL hostname 匹配 target |
### URL Hostname 匹配规则
根据 target 类型URL hostname 的匹配方式不同:
| Target 类型 | 匹配规则 | 示例 |
|------------|---------|------|
| `domain` | hostname == target 或 hostname 以 `.target` 结尾 | target=`example.com``example.com` ✓, `api.example.com` ✓, `other.com` ✗ |
| `ip` | hostname == target | target=`192.168.1.1``192.168.1.1` ✓, `192.168.1.2` ✗ |
| `cidr` | hostname 是 IP 且在 CIDR 范围内 | target=`10.0.0.0/8``10.1.2.3` ✓, `192.168.1.1` ✗ |
### 验证函数设计
新增文件:`go-backend/internal/pkg/validator/target.go`
```go
package validator
import (
"net"
"net/url"
"strings"
)
// IsURLMatchTarget checks if URL hostname matches target
// Returns true if the URL's hostname belongs to the target
func IsURLMatchTarget(urlStr, targetName, targetType string) bool {
parsed, err := url.Parse(urlStr)
if err != nil {
return false
}
hostname := strings.ToLower(parsed.Hostname())
if hostname == "" {
return false
}
targetName = strings.ToLower(targetName)
switch targetType {
case "domain":
// hostname equals target or ends with .target
return hostname == targetName || strings.HasSuffix(hostname, "."+targetName)
case "ip":
// hostname must exactly equal target
return hostname == targetName
case "cidr":
// hostname must be an IP within the CIDR range
ip := net.ParseIP(hostname)
if ip == nil {
return false
}
_, network, err := net.ParseCIDR(targetName)
if err != nil {
return false
}
return network.Contains(ip)
default:
return false
}
}
// IsSubdomainMatchTarget checks if subdomain belongs to target domain
// Returns true if subdomain equals target or ends with .target
func IsSubdomainMatchTarget(subdomain, targetDomain string) bool {
subdomain = strings.ToLower(strings.TrimSpace(subdomain))
targetDomain = strings.ToLower(strings.TrimSpace(targetDomain))
if subdomain == "" || targetDomain == "" {
return false
}
return subdomain == targetDomain || strings.HasSuffix(subdomain, "."+targetDomain)
}
```
### Service 层验证流程
批量创建时的验证流程:
```
输入数据 → 过滤空白 → 验证匹配 → 去重 → 批量插入
不匹配的静默跳过(计入 skipped
```
#### Subdomain BulkCreate 验证
```go
func (s *SubdomainService) BulkCreate(targetID int, names []string) (int, error) {
// 1. 获取 target
target, err := s.targetRepo.FindByID(targetID)
if err != nil {
return 0, ErrTargetNotFound
}
// 2. 验证 target 类型必须是 domain
if target.Type != "domain" {
return 0, ErrInvalidTargetType
}
// 3. 过滤并验证
var validSubdomains []model.Subdomain
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" {
continue // 跳过空白
}
if !validator.IsSubdomainMatchTarget(name, target.Name) {
continue // 跳过不匹配的
}
validSubdomains = append(validSubdomains, model.Subdomain{
TargetID: targetID,
Name: name,
})
}
// 4. 批量插入(去重由数据库 ON CONFLICT 处理)
return s.repo.BulkCreate(validSubdomains)
}
```
#### Website/Endpoint/Directory BulkCreate 验证
```go
func (s *EndpointService) BulkCreate(targetID int, urls []string) (int, error) {
// 1. 获取 target
target, err := s.targetRepo.FindByID(targetID)
if err != nil {
return 0, ErrTargetNotFound
}
// 2. 过滤并验证
var validEndpoints []model.Endpoint
for _, u := range urls {
u = strings.TrimSpace(u)
if u == "" {
continue // 跳过空白
}
if !validator.IsURLMatchTarget(u, target.Name, target.Type) {
continue // 跳过不匹配的
}
validEndpoints = append(validEndpoints, model.Endpoint{
TargetID: targetID,
URL: u,
Host: extractHostFromURL(u),
})
}
// 3. 批量插入
return s.repo.BulkCreate(validEndpoints)
}
```
### 新增错误类型
```go
var (
ErrInvalidTargetType = errors.New("invalid target type: subdomain can only be created for domain-type targets")
)
```
### DTO 定义
```go
// SubdomainListQuery represents subdomain list query parameters
type SubdomainListQuery struct {
PaginationQuery
Filter string `form:"filter"`
}
// SubdomainResponse represents subdomain response
type SubdomainResponse struct {
ID int `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
}
// BulkCreateSubdomainsRequest represents bulk create subdomains request
type BulkCreateSubdomainsRequest struct {
Subdomains []string `json:"subdomains" binding:"required,min=1,max=10000"`
}
// BulkCreateSubdomainsResponse represents bulk create subdomains response
type BulkCreateSubdomainsResponse struct {
CreatedCount int `json:"createdCount"`
}
// EndpointListQuery represents endpoint list query parameters
type EndpointListQuery struct {
PaginationQuery
Filter string `form:"filter"`
}
// EndpointResponse represents endpoint response
type EndpointResponse struct {
ID int `json:"id"`
URL string `json:"url"`
Host string `json:"host"`
Location string `json:"location"`
Title string `json:"title"`
Webserver string `json:"webserver"`
ContentType string `json:"contentType"`
StatusCode *int `json:"statusCode"`
ContentLength *int `json:"contentLength"`
ResponseBody string `json:"responseBody"`
Tech []string `json:"tech"`
Vhost *bool `json:"vhost"`
MatchedGFPatterns []string `json:"matchedGfPatterns"`
ResponseHeaders string `json:"responseHeaders"`
CreatedAt time.Time `json:"createdAt"`
}
// BulkCreateEndpointsRequest represents bulk create endpoints request
type BulkCreateEndpointsRequest struct {
URLs []string `json:"urls" binding:"required,min=1,max=10000"`
}
// BulkCreateEndpointsResponse represents bulk create endpoints response
type BulkCreateEndpointsResponse struct {
CreatedCount int `json:"createdCount"`
}
// DirectoryListQuery represents directory list query parameters
type DirectoryListQuery struct {
PaginationQuery
Filter string `form:"filter"`
}
// DirectoryResponse represents directory response
type DirectoryResponse struct {
ID int `json:"id"`
URL string `json:"url"`
Status *int `json:"status"`
ContentLength *int64 `json:"contentLength"`
Words *int `json:"words"`
Lines *int `json:"lines"`
ContentType string `json:"contentType"`
Duration *int64 `json:"duration"`
CreatedAt time.Time `json:"createdAt"`
}
// BulkCreateDirectoriesRequest represents bulk create directories request
type BulkCreateDirectoriesRequest struct {
URLs []string `json:"urls" binding:"required,min=1,max=10000"`
}
// BulkCreateDirectoriesResponse represents bulk create directories response
type BulkCreateDirectoriesResponse struct {
CreatedCount int `json:"createdCount"`
}
// BulkDeleteRequest represents bulk delete request (shared)
type BulkDeleteRequest struct {
IDs []int `json:"ids" binding:"required,min=1,max=10000"`
}
// BulkDeleteResponse represents bulk delete response (shared)
type BulkDeleteResponse struct {
DeletedCount int64 `json:"deletedCount"`
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: List returns only items belonging to target
*For any* target with associated assets (subdomains/endpoints/directories), when listing assets for that target, all returned items SHALL have target_id equal to the requested target ID.
**Validates: Requirements 1.1, 5.1, 11.1**
### Property 2: Pagination respects page and pageSize parameters
*For any* valid page and pageSize parameters, the returned results SHALL contain at most pageSize items, and the page number SHALL match the requested page.
**Validates: Requirements 1.2, 5.2, 11.2**
### Property 3: Filter returns only matching items
*For any* filter string and list of assets, all returned items SHALL contain the filter text in the appropriate field(s) (name for subdomains, url/host/title for endpoints, url for directories).
**Validates: Requirements 1.3, 5.3, 11.3**
### Property 4: Results are sorted by createdAt DESC
*For any* list of returned assets, the createdAt timestamps SHALL be in descending order (newest first).
**Validates: Requirements 1.5, 5.5, 11.5**
### Property 5: Bulk create is idempotent
*For any* list of asset names/URLs, calling bulk create twice with the same data SHALL result in the same final state (no duplicates created).
**Validates: Requirements 2.2, 7.2, 12.2**
### Property 6: Bulk create count matches actual created records
*For any* bulk create operation, the returned createdCount SHALL equal the number of new records actually inserted into the database.
**Validates: Requirements 2.3, 7.3, 12.3**
### Property 7: Bulk delete removes specified items
*For any* list of valid asset IDs, after bulk delete, none of those IDs SHALL exist in the database.
**Validates: Requirements 3.1, 9.1, 13.1**
### Property 8: Bulk delete count matches actual deleted records
*For any* bulk delete operation, the returned deletedCount SHALL equal the number of records actually removed from the database.
**Validates: Requirements 3.2, 9.2, 13.2**
### Property 9: Export content matches database records
*For any* target with assets, the exported file content SHALL contain exactly the same assets as querying the database directly.
**Validates: Requirements 4.1, 10.1, 14.1**
### Property 10: Pagination response format is correct
*For any* paginated response, totalPages SHALL equal ceil(total / pageSize), and results SHALL be an empty array (not null) when no items exist.
**Validates: Requirements 15.1, 15.3, 15.4**
## Error Handling
### 错误类型定义
```go
var (
ErrSubdomainNotFound = errors.New("subdomain not found")
ErrEndpointNotFound = errors.New("endpoint not found")
ErrDirectoryNotFound = errors.New("directory not found")
ErrTargetNotFound = errors.New("target not found") // 已存在
)
```
### HTTP 错误响应
| 场景 | HTTP Status | 响应格式 |
|------|-------------|----------|
| Target 不存在 | 404 | `{"error": "Target not found"}` |
| Asset 不存在 | 404 | `{"error": "Subdomain/Endpoint/Directory not found"}` |
| 请求参数无效 | 400 | `{"error": "Invalid request", "details": [...]}` |
| 服务器内部错误 | 500 | `{"error": "Internal server error"}` |
### 输入验证
- 批量创建:最大 10000 条记录
- 批量删除:最大 10000 个 ID
- 分页pageSize 最大 1000
- 空字符串和纯空白字符串在批量创建时静默跳过
## Seed Data Generation
种子数据生成文件 `go-backend/cmd/seed/main.go` 需要更新以支持新的资产类型。
### 新增生成函数
```go
// createSubdomains creates subdomains for domain-type targets
func createSubdomains(db *gorm.DB, targetIDs []int, subdomainsPerTarget int) error
// createEndpoints creates endpoints for targets
func createEndpoints(db *gorm.DB, targetIDs []int, endpointsPerTarget int) error
// createDirectories creates directories for targets
func createDirectories(db *gorm.DB, targetIDs []int, directoriesPerTarget int) error
```
### 数据生成策略
| 资产类型 | 每个 Target 数量 | 数据特征 |
|---------|-----------------|---------|
| Subdomain | 20 | 仅为 domain 类型 target 生成,格式:`{prefix}.{target_domain}` |
| Endpoint | 20 | 包含 URL、状态码、技术栈等 HTTP 元数据 |
| Directory | 20 | 包含 URL、状态码、内容长度等目录扫描结果 |
### clearData 更新
```go
func clearData(db *gorm.DB) error {
tables := []string{
"directory", // 新增
"endpoint", // 新增
"subdomain", // 新增
"website",
"organization_target",
"target",
"organization",
}
// ...
}
```
### 命令行参数
```bash
# 默认生成
go run cmd/seed/main.go
# 清除后重新生成
go run cmd/seed/main.go -clear
# 自定义数量
go run cmd/seed/main.go -orgs=50
```
## Python 后端差异分析
对比 Python 后端实现,以下是需要注意的差异:
### 1. Subdomain 批量创建响应
**Python 后端**返回详细统计:
```json
{
"createdCount": 10,
"skippedCount": 2,
"invalidCount": 1,
"mismatchedCount": 1,
"totalReceived": 14
}
```
**Go 设计**简化为:
```json
{
"createdCount": 10
}
```
**决策**Go 版本简化响应,因为前端主要只使用 `createdCount`。如需详细统计,可后续扩展。
### 3. 导出格式
**Python 后端**CSV 格式,导出所有数据库字段
**Go 设计**CSV 格式,导出模型的所有字段(与数据库表结构一致)
**导出原则**:模型有多少字段就导出多少,不做字段筛选。
### 4. 批量删除路由
**Python 后端**`POST /api/assets/subdomains/bulk-delete/`
**Go 设计**`POST /api/subdomains/bulk-delete/`(遵循 go-backend-conventions.md不加 assets 前缀)
**决策**Go 版本使用更简洁的路由,前端需要相应调整。
### 5. 验证逻辑(已对齐)
**Python 后端**
- Subdomain: 验证 target.type == "domain" + 域名后缀匹配
- Website/Endpoint/Directory: 验证 URL hostname 匹配 target
**Go 设计**:完全对齐 Python 行为,详见 "Asset-Target Matching Validation" 章节。
## Testing Strategy
### 索引覆盖分析
所有 filter 查询字段都已有索引覆盖:
| 资产类型 | Filter 字段 | 索引名称 | 索引类型 |
|---------|------------|---------|---------|
| Subdomain | `name` | `idx_subdomain_name` | B-tree |
| Endpoint | `url` | `idx_endpoint_url` | B-tree |
| Endpoint | `host` | `idx_endpoint_host` | B-tree |
| Endpoint | `title` | `idx_endpoint_title` | B-tree |
| Endpoint | `status_code` | `idx_endpoint_status_code` | B-tree |
| Endpoint | `tech` | `idx_endpoint_tech_gin` | GIN |
| Directory | `url` | `idx_directory_url` | B-tree |
| Directory | `status` | `idx_directory_status` | B-tree |
其他查询字段索引:
- `target_id`: 各模型都有 `idx_xxx_target` 索引
- `created_at`: 各模型都有 `idx_xxx_created_at` 索引(用于排序)
- `matched_gf_patterns`: `idx_endpoint_matched_gf_patterns_gin` (GIN)
**结论:不需要添加新索引。**
### 测试框架
- 单元测试Go 标准库 `testing`
- HTTP 测试:`net/http/httptest`
- 属性测试:`github.com/leanovate/gopter`
- Mock`github.com/stretchr/testify/mock`
### 测试层次
1. **Repository 层测试**:使用真实数据库(测试容器)验证 SQL 查询
2. **Service 层测试**Mock Repository验证业务逻辑
3. **Handler 层测试**:使用 httptest验证 HTTP 接口
### 属性测试配置
- 每个属性测试运行 100 次迭代
- 使用 gopter 生成随机测试数据
- 测试标签格式:`Feature: go-asset-apis, Property N: {property_text}`
### 单元测试覆盖
- 正常流程测试
- 边界条件测试(空列表、最大分页等)
- 错误处理测试404、400 等)

View File

@@ -1,184 +0,0 @@
# Requirements Document
## Introduction
本文档定义了 Go 后端资产 API 的需求,包括 Subdomain子域名、EndpointURL 端点)和 Directory目录三类资产的 CRUD 操作。这些 API 将替代现有的 Python Django 后端实现,保持与前端的兼容性。
## Glossary
- **Asset_API**: Go 后端资产管理 API 系统
- **Subdomain**: 子域名资产,关联到特定 Target
- **Endpoint**: URL 端点资产,包含 HTTP 响应元数据
- **Directory**: 目录扫描结果资产,包含 HTTP 状态和内容信息
- **Target**: 扫描目标,是所有资产的父级实体
- **Filter**: 智能过滤查询字符串,支持字段搜索和纯文本搜索
- **Bulk_Operation**: 批量操作,支持批量创建和批量删除
## Requirements
### Requirement 1: Subdomain 列表查询
**User Story:** As a security analyst, I want to list subdomains for a target, so that I can review discovered subdomains.
#### Acceptance Criteria
1. WHEN a user requests subdomains for a target, THE Asset_API SHALL return a paginated list of subdomains belonging to that target
2. WHEN pagination parameters are provided, THE Asset_API SHALL return the specified page with the specified page size
3. WHEN a filter parameter is provided, THE Asset_API SHALL filter subdomains by name containing the filter text
4. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error with message "Target not found"
5. THE Asset_API SHALL return subdomains sorted by creation time in descending order
### Requirement 2: Subdomain 批量创建
**User Story:** As a security analyst, I want to bulk create subdomains for a target, so that I can import discovered subdomains efficiently.
#### Acceptance Criteria
1. WHEN a user submits a list of subdomain names, THE Asset_API SHALL create subdomains that don't already exist
2. WHEN duplicate subdomain names are submitted, THE Asset_API SHALL skip duplicates and continue processing
3. THE Asset_API SHALL return the count of successfully created subdomains
4. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error
5. WHEN subdomain names are invalid (empty or whitespace only), THE Asset_API SHALL skip them silently
6. IF the target type is not "domain", THEN THE Asset_API SHALL return a 400 error with message "Invalid target type"
7. WHEN a subdomain does not match the target domain (not equal to or ending with .target), THE Asset_API SHALL skip it silently
### Requirement 3: Subdomain 批量删除
**User Story:** As a security analyst, I want to bulk delete subdomains, so that I can clean up unwanted data efficiently.
#### Acceptance Criteria
1. WHEN a user submits a list of subdomain IDs, THE Asset_API SHALL delete all specified subdomains
2. THE Asset_API SHALL return the count of successfully deleted subdomains
3. WHEN some IDs don't exist, THE Asset_API SHALL delete existing ones and ignore non-existent IDs
### Requirement 4: Subdomain 导出
**User Story:** As a security analyst, I want to export subdomains as a text file, so that I can use them with external tools.
#### Acceptance Criteria
1. WHEN a user requests subdomain export for a target, THE Asset_API SHALL return a text file with one subdomain per line
2. THE Asset_API SHALL set appropriate Content-Type and Content-Disposition headers for file download
3. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error
### Requirement 5: Endpoint 列表查询
**User Story:** As a security analyst, I want to list endpoints for a target, so that I can review discovered URLs.
#### Acceptance Criteria
1. WHEN a user requests endpoints for a target, THE Asset_API SHALL return a paginated list of endpoints belonging to that target
2. WHEN pagination parameters are provided, THE Asset_API SHALL return the specified page with the specified page size
3. WHEN a filter parameter is provided, THE Asset_API SHALL filter endpoints by URL, host, or title containing the filter text
4. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error with message "Target not found"
5. THE Asset_API SHALL return endpoints sorted by creation time in descending order
### Requirement 6: Endpoint 详情查询
**User Story:** As a security analyst, I want to view endpoint details, so that I can analyze HTTP response information.
#### Acceptance Criteria
1. WHEN a user requests an endpoint by ID, THE Asset_API SHALL return the complete endpoint details
2. THE Asset_API SHALL include all HTTP metadata fields (statusCode, contentLength, contentType, tech, etc.)
3. IF the endpoint does not exist, THEN THE Asset_API SHALL return a 404 error with message "Endpoint not found"
### Requirement 7: Endpoint 批量创建
**User Story:** As a security analyst, I want to bulk create endpoints for a target, so that I can import discovered URLs efficiently.
#### Acceptance Criteria
1. WHEN a user submits a list of URLs, THE Asset_API SHALL create endpoints that don't already exist
2. WHEN duplicate URLs are submitted, THE Asset_API SHALL skip duplicates and continue processing
3. THE Asset_API SHALL return the count of successfully created endpoints
4. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error
5. WHEN URLs are invalid (empty or whitespace only), THE Asset_API SHALL skip them silently
6. WHEN a URL hostname does not match the target (domain suffix, IP equality, or CIDR range), THE Asset_API SHALL skip it silently
### Requirement 8: Endpoint 单个删除
**User Story:** As a security analyst, I want to delete a single endpoint, so that I can remove unwanted data.
#### Acceptance Criteria
1. WHEN a user requests to delete an endpoint by ID, THE Asset_API SHALL delete the endpoint
2. THE Asset_API SHALL return 204 No Content on successful deletion
3. IF the endpoint does not exist, THEN THE Asset_API SHALL return a 404 error
### Requirement 9: Endpoint 批量删除
**User Story:** As a security analyst, I want to bulk delete endpoints, so that I can clean up unwanted data efficiently.
#### Acceptance Criteria
1. WHEN a user submits a list of endpoint IDs, THE Asset_API SHALL delete all specified endpoints
2. THE Asset_API SHALL return the count of successfully deleted endpoints
3. WHEN some IDs don't exist, THE Asset_API SHALL delete existing ones and ignore non-existent IDs
### Requirement 10: Endpoint 导出
**User Story:** As a security analyst, I want to export endpoints as a text file, so that I can use them with external tools.
#### Acceptance Criteria
1. WHEN a user requests endpoint export for a target, THE Asset_API SHALL return a text file with one URL per line
2. THE Asset_API SHALL set appropriate Content-Type and Content-Disposition headers for file download
3. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error
### Requirement 11: Directory 列表查询
**User Story:** As a security analyst, I want to list directories for a target, so that I can review discovered paths.
#### Acceptance Criteria
1. WHEN a user requests directories for a target, THE Asset_API SHALL return a paginated list of directories belonging to that target
2. WHEN pagination parameters are provided, THE Asset_API SHALL return the specified page with the specified page size
3. WHEN a filter parameter is provided, THE Asset_API SHALL filter directories by URL containing the filter text
4. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error with message "Target not found"
5. THE Asset_API SHALL return directories sorted by creation time in descending order
### Requirement 12: Directory 批量创建
**User Story:** As a security analyst, I want to bulk create directories for a target, so that I can import discovered paths efficiently.
#### Acceptance Criteria
1. WHEN a user submits a list of directory URLs, THE Asset_API SHALL create directories that don't already exist
2. WHEN duplicate URLs are submitted, THE Asset_API SHALL skip duplicates and continue processing
3. THE Asset_API SHALL return the count of successfully created directories
4. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error
5. WHEN URLs are invalid (empty or whitespace only), THE Asset_API SHALL skip them silently
6. WHEN a URL hostname does not match the target (domain suffix, IP equality, or CIDR range), THE Asset_API SHALL skip it silently
### Requirement 13: Directory 批量删除
**User Story:** As a security analyst, I want to bulk delete directories, so that I can clean up unwanted data efficiently.
#### Acceptance Criteria
1. WHEN a user submits a list of directory IDs, THE Asset_API SHALL delete all specified directories
2. THE Asset_API SHALL return the count of successfully deleted directories
3. WHEN some IDs don't exist, THE Asset_API SHALL delete existing ones and ignore non-existent IDs
### Requirement 14: Directory 导出
**User Story:** As a security analyst, I want to export directories as a text file, so that I can use them with external tools.
#### Acceptance Criteria
1. WHEN a user requests directory export for a target, THE Asset_API SHALL return a text file with one URL per line
2. THE Asset_API SHALL set appropriate Content-Type and Content-Disposition headers for file download
3. IF the target does not exist, THEN THE Asset_API SHALL return a 404 error
### Requirement 15: 分页响应格式一致性
**User Story:** As a frontend developer, I want consistent pagination response format, so that I can reuse pagination components.
#### Acceptance Criteria
1. THE Asset_API SHALL return pagination responses with fields: results, total, page, pageSize, totalPages
2. THE Asset_API SHALL use camelCase for JSON field names
3. WHEN results are empty, THE Asset_API SHALL return an empty array instead of null
4. THE Asset_API SHALL calculate totalPages correctly based on total and pageSize

View File

@@ -1,94 +0,0 @@
# Implementation Plan: Go Asset APIs
## Overview
实现 Go 后端的 Subdomain、Endpoint、Directory 三类资产 API包括列表查询、批量创建、批量删除、导出功能以及 Asset-Target 匹配验证。
## Tasks
- [x] 1. 创建 Target 匹配验证函数
- [x] 1.1 创建 `go-backend/internal/pkg/validator/target.go`
- 实现 `IsURLMatchTarget(urlStr, targetName, targetType string) bool`
- 实现 `IsSubdomainMatchTarget(subdomain, targetDomain string) bool`
- 实现 `DetectTargetType(name string) string`
- 使用 Go 标准库 `net/url``net``strings`
- _Requirements: 2.6, 2.7, 7.6, 12.6_
- [x] 1.2 编写验证函数单元测试
- 测试 domain 类型匹配(精确匹配、后缀匹配)
- 测试 IP 类型匹配
- 测试 CIDR 类型匹配
- 测试边界情况(空字符串、无效 URL
- _Requirements: 2.6, 2.7, 7.6, 12.6_
- [x] 2. 实现 Subdomain API
- [x] 2.1 创建 `go-backend/internal/dto/subdomain.go`
- [x] 2.2 创建 `go-backend/internal/repository/subdomain.go`
- [x] 2.3 创建 `go-backend/internal/service/subdomain.go`
- [x] 2.4 创建 `go-backend/internal/handler/subdomain.go`
- [x] 2.5 注册 Subdomain 路由到 main.go
- [x] 3. Checkpoint - 验证 Subdomain API ✓
- [x] 4. 实现 Endpoint API
- [x] 4.1 创建 `go-backend/internal/dto/endpoint.go`
- [x] 4.2 创建 `go-backend/internal/repository/endpoint.go`
- [x] 4.3 创建 `go-backend/internal/service/endpoint.go`
- [x] 4.4 创建 `go-backend/internal/handler/endpoint.go`
- [x] 4.5 注册 Endpoint 路由到 main.go
- [x] 5. Checkpoint - 验证 Endpoint API ✓
- [x] 6. 实现 Directory API
- [x] 6.1 创建 `go-backend/internal/dto/directory.go`
- [x] 6.2 创建 `go-backend/internal/repository/directory.go`
- [x] 6.3 创建 `go-backend/internal/service/directory.go`
- [x] 6.4 创建 `go-backend/internal/handler/directory.go`
- [x] 6.5 注册 Directory 路由到 main.go
- [x] 7. Checkpoint - 验证 Directory API ✓
- [x] 8. 更新 Website Service 添加验证
- [x] 8.1 修改 `go-backend/internal/service/website.go`
- 在 BulkCreate 中添加 URL 匹配验证
- 使用 validator.IsURLMatchTarget 函数
- [x] 9. 更新种子数据生成
- [x] 9.1 修改 `go-backend/cmd/seed/main.go`
- 添加 createSubdomains 函数(仅为 domain 类型 target 生成)
- 添加 createEndpoints 函数
- 添加 createDirectories 函数
- 每个资产类型生成 20 条数据
- 更新 clearData 函数添加新表
- [x] 10. Final Checkpoint ✓
- 所有代码编译通过
- 所有 API 路由已注册
## Notes
- 验证函数使用 Go 标准库,不需要第三方库
- CSV 导出使用流式处理,避免内存问题
- 所有 filter 字段都已有索引覆盖
- 数组字段tech使用 GIN 索引scope 包已支持
## Created Files
- `go-backend/internal/dto/subdomain.go`
- `go-backend/internal/dto/endpoint.go`
- `go-backend/internal/dto/directory.go`
- `go-backend/internal/repository/subdomain.go`
- `go-backend/internal/repository/endpoint.go`
- `go-backend/internal/repository/directory.go`
- `go-backend/internal/service/subdomain.go`
- `go-backend/internal/service/endpoint.go`
- `go-backend/internal/service/directory.go`
- `go-backend/internal/handler/subdomain.go`
- `go-backend/internal/handler/endpoint.go`
- `go-backend/internal/handler/directory.go`
## Modified Files
- `go-backend/internal/pkg/validator/target.go` - 添加 DetectTargetType 函数
- `go-backend/internal/service/website.go` - 添加 URL 匹配验证
- `go-backend/cmd/server/main.go` - 注册新路由
- `go-backend/cmd/seed/main.go` - 添加新资产类型种子数据生成

View File

@@ -1,135 +0,0 @@
# 设计文档: Go JWT 认证
## 概述
实现基于 JWT 的认证系统,使用 `golang-jwt/jwt` 库。
## 架构
```
请求 → AuthMiddleware → Handler
验证 JWT Token
注入用户信息到 Context
```
## 目录结构
```
internal/
├── auth/
│ ├── jwt.go # JWT 生成和验证
│ ├── password.go # 密码验证Django 兼容)
│ └── auth_test.go # 测试
├── handler/
│ └── auth_handler.go # 登录/刷新接口
├── middleware/
│ └── auth.go # 认证中间件
└── config/
└── config.go # 添加 JWT 配置
```
## 核心组件
### 1. JWT Token 结构
```go
type Claims struct {
UserID int `json:"userId"`
Username string `json:"username"`
jwt.RegisteredClaims
}
```
### 2. 登录流程
```
用户输入 → 查询数据库 → 验证密码 → 生成 Token → 返回
```
### 3. 密码验证Django 兼容)
Django 密码格式: `pbkdf2_sha256$iterations$salt$hash`
```go
func VerifyPassword(password, encoded string) bool {
// 1. 解析 encoded 字符串
// 2. 使用相同参数计算 PBKDF2
// 3. 比较哈希值
}
```
### 4. 中间件
```go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 1. 提取 Authorization header
// 2. 验证 Bearer Token
// 3. 解析 Claims
// 4. 注入到 Context
c.Set("user", claims)
c.Next()
}
}
```
## API 设计
### POST /api/auth/login
请求:
```json
{
"username": "admin",
"password": "admin"
}
```
响应:
```json
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 900
}
```
### POST /api/auth/refresh
请求:
```json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
```
响应:
```json
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"expiresIn": 900
}
```
## 依赖
- `github.com/golang-jwt/jwt/v5` - JWT 库
- `golang.org/x/crypto/pbkdf2` - PBKDF2 密码验证
## 配置
```yaml
jwt:
secret: "your-secret-key" # JWT 签名密钥
accessExpire: 15m # Access Token 过期时间
refreshExpire: 168h # Refresh Token 过期时间 (7天)
```
## 安全考虑
1. JWT Secret 必须足够长(至少 32 字符)
2. 生产环境必须通过环境变量配置 Secret
3. 密码验证失败不透露具体原因(统一返回"用户名或密码错误"
4. Token 不记录到日志

View File

@@ -1,72 +0,0 @@
# 需求文档: Go JWT 认证
## 简介
为 Go 后端实现 JWT 认证系统包括登录、Token 刷新、认证中间件。
## 术语表
- **JWT**: JSON Web Token无状态认证令牌
- **Access Token**: 短期令牌,用于 API 认证15分钟
- **Refresh Token**: 长期令牌,用于刷新 Access Token7天
## 需求
### 需求 1: 登录接口
**用户故事:** 作为用户,我希望通过用户名密码登录,获取 JWT Token。
#### 验收标准
1. THE API SHALL 提供 `POST /api/auth/login` 接口
2. THE API SHALL 验证用户名密码(兼容 Django pbkdf2_sha256 密码格式)
3. THE API SHALL 返回 access_token 和 refresh_token
4. THE API SHALL 在登录失败时返回 401 错误
### 需求 2: Token 刷新接口
**用户故事:** 作为用户,我希望在 Access Token 过期前刷新它。
#### 验收标准
1. THE API SHALL 提供 `POST /api/auth/refresh` 接口
2. THE API SHALL 验证 Refresh Token 有效性
3. THE API SHALL 返回新的 access_token
4. THE API SHALL 在 Refresh Token 无效时返回 401 错误
### 需求 3: 认证中间件
**用户故事:** 作为开发者,我希望有一个中间件自动验证 JWT Token。
#### 验收标准
1. THE Middleware SHALL 从 Authorization header 提取 Bearer Token
2. THE Middleware SHALL 验证 Token 签名和过期时间
3. THE Middleware SHALL 将用户信息注入到 Gin Context
4. THE Middleware SHALL 在 Token 无效时返回 401 错误
### 需求 4: 密码验证
**用户故事:** 作为系统,我需要验证用户密码与 Django 存储的密码哈希匹配。
#### 验收标准
1. THE System SHALL 支持 Django pbkdf2_sha256 密码格式
2. THE System SHALL 正确解析 `pbkdf2_sha256$iterations$salt$hash` 格式
3. THE System SHALL 使用相同算法验证密码
### 需求 5: 配置管理
**用户故事:** 作为运维,我希望通过环境变量配置 JWT 参数。
#### 验收标准
1. THE Config SHALL 支持 `JWT_SECRET` 环境变量
2. THE Config SHALL 支持 `JWT_ACCESS_EXPIRE` 环境变量(默认 15 分钟)
3. THE Config SHALL 支持 `JWT_REFRESH_EXPIRE` 环境变量(默认 7 天)
## 非功能需求
- Token 签名算法使用 HS256
- 密码验证使用 PBKDF2-SHA256兼容 Django
- 所有敏感信息不记录到日志

View File

@@ -1,54 +0,0 @@
# 实现计划: Go JWT 认证
## 概述
实现 JWT 认证系统包括登录、Token 刷新、认证中间件。
## 任务
- [x] 1. 添加依赖和配置
- [x] 1.1 添加 JWT 和 PBKDF2 依赖
- `go get github.com/golang-jwt/jwt/v5`
- `go get golang.org/x/crypto`
- _需求: 5_
- [x] 1.2 更新 config.go 添加 JWT 配置
- 添加 JWTSecret, JWTAccessExpire, JWTRefreshExpire
- _需求: 5_
- [x] 2. 实现核心认证逻辑
- [x] 2.1 创建 internal/auth/jwt.go
- 实现 GenerateAccessToken()
- 实现 GenerateRefreshToken()
- 实现 ValidateToken()
- 定义 Claims 结构
- _需求: 1, 2_
- [x] 2.2 创建 internal/auth/password.go
- 实现 VerifyDjangoPassword() - 兼容 Django pbkdf2_sha256
- 实现 HashPassword() - 用于创建新用户
- _需求: 4_
- [x] 3. 实现认证中间件
- [x] 3.1 创建 internal/middleware/auth.go
- 实现 AuthMiddleware()
- 从 Authorization header 提取 Token
- 验证 Token 并注入用户信息
- _需求: 3_
- [x] 4. 实现认证接口
- [x] 4.1 创建 internal/handler/auth_handler.go
- 实现 Login() - POST /api/auth/login
- 实现 RefreshToken() - POST /api/auth/refresh
- 实现 GetCurrentUser() - GET /api/auth/me
- _需求: 1, 2_
- [x] 4.2 注册路由
- 在 main.go 中注册认证路由
- _需求: 1, 2_
- [x] 5. 编写测试
- [x] 5.1 创建 internal/auth/auth_test.go
- 测试 JWT 生成和验证
- 测试 Django 密码验证
- _需求: 1, 2, 4_
- [x] 6. 检查点 - 验证认证功能
- 所有测试通过 ✅

View File

@@ -1,101 +0,0 @@
# 设计文档: Go CRUD API
## 概述
实现 RESTful CRUD API采用 Handler -> Service -> Repository 三层架构。
## 架构
```
HTTP Request → Handler → Service → Repository → Database
Validation
```
## 目录结构
```
internal/
├── handler/
│ ├── user_handler.go
│ ├── organization_handler.go
│ ├── target_handler.go
│ └── engine_handler.go
├── service/
│ ├── user_service.go
│ ├── organization_service.go
│ ├── target_service.go
│ └── engine_service.go
├── repository/
│ ├── user_repository.go
│ ├── organization_repository.go
│ ├── target_repository.go
│ └── engine_repository.go
└── dto/
├── request.go # 请求 DTO
├── response.go # 响应 DTO
└── pagination.go # 分页
```
## API 设计
### 统一响应格式
成功:
```json
{
"data": { ... },
"message": "success"
}
```
列表:
```json
{
"data": [ ... ],
"total": 100,
"page": 1,
"pageSize": 20
}
```
错误:
```json
{
"error": "error message"
}
```
### 分页参数
- `page`: 页码,默认 1
- `pageSize`: 每页数量,默认 20最大 100
### API 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| POST | /api/users | 创建用户 |
| GET | /api/users | 用户列表 |
| PUT | /api/users/:id/password | 修改密码 |
| GET | /api/organizations | 组织列表 |
| POST | /api/organizations | 创建组织 |
| GET | /api/organizations/:id | 获取组织 |
| PUT | /api/organizations/:id | 更新组织 |
| DELETE | /api/organizations/:id | 删除组织 |
| GET | /api/targets | 目标列表 |
| POST | /api/targets | 创建目标 |
| GET | /api/targets/:id | 获取目标 |
| PUT | /api/targets/:id | 更新目标 |
| DELETE | /api/targets/:id | 删除目标 |
| GET | /api/engines | 引擎列表 |
| POST | /api/engines | 创建引擎 |
| GET | /api/engines/:id | 获取引擎 |
| PUT | /api/engines/:id | 更新引擎 |
| DELETE | /api/engines/:id | 删除引擎 |
## 软删除
Organization 和 Target 使用软删除:
- 删除时设置 `deleted_at` 字段
- 查询时默认过滤已删除记录

View File

@@ -1,64 +0,0 @@
# 需求文档: Go CRUD API
## 简介
实现基础的 CRUD API包括用户管理、组织、目标、扫描引擎。
## 需求
### 需求 1: 用户管理
**用户故事:** 作为管理员,我希望能创建用户和修改密码。
#### 验收标准
1. THE API SHALL 提供 `POST /api/users` 创建用户
2. THE API SHALL 提供 `PUT /api/users/:id/password` 修改密码
3. THE API SHALL 提供 `GET /api/users` 获取用户列表
4. THE API SHALL 使用 bcrypt 加密密码
### 需求 2: 组织管理
**用户故事:** 作为用户,我希望能管理组织。
#### 验收标准
1. THE API SHALL 提供 `GET /api/organizations` 获取组织列表(支持分页)
2. THE API SHALL 提供 `POST /api/organizations` 创建组织
3. THE API SHALL 提供 `GET /api/organizations/:id` 获取单个组织
4. THE API SHALL 提供 `PUT /api/organizations/:id` 更新组织
5. THE API SHALL 提供 `DELETE /api/organizations/:id` 软删除组织
### 需求 3: 目标管理
**用户故事:** 作为用户,我希望能管理扫描目标。
#### 验收标准
1. THE API SHALL 提供 `GET /api/targets` 获取目标列表(支持分页、筛选)
2. THE API SHALL 提供 `POST /api/targets` 创建目标
3. THE API SHALL 提供 `GET /api/targets/:id` 获取单个目标
4. THE API SHALL 提供 `PUT /api/targets/:id` 更新目标
5. THE API SHALL 提供 `DELETE /api/targets/:id` 软删除目标
6. THE API SHALL 自动检测目标类型domain/ip/cidr
### 需求 4: 扫描引擎管理
**用户故事:** 作为用户,我希望能管理扫描引擎配置。
#### 验收标准
1. THE API SHALL 提供 `GET /api/engines` 获取引擎列表
2. THE API SHALL 提供 `POST /api/engines` 创建引擎
3. THE API SHALL 提供 `GET /api/engines/:id` 获取单个引擎
4. THE API SHALL 提供 `PUT /api/engines/:id` 更新引擎
5. THE API SHALL 提供 `DELETE /api/engines/:id` 删除引擎
### 需求 5: 通用功能
#### 验收标准
1. THE API SHALL 支持分页参数 `page``pageSize`
2. THE API SHALL 返回统一的响应格式
3. THE API SHALL 所有接口需要认证(除了登录)
4. THE API SHALL 返回 camelCase JSON 字段名

View File

@@ -1,43 +0,0 @@
# 实现计划: Go CRUD API
## 概述
实现基础 CRUD API采用三层架构。
## 任务
- [x] 1. 创建基础设施
- [x] 1.1 创建 dto 包(请求/响应/分页)
- [x] 1.2 创建 repository 基础接口
- [x] 2. 实现用户管理
- [x] 2.1 创建 user_repository.go
- [x] 2.2 创建 user_service.go
- [x] 2.3 创建 user_handler.go
- [x] 2.4 注册路由
- _需求: 1_
- [x] 3. 实现组织管理
- [x] 3.1 创建 organization_repository.go
- [x] 3.2 创建 organization_service.go
- [x] 3.3 创建 organization_handler.go
- [x] 3.4 注册路由
- _需求: 2_
- [x] 4. 实现目标管理
- [x] 4.1 创建 target_repository.go
- [x] 4.2 创建 target_service.go含类型检测
- [x] 4.3 创建 target_handler.go
- [x] 4.4 注册路由
- _需求: 3_
- [x] 5. 实现引擎管理
- [x] 5.1 创建 engine_repository.go
- [x] 5.2 创建 engine_service.go
- [x] 5.3 创建 engine_handler.go
- [x] 5.4 注册路由
- _需求: 4_
- [x] 6. 检查点
- 所有测试通过
- API 可正常调用

View File

@@ -1,412 +0,0 @@
# 设计文档
## 概述
创建 Go 后端项目的基础框架,为后续模块开发奠定基础。本阶段聚焦于项目结构、配置管理、数据库连接和基础模型定义。
## 架构
### 项目结构
```
go-backend/
├── cmd/
│ └── server/
│ └── main.go # Server 入口
├── internal/
│ ├── config/
│ │ └── config.go # 配置管理
│ ├── database/
│ │ └── database.go # 数据库连接
│ ├── model/ # 数据模型扁平结构Go 风格)
│ │ ├── organization.go
│ │ ├── target.go
│ │ ├── scan.go
│ │ ├── subdomain.go
│ │ ├── website.go
│ │ ├── endpoint.go
│ │ ├── directory.go
│ │ ├── vulnerability.go
│ │ ├── host_port_mapping.go
│ │ ├── worker_node.go
│ │ ├── scan_engine.go
│ │ └── user.go
│ ├── handler/ # HTTP 处理器
│ │ └── health.go
│ ├── middleware/
│ │ ├── logger.go
│ │ └── recovery.go
│ └── pkg/ # 内部工具包
│ ├── logger.go # 日志工具
│ └── response.go # 响应工具
├── go.mod
├── go.sum
├── Makefile
└── .env.example
```
### Go 风格规范
| 规范 | 说明 |
|------|------|
| 包名 | 小写单词,不用下划线(`model` 不是 `models` |
| 文件名 | 小写 + 下划线(`worker_node.go` |
| 导出标识符 | PascalCase`Target`, `GetByID` |
| 私有标识符 | camelCase`targetID`, `getByID` |
| 接口命名 | 动词 + er`Reader`, `Scanner` |
| 错误变量 | `Err` 前缀(`ErrNotFound` |
| 常量 | PascalCase 或全大写(`MaxRetries`, `DEFAULT_PORT` |
### 技术选型
| 组件 | 选择 | 版本 |
|------|------|------|
| Web 框架 | Gin | v1.9+ |
| ORM | GORM | v1.25+ |
| 配置管理 | Viper | v1.18+ |
| 日志 | Zap | v1.26+ |
| PostgreSQL 驱动 | pgx | v5+ |
## 组件和接口
### 配置结构
```go
// internal/config/config.go
type Config struct {
Server ServerConfig
Database DatabaseConfig
Redis RedisConfig
Log LogConfig
}
type ServerConfig struct {
Port int `mapstructure:"SERVER_PORT" default:"8888"`
Mode string `mapstructure:"GIN_MODE" default:"release"`
}
type DatabaseConfig struct {
Host string `mapstructure:"DB_HOST" default:"localhost"`
Port int `mapstructure:"DB_PORT" default:"5432"`
User string `mapstructure:"DB_USER" default:"postgres"`
Password string `mapstructure:"DB_PASSWORD"`
Name string `mapstructure:"DB_NAME" default:"xingrin"`
SSLMode string `mapstructure:"DB_SSLMODE" default:"disable"`
MaxOpenConns int `mapstructure:"DB_MAX_OPEN_CONNS" default:"25"`
MaxIdleConns int `mapstructure:"DB_MAX_IDLE_CONNS" default:"5"`
ConnMaxLifetime int `mapstructure:"DB_CONN_MAX_LIFETIME" default:"300"`
}
type RedisConfig struct {
Host string `mapstructure:"REDIS_HOST" default:"localhost"`
Port int `mapstructure:"REDIS_PORT" default:"6379"`
Password string `mapstructure:"REDIS_PASSWORD"`
DB int `mapstructure:"REDIS_DB" default:"0"`
}
type LogConfig struct {
Level string `mapstructure:"LOG_LEVEL" default:"info"`
Format string `mapstructure:"LOG_FORMAT" default:"json"`
}
```
### 数据库连接
```go
// internal/database/database.go
func NewDatabase(cfg *config.DatabaseConfig) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Name, cfg.SSLMode,
)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 使用单数表名
},
})
if err != nil {
return nil, err
}
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetime) * time.Second)
return db, nil
}
```
## 数据模型
### 基础模型定义
```go
// internal/model/target.go
type Target struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"column:name;size:300" json:"name"`
Type string `gorm:"column:type;size:20;default:'domain'" json:"type"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
LastScannedAt *time.Time `gorm:"column:last_scanned_at" json:"lastScannedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
}
func (Target) TableName() string {
return "target"
}
// internal/model/organization.go
type Organization struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"column:name;size:300" json:"name"`
Description string `gorm:"column:description;size:1000" json:"description"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
}
func (Organization) TableName() string {
return "organization"
}
// internal/model/scan.go
type Scan struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
EngineIDs pq.Int64Array `gorm:"column:engine_ids;type:integer[]" json:"engineIds"`
EngineNames datatypes.JSON `gorm:"column:engine_names;type:jsonb" json:"engineNames"`
YamlConfiguration string `gorm:"column:yaml_configuration;type:text" json:"yamlConfiguration"`
ScanMode string `gorm:"column:scan_mode;size:10;default:'full'" json:"scanMode"`
Status string `gorm:"column:status;size:20;default:'initiated'" json:"status"`
ResultsDir string `gorm:"column:results_dir;size:100" json:"resultsDir"`
ContainerIDs pq.StringArray `gorm:"column:container_ids;type:varchar(100)[]" json:"containerIds"`
WorkerID *int `gorm:"column:worker_id" json:"workerId"`
ErrorMessage string `gorm:"column:error_message;size:2000" json:"errorMessage"`
Progress int `gorm:"column:progress;default:0" json:"progress"`
CurrentStage string `gorm:"column:current_stage;size:50" json:"currentStage"`
StageProgress datatypes.JSON `gorm:"column:stage_progress;type:jsonb" json:"stageProgress"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
StoppedAt *time.Time `gorm:"column:stopped_at" json:"stoppedAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"-"`
// 缓存统计
CachedSubdomainsCount int `gorm:"column:cached_subdomains_count" json:"cachedSubdomainsCount"`
CachedWebsitesCount int `gorm:"column:cached_websites_count" json:"cachedWebsitesCount"`
CachedEndpointsCount int `gorm:"column:cached_endpoints_count" json:"cachedEndpointsCount"`
CachedIPsCount int `gorm:"column:cached_ips_count" json:"cachedIpsCount"`
CachedVulnsTotal int `gorm:"column:cached_vulns_total" json:"cachedVulnsTotal"`
}
func (Scan) TableName() string {
return "scan"
}
// internal/model/asset.go
type Subdomain struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
Name string `gorm:"column:name;size:1000" json:"name"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
}
func (Subdomain) TableName() string {
return "subdomain"
}
type WebSite struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
TargetID int `gorm:"column:target_id;not null" json:"targetId"`
URL string `gorm:"column:url;type:text" json:"url"`
Host string `gorm:"column:host;size:253" json:"host"`
Title string `gorm:"column:title;type:text" json:"title"`
StatusCode *int `gorm:"column:status_code" json:"statusCode"`
ContentLength *int `gorm:"column:content_length" json:"contentLength"`
Tech pq.StringArray `gorm:"column:tech;type:varchar(100)[]" json:"tech"`
Webserver string `gorm:"column:webserver;type:text" json:"webserver"`
ResponseHeaders string `gorm:"column:response_headers;type:text" json:"responseHeaders"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
}
func (WebSite) TableName() string {
return "website"
}
// internal/model/engine.go
type WorkerNode struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"column:name;size:100;uniqueIndex" json:"name"`
IPAddress string `gorm:"column:ip_address;type:inet" json:"ipAddress"`
SSHPort int `gorm:"column:ssh_port;default:22" json:"sshPort"`
Username string `gorm:"column:username;size:50;default:'root'" json:"username"`
Password string `gorm:"column:password;size:200" json:"-"`
IsLocal bool `gorm:"column:is_local;default:false" json:"isLocal"`
Status string `gorm:"column:status;size:20;default:'pending'" json:"status"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (WorkerNode) TableName() string {
return "worker_node"
}
type ScanEngine struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"column:name;size:200;uniqueIndex" json:"name"`
Configuration string `gorm:"column:configuration;size:10000" json:"configuration"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
UpdatedAt time.Time `gorm:"column:updated_at;autoUpdateTime" json:"updatedAt"`
}
func (ScanEngine) TableName() string {
return "scan_engine"
}
```
### 健康检查端点
```go
// internal/handler/health.go
type HealthHandler struct {
db *gorm.DB
redis *redis.Client
}
type HealthResponse struct {
Status string `json:"status"`
Database string `json:"database"`
Redis string `json:"redis"`
Details map[string]string `json:"details,omitempty"`
}
func (h *HealthHandler) Check(c *gin.Context) {
resp := HealthResponse{
Status: "healthy",
Database: "connected",
Redis: "connected",
}
// 检查数据库
sqlDB, _ := h.db.DB()
if err := sqlDB.Ping(); err != nil {
resp.Status = "unhealthy"
resp.Database = "disconnected"
}
// 检查 Redis
if err := h.redis.Ping(c).Err(); err != nil {
resp.Status = "unhealthy"
resp.Redis = "disconnected"
}
if resp.Status == "healthy" {
c.JSON(200, resp)
} else {
c.JSON(503, resp)
}
}
```
## 正确性属性
*正确性属性是系统在所有有效执行中都应保持的特性。*
### Property 1: 数据库表名映射正确性
*对于任意* Go 模型,其 TableName() 方法返回的表名应与 Django 模型的 db_table 一致。
**验证: 需求 4.1**
### Property 2: JSON 字段名转换正确性
*对于任意* Go 模型序列化为 JSON所有字段名应为 camelCase 格式。
**验证: 需求 4.6**
### Property 3: 数据库字段映射正确性
*对于任意* Go 模型字段,其 gorm column tag 应与数据库实际列名snake_case一致。
**验证: 需求 4.2**
### Property 4: 配置默认值正确性
*对于任意* 缺失的环境变量,配置系统应返回预定义的默认值。
**验证: 需求 2.4**
## 错误处理
### 启动错误
```go
func main() {
// 加载配置
cfg, err := config.Load()
if err != nil {
log.Fatal("配置加载失败", zap.Error(err))
}
// 连接数据库
db, err := database.NewDatabase(&cfg.Database)
if err != nil {
log.Fatal("数据库连接失败", zap.Error(err))
}
// 验证数据库连接
sqlDB, _ := db.DB()
if err := sqlDB.Ping(); err != nil {
log.Fatal("数据库 Ping 失败", zap.Error(err))
}
log.Info("服务启动成功", zap.Int("port", cfg.Server.Port))
}
```
## 测试策略
### 单元测试
1. **配置测试**: 验证环境变量读取和默认值
2. **模型测试**: 验证表名和字段映射
3. **数据库测试**: 验证连接和基本查询
### 属性测试
```go
// 测试 JSON 字段名为 camelCase
func TestJSONFieldNames(t *testing.T) {
target := model.Target{ID: 1, Name: "test.com"}
jsonBytes, _ := json.Marshal(target)
jsonStr := string(jsonBytes)
// 不应包含 snake_case
assert.NotContains(t, jsonStr, "created_at")
assert.NotContains(t, jsonStr, "last_scanned_at")
// 应包含 camelCase
assert.Contains(t, jsonStr, "createdAt")
assert.Contains(t, jsonStr, "lastScannedAt")
}
// 测试表名映射
func TestTableNames(t *testing.T) {
tests := []struct {
model interface{ TableName() string }
expected string
}{
{model.Target{}, "target"},
{model.Organization{}, "organization"},
{model.Scan{}, "scan"},
{model.Subdomain{}, "subdomain"},
{model.WebSite{}, "website"},
{model.WorkerNode{}, "worker_node"},
{model.ScanEngine{}, "scan_engine"},
}
for _, tt := range tests {
assert.Equal(t, tt.expected, tt.model.TableName())
}
}
```

View File

@@ -1,84 +0,0 @@
# 需求文档
## 简介
创建 Go 后端项目的基础框架,包括项目结构、配置管理、数据库连接、基础数据模型。这是 Go 重构的第一阶段为后续模块认证、API、Worker奠定基础。
## 术语表
- **Server**: Go 后端 API 服务
- **GORM**: Go 的 ORM 库,用于数据库操作
- **Gin**: Go 的 Web 框架
- **Viper**: Go 的配置管理库
## 需求
### 需求 1: Go 项目结构
**用户故事:** 作为开发者,我希望有清晰的项目结构,以便快速理解和扩展代码。
#### 验收标准
1. THE Go_Project SHALL 使用标准 Go 项目布局cmd/, internal/, pkg/
2. THE Go_Project SHALL 包含 go.mod 和 go.sum 依赖管理文件
3. THE Go_Project SHALL 包含 Makefile 用于常用构建命令
4. THE Go_Project SHALL 放置在 `go-backend/` 目录下,与现有 `backend/` 并存
### 需求 2: 配置管理
**用户故事:** 作为 DevOps 工程师,我希望使用环境变量配置服务,以便在不同环境部署。
#### 验收标准
1. THE Go_Server SHALL 从环境变量读取数据库连接信息
2. THE Go_Server SHALL 从环境变量读取 Redis 连接信息
3. THE Go_Server SHALL 从环境变量读取服务端口
4. WHEN 环境变量缺失时THE Go_Server SHALL 使用合理的默认值
5. THE Go_Server SHALL 支持与现有 Django 相同的环境变量名称
### 需求 3: 数据库连接
**用户故事:** 作为开发者,我希望 Go 服务能连接现有 PostgreSQL 数据库,以便复用现有数据。
#### 验收标准
1. THE Go_Server SHALL 使用 GORM 连接 PostgreSQL 数据库
2. THE Go_Server SHALL 复用现有数据库表,不创建新表
3. THE Go_Server SHALL 支持数据库连接池配置
4. WHEN 数据库连接失败时THE Go_Server SHALL 记录错误并退出
5. THE Go_Server SHALL 在启动时验证数据库连接
### 需求 4: 基础数据模型
**用户故事:** 作为开发者,我希望 Go 模型与 Django 模型兼容,以便读写相同的数据。
#### 验收标准
1. THE Go_Model SHALL 映射到现有数据库表(使用相同表名)
2. THE Go_Model SHALL 使用相同的字段名snake_case
3. THE Go_Model SHALL 支持软删除deleted_at 字段)
4. THE Go_Model SHALL 正确处理 PostgreSQL 数组类型(如 engine_ids
5. THE Go_Model SHALL 正确处理 JSONB 类型(如 stage_progress
6. WHEN 序列化为 JSON 时THE Go_Model SHALL 输出 camelCase 字段名
### 需求 5: 日志系统
**用户故事:** 作为运维人员,我希望有结构化日志,以便排查问题。
#### 验收标准
1. THE Go_Server SHALL 使用结构化 JSON 日志格式
2. THE Go_Server SHALL 支持可配置的日志级别DEBUG, INFO, WARN, ERROR
3. THE Go_Server SHALL 在日志中包含请求 ID 用于追踪
4. WHEN 发生错误时THE Go_Server SHALL 记录完整的错误堆栈
### 需求 6: 健康检查
**用户故事:** 作为运维人员,我希望有健康检查端点,以便监控服务状态。
#### 验收标准
1. THE Go_Server SHALL 提供 `/health` 端点
2. WHEN 数据库连接正常时THE Go_Server SHALL 返回 200 OK
3. WHEN 数据库连接异常时THE Go_Server SHALL 返回 503 Service Unavailable
4. THE 健康检查响应 SHALL 包含数据库和 Redis 的连接状态

View File

@@ -1,110 +0,0 @@
# 实现计划: Go 后端基础框架
## 概述
创建 Go 后端项目的基础框架,包括项目结构、配置管理、数据库连接、基础数据模型、日志系统和健康检查端点。
## 任务
- [x] 1. 初始化 Go 项目结构
- [x] 1.1 创建 `go-backend/` 目录和标准布局
- 创建 `cmd/server/`, `internal/config/`, `internal/database/`, `internal/model/`, `internal/handler/`, `internal/middleware/`, `internal/pkg/` 目录
- _需求: 1.1, 1.4_
- [x] 1.2 初始化 Go 模块和依赖
- 创建 `go.mod` 文件,添加 Gin, GORM, Viper, Zap 依赖
- _需求: 1.2_
- [x] 1.3 创建 Makefile
- 包含 `build`, `run`, `test`, `lint` 命令
- _需求: 1.3_
- [x] 1.4 创建 `.env.example` 配置示例文件
- _需求: 2.1, 2.2, 2.3_
- [x] 2. 实现配置管理
- [x] 2.1 实现配置结构体和加载逻辑
- 创建 `internal/config/config.go`
- 定义 ServerConfig, DatabaseConfig, RedisConfig, LogConfig 结构体
- 使用 Viper 从环境变量加载配置
- _需求: 2.1, 2.2, 2.3, 2.4, 2.5_
- [x] 2.2 编写配置默认值属性测试
- **Property 4: 配置默认值正确性**
- **验证: 需求 2.4**
- [x] 3. 实现日志系统
- [x] 3.1 实现 Zap 日志封装
- 创建 `internal/pkg/logger.go`
- 支持 JSON 格式和可配置日志级别
- _需求: 5.1, 5.2, 5.3, 5.4_
- [x] 4. 实现数据库连接
- [x] 4.1 实现数据库连接和连接池
- 创建 `internal/database/database.go`
- 使用 GORM 连接 PostgreSQL
- 配置连接池参数
- _需求: 3.1, 3.3, 3.4, 3.5_
- [x] 5. 实现基础数据模型
- [x] 5.1 实现 Organization 模型
- 创建 `internal/model/organization.go`
- _需求: 4.1, 4.2, 4.3, 4.6_
- [x] 5.2 实现 Target 模型
- 创建 `internal/model/target.go`
- _需求: 4.1, 4.2, 4.3, 4.6_
- [x] 5.3 实现 Scan 模型
- 创建 `internal/model/scan.go`
- 处理 PostgreSQL 数组类型和 JSONB 类型
- _需求: 4.1, 4.2, 4.4, 4.5, 4.6_
- [x] 5.4 实现资产模型 (Subdomain, WebSite)
- 创建 `internal/model/subdomain.go`, `internal/model/website.go`
- _需求: 4.1, 4.2, 4.6_
- [x] 5.5 实现引擎模型 (WorkerNode, ScanEngine)
- 创建 `internal/model/worker_node.go`, `internal/model/scan_engine.go`
- _需求: 4.1, 4.2, 4.6_
- [x] 5.6 编写模型表名映射属性测试
- **Property 1: 数据库表名映射正确性**
- **验证: 需求 4.1**
- [x] 5.7 编写 JSON 字段名属性测试
- **Property 2: JSON 字段名转换正确性**
- **验证: 需求 4.6**
- [x] 5.8 编写数据库字段映射属性测试
- **Property 3: 数据库字段映射正确性**
- **验证: 需求 4.2**
- [x] 6. 实现中间件
- [x] 6.1 实现日志中间件
- 创建 `internal/middleware/logger.go`
- 记录请求信息和请求 ID
- _需求: 5.3_
- [x] 6.2 实现 Recovery 中间件
- 创建 `internal/middleware/recovery.go`
- 捕获 panic 并记录错误堆栈
- _需求: 5.4_
- [x] 7. 实现健康检查端点
- [x] 7.1 实现健康检查 Handler
- 创建 `internal/handler/health.go`
- 检查数据库和 Redis 连接状态
- _需求: 6.1, 6.2, 6.3, 6.4_
- [x] 7.2 实现响应工具
- 创建 `internal/pkg/response.go`
- 统一 API 响应格式
- _需求: 6.4_
- [x] 8. 实现服务入口
- [x] 8.1 实现 main.go
- 创建 `cmd/server/main.go`
- 初始化配置、日志、数据库
- 注册路由和中间件
- 启动 HTTP 服务
- _需求: 3.4, 3.5_
- [x] 9. 检查点 - 验证基础框架
- 确保所有测试通过
- 验证服务能正常启动并连接数据库
- 验证 `/health` 端点返回正确状态
- 如有问题请询问用户
## 备注
- 每个任务都引用了具体的需求以便追踪
- 检查点用于确保增量验证
- 属性测试验证通用正确性属性

View File

@@ -1,66 +0,0 @@
# 设计文档
## 概述
补全 Go 后端的所有数据模型,确保与 Django 模型完全兼容。
## 模型设计原则
1. **表名一致** - TableName() 返回与 Django db_table 相同的值
2. **字段名一致** - gorm column tag 使用 snake_case
3. **JSON 输出** - json tag 使用 camelCase
4. **类型映射** - PostgreSQL 数组用 pq.StringArray/Int64ArrayJSONB 用 datatypes.JSON
## 模型文件组织
```
go-backend/internal/model/
├── organization.go # ✅ 已有
├── target.go # ✅ 已有
├── scan.go # ⚠️ 需补充字段
├── subdomain.go # ✅ 已有
├── website.go # ⚠️ 需补充字段
├── worker_node.go # ✅ 已有
├── scan_engine.go # ✅ 已有
├── endpoint.go # 新增
├── directory.go # 新增
├── host_port_mapping.go # 新增
├── vulnerability.go # 新增
├── screenshot.go # 新增
├── subdomain_snapshot.go # 新增
├── website_snapshot.go # 新增
├── endpoint_snapshot.go # 新增
├── directory_snapshot.go # 新增
├── host_port_mapping_snapshot.go # 新增
├── vulnerability_snapshot.go # 新增
├── screenshot_snapshot.go # 新增
├── scan_log.go # 新增
├── scan_input_target.go # 新增
├── scheduled_scan.go # 新增
├── subfinder_provider_settings.go # 新增
├── wordlist.go # 新增
├── nuclei_template_repo.go # 新增
├── notification.go # 新增
├── notification_settings.go # 新增
├── blacklist_rule.go # 新增
├── asset_statistics.go # 新增
├── statistics_history.go # 新增
├── user.go # 新增
├── session.go # 新增
└── model_test.go # 更新测试
```
## 正确性属性
### Property 1: 表名映射正确性
*对于任意* Go 模型,其 TableName() 方法返回的表名应与 Django 模型的 db_table 一致。
**验证: 需求 9.1**
### Property 2: JSON 字段名转换正确性
*对于任意* Go 模型序列化为 JSON所有字段名应为 camelCase 格式。
**验证: 需求 9.5**
## 测试策略
- 更新 model_test.go添加所有新模型的表名测试
- 验证 JSON 序列化输出格式

View File

@@ -1,152 +0,0 @@
# 需求文档
## 简介
补全 Go 后端的所有数据模型,确保与 Django 模型完全兼容。包括新增缺失模型和修复已有模型的缺失字段。
## 术语表
- **Model**: Go 结构体,映射到数据库表
- **GORM**: Go 的 ORM 库
- **Snapshot**: 快照表,记录某次扫描的结果
## 模型清单
### 已有模型(需检查字段完整性)
| 模型 | 表名 | 状态 |
|------|------|------|
| Organization | organization | ✅ 完整 |
| Target | target | ✅ 完整 |
| Scan | scan | ⚠️ 缺少字段 |
| Subdomain | subdomain | ✅ 完整 |
| WebSite | website | ⚠️ 缺少字段 |
| WorkerNode | worker_node | ✅ 完整 |
| ScanEngine | scan_engine | ✅ 完整 |
### 缺失模型
| 模型 | 表名 | 分类 |
|------|------|------|
| Endpoint | endpoint | 资产 |
| Directory | directory | 资产 |
| HostPortMapping | host_port_mapping | 资产 |
| Vulnerability | vulnerability | 资产 |
| SubdomainSnapshot | subdomain_snapshot | 快照 |
| WebsiteSnapshot | website_snapshot | 快照 |
| EndpointSnapshot | endpoint_snapshot | 快照 |
| DirectorySnapshot | directory_snapshot | 快照 |
| HostPortMappingSnapshot | host_port_mapping_snapshot | 快照 |
| VulnerabilitySnapshot | vulnerability_snapshot | 快照 |
| ScreenshotSnapshot | screenshot_snapshot | 快照 |
| Screenshot | screenshot | 资产 |
| ScanLog | scan_log | 扫描 |
| ScanInputTarget | scan_input_target | 扫描 |
| ScheduledScan | scheduled_scan | 扫描 |
| SubfinderProviderSettings | subfinder_provider_settings | 配置 |
| Wordlist | wordlist | 引擎 |
| NucleiTemplateRepo | nuclei_template_repo | 引擎 |
| Notification | notification | 通知 |
| NotificationSettings | notification_settings | 通知 |
| BlacklistRule | blacklist_rule | 配置 |
| AssetStatistics | asset_statistics | 统计 |
| StatisticsHistory | statistics_history | 统计 |
| User | auth_user | 认证 |
| Session | django_session | 认证 |
## 需求
### 需求 1: 修复已有模型
**用户故事:** 作为开发者,我希望已有的 Go 模型字段与 Django 完全一致。
#### 验收标准
1. THE Scan 模型 SHALL 添加缺失字段cached_directories_count, cached_screenshots_count, cached_vulns_critical/high/medium/low, stats_updated_at
2. THE WebSite 模型 SHALL 添加缺失字段location, response_body, content_type, vhost
### 需求 2: 资产模型
**用户故事:** 作为开发者,我希望 Go 后端有完整的资产模型。
#### 验收标准
1. THE Go_Model SHALL 实现 Endpoint 模型
2. THE Go_Model SHALL 实现 Directory 模型
3. THE Go_Model SHALL 实现 HostPortMapping 模型
4. THE Go_Model SHALL 实现 Vulnerability 模型
5. THE Go_Model SHALL 实现 Screenshot 模型
### 需求 3: 快照模型
**用户故事:** 作为开发者,我希望 Go 后端有完整的快照模型。
#### 验收标准
1. THE Go_Model SHALL 实现 SubdomainSnapshot 模型
2. THE Go_Model SHALL 实现 WebsiteSnapshot 模型
3. THE Go_Model SHALL 实现 EndpointSnapshot 模型
4. THE Go_Model SHALL 实现 DirectorySnapshot 模型
5. THE Go_Model SHALL 实现 HostPortMappingSnapshot 模型
6. THE Go_Model SHALL 实现 VulnerabilitySnapshot 模型
7. THE Go_Model SHALL 实现 ScreenshotSnapshot 模型
### 需求 4: 扫描相关模型
**用户故事:** 作为开发者,我希望 Go 后端有完整的扫描相关模型。
#### 验收标准
1. THE Go_Model SHALL 实现 ScanLog 模型
2. THE Go_Model SHALL 实现 ScanInputTarget 模型
3. THE Go_Model SHALL 实现 ScheduledScan 模型
4. THE Go_Model SHALL 实现 SubfinderProviderSettings 模型(单例)
### 需求 5: 引擎相关模型
**用户故事:** 作为开发者,我希望 Go 后端有完整的引擎相关模型。
#### 验收标准
1. THE Go_Model SHALL 实现 Wordlist 模型
2. THE Go_Model SHALL 实现 NucleiTemplateRepo 模型
### 需求 6: 通知和配置模型
**用户故事:** 作为开发者,我希望 Go 后端有完整的通知和配置模型。
#### 验收标准
1. THE Go_Model SHALL 实现 Notification 模型
2. THE Go_Model SHALL 实现 NotificationSettings 模型(单例)
3. THE Go_Model SHALL 实现 BlacklistRule 模型
### 需求 7: 统计模型
**用户故事:** 作为开发者,我希望 Go 后端有完整的统计模型。
#### 验收标准
1. THE Go_Model SHALL 实现 AssetStatistics 模型(单例)
2. THE Go_Model SHALL 实现 StatisticsHistory 模型
### 需求 8: 认证模型
**用户故事:** 作为开发者,我希望 Go 后端有用户模型,以便实现认证。
#### 验收标准
1. THE Go_Model SHALL 实现 User 模型(兼容 Django auth_user 表)
2. THE Go_Model SHALL 实现 Session 模型(兼容 Django django_session 表)
### 需求 9: 模型一致性
**用户故事:** 作为开发者,我希望所有 Go 模型与 Django 模型完全一致。
#### 验收标准
1. THE Go_Model SHALL 使用相同的表名TableName() 方法)
2. THE Go_Model SHALL 使用相同的字段名gorm column tag
3. THE Go_Model SHALL 正确处理 PostgreSQL 数组类型
4. THE Go_Model SHALL 正确处理 JSONB 类型
5. WHEN 序列化为 JSON 时THE Go_Model SHALL 输出 camelCase 字段名

View File

@@ -1,90 +0,0 @@
# 实现计划: Go 模型补全
## 概述
补全所有 Go 数据模型,确保与 Django 模型完全兼容。
## 任务
- [x] 1. 修复已有模型
- [x] 1.1 补充 Scan 模型缺失字段
- 添加 cached_directories_count, cached_screenshots_count
- 添加 cached_vulns_critical/high/medium/low
- 添加 stats_updated_at
- _需求: 1.1_
- [x] 1.2 补充 WebSite 模型缺失字段
- 添加 location, response_body, content_type, vhost
- _需求: 1.2_
- [x] 2. 实现资产模型
- [x] 2.1 实现 Endpoint 模型
- _需求: 2.1_
- [x] 2.2 实现 Directory 模型
- _需求: 2.2_
- [x] 2.3 实现 HostPortMapping 模型
- _需求: 2.3_
- [x] 2.4 实现 Vulnerability 模型
- _需求: 2.4_
- [x] 2.5 实现 Screenshot 模型
- _需求: 2.5_
- [x] 3. 实现快照模型
- [x] 3.1 实现 SubdomainSnapshot 模型
- _需求: 3.1_
- [x] 3.2 实现 WebsiteSnapshot 模型
- _需求: 3.2_
- [x] 3.3 实现 EndpointSnapshot 模型
- _需求: 3.3_
- [x] 3.4 实现 DirectorySnapshot 模型
- _需求: 3.4_
- [x] 3.5 实现 HostPortMappingSnapshot 模型
- _需求: 3.5_
- [x] 3.6 实现 VulnerabilitySnapshot 模型
- _需求: 3.6_
- [x] 3.7 实现 ScreenshotSnapshot 模型
- _需求: 3.7_
- [x] 4. 实现扫描相关模型
- [x] 4.1 实现 ScanLog 模型
- _需求: 4.1_
- [x] 4.2 实现 ScanInputTarget 模型
- _需求: 4.2_
- [x] 4.3 实现 ScheduledScan 模型
- _需求: 4.3_
- [x] 4.4 实现 SubfinderProviderSettings 模型
- _需求: 4.4_
- [x] 5. 实现引擎相关模型
- [x] 5.1 实现 Wordlist 模型
- _需求: 5.1_
- [x] 5.2 实现 NucleiTemplateRepo 模型
- _需求: 5.2_
- [x] 6. 实现通知和配置模型
- [x] 6.1 实现 Notification 模型
- _需求: 6.1_
- [x] 6.2 实现 NotificationSettings 模型
- _需求: 6.2_
- [x] 6.3 实现 BlacklistRule 模型
- _需求: 6.3_
- [x] 7. 实现统计模型
- [x] 7.1 实现 AssetStatistics 模型
- _需求: 7.1_
- [x] 7.2 实现 StatisticsHistory 模型
- _需求: 7.2_
- [x] 8. 实现认证模型
- [x] 8.1 实现 User 模型
- _需求: 8.1_
- [x] 8.2 实现 Session 模型
- _需求: 8.2_
- [x] 9. 更新测试
- [x] 9.1 更新 model_test.go
- 添加所有新模型的表名测试
- _需求: 9.1_
- [x] 10. 检查点 - 验证模型完整性
- 所有测试通过 ✅
- 模型数量与 Django 一致 ✅ (33 个模型)

View File

@@ -1,300 +0,0 @@
# 设计文档
## 概述
本文档描述 Go 后端网站快照WebsiteSnapshotAPI 的技术设计。该 API 作为扫描结果的写入入口,同时提供快照数据的查询和导出功能。
核心设计原则:
- **单一写入入口**Worker 通过一个接口提交扫描结果,内部自动同步到快照表和资产表
- **Service 层解耦**WebsiteSnapshotService 独立于 WebsiteService通过组合调用实现同步
- **复用现有代码**:复用已有的 WebsiteService.BulkUpsert 方法写入资产表
## 架构
```
┌─────────────────────────────────────────────────────────────────┐
│ HTTP Layer │
├─────────────────────────────────────────────────────────────────┤
│ WebsiteSnapshotHandler │
│ - BulkUpsert() POST /scans/{id}/websites/bulk-upsert │
│ - List() GET /scans/{id}/websites/ │
│ - Export() GET /scans/{id}/websites/export/ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Service Layer │
├─────────────────────────────────────────────────────────────────┤
│ WebsiteSnapshotService │
│ - SaveAndSync() 写入快照 + 同步资产 │
│ - ListByScan() 查询快照 │
│ - StreamByScan() 流式导出 │
│ │ │
│ ↓ 调用 │
│ WebsiteService已有
│ - BulkUpsert() 写入资产表 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Repository Layer │
├─────────────────────────────────────────────────────────────────┤
│ WebsiteSnapshotRepository WebsiteRepository已有
│ - BulkCreate() - BulkUpsert() │
│ - FindByScanID() │
│ - StreamByScanID() │
│ - CountByScanID() │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Database Layer │
├─────────────────────────────────────────────────────────────────┤
│ website_snapshot 表 website 表 │
│ - scan_id (FK) - target_id (FK) │
│ - url (unique with scan_id) - url (unique with target_id) │
│ - host, title, status_code... - host, title, status_code... │
└─────────────────────────────────────────────────────────────────┘
```
## 组件和接口
### Handler: WebsiteSnapshotHandler
```go
// go-backend/internal/handler/website_snapshot.go
type WebsiteSnapshotHandler struct {
svc *service.WebsiteSnapshotService
}
// BulkUpsert 批量写入网站快照(扫描结果导入)
// POST /api/scans/:scan_id/websites/bulk-upsert
func (h *WebsiteSnapshotHandler) BulkUpsert(c *gin.Context)
// List 查询网站快照列表
// GET /api/scans/:scan_id/websites/
func (h *WebsiteSnapshotHandler) List(c *gin.Context)
// Export 导出网站快照为 CSV
// GET /api/scans/:scan_id/websites/export/
func (h *WebsiteSnapshotHandler) Export(c *gin.Context)
```
### Service: WebsiteSnapshotService
```go
// go-backend/internal/service/website_snapshot.go
type WebsiteSnapshotService struct {
snapshotRepo *repository.WebsiteSnapshotRepository
scanRepo *repository.ScanRepository
websiteService *WebsiteService // 复用已有的资产 Service
}
// SaveAndSync 保存快照并同步到资产表
// 1. 验证 Scan 存在且未被软删除
// 2. 写入 website_snapshot 表
// 3. 调用 WebsiteService.BulkUpsert 写入 website 表
func (s *WebsiteSnapshotService) SaveAndSync(scanID int, items []dto.WebsiteSnapshotItem) (int64, error)
// ListByScan 查询指定扫描的快照列表
func (s *WebsiteSnapshotService) ListByScan(scanID int, query *dto.WebsiteSnapshotListQuery) ([]model.WebsiteSnapshot, int64, error)
// StreamByScan 流式获取快照数据(用于 CSV 导出)
func (s *WebsiteSnapshotService) StreamByScan(scanID int) (*sql.Rows, error)
// CountByScan 获取快照数量
func (s *WebsiteSnapshotService) CountByScan(scanID int) (int64, error)
```
### Repository: WebsiteSnapshotRepository
```go
// go-backend/internal/repository/website_snapshot.go
type WebsiteSnapshotRepository struct {
db *gorm.DB
}
// BulkCreate 批量创建快照ON CONFLICT DO NOTHING
func (r *WebsiteSnapshotRepository) BulkCreate(snapshots []model.WebsiteSnapshot) (int64, error)
// FindByScanID 查询指定扫描的快照(支持分页、过滤、排序)
func (r *WebsiteSnapshotRepository) FindByScanID(scanID int, page, pageSize int, filter, ordering string) ([]model.WebsiteSnapshot, int64, error)
// StreamByScanID 流式获取快照数据
func (r *WebsiteSnapshotRepository) StreamByScanID(scanID int) (*sql.Rows, error)
// CountByScanID 获取快照数量
func (r *WebsiteSnapshotRepository) CountByScanID(scanID int) (int64, error)
// ScanRow 扫描单行数据
func (r *WebsiteSnapshotRepository) ScanRow(rows *sql.Rows) (*model.WebsiteSnapshot, error)
```
## 数据模型
### 请求 DTO
```go
// go-backend/internal/dto/website_snapshot.go
// WebsiteSnapshotItem 单个网站快照数据
type WebsiteSnapshotItem struct {
URL string `json:"url" binding:"required,url"`
Host string `json:"host"`
Title string `json:"title"`
StatusCode *int `json:"statusCode"`
ContentLength *int64 `json:"contentLength"`
Location string `json:"location"`
Webserver string `json:"webserver"`
ContentType string `json:"contentType"`
Tech []string `json:"tech"`
ResponseBody string `json:"responseBody"`
Vhost *bool `json:"vhost"`
ResponseHeaders string `json:"responseHeaders"`
}
// BulkUpsertWebsiteSnapshotsRequest 批量写入请求
type BulkUpsertWebsiteSnapshotsRequest struct {
TargetID int `json:"targetId" binding:"required"`
Websites []WebsiteSnapshotItem `json:"websites" binding:"required,min=1,max=5000,dive"`
}
// BulkUpsertWebsiteSnapshotsResponse 批量写入响应
type BulkUpsertWebsiteSnapshotsResponse struct {
SnapshotCount int `json:"snapshotCount"`
AssetCount int `json:"assetCount"`
}
// WebsiteSnapshotListQuery 列表查询参数
type WebsiteSnapshotListQuery struct {
PaginationQuery
Filter string `form:"filter"`
Ordering string `form:"ordering"`
}
// WebsiteSnapshotResponse 快照响应
type WebsiteSnapshotResponse struct {
ID int `json:"id"`
ScanID int `json:"scanId"`
URL string `json:"url"`
Host string `json:"host"`
Title string `json:"title"`
StatusCode *int `json:"statusCode"`
ContentLength *int64 `json:"contentLength"`
Location string `json:"location"`
Webserver string `json:"webserver"`
ContentType string `json:"contentType"`
Tech []string `json:"tech"`
ResponseBody string `json:"responseBody"`
Vhost *bool `json:"vhost"`
ResponseHeaders string `json:"responseHeaders"`
CreatedAt time.Time `json:"createdAt"`
}
```
### 过滤字段映射
```go
var WebsiteSnapshotFilterMapping = scope.FilterMapping{
"url": {Column: "url"},
"host": {Column: "host"},
"title": {Column: "title"},
"status": {Column: "status_code", IsNumeric: true},
"webserver": {Column: "webserver"},
"tech": {Column: "tech", IsArray: true},
}
```
## 正确性属性
*正确性属性是系统在所有有效执行中应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*
### Property 1: 快照和资产同步写入
*For any* 有效的网站快照数据,通过 bulk-upsert 接口写入后,数据应同时存在于 website_snapshot 表和 website 表中,且字段值一致(除了 scan_id/target_id 的差异)。
**Validates: Requirements 1.1, 1.2**
### Property 2: 快照去重
*For any* 包含重复 URL 的请求,写入后 website_snapshot 表中同一 scan_id 下的 URL 应唯一,重复项被忽略。
**Validates: Requirements 1.4**
### Property 3: 资产 Upsert 保留 created_at
*For any* 已存在于 website 表的记录,通过快照同步更新后,其 created_at 字段应保持不变。
**Validates: Requirements 1.5**
### Property 4: 分页正确性
*For any* 分页查询,返回的记录数应不超过 pageSize且 total 应等于该 scan 下的所有快照数量。
**Validates: Requirements 2.3, 2.6**
### Property 5: 过滤正确性
*For any* 带 filter 参数的查询,返回的所有记录应满足过滤条件(文本字段模糊匹配,数字字段精确匹配)。
**Validates: Requirements 3.1, 3.2, 3.4**
### Property 6: 排序正确性
*For any* 带 ordering 参数的查询,返回的记录应按指定字段和方向排序。
**Validates: Requirements 4.1, 4.2, 4.3**
### Property 7: CSV 导出完整性
*For any* CSV 导出请求,导出的记录数应等于该 scan 下的所有快照数量,且每条记录包含所有必需字段。
**Validates: Requirements 5.1, 5.3, 5.4**
### Property 8: Scan 存在性验证
*For any* 快照请求(读或写),如果 scan_id 不存在或已被软删除,应返回 404 错误。
**Validates: Requirements 7.1, 7.2, 7.3, 7.4**
## 错误处理
| 场景 | HTTP 状态码 | 错误码 | 错误信息 |
|------|------------|--------|----------|
| scan_id 无效(非数字) | 400 | BAD_REQUEST | Invalid scan ID |
| scan 不存在 | 404 | NOT_FOUND | Scan not found |
| scan 已软删除 | 404 | NOT_FOUND | Scan not found |
| 请求体格式错误 | 400 | VALIDATION_ERROR | Invalid request body |
| 请求体为空 | 400 | VALIDATION_ERROR | websites is required |
| 超过最大数量限制 | 400 | VALIDATION_ERROR | websites must have at most 5000 items |
| 数据库错误 | 500 | SERVER_ERROR | Failed to save snapshots |
## 测试策略
### 单元测试
- Handler 层:测试请求解析、参数验证、错误响应
- Service 层测试业务逻辑、Scan 验证、同步调用
- Repository 层:测试 SQL 查询、批量操作、去重逻辑
### 属性测试
使用 `github.com/leanovate/gopter` 进行属性测试:
1. **Property 1 测试**:生成随机网站数据,调用 SaveAndSync验证两张表数据一致
2. **Property 2 测试**:生成包含重复 URL 的数据,验证去重行为
3. **Property 3 测试**:先插入资产,再通过快照同步更新,验证 created_at 不变
4. **Property 4 测试**:生成随机分页参数,验证返回结果符合分页规则
5. **Property 5 测试**:生成随机过滤条件,验证返回结果满足条件
6. **Property 6 测试**:生成随机排序参数,验证返回结果有序
7. **Property 7 测试**:生成随机快照数据,导出 CSV验证完整性
8. **Property 8 测试**:使用不存在/已删除的 scan_id验证返回 404
### 测试配置
- 每个属性测试运行 100 次迭代
- 使用测试数据库,每次测试前清理数据
- 测试标签格式:`Feature: go-snapshot-apis, Property N: {property_text}`

View File

@@ -1,106 +0,0 @@
# 需求文档
## 简介
本文档定义了 Go 后端网站快照WebsiteSnapshotAPI 的需求。这是快照 API 系列的第一个实现将作为其他快照类型Subdomain、Endpoint、Directory 等)的参考模板。
网站快照记录了每次扫描时发现的网站及其响应信息作为扫描Scan的嵌套资源提供访问。
**核心设计原则**:扫描结果通过快照 API 写入内部自动同步到资产表Worker 只需调用一个接口。
## 术语表
- **Snapshot_API**: 快照 API 服务,提供对扫描快照数据的访问和写入接口
- **Scan**: 扫描记录,快照的父资源
- **WebsiteSnapshot**: 网站快照记录扫描发现的网站及其响应信息URL、标题、状态码、技术栈等
- **Website**: 网站资产,从快照同步而来的去重资产记录
- **Filter_Query**: 过滤查询字符串,支持字段级别的模糊匹配
- **CSV_Export**: CSV 格式导出功能,支持流式输出
- **Save_And_Sync**: 保存并同步操作,同时写入快照表和资产表
## 需求
### 需求 1: 网站快照批量写入(扫描结果导入)
**用户故事:** 作为扫描 Worker我希望通过一个接口提交扫描发现的网站系统自动保存快照并同步到资产表。
#### 验收标准
1. WHEN Worker 请求 POST /api/scans/{scan_id}/websites/bulk-upsert THEN Snapshot_API SHALL 保存网站快照到 website_snapshot 表
2. WHEN 快照保存成功后 THEN Snapshot_API SHALL 自动同步数据到 website 资产表upsert 模式)
3. WHEN scan_id 不存在或已被软删除 THEN Snapshot_API SHALL 返回 404 Not Found 错误
4. WHEN 请求体包含重复的 URL THEN Snapshot_API SHALL 基于唯一约束scan_id + url去重忽略冲突
5. 资产表同步 SHALL 使用 upsert 策略:新记录插入,已存在记录更新(保留 created_at
6. 响应 SHALL 返回成功写入的记录数
### 需求 2: 网站快照列表查询
**用户故事:** 作为安全分析师,我希望查看特定扫描中发现的网站,以便分析 Web 服务及其技术栈。
#### 验收标准
1. WHEN 用户请求 GET /api/scans/{scan_id}/websites/ THEN Snapshot_API SHALL 返回该扫描的分页 WebsiteSnapshot 记录列表
2. WHEN scan_id 不存在 THEN Snapshot_API SHALL 返回 404 Not Found 错误及相应错误信息
3. WHEN 提供 page 和 pageSize 查询参数 THEN Snapshot_API SHALL 返回指定页的结果
4. WHEN 未提供 page 参数 THEN Snapshot_API SHALL 默认返回第 1 页
5. WHEN 未提供 pageSize 参数 THEN Snapshot_API SHALL 默认每页返回 20 条记录
6. 响应 SHALL 包含分页元数据:总数、当前页、每页大小
7. 响应 SHALL 包含所有网站字段id, scanId, url, host, title, statusCode, contentLength, location, webserver, contentType, tech, responseBody, vhost, responseHeaders, createdAt
### 需求 3: 网站快照过滤查询
**用户故事:** 作为安全分析师,我希望按多个字段过滤网站快照,以便快速找到相关记录。
#### 验收标准
1. WHEN 提供 filter 查询参数 THEN Snapshot_API SHALL 使用模糊匹配LIKE过滤 url、host、title、webserver 字段
2. WHEN filter 包含数字值 THEN Snapshot_API SHALL 同时使用精确匹配过滤 statusCode 字段
3. WHEN filter 参数为空或未提供 THEN Snapshot_API SHALL 返回该扫描的所有记录
4. 文本字段的过滤匹配 SHALL 不区分大小写
### 需求 4: 网站快照排序
**用户故事:** 作为安全分析师,我希望按不同字段对网站快照排序,以便根据分析需求组织数据。
#### 验收标准
1. WHEN 提供 ordering 查询参数 THEN Snapshot_API SHALL 按指定字段排序结果
2. Snapshot_API SHALL 支持按以下字段排序url, host, title, statusCode, createdAt
3. WHEN ordering 以 "-" 前缀开头 THEN Snapshot_API SHALL 按降序排序
4. WHEN 未提供 ordering 参数 THEN Snapshot_API SHALL 默认按 createdAt 降序排序
### 需求 5: 网站快照导出
**用户故事:** 作为安全分析师,我希望将网站快照导出为 CSV以便进行离线分析或与团队分享。
#### 验收标准
1. WHEN 用户请求 GET /api/scans/{scan_id}/websites/export/ THEN Snapshot_API SHALL 返回包含该扫描所有 WebsiteSnapshot 记录的 CSV 文件
2. WHEN scan_id 不存在 THEN Snapshot_API SHALL 返回 404 Not Found 错误
3. CSV 文件 SHALL 包含以下列url, host, location, title, status_code, content_length, content_type, webserver, tech, response_body, response_headers, vhost, created_at
4. tech 数组字段 SHALL 在 CSV 中格式化为逗号分隔的值
5. Snapshot_API SHALL 使用流式响应以高效处理大数据集
6. 响应 Content-Type 头 SHALL 设置为 text/csv
7. 响应 Content-Disposition 头 SHALL 包含文件名格式为scan-{scan_id}-websites.csv
### 需求 6: API 响应格式
**用户故事:** 作为前端开发者,我希望 API 响应格式一致,以便轻松与后端集成。
#### 验收标准
1. Snapshot_API SHALL 为列表端点返回结构一致的 JSON 响应
2. 列表响应 SHALL 遵循格式:{ "results": [...], "total": number, "page": number, "pageSize": number, "totalPages": number }
3. 错误响应 SHALL 遵循格式:{ "error": { "code": string, "message": string } }
4. Snapshot_API SHALL 返回适当的 HTTP 状态码200 表示成功400 表示请求错误404 表示未找到500 表示服务器错误
### 需求 7: 扫描存在性验证
**用户故事:** 作为系统,我希望在查询或写入快照前验证扫描是否存在,以便提供有意义的错误信息并防止孤立数据。
#### 验收标准
1. WHEN 处理任何快照请求 THEN Snapshot_API SHALL 首先验证扫描是否存在
2. WHEN 扫描不存在 THEN Snapshot_API SHALL 返回 404 并提示 "Scan not found"
3. WHEN 扫描已被软删除deleted_at 不为空THEN Snapshot_API SHALL 将其视为不存在
4. WHEN 扫描已删除但 Worker 仍在提交结果 THEN Snapshot_API SHALL 拒绝写入并返回 404

View File

@@ -1,84 +0,0 @@
# 实现计划: Go 网站快照 API
## 概述
实现 Go 后端的网站快照 API包括批量写入同步到资产表、列表查询、CSV 导出三个接口。
## 任务
- [x] 1. 创建 DTO 定义
- 创建 `go-backend/internal/dto/website_snapshot.go`
- 定义请求和响应结构体
- _Requirements: 1.6, 2.7, 5.3, 6.1, 6.2_
- [x] 2. 创建 Repository 层
- [x] 2.1 创建 WebsiteSnapshotRepository
- 创建 `go-backend/internal/repository/website_snapshot.go`
- 实现 BulkCreateON CONFLICT DO NOTHING
- 实现 FindByScanID分页、过滤、排序
- 实现 StreamByScanID 和 CountByScanID
- _Requirements: 1.4, 2.1, 3.1, 4.1_
- [x] 2.2 编写 Repository 单元测试
- 测试 BulkCreate 去重逻辑
- 测试分页、过滤、排序
- _Requirements: 1.4, 2.3, 3.1, 4.1_
- [x] 3. 创建 Service 层
- [x] 3.1 创建 WebsiteSnapshotService
- 创建 `go-backend/internal/service/website_snapshot.go`
- 实现 SaveAndSync写快照 + 调用 WebsiteService.BulkUpsert
- 实现 ListByScan、StreamByScan、CountByScan
- 添加 Scan 存在性验证
- _Requirements: 1.1, 1.2, 1.3, 7.1, 7.3_
- [x] 3.2 编写 Property 测试:快照和资产同步写入
- **Property 1: 快照和资产同步写入**
- **Validates: Requirements 1.1, 1.2**
- [x] 3.3 编写 Property 测试Scan 存在性验证
- **Property 8: Scan 存在性验证**
- **Validates: Requirements 7.1, 7.2, 7.3, 7.4**
- [x] 4. 创建 Handler 层
- [x] 4.1 创建 WebsiteSnapshotHandler
- 创建 `go-backend/internal/handler/website_snapshot.go`
- 实现 BulkUpsert 接口
- 实现 List 接口
- 实现 Export 接口
- _Requirements: 1.1, 2.1, 5.1_
- [x] 4.2 编写 Property 测试:分页正确性 (合并到集成测试)
- **Property 4: 分页正确性**
- **Validates: Requirements 2.3, 2.6**
- [x] 4.3 编写 Property 测试:过滤正确性 (合并到集成测试)
- **Property 5: 过滤正确性**
- **Validates: Requirements 3.1, 3.2, 3.4**
- [x] 5. 注册路由和依赖注入
- [x] 5.1 更新路由配置
-`go-backend/cmd/server/main.go` 或路由文件中注册新路由
- 配置依赖注入
- _Requirements: 1.1, 2.1, 5.1_
- [x] 6. Checkpoint - 确保所有测试通过
- 运行所有测试,确保通过
- 如有问题,询问用户
- [x] 7. 编写集成测试
- [x] 7.1 编写 API 集成测试
- 测试完整的请求-响应流程
- 测试错误场景
- _Requirements: 1.3, 2.2, 5.2, 6.3, 6.4_
- 注: Handler 测试已覆盖主要场景,包括分页、过滤、错误处理
- [x] 8. 最终 Checkpoint
- 确保所有测试通过
- 如有问题,询问用户
## 备注
- 每个任务引用具体的需求以便追溯
- Property 测试验证核心正确性属性
- 单元测试验证具体示例和边界情况

View File

@@ -1,253 +0,0 @@
# Design Document: Host Port Mapping API
## Overview
为 Go 后端实现 host_port_mapping 资产的 CRUD API。该 API 遵循项目现有的资产 API 设计模式(参考 website、subdomain提供列表查询、CSV 导出、批量创建、批量 upsert 和批量删除功能。
## Architecture
```
HTTP Request → Handler → Service → Repository → Database
↓ ↓ ↓
DTO Business GORM Model
Logic
```
### API Routes
| Method | Route | Handler | Description |
|--------|-------|---------|-------------|
| GET | `/targets/:id/host-port-mappings` | List | 分页列表(按 IP 聚合) |
| GET | `/targets/:id/host-port-mappings/export` | Export | CSV 导出(原始格式) |
| POST | `/targets/:id/host-port-mappings/bulk-upsert` | BulkUpsert | 批量 upsert扫描器写入 |
| POST | `/host-port-mappings/bulk-delete` | BulkDelete | 批量删除(按 IP 删除) |
### 响应格式说明
**List 接口**:返回按 IP 聚合的数据,与前端 `IPAddress` 类型匹配:
```typescript
// 前端期望的格式
interface IPAddress {
ip: string // IP 地址(唯一标识)
hosts: string[] // 关联的主机名列表
ports: number[] // 关联的端口列表
createdAt: string // 最早创建时间
}
```
**Export 接口**:返回原始格式 CSV每行一个 host+ip+port 组合):
```csv
ip,host,port,created_at
192.168.1.1,example.com,80,2024-01-01 12:00:00
192.168.1.1,example.com,443,2024-01-01 12:00:00
```
**Bulk Delete 接口**:接收 IP 字符串列表(不是 ID删除这些 IP 的所有映射记录
## Components and Interfaces
### Handler Layer
```go
// HostPortMappingHandler handles HTTP requests
type HostPortMappingHandler struct {
svc *HostPortMappingService
}
func (h *HostPortMappingHandler) List(c *gin.Context)
func (h *HostPortMappingHandler) Export(c *gin.Context)
func (h *HostPortMappingHandler) BulkUpsert(c *gin.Context)
func (h *HostPortMappingHandler) BulkDelete(c *gin.Context)
```
### Service Layer
```go
// HostPortMappingService handles business logic
type HostPortMappingService struct {
repo *HostPortMappingRepository
targetRepo *TargetRepository
}
// ListByTarget 返回按 IP 聚合的数据(与 Python 后端一致)
// 聚合逻辑:
// 1. 按 IP 分组,获取每个 IP 的最早 created_at
// 2. 对每个 IP收集其所有 hosts 和 ports去重排序
func (s *HostPortMappingService) ListByTarget(targetID int, query *dto.HostPortMappingListQuery) ([]dto.HostPortMappingResponse, int64, error)
// StreamByTarget 流式返回原始数据用于 CSV 导出
func (s *HostPortMappingService) StreamByTarget(targetID int) (*sql.Rows, error)
// BulkUpsert 批量创建(忽略冲突)
// 使用 ON CONFLICT DO NOTHING因为所有字段都在唯一约束中
func (s *HostPortMappingService) BulkUpsert(targetID int, items []dto.HostPortMappingItem) (int64, error)
// BulkDeleteByIPs 按 IP 列表删除所有相关映射
// 与 Python 后端一致:传入 IP 列表,删除这些 IP 的所有记录
func (s *HostPortMappingService) BulkDeleteByIPs(ips []string) (int64, error)
```
### Repository Layer
```go
// HostPortMappingRepository handles database operations
type HostPortMappingRepository struct {
db *gorm.DB
}
// GetIPAggregation 获取按 IP 聚合的数据
// SQL: SELECT ip, MIN(created_at) FROM host_port_mapping WHERE target_id = ? GROUP BY ip ORDER BY MIN(created_at) DESC
func (r *HostPortMappingRepository) GetIPAggregation(targetID int, filter string) ([]IPAggregationRow, error)
// GetHostsAndPortsByIP 获取指定 IP 的所有 hosts 和 ports
func (r *HostPortMappingRepository) GetHostsAndPortsByIP(targetID int, ip string, filter string) (hosts []string, ports []int, error)
// StreamByTargetID 流式返回原始数据用于 CSV 导出
func (r *HostPortMappingRepository) StreamByTargetID(targetID int) (*sql.Rows, error)
// BulkUpsert 批量插入(忽略冲突)
// 使用 ON CONFLICT (target_id, host, ip, port) DO NOTHING
func (r *HostPortMappingRepository) BulkUpsert(mappings []model.HostPortMapping) (int64, error)
// DeleteByIPs 按 IP 列表删除
// SQL: DELETE FROM host_port_mapping WHERE ip IN (?)
func (r *HostPortMappingRepository) DeleteByIPs(ips []string) (int64, error)
// ScanRow 扫描单行数据
func (r *HostPortMappingRepository) ScanRow(rows *sql.Rows) (*model.HostPortMapping, error)
```
## Data Models
### Existing Model (host_port_mapping.go)
```go
type HostPortMapping struct {
ID int `gorm:"primaryKey" json:"id"`
TargetID int `gorm:"column:target_id" json:"targetId"`
Host string `gorm:"column:host" json:"host"`
IP string `gorm:"column:ip;type:inet" json:"ip"`
Port int `gorm:"column:port" json:"port"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
}
```
### DTOs
```go
// Query
type HostPortMappingListQuery struct {
PaginationQuery
Filter string `form:"filter"`
}
// Response (聚合格式,按 IP 分组)
type HostPortMappingResponse struct {
IP string `json:"ip"` // IP 地址(唯一标识)
Hosts []string `json:"hosts"` // 关联的主机名列表
Ports []int `json:"ports"` // 关联的端口列表
CreatedAt time.Time `json:"createdAt"` // 最早创建时间
}
// Paginated Response
type HostPortMappingListResponse struct {
Results []HostPortMappingResponse `json:"results"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
TotalPages int `json:"totalPages"`
}
// Request Item (for bulk upsert)
type HostPortMappingItem struct {
Host string `json:"host" binding:"required"`
IP string `json:"ip" binding:"required,ip"`
Port int `json:"port" binding:"required,min=1,max=65535"`
}
// Bulk Upsert Request (for scanner import)
type BulkUpsertHostPortMappingsRequest struct {
Mappings []HostPortMappingItem `json:"mappings" binding:"required,min=1,max=5000,dive"`
}
// Bulk Upsert Response
type BulkUpsertHostPortMappingsResponse struct {
UpsertedCount int `json:"upsertedCount"`
}
// Bulk Delete Request (by IP list)
type BulkDeleteHostPortMappingsRequest struct {
IPs []string `json:"ips" binding:"required,min=1,dive,ip"`
}
// Bulk Delete Response
type BulkDeleteHostPortMappingsResponse struct {
DeletedCount int64 `json:"deletedCount"`
}
```
### Filter Mapping
```go
var HostPortMappingFilterMapping = scope.FilterMapping{
"host": {Column: "host"},
"ip": {Column: "ip"},
"port": {Column: "port"},
}
```
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system—essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Pagination returns correct subset
*For any* target with N mappings, requesting page P with pageSize S should return at most S items, and the total count should equal N.
**Validates: Requirements 1.1, 1.4**
### Property 2: Filter returns only matching results
*For any* filter query on host/ip/port, all returned mappings should match the filter criteria.
**Validates: Requirements 1.2**
### Property 3: Upsert creates new and preserves existing
*For any* set of mappings, upsert should create new records for non-existing combinations and preserve existing records.
**Validates: Requirements 4.1**
### Property 4: Bulk delete handles non-existent IPs gracefully
*For any* list of IPs (including non-existent ones), bulk delete should remove only existing records and return the count of actually deleted records.
**Validates: Requirements 4.1, 4.3**
## Error Handling
| Error | HTTP Status | Code |
|-------|-------------|------|
| Target not found | 404 | NOT_FOUND |
| Invalid request body | 400 | BAD_REQUEST |
| Invalid target ID | 400 | BAD_REQUEST |
| Internal error | 500 | INTERNAL_ERROR |
## Testing Strategy
### Unit Tests
- Handler: 测试请求解析和响应格式
- Service: 测试业务逻辑和错误处理
- Repository: 测试数据库操作
### Property-Based Tests
使用 `github.com/leanovate/gopter` 进行属性测试:
- Property 1: 分页正确性
- Property 2: 筛选正确性
- Property 3: Upsert 行为
- Property 4: 删除容错性
每个属性测试运行 100 次迭代。

View File

@@ -1,58 +0,0 @@
# Requirements Document
## Introduction
为 Go 后端实现 host_port_mapping 资产的 CRUD API用于管理目标下的主机-IP-端口映射关系。该 API 主要服务于端口扫描结果(如 naabu的存储和查询。
## Glossary
- **Host_Port_Mapping_Service**: 处理主机端口映射业务逻辑的服务层组件
- **Host_Port_Mapping_Repository**: 处理主机端口映射数据库操作的数据访问层组件
- **Host_Port_Mapping_Handler**: 处理主机端口映射 HTTP 请求的控制器组件
- **Target**: 扫描目标可以是域名、IP 或 CIDR
- **Mapping**: 一条 host-ip-port 的映射记录
## Requirements
### Requirement 1: List Host Port Mappings
**User Story:** As a user, I want to list all host-port mappings for a target with pagination and filtering, so that I can view and search the port scan results.
#### Acceptance Criteria
1. WHEN a user requests the list endpoint with a valid target ID, THE Host_Port_Mapping_Handler SHALL return a paginated list of mappings
2. WHEN a user provides filter parameters, THE Host_Port_Mapping_Service SHALL filter results by host, ip, or port
3. WHEN the target does not exist, THE Host_Port_Mapping_Handler SHALL return a 404 error
4. THE Host_Port_Mapping_Repository SHALL support pagination with page and pageSize parameters
### Requirement 2: Export Host Port Mappings
**User Story:** As a user, I want to export all host-port mappings for a target as CSV, so that I can analyze the data externally.
#### Acceptance Criteria
1. WHEN a user requests the export endpoint, THE Host_Port_Mapping_Handler SHALL return a CSV file with all mappings
2. THE Host_Port_Mapping_Service SHALL stream the CSV data to avoid memory issues with large datasets
3. WHEN the target does not exist, THE Host_Port_Mapping_Handler SHALL return a 404 error
### Requirement 3: Bulk Upsert Host Port Mappings
**User Story:** As a scanner, I want to import port scan results in bulk, so that the database stays up-to-date with the latest scan data.
#### Acceptance Criteria
1. WHEN a scanner submits mapping data, THE Host_Port_Mapping_Service SHALL create or update records
2. THE Host_Port_Mapping_Repository SHALL use ON CONFLICT DO NOTHING to handle duplicates (since all fields are in unique constraint)
3. WHEN the target does not exist, THE Host_Port_Mapping_Handler SHALL return a 404 error
4. THE Host_Port_Mapping_Handler SHALL return the count of upserted records
5. THE Host_Port_Mapping_Repository SHALL batch operations to avoid PostgreSQL parameter limits
### Requirement 4: Bulk Delete Host Port Mappings
**User Story:** As a user, I want to delete multiple host-port mappings at once by IP address, so that I can clean up outdated or incorrect data.
#### Acceptance Criteria
1. WHEN a user submits a list of IP addresses, THE Host_Port_Mapping_Service SHALL delete all mapping records for those IPs
2. THE Host_Port_Mapping_Handler SHALL return the count of deleted records
3. IF some IPs do not have any mappings, THE Host_Port_Mapping_Service SHALL delete only the existing ones without error

View File

@@ -1,62 +0,0 @@
# Implementation Plan: Host Port Mapping API
## Overview
为 Go 后端实现 host_port_mapping 资产的 CRUD API包括列表查询按 IP 聚合、CSV 导出、批量 upsert 和批量删除功能。
## Tasks
- [x] 1. 创建 DTO 定义
- [x] 1.1 创建 `go-backend/internal/dto/host_port_mapping.go`
- 定义 `HostPortMappingListQuery`(分页 + 过滤)
- 定义 `HostPortMappingResponse`聚合格式ip, hosts[], ports[], createdAt
- 定义 `HostPortMappingListResponse`(分页响应)
- 定义 `HostPortMappingItem`(单条映射)
- 定义 `BulkUpsertHostPortMappingsRequest/Response`
- 定义 `BulkDeleteHostPortMappingsRequest/Response`(按 IP 列表删除)
- _Requirements: 1.1, 1.4, 3.1, 4.1_
- [x] 2. 创建 Repository 层
- [x] 2.1 创建 `go-backend/internal/repository/host_port_mapping.go`
- 实现 `GetIPAggregation()` - 按 IP 分组查询
- 实现 `GetHostsAndPortsByIP()` - 获取指定 IP 的 hosts 和 ports
- 实现 `StreamByTargetID()` - 流式导出原始数据
- 实现 `BulkUpsert()` - 批量插入ON CONFLICT DO NOTHING
- 实现 `DeleteByIPs()` - 按 IP 列表删除
- 实现 `ScanRow()` - 扫描单行数据
- _Requirements: 1.1, 1.2, 2.1, 3.2, 4.1_
- [x] 3. 创建 Service 层
- [x] 3.1 创建 `go-backend/internal/service/host_port_mapping.go`
- 实现 `ListByTarget()` - 返回按 IP 聚合的分页数据
- 实现 `StreamByTarget()` - 流式导出
- 实现 `BulkUpsert()` - 批量 upsert分批处理
- 实现 `BulkDeleteByIPs()` - 按 IP 删除
- _Requirements: 1.1, 1.2, 2.2, 3.1, 4.1_
- [x] 4. 创建 Handler 层
- [x] 4.1 创建 `go-backend/internal/handler/host_port_mapping.go`
- 实现 `List()` - GET /targets/:id/host-port-mappings
- 实现 `Export()` - GET /targets/:id/host-port-mappings/export
- 实现 `BulkUpsert()` - POST /targets/:id/host-port-mappings/bulk-upsert
- 实现 `BulkDelete()` - POST /host-port-mappings/bulk-delete
- _Requirements: 1.1, 1.3, 2.1, 2.3, 3.3, 3.4, 4.2_
- [x] 5. 注册路由
- [x] 5.1 更新 `go-backend/cmd/server/main.go`
- 注册 `/targets/:id/host-port-mappings` 路由组
- 注册 `/host-port-mappings/bulk-delete` 独立路由
- _Requirements: 1.1, 2.1, 3.1, 4.1_
- [x] 6. Checkpoint - 编译测试
- 确保代码编译通过
- 手动测试 API 端点
- 如有问题请询问用户
## Notes
- 遵循项目现有的资产 API 设计模式(参考 website、subdomain
- List 接口返回按 IP 聚合的数据,与前端 `IPAddress` 类型匹配
- Export 接口返回原始格式 CSV每行一个 host+ip+port 组合)
- Bulk Delete 接收 IP 字符串列表(不是 ID删除这些 IP 的所有映射记录
- 批量操作使用 100 条/批次,避免 PostgreSQL 参数限制

View File

@@ -1,185 +0,0 @@
# Requirements Document: 业界标准研究与示例收集
## Introduction
本 spec 的目标是研究业界领先项目GitHub Actions, Terraform, Helm, Kubernetes如何实现命令模板、配置管理、Schema 验证等功能,并收集具体的代码示例和最佳实践,用于指导我们的重构工作。
## Glossary
- **GitHub Actions**: GitHub 的 CI/CD 平台,使用 YAML 定义工作流
- **Terraform**: HashiCorp 的基础设施即代码工具
- **Helm**: Kubernetes 的包管理器
- **JSON Schema**: 用于验证 JSON/YAML 数据结构的标准
- **Schema Generation**: 从代码或模板自动生成验证 Schema
- **Documentation Generation**: 从代码或 Schema 自动生成文档
## Requirements
### Requirement 1: GitHub Actions 实现研究
**User Story:** 作为开发者,我希望了解 GitHub Actions 如何定义 action 的输入参数,这样可以学习业界标准的参数定义方式。
#### Acceptance Criteria
1. THE 研究 SHALL 找到 GitHub Actions 的 action.yml 文件格式规范
2. THE 研究 SHALL 收集至少 3 个真实 GitHub Actions 项目的 action.yml 示例
3. THE 研究 SHALL 分析参数定义的结构inputs, description, required, default
4. THE 研究 SHALL 记录参数命名规范kebab-case, camelCase
5. THE 研究 SHALL 记录 deprecationMessage 的使用方式
### Requirement 2: Terraform Provider 实现研究
**User Story:** 作为开发者,我希望了解 Terraform Provider 如何定义变量和验证规则,这样可以学习参数类型系统和验证机制。
#### Acceptance Criteria
1. THE 研究 SHALL 找到 Terraform variable 定义的代码示例
2. THE 研究 SHALL 收集 validation block 的使用示例
3. THE 研究 SHALL 分析 sensitive 参数的处理方式
4. THE 研究 SHALL 记录 terraform-plugin-docs 的文档生成机制
5. THE 研究 SHALL 收集至少 2 个真实 Terraform Provider 的代码示例
### Requirement 3: Helm Chart 实现研究
**User Story:** 作为开发者,我希望了解 Helm 如何管理 values.yaml 和 values.schema.json这样可以学习配置验证和 Schema 生成。
#### Acceptance Criteria
1. THE 研究 SHALL 找到 Helm values.schema.json 的格式规范
2. THE 研究 SHALL 收集 helm-schema-gen 插件的使用示例
3. THE 研究 SHALL 分析 values.yaml 和 values.schema.json 的关系
4. THE 研究 SHALL 记录 Schema 验证的时机lint, install, upgrade
5. THE 研究 SHALL 收集至少 2 个真实 Helm Chart 的 Schema 示例
### Requirement 4: Kubernetes Operator 实现研究
**User Story:** 作为开发者,我希望了解 Kubernetes Operator 如何定义 CRD 和验证规则,这样可以学习声明式配置和验证机制。
#### Acceptance Criteria
1. THE 研究 SHALL 找到 CRD (CustomResourceDefinition) 的定义示例
2. THE 研究 SHALL 分析 OpenAPI v3 Schema 在 CRD 中的使用
3. THE 研究 SHALL 记录 validation rules 的定义方式
4. THE 研究 SHALL 收集 kubebuilder 或 operator-sdk 的代码生成示例
5. THE 研究 SHALL 分析 CRD 的版本管理机制
### Requirement 5: 命令模板模式研究
**User Story:** 作为开发者,我希望了解业界如何实现命令模板和参数替换,这样可以选择最佳的实现方式。
#### Acceptance Criteria
1. THE 研究 SHALL 搜索 "command template pattern" 的实现方式
2. THE 研究 SHALL 比较字符串替换 vs 模板引擎text/template, Jinja2
3. THE 研究 SHALL 收集参数占位符的命名规范({param}, {{param}}, $param
4. THE 研究 SHALL 分析参数类型转换的处理方式
5. THE 研究 SHALL 记录错误处理和验证的最佳实践
### Requirement 6: 配置文档生成研究
**User Story:** 作为开发者,我希望了解如何自动生成配置文档,这样可以减少手动维护文档的工作量。
#### Acceptance Criteria
1. THE 研究 SHALL 找到 terraform-plugin-docs 的实现原理
2. THE 研究 SHALL 分析文档生成的输入Schema, 注释, 示例)
3. THE 研究 SHALL 收集文档模板的格式Markdown, HTML
4. THE 研究 SHALL 记录文档生成的触发时机go generate, CI/CD
5. THE 研究 SHALL 收集至少 2 个文档生成工具的示例
### Requirement 7: JSON Schema 生成研究
**User Story:** 作为开发者,我希望了解如何从代码或配置自动生成 JSON Schema这样可以实现配置验证。
#### Acceptance Criteria
1. THE 研究 SHALL 找到从 Go struct 生成 JSON Schema 的工具
2. THE 研究 SHALL 找到从 YAML 生成 JSON Schema 的工具helm-schema-gen
3. THE 研究 SHALL 分析 JSON Schema 的版本选择draft-04, draft-07, 2020-12
4. THE 研究 SHALL 记录 Schema 验证库的选择Go, Python, JavaScript
5. THE 研究 SHALL 收集 Schema 生成的代码示例
### Requirement 8: 参数默认值管理研究
**User Story:** 作为开发者,我希望了解业界如何管理参数默认值,这样可以避免硬编码问题。
#### Acceptance Criteria
1. THE 研究 SHALL 分析 GitHub Actions 的 default 字段使用方式
2. THE 研究 SHALL 分析 Terraform 的 default 字段使用方式
3. THE 研究 SHALL 分析 Helm 的 values.yaml 默认值管理
4. THE 研究 SHALL 记录默认值的优先级规则
5. THE 研究 SHALL 收集默认值覆盖的代码示例
### Requirement 9: YAML 锚点和合并键研究
**User Story:** 作为开发者,我希望了解 YAML 锚点在实际项目中的使用方式,这样可以有效消除配置重复。
#### Acceptance Criteria
1. THE 研究 SHALL 找到 Docker Compose 中使用锚点的示例
2. THE 研究 SHALL 找到 Kubernetes 配置中使用锚点的示例
3. THE 研究 SHALL 找到 GitHub Actions 中使用锚点的示例
4. THE 研究 SHALL 记录锚点的命名规范x-*, _*
5. THE 研究 SHALL 分析合并键的优先级规则
### Requirement 10: 错误处理和验证研究
**User Story:** 作为开发者,我希望了解业界如何提供清晰的错误信息,这样可以改进我们的错误处理。
#### Acceptance Criteria
1. THE 研究 SHALL 收集 Terraform 的错误信息示例
2. THE 研究 SHALL 收集 Helm 的验证错误信息示例
3. THE 研究 SHALL 分析错误信息的结构(位置, 原因, 建议)
4. THE 研究 SHALL 记录错误信息的国际化处理
5. THE 研究 SHALL 收集错误恢复和建议的最佳实践
### Requirement 11: 真实项目代码收集
**User Story:** 作为开发者,我希望收集真实项目的代码示例,这样可以看到完整的实现。
#### Acceptance Criteria
1. THE 研究 SHALL 找到至少 3 个流行的 GitHub Actions 项目
2. THE 研究 SHALL 找到至少 2 个流行的 Terraform Provider 项目
3. THE 研究 SHALL 找到至少 2 个流行的 Helm Chart 项目
4. THE 研究 SHALL 收集这些项目的关键代码片段
5. THE 研究 SHALL 分析这些项目的目录结构和文件组织
### Requirement 12: 最佳实践总结
**User Story:** 作为开发者,我希望总结业界的最佳实践,这样可以指导我们的重构设计。
#### Acceptance Criteria
1. THE 研究 SHALL 总结参数定义的最佳实践
2. THE 研究 SHALL 总结配置验证的最佳实践
3. THE 研究 SHALL 总结文档生成的最佳实践
4. THE 研究 SHALL 总结错误处理的最佳实践
5. THE 研究 SHALL 创建对比表格,比较不同项目的实现方式
### Requirement 13: 示例代码编写
**User Story:** 作为开发者,我希望基于研究结果编写示例代码,这样可以验证设计的可行性。
#### Acceptance Criteria
1. THE 研究 SHALL 编写 Go 代码示例:从 struct 生成 JSON Schema
2. THE 研究 SHALL 编写 Go 代码示例:使用 JSON Schema 验证 YAML
3. THE 研究 SHALL 编写 Go 代码示例:生成配置文档
4. THE 研究 SHALL 编写 YAML 示例:使用锚点定义共享配置
5. THE 研究 SHALL 编写完整的示例项目结构
### Requirement 14: 文档更新
**User Story:** 作为开发者,我希望将研究结果更新到设计文档中,这样可以指导后续的实现。
#### Acceptance Criteria
1. THE 研究 SHALL 更新 design.md添加业界示例章节
2. THE 研究 SHALL 更新 design.md添加代码示例章节
3. THE 研究 SHALL 创建 INDUSTRY_EXAMPLES.md详细记录所有示例
4. THE 研究 SHALL 创建对比表格,比较当前实现 vs 业界标准
5. THE 研究 SHALL 提供重构建议和优先级

View File

@@ -1,599 +0,0 @@
# 设计文档
## 概述
本设计实现快速扫描模式,允许用户只扫描指定的目标(而非整个 Target 下的所有资产)。通过新建 `ScanInputTarget` 表存储用户输入,配合 `ScanInputTargetProvider` 和现有快照表,实现阶段间的精确数据传递。
### 核心价值
1. **精确扫描控制**
- 用户输入 `a.test.com`,只扫描 `a.test.com` 及其发现的子资产
- 不扫描 `test.com` 下的历史资产(如 `www.test.com``api.test.com`
2. **支持大量输入**
- 新建 `ScanInputTarget` 表存储用户输入(支持 1 万+ 条)
- 新建 `ScanInputTargetProvider` 分块迭代读取
- 复用 `SnapshotTargetProvider`阶段2+
- 复用 6 个已存在的快照表
3. **向后兼容**
- 默认使用完整扫描模式(`scan_mode='full'`
- 现有 API 和定时扫描不受影响
## 架构
### 快速扫描 vs 完整扫描流程对比
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 完整扫描(现有行为) │
│ │
│ 用户输入: a.test.com │
│ 创建 Target: test.com (id=1) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 子域名发现 │ → │ 端口扫描 │ → │ 网站扫描 │ → │ URL获取 │ │
│ │ │ │ │ │ │ │ │ │
│ │ Database │ │ Database │ │ Database │ │ Database │ │
│ │ Provider │ │ Provider │ │ Provider │ │ Provider │ │
│ │ (target=1) │ │ (target=1) │ │ (target=1) │ │ (target=1) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↓ ↓ ↓ ↓ │
│ 扫描 test.com 扫描所有子域名 扫描所有端口 扫描所有网站 │
│ 下所有子域名 (包括历史数据) (包括历史数据) (包括历史数据) │
│ │
│ 问题:用户只想扫描 a.test.com但系统扫描了整个 test.com 域 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ 快速扫描(新行为) │
│ │
│ 用户输入: 5000 个目标(域名/IP/URL 混合) │
│ 创建 Target: test.com (id=1) │
│ 创建 Scan: scan_id=100, scan_mode='quick' │
│ 写入 ScanInputTarget 表: 5000 条记录 (scan_id=100) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 子域名发现 │ → │ 端口扫描 │ → │ 网站扫描 │ → │ URL获取 │ │
│ │ │ │ │ │ │ │ │ │
│ │ ScanInput │ │ Snapshot │ │ Snapshot │ │ Snapshot │ │
│ │ Target │ │ Provider │ │ Provider │ │ Provider │ │
│ │ Provider │ │ (scan=100, │ │ (scan=100, │ │ (scan=100, │ │
│ │ (scan=100) │ │ type= │ │ type= │ │ type= │ │
│ │ │ │ subdomain) │ │ host_port) │ │ website) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ↓ ↓ ↓ ↓ │
│ │ │ │ │ │
│ ↓ ↓ ↓ ↓ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Subdomain │ │ HostPort │ │ Website │ │ Endpoint │ │
│ │ Snapshot │ │ Mapping │ │ Snapshot │ │ Snapshot │ │
│ │ (scan=100) │ │ Snapshot │ │ (scan=100) │ │ (scan=100) │ │
│ │ │ │ (scan=100) │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 结果:只扫描用户输入的目标及其发现的子资产,不扫描历史数据 │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Provider 选择逻辑
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ initiate_scan_flow │
│ │
│ 读取 Scan.scan_mode统一创建所有阶段的 Provider │
│ │
│ IF scan_mode == 'quick': │
│ providers = { │
│ 'subdomain_discovery': ScanInputTargetProvider(scan_id), │
│ 'port_scan': SnapshotTargetProvider(scan_id, type='subdomain'), │
│ 'site_scan': SnapshotTargetProvider(scan_id, type='host_port'), │
│ 'url_fetch': SnapshotTargetProvider(scan_id, type='website'), │
│ 'directory_scan': SnapshotTargetProvider(scan_id, type='website'), │
│ } │
│ ELSE: # scan_mode == 'full' │
│ providers = { │
│ 'subdomain_discovery': DatabaseTargetProvider(target_id), │
│ 'port_scan': DatabaseTargetProvider(target_id), │
│ 'site_scan': DatabaseTargetProvider(target_id), │
│ 'url_fetch': DatabaseTargetProvider(target_id), │
│ 'directory_scan': DatabaseTargetProvider(target_id), │
│ } │
│ │
│ 调用子 Flow 时传入对应的 provider │
└─────────────────────────────────────────────────────────────────────────────┘
┌───────────────┼───────────────┬───────────────┬───────────────┐
↓ ↓ ↓ ↓ ↓
subdomain_ port_scan_ site_scan_ url_fetch_ directory_
discovery_flow flow flow flow scan_flow
(provider) (provider) (provider) (provider) (provider)
每个子 Flow 只管用传入的 provider不关心 scan_mode
```
**设计原则**
- Provider 选择逻辑集中在 initiate_scan_flow编排层
- 子 Flow 只负责使用 Provider职责单一
- 不传递 scan_mode 参数,减少耦合
### 文件结构
```
backend/apps/scan/
├── providers/ # 已存在
│ ├── __init__.py
│ ├── base.py # TargetProvider 抽象基类
│ ├── database_provider.py # DatabaseTargetProvider
│ ├── list_provider.py # ListTargetProvider
│ ├── snapshot_provider.py # SnapshotTargetProvider
│ └── scan_input_provider.py # 新增ScanInputTargetProvider
├── models/
│ ├── scan.py # 需修改:添加 scan_mode 字段
│ └── scan_input_target.py # 新增ScanInputTarget 模型
├── flows/
│ ├── initiate_scan_flow.py # 需修改Provider 选择逻辑
│ ├── subdomain_discovery_flow.py # 需修改:写入快照表
│ ├── port_scan_flow.py # 需修改:写入快照表
│ ├── site_scan_flow.py # 需修改:写入快照表
│ └── url_fetch/
│ └── main_flow.py # 需修改:写入快照表
├── services/
│ ├── quick_scan_service.py # 已存在,需修改
│ └── scan_input_target_service.py # 新增ScanInputTarget 服务
└── views/
└── scan_views.py # 需修改API 层
backend/apps/asset/
├── models/
│ └── snapshot_models.py # 已存在6个快照表
└── services/
└── snapshot/ # 已存在
├── subdomain_snapshots_service.py
├── website_snapshots_service.py
├── endpoint_snapshots_service.py
└── host_port_mapping_snapshots_service.py
```
## 组件和接口
### 3.1 ScanInputTarget 模型(新增)
```python
# backend/apps/scan/models/scan_input_target.py
class ScanInputTarget(models.Model):
"""
扫描输入目标表
存储快速扫描时用户输入的目标支持大量数据1万+)的分块迭代。
"""
class InputType(models.TextChoices):
DOMAIN = 'domain', '域名'
IP = 'ip', 'IP地址'
CIDR = 'cidr', 'CIDR'
URL = 'url', 'URL'
id = models.AutoField(primary_key=True)
scan = models.ForeignKey(
'scan.Scan',
on_delete=models.CASCADE,
related_name='input_targets',
help_text='所属的扫描任务'
)
value = models.CharField(max_length=2000, help_text='用户输入的原始值')
input_type = models.CharField(
max_length=10,
choices=InputType.choices,
help_text='输入类型'
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'scan_input_target'
indexes = [
models.Index(fields=['scan']),
models.Index(fields=['input_type']),
]
```
### 3.2 Scan 模型扩展
```python
# backend/apps/scan/models/scan.py
class Scan(models.Model):
"""扫描任务模型"""
class ScanMode(models.TextChoices):
FULL = 'full', '完整扫描'
QUICK = 'quick', '快速扫描'
# ... 现有字段 ...
# 新增字段
scan_mode = models.CharField(
max_length=10,
choices=ScanMode.choices,
default=ScanMode.FULL,
help_text='扫描模式full=完整扫描quick=快速扫描'
)
# 注意:用户输入存储在 ScanInputTarget 表,通过 input_targets 关联
```
### 3.3 ScanInputTargetProvider新增
```python
# backend/apps/scan/providers/scan_input_provider.py
class ScanInputTargetProvider(TargetProvider):
"""
扫描输入目标提供者 - 从 ScanInputTarget 表读取用户输入
用于快速扫描的第一阶段,支持大量输入的分块迭代。
特点:
- 通过 scan_id 查询 ScanInputTarget 表
- 按 input_type 分类返回 hosts/urls
- 支持分块迭代chunk_size=1000
- 不应用黑名单过滤(用户明确指定的目标)
"""
def __init__(self, scan_id: int, context: Optional[ProviderContext] = None):
ctx = context or ProviderContext()
ctx.scan_id = scan_id
super().__init__(ctx)
self._scan_id = scan_id
def _iter_raw_hosts(self) -> Iterator[str]:
"""迭代 domain/ip/cidr 类型的输入"""
from apps.scan.models import ScanInputTarget
queryset = ScanInputTarget.objects.filter(
scan_id=self._scan_id,
input_type__in=['domain', 'ip', 'cidr']
)
for item in queryset.iterator(chunk_size=1000):
yield item.value
def iter_urls(self) -> Iterator[str]:
"""迭代 url 类型的输入"""
from apps.scan.models import ScanInputTarget
queryset = ScanInputTarget.objects.filter(
scan_id=self._scan_id,
input_type='url'
)
for item in queryset.iterator(chunk_size=1000):
yield item.value
def get_blacklist_filter(self) -> None:
"""用户输入不使用黑名单过滤"""
return None
```
### 3.4 initiate_scan_flow 改造
```python
# backend/apps/scan/flows/initiate_scan_flow.py
@flow(name='initiate_scan')
def initiate_scan_flow(
scan_id: int,
target_name: str,
target_id: int,
scan_workspace_dir: str,
engine_name: str,
scheduled_scan_name: str | None = None,
) -> dict:
"""
初始化扫描任务
根据 scan_mode 统一创建所有阶段的 Provider然后传给各子 Flow。
子 Flow 只负责使用 Provider不关心 scan_mode。
"""
# ... 现有代码 ...
# ==================== 创建 Provider ====================
from apps.scan.models import Scan
scan = Scan.objects.get(id=scan_id)
provider_context = ProviderContext(target_id=target_id, scan_id=scan_id)
if scan.scan_mode == Scan.ScanMode.QUICK:
# 快速扫描:各阶段使用不同的 Provider
providers = {
'subdomain_discovery': ScanInputTargetProvider(
scan_id=scan_id,
context=provider_context
),
'port_scan': SnapshotTargetProvider(
scan_id=scan_id,
snapshot_type='subdomain',
context=provider_context
),
'site_scan': SnapshotTargetProvider(
scan_id=scan_id,
snapshot_type='host_port',
context=provider_context
),
'url_fetch': SnapshotTargetProvider(
scan_id=scan_id,
snapshot_type='website',
context=provider_context
),
'directory_scan': SnapshotTargetProvider(
scan_id=scan_id,
snapshot_type='website',
context=provider_context
),
}
logger.info(f"✓ 快速扫描模式 - 创建各阶段 Provider")
else:
# 完整扫描:所有阶段使用 DatabaseTargetProvider
db_provider = DatabaseTargetProvider(target_id=target_id, context=provider_context)
providers = {
'subdomain_discovery': db_provider,
'port_scan': db_provider,
'site_scan': db_provider,
'url_fetch': db_provider,
'directory_scan': db_provider,
}
logger.info(f"✓ 完整扫描模式 - 使用 DatabaseTargetProvider")
# 调用子 Flow 时传入对应的 provider
# flow_kwargs['provider'] = providers[scan_type]
# ... 后续代码 ...
```
### 3.5 子 Flow 接口
子 Flow 只接收 provider 参数,直接使用,不关心 scan_mode
```python
# 示例port_scan_flow.py
@flow(name='port_scan')
def port_scan_flow(
scan_id: int,
target_name: str,
target_id: int,
scan_workspace_dir: str,
provider: TargetProvider, # 必需参数,由 initiate_scan_flow 传入
enabled_tools: dict = None,
) -> dict:
"""
端口扫描 Flow
直接使用传入的 provider 获取扫描目标,不关心 scan_mode。
"""
# 使用 provider 获取主机列表
for host in provider.iter_hosts():
# 扫描逻辑...
pass
```
### 3.6 快照写入逻辑
各 Flow 在保存数据时,需要同时写入快照表。已有的快照服务提供了 `save_and_sync` 方法:
```python
# 示例subdomain_discovery_flow.py 中的保存逻辑
from apps.asset.services.snapshot import SubdomainSnapshotsService
from apps.asset.dtos import SubdomainSnapshotDTO
# 构建快照 DTO
snapshot_dtos = [
SubdomainSnapshotDTO(
scan_id=scan_id,
target_id=target_id,
name=subdomain_name
)
for subdomain_name in discovered_subdomains
]
# 保存到快照表并同步到资产表
snapshot_service = SubdomainSnapshotsService()
snapshot_service.save_and_sync(snapshot_dtos)
```
### 3.7 API 层改造
```python
# backend/apps/scan/views/scan_views.py
class QuickScanView(APIView):
"""快速扫描 API"""
def post(self, request):
"""
发起快速扫描
请求体:
{
"inputs": ["a.com", "b.com", "192.168.1.1", ...], # 支持大量输入
"engineId": 1,
"configuration": "..."
}
设计原则:每个 Target 创建一个 Scan
- a.com 和 b.com 是不同的根域名,应该是不同的 Target
- 每个 Target 有独立的 Scan 记录,语义清晰
- 可以独立查看每个 Target 的扫描结果
"""
inputs = request.data.get('inputs', [])
engine_id = request.data.get('engine_id')
configuration = request.data.get('configuration', '')
# 1. 解析输入,创建 Target 和资产
quick_scan_service = QuickScanService()
result = quick_scan_service.process_quick_scan(
inputs=inputs,
engine_id=engine_id,
create_scan=True,
yaml_configuration=configuration
)
# 2. 返回结果(包含多个 Scan
return Response({
'count': len(result['scans']),
'scans': [ScanSerializer(s).data for s in result['scans']],
'target_stats': result['target_stats'],
'asset_stats': result['asset_stats'],
'errors': result['errors']
})
```
## 数据模型
### 4.1 ScanInputTarget 表(新增)
| 字段 | 类型 | 说明 |
|------|------|------|
| id | AutoField | 主键 |
| scan_id | ForeignKey | 关联的 Scan ID |
| value | CharField(2000) | 用户输入的原始值 |
| input_type | CharField(10) | 输入类型domain/ip/cidr/url |
| created_at | DateTimeField | 创建时间 |
索引:`scan_id`, `input_type`
### 4.2 Scan 模型新增字段
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| scan_mode | CharField(10) | 'full' | 扫描模式full/quick |
注意:用户输入存储在 `ScanInputTarget` 表,通过 `scan.input_targets` 关联访问。
### 4.2 快照表(已存在)
| 快照表 | 用途 | 关键字段 |
|--------|------|----------|
| SubdomainSnapshot | 子域名快照 | scan_id, name |
| HostPortMappingSnapshot | 主机端口映射快照 | scan_id, host, ip, port |
| WebsiteSnapshot | 网站快照 | scan_id, url, host |
| DirectorySnapshot | 目录快照 | scan_id, url |
| EndpointSnapshot | 端点快照 | scan_id, url, host |
| VulnerabilitySnapshot | 漏洞快照 | scan_id, url, vuln_type |
### 4.3 阶段间数据传递映射
| 阶段 | 输入 Provider | 输入 snapshot_type | 输出快照表 |
|------|---------------|-------------------|------------|
| 子域名发现 | ListTargetProvider | - | SubdomainSnapshot |
| 端口扫描 | SnapshotTargetProvider | subdomain | HostPortMappingSnapshot |
| 网站扫描 | SnapshotTargetProvider | host_port | WebsiteSnapshot |
| URL获取 | SnapshotTargetProvider | website | EndpointSnapshot |
| 目录扫描 | SnapshotTargetProvider | website | DirectorySnapshot |
| 漏洞扫描 | SnapshotTargetProvider | endpoint | VulnerabilitySnapshot |
## 正确性属性
### Property 1: 扫描模式隔离
*For any* 快速扫描任务,系统只扫描用户输入的目标及其发现的子资产,不扫描 Target 下的历史资产。
**验证方法**
- 创建 Target添加历史子域名
- 发起快速扫描,指定新的子域名
- 验证扫描结果只包含新子域名及其发现的资产
**Validates: Requirements 1, 6**
### Property 2: 快照数据完整性
*For any* 扫描任务,每个阶段发现的资产都会写入对应的快照表,且 scan_id 正确关联。
**验证方法**
- 发起扫描任务
- 验证各快照表中的记录都有正确的 scan_id
- 验证快照数量与扫描结果一致
**Validates: Requirements 2, 3, 7**
### Property 3: Provider 选择正确性
*For any* 扫描任务:
- scan_mode='full' 时,所有阶段使用 DatabaseTargetProvider
- scan_mode='quick' 时阶段1 使用 ListTargetProvider阶段2+ 使用 SnapshotTargetProvider
**验证方法**
- 分别发起完整扫描和快速扫描
- 验证各阶段使用的 Provider 类型
**Validates: Requirements 5, 6**
### Property 4: 向后兼容性
*For any* 现有 API 调用(不指定 scan_mode系统默认使用完整扫描模式行为与改造前一致。
**验证方法**
- 使用现有 API 发起扫描
- 验证 scan_mode 默认为 'full'
- 验证扫描行为与改造前一致
**Validates: Requirements 9**
### Property 5: 快照级联删除
*For any* Scan 记录被删除时,关联的所有快照数据都会被级联删除。
**验证方法**
- 创建扫描任务,生成快照数据
- 删除 Scan 记录
- 验证所有关联的快照表记录都被删除
**Validates: Requirements 10**
## 错误处理
### 6.1 输入验证错误
| 错误场景 | 处理方式 |
|----------|----------|
| user_input_targets 为空 | 返回 400 错误,提示"请输入扫描目标" |
| 输入格式无效 | 返回解析错误列表,继续处理有效输入 |
| 所有输入都无效 | 返回 400 错误,提示"没有有效的扫描目标" |
### 6.2 扫描执行错误
| 错误场景 | 处理方式 |
|----------|----------|
| 快照表为空 | SnapshotTargetProvider 返回空迭代器,阶段跳过 |
| 快照服务异常 | 记录错误日志,继续执行后续阶段 |
| Provider 创建失败 | 回退到 DatabaseTargetProvider |
## 测试策略
### 7.1 单元测试
| 测试目标 | 测试内容 |
|----------|----------|
| Scan 模型 | scan_mode 字段默认值、user_input_targets 序列化 |
| Provider 选择逻辑 | 根据 scan_mode 返回正确的 Provider 类型 |
| 快照写入 | 各 Flow 正确写入对应的快照表 |
### 7.2 集成测试
| 测试场景 | 验证内容 |
|----------|----------|
| 完整扫描流程 | 使用 DatabaseTargetProvider扫描所有资产 |
| 快速扫描流程 | 使用 ListTargetProvider + SnapshotTargetProvider只扫描指定目标 |
| 混合场景 | 同一 Target 下同时存在完整扫描和快速扫描 |
### 7.3 测试文件结构
```
backend/apps/scan/tests/
├── test_scan_model.py # Scan 模型测试
├── test_initiate_scan_flow.py # Provider 选择逻辑测试
├── flows/
│ ├── test_subdomain_discovery_flow.py # 快照写入测试
│ ├── test_port_scan_flow.py
│ └── ...
└── integration/
├── test_full_scan.py # 完整扫描集成测试
└── test_quick_scan.py # 快速扫描集成测试
```

View File

@@ -1,152 +0,0 @@
# 需求文档
## 简介
本文档定义了快速扫描模式的实现需求。快速扫描模式允许用户只扫描指定的目标(而非整个 Target 下的所有资产),通过快照表实现阶段间的精确数据传递。
## 术语表
- **快速扫描Quick Scan**: 只扫描用户指定的目标,不扫描 Target 下的历史资产
- **完整扫描Full Scan**: 扫描 Target 下的所有资产(现有行为)
- **扫描模式Scan Mode**: 区分快速扫描和完整扫描的标识
- **快照表Snapshot Table**: 存储单次扫描发现的资产,用于阶段间数据传递(已存在)
- **SubdomainSnapshot**: 子域名快照表(已存在)
- **HostPortMappingSnapshot**: 主机端口映射快照表(已存在)
- **WebsiteSnapshot**: 网站快照表(已存在)
- **DirectorySnapshot**: 目录快照表(已存在)
- **EndpointSnapshot**: 端点快照表(已存在)
- **VulnerabilitySnapshot**: 漏洞快照表(已存在)
- **SnapshotTargetProvider**: 从快照表读取数据的 Provider已存在需完善
- **ListTargetProvider**: 从内存列表读取数据的 Provider已存在
- **DatabaseTargetProvider**: 从数据库查询数据的 Provider已存在
- **scan_id**: 扫描任务唯一标识
- **target_id**: 目标唯一标识
- **用户输入目标User Input Targets**: 用户在快速扫描时指定的目标列表
## 需求
### 需求 1: 扫描模式标识
**用户故事:** 作为系统,我希望能够区分快速扫描和完整扫描模式,以便根据不同模式选择正确的数据源。
#### 验收标准
1. THE Scan 模型 SHALL 包含 scan_mode 字段,支持 'full' 和 'quick' 两种值
2. WHEN 创建扫描任务时, THE 系统 SHALL 根据请求参数设置 scan_mode
3. THE scan_mode 字段 SHALL 默认为 'full'(向后兼容)
4. WHEN scan_mode 为 'quick' 时, THE Scan 模型 SHALL 存储用户输入的目标列表
### 需求 2: 快照表数据模型(已存在)
**用户故事:** 作为系统,我希望有快照表来存储单次扫描发现的资产,以便在快速扫描模式下实现阶段间的精确数据传递。
#### 验收标准
1. THE SubdomainSnapshot 表 SHALL 包含 scan_id、name 字段(已存在)
2. THE HostPortMappingSnapshot 表 SHALL 包含 scan_id、host、ip、port 字段(已存在)
3. THE WebsiteSnapshot 表 SHALL 包含 scan_id、url、host 字段(已存在)
4. THE DirectorySnapshot 表 SHALL 包含 scan_id、url 字段(已存在)
5. THE EndpointSnapshot 表 SHALL 包含 scan_id、url、host 字段(已存在)
6. THE VulnerabilitySnapshot 表 SHALL 包含 scan_id、url、vuln_type、severity 字段(已存在)
7. THE 快照表 SHALL 通过 scan_id 建立索引以支持高效查询(已存在)
8. THE 快照表 SHALL 支持级联删除(删除 Scan 时自动删除关联快照)(已存在)
### 需求 3: 快照保存服务
**用户故事:** 作为开发者,我希望有统一的服务来保存快照数据,以便在各个 Flow 中复用。
#### 验收标准
1. WHEN 子域名发现完成时, THE 系统 SHALL 同时保存到 Subdomain 表和 SubdomainSnapshot 表
2. WHEN 端口扫描完成时, THE 系统 SHALL 同时保存到 HostPortMapping 表和 HostPortMappingSnapshot 表
3. WHEN 站点扫描完成时, THE 系统 SHALL 同时保存到 Website 表和 WebsiteSnapshot 表
4. WHEN URL 获取完成时, THE 系统 SHALL 同时保存到 Endpoint 表和 EndpointSnapshot 表
5. THE 快照保存 SHALL 使用批量插入以优化性能
6. IF target_id 为 None, THEN THE 系统 SHALL 跳过保存到主表(只保存快照)
### 需求 4: 快照查询服务
**用户故事:** 作为开发者,我希望有统一的服务来查询快照数据,以便 SnapshotTargetProvider 使用。
#### 验收标准
1. THE SubdomainSnapshotsService SHALL 提供 iter_subdomain_names_by_scan(scan_id) 方法
2. THE HostPortMappingSnapshotsService SHALL 提供 iter_by_scan(scan_id) 方法
3. THE WebsiteSnapshotsService SHALL 提供 iter_by_scan(scan_id) 方法
4. THE EndpointSnapshotsService SHALL 提供 iter_by_scan(scan_id) 方法
5. THE 查询方法 SHALL 支持分块迭代以优化内存使用
6. THE 查询方法 SHALL 返回迭代器而非列表
### 需求 5: SnapshotTargetProvider 完善
**用户故事:** 作为开发者,我希望 SnapshotTargetProvider 能够从快照表读取数据,以便在快速扫描的后续阶段使用。
#### 验收标准
1. WHEN snapshot_type 为 'subdomain' 时, THE SnapshotTargetProvider SHALL 从 SubdomainSnapshot 表读取主机列表
2. WHEN snapshot_type 为 'host_port' 时, THE SnapshotTargetProvider SHALL 从 HostPortMappingSnapshot 表读取主机端口列表
3. WHEN snapshot_type 为 'website' 时, THE SnapshotTargetProvider SHALL 从 WebsiteSnapshot 表读取 URL 列表
4. WHEN snapshot_type 为 'endpoint' 时, THE SnapshotTargetProvider SHALL 从 EndpointSnapshot 表读取 URL 列表
5. THE SnapshotTargetProvider SHALL 不应用黑名单过滤(数据已在上一阶段过滤)
6. WHEN 快照表为空时, THE SnapshotTargetProvider SHALL 返回空迭代器
### 需求 6: initiate_scan_flow 改造
**用户故事:** 作为系统,我希望 initiate_scan_flow 能够根据扫描模式选择正确的 Provider以便实现精确扫描。
#### 验收标准
1. WHEN scan_mode 为 'quick' 时, THE initiate_scan_flow SHALL 为第一个阶段创建 ListTargetProvider
2. WHEN scan_mode 为 'quick' 时, THE initiate_scan_flow SHALL 为后续阶段创建 SnapshotTargetProvider
3. WHEN scan_mode 为 'full' 时, THE initiate_scan_flow SHALL 为所有阶段创建 DatabaseTargetProvider
4. THE initiate_scan_flow SHALL 将 Provider 传递给所有子 Flow
5. THE initiate_scan_flow SHALL 从 Scan 模型读取用户输入的目标列表
### 需求 7: 各 Flow 保存逻辑改造
**用户故事:** 作为系统,我希望各个 Flow 在保存数据时同时写入快照表,以便支持快速扫描模式。
#### 验收标准
1. WHEN subdomain_discovery_flow 保存子域名时, THE 系统 SHALL 同时写入 SubdomainSnapshot
2. WHEN port_scan_flow 保存端口映射时, THE 系统 SHALL 同时写入 HostPortMappingSnapshot
3. WHEN site_scan_flow 保存网站时, THE 系统 SHALL 同时写入 WebsiteSnapshot
4. WHEN url_fetch_flow 保存端点时, THE 系统 SHALL 同时写入 EndpointSnapshot
5. THE 快照写入 SHALL 使用 scan_id 关联
6. THE 快照写入 SHALL 不影响现有的主表保存逻辑
### 需求 8: API 层改造
**用户故事:** 作为用户,我希望通过 API 发起快速扫描,以便只扫描我指定的目标。
#### 验收标准
1. THE /api/scans/quick/ 接口 SHALL 接收用户输入的目标列表
2. THE /api/scans/quick/ 接口 SHALL 将目标列表存储到 Scan 模型
3. THE /api/scans/quick/ 接口 SHALL 设置 scan_mode 为 'quick'
4. THE /api/scans/initiate/ 接口 SHALL 保持 scan_mode 为 'full'(向后兼容)
5. WHEN 快速扫描完成时, THE 系统 SHALL 返回扫描结果摘要
### 需求 9: 向后兼容性
**用户故事:** 作为系统维护者,我希望快速扫描改造不影响现有的完整扫描功能。
#### 验收标准
1. WHEN scan_mode 未指定时, THE 系统 SHALL 默认使用完整扫描模式
2. THE 现有 API 接口 SHALL 保持向后兼容
3. THE 现有 Flow 和 Task SHALL 在完整扫描模式下行为不变
4. THE 快照表写入 SHALL 对完整扫描模式透明(不影响性能)
5. THE 定时扫描 SHALL 继续使用完整扫描模式
### 需求 10: 快照数据清理
**用户故事:** 作为系统管理员,我希望能够清理过期的快照数据,以便节省存储空间。
#### 验收标准
1. WHEN Scan 记录被删除时, THE 系统 SHALL 级联删除关联的快照数据
2. THE 系统 SHALL 提供清理指定时间之前快照数据的方法
3. THE 快照清理 SHALL 不影响主表数据
4. THE 快照清理 SHALL 支持批量删除以优化性能

View File

@@ -1,441 +0,0 @@
# 实现任务
## 任务概览
本文档定义了快速扫描模式的实现任务。任务按依赖关系排序,每个任务都是独立可测试的。
## 任务列表
- [x] 1. ScanInputTarget 模型
**描述**: 新建 ScanInputTarget 模型,存储快速扫描时用户输入的目标
**相关需求**: 需求 1
**文件**:
- `backend/apps/scan/models/scan_input_target.py`(新建)
- `backend/apps/scan/models/__init__.py`(更新导出)
**实现步骤**:
1. 创建 ScanInputTarget 模型,包含 scan_id, value, input_type, created_at 字段
2. 添加 InputType 枚举domain/ip/cidr/url
3. 添加索引scan_id, input_type
4. 创建数据库迁移文件
5. 运行迁移
**验收标准**:
- [x] 模型可正常创建记录
- [x] 支持通过 scan.input_targets 反向查询
- [x] 迁移文件可正常执行
---
### Task 2: Scan 模型扩展
**描述**: 为 Scan 模型添加 scan_mode 字段
**相关需求**: 需求 1
**文件**:
- `backend/apps/scan/models/scan.py`
**实现步骤**:
1. 添加 ScanMode 枚举类FULL, QUICK
2. 添加 scan_mode 字段CharField默认 'full'
3. 创建数据库迁移文件
4. 运行迁移
**验收标准**:
- [x] scan_mode 字段默认值为 'full'
- [x] 迁移文件可正常执行
---
### Task 3: ScanInputTargetService
**描述**: 新建 ScanInputTargetService提供批量创建和查询功能
**相关需求**: 需求 1, 8
**依赖**: Task 1
**文件**:
- `backend/apps/scan/services/scan_input_target_service.py`(新建)
**实现步骤**:
1. 实现 bulk_create(scan_id, inputs) 方法,解析输入类型并批量写入
2. 实现 iter_by_scan(scan_id) 方法,分块迭代查询
3. 实现 iter_hosts_by_scan(scan_id) 方法,只返回 domain/ip/cidr 类型
4. 实现 iter_urls_by_scan(scan_id) 方法,只返回 url 类型
**验收标准**:
- [x] bulk_create 可批量写入 1 万条记录
- [x] iter_by_scan 支持分块迭代
- [x] 输入类型自动识别正确
---
### Task 4: ScanInputTargetProvider
**描述**: 新建 ScanInputTargetProvider从 ScanInputTarget 表读取数据
**相关需求**: 需求 5, 6
**依赖**: Task 1, Task 3
**文件**:
- `backend/apps/scan/providers/scan_input_provider.py`(新建)
- `backend/apps/scan/providers/__init__.py`(更新导出)
**实现步骤**:
1. 继承 TargetProvider 基类
2. 实现 _iter_raw_hosts() 方法,查询 domain/ip/cidr 类型
3. 实现 iter_urls() 方法,查询 url 类型
4. 实现 get_blacklist_filter() 返回 None
5. 添加单元测试
**验收标准**:
- [x] iter_hosts() 返回 domain/ip/cidr 类型的输入
- [x] iter_urls() 返回 url 类型的输入
- [x] 支持分块迭代chunk_size=1000
---
### Task 5: initiate_scan_flow Provider 选择逻辑
**描述**: 修改 initiate_scan_flow根据 scan_mode 统一创建所有阶段的 Provider
**相关需求**: 需求 6
**依赖**: Task 2, Task 4
**文件**:
- `backend/apps/scan/flows/initiate_scan_flow.py`
**实现步骤**:
1. 从 Scan 模型读取 scan_mode
2. 根据 scan_mode 创建 providers 字典,包含所有阶段的 Provider
3. 调用子 Flow 时传入对应的 provider
4. 移除 scan_mode 参数传递(子 Flow 不需要)
**验收标准**:
- [x] 快速扫描时各阶段使用正确的 ProviderScanInputTargetProvider/SnapshotTargetProvider
- [x] 完整扫描时所有阶段使用 DatabaseTargetProvider
- [x] 子 Flow 不再接收 scan_mode 参数
---
### Task 6: subdomain_discovery_flow 快照写入
**描述**: 修改子域名发现 Flow在保存数据时同时写入 SubdomainSnapshot
**相关需求**: 需求 3, 7
**依赖**: Task 5
**文件**:
- `backend/apps/scan/flows/subdomain_discovery_flow.py`
- `backend/apps/scan/tasks/subdomain_discovery/` 相关任务
**实现步骤**:
1. 检查现有保存逻辑,确认是否已使用 SubdomainSnapshotsService.save_and_sync()
2. 如果未使用,修改为使用 save_and_sync() 方法
3. 确保 scan_id 正确传递到 DTO
**验收标准**:
- [x] 子域名发现结果同时保存到 Subdomain 表和 SubdomainSnapshot 表
- [x] SubdomainSnapshot 记录包含正确的 scan_id
**备注**: Flow 签名已添加 provider 参数,快照写入逻辑已在现有 save_domains_task 中实现
---
### Task 7: port_scan_flow 快照读取和写入
**描述**: 修改端口扫描 Flow使用传入的 Provider并写入 HostPortMappingSnapshot
**相关需求**: 需求 3, 5, 7
**依赖**: Task 6
**文件**:
- `backend/apps/scan/flows/port_scan_flow.py`
- `backend/apps/scan/tasks/port_scan/` 相关任务
**实现步骤**:
1. 修改 Flow 签名provider 改为必需参数
2. 移除 scan_mode 参数和相关判断逻辑
3. 直接使用传入的 provider 获取扫描目标
4. 检查现有保存逻辑,确认是否已写入 HostPortMappingSnapshot
5. 如果未写入,添加快照写入逻辑
**验收标准**:
- [x] Flow 直接使用传入的 provider
- [x] 端口扫描结果同时保存到 HostPortMapping 表和 HostPortMappingSnapshot 表
**备注**: Flow 签名已添加 provider 参数export_hosts_task 已支持 provider 模式
---
### Task 8: site_scan_flow 快照读取和写入
**描述**: 修改网站扫描 Flow使用传入的 Provider并写入 WebsiteSnapshot
**相关需求**: 需求 3, 5, 7
**依赖**: Task 7
**文件**:
- `backend/apps/scan/flows/site_scan_flow.py`
- `backend/apps/scan/tasks/site_scan/` 相关任务
**实现步骤**:
1. 修改 Flow 签名provider 改为必需参数
2. 移除 scan_mode 参数和相关判断逻辑
3. 直接使用传入的 provider 获取扫描目标
4. 检查现有保存逻辑,确认是否已写入 WebsiteSnapshot
5. 如果未写入,添加快照写入逻辑
**验收标准**:
- [x] Flow 直接使用传入的 provider
- [x] 网站扫描结果同时保存到 Website 表和 WebsiteSnapshot 表
**备注**: Flow 签名已添加 provider 参数export_site_urls_task 已支持 provider 模式
---
### Task 9: url_fetch_flow 快照读取和写入
**描述**: 修改 URL 获取 Flow使用传入的 Provider并写入 EndpointSnapshot
**相关需求**: 需求 3, 5, 7
**依赖**: Task 8
**文件**:
- `backend/apps/scan/flows/url_fetch/main_flow.py`
- `backend/apps/scan/tasks/url_fetch/` 相关任务
**实现步骤**:
1. 修改 Flow 签名provider 改为必需参数
2. 移除 scan_mode 参数和相关判断逻辑
3. 直接使用传入的 provider 获取扫描目标
4. 检查现有保存逻辑,确认是否已写入 EndpointSnapshot
5. 如果未写入,添加快照写入逻辑
**验收标准**:
- [x] Flow 直接使用传入的 provider
- [x] URL 获取结果同时保存到 Endpoint 表和 EndpointSnapshot 表
**备注**: Flow 签名已添加 provider 参数(暂未使用,预留接口)
---
### Task 10: directory_scan_flow 快照读取和写入
**描述**: 修改目录扫描 Flow使用传入的 Provider并写入 DirectorySnapshot
**相关需求**: 需求 3, 5, 7
**依赖**: Task 8
**文件**:
- `backend/apps/scan/flows/directory_scan/main_flow.py`
- `backend/apps/scan/tasks/directory_scan/` 相关任务
**实现步骤**:
1. 修改 Flow 签名provider 改为必需参数
2. 移除 scan_mode 参数和相关判断逻辑
3. 直接使用传入的 provider 获取扫描目标
4. 检查现有保存逻辑,确认是否已写入 DirectorySnapshot
5. 如果未写入,添加快照写入逻辑
**验收标准**:
- [x] Flow 直接使用传入的 provider
- [x] 目录扫描结果同时保存到 Directory 表和 DirectorySnapshot 表
**备注**: Flow 签名已添加 provider 参数(暂未使用,预留接口)
---
### Task 11: QuickScanService 改造
**描述**: 修改 QuickScanService支持创建快速扫描模式的 Scan 记录并写入 ScanInputTarget
**相关需求**: 需求 8
**依赖**: Task 1, Task 2, Task 3
**文件**:
- `backend/apps/scan/services/quick_scan_service.py`
- `backend/apps/scan/services/scan_creation_service.py`
- `backend/apps/scan/services/scan_service.py`
**实现步骤**:
1. 修改 ScanCreationService.create_scans 方法,添加 scan_mode 参数
2. 修改 ScanService.create_scans 方法,传递 scan_mode 参数
3. 在 API 层调用时设置 scan_mode='quick'
4. 调用 ScanInputTargetService.bulk_create() 写入用户输入
**验收标准**:
- [x] 快速扫描创建的 Scan 记录 scan_mode 为 'quick'
- [x] 用户输入正确写入 ScanInputTarget 表
---
### Task 12: API 层改造
**描述**: 修改快速扫描 API支持新的扫描模式
**相关需求**: 需求 8
**依赖**: Task 11
**文件**:
- `backend/apps/scan/views/scan_views.py`
- `backend/apps/scan/serializers/` 相关序列化器
**实现步骤**:
1. 确认 /api/scans/quick/ 接口正确调用 QuickScanService
2. 确保响应包含 scan_mode 信息
3. 添加输入验证(至少一个有效目标)
**验收标准**:
- [x] /api/scans/quick/ 接口创建的 Scan 记录 scan_mode 为 'quick'
- [x] 响应包含 scan_mode 字段
- [x] 空输入返回 400 错误
---
### Task 13: 向后兼容性验证
**描述**: 验证现有 API 和定时扫描的向后兼容性
**相关需求**: 需求 9
**依赖**: Task 5
**文件**:
- `backend/apps/scan/views/scan_views.py`
- `backend/apps/scan/services/scheduled_scan_service.py`
**实现步骤**:
1. 验证 /api/scans/initiate/ 接口默认使用 scan_mode='full'
2. 验证定时扫描默认使用 scan_mode='full'
3. 验证完整扫描行为与改造前一致
**验收标准**:
- [x] /api/scans/initiate/ 创建的 Scan 记录 scan_mode 为 'full'
- [x] 定时扫描创建的 Scan 记录 scan_mode 为 'full'
- [x] 完整扫描使用 DatabaseTargetProvider
**备注**: initiate 接口和定时扫描都调用 create_scans() 时不传 scan_mode默认使用 'full'
---
### Task 14: 删除未使用的 Provider
**描述**: 删除不再使用的 ListTargetProvider 和 PipelineTargetProvider
**相关需求**: 代码清理
**依赖**: Task 4
**文件**:
- `backend/apps/scan/providers/list_provider.py`(删除)
- `backend/apps/scan/providers/pipeline_provider.py`(删除)
- `backend/apps/scan/providers/__init__.py`(移除导出)
- `backend/apps/scan/providers/tests/`(删除相关测试)
**实现步骤**:
1. 确认没有其他代码引用 ListTargetProvider 和 PipelineTargetProvider
2. 删除 list_provider.py 和 pipeline_provider.py 文件
3. 删除相关测试文件
4. 更新 __init__.py 移除导出
**验收标准**:
- [ ] ListTargetProvider 和 PipelineTargetProvider 相关文件已删除
- [ ] 无代码引用这两个 Provider
- [ ] 测试通过
**备注**: 暂时保留,因为测试文件中有引用。可在后续清理迭代中删除。
---
### Task 15: 集成测试
**描述**: 编写快速扫描模式的集成测试
**相关需求**: 所有需求
**依赖**: Task 1-14
**文件**:
- `backend/apps/scan/tests/integration/test_quick_scan.py`
**实现步骤**:
1. 编写快速扫描端到端测试
2. 验证各阶段使用正确的 Provider
3. 验证快照数据正确写入
4. 验证扫描结果只包含指定目标
**验收标准**:
- [ ] 快速扫描只扫描用户指定的目标
- [ ] 各阶段快照数据正确写入
- [ ] 不扫描 Target 下的历史资产
**备注**: 集成测试需要在实际环境中验证,暂不实现
---
## 任务依赖图
```
Task 1 (ScanInputTarget 模型) ──┬──→ Task 3 (ScanInputTargetService)
│ │
Task 2 (Scan 模型扩展) ─────────┼───────────┼──→ Task 4 (ScanInputTargetProvider)
│ │ │
│ │ ↓
│ └──→ Task 5 (initiate_scan_flow)
│ │
│ ├──→ Task 6 (subdomain_discovery_flow)
│ │ │
│ │ ↓
│ │ Task 7 (port_scan_flow)
│ │ │
│ │ ↓
│ │ Task 8 (site_scan_flow)
│ │ │
│ │ ├──→ Task 9 (url_fetch_flow)
│ │ │
│ │ └──→ Task 10 (directory_scan_flow)
│ │
│ └──→ Task 13 (向后兼容性验证)
└──→ Task 11 (QuickScanService)
└──→ Task 12 (API 层)
Task 1-14 ──→ Task 15 (集成测试)
```
## 估算工时
| 任务 | 估算工时 | 复杂度 |
|------|----------|--------|
| Task 1 | 1h | 低 |
| Task 2 | 0.5h | 低 |
| Task 3 | 1.5h | 中 |
| Task 4 | 1.5h | 中 |
| Task 5 | 2h | 中 |
| Task 6 | 2h | 中 |
| Task 7 | 2h | 中 |
| Task 8 | 2h | 中 |
| Task 9 | 2h | 中 |
| Task 10 | 2h | 中 |
| Task 11 | 1h | 低 |
| Task 12 | 1h | 低 |
| Task 13 | 1h | 低 |
| Task 14 | 0.5h | 低 |
| Task 15 | 3h | 中 |
| **总计** | **23h** | - |

View File

@@ -1,911 +0,0 @@
# 设计文档
## 概述
本设计实现扫描目标提供者的策略模式,将数据源抽象为统一的 `TargetProvider` 接口。这使得扫描任务可以灵活地从不同来源获取目标(数据库、内存列表、快照表、管道输出),同时保持向后兼容。
### 核心价值
1. **解耦扫描范围和结果归属**
- `DatabaseTargetProvider`: target_id 决定扫描什么(查询数据库所有资产)
- `ListTargetProvider`: targets 决定扫描什么target_id 只用于保存结果
- `SnapshotTargetProvider`: scan_id 决定扫描什么(只扫描本次发现的资产)
2. **支持精确扫描**
- 快速扫描:用户输入 `a.test.com`,只扫描 `a.test.com` 及其发现的子资产(不扫描整个 test.com 域)
- 完整扫描:扫描 Target 下的所有资产test.com + 所有子域名)
3. **代码复用和可测试性**
- 查询逻辑封装在 Provider 中,避免重复代码
- 测试时用 ListProvider不需要数据库
### 快照表方案
快照表用于解决快速扫描的精确控制问题:
**问题场景**
```
用户输入: a.test.com
创建 Target: test.com (id=1)
阶段1: 子域名发现
发现: b.a.test.com, c.a.test.com
保存到: Subdomain(target_id=1)
阶段2: 端口扫描
问题: 如何只扫描 b.a.test.com, c.a.test.com
❌ 使用 DatabaseTargetProvider(target_id=1)
→ 会扫描 target_id=1 下的所有子域名(包括历史数据 www.test.com, api.test.com
✅ 使用 SnapshotTargetProvider(scan_id=100, snapshot_type="subdomain")
→ 只扫描本次扫描scan_id=100发现的子域名
```
**快照表流程**
```
阶段1: 子域名发现
输入: ListTargetProvider(targets=["a.test.com"])
输出: b.a.test.com, c.a.test.com
保存: SubdomainSnapshot(scan_id=100) + Subdomain(target_id=1)
阶段2: 端口扫描
输入: SnapshotTargetProvider(scan_id=100, snapshot_type="subdomain")
输出: b.a.test.com, c.a.test.com只读取本次扫描的快照
保存: HostPortMappingSnapshot(scan_id=100) + HostPortMapping(target_id=1)
阶段3: 网站扫描
输入: SnapshotTargetProvider(scan_id=100, snapshot_type="host_port")
输出: 本次扫描发现的 IP:Port
保存: WebsiteSnapshot(scan_id=100) + Website(target_id=1)
阶段4: 端点扫描
输入: SnapshotTargetProvider(scan_id=100, snapshot_type="website")
输出: 本次扫描发现的网站 URL
保存: EndpointSnapshot(scan_id=100) + Endpoint(target_id=1)
```
**快照表优势**
- ✅ 天然隔离(通过 scan_id
- ✅ 内存友好(数据在数据库,按需查询)
- ✅ 可追溯历史扫描
- ✅ 支持扫描重放
- ✅ 易于清理旧数据
### 使用场景
| 场景 | Provider | target_id 用途 | 扫描范围 |
|------|----------|---------------|----------|
| **快速扫描阶段1** | `ListTargetProvider` + context | 保存结果 | 只扫描用户指定的目标 |
| **快速扫描阶段2+** | `SnapshotTargetProvider` | 保存结果 | 只扫描本次扫描发现的资产 |
| **完整扫描** | `DatabaseTargetProvider` | 查询数据 + 保存结果 | 扫描 Target 下所有资产 |
| **定时扫描** | `DatabaseTargetProvider` | 查询数据 + 保存结果 | 扫描 Target 下所有资产 |
| **临时测试** | `ListTargetProvider`(无 context | 不保存 | 只扫描指定目标 |
| **管道模式(预留)** | `PipelineTargetProvider` | 保存结果 | 使用上一阶段输出 |
## 架构
### 整体架构图
```
┌─────────────────────────────────────────────────────────────────────┐
│ 调用层 (Tasks/Flows) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ export_hosts_task / port_scan_flow / site_scan_flow ... │ │
│ │ │ │
│ │ # 直接创建具体的 Provider │ │
│ │ provider = SnapshotTargetProvider(scan_id=100, ...) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ TargetProvider (抽象基类) │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Database │ │ List │ │ Snapshot │ │ │
│ │ │ Provider │ │ Provider │ │ Provider │ │ │
│ │ │ (target_id) │ │ (targets) │ │ (scan_id) │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Pipeline │ ← 为 Phase 2 管道模式预留 │ │
│ │ │ Provider │ │ │
│ │ │(StageOutput)│ │ │
│ │ └─────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
### 文件结构
```
backend/apps/scan/
├── providers/ # 新增目录
│ ├── __init__.py # 导出公共接口
│ ├── base.py # TargetProvider 抽象基类 + ProviderContext
│ ├── database_provider.py # DatabaseTargetProvider
│ ├── list_provider.py # ListTargetProvider
│ ├── snapshot_provider.py # SnapshotTargetProvider新增
│ └── pipeline_provider.py # PipelineTargetProvider (预留)
├── services/
│ └── target_export_service.py # 重构:复用 Provider
└── tasks/
├── port_scan/
│ └── export_hosts_task.py # 改造:支持 provider 参数
└── site_scan/
└── export_site_urls_task.py # 改造:支持 provider 参数
```
## 组件和接口
### 3.1 ProviderContext 数据类
```python
# backend/apps/scan/providers/base.py
from dataclasses import dataclass
from typing import Optional
@dataclass
class ProviderContext:
"""
Provider 上下文,携带元数据
Attributes:
target_id: 关联的 Target ID用于结果保存None 表示临时扫描(不保存)
scan_id: 扫描任务 ID
判断是否保存结果:
- target_id 不为 None保存到数据库
- target_id 为 None临时扫描不保存
"""
target_id: Optional[int] = None
scan_id: Optional[int] = None
```
### 3.2 TargetProvider 抽象基类
```python
# backend/apps/scan/providers/base.py
from abc import ABC, abstractmethod
from typing import Iterator, Optional, TYPE_CHECKING
import ipaddress
if TYPE_CHECKING:
from apps.common.utils import BlacklistFilter
class TargetProvider(ABC):
"""
扫描目标提供者抽象基类
职责:
- 提供扫描目标域名、IP、URL 等)的迭代器
- 提供黑名单过滤器
- 携带上下文信息target_id, scan_id 等)
- 提供 CIDR 展开的通用逻辑
使用方式:
provider = create_target_provider(target_id=123)
for host in provider.iter_hosts():
print(host)
"""
def __init__(self, context: Optional[ProviderContext] = None):
"""
初始化 Provider
Args:
context: Provider 上下文None 时创建默认上下文
"""
self.context = context or ProviderContext()
@staticmethod
def _expand_host(host: str) -> Iterator[str]:
"""
展开主机(如果是 CIDR 则展开为多个 IP否则直接返回
这是一个通用的辅助方法,所有子类都可以使用。
Args:
host: 主机字符串IP/域名/CIDR
Yields:
str: 单个主机IP 或域名)
示例:
"192.168.1.0/30""192.168.1.1", "192.168.1.2"
"192.168.1.1""192.168.1.1"
"example.com""example.com"
"""
# 尝试解析为 CIDR
try:
network = ipaddress.ip_network(host, strict=False)
# 如果是单个 IP/32 或 /128
if network.num_addresses == 1:
yield str(network.network_address)
else:
# 展开 CIDR 为多个主机 IP
for ip in network.hosts():
yield str(ip)
except ValueError:
# 不是有效的 IP/CIDR直接返回可能是域名
yield host
@abstractmethod
def iter_hosts(self) -> Iterator[str]:
"""
迭代主机列表(域名/IP
注意:如果输入包含 CIDR子类应该使用 _expand_host() 展开
Yields:
str: 主机名或 IP 地址(单个,不包含 CIDR
"""
pass
@abstractmethod
def iter_urls(self) -> Iterator[str]:
"""
迭代 URL 列表
Yields:
str: URL 字符串
"""
pass
@abstractmethod
def get_blacklist_filter(self) -> Optional['BlacklistFilter']:
"""
获取黑名单过滤器
Returns:
BlacklistFilter: 黑名单过滤器实例,或 None不过滤
"""
pass
@property
def target_id(self) -> Optional[int]:
"""返回关联的 target_idNone 表示临时扫描(不保存)"""
return self.context.target_id
@property
def scan_id(self) -> Optional[int]:
"""返回关联的 scan_id"""
return self.context.scan_id
```
### 3.3 DatabaseTargetProvider
```python
# backend/apps/scan/providers/database_provider.py
from typing import Iterator, Optional
from .base import TargetProvider, ProviderContext
class DatabaseTargetProvider(TargetProvider):
"""
数据库目标提供者 - 从 Target 表及关联资产表查询
这是现有行为的封装,保持向后兼容。
数据来源:
- iter_hosts(): 根据 Target 类型返回域名/IP
- DOMAIN: 根域名 + Subdomain 表
- IP: 直接返回 IP
- CIDR: 使用 _expand_host() 展开为所有主机 IP
- iter_urls(): WebSite/Endpoint 表,带回退链
使用方式:
provider = DatabaseTargetProvider(target_id=123)
for host in provider.iter_hosts():
scan(host)
"""
def __init__(self, target_id: int, context: Optional[ProviderContext] = None):
"""
初始化数据库目标提供者
Args:
target_id: 目标 ID必需
context: Provider 上下文
"""
ctx = context or ProviderContext()
ctx.target_id = target_id
super().__init__(ctx)
self._target_id = target_id
self._blacklist_filter = None # 延迟加载
def iter_hosts(self) -> Iterator[str]:
"""
从数据库查询主机列表
根据 Target 类型决定数据来源:
- DOMAIN: 根域名 + Subdomain 表
- IP: 直接返回 target.name
- CIDR: 使用 _expand_host() 展开 CIDR 范围
"""
from apps.targets.services import TargetService
from apps.targets.models import Target
from apps.asset.services.asset.subdomain_service import SubdomainService
target = TargetService().get_target(self._target_id)
if not target:
return
blacklist = self.get_blacklist_filter()
if target.type == Target.TargetType.DOMAIN:
# 先返回根域名
if not blacklist or blacklist.is_allowed(target.name):
yield target.name
# 再返回子域名
subdomain_service = SubdomainService()
for domain in subdomain_service.iter_subdomain_names_by_target(
target_id=self._target_id,
chunk_size=1000
):
if domain != target.name: # 避免重复
if not blacklist or blacklist.is_allowed(domain):
yield domain
elif target.type == Target.TargetType.IP:
if not blacklist or blacklist.is_allowed(target.name):
yield target.name
elif target.type == Target.TargetType.CIDR:
# 使用基类的 _expand_host() 展开 CIDR
for ip_str in self._expand_host(target.name):
if not blacklist or blacklist.is_allowed(ip_str):
yield ip_str
def iter_urls(self) -> Iterator[str]:
"""
从数据库查询 URL 列表
使用现有的回退链逻辑Endpoint → WebSite → Default
"""
from apps.scan.services.target_export_service import (
_iter_urls_with_fallback, DataSource
)
blacklist = self.get_blacklist_filter()
for url, source in _iter_urls_with_fallback(
target_id=self._target_id,
sources=[DataSource.ENDPOINT, DataSource.WEBSITE, DataSource.DEFAULT],
blacklist_filter=blacklist
):
yield url
def get_blacklist_filter(self):
"""获取黑名单过滤器(延迟加载)"""
if self._blacklist_filter is None:
from apps.common.services import BlacklistService
from apps.common.utils import BlacklistFilter
rules = BlacklistService().get_rules(self._target_id)
self._blacklist_filter = BlacklistFilter(rules)
return self._blacklist_filter
```
### 3.4 ListTargetProvider
```python
# backend/apps/scan/providers/list_provider.py
from typing import Iterator, Optional, List
from .base import TargetProvider, ProviderContext
class ListTargetProvider(TargetProvider):
"""
列表目标提供者 - 直接使用内存中的列表
用于快速扫描、临时扫描等场景,只扫描用户指定的目标。
特点:
- 不查询数据库
- 不应用黑名单过滤(用户明确指定的目标)
- 通过 context 关联 target_id用于保存结果
- 自动检测输入类型URL/域名/IP/CIDR
- 自动展开 CIDR
与 DatabaseTargetProvider 的区别:
- DatabaseTargetProvider: target_id 决定扫描什么(查询数据库)
- ListTargetProvider: targets 决定扫描什么target_id 只用于保存结果
使用方式:
# 场景1: 快速扫描(需要保存结果)
# 用户输入: a.test.com
# 创建 Target: test.com (id=1)
context = ProviderContext(
target_id=1, # 关联到 test.com用于保存结果
scan_id=scan.id
)
provider = ListTargetProvider(
targets=["a.test.com"], # 只扫描用户指定的
context=context # 携带 target_id
)
for host in provider.iter_hosts():
scan(host) # 只扫描 a.test.com
# 保存结果到 target_id=1
# 场景2: 临时测试(不保存结果)
provider = ListTargetProvider(targets=["example.com"])
# target_id=None扫描任务会跳过保存
for host in provider.iter_hosts():
check_reachable(host) # 不保存结果
"""
def __init__(
self,
targets: Optional[List[str]] = None,
context: Optional[ProviderContext] = None
):
"""
初始化列表目标提供者
Args:
targets: 目标列表自动识别类型URL/域名/IP/CIDR
context: Provider 上下文
"""
from apps.common.validators import detect_input_type
ctx = context or ProviderContext()
super().__init__(ctx)
# 自动分类目标
self._hosts = []
self._urls = []
if targets:
for target in targets:
target = target.strip()
if not target:
continue
try:
input_type = detect_input_type(target)
if input_type == 'url':
self._urls.append(target)
else:
# domain/ip/cidr 都作为 host
self._hosts.append(target)
except ValueError:
# 无法识别类型,默认作为 host
self._hosts.append(target)
def _iter_raw_hosts(self) -> Iterator[str]:
"""迭代原始主机列表(可能包含 CIDR"""
yield from self._hosts
def iter_urls(self) -> Iterator[str]:
"""迭代 URL 列表"""
yield from self._urls
def get_blacklist_filter(self):
"""列表模式不使用黑名单过滤"""
return None
```
### 3.5 SnapshotTargetProvider新增
```python
# backend/apps/scan/providers/snapshot_provider.py
from typing import Iterator, Optional, Literal
from .base import TargetProvider, ProviderContext
SnapshotType = Literal["subdomain", "website", "endpoint", "host_port"]
class SnapshotTargetProvider(TargetProvider):
"""
快照目标提供者 - 从快照表读取本次扫描的数据
用于快速扫描的阶段间数据传递,解决精确扫描控制问题。
核心价值:
- 只返回本次扫描scan_id发现的资产
- 避免扫描历史数据DatabaseTargetProvider 会扫描所有历史资产)
特点:
- 通过 scan_id 过滤快照表
- 不应用黑名单过滤(数据已在上一阶段过滤)
- 支持多种快照类型subdomain/website/endpoint/host_port
使用场景:
# 快速扫描流程
用户输入: a.test.com
创建 Target: test.com (id=1)
创建 Scan: scan_id=100
# 阶段1: 子域名发现
provider = ListTargetProvider(
targets=["a.test.com"],
context=ProviderContext(target_id=1, scan_id=100)
)
# 发现: b.a.test.com, c.a.test.com
# 保存: SubdomainSnapshot(scan_id=100) + Subdomain(target_id=1)
# 阶段2: 端口扫描
provider = SnapshotTargetProvider(
scan_id=100,
snapshot_type="subdomain",
context=ProviderContext(target_id=1, scan_id=100)
)
# 只返回: b.a.test.com, c.a.test.com本次扫描发现的
# 不返回: www.test.com, api.test.com历史数据
# 阶段3: 网站扫描
provider = SnapshotTargetProvider(
scan_id=100,
snapshot_type="host_port",
context=ProviderContext(target_id=1, scan_id=100)
)
# 只返回本次扫描发现的 IP:Port
"""
def __init__(
self,
scan_id: int,
snapshot_type: SnapshotType,
context: Optional[ProviderContext] = None
):
"""
初始化快照目标提供者
Args:
scan_id: 扫描任务 ID必需
snapshot_type: 快照类型
- "subdomain": 子域名快照SubdomainSnapshot
- "website": 网站快照WebsiteSnapshot
- "endpoint": 端点快照EndpointSnapshot
- "host_port": 主机端口映射快照HostPortMappingSnapshot
context: Provider 上下文
"""
ctx = context or ProviderContext()
ctx.scan_id = scan_id
super().__init__(ctx)
self._scan_id = scan_id
self._snapshot_type = snapshot_type
def _iter_raw_hosts(self) -> Iterator[str]:
"""
从快照表迭代主机列表
根据 snapshot_type 选择不同的快照表:
- subdomain: SubdomainSnapshot.name
- host_port: HostPortMappingSnapshot.host
"""
if self._snapshot_type == "subdomain":
from apps.asset.services.snapshot import SubdomainSnapshotsService
service = SubdomainSnapshotsService()
yield from service.iter_subdomain_names_by_scan(
scan_id=self._scan_id,
chunk_size=1000
)
elif self._snapshot_type == "host_port":
from apps.asset.services.snapshot import HostPortMappingSnapshotsService
service = HostPortMappingSnapshotsService()
# 返回 host:port 格式(用于网站扫描)
for mapping in service.iter_by_scan(scan_id=self._scan_id, chunk_size=1000):
yield f"{mapping.host}:{mapping.port}"
else:
# 其他类型暂不支持 iter_hosts
return
def iter_urls(self) -> Iterator[str]:
"""
从快照表迭代 URL 列表
根据 snapshot_type 选择不同的快照表:
- website: WebsiteSnapshot.url
- endpoint: EndpointSnapshot.url
"""
if self._snapshot_type == "website":
from apps.asset.services.snapshot import WebsiteSnapshotsService
service = WebsiteSnapshotsService()
for website in service.iter_by_scan(scan_id=self._scan_id, chunk_size=1000):
yield website.url
elif self._snapshot_type == "endpoint":
from apps.asset.services.snapshot import EndpointSnapshotsService
service = EndpointSnapshotsService()
for endpoint in service.iter_by_scan(scan_id=self._scan_id, chunk_size=1000):
yield endpoint.url
else:
# 其他类型暂不支持 iter_urls
return
def get_blacklist_filter(self) -> None:
"""快照数据已在上一阶段过滤过了"""
return None
```
### 3.6 PipelineTargetProvider预留
```python
# backend/apps/scan/providers/pipeline_provider.py
from typing import Iterator, Optional, TYPE_CHECKING
from .base import TargetProvider, ProviderContext
if TYPE_CHECKING:
from apps.scan.pipeline.data import StageOutput
class PipelineTargetProvider(TargetProvider):
"""
管道目标提供者 - 使用上一阶段的输出
用于 Phase 2 管道模式的阶段间数据传递。
特点:
- 不查询数据库
- 不应用黑名单过滤(数据已在上一阶段过滤)
- 直接使用 StageOutput 中的数据
使用方式Phase 2
stage1_output = stage1.run(input)
provider = PipelineTargetProvider(
previous_output=stage1_output,
target_id=123
)
for host in provider.iter_hosts():
stage2.scan(host)
"""
def __init__(
self,
previous_output: 'StageOutput',
target_id: Optional[int] = None,
context: Optional[ProviderContext] = None
):
"""
初始化管道目标提供者
Args:
previous_output: 上一阶段的输出
target_id: 可选,关联到某个 Target用于保存结果
context: Provider 上下文
"""
ctx = context or ProviderContext(target_id=target_id)
super().__init__(ctx)
self._previous_output = previous_output
def _iter_raw_hosts(self) -> Iterator[str]:
"""迭代上一阶段输出的原始主机(可能包含 CIDR"""
yield from self._previous_output.hosts
def iter_urls(self) -> Iterator[str]:
"""迭代上一阶段输出的 URL"""
yield from self._previous_output.urls
def get_blacklist_filter(self) -> None:
"""管道传递的数据已经过滤过了"""
return None
```
### 3.7 模块导出
```python
# backend/apps/scan/providers/__init__.py
"""
扫描目标提供者模块
提供统一的目标获取接口,支持多种数据源:
- DatabaseTargetProvider: 从数据库查询(完整扫描)
- ListTargetProvider: 使用内存列表快速扫描阶段1
- SnapshotTargetProvider: 从快照表读取快速扫描阶段2+
- PipelineTargetProvider: 使用管道输出Phase 2
使用方式:
from apps.scan.providers import (
DatabaseTargetProvider,
ListTargetProvider,
SnapshotTargetProvider,
ProviderContext
)
# 数据库模式(完整扫描)
provider = DatabaseTargetProvider(target_id=123)
# 列表模式快速扫描阶段1
context = ProviderContext(target_id=1, scan_id=100)
provider = ListTargetProvider(
targets=["a.test.com"],
context=context
)
# 快照模式快速扫描阶段2+
context = ProviderContext(target_id=1, scan_id=100)
provider = SnapshotTargetProvider(
scan_id=100,
snapshot_type="subdomain",
context=context
)
# 使用 Provider
for host in provider.iter_hosts():
scan(host)
"""
from .base import TargetProvider, ProviderContext
from .database_provider import DatabaseTargetProvider
from .list_provider import ListTargetProvider
from .snapshot_provider import SnapshotTargetProvider, SnapshotType
from .pipeline_provider import PipelineTargetProvider, StageOutput
__all__ = [
'TargetProvider',
'ProviderContext',
'DatabaseTargetProvider',
'ListTargetProvider',
'SnapshotTargetProvider',
'SnapshotType',
'PipelineTargetProvider',
'StageOutput',
]
```
## 数据模型
### 4.1 ProviderContext
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| target_id | Optional[int] | None | 关联的 Target IDNone 表示临时扫描(不保存) |
| scan_id | Optional[int] | None | 扫描任务 ID |
### 4.2 StageOutputPhase 2 预留)
```python
# backend/apps/scan/pipeline/data.pyPhase 2 实现)
@dataclass
class StageOutput:
"""阶段输出数据"""
hosts: List[str] = field(default_factory=list)
urls: List[str] = field(default_factory=list)
new_targets: List[str] = field(default_factory=list)
stats: Dict[str, Any] = field(default_factory=dict)
success: bool = True
error: Optional[str] = None
```
## 正确性属性
*正确性属性是系统在所有有效执行中应保持为真的特征或行为——本质上是关于系统应该做什么的形式化陈述。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*
### Property 1: ListTargetProvider Round-Trip
*For any* 主机列表和 URL 列表,创建 ListTargetProvider 后迭代 iter_hosts() 和 iter_urls() 应该返回与输入相同的元素(顺序相同)。
**Validates: Requirements 3.1, 3.2**
### Property 2: PipelineTargetProvider Round-Trip
*For any* StageOutput 对象PipelineTargetProvider 的 iter_hosts() 和 iter_urls() 应该返回与 StageOutput 中 hosts 和 urls 列表相同的元素。
**Validates: Requirements 4.1, 4.2**
### Property 3: Factory Provider Type Selection
*For any* 参数组合create_target_provider 工厂函数应该根据优先级规则返回正确类型的 Provider
- previous_output 存在 → PipelineTargetProvider
- targets 存在 → ListTargetProvider
- 仅 target_id 存在 → DatabaseTargetProvider
**Validates: Requirements 5.1, 5.2, 5.3, 5.4**
### Property 4: Context Propagation
*For any* ProviderContext传入 Provider 构造函数后Provider 的 target_id 和 scan_id 属性应该与 context 中的值一致。
**Validates: Requirements 1.3, 1.5, 7.4, 7.5**
### Property 5: Non-Database Provider Blacklist Filter
*For any* ListTargetProvider 或 PipelineTargetProvider 实例get_blacklist_filter() 方法应该返回 None。
**Validates: Requirements 3.4, 9.4, 9.5**
### Property 6: DatabaseTargetProvider Blacklist Application
*For any* 带有黑名单规则的 target_idDatabaseTargetProvider 的 iter_hosts() 和 iter_urls() 应该过滤掉匹配黑名单规则的目标。
**Validates: Requirements 2.3, 9.1, 9.2, 9.3**
### Property 7: CIDR Expansion Consistency
*For any* CIDR 字符串(如 "192.168.1.0/24"),所有 ProviderDatabaseTargetProvider、ListTargetProvider的 iter_hosts() 方法应该将其展开为相同的单个 IP 地址列表。
**Validates: Requirements 1.1, 3.6**
### Property 8: Task Backward Compatibility
*For any* 任务调用,当仅提供 target_id 参数时,任务应该创建 DatabaseTargetProvider 并使用它进行数据访问,行为与改造前一致。
**Validates: Requirements 6.1, 6.2, 6.4, 6.5**
## 错误处理
### 6.1 工厂函数错误
| 错误场景 | 异常类型 | 错误消息 |
|----------|----------|----------|
| 未提供任何有效参数 | ValueError | "必须提供以下参数之一: target_id, targets, previous_output" |
### 6.2 DatabaseTargetProvider 错误
| 错误场景 | 处理方式 |
|----------|----------|
| target_id 不存在 | iter_hosts() 返回空迭代器,不抛出异常 |
| 数据库连接失败 | 抛出 Django 数据库异常 |
### 6.3 Task 错误
| 错误场景 | 异常类型 | 错误消息 |
|----------|----------|----------|
| 既未提供 provider 也未提供 target_id | ValueError | "必须提供 target_id 或 provider" |
## 测试策略
### 7.1 测试框架
- **单元测试框架**: pytest
- **属性测试框架**: hypothesis
- **Mock 框架**: pytest-mock / unittest.mock
### 7.2 测试类型
#### 单元测试
| 测试目标 | 测试内容 |
|----------|----------|
| ProviderContext | 默认值、字段赋值 |
| ListTargetProvider | 空列表、单元素、多元素、类型自动识别 |
| PipelineTargetProvider | 空 StageOutput、正常 StageOutput |
| create_target_provider | 各种参数组合、优先级验证 |
#### 属性测试
| Property | 测试策略 |
|----------|----------|
| Property 1 | 生成随机字符串列表,验证 round-trip |
| Property 2 | 生成随机 StageOutput验证 round-trip |
| Property 3 | 生成随机参数组合,验证返回类型 |
| Property 4 | 生成随机 ProviderContext验证属性传递 |
| Property 5 | 对所有非数据库 Provider 验证返回 None |
| Property 6 | 需要数据库 fixture验证黑名单过滤 |
| Property 7 | 验证 CIDR 展开一致性 |
| Property 8 | 需要 mock验证向后兼容 |
### 7.3 测试配置
```python
# pytest.ini 或 pyproject.toml
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
# hypothesis 配置
[tool.hypothesis]
max_examples = 100
```
### 7.4 测试文件结构
```
backend/tests/scan/providers/
├── __init__.py
├── test_base.py # ProviderContext, TargetProvider 基类测试
├── test_list_provider.py # ListTargetProvider 单元测试 + 属性测试
├── test_pipeline_provider.py # PipelineTargetProvider 单元测试 + 属性测试
├── test_database_provider.py # DatabaseTargetProvider 集成测试
├── test_factory.py # create_target_provider 单元测试 + 属性测试
└── conftest.py # 共享 fixtures
```

View File

@@ -1,135 +0,0 @@
# 需求文档
## 简介
本文档定义了扫描管道架构中目标提供者策略模式的实现需求。目标是抽象扫描目标的数据源,在保持向后兼容的同时支持灵活的输入方式。
## 术语表
- **Target目标**: 需要扫描的域名、IP 地址或 URL
- **Provider提供者**: 从各种来源提供扫描目标的组件
- **TargetProvider目标提供者**: 定义目标提供者接口的抽象基类
- **DatabaseTargetProvider数据库目标提供者**: 从数据库查询目标的提供者
- **ListTargetProvider列表目标提供者**: 使用内存列表的目标提供者
- **FileTargetProvider文件目标提供者**: 从文件读取目标的提供者
- **PipelineTargetProvider管道目标提供者**: 使用上一阶段输出的提供者
- **BlacklistFilter黑名单过滤器**: 过滤黑名单目标的组件
- **ProviderContext提供者上下文**: 提供者配置的元数据容器
- **Scan扫描**: 对一个或多个目标执行的安全扫描操作
- **Task任务**: 执行特定扫描操作的 Prefect 任务
- **Flow流程**: 编排多个任务的 Prefect 流程
## 需求
### 需求 1: 抽象目标提供者接口
**用户故事:** 作为开发者,我希望有一个统一的接口来提供扫描目标,以便在不修改扫描逻辑的情况下轻松切换不同的数据源。
#### 验收标准
1. THE TargetProvider SHALL 定义包含迭代主机和 URL 方法的抽象接口
2. THE TargetProvider SHALL 提供获取黑名单过滤器的方法
3. THE TargetProvider SHALL 携带包括 target_id 和 scan_id 的上下文信息
4. THE TargetProvider SHALL 暴露一个属性来指示是否应将结果保存到数据库
5. WHERE 提供了 ProviderContext, THE TargetProvider SHALL 使用它进行配置
6. THE TargetProvider SHALL 提供通用的 CIDR 展开辅助方法供所有子类使用
### 需求 2: 数据库目标提供者
**用户故事:** 作为系统,我希望保持与现有基于数据库的目标查询的向后兼容性,以便当前的扫描功能无需修改即可继续工作。
#### 验收标准
1. WHEN 提供了 target_id, THE DatabaseTargetProvider SHALL 从 Subdomain 表查询主机
2. WHEN 提供了 target_id, THE DatabaseTargetProvider SHALL 从 WebSite 和 Endpoint 表查询 URL
3. THE DatabaseTargetProvider SHALL 检索并应用指定目标的黑名单规则
4. THE DatabaseTargetProvider SHALL 复用现有的 TargetExportService 逻辑
5. THE DatabaseTargetProvider SHALL 在其上下文中设置 target_id
### 需求 3: 列表目标提供者
**用户故事:** 作为用户,我希望对我提供的特定目标执行快速扫描,以便只扫描我感兴趣的 URL 或主机,而不扫描目标下的所有资产。
#### 验收标准
1. WHEN 以列表形式提供目标, THE ListTargetProvider SHALL 自动识别每个目标的类型URL/域名/IP/CIDR
2. THE ListTargetProvider SHALL 将 URL 类型的目标通过 iter_urls() 返回
3. THE ListTargetProvider SHALL 将非 URL 类型的目标(域名/IP/CIDR通过 iter_hosts() 返回
4. THE ListTargetProvider SHALL 默认不应用黑名单过滤器
5. WHEN 未提供目标, THE ListTargetProvider SHALL 返回空迭代器
6. WHEN 目标列表包含 CIDR, THE ListTargetProvider SHALL 自动展开为单个 IP 地址
7. THE ListTargetProvider SHALL 使用 detect_input_type() 进行类型检测
8. WHEN 提供了 ProviderContext, THE ListTargetProvider SHALL 通过 target_id 属性暴露 context.target_id
9. THE ListTargetProvider SHALL 支持通过 context 关联 target_id用于保存扫描结果
### 需求 4: 管道目标提供者
**用户故事:** 作为开发者,我希望使用上一个管道阶段的输出作为后续阶段的输入,以便在扫描阶段之间实现高效的数据流。
#### 验收标准
1. WHEN 提供了 StageOutput, THE PipelineTargetProvider SHALL 迭代该输出中的主机
2. WHEN 提供了 StageOutput, THE PipelineTargetProvider SHALL 迭代该输出中的 URL
3. THE PipelineTargetProvider SHALL 不应用黑名单过滤器(数据已被过滤)
4. THE PipelineTargetProvider SHALL 可选地将结果与 target_id 关联
5. WHEN StageOutput 为空, THE PipelineTargetProvider SHALL 返回空迭代器
### 需求 5: 提供者工厂
**用户故事:** 作为开发者,我希望有一个工厂函数根据可用参数创建适当的提供者,以便不需要手动确定要实例化哪个提供者。
#### 验收标准
1. WHEN 提供了 previous_output, THE factory SHALL 创建 PipelineTargetProvider
2. WHEN 提供了 targets 列表, THE factory SHALL 创建 ListTargetProvider
3. WHEN 仅提供了 target_id, THE factory SHALL 创建 DatabaseTargetProvider
4. IF 未提供有效参数, THEN THE factory SHALL 抛出 ValueError
5. THE factory SHALL 按照优先级顺序选择 Provider 类型
### 需求 6: 向后兼容的任务重构
**用户故事:** 作为系统维护者,我希望现有任务支持新的提供者模式同时保持向后兼容性,以便现有代码无需修改即可继续工作。
#### 验收标准
1. WHEN 任务接收到 provider 参数, THE task SHALL 使用该提供者进行数据访问
2. WHEN 任务仅接收到 target_id 参数, THE task SHALL 创建 DatabaseTargetProvider
3. IF 既未提供 provider 也未提供 target_id, THEN THE task SHALL 抛出 ValueError
4. THE task SHALL 在过滤目标时使用提供者的黑名单过滤器
5. THE task SHALL 在保存结果时使用提供者的 target_id 属性
### 需求 7: 提供者上下文管理
**用户故事:** 作为开发者,我希望通过提供者传递元数据,以便扫描操作能够访问必要的上下文信息。
#### 验收标准
1. THE ProviderContext SHALL 包含可选的 target_id 字段
2. THE ProviderContext SHALL 包含可选的 scan_id 字段
3. WHEN target_id 为 None, THE 扫描任务 SHALL 跳过保存结果到数据库
4. WHEN 未提供 ProviderContext, THE TargetProvider SHALL 创建默认上下文
5. THE TargetProvider SHALL 通过其接口暴露上下文属性
### 需求 8: 基于迭代器的数据访问
**用户故事:** 作为开发者,我希望提供者使用迭代器进行数据访问,以便在处理大型数据集时保持内存使用效率。
#### 验收标准
1. THE iter_hosts 方法 SHALL 返回主机字符串的迭代器
2. THE iter_urls 方法 SHALL 返回 URL 字符串的迭代器
3. THE 迭代器 SHALL 支持惰性求值以最小化内存使用
4. THE 迭代器 SHALL 优雅地处理空数据集
5. THE 迭代器 SHALL 不将所有数据一次性加载到内存中
### 需求 9: 黑名单过滤器集成
**用户故事:** 作为用户,我希望在扫描数据库目标时自动应用黑名单规则,以便将黑名单中的主机和 URL 排除在扫描之外。
#### 验收标准
1. WHEN 使用 DatabaseTargetProvider, THE provider SHALL 检索目标的黑名单规则
2. THE get_blacklist_filter 方法 SHALL 返回 BlacklistFilter 实例或 None
3. THE BlacklistFilter SHALL 从通过 BlacklistService 检索的规则创建
4. WHEN 使用 ListTargetProvider, THE provider SHALL 为黑名单过滤器返回 None
5. WHEN 使用 PipelineTargetProvider, THE provider SHALL 返回 None数据已被过滤

View File

@@ -1,130 +0,0 @@
# 实现计划: 扫描目标提供者策略模式
## 概述
实现 TargetProvider 策略模式,让扫描任务可以从不同数据源获取目标(数据库、列表、文件),同时保持向后兼容。
## 任务
- [x] 1. 创建 providers 模块基础结构
- 创建 `backend/apps/scan/providers/` 目录
- 创建 `__init__.py` 导出公共接口
- 创建 `base.py` 定义 ProviderContext 和 TargetProvider 抽象基类
- 实现 `_expand_host()` 静态方法用于 CIDR 展开
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.6, 8.1, 8.2, 8.3_
- [x] 2. 实现具体 Provider 类
- [x] 2.1 实现 ListTargetProvider
- 创建 `list_provider.py`
- 实现 iter_hosts(), iter_urls(), get_blacklist_filter()
- 在 iter_hosts() 中使用 _expand_host() 展开 CIDR
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
- [x] 2.2 编写 ListTargetProvider 属性测试
- **Property 1: ListTargetProvider Round-Trip**
- **Validates: Requirements 3.1, 3.2**
- [x] 2.3 实现 DatabaseTargetProvider
- 创建 `database_provider.py`
- 实现从 Subdomain/WebSite/Endpoint 表查询
- 实现黑名单过滤器集成
- 复用现有 TargetExportService 逻辑
- 在 CIDR 类型处理中使用 _expand_host() 展开
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 9.1, 9.2, 9.3_
- [x] 2.4 编写 DatabaseTargetProvider 属性测试
- **Property 6: DatabaseTargetProvider Blacklist Application**
- **Validates: Requirements 2.3, 9.1, 9.2, 9.3_
- [x] 2.5 实现 PipelineTargetProvider预留
- 创建 `pipeline_provider.py`
- 实现从 StageOutput 读取数据
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_
- [x] 2.6 编写 PipelineTargetProvider 属性测试
- **Property 2: PipelineTargetProvider Round-Trip**
- **Validates: Requirements 4.1, 4.2**
- [x] 3. Checkpoint - 确保所有 Provider 测试通过
- 运行所有测试,确保 Provider 模块功能正确
- 如有问题请询问用户
- [x] 4. 改造现有 Task向后兼容
- [x] 4.1 改造 export_hosts_task
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 实现兼容逻辑:无 provider 时用 target_id 创建 DatabaseTargetProvider
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 4.2 编写 export_hosts_task 向后兼容测试
- **Property 8: Task Backward Compatibility**
- **Validates: Requirements 6.1, 6.2, 6.4, 6.5**
- [x] 4.3 改造 export_site_urls_task
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 实现兼容逻辑
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 5. 编写通用属性测试
- [x] 5.1 编写 Context Propagation 属性测试
- **Property 4: Context Propagation**
- **Validates: Requirements 1.3, 1.5, 7.4, 7.5**
- [x] 5.2 编写 Non-Database Provider Blacklist Filter 属性测试
- **Property 5: Non-Database Provider Blacklist Filter**
- **Validates: Requirements 3.4, 9.4, 9.5**
- [x] 5.3 编写 CIDR Expansion Consistency 属性测试
- **Property 7: CIDR Expansion Consistency**
- **Validates: Requirements 1.6, 3.6**
- [x] 6. Final Checkpoint - 确保所有测试通过
- 运行完整测试套件
- 验证现有扫描功能不受影响(回归测试)
- 如有问题请询问用户
- [ ] 7. 改造剩余 Export TaskPhase 2
- [x] 7.1 改造 url_fetch/export_sites_task
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 实现兼容逻辑:无 provider 时用 target_id 创建 DatabaseTargetProvider
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 7.2 改造 directory_scan/export_sites_task
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 实现兼容逻辑
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 7.3 改造 vuln_scan/export_endpoints_task
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 实现兼容逻辑
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 7.4 改造 fingerprint_detect/export_urls_task
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 实现兼容逻辑
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 8. Checkpoint - 确保所有 Export Task 改造完成
- 运行所有测试
- 验证向后兼容性
- 如有问题请询问用户
- [ ] 9. 改造 Screenshot FlowPhase 2 续)
- [x] 9.1 改造 screenshot_flow 支持 TargetProvider
- 添加 provider 参数
- 保留 target_id 参数(向后兼容)
- 当有 provider 时,使用 provider.iter_urls() 获取 URL
- 当无 provider 时,使用现有 get_urls_with_fallback()
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
## 备注
- 每个任务都引用了具体的需求以便追溯
- Checkpoint 任务用于增量验证
- 属性测试验证通用正确性属性
- 单元测试验证具体示例和边界情况

View File

@@ -1,361 +0,0 @@
# 设计文档
## 概述
本文档描述 Go 版本子域名发现 Worker 的技术设计。Worker 作为独立服务运行,通过 HTTP API 与 Server 通信,负责执行扫描工具并将结果回传。
## 架构
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 整体架构 │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────┐ HTTP API ┌─────────────────┐
│ Server │ ────────────────────────▶ │ Worker │
│ (server/) │ │ (worker/) │
│ │ ◀──────────────────────── │ │
│ - 发起扫描 │ 状态/结果回传 │ - 接收任务 │
│ - 存储结果 │ │ - 执行工具 │
│ - 提供 API │ │ - 解析结果 │
└─────────────────┘ └─────────────────┘
```
## 组件和接口
### 项目结构
```
worker/
├── cmd/
│ └── worker/
│ └── main.go # 入口
├── internal/
│ ├── config/
│ │ └── config.go # 配置管理
│ ├── client/
│ │ └── server_client.go # Server API 客户端
│ ├── flow/
│ │ ├── hooks.go # Flow 回调钩子
│ │ ├── runner.go # Flow 执行器
│ │ └── subdomain_discovery.go # 子域名发现 Flow
│ ├── tool/
│ │ ├── command_builder.go # 命令构建器
│ │ ├── runner.go # 工具执行器
│ │ └── templates.go # 命令模板
│ ├── parser/
│ │ └── subdomain.go # 结果解析器
│ ├── handler/
│ │ ├── scan.go # 扫描 API 处理器
│ │ └── health.go # 健康检查
│ └── pkg/
│ └── logger.go # 日志工具
├── go.mod
├── go.sum
├── Makefile
└── .env.example
```
### 核心组件
#### 1. Config配置管理
```go
type Config struct {
ServerURL string // Server API 地址
ServerToken string // 认证 token
ScanToolsBasePath string // 扫描工具路径
ResultsBasePath string // 结果存储路径
LogLevel string // 日志级别
Port int // HTTP 服务端口
}
```
#### 2. ServerClientServer API 客户端)
```go
type ServerClient interface {
// 更新扫描状态
UpdateScanStatus(scanID int, status string, errorMsg string) error
// 批量保存子域名
SaveSubdomains(scanID int, targetID int, subdomains []string) error
// 写入扫描日志
WriteScanLog(scanID int, flowName string, message string, level string) error
}
```
#### 3. ToolRunner工具执行器
```go
type ToolRunner interface {
// 执行单个工具
Run(ctx context.Context, cmd string, timeout time.Duration) (*ToolResult, error)
// 并行执行多个工具
RunParallel(ctx context.Context, cmds []ToolCommand) []*ToolResult
}
type ToolResult struct {
Tool string
OutputFile string
ExitCode int
Duration time.Duration
Error error
}
```
#### 4. CommandBuilder命令构建器
```go
type CommandBuilder interface {
// 构建扫描命令
Build(toolName string, scanType string, params map[string]string, config map[string]interface{}) (string, error)
}
```
#### 5. Flow扫描流程
```go
type Flow interface {
Name() string
Execute(ctx context.Context, input *FlowInput) (*FlowOutput, error)
}
type FlowInput struct {
ScanID int
TargetID int
TargetName string
WorkspaceDir string
Config map[string]interface{}
}
type FlowOutput struct {
Success bool
ProcessedCount int
FailedTools []string
SuccessfulTools []string
}
```
#### 6. FlowHooks回调钩子
```go
type FlowHooks struct {
OnStart func(scanID int, flowName string)
OnComplete func(scanID int, flowName string, output *FlowOutput)
OnFailure func(scanID int, flowName string, err error)
}
```
### HTTP API
#### POST /api/scans/execute
接收扫描任务。
请求体:
```json
{
"scanId": 123,
"targetId": 456,
"targetName": "example.com",
"workspaceDir": "/opt/xingrin/results/scan_20260115_123456",
"config": "subdomain_discovery:\n passive_tools:\n subfinder:\n enabled: true\n..."
}
```
响应:
- 202 Accepted: 任务已接收,异步执行
- 400 Bad Request: 请求参数无效
#### GET /health
健康检查。
响应:
```json
{
"status": "ok",
"version": "1.0.0"
}
```
## 数据模型
### 扫描配置YAML
沿用 Python 版本的配置格式:
```yaml
subdomain_discovery:
passive_tools:
subfinder:
enabled: true
timeout: 3600
sublist3r:
enabled: true
timeout: 3600
assetfinder:
enabled: true
timeout: 3600
bruteforce:
enabled: false
subdomain_bruteforce:
wordlist_name: subdomains-top1million-110000.txt
permutation:
enabled: true
subdomain_permutation_resolve:
timeout: 7200
resolve:
enabled: true
subdomain_resolve:
timeout: auto
```
### 命令模板
沿用 Python 版本的模板格式:
```go
var SubdomainDiscoveryCommands = map[string]CommandTemplate{
"subfinder": {
Base: "subfinder -d {domain} -all -o '{output_file}' -v",
Optional: map[string]string{
"threads": "-t {threads}",
"provider_config": "-pc '{provider_config}'",
},
},
"sublist3r": {
Base: "python3 '/usr/local/share/Sublist3r/sublist3r.py' -d {domain} -o '{output_file}'",
Optional: map[string]string{
"threads": "-t {threads}",
},
},
// ... 其他工具
}
```
## 正确性属性
*正确性属性是系统在所有有效执行中都应该保持的特性。每个属性都是一个可以通过属性测试验证的形式化规范。*
### Property 1: 配置验证完整性
*对于任意* 配置输入如果缺少必需的配置项SERVER_URL、SERVER_TOKEN配置加载应当返回错误如果所有必需项都存在配置加载应当成功。
**验证: 需求 2.2, 2.3**
### Property 2: 命令构建正确性
*对于任意* 有效的工具名、参数映射和配置,命令构建器应当:
1. 用实际值替换所有占位符
2. 当配置中存在可选参数时,将其追加到命令中
3. 生成的命令应当与 Python 版本兼容
**验证: 需求 4.2, 4.3**
### Property 3: 工具执行错误处理
*对于任意* 工具执行,当工具超时时应当返回超时错误,当工具失败时应当返回退出码和错误信息,正常完成时应当返回输出文件路径。
**验证: 需求 5.4, 5.5**
### Property 4: 并行执行等待
*对于任意* 并行执行的工具集合,执行器应当等待所有工具完成后才返回,返回的结果数量应当等于输入的工具数量。
**验证: 需求 5.6, 6.3**
### Property 5: Flow 阶段跳过
*对于任意* 扫描配置,当某个阶段的 `enabled` 为 false 时,该阶段应当被跳过,不执行任何工具。
**验证: 需求 6.2**
### Property 6: 结果去重
*对于任意* 子域名结果集合,解析和合并后的结果应当不包含重复项,且包含所有唯一的子域名。
**验证: 需求 6.4, 7.2**
### Property 7: API 请求认证
*对于任意* ServerClient 发出的 HTTP 请求,请求头中应当包含 Authorization token。
**验证: 需求 3.4**
### Property 8: 请求验证
*对于任意* 扫描执行请求如果缺少必需字段scanId、targetId、targetName、config应当返回 400 错误。
**验证: 需求 9.2, 9.3**
## 错误处理
### 错误类型
```go
var (
ErrConfigMissing = errors.New("missing required configuration")
ErrToolTimeout = errors.New("tool execution timeout")
ErrToolFailed = errors.New("tool execution failed")
ErrAPICallFailed = errors.New("server API call failed")
ErrInvalidRequest = errors.New("invalid request")
)
```
### 重试策略
ServerClient API 调用失败时:
1. 最多重试 3 次
2. 使用指数退避1s, 2s, 4s
3. 所有重试失败后记录错误并继续执行
### 错误传播
```
工具执行失败
记录到 FailedTools 列表
继续执行其他工具
Flow 完成时汇总失败信息
回调 Server 更新状态
```
## 测试策略
### 单元测试
- 配置加载和验证
- 命令构建逻辑
- 结果解析和去重
- 请求验证
### 属性测试
使用 `gopter` 库进行属性测试:
- Property 1: 配置验证完整性
- Property 2: 命令构建正确性
- Property 6: 结果去重
### 集成测试
- ServerClient 与 mock server 的交互
- Flow 完整执行流程(使用 mock 工具)
- HTTP API 端点测试
### 测试配置
- 属性测试最少运行 100 次迭代
- 使用 `testify` 进行断言
- 使用 `httptest` 进行 HTTP 测试

View File

@@ -1,157 +0,0 @@
# 需求文档
## 简介
本功能实现 Go 版本的子域名发现扫描 Worker作为独立服务运行通过 HTTP API 与 Server 通信。Worker 负责执行扫描工具、解析结果、并将结果回传给 Server。
本 spec 聚焦于子域名发现subdomain_discovery这一个扫描类型为后续其他扫描类型的迁移建立基础架构。
## 术语表
- **Worker**: 独立的扫描执行服务,运行在容器中,调用各种扫描工具
- **Server**: Go 后端 API 服务(`server/`),提供数据存储和 API 接口
- **Flow**: 一个扫描类型的完整执行流程(如 subdomain_discovery_flow
- **Stage**: Flow 内部的执行阶段(如被动收集、字典爆破)
- **Tool**: 具体的扫描工具(如 subfinder、assetfinder
- **ToolRunner**: 负责构建命令、执行工具、捕获输出的组件
- **ResultParser**: 负责解析工具输出的组件
- **ServerClient**: Worker 调用 Server API 的 HTTP 客户端
## 需求
### 需求 1: Worker 项目初始化
**用户故事:** 作为开发者,我希望创建一个独立的 Go 模块用于 Worker以便它可以独立于 Server 进行开发和部署。
#### 验收标准
1. Worker 应当作为独立的 Go 模块创建在 `worker/` 目录下
2. Worker 应当有自己的 `go.mod`,模块名为 `github.com/xingrin/worker`
3. Worker 应当遵循与 Server 相同的项目结构(`cmd/``internal/`
4. Worker 应当使用与 Server 相同的技术栈Gin 用于 HTTP、Zap 用于日志、Viper 用于配置)
### 需求 2: 配置管理
**用户故事:** 作为开发者,我希望 Worker 能从环境变量和配置文件加载配置,以便在不同环境中轻松配置。
#### 验收标准
1. Worker 应当从环境变量和 `.env` 文件加载配置
2. Worker 应当支持以下配置项:
- `SERVER_URL`: Server API 地址(如 `http://server:8888`
- `SERVER_TOKEN`: 访问 Server API 的认证 token
- `SCAN_TOOLS_BASE_PATH`: 扫描工具的基础路径(默认 `/usr/local/bin`
- `RESULTS_BASE_PATH`: 扫描结果的基础路径(默认 `/opt/xingrin/results`
- `LOG_LEVEL`: 日志级别(默认 `info`
3. 当缺少必需的配置项时Worker 应当启动失败并显示清晰的错误信息
### 需求 3: Server API 客户端
**用户故事:** 作为 Worker我希望通过 HTTP API 与 Server 通信,以便接收扫描任务并提交结果。
#### 验收标准
1. ServerClient 应当支持以下操作:
- 更新扫描状态running/completed/failed
- 批量保存子域名结果
- 写入扫描日志
2. 当 API 调用失败时ServerClient 应当使用指数退避策略重试最多 3 次
3. 当所有重试都失败时ServerClient 应当记录错误并继续执行
4. ServerClient 应当在所有请求中包含认证 token
### 需求 4: 命令模板系统
**用户故事:** 作为开发者,我希望为每个扫描工具定义命令模板,以便根据配置动态构建命令。
#### 验收标准
1. Worker 应当为子域名发现工具定义命令模板:
- subfinder
- sublist3r
- assetfinder
- subdomain_bruteforcepuredns
- subdomain_resolvepuredns
- subdomain_permutation_resolvednsgen + puredns
2. 当构建命令时Worker 应当用实际值替换占位符
3. 当配置中提供了可选参数时Worker 应当将其追加到命令中
4. 命令模板格式应当与现有 Python 实现兼容
### 需求 5: 工具执行器ToolRunner
**用户故事:** 作为 Worker我希望执行扫描工具并捕获其输出以便收集扫描结果。
#### 验收标准
1. ToolRunner 应当执行带有可配置超时的 shell 命令
2. ToolRunner 应当捕获 stdout 和 stderr
3. ToolRunner 应当将工具输出写入日志文件
4. 当工具超时时ToolRunner 应当终止进程并返回超时错误
5. 当工具失败时ToolRunner 应当返回退出码和错误信息
6. ToolRunner 应当支持多个工具的并行执行
### 需求 6: 子域名发现 Flow
**用户故事:** 作为 Worker我希望执行包含多个阶段的子域名发现流程以便全面发现子域名。
#### 验收标准
1. subdomain_discovery_flow 应当支持 4 个阶段:
- Stage 1: 被动收集(并行执行 subfinder、sublist3r、assetfinder
- Stage 2: 字典爆破(可选,使用 puredns bruteforce
- Stage 3: 变异生成 + 验证(可选,使用 dnsgen + puredns resolve
- Stage 4: DNS 存活验证(可选,使用 puredns resolve
2. 当某个阶段在配置中被禁用时Flow 应当跳过该阶段
3. 当 Stage 1 的工具并行运行时Flow 应当等待所有工具完成后再继续
4. 当多个阶段产生结果时Flow 应当合并并去重
5. Flow 应当在所有阶段完成后调用 Server API 保存结果
### 需求 7: 结果解析与保存
**用户故事:** 作为 Worker我希望解析工具输出并将结果保存到 Server以便发现的子域名被持久化。
#### 验收标准
1. Worker 应当从工具输出文件解析子域名结果(每行一个域名)
2. Worker 应当在保存前对子域名去重
3. Worker 应当调用 Server API 批量保存子域名
4. 保存结果时Worker 应当包含 scan_id 和 target_id
### 需求 8: 状态回调机制
**用户故事:** 作为 Worker我希望向 Server 报告扫描状态,以便用户可以跟踪扫描进度。
#### 验收标准
1. 当 Flow 开始时Worker 应当调用 Server API 将状态更新为 "running"
2. 当 Flow 成功完成时Worker 应当调用 Server API 将状态更新为 "completed"
3. 当 Flow 失败时Worker 应当调用 Server API 将状态更新为 "failed" 并附带错误信息
4. Worker 应当通过 API 将扫描日志写入 Server 以便用户查看
### 需求 9: HTTP API 接口
**用户故事:** 作为 Server我希望通过 HTTP API 在 Worker 上触发扫描,以便编排扫描执行。
#### 验收标准
1. Worker 应当暴露 POST `/api/scans/execute` 端点来接收扫描任务
2. 请求体应当包含:
- scan_id: 扫描 ID
- target_id: 目标 ID
- target_name: 目标名称(域名)
- workspace_dir: 工作目录路径
- config: 扫描配置YAML 格式)
3. Worker 应当验证请求,如果无效则返回 400
4. Worker 应当异步执行扫描并立即返回 202 Accepted
5. Worker 应当暴露 GET `/health` 端点用于健康检查
### 需求 10: 错误处理与日志
**用户故事:** 作为开发者,我希望有全面的错误处理和日志记录,以便轻松调试问题。
#### 验收标准
1. Worker 应当记录所有工具执行,包括命令、耗时和退出码
2. Worker 应当记录所有对 Server 的 API 调用,包括请求/响应详情
3. 当发生错误时Worker 应当记录带有上下文的错误scan_id、tool_name 等)
4. Worker 应当在生产环境使用 JSON 格式的结构化日志
5. Worker 应当在开发环境支持人类可读的日志

View File

@@ -1,163 +0,0 @@
# 任务列表
## 任务 1: 初始化 Worker 项目结构
**需求:** 需求 1
**依赖:**
### 子任务
- [x] 1.1 创建 `worker/` 目录和 `go.mod`
- [x] 1.2 创建项目目录结构cmd/、internal/
- [x] 1.3 创建 `cmd/worker/main.go` 入口文件
- [x] 1.4 创建 `Makefile``.env.example`
- [x] 1.5 添加基础依赖gin、zap、viper
---
## 任务 2: 实现配置管理
**需求:** 需求 2
**依赖:** 任务 1
### 子任务
- [x] 2.1 创建 `internal/config/config.go`
- [x] 2.2 实现从环境变量和 .env 文件加载配置
- [x] 2.3 实现必需配置项验证
- [ ] 2.4 编写配置加载单元测试
---
## 任务 3: 实现日志工具
**需求:** 需求 10
**依赖:** 任务 2
### 子任务
- [x] 3.1 创建 `internal/pkg/logger.go`
- [x] 3.2 实现 JSON 格式结构化日志
- [x] 3.3 实现开发环境人类可读日志
- [x] 3.4 支持日志级别配置
---
## 任务 4: 实现 Server API 客户端
**需求:** 需求 3
**依赖:** 任务 2, 任务 3
### 子任务
- [x] 4.1 创建 `internal/client/server_client.go`
- [x] 4.2 实现 UpdateScanStatus 方法
- [x] 4.3 实现 SaveSubdomains 方法
- [x] 4.4 实现 WriteScanLog 方法
- [x] 4.5 实现指数退避重试逻辑
- [ ] 4.6 编写 ServerClient 单元测试
---
## 任务 5: 实现命令模板系统
**需求:** 需求 4
**依赖:** 任务 1
### 子任务
- [x] 5.1 创建 `internal/tool/templates.go`
- [x] 5.2 定义子域名发现工具的命令模板
- [x] 5.3 创建 `internal/tool/command_builder.go`
- [x] 5.4 实现占位符替换逻辑
- [x] 5.5 实现可选参数追加逻辑
- [ ] 5.6 编写命令构建单元测试
---
## 任务 6: 实现工具执行器
**需求:** 需求 5
**依赖:** 任务 3, 任务 5
### 子任务
- [x] 6.1 创建 `internal/tool/runner.go`
- [x] 6.2 实现单工具执行(带超时)
- [x] 6.3 实现 stdout/stderr 捕获
- [x] 6.4 实现日志文件写入
- [x] 6.5 实现并行执行多个工具
- [ ] 6.6 编写工具执行器单元测试
---
## 任务 7: 实现结果解析器
**需求:** 需求 7
**依赖:** 任务 1
### 子任务
- [x] 7.1 创建 `internal/parser/subdomain.go`
- [x] 7.2 实现从文件解析子域名(每行一个)
- [x] 7.3 实现子域名去重逻辑
- [ ] 7.4 编写解析器单元测试
---
## 任务 8: 实现 Flow 框架
**需求:** 需求 6, 需求 8
**依赖:** 任务 4, 任务 6, 任务 7
### 子任务
- [x] 8.1 创建 `internal/flow/hooks.go`(回调钩子定义)
- [x] 8.2 创建 `internal/flow/types.go`Flow 类型定义)
- [x] 8.3 创建 `internal/flow/subdomain_discovery.go`
- [x] 8.4 实现 Stage 1: 被动收集(并行执行)
- [x] 8.5 实现 Stage 2: 字典爆破(可选)
- [x] 8.6 实现 Stage 3: 变异生成 + 验证(可选)
- [x] 8.7 实现 Stage 4: DNS 存活验证(可选)
- [x] 8.8 实现阶段跳过逻辑
- [x] 8.9 实现结果合并去重
- [ ] 8.10 编写 Flow 单元测试
---
## 任务 9: 实现 HTTP API
**需求:** 需求 9
**依赖:** 任务 8
### 子任务
- [x] 9.1 创建 `internal/handler/health.go`
- [x] 9.2 创建 `internal/handler/scan.go`
- [x] 9.3 实现请求验证
- [x] 9.4 实现异步扫描执行
- [ ] 9.5 编写 HTTP API 集成测试
---
## 任务 10: 集成和端到端测试
**需求:** 所有需求
**依赖:** 任务 9
### 子任务
- [ ] 10.1 编写 Flow 集成测试(使用 mock 工具)
- [ ] 10.2 编写完整扫描流程测试
- [ ] 10.3 更新 README 文档
- [ ] 10.4 创建 Dockerfile

File diff suppressed because it is too large Load Diff

View File

@@ -1,286 +0,0 @@
# 需求文档
## 简介
本功能实现基于 WebSocket 的轻量级 Agent用于替代当前的 SSH 任务分发方式。Agent 作为常驻服务运行在远程 VPS 上,主动连接 Server 建立长连接,接收任务指令并启动临时 Worker 容器执行扫描。
## 核心优势
- **无需公网 IP**Agent 主动连接 Server支持 NAT 穿透
- **无需 SSH**:不暴露 SSH 端口,更安全
- **一键安装**:单个二进制文件 + systemd 服务
- **轻量级**Go 编译,~10MB 内存占用
## 术语表
- **Agent**: 常驻在远程 VPS 上的轻量服务,负责接收任务、启动 Worker、上报状态
- **Server**: Go 后端 API 服务(`server/`),负责任务调度和 WebSocket 连接管理
- **Worker**: 临时容器,执行具体的扫描任务,完成后退出
- **Task**: 一个扫描任务,包含 task_id、scan_id、target、config 等信息
- **Heartbeat**: Agent 定期发送的心跳消息,包含系统负载信息
- **Agent API Key**: 绑定到单个 Agent 记录的认证密钥,用于 WebSocket 连接和 /api/agent/** HTTP 调用
## 需求
### 需求 1: Agent 项目初始化
**用户故事:** 作为开发者,我希望创建一个独立的 Go 模块用于 Agent以便它可以编译为单个二进制文件分发。
#### 验收标准
1. Agent 应当作为独立的 Go 模块创建在 `agent/` 目录下
2. Agent 应当有自己的 `go.mod`,模块名为 `github.com/orbit/agent`
3. Agent 应当编译为单个静态链接的二进制文件(~10MB
4. Agent 应当支持 Linux amd64 和 arm64 架构
### 需求 2: 配置管理
**用户故事:** 作为运维人员,我希望通过命令行参数配置 Agent以便快速部署。
#### 验收标准
1. Agent 应当支持以下命令行参数:
- `--server`: Server 基础地址(必需,仅 IP/域名/端口,不包含协议)
- `--key`: API Key 用于认证(必需)
- `--name`: Agent 名称(可选,默认为主机名)
2. 当缺少必需参数时Agent 应当显示帮助信息并退出
3. 调度参数maxTasks、cpuThreshold、memThreshold由 Server 动态下发,不通过 CLI 配置
3. Agent 应当支持从环境变量读取配置(`AGENT_SERVER_HOST``AGENT_API_KEY`),其中 `AGENT_SERVER_HOST` 为基础地址(不含协议)
4. Agent 根据 `--server` 自动派生(强制 https/wss
- WebSocket URL`wss://<server>/api/agents/ws`
- Worker HTTP URL`https://<server>`(用于注入到 Worker 的 `SERVER_URL`
5. TLS 校验默认关闭(跳过证书校验)
### 需求 3: WebSocket 连接管理
**用户故事:** 作为 Agent我希望与 Server 建立稳定的 WebSocket 长连接,以便接收实时控制通知(配置更新/取消/更新)。
#### 验收标准
1. Agent 启动时应当主动连接 Server 的 WebSocket 端点
2. 连接时应当在 Header 中携带 API Key 进行认证
3. 当连接失败时Agent 应当使用指数退避策略重试1s, 2s, 4s, 8s, 最大 60s
4. 当连接断开时Agent 应当自动重连
5. Agent 应当响应 Server 的 ping 消息以保持连接活跃
### 需求 4: 心跳与负载上报
**用户故事:** 作为 Server我希望实时了解 Agent 的负载情况,以便进行任务调度。
#### 验收标准
1. Agent 应当每 5 秒发送一次心跳消息
2. 心跳消息应当包含:
- `cpu`: CPU 使用率(%
- `mem`: 内存使用率(%
- `disk`: 磁盘使用率(%
- `tasks`: 当前运行的任务数
- `version`: Agent 版本号
- `hostname`: 主机名
- `uptime`: Agent 运行时长(秒)
3. 当 Server 超过 15 秒未收到心跳时,应当将 Agent 标记为离线
**说明IP/Host 记录)**
- Server 记录 `last_seen_ip`(来自 WebSocket 连接的 RemoteAddr 或代理头)
- Agent 在心跳中上报 `hostname`
- IP 仅用于诊断与展示,不作为身份校验
### 需求 5: 任务拉取与执行
**用户故事:** 作为 Agent我希望通过 HTTP 主动拉取任务并执行,而不是被动接收推送。
#### 验收标准
1. Agent 应当通过 `POST /api/agent/tasks/pull` 拉取任务(不使用 WebSocket 推送)
2. 任务拉取响应应当包含:
- task_id: 任务 IDscan_task.id
- scan_id: 扫描 ID任务所属 scan
- target_id: 目标 ID
- target_name: 目标名称
- target_type: 目标类型domain/ip/cidr/url
- workflow_name: 工作流名称(如 subdomain_discovery
- workspace_dir: 工作目录
- config: YAML 格式的扫描配置
- worker_image: Worker 镜像名称和版本(可选,如 `yyhuni/orbit-worker:v1.0.19`
3. Agent 不维护本地等待队列,仅跟踪运行中任务
4. Agent 应当跟踪每个任务的状态running/completed/failed
**说明**`taskId` 为 scan_task.id`scanId` 表示所属 scan单 workflow 时为 1:1但仍区分
**说明Worker 版本策略)**
- Server 负责决定 Worker 镜像版本(通常与 Server 版本同步)
- 任务中携带 `worker_image` 用于显式指定版本
- 若任务未指定 `worker_image`Agent 使用默认 Worker 镜像
### 需求 6: 任务调度Agent Pull + PostgreSQL 队列)
**用户故事:** 作为 Agent我希望在空闲时向 Server 主动拉取最高优先级的任务,避免饥饿并实现负载均衡。
#### 当前实现
**单 Workflow 模式**:一个 Scan 对应一个 scan_task执行 `subdomain_discovery` workflow。
- 创建 Scan 时,同时创建一个 `status='ready'` 的 scan_task
- workflow 内部的多阶段recon/bruteforce/permutation/resolve由 Worker 内部编排
- `depends_on` 字段预留为空数组,未来支持多 workflow 串联
#### 验收标准
1. Agent 应当持续监控本机 CPU、内存和磁盘使用率
2. 当同时满足以下条件时,调用 Server API 拉取任务pull
- 当前运行任务数 < max_tasks默认 5
- CPU 使用率 < cpu_threshold默认 85%
- 内存使用率 < mem_threshold默认 85%
- 磁盘使用率 < disk_threshold默认 90%
3. Server 从 PostgreSQL 中按优先级选择一个 `ready` 任务返回,并进行行级锁定,防止并发重复分配
4. 优先级计算:`priority = stage * 100 + wait_seconds`(当前所有任务 stage=0
5. 当任一条件不满足时Agent 等待后再检查
6. 调度参数由 Server 动态下发(通过 `config_update` 消息)
7. 磁盘使用率通过 gopsutil 的 `disk.Usage("/")` 获取根分区使用率
#### Server 端 SQL行级锁
```sql
-- 原子地取出一个最高优先级任务并锁住
WITH c AS (
SELECT id
FROM scan_task
WHERE status = 'ready'
ORDER BY priority DESC, id ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
)
UPDATE scan_task t
SET status = 'dispatched', dispatched_at = NOW(), agent_id = $1
FROM c
WHERE t.id = c.id
RETURNING t.*;
```
#### API
- `POST /api/agent/tasks/pull`Header: `X-Agent-Key`
- Request: `{}`
- Response: `{taskId, scanId, stage, workerImage, config, ...}``204 No Content`(无任务)
#### 未来扩展(预留)
- 支持多 workflow 串联subdomain → port_scan → vuln_scan
- Worker 端 `templates.yaml` 新增 `depends_on``output_type` 字段
- Server 端解析 workflow metadata自动构建 DAG
- 任务完成后触发下游任务 `pending``ready`
### 需求 7: Worker 容器管理
**用户故事:** 作为 Agent我希望管理 Worker 容器的生命周期,以便正确执行和清理任务。
#### 验收标准
1. Agent 应当使用 Docker APIGo SDK启动 Worker 容器
2. Worker 容器应当使用自动清理策略(等价于 `--rm`),完成后删除
3. Agent 应当使用任务中指定的 Worker 镜像版本
4. Agent 应当使用 “缺失时拉取” 策略(本地有镜像则直接使用,没有才拉取)
5. Agent 应当将任务参数通过环境变量传递给 Worker
6. Agent 应当挂载必要的目录(/opt/orbit
7. 当任务被取消时Agent 应当停止对应的 Worker 容器
8. Agent 应当捕获 Worker 的退出码判断任务成功/失败
### 需求 8: 任务状态管理
**用户故事:** 作为 Server我希望 Agent 负责管理任务状态scan_task以便统一状态更新入口。
#### 验收标准
1. Server 在拉取分配任务时将 `scan_task.status` 置为 `dispatched`,并同步将 `scan.status` 置为 `scheduled`
2. Agent 启动 Worker 成功后,应当调用 HTTP API 将 `scan_task.status` 更新为 `running`,并同步将 `scan.status` 置为 `running`
3. Agent 监控 Worker 退出码:
- 退出码为 0调用 HTTP API 更新为 `completed`(同步 `scan.status=completed`,并写入 `scan.stopped_at`
- 退出码非 0调用 HTTP API 更新为 `failed`(同步 `scan.status=failed`,写入 `scan.stopped_at`,并附带错误信息)
4. 任务被取消时Agent 应当调用 HTTP API 更新为 `cancelled`(同步 `scan.status=cancelled`,并写入 `scan.stopped_at`
5. Worker 不再负责更新状态,只负责执行扫描和保存结果
6. 错误信息获取Agent 使用 Docker SDK 读取 Worker 容器的最后 100 行日志作为 `errorMessage`
7. 日志截断:如果日志超过 4KB截断并添加 `[truncated]\n` 前缀
**HTTP API 端点**
```
PATCH /api/agent/tasks/:id/status
Header: X-Agent-Key: <agent_api_key>
Body: {"status": "running|completed|failed|cancelled", "errorMessage": "..."}
```
**说明**
- /api/agent/** 使用 X-Agent-Key
- /api/worker/** 仍使用 X-Worker-TokenWorker 容器内调用)
### 需求 9: Server 端 WebSocket 支持
**用户故事:** 作为 Server我希望管理多个 Agent 的 WebSocket 连接,以便接收心跳并下发实时控制通知(取消/配置更新/更新)。
#### 验收标准
1. Server 应当暴露 `/api/agents/ws` WebSocket 端点
2. Server 应当验证连接时的 API Key
3. Server 应当维护在线 Agent 列表
4. Server 应当能向指定 Agent 推送控制消息task_cancel/config_update/update_required以及可选的 task_available
5. Server 应当处理 Agent 断开连接的情况
6. Agent 连接成功后Server 应推送 `config_update` 消息(包含 maxTasks、cpuThreshold、memThreshold
7. 管理员通过 API 修改配置时Server 应推送 `config_update` 给在线 Agent
### 需求 10: Docker 镜像构建
**用户故事:** 作为运维人员,我希望通过一条 docker run 命令部署 Agent以便快速部署到多台机器。
#### 验收标准
1. Agent 应当提供 Dockerfile构建为 Docker 镜像
2. 镜像应当支持 linux/amd64 和 linux/arm64 架构
3. 镜像不需要包含 Docker CLI使用 Docker SDK
4. 部署命令应当挂载 `/var/run/docker.sock``/opt/orbit`
5. 使用 `--restart=always` 实现自动重启
6. Agent 容器使用 `--oom-score-adj=-500` 降低被 OOM 杀死的优先级
7. Worker 容器使用 `oom-score-adj=500` 提高被 OOM 杀死的优先级(保护 Agent
### 需求 10.1: 安装流程与脚本
**用户故事:** 作为管理员,我希望 Server 能生成安装命令和脚本,方便一键部署 Agent。
#### 验收标准
1. Server 创建 Agent 时生成 API Key并绑定到该 Agent
2. 创建 Agent 的响应应返回:
- agentId
- apiKey
- installCommand已拼好 --server 和 --key
- installScriptUrl例如 `/api/agents/install.sh?key=...`
3. Server 应提供安装脚本下载接口(`GET /api/agents/install.sh`
4. 安装脚本应:
- 检查 Docker 是否安装
- 创建 `/opt/orbit` 目录
- 使用 `docker run --restart=always ... --server <base> --key <apiKey>` 启动 Agent
- 若已有旧容器,先 stop/remove 再启动
5. Agent API Key 支持重置/禁用(可选)
6. 仅管理员可创建/删除 Agent权限控制
### 需求 11: 错误处理与日志
**用户故事:** 作为运维人员,我希望有清晰的日志输出,以便排查问题。
#### 验收标准
1. Agent 应当记录所有重要事件(连接、断开、任务开始/完成)
2. Agent 应当使用结构化日志JSON 格式)
3. Agent 应当支持日志级别配置debug/info/warn/error
4. 当发生错误时Agent 应当记录详细的错误信息和上下文
### 需求 12: 自动更新
**用户故事:** 作为运维人员,我希望 Agent 能自动更新到最新版本,无需手动干预。
#### 验收标准
1. Agent 心跳时应当上报当前版本号
2. Server 检测到版本不匹配时,应当发送 `update_required` 消息
3. Agent 收到更新指令后,应当:
- 拉取新版本镜像docker pull
- 启动新版本容器(使用相同配置)
- 退出当前容器(让新容器接管)
4. 更新过程应当记录详细日志
5. 更新失败时Agent 应当继续运行并上报错误

View File

@@ -1,294 +0,0 @@
# 任务列表
## 阶段 1Agent 端
## 任务 1: Agent 项目初始化
**需求**: 需求 1
**说明**: 创建 Agent 项目的基础结构,包括 Go 模块、目录结构和构建配置。
### 子任务
- [ ] 1.1 创建 `agent/` 目录和 `go.mod`(模块名 `github.com/orbit/agent`
- [ ] 1.2 创建项目目录结构cmd/agent、internal/config、internal/connection 等)
- [ ] 1.3 创建 Makefile支持 `build``build-linux-amd64``build-linux-arm64` 目标
- [ ] 1.4 创建 `cmd/agent/main.go` 入口文件(基础框架)
---
## 任务 2: 配置管理
**需求**: 需求 2
**说明**: 实现命令行参数解析和环境变量读取。
### 子任务
- [ ] 2.1 创建 `internal/config/config.go`,定义 Config 结构体
- [ ] 2.2 实现命令行参数解析(--server、--key、--name、--cpu-threshold、--mem-threshold
- [ ] 2.3 实现环境变量读取AGENT_SERVER_HOST、AGENT_API_KEY
- [ ] 2.4 实现配置验证(必需参数检查)
- [ ] 2.5 在 main.go 中集成配置加载
- [ ] 2.6 基于 `--server` 派生 WebSocket URLwss://<server>/api/agents/ws与 Worker HTTP URLhttps://<server>,强制 https/wssTLS 校验默认关闭
---
## 任务 3: 日志和系统信息
**需求**: 需求 10, 需求 4
**说明**: 实现结构化日志和系统信息采集。使用 `github.com/shirou/gopsutil/v3` 采集系统负载,正确处理容器 cgroup 限制。
### 子任务
- [ ] 3.1 创建 `internal/pkg/logger.go`,封装 zap 日志
- [ ] 3.2 支持日志级别配置debug/info/warn/error
- [ ] 3.3 创建 `internal/pkg/system.go`,使用 gopsutil 实现 CPU/内存使用率采集
- [ ] 3.4 编写系统信息采集的单元测试
---
## 任务 4: WebSocket 连接管理
**需求**: 需求 3
**说明**: 实现 WebSocket 客户端,包括连接、重连和消息收发。
### 子任务
- [ ] 4.1 创建 `internal/connection/message.go`,定义消息结构
- [ ] 4.2 创建 `internal/connection/client.go`,实现 WebSocketClient 接口
- [ ] 4.3 实现连接建立(携带 X-Agent-Key Header
- [ ] 4.4 实现消息发送和接收
- [ ] 4.5 创建 `internal/connection/reconnect.go`,实现指数退避重连
- [ ] 4.6 实现 ping/pong 响应
- [ ] 4.7 编写连接管理的单元测试
- [ ] 4.8 在连接建立时记录 last_seen_ipRemoteAddr / X-Forwarded-For
---
## 任务 5: 心跳上报
**需求**: 需求 4
**说明**: 实现定时心跳上报,包含系统负载信息。
### 子任务
- [ ] 5.1 创建 `internal/heartbeat/reporter.go`,实现 HeartbeatReporter
- [ ] 5.2 实现每 5 秒发送心跳消息
- [ ] 5.3 心跳包含 CPU、内存、任务数、版本号
- [ ] 5.4 在 main.go 中启动心跳协程
- [ ] 5.5 心跳携带 hostname并在 Server 侧保存
---
## 任务 6: 任务执行器
**需求**: 需求 5, 需求 6
**说明**: 实现基于 channel 的任务执行器,包含队列、调度和执行逻辑。使用 gopsutil 进行负载检查。
### 子任务
- [ ] 6.1 创建 `internal/task/executor.go`,实现 TaskExecutor
- [ ] 6.2 使用 channel 作为任务队列
- [ ] 6.3 实现 for 循环消费任务
- [ ] 6.4 实现负载检查(使用 gopsutil 获取 CPU/内存,与阈值比较)
- [ ] 6.5 实现任务取消(标记 + 停止容器)
- [ ] 6.6 编写任务执行器的单元测试
---
## 任务 7: Docker 容器管理
**需求**: 需求 7
**说明**: 实现 Worker 容器的启动、监控和清理。
### 子任务
- [ ] 7.1 创建 `internal/docker/runner.go`,实现 DockerRunner 接口
- [ ] 7.2 实现容器启动Docker SDK自动清理
- [ ] 7.3 实现环境变量传递SERVER_URL、SCAN_ID 等)
- [ ] 7.4 实现目录挂载(/opt/orbit
- [ ] 7.5 实现容器停止(用于任务取消)
- [ ] 7.6 实现等待容器退出并获取退出码
- [ ] 7.7 编写 Docker SDK 操作的集成测试
---
## 任务 8: 扫描状态管理
**需求**: 需求 8
**说明**: 实现 Agent 调用 HTTP API 更新扫描状态。
### 子任务
- [ ] 8.1 创建 `internal/server/client.go`,实现 HTTP 客户端
- [ ] 8.2 实现 `UpdateScanStatus(scanID, status, errorMessage)` 方法
- [ ] 8.3 在任务执行器中集成状态更新:
- 收到任务 → `scheduled`
- 启动 Worker → `running`
- Worker 退出码 0 → `completed`
- Worker 退出码非 0 → `failed`
- 任务取消 → `cancelled`
- [ ] 8.4 处理 HTTP 请求失败(重试 + 日志),并使用 X-Agent-Key 认证
---
## 任务 9: Agent 主循环
**需求**: 需求 3, 需求 5
**说明**: 实现 Agent 主循环,协调各组件。
### 子任务
- [ ] 9.1 在 main.go 中实现主循环
- [ ] 9.2 启动 WebSocket 连接(带重连)
- [ ] 9.3 启动心跳上报协程
- [ ] 9.4 启动任务执行器协程
- [ ] 9.5 实现消息分发(根据消息类型调用对应处理器)
- [ ] 9.6 实现优雅关闭SIGINT/SIGTERM
- [ ] 9.7 收到 task_assign 后发送 task_ack入队即确认
- [ ] 9.8 Server 端使用 ack_timeout=5s 进行投递重发
---
## 任务 10: Docker 镜像构建
**需求**: 需求 10
**说明**: 创建 Agent 的 Dockerfile 和构建配置。
### 子任务
- [ ] 10.1 创建 `agent/Dockerfile`(基于 alpine不包含 docker-cli
- [ ] 10.2 配置多架构构建amd64/arm64
- [ ] 10.3 在 Makefile 中添加 `docker-build` 目标
- [ ] 10.4 创建安装脚本 `install-agent.sh`
- [ ] 10.5 编写部署文档README.md
---
## 任务 11: 自动更新
**需求**: 需求 12
**说明**: 实现 Agent 自动更新功能。
### 子任务
- [ ] 11.1 创建 `internal/updater/updater.go`,实现 Updater 组件
- [ ] 11.2 实现镜像拉取docker pull
- [ ] 11.3 实现启动新版本容器(使用相同配置)
- [ ] 11.4 实现退出当前进程(让新容器接管)
- [ ] 11.5 在消息处理中集成 `update_required` 处理
- [ ] 11.6 编写更新流程的集成测试
---
## 阶段 2Server 端(后续实现)
## 任务 12: Server 端 WebSocket Hub
**需求**: 需求 9
**说明**: 在 Server 端实现 WebSocket 连接管理中心。
### 子任务
- [ ] 12.1 创建 `server/internal/websocket/message.go`,定义消息结构
- [ ] 12.2 创建 `server/internal/websocket/client.go`,封装单个 Agent 连接
- [ ] 12.3 创建 `server/internal/websocket/hub.go`,实现连接管理
- [ ] 12.4 实现 Agent 注册/注销
- [ ] 12.5 实现向指定 Agent 发送消息
- [ ] 12.6 实现心跳超时检测15 秒)
---
## 任务 13: Server 端 WebSocket 端点
**需求**: 需求 9
**说明**: 实现 WebSocket API 端点和认证。
### 子任务
- [ ] 13.1 创建 `server/internal/handler/agent_ws.go`
- [ ] 13.2 实现 `/api/agents/ws` WebSocket 端点
- [ ] 13.3 实现 API Key 认证(从 Header 读取 X-Agent-Key查数据库验证
- [ ] 13.4 认证成功后更新 agent 记录status=online, connected_at, hostname 等)
- [ ] 13.5 实现消息处理(心跳、任务状态等)
- [ ] 13.6 在路由中注册 WebSocket 端点
---
## 任务 14: Agent 数据模型和 API
**需求**: 需求 9
**说明**: 实现 Agent 数据模型和管理 API。
### 子任务
- [ ] 14.1 创建数据库迁移文件agent 表)
- [ ] 14.2 创建 `server/internal/model/agent.go` 模型
- [ ] 14.3 实现 Agent CRUD API创建、列表、删除
- [ ] 14.4 创建 Agent 时生成 API Key
- [ ] 14.5 返回部署命令(包含 Key
- [ ] 14.6 创建 `GET /api/agents/install.sh` 安装脚本接口
- [ ] 14.7 创建 Agent 时返回 installCommand 与 installScriptUrl
- [ ] 14.8 API 响应包含 last_seen_ip 与 hostname供前端展示
---
## 任务 15: Server 端任务分发
**需求**: 需求 9
**说明**: 实现基于 WebSocket 的任务分发服务。
### 子任务
- [ ] 15.1 创建 `server/internal/service/task_dispatcher.go`
- [ ] 15.2 实现选择最优 Agent基于心跳负载数据
- [ ] 15.3 实现任务推送(通过 WebSocket
- [ ] 15.4 实现任务状态回调处理
- [ ] 15.5 更新扫描 API使用新的任务分发服务
- [ ] 15.6 task_assign payload 包含 targetType、workflowName、workerImage若未提供 workerImageAgent 使用默认镜像
---
## 任务 16: Server 端版本检测
**需求**: 需求 12
**说明**: 在 Server 端实现 Agent 版本检测和更新触发。
### 子任务
- [ ] 16.1 在心跳处理中比较 Agent 版本和 Server 版本
- [ ] 16.2 版本不匹配时发送 `update_required` 消息
- [ ] 16.3 使用 Redis 锁防止重复触发60 秒内只触发一次)
- [ ] 16.4 记录更新日志
---
## 任务 17: 集成测试
**需求**: 全部
**说明**: 编写端到端集成测试。
### 子任务
- [ ] 17.1 编写 Agent 连接和认证测试
- [ ] 17.2 编写心跳上报测试
- [ ] 17.3 编写任务分配和执行测试
- [ ] 17.4 编写断线重连测试
- [ ] 17.5 编写多 Agent 负载均衡测试
- [ ] 17.6 编写自动更新测试

View File

@@ -1,740 +0,0 @@
# 当前系统文档Worker 命令模板系统
> 本文档记录了重构前的系统架构、代码实现和配置方式,供后续 AI 研究和对比。
>
> 创建时间2026-01-17
>
> 状态:待重构
---
## 目录
1. [系统概述](#系统概述)
2. [架构设计](#架构设计)
3. [核心组件](#核心组件)
4. [配置系统](#配置系统)
5. [数据流](#数据流)
6. [代码示例](#代码示例)
7. [存在的问题](#存在的问题)
---
## 系统概述
### 功能
Worker 命令模板系统负责:
1. 定义扫描工具的命令模板
2. 根据用户配置构建实际执行的命令
3. 管理参数的占位符替换
4. 支持可选参数的动态添加
### 技术栈
- **语言**: Go 1.21+
- **配置格式**: YAML
- **模板引擎**: 字符串替换(无第三方库)
- **嵌入资源**: `embed.FS`
---
## 架构设计
### 系统组件图
```
┌─────────────────────────────────────────────────────────────┐
│ Server 配置 (subdomain_discovery.yaml) │
│ - 用户定义启用哪些工具 │
│ - 用户覆盖参数值timeout, threads 等) │
└─────────────────────────────────────────────────────────────┘
│ 传递配置
┌─────────────────────────────────────────────────────────────┐
│ Workflow (subdomain_discovery) │
│ - 读取 Server 配置 │
│ - 调用各个 stage 执行扫描 │
└─────────────────────────────────────────────────────────────┘
│ 调用
┌─────────────────────────────────────────────────────────────┐
│ Stage (stage_passive.go, stage_bruteforce.go) │
│ - 遍历启用的工具 │
│ - 调用 buildCommand() 构建命令 │
│ - 调用 Runner 执行命令 │
└─────────────────────────────────────────────────────────────┘
│ 调用
┌─────────────────────────────────────────────────────────────┐
│ buildCommand() (helpers.go) │
│ - 获取模板 │
│ - 调用 CommandBuilder.Build() │
└─────────────────────────────────────────────────────────────┘
│ 调用
┌─────────────────────────────────────────────────────────────┐
│ TemplateLoader (template_loader.go) │
│ - 从 embed.FS 加载 templates.yaml │
│ - 使用 sync.Once 缓存模板 │
└─────────────────────────────────────────────────────────────┘
│ 返回模板
┌─────────────────────────────────────────────────────────────┐
│ CommandBuilder (command_builder.go) │
│ - 替换必需占位符 │
│ - 添加可选参数 │
│ - 返回最终命令字符串 │
└─────────────────────────────────────────────────────────────┘
```
### 两层配置系统
系统使用两层配置:
1. **Worker 模板层** (`templates.yaml`)
- 开发者定义
- 包含命令模板和可选参数映射
- 嵌入到 Worker 二进制文件中
2. **Server 配置层** (`subdomain_discovery.yaml`)
- 用户定义
- 控制启用哪些工具
- 覆盖参数值(如 timeout, threads
---
## 核心组件
### 1. CommandTemplate 结构体
**文件**: `worker/internal/activity/command_template.go`
```go
package activity
// CommandTemplate defines a command template for an activity
type CommandTemplate struct {
Base string `yaml:"base"` // Base command with required placeholders
Optional map[string]string `yaml:"optional"` // Optional parameters and their flags
}
```
**特点**:
- 扁平结构
- `Base`: 基础命令,包含必需占位符(如 `{domain}`, `{output-file}`
- `Optional`: 可选参数的标志模板(如 `"-t {threads}"`
- **没有默认值字段** - 默认值硬编码在命令字符串中
### 2. CommandBuilder
**文件**: `worker/internal/activity/command_builder.go`
```go
type CommandBuilder struct{}
func (b *CommandBuilder) Build(
tmpl CommandTemplate,
params map[string]string,
config map[string]any,
) (string, error) {
// 1. 从 base 开始
cmd := tmpl.Base
// 2. 替换必需占位符
for key, value := range params {
placeholder := "{" + key + "}"
cmd = strings.ReplaceAll(cmd, placeholder, value)
}
// 3. 添加可选参数(如果用户配置中存在)
for configKey, flagTemplate := range tmpl.Optional {
if value, ok := getConfigValue(config, configKey); ok {
flag := strings.ReplaceAll(flagTemplate, "{"+configKey+"}", fmt.Sprintf("%v", value))
cmd = cmd + " " + flag
}
}
// 4. 检查未替换的占位符
if strings.Contains(cmd, "{") && strings.Contains(cmd, "}") {
return "", fmt.Errorf("command contains unreplaced placeholders: %s", cmd)
}
return cmd, nil
}
```
**工作流程**:
1.`Base` 命令开始
2. 替换必需占位符(如 `{domain}``example.com`
3. 如果用户配置中有可选参数,添加对应的标志
4. 检查是否有未替换的占位符
5. 返回最终命令字符串
### 3. TemplateLoader
**文件**: `worker/internal/workflow/subdomain_discovery/template_loader.go`
```go
package subdomain_discovery
import (
"embed"
"github.com/orbit/worker/internal/activity"
)
//go:embed templates.yaml
var templatesFS embed.FS
// loader is the template loader for subdomain discovery workflow
var loader = activity.NewTemplateLoader(templatesFS, "templates.yaml")
// getTemplate returns the command template for a given tool
func getTemplate(toolName string) (activity.CommandTemplate, error) {
return loader.Get(toolName)
}
```
**特点**:
- 使用 `embed.FS` 嵌入 YAML 文件
- 使用 `sync.Once` 缓存已加载的模板
- 提供简单的 `getTemplate()` 接口
### 4. buildCommand 辅助函数
**文件**: `worker/internal/workflow/subdomain_discovery/helpers.go`
```go
func buildCommand(toolName string, params map[string]string, config map[string]any) (string, error) {
tmpl, err := getTemplate(toolName)
if err != nil {
return "", err
}
builder := activity.NewCommandBuilder()
return builder.Build(tmpl, params, config)
}
```
**作用**:
- 封装模板获取和命令构建的流程
- 被各个 stage 调用
---
## 配置系统
### Worker 模板配置
**文件**: `worker/internal/workflow/subdomain_discovery/templates.yaml`
```yaml
# 被动收集工具
subfinder:
base: "subfinder -d {domain} -all -o '{output-file}' -v"
optional:
threads: "-t {threads}"
provider-config: "-pc '{provider-config}'"
timeout: "-timeout {timeout}"
sublist3r:
base: "python3 '/usr/local/share/Sublist3r/sublist3r.py' -d {domain} -o '{output-file}'"
optional:
threads: "-t {threads}"
assetfinder:
base: "assetfinder --subs-only {domain} > '{output-file}'"
optional: {}
# 主动扫描工具
subdomain-bruteforce:
base: "puredns bruteforce '{wordlist}' {domain} -r '{resolvers}' --write '{output-file}' --quiet"
optional:
threads: "-t {threads}"
rate-limit: "--rate-limit {rate-limit}"
wildcard-tests: "--wildcard-tests {wildcard-tests}"
wildcard-batch: "--wildcard-batch {wildcard-batch}"
subdomain-resolve:
base: "puredns resolve '{input-file}' -r '{resolvers}' --write '{output-file}' --wildcard-tests 50 --wildcard-batch 1000000 --quiet"
optional:
threads: "-t {threads}"
rate-limit: "--rate-limit {rate-limit}"
wildcard-tests: "--wildcard-tests {wildcard-tests}"
wildcard-batch: "--wildcard-batch {wildcard-batch}"
```
**特点**:
- 扁平结构:`base``optional` 分开
- 默认值硬编码在 `base` 中(如 `--wildcard-tests 50`
- 没有参数类型定义
- 没有参数描述
### Server 配置
**文件**: `server/configs/engines/subdomain_discovery.yaml`
```yaml
# Stage 1: Passive Collection
passive-tools:
subfinder:
enabled: true
timeout: 3600 # 覆盖默认值
# threads: 10 # 可选,注释掉表示使用默认值
sublist3r:
enabled: true
timeout: 3600
assetfinder:
enabled: true
timeout: 3600
# Stage 2: Dictionary Bruteforce
bruteforce:
enabled: false
subdomain-bruteforce:
timeout: 86400
wordlist-name: subdomains-top1million-110000.txt
# Stage 3: Permutation + Resolve
permutation:
enabled: true
subdomain-permutation-resolve:
timeout: 86400
# Stage 4: DNS Resolution Validation
resolve:
enabled: true
subdomain-resolve:
timeout: 86400
```
**特点**:
- 扁平结构
- 使用 `enabled` 控制工具是否启用
- 用户可以覆盖参数值
- 没有参数说明(用户不知道有哪些参数可配置)
---
## 数据流
### 完整的命令构建流程
```
1. 用户配置
server/configs/engines/subdomain_discovery.yaml
{
"passive-tools": {
"subfinder": {
"enabled": true,
"timeout": 3600,
"threads": 20
}
}
}
2. Workflow 读取配置
stage_passive.go: runPassiveStage()
- 遍历 passive-tools
- 检查 enabled: true
- 调用 createPassiveCommand()
3. 构建命令参数
stage_passive.go: createPassiveCommand()
params = {
"domain": "example.com",
"output-file": "/path/to/output.txt"
}
config = {
"timeout": 3600,
"threads": 20
}
4. 调用 buildCommand()
helpers.go: buildCommand()
- 调用 getTemplate("subfinder")
- 调用 CommandBuilder.Build()
5. 获取模板
template_loader.go: getTemplate()
返回:
{
"base": "subfinder -d {domain} -all -o '{output-file}' -v",
"optional": {
"threads": "-t {threads}",
"timeout": "-timeout {timeout}"
}
}
6. 构建命令
command_builder.go: Build()
步骤 1: 从 base 开始
cmd = "subfinder -d {domain} -all -o '{output-file}' -v"
步骤 2: 替换必需占位符
cmd = "subfinder -d example.com -all -o '/path/to/output.txt' -v"
步骤 3: 添加可选参数
- config 中有 "timeout": 3600
- 添加 "-timeout 3600"
- config 中有 "threads": 20
- 添加 "-t 20"
cmd = "subfinder -d example.com -all -o '/path/to/output.txt' -v -timeout 3600 -t 20"
步骤 4: 检查未替换占位符
- 没有 "{...}" 格式的字符串
- 验证通过
7. 返回最终命令
"subfinder -d example.com -all -o '/path/to/output.txt' -v -timeout 3600 -t 20"
8. 执行命令
Runner.RunParallel()
```
### 参数优先级
当前系统的参数值来源:
1. **硬编码默认值** (最低优先级)
-`base` 命令中硬编码
- 例如:`--wildcard-tests 50`
2. **用户配置** (最高优先级)
- 在 Server 配置文件中定义
- 例如:`timeout: 3600`
**问题**: 如果默认值在 `base` 中硬编码,用户无法覆盖它们(除非修改 Worker 模板)
---
## 代码示例
### 示例 1: 构建 subfinder 命令
```go
// 输入
toolName := "subfinder"
params := map[string]string{
"domain": "example.com",
"output-file": "/tmp/subfinder_output.txt",
}
config := map[string]any{
"timeout": 3600,
"threads": 20,
}
// 调用
cmd, err := buildCommand(toolName, params, config)
// 输出
// cmd = "subfinder -d example.com -all -o '/tmp/subfinder_output.txt' -v -timeout 3600 -t 20"
```
### 示例 2: 构建 puredns resolve 命令
```go
// 输入
toolName := "subdomain-resolve"
params := map[string]string{
"input-file": "/tmp/subdomains.txt",
"output-file": "/tmp/resolved.txt",
"resolvers": "/etc/resolvers.txt",
}
config := map[string]any{
"threads": 100,
}
// 调用
cmd, err := buildCommand(toolName, params, config)
// 输出
// cmd = "puredns resolve '/tmp/subdomains.txt' -r '/etc/resolvers.txt' --write '/tmp/resolved.txt' --wildcard-tests 50 --wildcard-batch 1000000 --quiet -t 100"
//
// 注意:--wildcard-tests 50 和 --wildcard-batch 1000000 是硬编码的,无法覆盖
```
### 示例 3: Stage 执行流程
```go
// stage_passive.go
func (w *Workflow) runPassiveStage(ctx *workflowContext) stageResult {
// 1. 获取 stage 配置
stageConfig, ok := ctx.config[stagePassive].(map[string]any)
if !ok {
return stageResult{}
}
var commands []activity.Command
// 2. 遍历所有域名
for _, domain := range ctx.domains {
// 3. 遍历所有被动工具
for _, toolName := range passiveTools {
// 4. 检查工具是否启用
if !isToolEnabled(stageConfig, toolName) {
continue
}
// 5. 获取工具配置
toolConfig, _ := stageConfig[toolName].(map[string]any)
// 6. 创建命令
cmd := w.createPassiveCommand(ctx, domain, toolName, toolConfig)
if cmd != nil {
commands = append(commands, *cmd)
}
}
}
// 7. 并行执行所有命令
results := w.runner.RunParallel(ctx.ctx, commands)
return processResults(results)
}
```
---
## 存在的问题
### 1. 默认值硬编码
**问题**: 默认参数值直接写在 `base` 命令中
```yaml
subdomain-resolve:
base: "puredns resolve '{input-file}' -r '{resolvers}' --write '{output-file}' --wildcard-tests 50 --wildcard-batch 1000000 --quiet"
```
**影响**:
- 用户无法覆盖这些默认值
- 修改默认值需要修改 Worker 模板并重新编译
- 默认值不可见(用户不知道有哪些默认值)
### 2. 配置重复
**问题**: 多个工具有相同的参数配置,但需要重复定义
```yaml
# 每个工具都要定义 threads, timeout
subfinder:
optional:
threads: "-t {threads}"
timeout: "-timeout {timeout}"
sublist3r:
optional:
threads: "-t {threads}"
timeout: "-timeout {timeout}" # 重复
subdomain-bruteforce:
optional:
threads: "-t {threads}" # 重复
rate-limit: "--rate-limit {rate-limit}"
wildcard-tests: "--wildcard-tests {wildcard-tests}"
wildcard-batch: "--wildcard-batch {wildcard-batch}"
subdomain-resolve:
optional:
threads: "-t {threads}" # 重复
rate-limit: "--rate-limit {rate-limit}" # 重复
wildcard-tests: "--wildcard-tests {wildcard-tests}" # 重复
wildcard-batch: "--wildcard-batch {wildcard-batch}" # 重复
```
**影响**:
- 修改共享配置需要改多处
- 容易出现不一致
- 维护成本高
### 3. 缺少验证
**问题**: 模板加载时没有验证
```go
// template_loader.go 只是简单加载,没有验证
func (l *TemplateLoader) Load() (map[string]CommandTemplate, error) {
// 读取 YAML
data, err := l.fs.ReadFile(l.filename)
// 解析 YAML
if err := yaml.Unmarshal(data, &l.cache); err != nil {
return nil, err
}
// 没有验证!
return l.cache, nil
}
```
**影响**:
- 模板错误只在运行时才能发现
- 错误信息不清晰
- 难以调试
### 4. 类型不明确
**问题**: 参数类型未定义
```yaml
subfinder:
optional:
threads: "-t {threads}" # threads 是什么类型int? string?
timeout: "-timeout {timeout}" # timeout 是什么类型?
```
**影响**:
- 容易出现类型错误
- 没有类型验证
- 用户不知道应该传什么类型的值
### 5. 错误信息不清晰
**问题**: 构建失败时错误信息简单
```go
if strings.Contains(cmd, "{") && strings.Contains(cmd, "}") {
return "", fmt.Errorf("command contains unreplaced placeholders: %s", cmd)
}
```
**影响**:
- 不知道哪个占位符未替换
- 不知道缺少哪个参数
- 难以快速定位问题
### 6. 缺少参数文档
**问题**: Server 配置文件没有参数说明
```yaml
passive-tools:
subfinder:
enabled: true
timeout: 3600
# 用户不知道还有哪些参数可以配置
# 用户不知道参数的含义和取值范围
```
**影响**:
- 用户需要查看 Worker 模板才知道有哪些参数
- 没有参数说明和默认值
- 配置困难
### 7. 配置结构不一致
**问题**: Worker 模板和 Server 配置结构不同
```yaml
# Worker 模板(扁平)
subfinder:
base: "..."
optional:
threads: "-t {threads}"
# Server 配置(也是扁平,但结构不同)
passive-tools:
subfinder:
enabled: true
threads: 20
```
**影响**:
- 两层配置难以对应
- 用户不知道 Worker 模板中定义了什么
- 维护困难
---
## 总结
### 当前系统的优点
1.**简单直观**: 扁平结构,易于理解
2.**性能良好**: 使用 `sync.Once` 缓存,避免重复加载
3.**灵活**: 支持任意参数的动态添加
### 当前系统的缺点
1.**默认值硬编码**: 无法覆盖,不易维护
2.**配置重复**: 大量重复定义,维护成本高
3.**缺少验证**: 错误只在运行时发现
4.**类型不明确**: 没有类型定义和验证
5.**错误信息简单**: 难以快速定位问题
6.**缺少文档**: 用户不知道有哪些参数可配置
7.**结构不一致**: Worker 模板和 Server 配置难以对应
### 重构目标
基于以上问题,重构应该实现:
1. ✅ 默认值在模板中明确定义
2. ✅ 使用 YAML 锚点消除重复
3. ✅ 启动时验证所有模板
4. ✅ 明确的参数类型定义
5. ✅ 详细的错误信息
6. ✅ 自动生成配置文档
7. ✅ 统一的配置结构(符合业界标准)
---
## 附录
### 相关文件清单
**Worker 端**:
- `worker/internal/activity/command_template.go` - 模板结构定义
- `worker/internal/activity/command_builder.go` - 命令构建器
- `worker/internal/activity/template_loader.go` - 通用模板加载器
- `worker/internal/workflow/subdomain_discovery/templates.yaml` - 工具模板
- `worker/internal/workflow/subdomain_discovery/template_loader.go` - Workflow 特定加载器
- `worker/internal/workflow/subdomain_discovery/helpers.go` - 辅助函数
- `worker/internal/workflow/subdomain_discovery/stage_passive.go` - 被动收集阶段
- `worker/internal/workflow/subdomain_discovery/stage_bruteforce.go` - 爆破阶段
**Server 端**:
- `server/configs/engines/subdomain_discovery.yaml` - 用户配置
### 关键常量
```go
// worker/internal/workflow/subdomain_discovery/workflow.go
const (
// Stage names
stagePassive = "passive-tools"
stageBruteforce = "bruteforce"
stagePermutation = "permutation"
stageResolve = "resolve"
// Tool names
toolSubfinder = "subfinder"
toolSublist3r = "sublist3r"
toolAssetfinder = "assetfinder"
toolSubdomainBruteforce = "subdomain-bruteforce"
toolSubdomainResolve = "subdomain-resolve"
toolSubdomainPermutation = "subdomain-permutation-resolve"
// Default timeout
defaultTimeout = 86400 // 24 hours
)
```
---
**文档结束**

View File

@@ -1,438 +0,0 @@
# Go Template 快速参考
本文档提供 Worker 命令模板系统使用 Go Template 的快速参考。
## 为什么选择 Go Template
### 代码量对比
| 方案 | 代码量 | 功能 |
|------|--------|------|
| 自定义字符串替换 | ~140 行 | 基础替换 |
| **Go Template** | **~50 行** | 替换 + 条件 + 循环 + 函数 |
**减少 64% 代码量,功能更强大!**
### 核心优势
1.**自动错误检测**: `missingkey=error` 自动检测缺失字段
2.**详细错误信息**: 提供行号、列号、字段名
3.**业界标准**: Helm、Kubernetes 都使用
4.**功能强大**: 支持条件、循环、函数
5.**减少维护**: 不需要自己实现验证逻辑
## 基础语法
### 占位符
```yaml
# 基础占位符PascalCase
base_command: "subfinder -d {{.Domain}} -o {{.OutputFile}}"
# 必需字段
{{.Domain}} # 目标域名
{{.OutputFile}} # 输出文件路径
{{.InputFile}} # 输入文件路径
{{.Target}} # 目标 URL
# 可选字段
{{.Timeout}} # 超时时间
{{.Threads}} # 线程数
{{.Verbose}} # 详细输出(布尔值)
```
### 函数调用
```yaml
# quote - 自动添加引号
base_command: "subfinder -d {{.Domain}} -o {{quote .OutputFile}}"
# 输出: subfinder -d example.com -o "/tmp/output.txt"
# default - 提供默认值
base_command: "subfinder -d {{.Domain}} -timeout {{default 3600 .Timeout}}"
# 如果 Timeout 未设置,使用 3600
# lower/upper - 大小写转换
base_command: "tool --mode {{lower .Mode}}"
# Mode="DEBUG" -> --mode debug
```
### 条件渲染
```yaml
# if - 条件判断
base_command: "nuclei -u {{.Target}} {{if .Verbose}}-v{{end}}"
# Verbose=true -> nuclei -u example.com -v
# Verbose=false -> nuclei -u example.com
# if-else
base_command: "tool {{if .Debug}}-debug{{else}}-quiet{{end}}"
# 多条件
base_command: "tool {{if .Verbose}}-v{{end}} {{if .Debug}}-d{{end}}"
```
### 循环(高级)
```yaml
# range - 遍历数组
base_command: "tool {{range .Domains}}-d {{.}} {{end}}"
# Domains=["a.com", "b.com"] -> tool -d a.com -d b.com
```
## 模板文件格式
### 完整示例
```yaml
# 共享参数定义
x-common-params: &common-params
Timeout:
flag: "-timeout {{.Timeout}}"
default: 3600
type: "int"
required: false
description: "扫描超时时间(秒)"
RateLimit:
flag: "-rl {{.RateLimit}}"
default: 150
type: "int"
required: false
description: "每秒请求数限制"
# 工具模板
subfinder:
base_command: "subfinder -d {{.Domain}} -all -o {{quote .OutputFile}} -v"
parameters:
<<: *common-params # 引用共享参数
Threads:
flag: "-t {{.Threads}}"
default: 10
type: "int"
required: false
description: "并发线程数"
ProviderConfig:
flag: "-pc {{quote .ProviderConfig}}"
default: null
type: "string"
required: false
description: "Provider 配置文件路径"
# 条件渲染示例
nuclei:
base_command: "nuclei -u {{.Target}} {{if .Verbose}}-v{{end}} {{if .Debug}}-debug{{end}}"
parameters:
Verbose:
flag: "" # 条件在 base_command 中处理
default: false
type: "bool"
required: false
description: "启用详细输出"
Debug:
flag: ""
default: false
type: "bool"
required: false
description: "启用调试模式"
```
## 命名规范
### 占位符命名
| 类型 | 格式 | 示例 |
|------|------|------|
| 必需字段 | PascalCase | `{{.Domain}}`, `{{.OutputFile}}` |
| 可选字段 | PascalCase | `{{.Timeout}}`, `{{.Threads}}` |
| 函数调用 | 小写 | `{{quote .Domain}}`, `{{default 3600 .Timeout}}` |
### 参数名映射
用户配置snake_case自动映射到 Go TemplatePascalCase
| 用户配置 | Go Template |
|---------|-------------|
| `timeout` | `{{.Timeout}}` |
| `rate_limit` | `{{.RateLimit}}` |
| `provider_config` | `{{.ProviderConfig}}` |
| `output_file` | `{{.OutputFile}}` |
## 代码实现
### CommandBuilder
```go
type CommandBuilder struct {
funcMap template.FuncMap
}
func NewCommandBuilder() *CommandBuilder {
return &CommandBuilder{
funcMap: template.FuncMap{
"quote": func(s string) string {
return fmt.Sprintf("%q", s)
},
"default": func(def, val interface{}) interface{} {
if val == nil || val == "" {
return def
}
return val
},
"lower": strings.ToLower,
"upper": strings.ToUpper,
},
}
}
func (b *CommandBuilder) Build(
tmpl CommandTemplate,
params map[string]any,
config map[string]any,
) (string, error) {
// 1. 合并数据
data := mergeParameters(tmpl, params, config)
// 2. 构建完整模板
cmdTemplate := buildCommandTemplate(tmpl, data)
// 3. 执行 Go Template
t, err := template.New("command").
Funcs(b.funcMap).
Option("missingkey=error"). // 自动检测缺失字段
Parse(cmdTemplate)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return "", fmt.Errorf("execute template: %w", err)
}
return buf.String(), nil
}
```
### 辅助函数
```go
// mergeParameters 合并必需参数、默认值和用户配置
func mergeParameters(
tmpl CommandTemplate,
params map[string]any,
config map[string]any,
) map[string]any {
result := make(map[string]any)
// 1. 添加必需参数
for key, value := range params {
result[key] = value
}
// 2. 添加可选参数(默认值 + 用户配置)
for name, param := range tmpl.Parameters {
if userValue, exists := config[name]; exists {
result[name] = userValue
} else if param.Default != nil {
result[name] = param.Default
}
}
return result
}
// buildCommandTemplate 构建完整的命令模板字符串
func buildCommandTemplate(tmpl CommandTemplate, data map[string]any) string {
cmd := tmpl.BaseCommand
// 只添加有值的参数的 flag
for name, param := range tmpl.Parameters {
if _, exists := data[name]; exists {
cmd += " " + param.Flag
}
}
return cmd
}
// convertKeys 转换 snake_case 到 PascalCase
func convertKeys(config map[string]any) map[string]any {
result := make(map[string]any)
for key, value := range config {
pascalKey := snakeToPascal(key)
result[pascalKey] = value
}
return result
}
func snakeToPascal(s string) string {
parts := strings.Split(s, "_")
for i, part := range parts {
if len(part) > 0 {
parts[i] = strings.ToUpper(part[:1]) + part[1:]
}
}
return strings.Join(parts, "")
}
```
## 错误处理
### Go Template 自动错误检测
```go
// 配置 missingkey=error
t := template.New("command").
Option("missingkey=error"). // 自动检测缺失字段
Parse(cmdTemplate)
```
### 错误信息示例
```
# 缺失字段错误
template: command:1:15: executing "command" at <.Domain>: map has no entry for key "Domain"
// ↑ 行号:列号 ↑ 字段名 ↑ 具体错误
# 语法错误
template: command:1: unexpected "}" in operand
// ↑ 具体的语法问题
# 函数调用错误
template: command:1:20: executing "command" at <quote .Domain>: error calling quote: invalid argument type
```
## 测试示例
### 单元测试
```go
func TestCommandBuilder_Build(t *testing.T) {
builder := NewCommandBuilder()
tmpl := CommandTemplate{
BaseCommand: "subfinder -d {{.Domain}} -o {{quote .OutputFile}}",
Parameters: map[string]Parameter{
"Timeout": {
Flag: "-timeout {{.Timeout}}",
Default: 3600,
Type: "int",
},
},
}
params := map[string]any{
"Domain": "example.com",
"OutputFile": "/tmp/out.txt",
}
config := map[string]any{
"Timeout": 7200,
}
cmd, err := builder.Build(tmpl, params, config)
assert.NoError(t, err)
assert.Equal(t, `subfinder -d example.com -o "/tmp/out.txt" -timeout 7200`, cmd)
}
func TestCommandBuilder_MissingField(t *testing.T) {
builder := NewCommandBuilder()
tmpl := CommandTemplate{
BaseCommand: "subfinder -d {{.Domain}}",
Parameters: map[string]Parameter{},
}
params := map[string]any{} // 缺少 Domain
config := map[string]any{}
_, err := builder.Build(tmpl, params, config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "Domain") // 错误信息包含字段名
}
```
## 迁移指南
### 从自定义替换迁移到 Go Template
**步骤 1**: 更新占位符格式
```yaml
# 旧格式
base_command: "subfinder -d {domain} -o '{output-file}'"
# 新格式
base_command: "subfinder -d {{.Domain}} -o {{quote .OutputFile}}"
```
**步骤 2**: 更新参数名
```yaml
# 旧格式kebab-case
{domain}
{output-file}
{provider-config}
# 新格式PascalCase
{{.Domain}}
{{.OutputFile}}
{{.ProviderConfig}}
```
**步骤 3**: 使用函数替代手动引号
```yaml
# 旧格式
base_command: "subfinder -d {domain} -o '{output-file}'"
# 新格式(使用 quote 函数)
base_command: "subfinder -d {{.Domain}} -o {{quote .OutputFile}}"
```
**步骤 4**: 使用条件替代可选标志
```yaml
# 旧格式(需要在代码中判断)
base_command: "nuclei -u {target}"
# 代码中: if verbose { cmd += " -v" }
# 新格式(模板内条件)
base_command: "nuclei -u {{.Target}} {{if .Verbose}}-v{{end}}"
```
## 常见问题
### Q: 为什么使用 PascalCase 而不是 snake_case
A: Go Template 中的字段名必须是导出的(首字母大写),所以使用 PascalCase。用户配置仍然可以使用 snake_case会自动转换。
### Q: 如何处理文件路径中的空格?
A: 使用 `{{quote .OutputFile}}` 函数自动添加引号。
### Q: 如何处理布尔标志?
A: 使用条件渲染:`{{if .Verbose}}-v{{end}}`
### Q: 如何调试模板错误?
A: Go Template 提供详细的错误信息,包含行号、列号和字段名。启动时会验证所有模板语法。
### Q: 性能如何?
A: Go Template 性能优秀,模板会被缓存,执行速度快。比自定义字符串替换更快。
## 参考资源
- [Go text/template 官方文档](https://pkg.go.dev/text/template)
- [Helm Template 指南](https://helm.sh/docs/chart_template_guide/)
- [Kubernetes Go Template](https://kubernetes.io/docs/reference/kubectl/jsonpath/)
---
**版本**: 1.0
**最后更新**: 2026-01-17

View File

@@ -1,613 +0,0 @@
# 配置文件元数据设计
## 概述
为了让配置文件更清晰、更易维护,我们在 Worker 模板中添加元数据,描述:
1. Workflow 的整体流程
2. 每个 Stage 的作用和依赖关系
3. 每个 Tool 的用途和参数说明
## 设计原则
1. **单一数据源**:元数据定义在 Worker 模板中,自动生成到文档
2. **自描述**:配置文件本身包含足够的信息,用户无需查看代码
3. **可验证**:元数据可用于验证配置的正确性
4. **业界标准**:参考 GitHub Actions、Terraform 的元数据格式
## 元数据结构
### Worker 模板元数据
```yaml
# worker/internal/workflow/subdomain_discovery/templates.yaml
# Workflow 元数据
metadata:
name: "subdomain_discovery"
display_name: "子域名发现"
description: "通过被动收集、字典爆破、排列组合等方式发现目标域名的所有子域名"
version: "1.0.0"
target_types: ["domain"] # 支持的目标类型
# 阶段定义
stages:
- id: "passive"
name: "被动收集"
description: "使用多个数据源被动收集子域名,不产生主动扫描流量"
order: 1
required: true
parallel: true # 阶段内工具并行执行
depends_on: []
outputs: ["subdomains"]
- id: "bruteforce"
name: "字典爆破"
description: "使用字典对域名进行爆破,发现未公开的子域名"
order: 2
required: false
parallel: false
depends_on: []
outputs: ["subdomains"]
- id: "permutation"
name: "排列组合"
description: "对已发现的子域名进行排列组合,生成新的可能子域名"
order: 3
required: false
parallel: false
depends_on: ["passive", "bruteforce"] # 依赖前面阶段的输出
outputs: ["subdomains"]
- id: "resolve"
name: "DNS 解析验证"
description: "验证所有发现的子域名是否可解析"
order: 4
required: false
parallel: false
depends_on: ["passive", "bruteforce", "permutation"]
outputs: ["subdomains"]
# 共享参数定义
x-common-params: &common-params
Timeout:
flag: "-timeout {{.Timeout}}"
default: 3600
type: "int"
required: false
description: "扫描超时时间(秒)"
min: 1
max: 86400
RateLimit:
flag: "-rl {{.RateLimit}}"
default: 150
type: "int"
required: false
description: "每秒请求数限制"
min: 1
max: 10000
# 工具模板
tools:
# 被动收集工具
subfinder:
metadata:
display_name: "Subfinder"
description: "使用多个数据源Shodan、Censys、VirusTotal 等)被动收集子域名"
stage: "passive"
category: "passive_collection"
homepage: "https://github.com/projectdiscovery/subfinder"
requires_api_keys: true
api_providers: ["shodan", "censys", "virustotal", "securitytrails"]
base_command: "subfinder -d {{.Domain}} -all -o {{quote .OutputFile}} -v"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 10
type: "int"
required: false
description: "并发线程数"
min: 1
max: 100
ProviderConfig:
flag: "-pc {{quote .ProviderConfig}}"
default: null
type: "string"
required: false
description: "API 提供商配置文件路径(包含 API keys"
sublist3r:
metadata:
display_name: "Sublist3r"
description: "使用搜索引擎Google、Bing、Yahoo 等)被动收集子域名"
stage: "passive"
category: "passive_collection"
homepage: "https://github.com/aboul3la/Sublist3r"
requires_api_keys: false
base_command: "python3 '/usr/local/share/Sublist3r/sublist3r.py' -d {{.Domain}} -o {{quote .OutputFile}}"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 10
type: "int"
required: false
description: "并发线程数"
min: 1
max: 100
assetfinder:
metadata:
display_name: "Assetfinder"
description: "使用多个数据源快速查找子域名"
stage: "passive"
category: "passive_collection"
homepage: "https://github.com/tomnomnom/assetfinder"
requires_api_keys: false
base_command: "assetfinder --subs-only {{.Domain}} > {{quote .OutputFile}}"
parameters:
<<: *common-params
# 主动扫描工具
subdomain-bruteforce:
metadata:
display_name: "Subdomain Bruteforce"
description: "使用字典对域名进行 DNS 爆破,发现未公开的子域名"
stage: "bruteforce"
category: "active_scan"
homepage: "https://github.com/d3mondev/puredns"
requires_api_keys: false
warning: "主动扫描会产生大量 DNS 请求,可能被目标检测"
base_command: "puredns bruteforce {{quote .Wordlist}} {{.Domain}} -r {{quote .Resolvers}} --write {{quote .OutputFile}} --quiet"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 100
type: "int"
required: false
description: "并发线程数"
min: 1
max: 1000
RateLimit:
flag: "--rate-limit {{.RateLimit}}"
default: 500
type: "int"
required: false
description: "每秒 DNS 请求数限制"
min: 1
max: 10000
WildcardTests:
flag: "--wildcard-tests {{.WildcardTests}}"
default: 50
type: "int"
required: false
description: "泛解析检测测试次数"
min: 1
max: 1000
WildcardBatch:
flag: "--wildcard-batch {{.WildcardBatch}}"
default: 1000000
type: "int"
required: false
description: "泛解析检测批次大小"
min: 1000
max: 10000000
subdomain-permutation-resolve:
metadata:
display_name: "Subdomain Permutation + Resolve"
description: "对已发现的子域名进行排列组合,生成新的可能子域名并验证"
stage: "permutation"
category: "permutation"
homepage: "https://github.com/ProjectAnte/dnsgen"
requires_api_keys: false
depends_on_stages: ["passive", "bruteforce"]
base_command: "cat {{quote .InputFile}} | dnsgen - | puredns resolve -r {{quote .Resolvers}} --write {{quote .OutputFile}} --quiet"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 100
type: "int"
required: false
description: "并发线程数"
min: 1
max: 1000
RateLimit:
flag: "--rate-limit {{.RateLimit}}"
default: 500
type: "int"
required: false
description: "每秒 DNS 请求数限制"
min: 1
max: 10000
subdomain-resolve:
metadata:
display_name: "Subdomain Resolve"
description: "验证所有发现的子域名是否可解析,过滤无效子域名"
stage: "resolve"
category: "validation"
homepage: "https://github.com/d3mondev/puredns"
requires_api_keys: false
depends_on_stages: ["passive", "bruteforce", "permutation"]
base_command: "puredns resolve {{quote .InputFile}} -r {{quote .Resolvers}} --write {{quote .OutputFile}} --quiet"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 100
type: "int"
required: false
description: "并发线程数"
min: 1
max: 1000
RateLimit:
flag: "--rate-limit {{.RateLimit}}"
default: 500
type: "int"
required: false
description: "每秒 DNS 请求数限制"
min: 1
max: 10000
WildcardTests:
flag: "--wildcard-tests {{.WildcardTests}}"
default: 50
type: "int"
required: false
description: "泛解析检测测试次数"
min: 1
max: 1000
WildcardBatch:
flag: "--wildcard-batch {{.WildcardBatch}}"
default: 1000000
type: "int"
required: false
description: "泛解析检测批次大小"
min: 1000
max: 10000000
```
## 生成的配置文档
从上述元数据自动生成用户配置文档:
```markdown
# 子域名发现配置参考
## 概述
**名称**: 子域名发现 (subdomain_discovery)
**版本**: 1.0.0
**描述**: 通过被动收集、字典爆破、排列组合等方式发现目标域名的所有子域名
**支持的目标类型**: domain
## 扫描流程
子域名发现包含 4 个阶段,按顺序执行:
### 阶段 1: 被动收集 (passive) [必需]
**描述**: 使用多个数据源被动收集子域名,不产生主动扫描流量
**执行方式**: 并行执行
**依赖**: 无
**输出**: 子域名列表
**可用工具**:
- subfinder - 使用多个数据源Shodan、Censys、VirusTotal 等)被动收集子域名
- sublist3r - 使用搜索引擎Google、Bing、Yahoo 等)被动收集子域名
- assetfinder - 使用多个数据源快速查找子域名
### 阶段 2: 字典爆破 (bruteforce) [可选]
**描述**: 使用字典对域名进行爆破,发现未公开的子域名
**执行方式**: 顺序执行
**依赖**: 无
**输出**: 子域名列表
**警告**: 主动扫描会产生大量 DNS 请求,可能被目标检测
**可用工具**:
- subdomain-bruteforce - 使用字典对域名进行 DNS 爆破
### 阶段 3: 排列组合 (permutation) [可选]
**描述**: 对已发现的子域名进行排列组合,生成新的可能子域名
**执行方式**: 顺序执行
**依赖**: passive, bruteforce
**输出**: 子域名列表
**可用工具**:
- subdomain-permutation-resolve - 对已发现的子域名进行排列组合并验证
### 阶段 4: DNS 解析验证 (resolve) [可选]
**描述**: 验证所有发现的子域名是否可解析
**执行方式**: 顺序执行
**依赖**: passive, bruteforce, permutation
**输出**: 已验证的子域名列表
**可用工具**:
- subdomain-resolve - 验证所有发现的子域名是否可解析
## 工具配置
### subfinder
**描述**: 使用多个数据源Shodan、Censys、VirusTotal 等)被动收集子域名
**阶段**: passive
**主页**: https://github.com/projectdiscovery/subfinder
**需要 API Keys**: 是(支持 shodan, censys, virustotal, securitytrails
#### 参数
| 参数 | 类型 | 默认值 | 必需 | 范围 | 描述 |
|------|------|--------|------|------|------|
| timeout | int | 3600 | 否 | 1-86400 | 扫描超时时间(秒) |
| threads | int | 10 | 否 | 1-100 | 并发线程数 |
| provider_config | string | - | 否 | - | API 提供商配置文件路径 |
#### 示例
```yaml
subfinder:
enabled: true
timeout: 7200
threads: 20
```
### subdomain-bruteforce
**描述**: 使用字典对域名进行 DNS 爆破,发现未公开的子域名
**阶段**: bruteforce
**主页**: https://github.com/d3mondev/puredns
**警告**: ⚠️ 主动扫描会产生大量 DNS 请求,可能被目标检测
#### 参数
| 参数 | 类型 | 默认值 | 必需 | 范围 | 描述 |
|------|------|--------|------|------|------|
| timeout | int | 3600 | 否 | 1-86400 | 扫描超时时间(秒) |
| threads | int | 100 | 否 | 1-1000 | 并发线程数 |
| rate_limit | int | 500 | 否 | 1-10000 | 每秒 DNS 请求数限制 |
| wildcard_tests | int | 50 | 否 | 1-1000 | 泛解析检测测试次数 |
| wildcard_batch | int | 1000000 | 否 | 1000-10000000 | 泛解析检测批次大小 |
#### 示例
```yaml
subdomain-bruteforce:
enabled: true
timeout: 86400
threads: 200
rate_limit: 1000
```
## 完整配置示例
```yaml
# 阶段 1: 被动收集(必需)
passive-tools:
subfinder:
enabled: true
timeout: 7200
threads: 20
sublist3r:
enabled: true
timeout: 3600
assetfinder:
enabled: true
# 阶段 2: 字典爆破(可选)
bruteforce:
enabled: false
subdomain-bruteforce:
timeout: 86400
threads: 200
# 阶段 3: 排列组合(可选)
permutation:
enabled: true
subdomain-permutation-resolve:
timeout: 86400
# 阶段 4: DNS 解析验证(可选)
resolve:
enabled: true
subdomain-resolve:
timeout: 86400
```
```
## Server 配置文件增强
在 Server 配置文件中添加注释,引用元数据:
```yaml
# server/configs/engines/subdomain_discovery.yaml
# 子域名发现配置
# 版本: 1.0.0
# 文档: docs/config-reference.md#subdomain_discovery
#
# 扫描流程:
# 1. 被动收集 (passive) - 必需,并行执行
# 2. 字典爆破 (bruteforce) - 可选
# 3. 排列组合 (permutation) - 可选,依赖前面阶段
# 4. DNS 解析验证 (resolve) - 可选,依赖前面阶段
# ============================================================
# 阶段 1: 被动收集 (必需)
# ============================================================
# 使用多个数据源被动收集子域名,不产生主动扫描流量
# 工具并行执行,互不影响
passive-tools:
# Subfinder - 使用多个数据源Shodan、Censys 等)
# 需要 API Keys 以获得最佳效果
subfinder:
enabled: true
timeout: 3600 # 1 小时
# threads: 10 # 可选,默认 10
# Sublist3r - 使用搜索引擎
sublist3r:
enabled: true
timeout: 3600
# Assetfinder - 快速查找
assetfinder:
enabled: true
timeout: 3600
# ============================================================
# 阶段 2: 字典爆破 (可选)
# ============================================================
# 使用字典对域名进行 DNS 爆破
# ⚠️ 警告: 主动扫描会产生大量 DNS 请求
bruteforce:
enabled: false # 默认禁用
subdomain-bruteforce:
timeout: 86400 # 24 小时
wordlist-name: subdomains-top1million-110000.txt
# threads: 100 # 可选,默认 100
# rate_limit: 500 # 可选,默认 500
# ============================================================
# 阶段 3: 排列组合 (可选)
# ============================================================
# 对已发现的子域名进行排列组合
# 依赖: passive, bruteforce
permutation:
enabled: true
subdomain-permutation-resolve:
timeout: 86400
# ============================================================
# 阶段 4: DNS 解析验证 (可选)
# ============================================================
# 验证所有发现的子域名是否可解析
# 依赖: passive, bruteforce, permutation
resolve:
enabled: true
subdomain-resolve:
timeout: 86400
```
## 元数据的用途
### 1. 自动生成文档
从元数据生成:
- 配置参考文档Markdown
- JSON Schema用于验证
- API 文档Swagger/OpenAPI
### 2. 配置验证
```go
// 验证阶段依赖
func ValidateStageDependencies(config map[string]any, metadata Metadata) error {
for _, stage := range metadata.Stages {
if !isStageEnabled(config, stage.ID) {
continue
}
// 检查依赖的阶段是否启用
for _, dep := range stage.DependsOn {
if !isStageEnabled(config, dep) {
return fmt.Errorf("stage %s depends on %s, but %s is not enabled",
stage.ID, dep, dep)
}
}
}
return nil
}
```
### 3. UI 展示
前端可以使用元数据:
- 显示阶段流程图
- 显示工具说明和链接
- 显示参数范围和默认值
- 显示警告信息
### 4. 参数范围验证
```go
// 验证参数范围
func ValidateParameterRange(value int, param Parameter) error {
if param.Min != nil && value < *param.Min {
return fmt.Errorf("value %d is less than minimum %d", value, *param.Min)
}
if param.Max != nil && value > *param.Max {
return fmt.Errorf("value %d is greater than maximum %d", value, *param.Max)
}
return nil
}
```
## 实施建议
### 阶段 1: 添加基础元数据1-2 天)
1. 在 Worker 模板中添加 `metadata``tools.*.metadata` 字段
2. 为每个工具添加基本元数据display_name, description, stage
3. 更新 TemplateLoader 解析元数据
### 阶段 2: 生成文档1-2 天)
1. 更新 DocGenerator 使用元数据生成文档
2. 生成包含阶段流程的文档
3. 生成包含工具说明的文档
### 阶段 3: 配置验证2-3 天)
1. 实现阶段依赖验证
2. 实现参数范围验证
3. 集成到 Server 启动流程
### 阶段 4: UI 集成可选3-5 天)
1. 前端读取元数据
2. 显示阶段流程图
3. 显示工具说明和参数范围
## 总结
添加元数据的好处:
1.**自描述**: 配置文件本身包含足够的信息
2.**自动文档**: 从元数据生成文档,保持同步
3.**更好的验证**: 验证阶段依赖和参数范围
4.**更好的 UI**: 前端可以展示流程图和说明
5.**易于维护**: 元数据集中管理,修改一处即可
需要我更新 design.md 和 tasks.md 来包含元数据功能吗?

File diff suppressed because it is too large Load Diff

View File

@@ -1,190 +0,0 @@
# Requirements Document
## Introduction
本文档定义了 Worker 命令模板系统重构的需求。当前系统存在默认值硬编码、配置重复、缺少验证等问题,导致系统不够生产级、易出错、难维护。重构目标是建立一个清晰、可靠、易维护的命令模板系统。
## Glossary
- **CommandTemplate**: 命令模板结构体,定义工具的基础命令和可选参数
- **CommandBuilder**: 命令构建器,根据模板和配置生成最终的命令字符串
- **TemplateLoader**: 模板加载器,从 YAML 文件加载和缓存命令模板
- **Worker**: 执行扫描任务的临时容器
- **YAML 锚点 (Anchor)**: YAML 语法特性,用于定义可重用的配置片段(`&anchor`
- **YAML 合并键 (Merge Key)**: YAML 语法特性,用于引用和合并锚点定义的配置(`<<: *anchor`
- **默认值 (Default Value)**: 参数未在用户配置中指定时使用的预设值
- **参数覆盖 (Parameter Override)**: 用户配置值覆盖默认值的机制
- **Schema 验证**: 在加载时检查配置结构和类型的正确性
## Requirements
### Requirement 1: 默认值管理
**User Story:** 作为开发者,我希望默认参数值在模板中明确定义,而不是硬编码在命令字符串中,这样我可以轻松查看和修改默认值。
#### Acceptance Criteria
1. WHEN 模板定义包含参数时THEN 系统 SHALL 支持为每个参数指定默认值
2. WHEN 用户配置未提供参数值时THEN 系统 SHALL 使用模板中定义的默认值
3. WHEN 用户配置提供了参数值时THEN 系统 SHALL 使用用户配置值覆盖默认值
4. THE 系统 SHALL 在模板文件中集中管理所有默认值,不在命令字符串中硬编码
5. WHEN 查看模板文件时THEN 开发者 SHALL 能够清晰看到每个参数的默认值
### Requirement 2: 配置复用
**User Story:** 作为开发者,我希望使用 YAML 锚点消除重复配置,这样修改共享配置时只需改一处。
#### Acceptance Criteria
1. THE 系统 SHALL 支持在模板文件中使用 YAML 锚点定义共享配置
2. THE 系统 SHALL 支持使用 YAML 合并键引用共享配置
3. WHEN 多个工具共享相同参数时THEN 系统 SHALL 允许通过锚点定义一次,多处引用
4. WHEN 修改锚点定义的配置时THEN 所有引用该锚点的工具 SHALL 自动继承修改
5. THE 模板文件 SHALL 包含清晰的注释说明锚点的用途和使用方式
### Requirement 3: 参数类型定义
**User Story:** 作为开发者,我希望参数类型明确定义,这样可以避免类型错误。
#### Acceptance Criteria
1. THE 系统 SHALL 为每个参数定义明确的类型string, int, bool
2. WHEN 加载模板时THEN 系统 SHALL 验证参数类型定义的正确性
3. WHEN 构建命令时THEN 系统 SHALL 验证参数值与定义的类型匹配
4. IF 参数类型不匹配THEN 系统 SHALL 返回清晰的错误信息,指明参数名和期望类型
5. THE 系统 SHALL 支持类型转换(如 int 转 string以适配命令行参数
### Requirement 4: 模板验证
**User Story:** 作为开发者,我希望模板加载时自动验证,这样可以在启动时就发现配置错误,而不是运行时。
#### Acceptance Criteria
1. WHEN 系统启动时THEN 系统 SHALL 加载并验证所有命令模板
2. WHEN 模板包含语法错误时THEN 系统 SHALL 拒绝启动并返回详细错误信息
3. WHEN 模板引用未定义的参数时THEN 系统 SHALL 在加载时报错
4. WHEN 模板的默认值类型与参数类型不匹配时THEN 系统 SHALL 在加载时报错
5. WHEN 模板验证失败时THEN 错误信息 SHALL 包含文件名、工具名、参数名和具体错误原因
6. THE 系统 SHALL 在所有模板验证通过后才允许接收扫描任务
### Requirement 5: 错误信息改进
**User Story:** 作为开发者,我希望得到清晰的错误信息,这样可以快速定位问题。
#### Acceptance Criteria
1. WHEN 命令构建失败时THEN 错误信息 SHALL 包含工具名称
2. WHEN 参数缺失时THEN 错误信息 SHALL 明确指出缺失的参数名
3. WHEN 参数类型错误时THEN 错误信息 SHALL 显示期望类型和实际类型
4. WHEN 模板未找到时THEN 错误信息 SHALL 列出所有可用的模板名称
5. WHEN 占位符未替换时THEN 错误信息 SHALL 显示未替换的占位符列表
### Requirement 6: 配置文档化
**User Story:** 作为运维人员,我希望配置文件有清晰的注释和示例,这样我可以理解每个参数的作用。
#### Acceptance Criteria
1. THE 模板文件 SHALL 包含文件级注释,说明整体结构和使用方式
2. THE 模板文件 SHALL 为每个工具添加注释,说明工具用途
3. THE 模板文件 SHALL 为共享配置锚点添加注释,说明适用范围
4. THE 模板文件 SHALL 为每个参数添加注释,说明参数含义、单位和取值范围
5. THE 服务端配置文件 SHALL 包含示例,展示如何覆盖默认值
### Requirement 7: 简化配置
**User Story:** 作为运维人员,我希望只需配置 `enabled: true` 就能使用默认值启动工具,这样可以快速开始使用。
#### Acceptance Criteria
1. WHEN 工具配置只包含 `enabled: true`THEN 系统 SHALL 使用所有参数的默认值
2. WHEN 工具配置省略某个参数时THEN 系统 SHALL 使用该参数的默认值
3. THE 系统 SHALL 允许用户选择性覆盖部分参数,其余参数使用默认值
4. THE 默认值 SHALL 适用于大多数常见场景,无需额外配置即可正常工作
5. THE 文档 SHALL 明确说明哪些参数有默认值,哪些参数必须配置
### Requirement 8: 性能保持
**User Story:** 作为系统架构师,我希望重构不影响性能,这样系统响应速度保持不变。
#### Acceptance Criteria
1. THE 系统 SHALL 继续使用 `sync.Once` 缓存已加载的模板
2. WHEN 多次构建命令时THEN 系统 SHALL 复用缓存的模板,不重复加载
3. THE 模板验证 SHALL 只在启动时执行一次,不在每次命令构建时执行
4. THE 命令构建时间 SHALL 不超过重构前的 110%
5. THE 内存占用 SHALL 不超过重构前的 120%
### Requirement 9: 参数覆盖优先级
**User Story:** 作为开发者,我希望参数覆盖优先级清晰明确,这样可以预测最终使用的参数值。
#### Acceptance Criteria
1. THE 系统 SHALL 按以下优先级应用参数值:用户配置 > 模板默认值
2. WHEN 用户配置和模板默认值都存在时THEN 系统 SHALL 使用用户配置值
3. WHEN 用户配置不存在但模板默认值存在时THEN 系统 SHALL 使用模板默认值
4. WHEN 用户配置和模板默认值都不存在时THEN 系统 SHALL 不添加该参数到命令
5. THE 文档 SHALL 明确说明参数覆盖的优先级规则
### Requirement 10: 模板结构扩展
**User Story:** 作为开发者,我希望模板结构采用业界标准的嵌套格式,这样可以集中管理每个参数的所有属性。
#### Acceptance Criteria
1. THE CommandTemplate 结构体 SHALL 使用 `parameters` 字段map每个参数包含所有属性
2. THE Parameter 结构体 SHALL 包含 `flag`, `default`, `type`, `required`, `description` 字段
3. WHEN 解析 YAML 模板时THEN 系统 SHALL 正确加载嵌套的参数定义
4. THE `default` 字段 SHALL 支持 string、int、bool 类型的值
5. THE `type` 字段 SHALL 使用字符串表示类型("string", "int", "bool"
6. THE 结构 SHALL 符合 GitHub Actions 和 Terraform 的命名规范(使用单数形式)
### Requirement 11: 命令构建逻辑增强
**User Story:** 作为开发者,我希望命令构建器支持从嵌套参数定义中获取值,这样可以自动处理参数覆盖逻辑。
#### Acceptance Criteria
1. WHEN 构建命令时THEN CommandBuilder SHALL 遍历所有参数定义
2. WHEN 用户配置存在时THEN CommandBuilder SHALL 用用户配置覆盖参数默认值
3. WHEN 参数值需要类型转换时THEN CommandBuilder SHALL 自动执行转换
4. WHEN 参数类型不匹配且无法转换时THEN CommandBuilder SHALL 返回类型错误
5. WHEN 参数标记为 Required 但未提供值时THEN CommandBuilder SHALL 返回缺失参数错误
6. THE CommandBuilder SHALL 在构建前验证所有必需的占位符都已提供
### Requirement 12: 解析器支持 YAML 锚点
**User Story:** 作为开发者,我希望 YAML 解析器正确处理锚点和合并键,这样可以使用 YAML 的高级特性。
#### Acceptance Criteria
1. THE 系统 SHALL 使用支持 YAML 1.2 规范的解析器
2. WHEN 模板包含锚点定义时THEN 解析器 SHALL 正确识别和存储锚点
3. WHEN 模板使用合并键引用锚点时THEN 解析器 SHALL 正确合并配置
4. WHEN 锚点和本地配置冲突时THEN 解析器 SHALL 优先使用本地配置
5. IF 引用的锚点不存在THEN 解析器 SHALL 返回明确的错误信息
### Requirement 13: 日志记录增强
**User Story:** 作为运维人员,我希望系统记录详细的日志,这样可以追踪参数来源和命令构建过程。
#### Acceptance Criteria
1. WHEN 加载模板时THEN 系统 SHALL 记录加载的模板数量和文件路径
2. WHEN 应用默认值时THEN 系统 SHALL 记录使用默认值的参数名和值
3. WHEN 用户配置覆盖默认值时THEN 系统 SHALL 记录被覆盖的参数名和新旧值
4. WHEN 构建命令成功时THEN 系统 SHALL 记录最终的命令字符串
5. WHEN 构建命令失败时THEN 系统 SHALL 记录详细的错误堆栈和上下文信息
### Requirement 14: 测试覆盖
**User Story:** 作为开发者,我希望重构后的代码有完整的测试覆盖,这样可以确保功能正确性。
#### Acceptance Criteria
1. THE 系统 SHALL 为 CommandTemplate 结构体编写单元测试
2. THE 系统 SHALL 为 CommandBuilder 编写单元测试,覆盖默认值合并逻辑
3. THE 系统 SHALL 为 TemplateLoader 编写单元测试,覆盖验证逻辑
4. THE 系统 SHALL 编写集成测试,验证完整的命令构建流程
5. THE 测试 SHALL 覆盖边界情况:空配置、类型错误、缺失参数等

View File

@@ -1,958 +0,0 @@
# Tasks
## 阶段 1: 更新数据结构
### 1.1 更新 CommandTemplate 结构体
更新 `worker/internal/activity/command_template.go`,将扁平结构改为嵌套结构,使用 Go Template 语法,并添加元数据支持:
```go
type CommandTemplate struct {
Metadata ToolMetadata `yaml:"metadata"`
BaseCommand string `yaml:"base_command"` // 使用 {{.Var}} 占位符
Parameters map[string]Parameter `yaml:"parameters"`
}
type Parameter struct {
Flag string `yaml:"flag"` // 使用 {{.Var}} 占位符
Default interface{} `yaml:"default"`
Type string `yaml:"type"` // "string", "int", "bool"
Required bool `yaml:"required"`
Description string `yaml:"description"`
DeprecationMessage string `yaml:"deprecation_message,omitempty"`
}
type ToolMetadata struct {
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
Stage string `yaml:"stage"`
Category string `yaml:"category"`
Homepage string `yaml:"homepage"`
RequiresAPIKeys bool `yaml:"requires_api_keys"`
APIProviders []string `yaml:"api_providers,omitempty"`
Warning string `yaml:"warning,omitempty"`
DependsOnStages []string `yaml:"depends_on_stages,omitempty"`
}
type WorkflowMetadata struct {
Name string `yaml:"name"`
DisplayName string `yaml:"display_name"`
Description string `yaml:"description"`
Version string `yaml:"version"`
TargetTypes []string `yaml:"target_types"`
Stages []StageMetadata `yaml:"stages"`
}
type StageMetadata struct {
ID string `yaml:"id"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Order int `yaml:"order"`
Required bool `yaml:"required"`
Parallel bool `yaml:"parallel"`
DependsOn []string `yaml:"depends_on"`
Outputs []string `yaml:"outputs"`
}
```
**验收标准**:
- 结构体定义符合业界标准GitHub Actions/Terraform 风格)
- 支持所有必需字段flag, default, type, required, description
- 支持可选字段deprecation_message
- 支持工具元数据display_name, description, stage, homepage, warning 等)
- 支持 Workflow 元数据stages, version, target_types
- 注释说明使用 Go Template 语法
- 编译通过
### 1.2 更新 CommandBuilder
更新 `worker/internal/activity/command_builder.go`,使用 Go Template 替代字符串替换:
**需要实现**:
1. 创建 `funcMap` 提供模板函数quote, default, lower, upper, join
2. 合并必需参数和可选参数到一个 data map
3. 应用参数覆盖逻辑(用户配置 > 默认值)
4. 构建完整的命令模板字符串base_command + 启用的参数 flags
5. 使用 `text/template` 解析和执行模板
6. 配置 `missingkey=error` 自动检测缺失字段
7. 返回最终命令
**代码示例**:
```go
type CommandBuilder struct {
funcMap template.FuncMap
}
func NewCommandBuilder() *CommandBuilder {
return &CommandBuilder{
funcMap: template.FuncMap{
"quote": func(s string) string {
return fmt.Sprintf("%q", s)
},
"default": func(def, val interface{}) interface{} {
if val == nil || val == "" {
return def
}
return val
},
},
}
}
func (b *CommandBuilder) Build(
tmpl CommandTemplate,
params map[string]any,
config map[string]any,
) (string, error) {
// 1. 合并数据
data := mergeParameters(tmpl, params, config)
// 2. 构建完整模板
cmdTemplate := buildCommandTemplate(tmpl, data)
// 3. 执行 Go Template
t, err := template.New("command").
Funcs(b.funcMap).
Option("missingkey=error").
Parse(cmdTemplate)
if err != nil {
return "", fmt.Errorf("parse template: %w", err)
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return "", fmt.Errorf("execute template: %w", err)
}
return buf.String(), nil
}
```
**验收标准**:
- `Build()` 方法使用 Go Template
- 参数覆盖逻辑正确(用户配置 > 默认值)
- 自动检测缺失字段(`missingkey=error`
- 提供常用模板函数quote, default 等)
- 错误信息清晰(包含工具名、字段名、位置信息)
- 代码量约 50 行(比自定义替换少 64%
- 编译通过
### 1.3 编写单元测试
为新的数据结构和 Go Template 实现编写单元测试:
**测试用例**:
1. 解析嵌套参数定义
2. 解析工具元数据display_name, stage, homepage 等)
3. 解析 Workflow 元数据stages, version
4. 参数默认值应用
5. 用户配置覆盖默认值
6. Go Template 占位符替换(`{{.Domain}}`
7. Go Template 函数调用(`{{quote .OutputFile}}`
8. Go Template 条件渲染(`{{if .Verbose}}-v{{end}}`
9. 必需参数验证(自动检测缺失字段)
10. 可选参数处理(无默认值时不添加)
11. 参数范围验证min/max 约束)
12. 错误信息格式Go Template 提供详细位置)
13. snake_case 到 PascalCase 转换
**验收标准**:
- 所有测试通过
- 覆盖率 > 90%
- 测试用例清晰易懂
- 验证 Go Template 自动错误检测
- 验证元数据解析正确
## 阶段 2: 重构模板文件
### 2.1 重构 templates.yaml
重构 `worker/internal/workflow/subdomain_discovery/templates.yaml`,使用 Go Template 语法、YAML 锚点和元数据:
**需要实现**:
1. 添加 Workflow 元数据name, version, stages
2. 为每个工具添加元数据display_name, description, stage, homepage, warning
3. 定义共享参数锚点(`x-common-params`
4. 将所有工具模板改为 Go Template 语法(`{{.Var}}`
5. 使用 `<<:` 合并键引用共享参数
6. 添加参数描述和类型定义
7. 添加参数范围约束min/max
8. 移除硬编码的默认值到参数定义中
9. 使用 PascalCase 命名占位符
**示例**:
```yaml
# Workflow 元数据
metadata:
name: "subdomain_discovery"
display_name: "子域名发现"
description: "通过被动收集、字典爆破、排列组合等方式发现目标域名的所有子域名"
version: "1.0.0"
target_types: ["domain"]
stages:
- id: "passive"
name: "被动收集"
description: "使用多个数据源被动收集子域名"
order: 1
required: true
parallel: true
depends_on: []
outputs: ["subdomains"]
# 共享参数
x-common-params: &common-params
Timeout:
flag: "-timeout {{.Timeout}}"
default: 3600
type: "int"
required: false
description: "扫描超时时间(秒)"
# 工具模板
tools:
subfinder:
metadata:
display_name: "Subfinder"
description: "使用多个数据源被动收集子域名"
stage: "passive"
category: "passive_collection"
homepage: "https://github.com/projectdiscovery/subfinder"
requires_api_keys: true
api_providers: ["shodan", "censys", "virustotal"]
base_command: "subfinder -d {{.Domain}} -all -o {{quote .OutputFile}} -v"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 10
type: "int"
required: false
description: "并发线程数"
subdomain-bruteforce:
metadata:
display_name: "Subdomain Bruteforce"
description: "使用字典对域名进行 DNS 爆破"
stage: "bruteforce"
category: "active_scan"
homepage: "https://github.com/d3mondev/puredns"
requires_api_keys: false
warning: "主动扫描会产生大量 DNS 请求,可能被目标检测"
base_command: "puredns bruteforce {{quote .Wordlist}} {{.Domain}} -r {{quote .Resolvers}} --write {{quote .OutputFile}} --quiet"
parameters:
<<: *common-params
Threads:
flag: "-t {{.Threads}}"
default: 100
type: "int"
required: false
description: "并发线程数"
```
**验收标准**:
- 所有工具模板使用 Go Template 语法
- 占位符使用 PascalCase`{{.Domain}}`, `{{.OutputFile}}`
- 共享参数使用 YAML 锚点定义
- 所有参数有明确的类型和描述
- 所有工具有完整的元数据
- Workflow 有完整的元数据stages 定义)
- 默认值从命令字符串移到参数定义
- 使用 `{{quote}}` 函数处理文件路径
- 使用 `{{if}}` 处理布尔标志
- YAML 语法正确
### 2.2 更新 TemplateLoader
更新 `worker/internal/activity/template_loader.go`支持新的模板格式、Go Template 验证和元数据:
**需要实现**:
1. 解析 Workflow 元数据metadata 字段)
2. 解析嵌套参数结构(包含工具元数据)
3. 正确处理 YAML 锚点和合并键
4. 验证参数类型定义(只允许 string, int, bool
5. 验证默认值类型与参数类型匹配
6. 检查必需参数是否有默认值(逻辑冲突)
7. **验证 Go Template 语法**(尝试解析所有模板字符串)
8. **验证元数据完整性**必需字段、stage 引用)
9. **验证参数范围约束**min <= default <= max
10. **提供阶段依赖验证方法**
**Go Template 验证示例**:
```go
func (l *TemplateLoader) Validate() error {
// 验证元数据
if err := l.validateMetadata(); err != nil {
return err
}
// 验证工具模板
for toolName, tmpl := range l.templates {
// 验证 base_command
if _, err := template.New("test").Parse(tmpl.BaseCommand); err != nil {
return fmt.Errorf("tool %s: invalid base_command template: %w", toolName, err)
}
// 验证每个参数的 flag
for paramName, param := range tmpl.Parameters {
if param.Flag != "" {
if _, err := template.New("test").Parse(param.Flag); err != nil {
return fmt.Errorf("tool %s, parameter %s: invalid flag template: %w",
toolName, paramName, err)
}
}
// 验证参数范围
if err := validateParameterRange(param); err != nil {
return fmt.Errorf("tool %s, parameter %s: %w", toolName, paramName, err)
}
}
// 验证工具元数据
if err := l.validateToolMetadata(toolName, tmpl.Metadata); err != nil {
return err
}
}
return nil
}
func (l *TemplateLoader) validateMetadata() error {
if l.metadata.Name == "" {
return fmt.Errorf("workflow metadata: name is required")
}
if l.metadata.Version == "" {
return fmt.Errorf("workflow metadata: version is required")
}
// 验证 stages 定义...
return nil
}
func (l *TemplateLoader) validateToolMetadata(toolName string, meta ToolMetadata) error {
if meta.DisplayName == "" {
return fmt.Errorf("tool %s: display_name is required", toolName)
}
if meta.Stage == "" {
return fmt.Errorf("tool %s: stage is required", toolName)
}
// 验证 stage 引用的阶段存在
if !l.stageExists(meta.Stage) {
return fmt.Errorf("tool %s: stage %s not defined in workflow metadata", toolName, meta.Stage)
}
return nil
}
func validateParameterRange(param Parameter) error {
// 移除范围验证 - 由工具自己验证
return nil
}
```
**验收标准**:
- 正确解析嵌套结构
- 正确解析 Workflow 元数据
- 正确解析工具元数据
- YAML 锚点正确合并
- 验证逻辑完整
- **Go Template 语法验证**(启动时检测模板错误)
- **元数据完整性验证**必需字段、stage 引用)
- 错误信息清晰
- 编译通过
### 2.3 更新调用点
更新所有调用 `buildCommand()` 的地方,适配 Go Template 和新的参数格式:
**文件列表**:
- `worker/internal/workflow/subdomain_discovery/stage_passive.go`
- `worker/internal/workflow/subdomain_discovery/stage_bruteforce.go`
- `worker/internal/workflow/subdomain_discovery/stage_merge.go`
- 其他使用命令构建的文件
**需要修改**:
1. 参数类型从 `map[string]string` 改为 `map[string]any`
2. 参数名从 snake_case 改为 PascalCase或添加转换函数
3. 传递正确的必需参数Domain, OutputFile 等)
**示例**:
```go
// 旧代码
params := map[string]string{
"domain": domain,
"output-file": outputFile,
}
config := map[string]any{
"timeout": 3600,
"threads": 10,
}
// 新代码
params := map[string]any{
"Domain": domain,
"OutputFile": outputFile,
}
config := convertKeys(map[string]any{ // snake_case -> PascalCase
"timeout": 3600,
"threads": 10,
})
```
**验收标准**:
- 所有调用点更新
- 参数名使用 PascalCase
- 传递正确的参数类型
- 编译通过
### 2.4 编写集成测试
编写集成测试验证完整流程:
**测试场景**:
1. 加载模板 → 构建命令 → 验证命令字符串
2. 使用默认值构建命令
3. 用户配置覆盖默认值
4. YAML 锚点正确合并
5. Go Template 占位符正确替换
6. Go Template 函数正确调用quote
7. Go Template 条件正确渲染if
8. 错误场景缺少必需参数、Go Template 语法错误等)
9. snake_case 到 PascalCase 自动转换
**验收标准**:
- 所有集成测试通过
- 覆盖主要使用场景
- 错误场景正确处理
- 验证 Go Template 自动错误检测
## 阶段 3: 实现 Schema 生成
### 3.1 创建 SchemaGenerator
创建 `worker/cmd/schema-gen/main.go`,从 Worker 模板生成 JSON Schema
**需要实现**:
1. 读取 templates.yaml 文件
2. 解析为 CommandTemplate 结构(包含元数据)
3. 遍历所有工具和参数
4. 生成 JSON Schema 结构:
- 类型映射string → string, int → integer, bool → boolean
- Required 参数添加到 required 数组
- Description 映射到 description 字段
- Default 映射到 default 字段
- **使用工具元数据生成 x-stage, x-homepage, x-warning 等扩展字段**
- **使用 Workflow 元数据生成 title, description, x-metadata 字段**
5. 输出 JSON 文件
**验收标准**:
- 生成的 Schema 符合 JSON Schema Draft 7 规范
- 所有参数正确映射
- 类型、默认值、描述完整
- 工具元数据映射到扩展字段
- Workflow 元数据映射到顶层字段
- JSON 格式正确
### 3.2 配置 go generate
`worker/internal/workflow/subdomain_discovery/template_loader.go` 添加:
```go
//go:generate go run ../../cmd/schema-gen/main.go -input templates.yaml -output ../../../server/configs/engines/subdomain_discovery.schema.json
```
**验收标准**:
- `go generate` 命令正确执行
- Schema 文件生成到正确位置
- 生成的 Schema 有效
### 3.3 编写单元测试
为 SchemaGenerator 编写单元测试:
**测试用例**:
1. 简单工具的 Schema 生成
2. 包含所有类型参数的 Schema 生成
3. 必需参数映射到 required 数组
4. 默认值正确映射
5. 描述正确映射
6. JSON 格式验证
**验收标准**:
- 所有测试通过
- 生成的 Schema 可以被 JSON Schema 验证器加载
## 阶段 4: 实现文档生成
### 4.1 创建 DocGenerator
创建 `worker/cmd/doc-gen/main.go`,从 Worker 模板生成配置文档:
**需要实现**:
1. 读取 templates.yaml 文件
2. 解析为 CommandTemplate 结构(包含元数据)
3. 生成 Markdown 文档:
- **使用 Workflow 元数据生成概述章节**(名称、版本、描述)
- **使用 Stage 元数据生成扫描流程章节**(阶段顺序、依赖关系)
- 每个工具一个章节,**使用工具元数据**display_name, description, stage, homepage
- 参数表格(名称、类型、默认值、必需、描述)
- 标记废弃的参数
- **显示警告信息**(如主动扫描警告)
- **显示 API Keys 需求**
- 包含使用示例
4. 输出 Markdown 文件
**验收标准**:
- 生成的文档格式正确
- 包含 Workflow 概述章节
- 包含扫描流程章节(阶段顺序、依赖)
- 表格包含所有必需列
- 显示工具元数据(阶段、主页、警告)
- 示例代码可用
- Markdown 语法正确
### 4.2 配置 go generate
`worker/internal/workflow/subdomain_discovery/template_loader.go` 添加:
```go
//go:generate go run ../../cmd/doc-gen/main.go -input templates.yaml -output ../../../docs/config-reference.md
```
**验收标准**:
- `go generate` 命令正确执行
- 文档文件生成到正确位置
- 生成的文档可读
### 4.3 编写单元测试
为 DocGenerator 编写单元测试:
**测试用例**:
1. 简单工具的文档生成
2. 包含所有字段的文档生成
3. 表格格式正确
4. 示例代码格式正确
5. Markdown 语法验证
**验收标准**:
- 所有测试通过
- 生成的文档可以被 Markdown 渲染器正确显示
## 阶段 5: 实现 Server 端验证
### 5.1 创建 ConfigValidator
创建 `server/internal/config/validator.go`,实现配置验证器:
**需要实现**:
1. `LoadSchema(schemaPath, metadataPath string) error` - 加载 JSON Schema 和元数据
2. `Validate(config map[string]interface{}) []error` - 验证用户配置
3. `ValidateStageDependencies(config map[string]interface{}) []error` - 验证阶段依赖
4. 使用 JSON Schema 验证库(如 `github.com/xeipuuv/gojsonschema`
5. 返回详细的验证错误列表
**阶段依赖验证示例**:
```go
func (v *ConfigValidator) ValidateStageDependencies(config map[string]interface{}) []error {
var errors []error
// 获取所有启用的工具及其所属阶段
enabledStages := make(map[string]bool)
for toolName, toolConfig := range config {
if enabled, ok := toolConfig.(map[string]interface{})["enabled"].(bool); ok && enabled {
// 从元数据获取工具所属阶段
stage := v.getToolStage(toolName)
if stage != "" {
enabledStages[stage] = true
}
}
}
// 验证每个启用的阶段的依赖
for stage := range enabledStages {
stageMeta := v.getStageMetadata(stage)
for _, dep := range stageMeta.DependsOn {
if !enabledStages[dep] {
errors = append(errors, fmt.Errorf(
"stage %s depends on %s, but no tools in stage %s are enabled",
stage, dep, dep))
}
}
}
return errors
}
```
**验收标准**:
- 正确加载 JSON Schema
- 正确加载元数据
- 验证逻辑正确
- **阶段依赖验证正确**
- 错误信息清晰(包含字段名、期望类型、实际值)
- 编译通过
### 5.2 集成到 Server 启动流程
在 Server 启动代码中集成配置验证:
**需要实现**:
1. 在 Server 启动时加载 Schema
2. 验证用户配置
3. 验证失败时拒绝启动
4. 记录详细的验证错误
**示例**:
```go
validator := config.NewValidator()
if err := validator.LoadSchema("configs/engines/subdomain_discovery.schema.json"); err != nil {
log.Fatal(err)
}
if errs := validator.Validate(engineConfig); len(errs) > 0 {
for _, err := range errs {
log.Error(err)
}
log.Fatal("Configuration validation failed")
}
```
**验收标准**:
- Server 启动时正确验证配置
- 验证失败时拒绝启动
- 错误信息清晰
- 不影响启动性能
### 5.3 编写集成测试
编写集成测试验证验证流程:
**测试场景**:
1. 有效配置验证通过
2. 类型不匹配的配置验证失败
3. 缺少必需字段的配置验证失败
4. 包含未知字段的配置验证失败
5. **阶段依赖验证**:启用的阶段依赖未启用的阶段(应失败)
6. **阶段依赖验证**:所有依赖都启用(应通过)
**验收标准**:
- 所有集成测试通过
- 覆盖主要验证场景
- 覆盖阶段依赖验证场景
- 错误信息清晰
## 阶段 6: 迁移现有工具
### 6.1 迁移被动收集工具
迁移以下工具到新格式:
- subfinder
- sublist3r
- assetfinder
- amass
**验收标准**:
- 所有工具模板使用嵌套结构
- 参数定义完整(类型、默认值、描述)
- 命令构建正确
- 测试通过
### 6.2 迁移主动扫描工具
迁移以下工具到新格式:
- subdomain-bruteforce
- subdomain-resolve
- subdomain-permutation-resolve
**验收标准**:
- 所有工具模板使用嵌套结构
- 参数定义完整
- 命令构建正确
- 测试通过
### 6.3 迁移其他工具
迁移其他扫描工具(如 httpx, nuclei 等)到新格式。
**验收标准**:
- 所有工具模板使用嵌套结构
- 参数定义完整
- 命令构建正确
- 测试通过
### 6.4 更新用户配置示例
更新 `server/configs/engines/subdomain_discovery.yaml` 中的示例和注释:
**需要更新**:
1. 添加参数说明注释
2. 提供完整的配置示例
3. 说明默认值
4. 说明可选参数
**验收标准**:
- 示例配置清晰易懂
- 注释完整
- 用户可以快速上手
### 6.5 更新文档
更新以下文档:
- `docs/config-reference.md` - 配置参考(自动生成)
- `README.md` - 项目说明
- 其他相关文档
**验收标准**:
- 文档完整准确
- 示例代码可用
- 说明清晰
### 6.6 运行完整测试
运行所有测试验证迁移正确:
**测试类型**:
1. 单元测试
2. 集成测试
3. 属性测试
4. 端到端测试
**验收标准**:
- 所有测试通过
- 覆盖率 > 90%
- 性能无明显下降
## 阶段 7: 清理和优化
### 7.1 清理旧代码
删除不再使用的旧代码:
**需要清理**:
1. 旧的命令构建逻辑(如果有)
2. 硬编码的默认值
3. 废弃的辅助函数
4. 临时的兼容代码
**验收标准**:
- 代码整洁
- 无死代码
- 编译通过
### 7.2 性能优化
优化系统性能:
**优化点**:
1. 模板加载性能(使用 sync.Once
2. Schema 验证性能(缓存 Schema
3. 命令构建性能(减少内存分配)
4. 日志性能(异步日志)
**验收标准**:
- 模板加载时间 < 100ms
- Schema 验证时间 < 10ms
- 命令构建时间 < 1ms
- 内存占用不超过重构前的 120%
### 7.3 代码审查
进行代码审查:
**审查内容**:
1. 代码风格一致性
2. 错误处理完整性
3. 日志记录合理性
4. 注释清晰度
5. 测试覆盖率
**验收标准**:
- 代码符合 Go 规范
- 错误处理完整
- 日志合理
- 注释清晰
- 测试覆盖率 > 90%
### 7.4 更新文档
更新所有相关文档:
**文档列表**:
1. 开发者文档(架构、设计、实现)
2. 用户文档(配置、使用、故障排查)
3. API 文档(接口、数据结构)
4. 迁移指南
**验收标准**:
- 文档完整准确
- 示例代码可用
- 说明清晰
- 格式统一
### 7.5 发布准备
准备发布新版本:
**准备工作**:
1. 更新 CHANGELOG
2. 更新版本号
3. 打 tag
4. 构建 Docker 镜像
5. 更新部署文档
**验收标准**:
- CHANGELOG 完整
- 版本号正确
- Docker 镜像可用
- 部署文档准确
## 属性测试任务
### Property 1-2: 默认值和覆盖
编写属性测试验证默认值应用和覆盖逻辑:
**测试策略**:
- 生成随机参数定义和用户配置
- 验证默认值应用正确
- 验证用户配置覆盖默认值
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 1: 参数默认值支持`
### Property 3: YAML 锚点
编写属性测试验证 YAML 锚点解析:
**测试策略**:
- 生成包含锚点的随机 YAML
- 验证解析和合并正确性
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 3: YAML 锚点正确解析`
### Property 4-5: 类型验证和转换
编写属性测试验证类型检查和转换:
**测试策略**:
- 生成随机类型的参数值
- 验证类型检查正确
- 验证类型转换正确
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 4: 参数类型验证`
### Property 6: 模板验证
编写属性测试验证模板验证逻辑:
**测试策略**:
- 生成包含各种错误的模板
- 验证所有错误都能检测
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 6: 模板验证综合`
### Property 7: 错误信息
编写属性测试验证错误信息完整性:
**测试策略**:
- 触发各种错误场景
- 验证错误信息包含必要上下文
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 7: 错误信息完整性`
### Property 8: 缓存
编写属性测试验证模板缓存:
**测试策略**:
- 多次构建命令
- 验证模板只加载一次
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 8: 模板缓存复用`
### Property 9-14: 边界情况
编写属性测试验证边界情况:
**测试场景**:
- 可选参数处理
- 嵌套参数解析
- 必需参数验证
- YAML 锚点边界情况
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 9-14`
### Property 15-19: 新增功能
编写属性测试验证新增功能:
**测试场景**:
- Schema 生成正确性(包含元数据)
- 文档生成完整性(包含阶段流程和工具元数据)
- Server 配置验证
- 阶段依赖验证
- 元数据完整性验证
**验收标准**:
- 最小 100 次迭代
- 所有测试通过
- 标签格式:`Feature: worker-command-template-refactor, Property 15-19`
## 总结
**预计时间**: 2-3 周(比原计划减少 1 周,因为 Go Template 代码量更少)
**关键里程碑**:
1. 阶段 1-2 完成新的数据结构、Go Template 实现和元数据支持可用
2. 阶段 3-4 完成Schema 和文档自动生成(使用元数据)
3. 阶段 5 完成Server 端验证集成(包含阶段依赖验证)
4. 阶段 6 完成:所有工具迁移完成
5. 阶段 7 完成:代码清理和优化,准备发布
**Go Template 方案优势**:
1. **代码量更少**: ~50 行 vs ~140 行(减少 64%
2. **自动错误检测**: `missingkey=error` 自动检测缺失字段
3. **更强大**: 支持条件、循环、函数等高级特性
4. **业界标准**: Helm、Kubernetes 都使用 Go Template
5. **更好的错误信息**: 提供详细的行号、列号、字段名
6. **减少维护成本**: 不需要自己实现替换、验证、类型转换逻辑
**元数据功能优势**:
1. **自描述**: 配置文件本身包含足够的信息(阶段流程、工具说明)
2. **自动文档**: 从元数据生成文档,保持同步
3. **更好的验证**: 验证阶段依赖和参数范围
4. **更好的 UI**: 前端可以展示流程图和说明
5. **易于维护**: 元数据集中管理,修改一处即可
6. **用户友好**: 清晰的警告信息、API Keys 需求说明
**风险和缓解**:
1. **风险**: Go Template 语法学习曲线
- **缓解**: 提供详细的文档和示例,语法简单(主要是 `{{.Var}}`
2. **风险**: YAML 锚点解析复杂
- **缓解**: 使用成熟的 YAML 库gopkg.in/yaml.v3
3. **风险**: 元数据维护成本
- **缓解**: 元数据验证确保完整性,自动生成文档减少手动维护
4. **风险**: 性能下降
- **缓解**: 使用缓存Go Template 性能优秀
5. **风险**: 文档不同步
- **缓解**: 自动生成文档,集成到 CI/CD

View File

@@ -1,130 +0,0 @@
---
inclusion: fileMatch
fileMatchPattern: "frontend/**/*.{ts,tsx}"
---
# Frontend API Conventions
## API Client Usage
### ALWAYS Use `api` Client
```typescript
// ✅ CORRECT - Uses api client, automatically adds JWT token
import { api } from '@/lib/api-client'
const response = await api.get<WebsiteListResponse>('/targets/${id}/websites/', { params })
const response = await api.post('/websites/bulk-delete/', { ids })
// ❌ WRONG - Native fetch doesn't add JWT token, will get 401
const response = await fetch('/api/targets/${id}/websites/')
```
### Why This Matters
The `api` client (axios instance) automatically:
1. Adds `Authorization: Bearer <token>` header
2. Handles token refresh on 401 errors
3. Logs requests/responses in development
4. Provides consistent error handling
## JWT Token Auto-Refresh
The API client automatically refreshes tokens:
1. Request returns 401 (token expired)
2. Client uses Refresh Token to get new Access Token
3. Original request is retried with new token
4. Multiple concurrent 401s are queued and retried together
Token expiration:
- Access Token: 15 minutes
- Refresh Token: 7 days
Users only need to re-login if Refresh Token expires (after 7 days of inactivity).
## Service Layer Pattern
### Organize API Calls in Services
```typescript
// ✅ Good - Centralized in service file
// frontend/services/website.service.ts
export class WebsiteService {
static async bulkDelete(ids: number[]) {
const response = await api.post('/websites/bulk-delete/', { ids })
return response.data
}
}
// ❌ Avoid - Scattered API calls in components
// frontend/components/SomeComponent.tsx
const response = await api.post('/websites/bulk-delete/', { ids })
```
### Hooks for React Query
```typescript
// frontend/hooks/use-websites.ts
export function useBulkDeleteWebsites() {
return useMutation({
mutationFn: (ids: number[]) => WebsiteService.bulkDelete(ids),
// ... toast messages, cache invalidation
})
}
```
## API Path Conventions
### Match Backend Routes Exactly
```typescript
// Backend route: GET /api/targets/:id/websites
// Frontend call:
api.get(`/targets/${id}/websites/`)
// Backend route: POST /api/websites/bulk-delete
// Frontend call:
api.post('/websites/bulk-delete/', { ids })
```
### Trailing Slash
Always include trailing slash to match Next.js rewrite rules:
```typescript
// ✅ With trailing slash
api.get('/targets/')
api.post('/websites/bulk-delete/')
// ❌ Without trailing slash (may cause redirect issues)
api.get('/targets')
api.post('/websites/bulk-delete')
```
## Blob Downloads (CSV/Excel Export)
### Handle Error Responses in Blob Mode
When using `responseType: 'blob'`, error responses are also returned as Blob:
```typescript
static async exportByTargetId(targetId: number): Promise<Blob> {
const response = await api.get<Blob>(`/targets/${targetId}/websites/export/`, {
responseType: 'blob',
})
// Check if response is actually an error (JSON instead of CSV)
if (response.data.type === 'application/json') {
const text = await response.data.text()
const error = JSON.parse(text)
throw new Error(error.error?.message || 'Export failed')
}
return response.data
}
```

View File

@@ -1,788 +0,0 @@
---
inclusion: fileMatch
fileMatchPattern: "go-backend/**/*.go"
---
# Go Backend Code Conventions
## Language Requirements
All code in `go-backend/` must be written in English:
- Comments, error messages, log messages, API responses
- Variable names and function names
## Project Structure
```
go-backend/
├── cmd/server/main.go # Application entrypoint
├── internal/
│ ├── config/ # Configuration loading
│ ├── database/ # Database connection
│ ├── model/ # Domain models (GORM)
│ ├── dto/ # Request/Response DTOs
│ ├── repository/ # Data access layer
│ ├── service/ # Business logic layer
│ ├── handler/ # HTTP handlers
│ ├── middleware/ # HTTP middleware
│ ├── auth/ # Authentication (JWT)
│ └── pkg/ # Internal utilities
├── go.mod
└── Makefile
```
## File Naming
Use concise names without redundant suffixes:
```
✅ handler/target.go
✅ service/target.go
✅ repository/target.go
❌ handler/target_handler.go
❌ service/target_service.go
```
## Architecture Layers
```
HTTP Request → Handler → Service → Repository → Database
↓ ↓ ↓
DTO Business GORM Model
Logic
```
- **Handler**: Parse request, validate input, call service, return response
- **Service**: Business logic, orchestrate repositories, no HTTP awareness
- **Repository**: Database operations only, return models
## Error Handling
```go
// ✅ Wrap errors with context
if err != nil {
return fmt.Errorf("failed to create target: %w", err)
}
// ✅ Define domain errors in service layer
var (
ErrTargetNotFound = errors.New("target not found")
ErrTargetExists = errors.New("target already exists")
)
// ✅ Check specific errors
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrTargetNotFound
}
```
## Interface Design
```go
// ✅ Define interfaces where they are used (service layer)
type TargetRepository interface {
Create(target *model.Target) error
FindByID(id int) (*model.Target, error)
}
// ✅ Accept interfaces, return structs
func NewTargetService(repo TargetRepository) *TargetService {
return &TargetService{repo: repo}
}
```
## Context Usage
```go
// ✅ Pass context as first parameter
func (r *TargetRepository) FindByID(ctx context.Context, id int) (*model.Target, error) {
var target model.Target
err := r.db.WithContext(ctx).First(&target, id).Error
return &target, err
}
// ✅ Use context for cancellation and timeouts
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
```
## Dependency Injection
```go
// ✅ Constructor injection
func NewTargetService(repo *TargetRepository, orgRepo *OrganizationRepository) *TargetService {
return &TargetService{
repo: repo,
orgRepo: orgRepo,
}
}
// ❌ Avoid global state
var globalDB *gorm.DB // Don't do this
```
## Testing
```go
// ✅ Table-driven tests
func TestDetectTargetType(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"domain", "example.com", "domain"},
{"ip", "192.168.1.1", "ip"},
{"cidr", "10.0.0.0/8", "cidr"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := detectTargetType(tt.input)
if result != tt.expected {
t.Errorf("got %s, want %s", result, tt.expected)
}
})
}
}
```
## GORM Model Conventions
```go
type Target struct {
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"column:name;uniqueIndex" json:"name"`
Type string `gorm:"column:type" json:"type"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"createdAt"`
DeletedAt *time.Time `gorm:"column:deleted_at;index" json:"deletedAt,omitempty"`
}
// Use singular table names
func (Target) TableName() string {
return "target"
}
```
## HTTP Handler Pattern
```go
func (h *TargetHandler) Create(c *gin.Context) {
// 1. Parse and validate request
var req dto.CreateTargetRequest
if err := c.ShouldBindJSON(&req); err != nil {
dto.BadRequest(c, "invalid request body")
return
}
// 2. Call service
target, err := h.svc.Create(&req)
if err != nil {
// 3. Handle domain errors
if errors.Is(err, service.ErrTargetExists) {
dto.Conflict(c, err.Error())
return
}
dto.InternalError(c, "failed to create target")
return
}
// 4. Return response
dto.Created(c, target)
}
```
## Concurrency Safety
```go
// ✅ Use channels for communication
results := make(chan Result, len(items))
for _, item := range items {
go func(item Item) {
results <- process(item)
}(item)
}
// ✅ Use sync primitives when needed
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
// ✅ Always close resources
defer rows.Close()
defer resp.Body.Close()
```
## Logging
```go
// ✅ Use structured logging (zap)
pkg.Info("target created",
zap.Int("id", target.ID),
zap.String("name", target.Name),
)
// ✅ Log errors with context
pkg.Error("failed to create target",
zap.Error(err),
zap.String("name", req.Name),
)
```
## Key Principles
1. **Simplicity** - Write clear, straightforward code
2. **Explicit** - No magic, explicit dependencies and error handling
3. **Testable** - Design for easy unit testing with interfaces
4. **Layered** - Clear separation between handler/service/repository
5. **Idiomatic** - Follow Go conventions and standard library patterns
## API Response Format (Industry Standard)
Follow Stripe/GitHub style - return data directly without wrapper.
### Success Response
```go
// Single resource - return directly
{"id": 1, "name": "example.com", "type": "domain"}
// List/Operation result - return directly
{"count": 3, "items": [...]}
// Use dto helpers
dto.OK(c, target) // 200 - returns data directly
dto.Created(c, target) // 201 - returns data directly
dto.NoContent(c) // 204 - no body
```
### Error Response
```go
// Error format with code for i18n
{
"error": {
"code": "NOT_FOUND",
"message": "target not found" // Debug info, not for display
}
}
// With field-level details
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{"field": "name", "message": "required"}
]
}
}
// Use dto helpers
dto.BadRequest(c, "message") // 400
dto.Unauthorized(c, "message") // 401
dto.NotFound(c, "message") // 404
dto.Conflict(c, "message") // 409
dto.InternalError(c, "message") // 500
dto.ValidationError(c, details) // 400 with field details
```
## Pagination
### Query Parameters
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| page | int | 1 | Page number (1-based) |
| pageSize | int | 20 | Items per page (max 100) |
### Paginated Response Format
```go
// Matches Python format for frontend compatibility
{
"results": [...], // Data array
"total": 100, // Total count
"page": 1, // Current page
"pageSize": 20, // Items per page
"totalPages": 5 // Total pages
}
```
### Usage
```go
// In handler
func (h *TargetHandler) List(c *gin.Context) {
var query dto.TargetListQuery
if err := c.ShouldBindQuery(&query); err != nil {
dto.BadRequest(c, "invalid query parameters")
return
}
targets, total, err := h.svc.List(&query)
if err != nil {
dto.InternalError(c, "failed to list targets")
return
}
// Use generic Paginated helper
dto.Paginated(c, targets, total, query.GetPage(), query.GetPageSize())
}
```
### Empty Array Handling
`dto.Paginated` automatically converts `nil` slices to empty arrays `[]` in JSON output.
```go
// ✅ Handler can use simple var declaration
var resp []dto.TargetResponse
for _, t := range targets {
resp = append(resp, toResponse(&t))
}
dto.Paginated(c, resp, total, page, pageSize)
// Output: {"results": [], ...} (not null)
// ❌ For non-paginated responses, initialize explicitly
failedTargets := []dto.FailedTarget{} // Not: var failedTargets []dto.FailedTarget
dto.Success(c, BatchResponse{FailedTargets: failedTargets})
```
**Rule**: Use `dto.Paginated` for list APIs - it handles nil → `[]` automatically. For other responses with array fields, initialize with `[]T{}` to avoid `null` in JSON.
## Filter (Unified Search Parameter)
All list APIs use a unified `filter` parameter for searching and filtering.
### Query Parameter
| Parameter | Type | Description |
|-----------|------|-------------|
| filter | string | Plain text or smart filter syntax |
### Filter Syntax
| Syntax | Description | Example |
|--------|-------------|---------|
| Plain text | Fuzzy search on default field | `filter=portal` |
| `field="value"` | Fuzzy match (ILIKE) | `filter=name="portal"` |
| `field=="value"` | Exact match | `filter=name=="example.com"` |
| `field!="value"` | Not equal | `filter=type!="ip"` |
| `\|\|` or `or` | OR logic | `filter=name="a" \|\| name="b"` |
| `&&` or `and` | AND logic | `filter=name="a" && type="domain"` |
### Implementation
```go
// 1. Define filter mapping in repository
var TargetFilterMapping = scope.FilterMapping{
"name": {Column: "target.name", IsArray: false},
"type": {Column: "target.type", IsArray: false},
}
// 2. Use WithFilterDefault in repository (with default field for plain text)
query = query.Scopes(scope.WithFilterDefault(filter, TargetFilterMapping, "name"))
// 3. DTO uses "filter" parameter
type TargetListQuery struct {
PaginationQuery
Filter string `form:"filter"`
Type string `form:"type"`
}
```
### Frontend Usage
```typescript
// Plain text search (searches default field)
const response = await api.get('/targets/', { params: { filter: 'portal' } })
// Smart filter syntax
const response = await api.get('/targets/', { params: { filter: 'name="portal"' } })
```
## Validation
Use `github.com/asaskevich/govalidator` for common validations. Don't write custom regex.
```go
import "github.com/asaskevich/govalidator"
// ✅ Use govalidator
govalidator.IsDNSName("example.com") // Domain
govalidator.IsURL("https://example.com") // URL
govalidator.IsIP("192.168.1.1") // IP
// ✅ Use standard library for IP/CIDR
net.ParseIP("192.168.1.1")
net.ParseCIDR("10.0.0.0/8")
// ❌ Don't write custom regex for domain/IP/URL validation
regexp.MustCompile(`^([a-zA-Z0-9]...`)
```
## Delete API Convention
### Single Delete (RESTful)
```
DELETE /api/{resource}/{id}/
Response: 204 No Content (no body)
```
```go
// Handler
func (h *TargetHandler) Delete(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.svc.Delete(id); err != nil {
if errors.Is(err, service.ErrTargetNotFound) {
dto.NotFound(c, "Target not found")
return
}
dto.InternalError(c, "Failed to delete target")
return
}
dto.NoContent(c) // 204
}
```
### Bulk Delete
```
POST /api/{resource}/bulk-delete/
Request: {"ids": [1, 2, 3]}
Response: 200 {"deletedCount": 3}
```
```go
// Handler
func (h *TargetHandler) BulkDelete(c *gin.Context) {
var req dto.BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
dto.BadRequest(c, "Invalid request body")
return
}
deletedCount, err := h.svc.BulkDelete(req.IDs)
if err != nil {
dto.InternalError(c, "Failed to delete targets")
return
}
dto.Success(c, dto.BulkDeleteResponse{DeletedCount: deletedCount})
}
```
### Frontend Integration
- Single delete: Frontend uses local data for success toast (name already known)
- Bulk delete: Frontend uses `deletedCount` from response for success toast
## Batch Operation Limits
| Operation | Max Items | Notes |
|-----------|-----------|-------|
| Batch Create | 5000 | `binding:"max=5000"` |
| Bulk Delete | No limit | But consider performance |
```go
// Example: Batch create with limit
type BatchCreateTargetRequest struct {
Targets []TargetItem `json:"targets" binding:"required,min=1,max=5000,dive"`
}
```
## Request Binding (Industry Standard)
Use `dto.BindJSON`, `dto.BindQuery`, `dto.BindURI` helpers for automatic validation error handling.
```go
// ✅ Use dto.BindJSON - one line, auto error response
var req dto.CreateTargetRequest
if !dto.BindJSON(c, &req) {
return
}
// ❌ Don't use c.ShouldBindJSON directly
if err := c.ShouldBindJSON(&req); err != nil {
// Manual error handling...
}
```
Benefits:
- Automatic validation error translation (e.g., "targets must have at most 5000 items")
- Consistent error response format across all handlers
- Less boilerplate code
## Standard Library First
Prefer Go standard library over custom implementations or third-party packages.
```go
// ✅ Use encoding/csv for CSV operations
import "encoding/csv"
writer := csv.NewWriter(w)
writer.Write([]string{"a", "b", "c"})
// ✅ Use encoding/json for JSON
import "encoding/json"
// ✅ Use net/http for HTTP clients
import "net/http"
// ✅ Use time for time operations
import "time"
// ❌ Don't write custom CSV escaping
func escapeCSV(s string) string { ... } // Use encoding/csv instead
```
Benefits:
- Well-tested and maintained
- Handles edge cases (escaping, encoding)
- Familiar to other Go developers
## File Export (CSV/Excel)
### Streaming Export Pattern
For large data exports, use streaming to avoid memory issues:
```go
func (h *Handler) Export(c *gin.Context) {
// 1. Get streaming cursor
rows, err := h.svc.Stream(id)
// 2. Use csv.StreamCSV helper (no Content-Length for streaming)
csv.StreamCSV(c, rows, headers, filename, mapper, 0)
}
```
### IMPORTANT: Don't Set Content-Length for Streaming
```go
// ❌ DON'T set Content-Length for streaming exports
// Estimated size may not match actual size, causing connection to close early
c.Header("Content-Length", strconv.Itoa(estimatedSize))
// ✅ DO use chunked transfer encoding (default when no Content-Length)
// Browser will show "unknown size" but download completes correctly
```
### UTF-8 BOM for Excel
Add UTF-8 BOM at the start for Excel to recognize Chinese characters:
```go
var UTF8BOM = []byte{0xEF, 0xBB, 0xBF}
c.Writer.Write(UTF8BOM)
```
## RESTful Route Design
### Nested Resources (belongs to parent)
Use nested routes when the child resource is always accessed in context of a parent:
```
GET /targets/:id/websites # List websites for a target
POST /targets/:id/websites/bulk-create # Create websites for a target
GET /targets/:id/websites/export # Export websites for a target
```
### Standalone Resources (independent operations)
Use standalone routes when the operation doesn't require parent context:
```
GET /websites/:id # Get single website by ID
DELETE /websites/:id # Delete single website by ID
POST /websites/bulk-delete # Bulk delete by IDs (no target needed)
```
### Route Naming Rules
```
✅ /targets/:id/websites # Nested under parent
✅ /websites/:id # Standalone by ID
✅ /websites/bulk-delete # Action on resource
❌ /assets/websites/bulk-delete # Don't add unnecessary prefixes
❌ /api/v1/assets/targets/... # Keep URLs short and clean
```
## Bulk Create vs Bulk Upsert
For asset tables (website, subdomain, endpoint, etc.), provide two separate interfaces:
### bulk-create (Frontend manual add)
```
POST /targets/:id/websites/bulk-create
Request: {"urls": ["https://..."]}
Behavior: ON CONFLICT DO NOTHING (only create new, ignore duplicates)
Use case: User manually adds assets from frontend
```
### bulk-upsert (Scanner import)
```
POST /targets/:id/websites/bulk-upsert
Request: {"websites": [{url, title, statusCode, tech, ...}]}
Behavior: ON CONFLICT DO UPDATE with COALESCE + array merge
Use case: Scanner imports assets with full data
```
### Target Ownership Validation
Both interfaces MUST validate that assets belong to the target:
```go
// Service layer - filter items that match target
for _, item := range items {
if validator.IsURLMatchTarget(item.URL, target.Name, target.Type) {
// Only matching URLs are processed
websites = append(websites, ...)
}
// Non-matching URLs are silently filtered out
}
```
This ensures:
- Website URL must match target domain/IP/CIDR
- Subdomain must be under target domain
- Invalid items are silently skipped (not rejected with error)
### Upsert Update Strategy
| Field Type | Strategy | Example |
|------------|----------|---------|
| String fields | `COALESCE(NULLIF(new, ''), old)` | Only update if new value is non-empty |
| Nullable fields | `COALESCE(new, old)` | Only update if new value is not null |
| Array fields | Merge + deduplicate + sort | `tech` array merges and removes duplicates |
| Primary key | Don't update | `url`, `target_id` |
| Timestamp | Don't update | `created_at` |
### Implementation Example
```go
// Repository layer - use GORM OnConflict with custom assignments
result := r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "url"}, {Name: "target_id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"title": gorm.Expr("COALESCE(NULLIF(EXCLUDED.title, ''), website.title)"),
"status_code": gorm.Expr("COALESCE(EXCLUDED.status_code, website.status_code)"),
"tech": gorm.Expr(`(
SELECT ARRAY(SELECT DISTINCT unnest FROM unnest(
COALESCE(website.tech, ARRAY[]::varchar(100)[]) ||
COALESCE(EXCLUDED.tech, ARRAY[]::varchar(100)[])
) ORDER BY unnest)
)`),
}),
}).Create(&websites)
```
## GORM Bulk Operations (IMPORTANT)
Always use GORM's `clause.OnConflict` for bulk insert/upsert operations. Never use raw SQL with loops.
### ✅ Correct: GORM Batch Insert
```go
// BulkCreate - ON CONFLICT DO NOTHING
result := r.db.Clauses(clause.OnConflict{DoNothing: true}).Create(&batch)
// BulkUpsert - ON CONFLICT DO UPDATE
result := r.db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "scan_id"}, {Name: "url"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"status_code": gorm.Expr("COALESCE(EXCLUDED.status_code, table.status_code)"),
"image": gorm.Expr("COALESCE(EXCLUDED.image, table.image)"),
}),
}).Create(&batch)
```
### ❌ Wrong: Raw SQL Loop (N times slower)
```go
// DON'T DO THIS - executes N SQL statements instead of 1
sql := `INSERT INTO ... ON CONFLICT DO UPDATE SET ...`
for _, item := range items {
r.db.Exec(sql, item.Field1, item.Field2, ...) // ❌ One SQL per item
}
```
### Performance Comparison
| Method | 1000 Records | SQL Statements |
|--------|--------------|----------------|
| GORM `Create(&batch)` | ~50ms | 1 |
| Raw SQL loop | ~5000ms | 1000 |
### When Raw SQL is Acceptable
- Simple DELETE/UPDATE with WHERE clause (not bulk insert)
- Complex queries that GORM can't express
- One-time operations (not in hot paths)
## PostgreSQL Parameter Limits
PostgreSQL has a hard limit of **65535 parameters** per SQL statement. Bulk operations MUST use batching.
### Calculation
```
Parameters = Records × Fields
Example: 5000 websites × 14 fields = 70000 > 65535 ❌
```
### Required Pattern
```go
func (r *Repository) BulkUpsert(items []model.Item) (int64, error) {
var totalAffected int64
// MUST batch to avoid "too many parameters" error
batchSize := 100 // Safe batch size
for i := 0; i < len(items); i += batchSize {
end := i + batchSize
if end > len(items) {
end = len(items)
}
batch := items[i:end]
affected, err := r.upsertBatch(batch)
if err != nil {
return totalAffected, err
}
totalAffected += affected
}
return totalAffected, nil
}
```
### Batch Size Guidelines
| Fields per Record | Max Batch Size | Recommended |
|-------------------|----------------|-------------|
| 5-10 | ~6000 | 500 |
| 10-15 | ~4000 | 100 |
| 15-20 | ~3000 | 100 |
| 20+ | ~2000 | 50 |

View File

@@ -1,118 +0,0 @@
# Go 后端迁移策略
## 迁移目标
本项目正在从 Python (Django) 完全迁移到 Go。`server/` 是新的主后端,`backend/` (Python) 将逐步废弃。
## 迁移原则
1. **全新数据库 schema** - Go 后端拥有独立的数据库 schema 管理权,不依赖 Django migrations
2. **SQL 迁移文件** - 使用 golang-migrate 管理数据库迁移,手写 SQL 文件
3. **版本控制** - 每次 schema 变更创建新的迁移文件,支持回滚
## 数据库迁移
### 当前策略
- 使用 `golang-migrate` 执行 SQL 迁移文件
- 迁移文件位于 `server/cmd/server/migrations/`
- 启动时自动执行所有待执行的迁移
- 外键约束已启用,使用 `ON DELETE CASCADE`
### 迁移文件命名规范
```
000001_init_schema.up.sql # 创建初始表结构
000001_init_schema.down.sql # 回滚初始表结构
000002_add_gin_indexes.up.sql # 添加 GIN 索引
000002_add_gin_indexes.down.sql
```
### 创建新迁移
```bash
# 手动创建迁移文件
touch server/cmd/server/migrations/000003_add_new_table.up.sql
touch server/cmd/server/migrations/000003_add_new_table.down.sql
```
### 迁移 API
```go
// 执行所有待执行的迁移
database.RunMigrations(sqlDB)
// 回滚最后一个迁移
database.MigrateDown(sqlDB)
// 迁移到指定版本
database.MigrateToVersion(sqlDB, 1)
// 获取当前版本
version, dirty, err := database.GetMigrationVersion(sqlDB)
```
### GIN 索引
PostgreSQL 数组字段需要 GIN 索引以支持高效查询:
```sql
-- 创建 GIN 索引
CREATE INDEX idx_website_tech_gin ON website USING GIN (tech);
-- 查询示例(使用 @> 包含操作符)
SELECT * FROM website WHERE tech @> ARRAY['nginx'];
```
已添加 GIN 索引的字段:
- `website.tech`
- `endpoint.tech`
- `endpoint.matched_gf_patterns`
- `scan.engine_ids`
- `scan.container_ids`
## 模型定义规范
参考 `项目信息以及如何快速自动化操作.md` 中的 Go 后端部分。
**注意**: 模型定义仅用于 GORM 查询不再用于自动迁移。Schema 变更必须通过 SQL 迁移文件。
## 迁移进度
- [x] 基础模型Target, Scan
- [x] 资产模型Subdomain, Host, Website
- [x] 快照模型(各类 Snapshot
- [x] GIN 索引(数组字段)
- [ ] API 接口迁移
- [ ] 扫描引擎迁移
- [ ] 任务队列(硬删除 + CASCADE 清理)
## 待完善功能
### 删除策略
当前状态:
- ✅ 软删除已实现(设置 `deleted_at`
- ❌ 硬删除未实现
计划:
- 等任务队列完善后,实现后台硬删除任务
- 硬删除时使用数据库 CASCADE 自动清理关联数据Website、Subdomain 等)
- 流程:用户删除 → 软删除 → 后台任务 → 硬删除 + CASCADE
### 软删除注意事项
统计关联数据时必须排除已软删除的记录:
```go
// ❌ 错误 - 会统计已删除的 target
SELECT COUNT(*) FROM organization_target
WHERE organization_id = ?
// ✅ 正确 - 排除已软删除的 target
SELECT COUNT(*) FROM organization_target
INNER JOIN target ON target.id = organization_target.target_id
WHERE organization_id = ? AND target.deleted_at IS NULL
```
查询关联数据时同理,始终添加 `deleted_at IS NULL` 条件。

View File

@@ -1,360 +0,0 @@
# Go 推荐库和最佳实践
本文档列出了项目中推荐使用的 Go 第三方库,以及使用这些库的最佳实践。
## 核心原则
1. **优先使用成熟的开源库**:避免重复造轮子
2. **选择活跃维护的项目**:确保长期支持和安全更新
3. **考虑性能和安全性**:特别是在安全扫描场景下
4. **保持依赖最小化**:只引入真正需要的库
## 推荐库列表
### 网络和验证
#### 1. ProjectDiscovery Utils
**包名**`github.com/projectdiscovery/utils`
**用途**:网络相关的工具函数,专为安全工具优化
**推荐使用的子包**
- `github.com/projectdiscovery/utils/ip` - IP 地址验证和处理
- `github.com/projectdiscovery/utils/strings` - 字符串处理
**使用示例**
```go
import (
iputil "github.com/projectdiscovery/utils/ip"
)
// IP 地址验证
if iputil.IsIP("192.168.1.1") {
// 处理 IP 地址
}
// IPv4 验证
if iputil.IsIPv4("192.168.1.1") {
// 处理 IPv4
}
// IPv6 验证
if iputil.IsIPv6("2001:db8::1") {
// 处理 IPv6
}
// CIDR 验证
if iputil.IsCIDR("192.168.1.0/24") {
// 处理 CIDR
}
// 内网 IP 检测
if iputil.IsInternal("192.168.1.1") {
// 处理内网 IP
}
```
**优势**
- 与 nuclei、subfinder 等工具使用相同的底层库
- 针对大规模扫描场景优化
- 活跃的安全社区维护
**项目中的使用**
- `worker/internal/pkg/validator/domain.go` - 子域名验证中的 IP 检测
#### 2. govalidator
**包名**`github.com/asaskevich/govalidator`
**用途**:通用的字符串验证库
**常用函数**
```go
import "github.com/asaskevich/govalidator"
// DNS 名称验证
if govalidator.IsDNSName("example.com") {
// 有效的域名
}
// Email 验证
if govalidator.IsEmail("user@example.com") {
// 有效的邮箱
}
// URL 验证
if govalidator.IsURL("https://example.com") {
// 有效的 URL
}
// IP 验证(通用)
if govalidator.IsIP("192.168.1.1") {
// 有效的 IP
}
```
**优势**
- 成熟稳定,广泛使用
- 支持多种验证类型
- 依赖少,性能好
**项目中的使用**
- `worker/internal/pkg/validator/domain.go` - 域名格式验证
### 其他 ProjectDiscovery 生态系统库
#### 3. mapcidr
**包名**`github.com/projectdiscovery/mapcidr`
**用途**CIDR 操作和 IP 范围处理
**使用场景**
- 扩展 CIDR 为 IP 列表
- IP 范围计算
- 子网操作
**示例**
```go
import "github.com/projectdiscovery/mapcidr"
// 扩展 CIDR
ips, err := mapcidr.IPAddresses("192.168.1.0/24")
// 返回 192.168.1.0 到 192.168.1.255 的所有 IP
// 计算 CIDR 中的 IP 数量
count := mapcidr.CountIPsInCIDR("192.168.1.0/24")
```
#### 4. cdncheck
**包名**`github.com/projectdiscovery/cdncheck`
**用途**:检测 IP 是否属于 CDN 或云服务提供商
**使用场景**
- 识别 CDN IP
- 过滤云服务 IP
- 优化扫描目标
**示例**
```go
import (
"net"
"github.com/projectdiscovery/cdncheck"
)
client := cdncheck.New()
// 检查是否为 CDN
if client.Check(net.ParseIP("1.1.1.1")) {
// 这是 CDN IP
}
// 检查域名
if matched, _, err := client.CheckDomainWithFallback("example.com"); matched {
// 域名使用了 CDN
}
```
#### 5. retryabledns
**包名**`github.com/projectdiscovery/retryabledns`
**用途**:可重试的 DNS 客户端
**使用场景**
- 高可靠性 DNS 查询
- 自定义 DNS 解析器
- 批量 DNS 查询
**示例**
```go
import "github.com/projectdiscovery/retryabledns"
// 创建客户端
client := retryabledns.New([]string{"8.8.8.8:53", "1.1.1.1:53"}, 3)
// DNS 查询
ips, err := client.Resolve("example.com")
```
### 日志和配置
#### 6. zap
**包名**`go.uber.org/zap`
**用途**:高性能结构化日志
**项目中的使用**
```go
import "go.uber.org/zap"
// 记录日志
pkg.Logger.Info("Operation completed",
zap.String("operation", "scan"),
zap.Int("count", 100),
zap.Duration("duration", time.Second))
// 错误日志
pkg.Logger.Error("Operation failed",
zap.String("operation", "scan"),
zap.Error(err))
```
**优势**
- 高性能,零内存分配
- 结构化日志,易于解析
- 类型安全
#### 7. viper
**包名**`github.com/spf13/viper`
**用途**:配置管理
**使用场景**
- 读取配置文件YAML、JSON、TOML
- 环境变量管理
- 配置热更新
## 使用规范
### 1. 导入别名
当包名可能冲突时,使用别名:
```go
import (
iputil "github.com/projectdiscovery/utils/ip"
strutil "github.com/projectdiscovery/utils/strings"
)
```
### 2. 错误处理
始终检查错误,不要忽略:
```go
// ✅ 正确
if iputil.IsIP(s) {
// 处理 IP
}
// ❌ 错误 - 不要自己实现已有的功能
func isIPLike(s string) bool {
// 自定义实现...
}
```
### 3. 性能考虑
在高频调用的场景下,选择性能优化的库:
```go
// ProjectDiscovery 的库针对大规模扫描优化
for _, subdomain := range millions {
if iputil.IsIP(subdomain) {
continue
}
// 处理子域名
}
```
### 4. 依赖管理
添加新依赖后,运行:
```bash
go mod tidy
```
确保 `go.mod``go.sum` 正确更新。
## 避免使用的模式
### ❌ 不要重复造轮子
```go
// ❌ 错误 - 自己实现 IP 验证
func isIPLike(s string) bool {
parts := strings.Split(s, ".")
if len(parts) != 4 {
return false
}
// ... 更多代码
}
// ✅ 正确 - 使用现有库
if iputil.IsIP(s) {
// 处理 IP
}
```
### ❌ 不要忽略错误
```go
// ❌ 错误
_ = file.Close()
// ✅ 正确
if err := file.Close(); err != nil {
log.Warn("Failed to close file", zap.Error(err))
}
```
### ❌ 不要使用过时的库
在选择库时,检查:
- 最后更新时间
- GitHub stars 和 forks
- Issue 响应速度
- 社区活跃度
## 库版本管理
### 更新依赖
定期检查和更新依赖:
```bash
# 查看可更新的依赖
go list -u -m all
# 更新特定依赖
go get -u github.com/projectdiscovery/utils
# 更新所有依赖
go get -u ./...
```
### 安全更新
关注安全公告,及时更新有漏洞的依赖:
```bash
# 使用 govulncheck 检查漏洞
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...
```
## 参考资源
### ProjectDiscovery 生态系统
- 官方文档https://docs.projectdiscovery.io/
- GitHub 组织https://github.com/projectdiscovery
- 工具列表nuclei, subfinder, httpx, dnsx, naabu, katana
### Go 标准库
- 官方文档https://pkg.go.dev/std
- 优先使用标准库,除非有特殊需求
### 库选择指南
1. 检查 GitHub stars> 1000 为佳)
2. 查看最近更新时间(< 6 个月为佳)
3. 阅读文档和示例
4. 检查 Issue 和 PR 活跃度
5. 查看依赖数量(越少越好)
## 总结
- **优先使用 ProjectDiscovery 生态系统的库**:专为安全工具优化
- **使用成熟的通用库**:如 govalidator、zap
- **避免重复造轮子**:充分利用开源社区的成果
- **保持依赖更新**:定期检查安全更新
- **遵循最佳实践**:错误处理、性能优化、代码可读性

View File

@@ -1,116 +0,0 @@
---
inclusion: manual
---
# 表格列宽度标准
## 列宽度规范
### 长文本列域名、URL、任务名称等
- `size: 350` - 默认宽度
- `minSize: 250` - 最小宽度,保证可读性
-`maxSize` - 允许用户自由拖拽扩展
- 样式:`break-all leading-relaxed whitespace-normal`
### 中等文本列(引擎名称、状态等)
- `size: 120-150`
- `minSize: 80-100`
- 通常使用 Badge 显示
### 短文本/数字列(日期、计数、进度等)
- `size: 100-150`
- `minSize: 80-120`
### 固定列(选择框、操作按钮)
- `size/minSize/maxSize` 相同值
- `enableResizing: false`
- 选择框40px
- 操作按钮60-120px
## 单元格样式
### 多行文本(推荐)
```tsx
<div className="flex-1 min-w-0">
<span className="text-sm font-medium break-all leading-relaxed whitespace-normal">
{text}
</span>
</div>
```
### 可点击链接
```tsx
<button className="text-sm font-medium hover:text-primary hover:underline underline-offset-2 transition-colors cursor-pointer text-left break-all leading-relaxed whitespace-normal">
{text}
</button>
```
### Badge 列表(横向自动换行)
```tsx
<div className="flex flex-wrap items-center gap-1.5">
{badges}
</div>
```
## 列定义示例
```tsx
// 长文本列(域名/URL/名称)
{
accessorKey: "targetName",
size: 350,
minSize: 250,
header: ({ column }) => <DataTableColumnHeader column={column} title="Target" />,
cell: ({ row }) => {
const value = row.getValue("targetName") as string
return (
<div className="flex-1 min-w-0">
<span className="text-sm font-medium break-all leading-relaxed whitespace-normal">
{value}
</span>
</div>
)
},
}
// 中等文本列Badge
{
accessorKey: "engineName",
size: 120,
minSize: 80,
header: ({ column }) => <DataTableColumnHeader column={column} title="Engine" />,
cell: ({ row }) => (
<Badge variant="secondary">{row.getValue("engineName")}</Badge>
),
}
// 日期列
{
accessorKey: "createdAt",
size: 150,
minSize: 120,
header: ({ column }) => <DataTableColumnHeader column={column} title="Created At" />,
cell: ({ row }) => (
<span className="text-sm text-muted-foreground">
{formatDate(row.getValue("createdAt"))}
</span>
),
}
// 固定操作列
{
id: "actions",
size: 80,
minSize: 80,
maxSize: 80,
enableResizing: false,
cell: ({ row }) => <ActionButtons row={row} />,
}
```
## 注意事项
1. 不要使用折叠/省略号 + Popover改用多行显示
2. 长文本列不设置 `maxSize`,让用户自由调整
3. 使用 `flex-wrap` 让 Badge 列表自动换行
4. 保持列宽一致性,相同类型的列使用相同的宽度配置

View File

@@ -1,231 +0,0 @@
---
inclusion: manual
---
# UI/UX Pro Max - Design Intelligence
Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.
## Prerequisites
Check if Python is installed:
```bash
python3 --version || python --version
```
If Python is not installed, install it based on user's OS:
**macOS:**
```bash
brew install python3
```
**Ubuntu/Debian:**
```bash
sudo apt update && sudo apt install python3
```
**Windows:**
```powershell
winget install Python.Python.3.12
```
---
## How to Use This Workflow
When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:
### Step 1: Analyze User Requirements
Extract key information from user request:
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
- **Industry**: healthcare, fintech, gaming, education, etc.
- **Stack**: React, Vue, Next.js, or default to `html-tailwind`
### Step 2: Search Relevant Domains
Use `search.py` multiple times to gather comprehensive information. Search until you have enough context.
```bash
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]
```
**Recommended search order:**
1. **Product** - Get style recommendations for product type
2. **Style** - Get detailed style guide (colors, effects, frameworks)
3. **Typography** - Get font pairings with Google Fonts imports
4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)
5. **Landing** - Get page structure (if landing page)
6. **Chart** - Get chart recommendations (if dashboard/analytics)
7. **UX** - Get best practices and anti-patterns
8. **Stack** - Get stack-specific guidelines (default: html-tailwind)
### Step 3: Stack Guidelines (Default: html-tailwind)
If user doesn't specify a stack, **default to `html-tailwind`**.
```bash
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind
```
Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`, `shadcn`
---
## Search Reference
### Available Domains
| Domain | Use For | Example Keywords |
|--------|---------|------------------|
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
| `prompt` | AI prompts, CSS keywords | (style name) |
### Available Stacks
| Stack | Focus |
|-------|-------|
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
| `react` | State, hooks, performance, patterns |
| `nextjs` | SSR, routing, images, API routes |
| `vue` | Composition API, Pinia, Vue Router |
| `svelte` | Runes, stores, SvelteKit |
| `swiftui` | Views, State, Navigation, Animation |
| `react-native` | Components, Navigation, Lists |
| `flutter` | Widgets, State, Layout, Theming |
| `shadcn` | shadcn/ui components, theming, forms, patterns |
---
## Example Workflow
**User request:** "Build a landing page for a skincare service"
**AI should:**
```bash
# 1. Search product type
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product
# 2. Search style (based on industry: beauty, elegant)
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style
# 3. Search typography
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography
# 4. Search color palette
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color
# 5. Search landing page structure
python3 .shared/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing
# 6. Search UX guidelines
python3 .shared/ui-ux-pro-max/scripts/search.py "animation" --domain ux
python3 .shared/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux
# 7. Search stack guidelines (default: html-tailwind)
python3 .shared/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind
```
**Then:** Synthesize all search results and implement the design.
---
## Tips for Better Results
1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app"
2. **Search multiple times** - Different keywords reveal different insights
3. **Combine domains** - Style + Typography + Color = Complete design system
4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues
5. **Use stack flag** - Get implementation-specific best practices
6. **Iterate** - If first search doesn't match, try different keywords
7. **Split Into Multiple Files** - For better maintainability:
- Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)
- Extract reusable styles into dedicated files
- Keep each file focused and under 200-300 lines
---
## Common Rules for Professional UI
These are frequently overlooked issues that make UI look unprofessional:
### Icons & Visual Elements
| Rule | Do | Don't |
|------|----|----- |
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like :art: :rocket: :gear: as UI icons |
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
### Interaction & Cursor
| Rule | Do | Don't |
|------|----|----- |
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
### Light/Dark Mode Contrast
| Rule | Do | Don't |
|------|----|----- |
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
### Layout & Spacing
| Rule | Do | Don't |
|------|----|----- |
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
---
## Pre-Delivery Checklist
Before delivering UI code, verify these items:
### Visual Quality
- [ ] No emojis used as icons (use SVG instead)
- [ ] All icons from consistent icon set (Heroicons/Lucide)
- [ ] Brand logos are correct (verified from Simple Icons)
- [ ] Hover states don't cause layout shift
### Interaction
- [ ] All clickable elements have `cursor-pointer`
- [ ] Hover states provide clear visual feedback
- [ ] Transitions are smooth (150-300ms)
- [ ] Focus states visible for keyboard navigation
### Light/Dark Mode
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
- [ ] Glass/transparent elements visible in light mode
- [ ] Borders visible in both modes
- [ ] Test both modes before delivery
### Layout
- [ ] Floating elements have proper spacing from edges
- [ ] No content hidden behind fixed navbars
- [ ] Responsive at 320px, 768px, 1024px, 1440px
- [ ] No horizontal scroll on mobile
### Accessibility
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Color is not the only indicator
- [ ] `prefers-reduced-motion` respected

View File

@@ -1,398 +0,0 @@
---
inclusion: always
---
## 语言规范
无论用户使用什么语言输入Kiro 必须始终使用中文回复。代码注释和变量名除外。
# 分布式扫描架构 (重要)
## 组件部署位置
```
┌─────────────────────────────────────────────────────────────────┐
│ 本地服务器(你的机器) │
├─────────────────────────────────────────────────────────────────┤
│ Server (Go) - API 服务、任务调度、数据库连接 │
│ PostgreSQL - 数据存储 │
│ Redis - 缓存、心跳数据 │
│ Frontend - Web 界面 │
└─────────────────────────────────────────────────────────────────┘
│ WebSocket (Agent 主动连接)
┌─────────────────────────────────────────────────────────────────┐
│ 远程 VPS多台
├─────────────────────────────────────────────────────────────────┤
│ Agent - 常驻服务,接收任务,启动 Worker │
│ Worker - 临时容器,执行扫描,完成后销毁 │
└─────────────────────────────────────────────────────────────────┘
```
## 数据传输方式
**关键约束**Worker 在远程 VPS 上,无法与 Server 共享文件系统。
| 数据类型 | 传输方式 | 说明 |
|---------|---------|------|
| 小数据(< 1MB | WebSocket 消息体 | target_name、config 等 |
| 大数据(子域名列表等) | WebSocket 消息体 | 序列化后传输Agent 写入本地文件 |
| 扫描结果 | HTTP 回调 | Worker 直接调 Server API |
## 数据流向
```
Server Agent Worker
│ │ │
│ 1. task_assign │ │
│ {targetName, config, │ │
│ subdomains: [...]} │ │
│ ────────────────────────────▶│ │
│ │ │
│ │ 2. 写入本地文件 │
│ │ /opt/orbit/inputs/xxx.txt │
│ │ │
│ │ 3. docker run │
│ │ -v /opt/orbit:/opt/orbit │
│ │ ───────────────────────────▶│
│ │ │
│ │ │ 4. 读取文件执行
│ │ │
│ 5. HTTP 回调 │ │
│ POST /api/scans/{id}/... │ │
│ ◀──────────────────────────────────────────────────────────│
```
## 设计原则
- **Worker 无状态**:不连数据库,不做决策,只执行
- **Server 是大脑**:决定扫什么、准备数据、接收结果
- **Agent 是桥梁**:接收任务、准备本地文件、启动 Worker、监控状态
# 项目环境与开发规范
## 认证信息
- Web 界面: `admin` / `admin`
- 数据库端口: `5432` (凭据在 `docker/.env` 中)
## Docker 部署
所有服务通过 Docker 运行。代码变更需要更新容器。
### 部署策略 (按优先级)
1. **首选**: `docker cp <文件> <容器>:<路径>` 然后 `docker restart <容器>`
2. **备选**: 重新构建镜像
### 配置文件
| 环境 | 路径 |
|------|------|
| 开发环境 | `docker/docker-compose.dev.yml` |
| 生产环境 | `docker/docker-compose.yml` |
## API 命名规范 (重要)
### Python 后端 (Django)
本项目使用 `djangorestframework-camel-case` 进行自动大小写转换。
| 层级 | 命名规范 | 示例 |
|------|----------|------|
| 前端 (TypeScript) | camelCase | `apiKey`, `apiSecret` |
| 后端 (Django views/serializers) | snake_case | `api_key`, `api_secret` |
| 数据库模型 | snake_case | `api_key`, `api_secret` |
**规则**: 在 Django 视图代码中,访问请求数据时始终使用 `snake_case`:
```python
# ✅ 正确
config.get('api_key', '')
# ❌ 错误 - 会返回空值/None
config.get('apiKey', '')
```
### Go 后端 (迁移中)
Go 后端位于 `server/` 目录,使用 JSON tag 进行字段名转换。
| 层级 | 命名规范 | 示例 |
|------|----------|------|
| 前端 (TypeScript) | camelCase | `apiKey`, `apiSecret` |
| Go 结构体字段 | PascalCase | `APIKey`, `APISecret` |
| Go JSON 输出 | camelCase | `json:"apiKey"` |
| 数据库列 (GORM) | snake_case | `gorm:"column:api_key"` |
**Go 风格规范**:
| 规范 | 说明 | 示例 |
|------|------|------|
| 包名 | 小写单词,不用下划线 | `model`, `handler` |
| 文件名 | 小写 + 下划线 | `worker_node.go` |
| 导出标识符 | PascalCase | `Target`, `GetByID` |
| 私有标识符 | camelCase | `targetID`, `getByID` |
| 接口命名 | 动词 + er | `Reader`, `Scanner` |
| 错误变量 | Err 前缀 | `ErrNotFound` |
**示例**:
```go
// ✅ 正确 - Go 模型定义
type Target struct {
ID int `gorm:"primaryKey" json:"id"`
Name string `gorm:"column:name" json:"name"`
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
}
// ✅ 正确 - 表名使用单数
func (Target) TableName() string {
return "target"
}
```
## Worker 镜像部署 (重要)
扫描任务由临时启动的 Worker 容器执行,每次扫描都从镜像新建容器。
### 镜像配置
| 配置项 | 位置 | 说明 |
|--------|------|------|
| `TASK_EXECUTOR_IMAGE` | `docker/.env` | Worker 镜像名称 |
| `IMAGE_TAG` | `docker/.env` | 版本标签 |
### 部署流程
修改 Worker 相关代码(如 `backend/apps/scan/`)后:
```bash
# 1. 重新构建镜像
docker build -t docker-worker -f docker/worker/Dockerfile .
# 2. 打上 .env 中配置的 tag查看 TASK_EXECUTOR_IMAGE 的值)
docker tag docker-worker docker-worker:v1.3.14-dev
```
### 注意事项
- `docker cp` 对 Worker **无效**Worker 是临时容器每次扫描都从镜像创建cp 的修改下次启动就丢失
- `docker cp` 对 Server **有效**Server 是常驻容器,可以用 cp + restart 快速更新
- 修改 `command_templates.py` 等扫描配置后,必须重新构建 Worker 镜像
## 扫描功能测试流程
修改扫描相关代码后,按以下流程测试:
### 1. 构建 Worker 镜像
```bash
docker build -t docker-worker -f docker/worker/Dockerfile .
docker tag docker-worker docker-worker:v1.3.14-dev
```
### 2. 发起扫描API 方式)
```bash
curl -s 'http://localhost:3000/api/scans/initiate/' \
-X POST \
-H 'Content-Type: application/json' \
-H 'Cookie: sessionid=<your_session_id>' \
--data-raw '{"targetId":1,"configuration":"subdomain_discovery:\n passive_tools:\n subfinder:\n enabled: true\n timeout: 3600\n","engineIds":[2],"engineNames":["subdomain_discovery"]}'
```
返回示例:`{"count":1,"scans":[{"id":9,"resultsDir":"/opt/xingrin/results/scan_20260105_095752_68bee193",...}]}`
### 3. 查看扫描日志
```bash
# 等待扫描完成后查看日志
cat /opt/xingrin/results/scan_<timestamp>/subdomain_discovery/subfinder_*.log
```
### 4. 手动测试扫描工具(绕过系统直接测试)
```bash
# 挂载配置文件目录,直接在 Worker 容器中运行工具
docker run --rm -v /opt/xingrin/results/<scan_dir>/subdomain_discovery:/config \
docker-worker:v1.3.14-dev \
subfinder -d example.com -all -pc /config/provider-config.yaml -v
```
### 5. 常见问题排查
| 问题 | 排查方法 |
|------|----------|
| 扫描结果不符合预期 | 查看日志中的 `Selected source(s)` 确认使用了哪些数据源 |
| API key 未生效 | 检查 `provider-config.yaml` 内容,确认格式正确 |
| 修改未生效 | 确认已重新构建镜像并打上正确的 tag |
## Go 后端开发 (迁移中)
Go 后端位于 `server/` 目录,与现有 `backend/` (Python) 并存。
### 服务端口
| 服务 | 端口 | 说明 |
|------|------|------|
| Go 后端 | 8888 | HTTP API 服务 |
| Python 后端 | 8000 | Django API 服务 |
| 前端 | 3000 | Next.js 开发服务器 |
| 数据库 | 5432 | PostgreSQL |
### 项目结构
```
server/
├── cmd/server/main.go # 入口
├── internal/
│ ├── config/ # 配置管理
│ ├── database/ # 数据库连接
│ ├── model/ # 数据模型(扁平结构)
│ ├── handler/ # HTTP 处理器
│ ├── middleware/ # 中间件
│ └── pkg/ # 内部工具
├── go.mod
└── Makefile
```
### 技术栈
| 组件 | 选择 |
|------|------|
| Web 框架 | Gin |
| ORM | GORM |
| 配置管理 | Viper |
| 日志 | Zap |
### 常用命令
```bash
# 进入 Go 项目目录
cd server
# 运行服务
make run
# 运行测试
make test
# 构建
make build
```
### API 测试
测试 Go 后端 API 需要先登录获取 JWT token
```bash
# 1. 登录获取 token
curl -s -X POST http://localhost:8888/api/auth/login/ \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin"}' | python3 -m json.tool
# 2. 使用 token 访问 API
TOKEN="<access_token>"
curl -s http://localhost:8888/api/targets/ \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
# 3. 获取目标详情(包含统计数据)
curl -s http://localhost:8888/api/targets/5050/ \
-H "Authorization: Bearer $TOKEN" | python3 -m json.tool
```
### 模型规范
- 模型文件放在 `internal/model/` 下,扁平结构
- 每个模型一个文件(如 `target.go`, `scan.go`
- 使用 `TableName()` 方法指定表名(单数形式)
- JSON tag 使用 camelCaseGORM column tag 使用 snake_case
## 自动化规则
- **禁止**在自动化脚本中使用 `echo` (会导致意外行为)
- 调试输出请使用日志文件
- **禁止**在命令行中打印特殊字符(如 `=====``#####``\n` 换行符等装饰性输出),这些字符可能导致 shell 解析问题或命令执行异常
- 测试 API 时直接执行 curl 命令,不要添加额外的格式化输出
## 代码规范
### 验证工具复用
项目中已有统一的验证工具 `backend/apps/common/validators.py`,包含:
- `is_valid_domain(domain: str) -> bool` - 域名验证(支持 IDN
- `is_valid_ip(ip: str) -> bool` - IP 地址验证IPv4/IPv6
- `validate_cidr(cidr: str)` - CIDR 格式验证
- `is_valid_url(url: str) -> bool` - URL 格式验证
- `detect_target_type(name: str) -> str` - 自动检测目标类型
**规则**
- ✅ 使用 `validators.py` 中的函数进行验证
- ❌ 不要自己写正则表达式验证域名/IP/URL
- ✅ 复用已有的验证逻辑,保持一致性
**示例**
```python
from apps.common.validators import is_valid_domain, is_valid_ip
# ✅ 正确
if is_valid_domain(host):
process_domain(host)
# ❌ 错误 - 不要自己写正则
if re.match(r'^[a-z0-9.-]+$', host):
process_domain(host)
```
### 方案设计规范
在讨论技术方案时,应该使用流程图而不是具体代码来说明方案。
**规则**
- ✅ 使用流程图展示方案的整体流程
- ✅ 使用对比表格说明方案的优缺点
- ✅ 在流程图中标注关键的数据流向和内存占用
- ❌ 不要在方案对比中直接写大量具体代码
- ✅ 代码示例只在最终确定方案后提供
**原因**
- 流程图更直观,便于理解整体架构
- 避免过早陷入实现细节
- 方便对比不同方案的优劣
- 减少沟通成本
**示例**
```
❌ 错误方式:
方案 1: 使用 Pipeline
```python
def task1():
# 50 行代码...
def task2():
# 50 行代码...
```
方案 2: 查询数据库
```python
def task1():
# 50 行代码...
```
✅ 正确方式:
方案 1: 使用 Pipeline
```
用户输入 → 阶段1 → StageOutput → 阶段2
内存占用: ~10MB
优点: 快速
缺点: 内存压力大
```
方案 2: 查询数据库
```
用户输入 → 阶段1 → 保存DB → 阶段2查询DB
内存占用: ~1KB
优点: 内存占用低
缺点: 数据库查询多
```
```