提交 001a68a9 authored 作者: 陈泽健's avatar 陈泽健

feat(security): 新增API接口安全测试模块

后端安全测试引擎(与PlaywrightExecutor解耦,基于requests+AES-CBC签名):
- 新增 SecurityConfig/VulnerabilityResult ORM 与 Pydantic schema
- 新增 HttpClient(AES-CBC签名算法,X-RANDOM/X-TIMESTAMP/X-SIGN)
- 新增 AuthHelper 多账号登录辅助
- 新增 SecurityExecutor 安全测试执行引擎
- 新增 security_service 业务逻辑层与 knowledge_base 知识库
- 新增 /api/security 路由(11个接口)

执行调度分流修复:
- execution_service.run_execution 按 case_type 分流,security 走专用 _run_security_execution
- 安全用例不再误交给 PlaywrightExecutor 解析 dict steps
- test_case schema steps 字段支持 Union(list/dict)

前端适配:
- 新增 types/security.ts 与 api/security.ts
- Cases.vue 支持安全用例风险等级标签与详情弹窗
- Execution.vue 执行弹窗分流 security/UI 执行逻辑

配套脚本与文档:
- create_security_cases / create_default_security_config / test_security_e2e
- PRD 需求文档、计划执行文档、报告设计文档
Co-Authored-By: 's avatarClaude <noreply@anthropic.com>
上级 d3587c55
# 【安全测试】执行计划 - 修复安全测试执行报错
> **文档类型**: 计划执行文档(安全测试模块)
> **创建日期**: 2026-07-21
> **关联问题**: `_问题处理_安全测试执行报错str对象无get属性.md`
> **执行策略**: 直接修复,无需分阶段
---
## 一、执行目标
修复安全测试模块执行时的 3 个问题,使安全测试用例能正常执行并保存结果。
| 修复点 | 优先级 | 目标 |
|--------|--------|------|
| 1. ORM 对象在 run_in_executor 中脱钩 | P0 | 安全测试用例正常执行 |
| 2. metadata 字段名冲突 | P0 | 漏洞结果正常保存 |
| 3. 前端 metadata 引用同步 | P0 | 接口返回正常 |
---
## 二、任务清单
### 2.1 后端修复
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | 重构 run_execution 执行逻辑 | `backend/app/services/security_service.py` | 在 async 上下文内预先转纯字典 |
| 1.2 | 修复字段名 metadata → extra_data | `backend/app/services/security_service.py` | `_save_vulnerability_result` |
| 1.3 | 修复 schema 字段名 | `backend/app/schemas/security.py` | `VulnerabilityResultResponse` |
### 2.2 前端修复
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 2.1 | 同步类型定义 | `frontend/src/types/security.ts` | `VulnerabilityResult` 字段 |
### 2.3 验证
| # | 任务 | 说明 |
|---|------|------|
| 3.1 | 重启后端服务 | 加载新代码 |
| 3.2 | 执行安全测试 | 全部用例不再 error |
| 3.3 | 查看执行结果 | 漏洞结果正常保存 |
---
## 三、详细改动
### 3.1 后端:security_service.py(已实施)
#### 改动 A:run_execution 中预先转纯字典
**位置**: `run_execution()` 方法
**改动前**
```python
def sync_run():
executor.start()
return executor.execute_cases(
[{"id": c.id, "name": c.name, "steps": c.steps} for c in cases],
...
)
results = await loop.run_in_executor(None, sync_run)
```
**改动后**
```python
import json as _json
def _to_plain_steps(steps_val):
"""把 steps 字段统一转成 dict(兼容 str/dict/None)"""
if isinstance(steps_val, str):
try:
parsed = _json.loads(steps_val)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
if isinstance(steps_val, dict):
return steps_val
return {}
# 在 async 上下文内(run_in_executor 之前)就完成转换
plain_cases = [
{"id": c.id, "name": c.name, "steps": _to_plain_steps(c.steps)}
for c in cases
]
client_config = config.to_client_config()
def sync_run():
executor = SecurityExecutor(client_config)
executor.start()
return executor.execute_cases(plain_cases, ...)
results = await loop.run_in_executor(None, sync_run)
```
**原理**:线程池闭包只捕获纯字典数据,不再持有 ORM 对象引用,避免脱钩。
#### 改动 B:_save_vulnerability_result 字段名
**改动前**
```python
vuln_result = VulnerabilityResult(
...
metadata=result.metadata,
)
```
**改动后**
```python
vuln_result = VulnerabilityResult(
...
extra_data=result.metadata,
)
```
### 3.2 后端:schemas/security.py(已实施)
**改动**`VulnerabilityResultResponse``metadata: Dict[str, Any]``extra_data: Dict[str, Any]`
### 3.3 前端:types/security.ts(待实施)
**改动**`VulnerabilityResult` 接口中 `metadata: Record<string, any>``extraData: Record<string, any>`
---
## 四、验证检查清单
### 4.1 后端验证
- [ ] `security_service.py``run_execution` 已用 `_to_plain_steps` 预转换
- [ ] `security_service.py``_save_vulnerability_result` 已用 `extra_data`
- [ ] `schemas/security.py``VulnerabilityResultResponse` 已用 `extra_data`
### 4.2 执行验证
- [ ] 重启 8001 后端服务
- [ ] 前端 `/execution/security` 执行用例
- [ ] 全部用例状态为 passed/failed(不再 error)
- [ ] 执行记录可展开查看漏洞结果
### 4.3 时间显示(记录备案,不修复)
- [ ] 全项目时间显示问题单独评估(UTC → 本地时区转换)
---
## 五、风险评估
| 风险 | 影响 | 缓解 |
|------|------|------|
| 预转换逻辑遗漏嵌套字段 | 用例执行失败 | `_to_plain_steps` 兼容 str/dict/None 三种情况 |
| 前端未同步 extraData | 详情显示异常 | 前端 types 同步修改 |
| 现有执行记录数据 | 无 | 仅影响新执行,旧数据不受影响 |
---
*本文档为安全测试执行报错修复的计划执行文档。*
# 【安全测试】问题处理 - 安全测试执行报错 'str' object has no attribute 'get'
> **文档类型**: 问题处理文档(安全测试模块)
> **创建日期**: 2026-07-21
> **最后更新**: 2026-07-21(修正根因)
> **问题等级**: P0(阻塞,安全测试全部执行失败)
> **关联模块**: 安全测试(security)
> **状态**: ✅ 已修复并验证
---
## 一、问题现象
在执行中心执行安全测试用例时,**全部用例执行失败**,前端报错:
```
执行异常: 'str' object has no attribute 'get'
```
后端日志关键行:
```
app.executors.playwright_executor - ERROR - 用例执行异常: 水平越权访问其他用户会议详情, 'str' object has no attribute 'get'
```
同时观察到执行记录的**时间显示为 UTC 时间**(如本地 17 点执行,显示为 9 点)。
---
## 二、根因分析(已修正)
### 2.1 ⚠️ 真正根因:安全测试用例被误交给 PlaywrightExecutor 执行
**最初误判**:以为是 `run_in_executor` 线程池导致 ORM 对象脱钩、JSON 字段退化为字符串。
**实际真相**(通过后端日志确认):
错误来源是 **`playwright_executor`**,而不是我新写的 `security_executor`。这说明安全测试用例**根本没有走安全测试执行器**,而是走了 UI 用例的通用执行链路。
**执行链路**
```
前端 Cases.vue 点"执行选中"
└─ executionApi.create() # 通用执行接口
└─ executionApi.run() # 通用执行触发
└─ ExecutionService.run_execution()# 后端统一入口
└─ PlaywrightExecutor # ⚠️ 无脑用 UI 执行器
└─ 解析 steps dict # 安全用例 steps 是 dict,不是步骤列表
└─ dict.get('action') # ❌ str 没 .get() → 报错
```
**根因**
1. 前端从「用例管理页 Cases.vue」点"执行选中",调用的是**通用执行 API**`/api/executions`),不是安全测试专用 API(`/api/security/executions`
2. 后端 `ExecutionService.run_execution()` **没有按 case_type 分流**,所有用例都交给 `PlaywrightExecutor`
3. 安全测试用例的 `steps` 是 dict(`{"test_type":"api_security", ...}`),而 `PlaywrightExecutor` 期望的是步骤列表 `[{order, action, ...}]`
4. Playwright 解析 dict 时 `dict[xxx].get(...)``str.get(...)` 触发 `'str' object has no attribute 'get'`
**验证**
```bash
# 端到端测试脚本 test_security_e2e.py
# 修复前:日志显示 playwright_executor 报错
# 修复后:日志显示 security_executor 执行,3/3 用例正常完成
```
### 2.2 次要问题:模型字段名 `metadata` 与 SQLAlchemy 保留字冲突
用户/linter 已将 `VulnerabilityResult.metadata` 改名为 `extra_data``metadata` 是 SQLAlchemy 保留字段名),但以下代码未同步:
- `security_service.py``_save_vulnerability_result()` 仍用 `metadata=result.metadata`
- `schemas/security.py``VulnerabilityResultResponse` 仍用 `metadata`
这会导致**漏洞结果保存失败****接口返回报错**
### 2.3 防御性增强:ORM 对象在 run_in_executor 中脱钩
虽然真正根因不是脱钩,但作为**防御性增强**,在丢入线程池前把 ORM 对象转纯字典仍然是好实践,避免后续 session 行为变化引入隐患。
### 2.4 时间显示问题(已有行为,非安全测试独有)
全项目统一使用 `datetime.utcnow()` 存储 UTC 时间,前端直接显示未做 +8 时区转换。
- 数据库存 UTC 时间(如 09:00 UTC = 17:00 本地)
- 前端按字符串直接显示,显示为 09:00
这是**全项目既有行为**(会议管理、数据分析等所有模块都一样),不属于本次安全测试引入的问题,记录在案但**不在本次修复范围**(修复需统一改全项目时间显示逻辑,影响面大)。
---
## 三、修复方案
### 3.1 修复点 1(核心):execution_service 按 case_type 分流(P0)
**文件**: `backend/app/services/execution_service.py``run_execution()`
**改动**:在 `run_execution()` 入口处判断 `case_type`,安全测试分流到专用方法:
```python
async def run_execution(self, execution_id, config=None):
execution = await self.get_execution(execution_id)
if not execution:
raise ValueError(f"执行记录不存在: {execution_id}")
# ===== 按用例类型分流:安全测试走专用执行器 =====
if execution.case_type == "security":
return await self._run_security_execution(execution_id, config)
# 以下为原有 UI 用例执行逻辑(PlaywrightExecutor)...
```
**新增方法** `_run_security_execution()`
1. 从执行记录取关联用例(按模块分组排序)
2. 加载默认安全测试配置(`SecurityConfig`
3. 在 async 上下文内预先把 ORM 对象转纯字典(防御脱钩)
4. `run_in_executor` 调用 `SecurityExecutor` 执行
5. 结果写回 `CaseResult`(供执行中心展示)+ `VulnerabilityResult`(供报告中心)
6. 广播 WebSocket 执行完成
### 3.2 修复点 2:模型字段名同步 `metadata` → `extra_data`(P0)
| 文件 | 改动 |
|------|------|
| `services/security_service.py` | `_save_vulnerability_result()``metadata=result.metadata``extra_data=result.metadata` |
| `services/execution_service.py` | `_run_security_execution()``metadata=r.metadata``extra_data=r.metadata` |
| `schemas/security.py` | `VulnerabilityResultResponse``metadata``extra_data` |
### 3.3 修复点 3:前端 `metadata` 引用同步(P0)
| 文件 | 改动 |
|------|------|
| `frontend/src/types/security.ts` | `VulnerabilityResult.metadata``extraData` |
### 3.4 时间显示问题(不在本次修复范围,记录备案)
全项目使用 UTC 时间存储,若需修复需统一在前端 `formatTime` 工具函数中做时区转换,影响所有页面,单独评估。
---
## 四、验证结果(已自测通过)
端到端测试脚本:`backend/scripts/test_security_e2e.py`
```
=== 1. 创建执行 ===
创建状态码: 201
执行ID: exec_xxx
case_type: security
=== 2. 触发执行 ===
触发状态码: 200
{"message":"执行完成","status":"completed"}
=== 3. 等待执行完成 ===
轮询 1: 3/3 完成
=== 4. 执行结果 ===
Nacos未授权访问: failed | Nacos控制台可匿名访问
Swagger文档暴露: passed |
SQL注入-会议查询接口: passed |
```
**验证结论**
- ✅ 安全用例正确走 SecurityExecutor(不再走 PlaywrightExecutor)
- ✅ 3/3 用例完成,无 `'str' object has no attribute 'get'` 错误
- ✅ 漏洞判定正确(Nacos 未授权=漏洞,Swagger/SQL注入=已防护)
- ✅ 结果同时写入 CaseResult 和 VulnerabilityResult
---
## 五、踩坑记录(修正后)
| # | 现象 | 根因 | 正确做法 |
|---|------|------|---------|
| S1 | 安全测试执行报 `'str' object has no attribute 'get'` | `execution_service` 未按 case_type 分流,安全用例被交给 PlaywrightExecutor 解析 dict steps | **在 `run_execution` 入口按 `case_type` 分流**,security 走专用 `_run_security_execution` |
| S2 | `metadata` 字段保存/查询报错 | `metadata` 是 SQLAlchemy 保留字段名 | 模型用 `extra_data`,service/schema 同步 |
| S3 | 执行时间显示比实际少 8 小时 | 全项目用 `utcnow()` 存 UTC 时间,前端未转时区 | 全项目既有行为,需统一在前端做时区转换(单独评估) |
| S4 | 误判为"ORM 脱钩" | 调试时用同步 engine 复现不出,未直接看生产日志 | **排查异常时先看后端日志的错误来源模块**,不要只看错误消息臆测 |
---
## 六、教训
1. **排查异常先看日志来源模块**:日志明确写了 `app.executors.playwright_executor`,我一开始却以为是 `security_executor` 的脱钩问题,浪费了时间。**应该先 grep 错误消息确认是哪个模块报的**
2. **多入口要统一分流**:安全测试有两个执行入口(Cases.vue 的通用执行 + Execution.vue 的专用执行),后端 `run_execution` 必须在入口统一分流,不能假设前端会调用专用 API。
3. **防御性编程**:即便不是真正根因,在 `run_in_executor` 前把 ORM 转纯字典也是好习惯,避免 session 行为变化引入隐患。
---
*本文档为安全测试模块问题处理记录,供后续维护参考。*
# 【安全测试】报告中心 - 报告设计文档(分析,不写代码)
> **文档类型**: 设计文档(安全测试报告)
> **创建日期**: 2026-07-21
> **参考实现**: `临时目录/安全测试/ApiSecurityTest/utils/report_generator.py`
> **状态**: 设计阶段,未实现
---
## 一、现状分析
### 1.1 当前平台报告中心的问题
平台报告中心(`/reports/security`)目前**只存原始执行数据**`CaseResult` + `VulnerabilityResult`),展示方式与 UI 测试报告一致:
- 展示执行记录列表 + 用例结果表格
- 每条结果只显示:用例名、状态、耗时、错误信息
- **缺少**:风险等级统计、OWASP 覆盖矩阵、漏洞详情(请求/响应/复现步骤/修复建议)、历史回归验证、华为红线合规检查
### 1.2 参考实现的报告优势
参考实现 `report_generator.py` 生成一份**结构化 Markdown 总报告**,包含 7 大章节,直接可用于交付:
- 执行概要 + OWASP 覆盖矩阵
- 漏洞详情(按风险等级排序,含复现步骤)
- 已验证安全项
- 风险评估与修复建议
- 已知安全问题清单
- 历史漏洞回归验证
- 华为安全红线合规性检查
---
## 二、参考实现的报告结构(详细分析)
### 2.1 报告命名规则
```
{服务器IP}_安全测试报告_{时间戳}.md
例:192.168.5.44_安全测试报告_20260721_143025.md
```
### 2.2 报告章节结构
#### 标题与基本信息
```
# 接口安全测试总报告
> 生成时间:2026-07-21 14:30:25
> 测试目标:https://192.168.5.44
> 测试标准:OWASP API Security Top 10 (2019)
> 测试工具:ApiSecurityTest
```
#### 一、执行概要
**1.1 测试统计总览**(表格):
| 统计项 | 数值 |
|--------|------|
| 测试用例总数 | 90 |
| 发现漏洞总数 | 20 |
| 🔴 高危漏洞 | 8 |
| 🟠 中危漏洞 | 12 |
| 🟡 低危漏洞 | 0 |
| 🔵 信息类 | 0 |
| 🟢 已验证安全 | 69 |
| 测试时间 | 14:00:00 ~ 14:30:25 |
**1.2 OWASP API Security Top 10 覆盖矩阵**(表格):
| 编号 | 安全风险 | 用例数 | 🔴高危 | 🟠中危 | 🟡低危 | 🔵信息 | 🟢安全 |
|------|---------|--------|--------|--------|--------|--------|--------|
| API1 | 对象级别授权失效 | 8 | 2 | 1 | 0 | 0 | 5 |
| API2 | 身份认证失效 | 13 | 3 | 4 | 0 | 0 | 6 |
| ... | | | | | | | |
| **合计** | | **90** | **8** | **12** | **0** | **0** | **69** |
> 按模块统计每个 OWASP 类别的漏洞分布,一眼看出哪个类别风险最高。
#### 二、漏洞详情
按风险等级排序(critical > high > medium > low > info),每个漏洞包含:
```markdown
### 2.1 [高危] 水平越权访问其他用户会议详情
- **用例编号**: 2.1.1
- **风险等级**: 🔴 高危
- **漏洞描述**: user 使用 Token 成功访问了 superadmin 的会议详情,存在水平越权漏洞
**复现步骤 — 请求信息:**
请求方法: GET
请求路径: /api/message/getMessageById
查询参数: {"id": "12345"}
使用 Token: user (越权访问 superadmin 的会议 ID: 12345)
**响应信息:**
状态码: 200
响应体: {"code":200,"data":{"id":12345,"title":"管理员会议"...}}
**修复建议**: 在接口中增加用户身份校验,确保用户只能访问自己创建的会议数据
---
```
> 含完整的请求/响应信息,可直接复现漏洞。
#### 三、已验证安全项
按 OWASP 模块分组,展示通过的测试用例:
```markdown
### API1 对象级别授权失效
| 编号 | 测试用例 | 结果 |
|------|---------|------|
| 2.1.5 | 水平越权访问运维集控设备接口 | ✅ 安全 |
| 2.1.6 | 水平越权跨公司数据访问 | ✅ 安全 |
```
#### 四、风险评估与修复建议
按风险等级给出通用修复方案:
```markdown
### 4.1 高危风险(需立即修复)
#### 越权漏洞(API1 / API3)
- 问题描述:普通用户可访问其他用户的数据
- 影响范围:用户数据泄露、数据篡改
- 修复建议:
1. 后端接口增加严格的资源所有权校验
2. 实现基于 RBAC 的权限控制
3. 在 API 网关层增加统一的权限拦截器
#### 身份认证缺陷(API2)
- 问题描述:Token 伪造可用、暴力破解无限制
- 修复建议:
1. 加强 Token 校验机制
2. 实现 Token 黑名单机制
3. 登录接口增加速率限制
4. 验证码应随机生成
```
#### 五、已知安全问题清单
测试前已确认的问题(非本次测试发现,来自需求文档/抓包):
| 编号 | 问题描述 | 风险等级 | 来源 |
|------|---------|---------|------|
| KN-01 | 维护平台验证码固定为 csba | 🟠中危 | 需求文档 |
| KN-02 | 登录密码 SHA256 无加盐 | 🟠中危 | 网络抓包 |
| KN-03 | company_secret 明文出现在 URL | 🔴高危 | 网络抓包 |
#### 六、历史漏洞回归验证
基于 35 个历史漏洞报告的回归测试结果:
| 编号 | 漏洞来源 | 测试项 | 当前状态 | 风险等级 |
|------|---------|--------|---------|---------|
| HV-001 | 长安深蓝汽车 | SQL注入-会议预定接口 | ✅ 已修复 | 高危 |
| HV-005 | 南山区委 | Nacos未授权访问 | 🔴 未修复 | 高危 |
| HV-026 | 新统一平台 | NoSQL注入-登录接口 | 🔴 未修复 | 严重 |
#### 七、华为安全红线合规性检查
22 项华为安全红线的合规状态:
| 检查编号 | 检查类别 | 要求 | 测试结果 | 合规状态 |
|---------|---------|------|---------|---------|
| HW-01 | 加密规范 | 密码存储加盐哈希 | SHA256无盐 | 🔴 不合规 |
| HW-06 | 鉴权机制 | 注销后Token立即失效 | Token仍可用 | 🔴 不合规 |
| HW-10 | 配置管理 | Swagger不暴露 | 已拦截 | ✅ 合规 |
---
## 三、平台报告设计方案(待实现)
### 3.1 数据来源
平台已有数据可直接复用,无需新增采集:
| 报告章节 | 数据来源 |
|---------|---------|
| 执行概要 | `VulnerabilityResult` 按 level 统计 |
| OWASP 覆盖矩阵 | `VulnerabilityResult.test_id` 前缀分组 |
| 漏洞详情 | `VulnerabilityResult`(request_info/response_info/fix_suggestion) |
| 已验证安全项 | `VulnerabilityResult` where is_vulnerable=False |
| 历史漏洞回归 | `VulnerabilityResult.extra_data` 标记 is_regression |
| 华为红线检查 | `VulnerabilityResult.extra_data` 标记 is_huawei_redline |
| 已知问题清单 | 知识库 `KNOWN_ISSUES`(静态数据) |
### 3.2 生成时机
安全测试执行完成(`_run_security_execution` 末尾)自动生成 Markdown 报告:
- 路径:`backend/data/reports/{server_ip}_安全测试报告_{timestamp}.md`
- 同时在 `Execution` 记录存储报告路径(`config.report_path`
### 3.3 后端实现要点
**新增** `backend/app/services/security_report_service.py`(迁移参考实现 `report_generator.py`):
| 参考实现 | 平台适配 |
|---------|---------|
| `VulnResult` 数据类 | 直接用 `VulnerabilityResult` ORM |
| `config.yaml` 读 target | 从 `SecurityConfig` 读 target_url |
| `KNOWLEDGE_BASE` 常量 | 从 `services/knowledge_base.py` 导入 |
| 独立运行 | 在 `_run_security_execution` 末尾调用 |
| 网盘上传 | 可选,先不做 |
**新增 API**`routers/security.py`):
- `GET /api/security/executions/{id}/report` — 返回报告内容(Markdown)
- `GET /api/security/executions/{id}/report/download` — 下载 .md 文件
### 3.4 前端实现要点
**修改** `frontend/src/views/Reports.vue`(或 `views/security/ReportTab.vue`):
| 区域 | 实现 |
|------|------|
| 报告列表 | 展示安全测试执行记录,点击进入报告详情 |
| 报告预览 | 用 `markdown-it` 渲染 Markdown(需加依赖) |
| 下载按钮 | 调用 download API 下载 .md |
### 3.5 风险等级体系对齐
参考实现用中文等级(高危/中危/低危/信息类),平台用英文(critical/high/medium/low/info)。报告生成时需映射:
| 平台 level | 报告显示 | emoji |
|-----------|---------|-------|
| critical | 严重 | 🔴🔴🔴 |
| high | 高危 | 🔴 |
| medium | 中危 | 🟠 |
| low | 低危 | 🟡 |
| info | 信息 | 🔵 |
---
## 四、实现优先级
| 优先级 | 任务 | 工作量 |
|--------|------|--------|
| P0 | 后端报告生成服务(7章节 Markdown) | 中 |
| P0 | 后端报告查看/下载 API | 小 |
| P1 | 前端报告列表 + Markdown 预览 | 中 |
| P2 | 网盘上传(参考实现功能) | 小 |
| P2 | ERP 任务创建(参考实现功能) | 中 |
---
## 五、与参考实现的差异
| 对比项 | 参考实现 | 平台方案 |
|--------|---------|---------|
| 运行方式 | 独立 Python 脚本 | 集成到平台,执行后自动生成 |
| 报告存储 | 本地 reports/ 目录 | 平台 data/reports/ + DB 记录路径 |
| 数据来源 | 内存 VulnResult 列表 | DB VulnerabilityResult 表 |
| 查看方式 | 打开 .md 文件 | 前端在线预览 + 下载 |
| 网盘/ERP | 有 | 暂不做(可选) |
---
*本文档为安全测试报告的设计分析,待实现时参考。*
# 安全测试模块 - 计划执行文档
> **文档版本**: v1.0
> **创建日期**: 2026-07-21
> **关联需求**: `_PRD_安全测试模块需求文档.md`
> **执行策略**: 分阶段迭代,每阶段可独立验证
---
## 一、执行总览
### 1.1 阶段划分
| 阶段 | 名称 | 核心交付 | 预计工期 |
|------|------|---------|---------|
| Phase 1 | 后端安全测试引擎 | SecurityExecutor + HttpClient + AuthHelper | 3天 |
| Phase 2 | 前端安全测试页面 | Security.vue + 路由 + API封装 | 2天 |
| Phase 3 | OWASP Top 10 用例导入 | 90+ 安全测试用例 | 2天 |
| Phase 4 | 报告生成与导出 | Markdown报告 + 前端展示 | 1天 |
| Phase 5 | 知识库集成与联调 | 历史漏洞 + 华为红线 + 端到端测试 | 1天 |
### 1.2 依赖关系
```
Phase 1 (后端引擎) ──→ Phase 2 (前端页面) ──→ Phase 4 (报告)
──→ Phase 3 (用例导入) ──→ Phase 5 (知识库+联调)
```
---
## 二、Phase 1: 后端安全测试引擎
### 2.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 1.1 | 创建安全测试配置模型 | `backend/app/models/security_config.py` | SecurityConfig ORM |
| 1.2 | 创建漏洞结果模型 | `backend/app/models/vulnerability_result.py` | VulnerabilityResult ORM |
| 1.3 | 创建安全测试配置Schema | `backend/app/schemas/security.py` | Pydantic 校验 |
| 1.4 | 迁移 HttpClient | `backend/app/executors/http_client.py` | 从参考实现迁移,适配平台 |
| 1.5 | 迁移 AuthHelper | `backend/app/executors/auth_helper.py` | 多账号登录 |
| 1.6 | 开发 SecurityExecutor | `backend/app/executors/security_executor.py` | 安全测试执行引擎 |
| 1.7 | 开发断言引擎 | `backend/app/executors/assertion_engine.py` | 漏洞判定逻辑 |
| 1.8 | 创建安全测试服务 | `backend/app/services/security_service.py` | 业务逻辑层 |
| 1.9 | 创建安全测试路由 | `backend/app/routers/security.py` | API 路由 |
| 1.10 | 注册路由到 main.py | `backend/app/main.py` | 添加 security 路由 |
| 1.11 | 数据库迁移 | `backend/app/database.py` | 新增表初始化 |
### 2.2 详细设计
#### 2.2.1 SecurityExecutor 核心类
```python
class SecurityExecutor:
"""
安全测试执行器
与 PlaywrightExecutor 并列,负责 API 安全测试的执行。
不依赖浏览器,使用 requests 库发送 HTTP 请求。
"""
def __init__(self, config: dict):
self.client = HttpClient(config)
self.auth = AuthHelper(self.client)
self.results: List[VulnResult] = []
self._is_running = False
def start(self) -> bool:
"""初始化执行环境,登录所有账号"""
def execute_case(self, case: TestCase) -> CaseSecurityResult:
"""执行单个安全测试用例"""
def stop(self):
"""停止执行"""
def _execute_step(self, step: dict) -> StepSecurityResult:
"""执行单个测试步骤"""
def _evaluate_assertion(self, assertion: dict, response) -> bool:
"""评估断言,判定是否存在漏洞"""
```
#### 2.2.2 HttpClient 适配
从参考实现迁移,关键适配点:
| 参考实现 | 平台适配 | 说明 |
|---------|---------|------|
| `config.yaml` 文件读取 | 从 `SecurityConfig` 数据库模型读取 | 配置持久化 |
| `utils.logger` | `logging.getLogger` | 统一日志 |
| `VulnResult` 数据类 | `VulnerabilityResult` ORM | 结果持久化 |
| 独立运行 | 集成到 `ExecutionService` | 统一调度 |
#### 2.2.3 执行流程集成
```python
# execution_service.py 扩展
async def run_execution(self, execution_id: str):
execution = await self._get_execution(execution_id)
if execution.case_type == "security":
# 安全测试走 SecurityExecutor
executor = SecurityExecutor(config)
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self._run_security, executor, execution)
else:
# UI 测试走 PlaywrightExecutor
executor = PlaywrightExecutor()
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self._run_ui, executor, execution)
```
#### 2.2.4 API 路由设计
```python
# backend/app/routers/security.py
@router.get("/config") # 获取安全测试配置
@router.put("/config") # 更新安全测试配置
@router.post("/executions") # 创建安全测试执行
@router.get("/executions/{execution_id}") # 获取执行详情
@router.get("/reports/{execution_id}") # 获取安全测试报告
@router.get("/knowledge-base") # 获取知识库
@router.get("/knowledge-base/vulns") # 获取历史漏洞列表
@router.get("/knowledge-base/redlines") # 获取华为红线列表
```
### 2.3 验收标准
- [ ] SecurityExecutor 可初始化并登录测试账号
- [ ] 可执行单个安全测试用例并返回结果
- [ ] 漏洞结果正确持久化到数据库
- [ ] API 接口可通过 Swagger 文档测试
- [ ] WebSocket 实时推送执行进度
---
## 三、Phase 2: 前端安全测试页面
### 3.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 2.1 | 创建安全测试页面 | `frontend/src/views/Security.vue` | 主页面(Tab 切换) |
| 2.2 | 创建配置管理组件 | `frontend/src/views/security/ConfigTab.vue` | 目标/账号/认证配置 |
| 2.3 | 创建用例管理组件 | `frontend/src/views/security/CasesTab.vue` | OWASP 用例列表 |
| 2.4 | 创建执行中心组件 | `frontend/src/views/security/ExecutionTab.vue` | 执行+实时进度 |
| 2.5 | 创建报告中心组件 | `frontend/src/views/security/ReportTab.vue` | 报告查看+导出 |
| 2.6 | 创建 API 封装 | `frontend/src/api/security.ts` | axios 调用 |
| 2.7 | 创建类型定义 | `frontend/src/types/security.ts` | TypeScript 类型 |
| 2.8 | 添加路由 | `frontend/src/router/index.ts` | /security 路由 |
| 2.9 | 侧边栏添加入口 | `frontend/src/App.vue` | Shield 图标 |
### 3.2 页面设计
#### Security.vue 主页面
```
┌─────────────────────────────────────────────────┐
│ 🛡️ 安全测试 │
├─────────────────────────────────────────────────┤
│ [配置管理] [用例管理] [执行中心] [报告中心] │
├─────────────────────────────────────────────────┤
│ │
│ (Tab 内容区域) │
│ │
└─────────────────────────────────────────────────┘
```
#### 配置管理 Tab
```
┌─────────────────────────────────────────────────┐
│ 目标服务器 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 服务器地址: [https://192.168.5.44 ] │ │
│ │ SSL验证: [✗] 超时: [30]秒 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 测试账号 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 超级管理员: superadmin / ******** [测试] │ │
│ │ 管理员: admin@aq / ******** [测试] │ │
│ │ 普通用户: user@aq / ******** [测试] │ │
│ │ 验证码: [csba] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 认证配置 │
│ ┌─────────────────────────────────────────────┐ │
│ │ Token类型: [accessToken] 机制: [JWT] │ │
│ │ 登录路径: [/platform/api/auth/login] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ 限流配置 │
│ ┌─────────────────────────────────────────────┐ │
│ │ 暴力破解上限: [20] 限流测试上限: [100] │ │
│ │ 请求间隔: [0.5]秒 │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ [保存配置] │
└─────────────────────────────────────────────────┘
```
#### 用例管理 Tab
```
┌─────────────────────────────────────────────────┐
│ OWASP API Security Top 10 │
│ │
│ ┌─ API1 对象级别授权失效 ──────────── [8用例] ─┐ │
│ │ ✅ 2.1.1 水平越权访问其他用户会议详情 │ │
│ │ ✅ 2.1.2 水平越权修改/取消其他用户会议 │ │
│ │ ✅ 2.1.3 水平越权访问其他用户收藏列表 │ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─ API2 身份认证失效 ────────────── [13用例] ─┐ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 历史漏洞回归 ──────────────────────── [35条] │
│ 华为安全红线 ──────────────────────── [22项] │
│ │
│ [全选] [反选] [执行选中用例] │
└─────────────────────────────────────────────────┘
```
#### 执行中心 Tab
```
┌─────────────────────────────────────────────────┐
│ 执行进度 │
│ │
│ ████████████████░░░░ 72/90 用例完成 │
│ │
│ ┌─ 实时日志 ──────────────────────────────────┐ │
│ │ [14:05:30] ✅ 2.1.1 水平越权-会议详情 安全 │ │
│ │ [14:05:31] 🔴 2.1.2 水平越权-修改会议 漏洞! │ │
│ │ [14:05:32] ✅ 2.1.3 水平越权-收藏列表 安全 │ │
│ │ [14:05:33] ⏳ 2.1.4 水平越权-跨公司 执行中...│ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 统计: 🔴高危 3 | 🟠中危 5 | 🟡低危 0 | ✅安全 64 │
│ │
│ [暂停] [停止] │
└─────────────────────────────────────────────────┘
```
#### 报告中心 Tab
```
┌─────────────────────────────────────────────────┐
│ 安全测试报告 │
│ │
│ ┌─ 报告列表 ──────────────────────────────────┐ │
│ │ 192.168.5.44_安全测试报告_20260721.md [查看]│ │
│ │ 192.168.5.44_安全测试报告_20260715.md [查看]│ │
│ └──────────────────────────────────────────────┘ │
│ │
│ ┌─ 报告预览 ──────────────────────────────────┐ │
│ │ # 接口安全测试总报告 │ │
│ │ > 测试目标: https://192.168.5.44 │ │
│ │ │ │
│ │ ## 一、执行概要 │ │
│ │ | 统计项 | 数值 | │ │
│ │ |--------|------| │ │
│ │ | 总用例 | 90 | │ │
│ │ | 高危 | 3 | │ │
│ │ ... │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ [下载Markdown] [下载HTML] [上传网盘] │
└─────────────────────────────────────────────────┘
```
### 3.3 验收标准
- [ ] /security 路由可正常访问
- [ ] 侧边栏显示安全测试入口(Shield 图标)
- [ ] 配置管理可保存和加载
- [ ] 用例管理可展示 OWASP 模块和用例
- [ ] 执行中心可触发执行并实时显示进度
- [ ] 报告中心可查看和下载报告
---
## 四、Phase 3: OWASP Top 10 用例导入
### 4.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 3.1 | 创建用例导入脚本 | `backend/scripts/create_security_cases.py` | 批量创建安全测试用例 |
| 3.2 | 导入 API1 用例 | 同上 | 8个水平越权用例 |
| 3.3 | 导入 API2 用例 | 同上 | 13个身份认证用例 |
| 3.4 | 导入 API3 用例 | 同上 | 9个对象属性越权用例 |
| 3.5 | 导入 API4 用例 | 同上 | 7个资源消耗用例 |
| 3.6 | 导入 API5 用例 | 同上 | 8个功能越权用例 |
| 3.7 | 导入 API6 用例 | 同上 | 6个业务流用例 |
| 3.8 | 导入 API7 用例 | 同上 | 9个SSRF用例 |
| 3.9 | 导入 API8 用例 | 同上 | 15个安全配置用例 |
| 3.10 | 导入 API9 用例 | 同上 | 8个库存管理用例 |
| 3.11 | 导入 API10 用例 | 同上 | 7个第三方API用例 |
| 3.12 | 导入历史漏洞回归用例 | 同上 | 35个历史漏洞验证 |
| 3.13 | 导入华为红线检查用例 | 同上 | 22项红线检查 |
### 4.2 用例数据结构
每个安全测试用例的 `steps` 字段结构:
```json
{
"test_type": "api_security",
"target": {
"base_url": "https://192.168.5.44",
"path": "/api/message/getMessageById",
"method": "GET"
},
"auth": {
"required": true,
"account": "user",
"expect_different_account": "superadmin"
},
"request": {
"params": {"id": "${meeting_id}"},
"headers": {},
"body": null
},
"pre_steps": [
{
"action": "get_resource_id",
"account": "superadmin",
"path": "/api/message/getMeetingList",
"params": {"pageNo": 1, "pageSize": 5},
"extract": {"meeting_id": "data.records[0].id"}
}
],
"assertions": [
{
"type": "status_code_not_equal",
"expect": 200,
"description": "越权访问应被拒绝"
},
{
"type": "response_code_not_contain",
"expect": ["401", "403", "B0027"],
"description": "应返回权限不足"
}
],
"vulnerability": {
"id": "2.1.1",
"name": "水平越权访问其他用户会议详情",
"level": "high",
"description": "user 使用 Token 成功访问了 superadmin 的会议详情",
"fix_suggestion": "在接口中增加用户身份校验,确保用户只能访问自己创建的会议数据"
}
}
```
### 4.3 用例映射(参考实现 → 平台用例)
| 参考实现函数 | 平台用例ID | 用例名称 |
|-------------|-----------|---------|
| `test_2_1_1` | sec_2_1_1 | 水平越权访问其他用户会议详情 |
| `test_2_1_2` | sec_2_1_2a | 水平越权修改其他用户会议 |
| `test_2_1_2` | sec_2_1_2b | 水平越权取消其他用户会议 |
| `test_2_1_3` | sec_2_1_3 | 水平越权访问其他用户收藏列表 |
| `test_2_1_4` | sec_2_1_4 | 水平越权跨公司创建会议 |
| `test_2_1_5` | sec_2_1_5 | 水平越权访问运维集控设备接口 |
| `test_2_1_6` | sec_2_1_6 | 水平越权跨公司数据访问 |
| `test_2_1_7` | sec_2_1_7 | IDOR遍历访问签到记录 |
| `test_2_1_8` | sec_2_1_8 | IDOR遍历访问其他用户通知 |
| ... | ... | ... |
### 4.4 验收标准
- [ ] 10个 OWASP 模块全部创建
- [ ] 90+ 安全测试用例全部导入
- [ ] 35个历史漏洞回归用例导入
- [ ] 22项华为红线检查用例导入
- [ ] 用例可在前端用例管理页面查看
---
## 五、Phase 4: 报告生成与导出
### 5.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 4.1 | 迁移报告生成器 | `backend/app/services/security_report_service.py` | 从参考实现迁移 |
| 4.2 | 适配平台数据模型 | 同上 | VulnResult → VulnerabilityResult |
| 4.3 | 报告存储 | 同上 | 保存到 data/reports/ |
| 4.4 | 报告查看 API | `backend/app/routers/security.py` | 返回报告内容 |
| 4.5 | 前端报告展示 | `frontend/src/views/security/ReportTab.vue` | Markdown 渲染 |
| 4.6 | 报告导出 | 同上 | 下载 Markdown/HTML |
### 5.2 报告生成流程
```
1. 收集所有 VulnerabilityResult
2. 按风险等级排序
3. 生成 OWASP 覆盖矩阵
4. 生成漏洞详情(含复现步骤)
5. 生成修复建议
6. 附加历史漏洞回归结果
7. 附加华为红线检查结果
8. 输出 Markdown 文件
```
### 5.3 验收标准
- [ ] 执行完成后自动生成 Markdown 报告
- [ ] 报告包含完整的漏洞详情和修复建议
- [ ] 前端可预览报告内容
- [ ] 可下载 Markdown 格式报告
---
## 六、Phase 5: 知识库集成与联调
### 6.1 任务清单
| # | 任务 | 文件 | 说明 |
|---|------|------|------|
| 5.1 | 迁移知识库模块 | `backend/app/services/knowledge_base.py` | 历史漏洞+红线+载荷 |
| 5.2 | 知识库 API | `backend/app/routers/security.py` | 查询接口 |
| 5.3 | 前端知识库展示 | `frontend/src/views/security/KnowledgeTab.vue` | 浏览知识库 |
| 5.4 | 端到端联调 | - | 全流程测试 |
| 5.5 | 修复集成问题 | - | Bug 修复 |
### 6.2 知识库数据结构
```python
KNOWLEDGE_BASE = {
"historical_vulns": [...], # 35个历史漏洞
"huawei_redlines": [...], # 22项华为红线
"sql_injection_payloads": [...], # SQL注入载荷
"sensitive_paths": [...], # 敏感路径字典
"nginx_analysis": {...}, # Nginx配置分析
"appscan_summary": [...], # AppScan报告摘要
"iast_summary": [...], # IAST报告摘要
}
```
### 6.3 验收标准
- [ ] 知识库数据可通过 API 查询
- [ ] 前端可浏览历史漏洞和红线检查
- [ ] 端到端流程:配置 → 选用例 → 执行 → 报告 完整可用
- [ ] WebSocket 实时推送正常
---
## 七、技术要点与风险
### 7.1 关键技术决策
| 决策点 | 方案 | 理由 |
|--------|------|------|
| 执行引擎 | 独立 SecurityExecutor | 与 PlaywrightExecutor 解耦,不依赖浏览器 |
| 配置存储 | 数据库 SecurityConfig | 支持多环境配置切换 |
| 签名算法 | 迁移参考实现 | 已验证可用,避免重复开发 |
| 报告格式 | Markdown | 与参考实现一致,支持版本对比 |
| 用例存储 | 复用 TestCase 模型 | case_type=security 区分,统一管理 |
### 7.2 风险与缓解
| 风险 | 影响 | 缓解措施 |
|------|------|---------|
| 签名算法变更 | 用例无法执行 | 签名逻辑封装为独立模块,便于更新 |
| 测试账号不可用 | 越权测试无法进行 | 配置页面支持账号测试连接 |
| 被测系统限流 | 误报漏洞 | 请求间隔可配置,默认 0.5秒 |
| SQLite 并发写锁 | 执行结果丢失 | 复用现有 pool_pre_ping 方案 |
| Windows 子进程限制 | 执行阻塞 | SecurityExecutor 不启动子进程,无此问题 |
### 7.3 多窗口并行开发注意事项
根据 `Docs/多窗口并行开发指南.md`
| 项目 | 本窗口(安全测试) | 主窗口(会议管理) |
|------|-------------------|-------------------|
| 后端端口 | 8002 | 8001 |
| 前端端口 | 3001 | 3000 |
| 开发模块 | 安全测试(新模块) | 会议管理 |
| 数据库操作 | 错开时间 | 错开时间 |
| Playwright | 不执行UI用例 | 可执行UI用例 |
| Git 提交 | 提交前先 pull | 提交后 push |
---
## 八、文件清单(新增/修改)
### 8.1 新增文件
```
backend/app/models/security_config.py # 安全测试配置模型
backend/app/models/vulnerability_result.py # 漏洞结果模型
backend/app/schemas/security.py # 安全测试Schema
backend/app/executors/http_client.py # HTTP客户端(迁移)
backend/app/executors/auth_helper.py # 认证辅助(迁移)
backend/app/executors/security_executor.py # 安全测试执行器
backend/app/executors/assertion_engine.py # 断言引擎
backend/app/services/security_service.py # 安全测试服务
backend/app/services/security_report_service.py # 安全报告服务
backend/app/services/knowledge_base.py # 知识库
backend/app/routers/security.py # 安全测试路由
backend/scripts/create_security_cases.py # 用例导入脚本
frontend/src/views/Security.vue # 安全测试主页面
frontend/src/views/security/ConfigTab.vue # 配置管理
frontend/src/views/security/CasesTab.vue # 用例管理
frontend/src/views/security/ExecutionTab.vue # 执行中心
frontend/src/views/security/ReportTab.vue # 报告中心
frontend/src/views/security/KnowledgeTab.vue # 知识库
frontend/src/api/security.ts # API封装
frontend/src/types/security.ts # 类型定义
```
### 8.2 修改文件
```
backend/app/main.py # 注册 security 路由
backend/app/database.py # 新增表初始化
backend/app/models/__init__.py # 导出新模型
backend/app/services/execution_service.py # 支持 case_type=security
frontend/src/router/index.ts # 添加 /security 路由
frontend/src/App.vue # 侧边栏添加安全测试入口
```
---
## 九、执行检查清单
### Phase 1 完成标准
- [ ] `SecurityExecutor` 可初始化
- [ ] `HttpClient` 签名算法正确
- [ ] `AuthHelper` 可登录3个账号
- [ ] 安全测试 API 可通过 Swagger 测试
- [ ] 数据库表正确创建
### Phase 2 完成标准
- [ ] /security 页面可正常访问
- [ ] 4个Tab可正常切换
- [ ] 配置可保存和加载
- [ ] 用例列表可展示
### Phase 3 完成标准
- [ ] 10个OWASP模块创建成功
- [ ] 90+用例导入成功
- [ ] 前端可查看所有用例
### Phase 4 完成标准
- [ ] 执行后自动生成报告
- [ ] 报告可在前端预览
- [ ] 报告可下载
### Phase 5 完成标准
- [ ] 知识库可查询
- [ ] 端到端流程完整可用
- [ ] 无阻塞性Bug
---
*文档结束*
\ No newline at end of file
# PRD - 安全测试模块需求文档
> **文档版本**: v1.0
> **创建日期**: 2026-07-21
> **模块类型**: 安全测试
> **优先级**: P1
---
## 一、背景与目标
### 1.1 背景
当前平台已实现 UI 自动化测试功能,支持用例录制、执行和报告生成。但在实际项目交付过程中,还需要进行 API 接口安全测试,确保系统符合安全规范。
参考现有 `ApiSecurityTest` 工具的实现经验,该工具基于 OWASP API Security Top 10 标准,已成功应用于多个项目的安全测试:
- **长安深蓝汽车**: 35个历史漏洞回归验证
- **南山区委**: 渗透测试+XDR告警验证
- **天津海油**: 运维集控系统安全复测
- **厦门银行总行大厦**: 华为Web漏扫集成
- **新统一平台**: HCL AppScan + IAST 报告整合
### 1.2 目标
将安全测试能力集成到现有平台,实现:
1. **统一入口**: 安全测试用例与 UI 用例统一管理
2. **可视化执行**: 前端界面触发安全测试,实时查看进度
3. **标准化报告**: 生成符合 OWASP 标准的安全测试报告
4. **知识库复用**: 历史漏洞回归测试、华为安全红线检查
---
## 二、功能范围
### 2.1 核心功能
| 功能 | 描述 | 优先级 |
|------|------|--------|
| 安全测试模块管理 | 创建/编辑安全测试模块 | P0 |
| 安全测试用例管理 | 基于 OWASP API Top 10 的用例模板 | P0 |
| 安全测试执行引擎 | HTTP 请求 + 签名算法 + 结果判定 | P0 |
| 安全测试报告生成 | Markdown 格式,含漏洞详情和修复建议 | P0 |
| 多账号权限测试 | 支持 superadmin/admin/user 三角色越权测试 | P1 |
| 历史漏洞回归 | 35个历史漏洞的修复验证 | P1 |
| 华为安全红线 | 22项安全红线合规检查 | P1 |
| 知识库管理 | 安全测试载荷、漏洞模式库 | P2 |
### 2.2 OWASP API Security Top 10 覆盖
| 编号 | 安全风险 | 测试要点 | 用例数 |
|------|---------|---------|--------|
| API1 | 对象级别授权失效 | 水平越权、IDOR遍历、company_id篡改 | 8 |
| API2 | 身份认证失效 | Token伪造、NoSQL注入、暴力破解、注销失效 | 13 |
| API3 | 对象属性级别授权失效 | 垂直越权、API成批分配 | 9 |
| API4 | 资源消耗不受限 | 速率限制、文件上传、分页滥用 | 7 |
| API5 | 功能级别授权失效 | 普通用户访问管理员接口 | 8 |
| API6 | 无限制访问敏感业务流 | 批量注册、短信轰炸、NoLogin接口滥用 | 6 |
| API7 | 服务器端请求伪造 | SSRF内网探测(Nacos/Redis/MySQL) | 9 |
| API8 | 安全配置错误 | Nacos未授权、Swagger暴露、SQL注入、安全响应头缺失 | 15 |
| API9 | 库存管理不当 | 隐藏接口、旧版API、内部API暴露 | 8 |
| API10 | 不安全的第三方API集成 | 讯飞/腾讯/钉钉/华为云凭证泄露 | 7 |
**总计**: 90+ 安全测试用例
---
## 三、详细需求
### 3.1 安全测试模块管理
#### 3.1.1 模块类型扩展
现有平台 `Module` 模型已有 `module_type` 字段,需扩展支持:
```python
module_type: Mapped[str] = mapped_column(
String(20),
default="standard",
comment="模块类型: standard(标准模块)/custom(项目定制模块)/security(安全测试模块)"
)
```
#### 3.1.2 安全测试模块预置
系统初始化时预置 10 个 OWASP 模块:
| 模块ID | 名称 | 描述 |
|--------|------|------|
| sec_api01 | API1 - 对象级别授权失效 | 水平越权、IDOR测试 |
| sec_api02 | API2 - 身份认证失效 | Token伪造、注入、暴力破解 |
| sec_api03 | API3 - 对象属性级别授权失效 | 垂直越权、成批分配 |
| sec_api04 | API4 - 资源消耗不受限 | 速率限制、DoS测试 |
| sec_api05 | API5 - 功能级别授权失效 | RBAC绕过测试 |
| sec_api06 | API6 - 无限制访问敏感业务流 | 业务逻辑漏洞 |
| sec_api07 | API7 - 服务器端请求伪造 | SSRF内网探测 |
| sec_api08 | API8 - 安全配置错误 | 配置审计、敏感路径 |
| sec_api09 | API9 - 库存管理不当 | API资产管理 |
| sec_api10 | API10 - 不安全的第三方API集成 | 第三方凭证泄露 |
### 3.2 安全测试用例模型
#### 3.2.1 用例类型扩展
现有 `TestCase` 模型已有 `case_type` 字段,需扩展支持:
```python
case_type: Mapped[str] = mapped_column(
String(20),
default="ui",
comment="用例类型: ui/api/security/deploy/performance/functional"
)
```
#### 3.2.2 安全测试用例结构
安全测试用例的 `steps` 字段采用特殊结构:
```json
{
"test_type": "api_security",
"target": {
"base_url": "https://192.168.5.44",
"path": "/api/message/getMessageById",
"method": "GET"
},
"auth": {
"required": true,
"account": "user",
"expect_different_account": "superadmin"
},
"request": {
"params": {"id": "${meeting_id}"},
"headers": {},
"body": null
},
"assertions": [
{
"type": "status_code",
"expect": 200,
"mode": "should_not_equal",
"description": "越权访问应被拒绝"
},
{
"type": "response_code",
"expect": "403",
"mode": "should_contain",
"description": "应返回权限不足"
}
],
"vulnerability": {
"id": "2.1.1",
"name": "水平越权访问其他用户会议详情",
"level": "high",
"description": "user 使用 Token 成功访问了 superadmin 的会议详情",
"fix_suggestion": "在接口中增加用户身份校验"
}
}
```
### 3.3 安全测试执行引擎
#### 3.3.1 执行器架构
```
SecurityExecutor
├── HttpClient(复用参考实现)
│ ├── 自动签名(AES-CBC)
│ ├── Token 管理
│ └── 重试机制
├── AuthHelper
│ ├── 多账号登录
│ └── Token 刷新
└── AssertionEngine
├── 状态码断言
├── 响应体断言
└── 业务码断言
```
#### 3.3.2 执行流程
```
1. 初始化 HttpClient(加载配置)
2. 登录测试账号(superadmin/admin/user)
3. 执行前置准备(获取测试数据)
4. 发送测试请求(带签名)
5. 判定漏洞状态
6. 记录结果
7. 生成报告
```
#### 3.3.3 签名算法
沿用现有签名机制(从前端 JS 逆向):
```
1. x_random = 随机字符串(8-16位)
2. x_timestamp = 当前毫秒时间戳
3. sign_str = timestamp + JSON.stringify(body) + random
4. sign_hash = SHA256(sign_str)
5. aes_key_source = SHA256(bearer_token 或 随机60位)
6. aes_key = aes_key_source[16:32]
7. aes_iv = aes_key_source[0:8] + aes_key_source[-8:]
8. x_sign = AES.encrypt(sign_hash, aes_key, aes_iv) → Base64
```
### 3.4 安全测试报告
#### 3.4.1 报告结构
```markdown
# 接口安全测试总报告
> **生成时间**: 2026-07-21
> **测试目标**: https://192.168.5.44
> **测试标准**: OWASP API Security Top 10 (2019)
## 一、执行概要
- 测试统计总览
- OWASP 覆盖矩阵
## 二、漏洞详情
- 按风险等级排序
- 复现步骤
- 修复建议
## 三、已验证安全项
## 四、风险评估与修复建议
## 五、已知安全问题清单
## 六、历史漏洞回归验证
## 七、华为安全红线合规性检查
## 八、测试环境与工具
```
#### 3.4.2 风险等级
| 等级 | 颜色标记 | 说明 |
|------|---------|------|
| Critical | 🔴🔴🔴 | 严重漏洞,需立即修复(如 NoSQL 注入 CVSS 9.4) |
| High | 🔴 | 高危漏洞,需优先修复 |
| Medium | 🟠 | 中危漏洞,建议尽快修复 |
| Low | 🟡 | 低危漏洞,建议择期修复 |
| Info | 🔵 | 信息类,可作参考 |
### 3.5 知识库模块
#### 3.5.1 历史漏洞回归测试
从参考实现提取 35 个历史漏洞:
| 漏洞ID | 标题 | 来源项目 | 严重等级 |
|--------|------|---------|---------|
| HV-001 | SQL注入 - 会议预定接口 | 长安深蓝汽车 | High |
| HV-005 | Nacos 未授权访问 | 南山区委 | High |
| HV-011 | Nacos serverIdentity 权限绕过 | 长安深蓝汽车 | High |
| HV-016 | JWT弱密钥 | 长安深蓝汽车 | High |
| HV-026 | NoSQL注入 - 登录接口 | 新统一平台 | Critical |
| ... | ... | ... | ... |
#### 3.5.2 华为安全红线检查
22 项华为安全红线检查:
| 检查ID | 类别 | 要求 |
|--------|------|------|
| HW-01 | 加密规范 | 密码存储必须使用加盐哈希 |
| HW-02 | 加密规范 | 密钥不能硬编码在代码中 |
| HW-03 | 传输安全 | 必须使用 HTTPS |
| HW-04 | 传输安全 | TLS 版本不低于 1.2 |
| ... | ... | ... |
#### 3.5.3 测试载荷库
- SQL 注入载荷(21条)
- 敏感路径字典(70+条)
- SSRF 内网探测目标
- NoSQL 注入载荷
---
## 四、非功能需求
### 4.1 性能要求
| 指标 | 要求 |
|------|------|
| 单个用例执行时间 | < 30秒 |
| 90个用例批量执行 | < 15分钟 |
| 报告生成时间 | < 10秒 |
### 4.2 安全要求
- 测试账号密码加密存储
- 报告文件访问需鉴权
- 敏感接口限制内网访问
### 4.3 兼容性
- Python 3.8+
- 支持并发执行(线程池)
- Windows / Linux 跨平台
---
## 五、接口设计
### 5.1 后端 API
| 方法 | 路径 | 描述 |
|------|------|------|
| GET | /api/security/config | 获取安全测试配置 |
| PUT | /api/security/config | 更新安全测试配置 |
| POST | /api/security/executions | 创建安全测试执行 |
| GET | /api/security/executions/{id} | 获取执行详情 |
| GET | /api/security/reports/{id} | 获取安全测试报告 |
| GET | /api/security/knowledge-base | 获取知识库内容 |
### 5.2 WebSocket 推送
```
Topic: /topic/security/{execution_id}
消息格式:
{
"type": "case_result",
"data": {
"case_id": "sec_2.1.1",
"status": "passed|failed",
"is_vulnerable": false,
"duration": 1.23
}
}
```
---
## 六、数据模型
### 6.1 新增模型
#### SecurityConfig(安全测试配置)
```python
class SecurityConfig(Base):
__tablename__ = "security_configs"
id: Mapped[str] = mapped_column(String(32), primary_key=True)
name: Mapped[str] = mapped_column(String(100))
target_url: Mapped[str] = mapped_column(String(500))
verify_ssl: Mapped[bool] = mapped_column(default=False)
timeout: Mapped[int] = mapped_column(default=30)
# 测试账号
accounts: Mapped[dict] = mapped_column(JSON)
# 认证配置
auth_config: Mapped[dict] = mapped_column(JSON)
# 限流配置
rate_limits: Mapped[dict] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime)
updated_at: Mapped[datetime] = mapped_column(DateTime)
```
#### VulnerabilityResult(漏洞结果)
```python
class VulnerabilityResult(Base):
__tablename__ = "vulnerability_results"
id: Mapped[str] = mapped_column(String(32), primary_key=True)
execution_id: Mapped[str] = mapped_column(String(32), ForeignKey("executions.id"))
case_id: Mapped[str] = mapped_column(String(32))
# 漏洞信息
vuln_id: Mapped[str] = mapped_column(String(20))
name: Mapped[str] = mapped_column(String(200))
level: Mapped[str] = mapped_column(String(20)) # critical/high/medium/low/info
description: Mapped[str] = mapped_column(Text)
# 请求/响应信息
request_info: Mapped[str] = mapped_column(Text)
response_info: Mapped[str] = mapped_column(Text)
# 结果
is_vulnerable: Mapped[bool] = mapped_column(default=False)
fix_suggestion: Mapped[str] = mapped_column(Text)
# 元数据
metadata: Mapped[dict] = mapped_column(JSON)
created_at: Mapped[datetime] = mapped_column(DateTime)
```
---
## 七、前端设计
### 7.1 页面结构
```
/security
├── 配置管理
│ ├── 目标服务器配置
│ ├── 测试账号配置
│ └── 认证参数配置
├── 用例管理
│ ├── OWASP Top 10 模块
│ ├── 历史漏洞回归
│ └── 华为安全红线
├── 执行中心
│ ├── 新建执行
│ ├── 执行进度
│ └── 实时日志
└── 报告中心
├── 报告列表
├── 报告详情
└── 报告导出
```
### 7.2 侧边栏扩展
在现有侧边栏增加"安全测试"入口:
```typescript
const menuItems = [
{ icon: 'Odometer', title: '测试总览', path: '/' },
{ icon: 'Folder', title: '模块管理', path: '/modules' },
{ icon: 'Document', title: '用例管理', path: '/cases' },
{ icon: 'VideoCamera', title: '用例录制', path: '/recorder' },
{ icon: 'CaretRight', title: '执行中心', path: '/execution' },
{ icon: 'DataAnalysis', title: '报告中心', path: '/reports' },
{ icon: 'Shield', title: '安全测试', path: '/security' }, // 新增
{ icon: 'Setting', title: '系统配置', path: '/settings' }
]
```
---
## 八、里程碑
| 阶段 | 内容 | 工期 |
|------|------|------|
| Phase 1 | 后端安全测试引擎开发 | 3天 |
| Phase 2 | 前端安全测试页面开发 | 2天 |
| Phase 3 | OWASP Top 10 用例导入 | 2天 |
| Phase 4 | 报告生成与导出 | 1天 |
| Phase 5 | 知识库集成与测试 | 1天 |
**总工期**: 约 9 个工作日
---
## 九、参考资料
- `临时目录/安全测试/ApiSecurityTest/` - 参考实现
- OWASP API Security Top 10 (2019)
- 华为安全红线规范
- HCL AppScan 报告格式
- IAST 检测报告格式
---
## 十、附录
### 10.1 依赖包
```txt
requests>=2.28.0
pycryptodome>=3.18.0
colorama>=0.4.6
pyyaml>=6.0
```
### 10.2 配置文件示例
```yaml
# security_config.yaml
target:
base_url: "https://192.168.5.44"
verify_ssl: false
timeout: 30
accounts:
superadmin:
username: "superadmin"
password: "Ubains@1357"
admin:
username: "admin@aq"
password: "Ubains@1357"
user:
username: "user@aq"
password: "Ubains@1357"
captcha: "csba"
limits:
brute_force_max: 20
rate_limit_max: 100
request_interval: 0.5
```
---
*文档结束*
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:auth_helper.py
模块描述:安全测试认证辅助模块,提供多账号登录、Token 管理等功能
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
import logging
from typing import Dict, Optional
logger = logging.getLogger(__name__)
class AuthHelper:
"""
认证辅助类,管理多账号登录和 Token 获取
支持三个测试账号(superadmin/admin/user)的登录和 Token 管理,
用于越权测试场景中获取不同角色的 Token。
"""
# 支持的账号键名
ACCOUNT_KEYS = ["superadmin", "admin", "user"]
def __init__(self, http_client):
"""
初始化认证辅助
Args:
http_client: HttpClient 实例
"""
self.client = http_client
def login_all_accounts(self) -> Dict[str, Optional[str]]:
"""
使用全部配置账号登录
Returns:
dict: {账号键名: Token} 的字典,失败的账号值为 None
"""
results = {}
for account_key in self.ACCOUNT_KEYS:
logger.info(f"尝试登录账号: {account_key}")
token = self.client.login(account_key)
results[account_key] = token
if token:
logger.info(f"账号 [{account_key}] 登录成功")
else:
logger.warning(f"账号 [{account_key}] 登录失败")
return results
def get_superadmin_token(self) -> Optional[str]:
"""获取超管 Token"""
return self.client.get_token("superadmin")
def get_admin_token(self) -> Optional[str]:
"""获取管理员 Token"""
return self.client.get_token("admin")
def get_user_token(self) -> Optional[str]:
"""获取普通用户 Token"""
return self.client.get_token("user")
def get_token(self, account_key: str) -> Optional[str]:
"""
获取指定账号的 Token
Args:
account_key: 账号键名
Returns:
str: Token 字符串,失败返回 None
"""
return self.client.get_token(account_key)
def refresh_all_tokens(self) -> Dict[str, Optional[str]]:
"""
刷新所有 Token(清除缓存后重新登录)
Returns:
dict: {账号键名: 新Token}
"""
self.client.clear_token()
return self.login_all_accounts()
def get_tokens_dict(self) -> Dict[str, str]:
"""
获取所有已缓存的 Token
Returns:
dict: {账号键名: Token}
"""
return dict(self.client._tokens)
def test_account(self, account_key: str) -> Dict[str, object]:
"""
测试指定账号是否可以登录
Args:
account_key: 账号键名
Returns:
dict: {"success": bool, "token_preview": str, "error": str}
"""
# 清除旧 Token,强制重新登录
self.client.clear_token(account_key)
token = self.client.login(account_key)
if token:
return {
"success": True,
"token_preview": f"{token[:20]}...{token[-10:]}",
"error": ""
}
else:
return {
"success": False,
"token_preview": "",
"error": f"账号 {account_key} 登录失败"
}
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:http_client.py
模块描述:安全测试 HTTP 客户端,封装 requests 库,自动管理 Token、请求签名、日志记录
签名算法(从前端 JS 逆向):
1. x_random = 随机字符串(8-16位字母数字)
2. x_timestamp = 当前毫秒时间戳
3. sign_str = timestamp + JSON.stringify(body) + random (有body时)
sign_str = timestamp + random (无body时)
4. sign_hash = SHA256(sign_str) → 中间哈希
5. aes_key_source = SHA256(bearer_token 或 随机60位) → 64字符hex
6. aes_key = aes_key_source[16:32] → 32字符hex=16字节
7. aes_iv = aes_key_source[0:8] + aes_key_source[-8:] → 16字符hex=8字节
8. x_sign = AES.encrypt(sign_hash, aes_key, aes_iv) → Base64编码
9. 无 Token 时额外生成 RandomCode 头部(60位随机字符串)
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
import hashlib
import random
import string
import time
import json
import logging
from typing import Optional, Dict, Any
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# 尝试导入 pycryptodome,不存在则降级
try:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64 as b64
HAS_CRYPTO = True
except ImportError:
HAS_CRYPTO = False
logger = logging.getLogger(__name__)
class HttpClient:
"""
HTTP 请求客户端,支持自动 Token 管理和请求签名
用于安全测试的 HTTP 请求发送,自动处理:
- 请求签名(AES-CBC)
- Token 管理
- 重试机制
- SSL 验证
"""
def __init__(self, config: Dict[str, Any]):
"""
初始化 HTTP 客户端
Args:
config: 配置字典,包含:
- target.base_url: 目标服务器地址
- target.verify_ssl: 是否验证SSL
- target.timeout: 请求超时时间
- accounts: 测试账号配置
- auth: 认证配置
"""
self.config = config
# 目标配置
target = config.get("target", {})
self.base_url = target.get("base_url", "")
self.verify_ssl = target.get("verify_ssl", False)
self.timeout = target.get("timeout", 30)
# Token 存储:{账号名: token字符串}
self._tokens: Dict[str, str] = {}
# 验证码 uuid 存储
self._captcha_uuid: Optional[str] = None
# 创建带重试机制的 session
self.session = self._create_session()
# 检查加密库
if not HAS_CRYPTO:
logger.warning("pycryptodome 未安装,AES 签名将不可用。请执行: pip install pycryptodome")
def _create_session(self) -> requests.Session:
"""
创建带重试机制的 requests Session
Returns:
requests.Session: 配置好的会话对象
"""
session = requests.Session()
# 重试策略:最多重试 2 次,仅对连接错误重试
retry_strategy = Retry(
total=2,
backoff_factor=0.5,
status_forcelist=[500, 502, 503, 504],
allowed_methods=["GET", "POST", "PUT", "DELETE"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
session.mount("https://", adapter)
session.mount("http://", adapter)
# 禁用 SSL 警告(测试环境)
if not self.verify_ssl:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
return session
@staticmethod
def encrypt_password(password: str) -> str:
"""
使用 SHA256 加密密码(与前端加密方式一致)
Args:
password: 明文密码
Returns:
str: SHA256 十六进制摘要
"""
return hashlib.sha256(password.encode('utf-8')).hexdigest()
def _generate_random_string(self, min_len: int = 8, max_len: int = 16, digits: bool = True) -> str:
"""
生成指定长度的随机字符串
Args:
min_len: 最小长度
max_len: 最大长度
digits: 是否包含数字
Returns:
str: 随机字符串
"""
length = random.randint(min_len, max_len)
chars = string.ascii_letters + (string.digits if digits else '')
return ''.join(random.choice(chars) for _ in range(length))
@staticmethod
def _sha256(text: str) -> str:
"""计算 SHA256 哈希值"""
return hashlib.sha256(text.encode('utf-8')).hexdigest()
@staticmethod
def _aes_encrypt(plaintext: str, key_str: str, iv_str: str) -> str:
"""
AES CBC 模式加密(与前端 CryptoJS 行为一致)
CryptoJS 传入字符串时按 UTF-8 编码为密钥/IV,不是 hex 解析
Args:
plaintext: 明文字符串(SHA256 后的 hex 字符串)
key_str: 16字符 AES 密钥字符串
iv_str: 16字符 AES IV 字符串
Returns:
str: Base64 编码的密文
"""
if not HAS_CRYPTO:
return plaintext # 降级:无加密库时返回原文
# CryptoJS 传入字符串时直接按 UTF-8 编码为 bytes
key_bytes = key_str.encode('utf-8') # 16 bytes (AES-128)
iv_bytes = iv_str.encode('utf-8') # 16 bytes
cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes)
# 对明文进行 PKCS7 填充
padded = pad(plaintext.encode('utf-8'), AES.block_size)
encrypted = cipher.encrypt(padded)
return b64.b64encode(encrypted).decode('utf-8')
def _generate_sign(self, body_data: Optional[dict] = None, bearer_token: str = "") -> Dict[str, str]:
"""
根据前端 JS 逆向的签名算法生成请求签名
Args:
body_data: 请求体数据(字典或None)
bearer_token: Bearer Token 字符串(含 "Bearer " 前缀)
Returns:
dict: {x-random, x-timestamp, x-sign, [RandomCode]}
"""
# 步骤1: 生成随机字符串 x-random(8-16位)
x_random = self._generate_random_string(8, 16)
# 步骤2: 毫秒时间戳
x_timestamp = str(int(time.time() * 1000))
# 步骤3: 构造签名原文
if body_data is not None:
sign_str = x_timestamp + json.dumps(body_data, separators=(',', ':'), ensure_ascii=False) + x_random
else:
sign_str = x_timestamp + x_random
# 步骤4: SHA256 中间哈希
sign_hash = self._sha256(sign_str)
# 步骤5-8: AES 加密生成 x-sign
random_code = None
if bearer_token:
# 有 Token 时用 Token 做 AES 密钥源
aes_key_source = self._sha256(bearer_token)
else:
# 无 Token 时用随机60位字符串,并将该字符串作为 RandomCode 头部
random_code = self._generate_random_string(60, 60)
aes_key_source = self._sha256(random_code)
# 步骤6: AES 密钥 = sha256结果的[16:32](16字节)
aes_key = aes_key_source[16:32]
# 步骤7: AES IV = sha256结果的[0:8] + 末尾8字符
aes_iv = aes_key_source[0:8] + aes_key_source[-8:]
# 步骤8: AES 加密
x_sign = self._aes_encrypt(sign_hash, aes_key, aes_iv)
headers = {
'X-RANDOM': x_random,
'X-TIMESTAMP': x_timestamp,
'X-SIGN': x_sign,
}
if random_code:
headers['RandomCode'] = random_code
return headers
def get_captcha_uuid(self) -> Optional[str]:
"""
获取验证码 UUID(从验证码接口获取)
Returns:
str: 验证码 UUID
"""
auth_config = self.config.get("auth", {})
captcha_path = auth_config.get("captcha_path", "/platform/api/code")
url = f"{self.base_url}{captcha_path}"
try:
resp = self.session.get(url, verify=self.verify_ssl, timeout=self.timeout)
if resp.status_code == 200:
data = resp.json()
# 从响应中提取 uuid
uuid_val = data.get('uuid') or data.get('data', {}).get('uuid')
if uuid_val:
self._captcha_uuid = uuid_val
logger.debug(f"获取验证码 UUID: {uuid_val}")
return uuid_val
except Exception as e:
logger.warning(f"获取验证码 UUID 失败: {e}")
return None
def login(self, account_key: str = "superadmin") -> Optional[str]:
"""
使用指定账号登录系统并获取 Token
Args:
account_key: 账号配置键名(superadmin/admin/user)
Returns:
str: 访问 Token,失败返回 None
"""
# 如果已有有效 Token,直接返回
if account_key in self._tokens:
return self._tokens[account_key]
accounts = self.config.get("accounts", {})
account = accounts.get(account_key)
if not account:
logger.error(f"未找到账号配置: {account_key}")
return None
auth_config = self.config.get("auth", {})
login_path = auth_config.get("login_path", "/platform/api/auth/login")
url = f"{self.base_url}{login_path}"
captcha = accounts.get("captcha", "csba")
# 获取验证码 UUID
uuid_val = self.get_captcha_uuid() or ''
# 构造登录请求体
encrypted_pwd = self.encrypt_password(account['password'])
payload = {
"username": account['username'],
"password": encrypted_pwd,
"code": captcha,
"uuid": uuid_val,
}
# 生成签名头部(登录时无 Token,使用随机密钥)
sign_headers = self._generate_sign(body_data=payload, bearer_token="")
# 通用请求头
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
'referer': '',
}
headers.update(sign_headers)
try:
logger.info(f"正在登录 [{account_key}]: {account['username']}")
resp = self.session.post(
url, json=payload, headers=headers,
verify=self.verify_ssl, timeout=self.timeout
)
if resp.status_code == 200:
data = resp.json()
# 尝试从响应中提取 Token
token = None
if isinstance(data, dict):
token = (
data.get('accessToken') or
data.get('token') or
data.get('access_token') or
data.get('data', {}).get('accessToken') or
data.get('data', {}).get('token') or
data.get('data', {}).get('access_token')
)
if token:
self._tokens[account_key] = token
logger.info(f"登录成功 [{account_key}], Token: {token[:30]}...")
return token
else:
logger.warning(f"登录响应中未找到 Token: {json.dumps(data, ensure_ascii=False)[:300]}")
else:
logger.warning(f"登录失败 [{account_key}], HTTP {resp.status_code}: {resp.text[:200]}")
except Exception as e:
logger.error(f"登录异常 [{account_key}]: {e}")
return None
def get_token(self, account_key: str = "superadmin") -> Optional[str]:
"""
获取指定账号的 Token(如未登录则自动登录)
Args:
account_key: 账号配置键名
Returns:
str: Token 字符串
"""
if account_key in self._tokens:
return self._tokens[account_key]
return self.login(account_key)
def clear_token(self, account_key: Optional[str] = None):
"""
清除 Token 缓存
Args:
account_key: 账号键名,为None则清除所有
"""
if account_key:
self._tokens.pop(account_key, None)
else:
self._tokens.clear()
def _get_bearer_token(self, account_key: str) -> str:
"""获取 Bearer Token 字符串(含前缀)"""
token = self.get_token(account_key)
if token:
return f"Bearer {token}"
return ""
def _build_headers(self, token: Optional[str] = None, extra_headers: Optional[dict] = None) -> dict:
"""
构造请求头部
Args:
token: 访问 Token
extra_headers: 额外头部
Returns:
dict: 请求头部字典
"""
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json, text/plain, */*',
}
if token:
headers['Authorization'] = f"Bearer {token}"
if extra_headers:
headers.update(extra_headers)
return headers
def _request_with_sign(
self,
method: str,
path: str,
json_data: Optional[dict] = None,
params: Optional[dict] = None,
token: Optional[str] = None,
account: str = "superadmin",
**kwargs
) -> Optional[requests.Response]:
"""
带签名的请求发送(内部方法)
Args:
method: HTTP 方法
path: 接口路径
json_data: 请求体
params: URL 查询参数
token: Token
account: 账号键名
Returns:
requests.Response 或 None
"""
url = f"{self.base_url}{path}"
# 获取 Token
if token is None and account:
token = self.get_token(account)
bearer_str = f"Bearer {token}" if token else ""
# 构造签名(根据方法决定 body_data)
body_for_sign = None
if method.lower() in ('post', 'put', 'patch') and json_data is not None:
body_for_sign = json_data
elif method.lower() in ('get', 'delete') and params:
body_for_sign = params
sign_headers = self._generate_sign(body_data=body_for_sign, bearer_token=bearer_str)
# 合并头部
headers = self._build_headers(token=token)
headers.update(sign_headers)
logger.debug(f"{method.upper()} {path}")
try:
resp = self.session.request(
method, url,
json=json_data if method.lower() not in ('get', 'delete') else None,
params=params if method.lower() in ('get', 'delete') else None,
headers=headers,
verify=self.verify_ssl,
timeout=self.timeout,
**kwargs
)
return resp
except requests.RequestException as e:
logger.error(f"{method.upper()} 请求异常 [{path}]: {e}")
return None
def get(
self,
path: str,
params: Optional[dict] = None,
token: Optional[str] = None,
account: str = "superadmin",
**kwargs
) -> Optional[requests.Response]:
"""
发送 GET 请求
Args:
path: 接口路径
params: 查询参数
token: 指定 Token
account: 账号键名
Returns:
requests.Response
"""
return self._request_with_sign('GET', path, params=params, token=token, account=account, **kwargs)
def post(
self,
path: str,
json_data: Optional[dict] = None,
token: Optional[str] = None,
account: str = "superadmin",
**kwargs
) -> Optional[requests.Response]:
"""
发送 POST 请求
Args:
path: 接口路径
json_data: 请求体
token: 指定 Token
account: 账号键名
Returns:
requests.Response
"""
return self._request_with_sign('POST', path, json_data=json_data, token=token, account=account, **kwargs)
def put(
self,
path: str,
json_data: Optional[dict] = None,
token: Optional[str] = None,
account: str = "superadmin",
**kwargs
) -> Optional[requests.Response]:
"""
发送 PUT 请求
"""
return self._request_with_sign('PUT', path, json_data=json_data, token=token, account=account, **kwargs)
def delete(
self,
path: str,
token: Optional[str] = None,
account: str = "superadmin",
**kwargs
) -> Optional[requests.Response]:
"""
发送 DELETE 请求
"""
return self._request_with_sign('DELETE', path, token=token, account=account, **kwargs)
def request(
self,
method: str,
path: str,
token: Optional[str] = None,
account: str = "superadmin",
json_data: Optional[dict] = None,
params: Optional[dict] = None,
**kwargs
) -> Optional[requests.Response]:
"""
发送自定义 HTTP 方法的请求
Args:
method: HTTP 方法(OPTIONS、PATCH、TRACE 等)
path: 接口路径
token: 指定 Token
account: 账号键名
json_data: 请求体
params: 查询参数
Returns:
requests.Response
"""
return self._request_with_sign(
method, path, json_data=json_data,
params=params, token=token,
account=account, **kwargs
)
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_executor.py
模块描述:安全测试执行引擎,负责执行 API 安全测试用例
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
import logging
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, List, Dict, Any, Callable
from app.executors.http_client import HttpClient
from app.executors.auth_helper import AuthHelper
from app.config import settings
logger = logging.getLogger(__name__)
# ==================== 数据类 ====================
@dataclass
class SecurityStepResult:
"""
安全测试步骤结果
Attributes:
order (int): 步骤顺序
name (str): 步骤名称
action (str): 动作类型
status (str): 状态 (pending/running/passed/failed/skipped)
duration (float): 耗时(秒)
log (str): 执行日志
error (Optional[str]): 错误信息
is_vulnerable (bool): 是否存在漏洞
level (str): 风险等级
request_info (str): 请求信息
response_info (str): 响应信息
start_time (datetime): 开始时间
end_time (datetime): 结束时间
"""
order: int
name: str = ""
action: str = ""
status: str = "pending"
duration: float = 0.0
log: str = ""
error: Optional[str] = None
is_vulnerable: bool = False
level: str = "info"
request_info: str = ""
response_info: str = ""
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
@dataclass
class SecurityCaseResult:
"""
安全测试用例结果
Attributes:
case_id (str): 用例ID
test_id (str): OWASP测试编号
name (str): 用例名称
status (str): 状态 (passed/failed/skipped/error)
is_vulnerable (bool): 是否存在漏洞
level (str): 风险等级
description (str): 漏洞描述
request_info (str): 请求信息
response_info (str): 响应信息
fix_suggestion (str): 修复建议
duration (float): 执行耗时(秒)
steps (list): 步骤结果列表
error (Optional[str]): 错误信息
metadata (dict): 额外元数据
start_time (datetime): 开始时间
end_time (datetime): 结束时间
"""
case_id: str = ""
test_id: str = ""
name: str = ""
status: str = "pending"
is_vulnerable: bool = False
level: str = "info"
description: str = ""
request_info: str = ""
response_info: str = ""
fix_suggestion: str = ""
duration: float = 0.0
steps: List[SecurityStepResult] = field(default_factory=list)
error: Optional[str] = None
metadata: Dict[str, Any] = field(default_factory=dict)
start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
# ==================== 断言引擎 ====================
class AssertionEngine:
"""
断言引擎,评估测试响应是否存在漏洞
"""
# 权限拒绝相关的错误码
AUTH_DENIED_CODES = {
"401", "403", "40301", "40101", # 通用鉴权错误
"B0027", # 权限不足。无法请求接口
"B0017", # 接口请求方式错误
"B0002", # 验证码失效
"A0076", # 无效token
}
# 权限拒绝相关的消息关键词
DENY_KEYWORDS = [
"权限不足", "不允许访问", "拒绝访问",
"未授权", "请登录", "重新登录", "已退出",
"无效token", "token无效", "token已过期",
"AccessDenied", "用户不存在",
]
@classmethod
def is_success(cls, response) -> bool:
"""
判断响应是否表示业务成功(即越权成功)
Args:
response: requests.Response 对象
Returns:
bool: True 表示接口返回了正常数据(存在越权风险)
"""
if response is None:
return False
# 401/403 直接表示鉴权拦截
if response.status_code in (401, 403):
return False
# 非 200 状态码不算成功
if response.status_code != 200:
return False
try:
data = response.json()
# 1. 检查 success 字段
if "success" in data and not data["success"]:
return False
# 2. 检查业务错误码
code = data.get("code", data.get("status", data.get("errcode")))
if code is not None:
if str(code) in cls.AUTH_DENIED_CODES:
return False
# 3. 检查 message/msg 中的权限拒绝关键词
message = str(data.get("message", data.get("msg", "")))
if message:
if any(kw in message for kw in cls.DENY_KEYWORDS):
return False
# 4. 检查是否有有效数据返回
result_data = data.get("data")
if result_data is None or result_data == {} or result_data == []:
# 允许 success=True 但 data 为空的情况(如删除/修改操作)
if data.get("success") is True:
return True
return False
return True
except Exception:
pass
return False
@classmethod
def evaluate_assertions(cls, response, assertions: List[Dict]) -> tuple:
"""
评估断言列表
Args:
response: requests.Response 对象
assertions: 断言配置列表
Returns:
tuple: (is_vulnerable, description)
"""
if response is None:
return False, "无响应"
is_vulnerable = False
descriptions = []
for assertion in assertions:
atype = assertion.get("type", "")
expect = assertion.get("expect")
desc = assertion.get("description", "")
if atype == "status_code_not_equal":
# 状态码不应该等于某个值(如越权访问应返回403,不应返回200)
if response.status_code == expect:
is_vulnerable = True
descriptions.append(f"状态码={response.status_code},{desc}")
elif atype == "status_code_equal":
# 状态码应该等于某个值
if response.status_code != expect:
is_vulnerable = True
descriptions.append(f"预期状态码{expect},实际{response.status_code}")
elif atype == "response_code_not_contain":
# 响应业务码不应包含某些值
try:
data = response.json()
code = str(data.get("code", data.get("status", "")))
if code not in [str(e) for e in expect]:
is_vulnerable = True
descriptions.append(f"业务码={code},{desc}")
except Exception:
pass
elif atype == "response_code_contain":
# 响应业务码应包含某个值
try:
data = response.json()
code = str(data.get("code", data.get("status", "")))
if code not in [str(e) for e in expect]:
is_vulnerable = True
descriptions.append(f"预期业务码{expect},实际{code}")
except Exception:
pass
elif atype == "body_contains":
# 响应体应包含某字符串
if expect not in response.text:
is_vulnerable = True
descriptions.append(f"响应体不包含'{expect}',{desc}")
elif atype == "body_not_contains":
# 响应体不应包含某字符串
if expect in response.text:
is_vulnerable = True
descriptions.append(f"响应体包含'{expect}',{desc}")
elif atype == "is_success":
# 响应应表示业务成功(用于验证越权)
if cls.is_success(response) == expect:
is_vulnerable = True
descriptions.append(desc)
return is_vulnerable, "; ".join(descriptions) if descriptions else "断言通过"
# ==================== 执行器 ====================
class SecurityExecutor:
"""
安全测试执行器
负责执行 API 安全测试用例,不依赖浏览器,使用 requests 库发送 HTTP 请求。
与 PlaywrightExecutor 并列,通过 case_type=security 区分调用。
"""
def __init__(self, config: Dict[str, Any]):
"""
初始化安全测试执行器
Args:
config: 安全测试配置字典(来自 SecurityConfig.to_client_config())
"""
self.config = config
self.client = HttpClient(config)
self.auth = AuthHelper(self.client)
self.results: List[SecurityCaseResult] = []
self._is_running = False
self._stop_requested = False
self._progress_callback: Optional[Callable] = None
def start(self) -> bool:
"""
初始化执行环境,登录所有账号
Returns:
bool: 是否成功初始化
"""
logger.info("初始化安全测试执行环境...")
# 登录所有测试账号
tokens = self.auth.login_all_accounts()
success_count = sum(1 for v in tokens.values() if v)
logger.info(f"登录完成: {success_count}/{len(tokens)} 个账号成功")
if success_count == 0:
logger.error("所有账号登录失败,无法继续测试")
return False
self._is_running = True
return True
def stop(self):
"""停止执行"""
self._stop_requested = True
self._is_running = False
logger.info("安全测试执行器已停止")
def set_progress_callback(self, callback: Callable):
"""
设置进度回调函数
Args:
callback: 回调函数,签名为 callback(case_id, status, result)
"""
self._progress_callback = callback
def execute_case(self, case: Dict[str, Any]) -> SecurityCaseResult:
"""
执行单个安全测试用例
Args:
case: 用例配置字典,包含:
- id: 用例ID
- name: 用例名称
- steps: 测试步骤定义(JSON)
Returns:
SecurityCaseResult: 执行结果
"""
start_time = datetime.now()
case_id = case.get("id", "")
case_name = case.get("name", "")
result = SecurityCaseResult(
case_id=case_id,
name=case_name,
start_time=start_time,
)
steps_config = case.get("steps", {})
# 提取测试配置
test_type = steps_config.get("test_type", "api_security")
if test_type != "api_security":
result.status = "skipped"
result.error = f"不支持的测试类型: {test_type}"
result.end_time = datetime.now()
return result
target = steps_config.get("target", {})
auth_config = steps_config.get("auth", {})
request_config = steps_config.get("request", {})
assertions = steps_config.get("assertions", [])
vulnerability_info = steps_config.get("vulnerability", {})
# 填充漏洞信息
result.test_id = vulnerability_info.get("id", "")
result.level = vulnerability_info.get("level", "info")
result.description = vulnerability_info.get("description", "")
result.fix_suggestion = vulnerability_info.get("fix_suggestion", "")
result.metadata = {
"vuln_name": vulnerability_info.get("name", ""),
}
try:
# 获取认证 Token
account = auth_config.get("account", "user")
token = self.auth.get_token(account)
if auth_config.get("required", False) and not token:
result.status = "error"
result.error = f"账号 {account} 登录失败,无法获取 Token"
result.level = "info"
return result
# 发送请求
method = target.get("method", "GET").upper()
path = target.get("path", "/")
params = request_config.get("params", {})
body = request_config.get("body")
request_info = f"请求方法: {method}\n请求路径: {path}"
if params:
request_info += f"\n查询参数: {params}"
if body:
request_info += f"\n请求体: {body}"
request_info += f"\n使用账号: {account}"
result.request_info = request_info
# 执行请求
step_start = time.time()
response = self.client.request(
method=method,
path=path,
token=token,
account=account,
json_data=body,
params=params,
)
step_duration = time.time() - step_start
# 构建响应信息
if response:
result.response_info = (
f"状态码: {response.status_code}\n"
f"响应体: {response.text[:500]}"
)
else:
result.response_info = "无响应"
# 评估断言
is_vulnerable, desc = AssertionEngine.evaluate_assertions(response, assertions)
result.is_vulnerable = is_vulnerable
result.duration = step_duration
result.status = "passed" if not is_vulnerable else "failed"
if is_vulnerable:
result.description = vulnerability_info.get("description", desc)
logger.warning(f"[{result.test_id}] 发现漏洞: {result.description}")
else:
logger.info(f"[{result.test_id}] 安全: {case_name}")
# 通知进度
if self._progress_callback:
self._progress_callback(case_id, result.status, result)
except Exception as e:
result.status = "error"
result.error = str(e)
logger.error(f"执行用例 {case_id} 异常: {e}")
result.end_time = datetime.now()
result.duration = (result.end_time - result.start_time).total_seconds()
return result
def execute_cases(
self,
cases: List[Dict[str, Any]],
progress_callback: Optional[Callable] = None
) -> List[SecurityCaseResult]:
"""
批量执行安全测试用例
Args:
cases: 用例配置列表
progress_callback: 进度回调函数
Returns:
list: SecurityCaseResult 列表
"""
if progress_callback:
self.set_progress_callback(progress_callback)
self.results = []
total = len(cases)
for idx, case in enumerate(cases, 1):
if self._stop_requested:
logger.info("收到停止请求,中断执行")
break
logger.info(f"执行用例 [{idx}/{total}]: {case.get('name', '')}")
result = self.execute_case(case)
self.results.append(result)
# 请求间隔(防止触发限流)
limits = self.config.get("limits", {})
interval = limits.get("request_interval", 0.5)
time.sleep(interval)
return self.results
def get_summary(self) -> Dict[str, int]:
"""
获取执行摘要统计
Returns:
dict: 统计数据
"""
summary = {
"total": len(self.results),
"critical": 0,
"high": 0,
"medium": 0,
"low": 0,
"info": 0,
"passed": 0,
"failed": 0,
"error": 0,
}
for r in self.results:
if r.is_vulnerable:
level = r.level
if level in summary:
summary[level] += 1
summary["failed"] += 1
else:
summary["passed"] += 1
if r.status == "error":
summary["error"] += 1
return summary
@property
def is_running(self) -> bool:
"""是否正在执行"""
return self._is_running
\ No newline at end of file
......@@ -27,7 +27,7 @@ from fastapi.responses import FileResponse
from app.config import settings
from app.database import init_db
from app.routers import modules, cases, executions, recorder, stats, reports, cleanup, batch, dependencies
from app.routers import modules, cases, executions, recorder, stats, reports, cleanup, batch, dependencies, security
# 配置日志
logging.basicConfig(
......@@ -140,6 +140,12 @@ app.include_router(
tags=["依赖管理"]
)
app.include_router(
security.router,
prefix="/api/security",
tags=["安全测试"]
)
# ==================== 根路径 ====================
......
......@@ -13,6 +13,8 @@ from app.models.test_case import TestCase
from app.models.execution import Execution
from app.models.case_result import CaseResult
from app.models.case_dependency import CaseDependency
from app.models.security_config import SecurityConfig
from app.models.vulnerability_result import VulnerabilityResult
__all__ = [
"Module",
......@@ -20,4 +22,6 @@ __all__ = [
"Execution",
"CaseResult",
"CaseDependency",
"SecurityConfig",
"VulnerabilityResult",
]
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_config.py
模块描述:安全测试配置数据库模型定义
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Text, JSON, DateTime, Boolean, Integer
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class SecurityConfig(Base):
"""
安全测试配置数据库模型
存储安全测试的目标服务器、测试账号、认证配置等信息。
Attributes:
id (str): 配置唯一标识
name (str): 配置名称
target_url (str): 目标服务器地址
verify_ssl (bool): 是否验证SSL证书
timeout (int): 请求超时时间(秒)
accounts (dict): 测试账号配置(JSON)
auth_config (dict): 认证配置(JSON)
rate_limits (dict): 限流配置(JSON)
is_default (bool): 是否为默认配置
created_at (datetime): 创建时间
updated_at (datetime): 更新时间
"""
__tablename__ = "security_configs"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="配置ID")
name: Mapped[str] = mapped_column(String(100), nullable=False, comment="配置名称")
target_url: Mapped[str] = mapped_column(String(500), nullable=False, comment="目标服务器地址")
server_ip: Mapped[Optional[str]] = mapped_column(String(50), default="", comment="服务器IP")
verify_ssl: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否验证SSL证书")
timeout: Mapped[int] = mapped_column(Integer, default=30, comment="请求超时时间(秒)")
# 测试账号配置(JSON)
# 格式: {"superadmin": {"username": "...", "password": "..."}, "admin": {...}, "user": {...}, "captcha": "csba"}
accounts: Mapped[dict] = mapped_column(JSON, default=dict, comment="测试账号配置")
# 认证配置(JSON)
# 格式: {"token_type": "accessToken", "mechanism": "JWT", "login_path": "/platform/api/auth/login", ...}
auth_config: Mapped[dict] = mapped_column(JSON, default=dict, comment="认证配置")
# 限流配置(JSON)
# 格式: {"brute_force_max": 20, "rate_limit_max": 100, "request_interval": 0.5}
rate_limits: Mapped[dict] = mapped_column(JSON, default=dict, comment="限流配置")
# API路径前缀(JSON)
# 格式: {"meeting": "/api/", "monitor": "/monitor/api2/api/", ...}
api_prefixes: Mapped[dict] = mapped_column(JSON, default=dict, comment="API路径前缀")
is_default: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否为默认配置")
description: Mapped[Optional[str]] = mapped_column(Text, default="", comment="配置描述")
created_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, comment="创建时间"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, comment="更新时间"
)
def __repr__(self) -> str:
"""字符串表示"""
return f"<SecurityConfig(id={self.id}, name={self.name})>"
def to_dict(self) -> dict:
"""
转换为字典
Returns:
dict: 配置数据字典
"""
return {
"id": self.id,
"name": self.name,
"target_url": self.target_url,
"server_ip": self.server_ip,
"verify_ssl": self.verify_ssl,
"timeout": self.timeout,
"accounts": self.accounts or {},
"auth_config": self.auth_config or {},
"rate_limits": self.rate_limits or {},
"api_prefixes": self.api_prefixes or {},
"is_default": self.is_default,
"description": self.description or "",
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
def to_client_config(self) -> dict:
"""
转换为 HttpClient 可用的配置格式(兼容参考实现 config.yaml 结构)
Returns:
dict: HttpClient 配置字典
"""
return {
"target": {
"base_url": self.target_url,
"server_ip": self.server_ip or self.target_url.replace("https://", "").replace("http://", "").split(":")[0],
"verify_ssl": self.verify_ssl,
"timeout": self.timeout,
},
"accounts": self.accounts or {},
"auth": self.auth_config or {},
"limits": self.rate_limits or {},
"api_prefixes": self.api_prefixes or {},
}
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:vulnerability_result.py
模块描述:漏洞测试结果数据库模型定义
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
from datetime import datetime
from typing import Optional
from sqlalchemy import String, Text, JSON, DateTime, Boolean, Float, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.database import Base
class VulnerabilityResult(Base):
"""
漏洞测试结果数据库模型
存储安全测试用例的执行结果,包括漏洞信息、请求响应详情等。
Attributes:
id (str): 结果唯一标识
execution_id (str): 关联的执行记录ID
case_id (str): 关联的测试用例ID
test_id (str): OWASP测试编号(如 2.1.1)
name (str): 测试用例名称
level (str): 风险等级(critical/high/medium/low/info)
description (str): 漏洞描述
request_info (str): 请求信息(用于复现)
response_info (str): 响应信息(用于复现)
is_vulnerable (bool): 是否存在漏洞
fix_suggestion (str): 修复建议
duration (float): 执行耗时(秒)
status (str): 执行状态(passed/failed/skipped/error)
error_message (str): 错误信息
metadata (dict): 额外元数据(回归测试标记、红线标记等)
created_at (datetime): 创建时间
"""
__tablename__ = "vulnerability_results"
# 风险等级常量
LEVEL_CRITICAL = "critical"
LEVEL_HIGH = "high"
LEVEL_MEDIUM = "medium"
LEVEL_LOW = "low"
LEVEL_INFO = "info"
id: Mapped[str] = mapped_column(String(32), primary_key=True, comment="结果ID")
execution_id: Mapped[str] = mapped_column(
String(32), ForeignKey("executions.id", ondelete="CASCADE"),
nullable=False, comment="关联执行ID"
)
case_id: Mapped[str] = mapped_column(String(32), default="", comment="关联用例ID")
# 漏洞信息
test_id: Mapped[str] = mapped_column(String(20), default="", comment="OWASP测试编号(如2.1.1)")
name: Mapped[str] = mapped_column(String(200), nullable=False, comment="测试用例名称")
level: Mapped[str] = mapped_column(
String(20), default="info",
comment="风险等级: critical/high/medium/low/info"
)
description: Mapped[str] = mapped_column(Text, default="", comment="漏洞描述")
# 请求/响应信息
request_info: Mapped[str] = mapped_column(Text, default="", comment="请求信息(用于复现)")
response_info: Mapped[str] = mapped_column(Text, default="", comment="响应信息(用于复现)")
# 结果
is_vulnerable: Mapped[bool] = mapped_column(Boolean, default=False, comment="是否存在漏洞")
fix_suggestion: Mapped[str] = mapped_column(Text, default="", comment="修复建议")
# 执行信息
duration: Mapped[float] = mapped_column(Float, default=0.0, comment="执行耗时(秒)")
status: Mapped[str] = mapped_column(
String(20), default="pending",
comment="执行状态: pending/running/passed/failed/skipped/error"
)
error_message: Mapped[Optional[str]] = mapped_column(
Text, nullable=True, default=None, comment="错误信息"
)
# 元数据(注意:metadata 是 SQLAlchemy 保留字段名)
# 格式: {"is_regression": true, "vuln_source": "长安深蓝汽车", "is_huawei_redline": false, ...}
extra_data: Mapped[dict] = mapped_column(JSON, default=dict, comment="额外元数据")
created_at: Mapped[datetime] = mapped_column(
DateTime, default=datetime.utcnow, comment="创建时间"
)
# 关联执行记录(多对一)
execution: Mapped["Execution"] = relationship(
"Execution", back_populates="vulnerability_results"
)
def __repr__(self) -> str:
"""字符串表示"""
vuln_mark = "🔴" if self.is_vulnerable else "✅"
return f"<VulnerabilityResult(id={self.id}, test_id={self.test_id}, {vuln_mark} {self.level})>"
def to_dict(self) -> dict:
"""
转换为字典
Returns:
dict: 结果数据字典
"""
return {
"id": self.id,
"execution_id": self.execution_id,
"case_id": self.case_id,
"test_id": self.test_id,
"name": self.name,
"level": self.level,
"description": self.description,
"request_info": self.request_info,
"response_info": self.response_info,
"is_vulnerable": self.is_vulnerable,
"fix_suggestion": self.fix_suggestion,
"duration": self.duration,
"status": self.status,
"error_message": self.error_message,
"extra_data": self.extra_data or {},
"created_at": self.created_at.isoformat() if self.created_at else None,
}
@staticmethod
def level_order(level: str) -> int:
"""
获取风险等级排序值(用于按等级排序)
Args:
level: 风险等级
Returns:
int: 排序值,数值越小风险越高
"""
order_map = {
"critical": 0,
"high": 1,
"medium": 2,
"low": 3,
"info": 4,
}
return order_map.get(level, 99)
@property
def level_emoji(self) -> str:
"""风险等级对应的emoji标记"""
emoji_map = {
"critical": "🔴🔴🔴",
"high": "🔴",
"medium": "🟠",
"low": "🟡",
"info": "🔵",
}
return emoji_map.get(self.level, "⚪")
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security.py
模块描述:安全测试 API 路由
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.schemas.security import (
SecurityConfigCreate,
SecurityConfigUpdate,
SecurityConfigResponse,
SecurityExecutionCreate,
SecurityExecutionResponse,
VulnerabilityResultResponse,
AccountTestRequest,
AccountTestResponse,
KnowledgeBaseResponse,
HistoricalVuln,
HuaweiRedlineCheck,
)
from app.services.security_service import SecurityService
logger = logging.getLogger(__name__)
router = APIRouter()
# ==================== 配置管理 ====================
@router.get("/config", summary="获取安全测试配置列表")
async def list_configs(
skip: int = Query(0, ge=0, description="跳过记录数"),
limit: int = Query(20, ge=1, le=100, description="返回记录数"),
db: AsyncSession = Depends(get_db)
):
"""
获取安全测试配置列表
Returns:
dict: 配置列表和总数
"""
service = SecurityService(db)
configs, total = await service.list_configs(skip=skip, limit=limit)
return {
"items": [SecurityConfigResponse(**c.to_dict()) for c in configs],
"total": total
}
@router.get("/config/default", summary="获取默认安全测试配置")
async def get_default_config(db: AsyncSession = Depends(get_db)):
"""
获取默认安全测试配置
Returns:
SecurityConfigResponse: 默认配置
"""
service = SecurityService(db)
config = await service.get_default_config()
if not config:
raise HTTPException(status_code=404, detail="未找到默认安全测试配置")
return SecurityConfigResponse(**config.to_dict())
@router.get("/config/{config_id}", summary="获取指定安全测试配置")
async def get_config(
config_id: str,
db: AsyncSession = Depends(get_db)
):
"""
获取指定安全测试配置
Args:
config_id: 配置ID
Returns:
SecurityConfigResponse: 配置详情
"""
service = SecurityService(db)
config = await service.get_config(config_id)
if not config:
raise HTTPException(status_code=404, detail=f"配置不存在: {config_id}")
return SecurityConfigResponse(**config.to_dict())
@router.post("/config", summary="创建安全测试配置")
async def create_config(
config_data: SecurityConfigCreate,
db: AsyncSession = Depends(get_db)
):
"""
创建安全测试配置
Returns:
SecurityConfigResponse: 创建的配置
"""
service = SecurityService(db)
config = await service.create_config(config_data.model_dump(by_alias=True))
return SecurityConfigResponse(**config.to_dict())
@router.put("/config/{config_id}", summary="更新安全测试配置")
async def update_config(
config_id: str,
config_data: SecurityConfigUpdate,
db: AsyncSession = Depends(get_db)
):
"""
更新安全测试配置
Args:
config_id: 配置ID
Returns:
SecurityConfigResponse: 更新后的配置
"""
service = SecurityService(db)
config = await service.update_config(
config_id,
config_data.model_dump(by_alias=True, exclude_none=True)
)
if not config:
raise HTTPException(status_code=404, detail=f"配置不存在: {config_id}")
return SecurityConfigResponse(**config.to_dict())
@router.delete("/config/{config_id}", summary="删除安全测试配置")
async def delete_config(
config_id: str,
db: AsyncSession = Depends(get_db)
):
"""
删除安全测试配置
Args:
config_id: 配置ID
Returns:
dict: 删除结果
"""
service = SecurityService(db)
success = await service.delete_config(config_id)
if not success:
raise HTTPException(status_code=404, detail=f"配置不存在: {config_id}")
return {"message": "删除成功"}
# ==================== 账号测试 ====================
@router.post("/test-account", summary="测试账号连接")
async def test_account(
request: AccountTestRequest,
db: AsyncSession = Depends(get_db)
):
"""
测试指定账号是否可以登录
Returns:
AccountTestResponse: 测试结果
"""
service = SecurityService(db)
result = await service.test_account(request.config_id, request.account_key)
return AccountTestResponse(**result)
# ==================== 执行管理 ====================
@router.post("/executions", summary="创建安全测试执行")
async def create_execution(
execution_data: SecurityExecutionCreate,
db: AsyncSession = Depends(get_db)
):
"""
创建安全测试执行
Returns:
SecurityExecutionResponse: 执行记录
"""
service = SecurityService(db)
execution = await service.create_execution(
config_id=execution_data.config_id,
module_ids=execution_data.module_ids,
case_ids=execution_data.case_ids,
name=execution_data.name,
)
return SecurityExecutionResponse(
id=execution.id,
name=execution.name,
status=execution.status,
total_cases=execution.total_cases,
passed=execution.passed,
failed=execution.failed,
skipped=execution.skipped,
duration=execution.duration,
config_id=execution_data.config_id,
start_time=execution.start_time.isoformat() if execution.start_time else None,
end_time=execution.end_time.isoformat() if execution.end_time else None,
)
@router.post("/executions/{execution_id}/run", summary="执行安全测试")
async def run_execution(
execution_id: str,
db: AsyncSession = Depends(get_db)
):
"""
触发安全测试执行(异步)
Args:
execution_id: 执行ID
Returns:
dict: 执行状态
"""
service = SecurityService(db)
# 异步执行(不等待完成)
import asyncio
asyncio.create_task(service.run_execution(execution_id))
return {
"execution_id": execution_id,
"status": "running",
"message": "安全测试已开始执行"
}
@router.get("/executions/{execution_id}/results", summary="获取安全测试结果")
async def get_execution_results(
execution_id: str,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
level: Optional[str] = Query(None, description="风险等级筛选"),
is_vulnerable: Optional[bool] = Query(None, description="是否漏洞"),
db: AsyncSession = Depends(get_db)
):
"""
获取安全测试执行结果
Args:
execution_id: 执行ID
Returns:
dict: 结果列表和总数
"""
service = SecurityService(db)
results, total = await service.get_vulnerability_results(
execution_id=execution_id,
skip=skip,
limit=limit,
level=level,
is_vulnerable=is_vulnerable,
)
return {
"items": [VulnerabilityResultResponse(**r.to_dict()) for r in results],
"total": total
}
@router.get("/executions/{execution_id}/summary", summary="获取安全测试摘要")
async def get_execution_summary(
execution_id: str,
db: AsyncSession = Depends(get_db)
):
"""
获取安全测试执行摘要
Args:
execution_id: 执行ID
Returns:
dict: 摘要数据
"""
service = SecurityService(db)
return await service.get_execution_summary(execution_id)
# ==================== 知识库 ====================
@router.get("/knowledge-base", summary="获取安全知识库")
async def get_knowledge_base():
"""
获取安全测试知识库内容
Returns:
KnowledgeBaseResponse: 知识库数据
"""
try:
from app.services.knowledge_base import KNOWLEDGE_BASE
except ImportError:
# 知识库模块尚未创建时返回空数据
return KnowledgeBaseResponse()
# 转换历史漏洞
historical_vulns = []
for v in KNOWLEDGE_BASE.get("historical_vulns", []):
historical_vulns.append(HistoricalVuln(
vuln_id=v.get("vuln_id", ""),
title=v.get("title", ""),
severity=v.get("severity", ""),
vuln_type=v.get("vuln_type", ""),
source_project=v.get("source_project", ""),
detection_method=v.get("detection_method", ""),
regression_note=v.get("regression_note", ""),
mapped_paths=v.get("mapped_paths", []),
))
# 转换华为红线
huawei_redlines = []
for r in KNOWLEDGE_BASE.get("huawei_redlines", []):
huawei_redlines.append(HuaweiRedlineCheck(
check_id=r.get("check_id", ""),
category=r.get("category", ""),
requirement=r.get("requirement", ""),
severity=r.get("severity", ""),
check_method=r.get("check_method", ""),
))
return KnowledgeBaseResponse(
historical_vulns=historical_vulns,
huawei_redlines=huawei_redlines,
total_vulns=len(historical_vulns),
total_redlines=len(huawei_redlines),
)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security.py
模块描述:安全测试 Pydantic 模式定义
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field, ConfigDict
def to_camel(string: str) -> str:
"""Convert snake_case to camelCase"""
components = string.split('_')
return components[0] + ''.join(x.title() for x in components[1:])
# ==================== 安全测试配置 ====================
class AccountConfig(BaseModel):
"""测试账号配置"""
username: str = Field(..., description="用户名")
password: str = Field(..., description="密码")
description: str = Field("", description="账号描述")
class AuthConfigDetail(BaseModel):
"""认证配置详情"""
token_type: str = Field("accessToken", description="Token类型")
mechanism: str = Field("JWT", description="认证机制")
login_path: str = Field("/platform/api/auth/login", description="登录路径")
token_header: str = Field("accessToken", description="Token请求头名称")
company_number: str = Field("", description="公司编号")
company_secret: str = Field("", description="公司密钥")
captcha_path: str = Field("/platform/api/code", description="验证码获取路径")
class RateLimitConfig(BaseModel):
"""限流配置"""
brute_force_max: int = Field(20, description="暴力破解最大尝试次数")
rate_limit_max: int = Field(100, description="限流测试最大请求数")
batch_max: int = Field(50, description="批量测试最大请求数")
request_interval: float = Field(0.5, description="请求间隔(秒)")
class SecurityConfigCreate(BaseModel):
"""创建安全测试配置"""
name: str = Field(..., min_length=1, max_length=100, description="配置名称")
target_url: str = Field(..., description="目标服务器地址")
server_ip: str = Field("", description="服务器IP")
verify_ssl: bool = Field(False, description="是否验证SSL证书")
timeout: int = Field(30, ge=5, le=120, description="请求超时时间(秒)")
accounts: Dict[str, Any] = Field(default_factory=dict, description="测试账号配置")
auth_config: Dict[str, Any] = Field(default_factory=dict, description="认证配置")
rate_limits: Dict[str, Any] = Field(default_factory=dict, description="限流配置")
api_prefixes: Dict[str, Any] = Field(default_factory=dict, description="API路径前缀")
is_default: bool = Field(False, description="是否为默认配置")
description: str = Field("", description="配置描述")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class SecurityConfigUpdate(BaseModel):
"""更新安全测试配置"""
name: Optional[str] = Field(None, description="配置名称")
target_url: Optional[str] = Field(None, description="目标服务器地址")
server_ip: Optional[str] = Field(None, description="服务器IP")
verify_ssl: Optional[bool] = Field(None, description="是否验证SSL证书")
timeout: Optional[int] = Field(None, ge=5, le=120, description="请求超时时间(秒)")
accounts: Optional[Dict[str, Any]] = Field(None, description="测试账号配置")
auth_config: Optional[Dict[str, Any]] = Field(None, description="认证配置")
rate_limits: Optional[Dict[str, Any]] = Field(None, description="限流配置")
api_prefixes: Optional[Dict[str, Any]] = Field(None, description="API路径前缀")
is_default: Optional[bool] = Field(None, description="是否为默认配置")
description: Optional[str] = Field(None, description="配置描述")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class SecurityConfigResponse(BaseModel):
"""安全测试配置响应"""
id: str
name: str
target_url: str
server_ip: str = ""
verify_ssl: bool = False
timeout: int = 30
accounts: Dict[str, Any] = {}
auth_config: Dict[str, Any] = {}
rate_limits: Dict[str, Any] = {}
api_prefixes: Dict[str, Any] = {}
is_default: bool = False
description: str = ""
created_at: Optional[str] = None
updated_at: Optional[str] = None
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
# ==================== 安全测试执行 ====================
class SecurityExecutionCreate(BaseModel):
"""创建安全测试执行"""
config_id: str = Field(..., description="安全测试配置ID")
module_ids: List[str] = Field(default_factory=list, description="要执行的模块ID列表(空=全部)")
case_ids: List[str] = Field(default_factory=list, description="要执行的用例ID列表")
include_regression: bool = Field(True, description="是否包含历史漏洞回归测试")
include_redline: bool = Field(True, description="是否包含华为安全红线检查")
name: str = Field("", description="执行名称")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class SecurityExecutionResponse(BaseModel):
"""安全测试执行响应"""
id: str
name: str = ""
status: str = "pending"
total_cases: int = 0
passed: int = 0
failed: int = 0
skipped: int = 0
duration: float = 0.0
config_id: str = ""
start_time: Optional[str] = None
end_time: Optional[str] = None
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
# ==================== 漏洞结果 ====================
class VulnerabilityResultResponse(BaseModel):
"""漏洞测试结果响应"""
id: str
execution_id: str
case_id: str = ""
test_id: str = ""
name: str
level: str = "info"
description: str = ""
request_info: str = ""
response_info: str = ""
is_vulnerable: bool = False
fix_suggestion: str = ""
duration: float = 0.0
status: str = "pending"
error_message: Optional[str] = None
extra_data: Dict[str, Any] = {}
created_at: Optional[str] = None
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
# ==================== 知识库 ====================
class HistoricalVuln(BaseModel):
"""历史漏洞条目"""
vuln_id: str = Field(..., description="漏洞ID")
title: str = Field(..., description="漏洞标题")
severity: str = Field(..., description="严重等级")
vuln_type: str = Field(..., description="漏洞类型")
source_project: str = Field("", description="来源项目")
detection_method: str = Field("", description="检测方法")
regression_note: str = Field("", description="回归验证说明")
mapped_paths: List[str] = Field(default_factory=list, description="映射路径")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class HuaweiRedlineCheck(BaseModel):
"""华为安全红线检查项"""
check_id: str = Field(..., description="检查编号")
category: str = Field(..., description="检查类别")
requirement: str = Field(..., description="要求描述")
severity: str = Field(..., description="严重等级")
check_method: str = Field("", description="检查方法")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class KnowledgeBaseResponse(BaseModel):
"""知识库响应"""
historical_vulns: List[HistoricalVuln] = Field(default_factory=list, description="历史漏洞列表")
huawei_redlines: List[HuaweiRedlineCheck] = Field(default_factory=list, description="华为红线列表")
total_vulns: int = Field(0, description="历史漏洞总数")
total_redlines: int = Field(0, description="红线检查总数")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
# ==================== 账号测试 ====================
class AccountTestRequest(BaseModel):
"""账号连接测试请求"""
config_id: str = Field(..., description="安全测试配置ID")
account_key: str = Field(..., description="账号键名(superadmin/admin/user)")
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
class AccountTestResponse(BaseModel):
"""账号连接测试响应"""
account_key: str
success: bool
token_preview: str = ""
error_message: str = ""
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
......@@ -10,7 +10,7 @@
"""
from datetime import datetime
from typing import Optional, List, Dict, Any
from typing import Optional, List, Dict, Any, Union
from pydantic import BaseModel, Field, ConfigDict
......@@ -62,7 +62,10 @@ class TestCaseBase(BaseModel):
description: str = Field(default="", max_length=1000, description="用例描述")
priority: str = Field(default="medium", description="优先级: high/medium/low")
tags: List[str] = Field(default_factory=list, description="标签列表")
steps: List[StepDefinition] = Field(default_factory=list, description="测试步骤")
steps: Union[List[StepDefinition], Dict[str, Any]] = Field(
default_factory=list,
description="测试步骤(UI用例为步骤列表,安全测试用例为配置字典)"
)
config: Dict[str, Any] = Field(
default_factory=lambda: {"timeout": 30000, "retry": 0, "screenshot": True},
description="执行配置"
......@@ -106,7 +109,9 @@ class TestCaseUpdate(BaseModel):
status: Optional[str] = Field(None, description="状态: active/disabled")
priority: Optional[str] = Field(None, description="优先级")
tags: Optional[List[str]] = Field(None, description="标签列表")
steps: Optional[List[StepDefinition]] = Field(None, description="测试步骤")
steps: Optional[Union[List[StepDefinition], Dict[str, Any]]] = Field(
None, description="测试步骤(UI用例为步骤列表,安全测试用例为配置字典)"
)
config: Optional[Dict[str, Any]] = Field(None, description="执行配置")
case_type: Optional[str] = Field(None, description="用例类型: ui/api/security/deploy")
......
......@@ -182,12 +182,23 @@ class CaseService:
if len(data.name) > 200:
raise ValueError("用例名称不能超过200字符")
# 步骤校验
if data.steps:
# 步骤校验(仅UI用例需要列表格式校验)
if data.steps and data.case_type == "ui":
if isinstance(data.steps, list):
for i, step in enumerate(data.steps):
if not step.action:
raise ValueError(f"第{i+1}步的动作类型不能为空")
# 处理 steps 字段:UI用例转列表,安全测试用例保持字典
steps_data = []
if data.steps:
if isinstance(data.steps, list):
# UI用例:StepDefinition列表转字典列表
steps_data = [s.model_dump() for s in data.steps]
elif isinstance(data.steps, dict):
# 安全测试用例:直接使用字典
steps_data = data.steps
# 创建用例对象
test_case = TestCase(
id=generate_id("case"),
......@@ -196,7 +207,7 @@ class CaseService:
description=data.description or "",
priority=data.priority or "medium",
tags=data.tags or [],
steps=[s.model_dump() for s in data.steps] if data.steps else [],
steps=steps_data,
config=data.config or {"timeout": 30000, "retry": 0, "screenshot": True},
case_type=data.case_type or "ui",
)
......
......@@ -210,6 +210,10 @@ class ExecutionService:
if not execution:
raise ValueError(f"执行记录不存在: {execution_id}")
# ===== 按用例类型分流:安全测试走专用执行器 =====
if execution.case_type == "security":
return await self._run_security_execution(execution_id, config)
# 更新状态为运行中
execution.status = "running"
execution.start_time = datetime.now()
......@@ -588,6 +592,188 @@ class ExecutionService:
return callback
async def _run_security_execution(
self,
execution_id: str,
config: Optional[dict] = None,
) -> Execution:
"""
安全测试专用执行流程
安全测试用例不使用 Playwright(无浏览器),而是用 requests + 签名算法
直接调用被测系统 API。本方法:
1. 从执行记录中取出关联的用例
2. 加载默认安全测试配置
3. 用 SecurityExecutor 执行
4. 把结果写回 CaseResult + VulnerabilityResult
Args:
execution_id: 执行记录ID
config: 执行配置(未使用,保留兼容)
Returns:
Execution: 更新后的执行记录
"""
# 延迟导入,避免循环依赖
from app.executors.security_executor import SecurityExecutor, SecurityCaseResult
from app.models.security_config import SecurityConfig
from app.models.vulnerability_result import VulnerabilityResult
execution = await self.get_execution(execution_id)
if not execution:
raise ValueError(f"执行记录不存在: {execution_id}")
# 广播执行开始
await manager.broadcast(execution_id, {
"type": "execution_start",
"data": execution.to_dict(),
})
# 获取所有待执行的用例结果(按模块分组排序)
results_query = (
select(CaseResult)
.join(TestCase, CaseResult.case_id == TestCase.id)
.where(
CaseResult.execution_id == execution_id,
CaseResult.status == "pending",
)
.order_by(TestCase.module_id, TestCase.order, TestCase.created_at)
)
results_data = await self.db.execute(results_query)
case_results = list(results_data.scalars().all())
# 获取用例定义
case_ids = [r.case_id for r in case_results]
cases_query = select(TestCase).where(TestCase.id.in_(case_ids))
cases_data = await self.db.execute(cases_query)
cases = list(cases_data.scalars().all())
cases_map = {c.id: c for c in cases}
# 加载默认安全测试配置
cfg_query = select(SecurityConfig).where(SecurityConfig.is_default == True)
cfg_result = await self.db.execute(cfg_query)
sec_config = cfg_result.scalar_one_or_none()
if not sec_config:
# 没有配置,全部标记失败
for cr in case_results:
cr.status = "error"
cr.error_message = "未找到默认安全测试配置"
execution.status = "failed"
execution.end_time = datetime.now()
await self.db.flush()
return execution
client_config = sec_config.to_client_config()
# 在 async 上下文中预先把 ORM 对象转成纯字典(避免线程池脱钩)
def _to_plain_steps(steps_val):
if isinstance(steps_val, str):
try:
parsed = json.loads(steps_val)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
return steps_val if isinstance(steps_val, dict) else {}
plain_cases = [
{"id": c.id, "name": c.name, "steps": _to_plain_steps(c.steps)}
for c in cases
]
# 定义线程安全的同步执行函数
loop = asyncio.get_event_loop()
def sync_run_security():
executor = SecurityExecutor(client_config)
executor.start()
results = []
for i, case_dict in enumerate(plain_cases):
case_id = case_dict.get("id", "")
case_name = case_dict.get("name", "")
logger.info(f"⏳ 安全测试用例 {i+1}/{len(plain_cases)}: {case_name}")
try:
result = executor.execute_case(case_dict)
except Exception as e:
logger.error(f"安全用例执行异常: {case_name}, {e}")
result = SecurityCaseResult(
case_id=case_id, name=case_name,
status="error", error=str(e),
)
results.append(result)
return results
try:
results: List[SecurityCaseResult] = await loop.run_in_executor(
None, sync_run_security
)
except Exception as e:
logger.error(f"安全测试执行异常: {e}")
execution.status = "failed"
execution.end_time = datetime.now()
await self.db.flush()
return execution
# 更新用例结果 + 保存漏洞结果
passed = 0
failed = 0
for r in results:
# 找到对应的 CaseResult 更新状态
cr = next((c for c in case_results if c.case_id == r.case_id), None)
if cr:
if r.status == "error":
cr.status = "error"
cr.error_message = r.error
elif r.is_vulnerable:
cr.status = "failed"
cr.error_message = r.description[:500] if r.description else None
failed += 1
else:
cr.status = "passed"
passed += 1
cr.duration = r.duration
cr.end_time = datetime.now()
# 保存漏洞测试结果
vuln_result = VulnerabilityResult(
id=generate_id("vuln"),
execution_id=execution_id,
case_id=r.case_id,
test_id=r.test_id,
name=r.name,
level=r.level,
description=r.description,
request_info=r.request_info,
response_info=r.response_info,
is_vulnerable=r.is_vulnerable,
fix_suggestion=r.fix_suggestion,
duration=r.duration,
status=r.status,
error_message=r.error,
extra_data=r.metadata,
)
self.db.add(vuln_result)
# 更新执行统计
execution.passed = passed
execution.failed = failed
execution.skipped = len(case_results) - passed - failed
execution.total_cases = len(case_results)
execution.status = "completed"
execution.end_time = datetime.now()
execution.duration = (execution.end_time - execution.start_time).total_seconds() if execution.start_time else 0
execution.pass_rate = round((passed / execution.total_cases) * 100, 2) if execution.total_cases > 0 else 0
await self.db.flush()
# 广播执行完成
await manager.broadcast(execution_id, {
"type": "execution_complete",
"data": execution.to_dict(),
})
logger.info(f"安全测试执行完成: {execution_id}, 通过: {passed}, 失败: {failed}")
return execution
async def _count_results(self, execution_id: str, status: str) -> int:
"""
统计指定状态的用例结果数量
......
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:knowledge_base.py
模块描述:安全测试知识库,包含历史漏洞、华为安全红线、测试载荷等数据
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
# 安全测试知识库数据
# 来源:临时目录/安全测试/ApiSecurityTest/utils/knowledge_base.py
KNOWLEDGE_BASE = {
# ==================== 历史漏洞回归测试 ====================
"historical_vulns": [
{
"vuln_id": "HV-001",
"title": "SQL注入 - 会议预定接口",
"severity": "high",
"vuln_type": "SQL_INJECTION",
"source_project": "长安深蓝汽车",
"detection_method": "IAST插桩检测",
"regression_note": "验证会议预定接口是否已使用预编译语句",
"mapped_paths": ["/api/message/book", "/oldmeeting/api/message/book"],
},
{
"vuln_id": "HV-002",
"title": "Spring Actuator 信息泄露 - Nacos",
"severity": "medium",
"vuln_type": "INFO_DISCLOSURE",
"source_project": "南山区委",
"detection_method": "XDR告警+手动验证",
"regression_note": "验证Nacos Actuator端点是否已关闭或被Nginx拦截",
"mapped_paths": ["/nacos/actuator/", "/nacos/actuator/health"],
},
{
"vuln_id": "HV-003",
"title": "安全响应头缺失(全站)",
"severity": "medium",
"vuln_type": "MISSING_HEADERS",
"source_project": "厦门银行总行大厦、龙华儿童医院",
"detection_method": "华为Web漏扫",
"regression_note": "验证X-Frame-Options、CSP、HSTS等安全头是否已配置",
"mapped_paths": ["全局"],
},
{
"vuln_id": "HV-005",
"title": "Nacos 未授权访问",
"severity": "high",
"vuln_type": "UNAUTH_ACCESS",
"source_project": "南山区委",
"detection_method": "攻防演练",
"regression_note": "验证Nacos是否开启鉴权,配置列表和服务列表是否需Token",
"mapped_paths": ["/nacos/", "/nacos/v1/cs/configs", "/nacos/v1/ns/service/list"],
},
{
"vuln_id": "HV-006",
"title": "Swagger 文档暴露",
"severity": "medium",
"vuln_type": "INFO_DISCLOSURE",
"source_project": "历史多个项目",
"detection_method": "手动探测",
"regression_note": "验证各路径下的Swagger文档是否已全部拦截",
"mapped_paths": ["/swagger-ui.html", "/v2/api-docs"],
},
{
"vuln_id": "HV-007",
"title": "固定验证码 csba",
"severity": "medium",
"vuln_type": "WEAK_AUTH",
"source_project": "需求文档已确认",
"detection_method": "已知问题",
"regression_note": "记录为已知安全问题,验证是否已改为随机验证码",
"mapped_paths": ["/platform/api/auth/login"],
},
{
"vuln_id": "HV-008",
"title": "密码SHA256无加盐哈希",
"severity": "medium",
"vuln_type": "WEAK_CRYPTO",
"source_project": "网络抓包确认",
"detection_method": "已知问题",
"regression_note": "验证是否已升级为bcrypt/argon2等加盐哈希",
"mapped_paths": ["登录接口"],
},
{
"vuln_id": "HV-009",
"title": "WebSocket端点无认证",
"severity": "high",
"vuln_type": "UNAUTH_ACCESS",
"source_project": "Nginx配置分析",
"detection_method": "配置审查",
"regression_note": "验证WebSocket连接是否需要认证",
"mapped_paths": ["/ws"],
},
{
"vuln_id": "HV-011",
"title": "Nacos serverIdentity 权限绕过",
"severity": "high",
"vuln_type": "PRIVILEGE_BYPASS",
"source_project": "长安深蓝汽车(POC漏洞)",
"detection_method": "POC扫描",
"regression_note": "验证/nacos/v1/auth/users是否仍可匿名访问用户列表",
"mapped_paths": ["/nacos/v1/auth/users"],
},
{
"vuln_id": "HV-016",
"title": "JWT弱密钥",
"severity": "high",
"vuln_type": "WEAK_CRYPTO",
"source_project": "长安深蓝汽车(安全测试报告)",
"detection_method": "渗透测试",
"regression_note": "验证JWT密钥是否已更换为强密钥",
"mapped_paths": ["全部JWT接口"],
},
{
"vuln_id": "HV-026",
"title": "NoSQL注入 - 登录接口",
"severity": "critical",
"vuln_type": "NOSQL_INJECTION",
"source_project": "新统一平台(HCL AppScan)",
"detection_method": "AppScan自动化扫描",
"regression_note": "验证username参数类型校验是否已加强",
"mapped_paths": ["/platform/api/auth/login"],
},
{
"vuln_id": "HV-027",
"title": "API成批分配 - 用户查询接口",
"severity": "high",
"vuln_type": "MASS_ASSIGNMENT",
"source_project": "新统一平台(HCL AppScan)",
"detection_method": "AppScan自动化扫描",
"regression_note": "验证请求体中是否可注入is_admin/role字段",
"mapped_paths": ["/api/manageUser/getManagerPageForBook"],
},
{
"vuln_id": "HV-030",
"title": "注销后Token/会话未失效",
"severity": "high",
"vuln_type": "SESSION_MANAGEMENT",
"source_project": "南山区委(前台渗透测试报告)",
"detection_method": "渗透测试",
"regression_note": "注销后使用旧Token调用需认证接口,验证是否返回401",
"mapped_paths": ["/platform/api/auth/logout", "/platform/api/system/logout"],
},
{
"vuln_id": "HV-031",
"title": "运维集控越权访问 - company_id参数未校验",
"severity": "high",
"vuln_type": "PRIVILEGE_ESCALATION",
"source_project": "天津海油(渗透测试复测)",
"detection_method": "渗透测试",
"regression_note": "使用普通用户Token访问/monitor/api2/api/的管理接口",
"mapped_paths": ["/monitor/api2/api/roommaster/", "/monitor/api2/api/alarmststus/"],
},
],
# ==================== 华为安全红线检查 ====================
"huawei_redlines": [
{
"check_id": "HW-01",
"category": "加密规范",
"requirement": "密码存储必须使用加盐哈希(bcrypt/argon2)",
"severity": "high",
"check_method": "检查密码存储算法,禁止MD5/SHA256无盐哈希",
},
{
"check_id": "HW-02",
"category": "加密规范",
"requirement": "密钥不能硬编码在代码中",
"severity": "high",
"check_method": "代码审计,检查是否有硬编码密钥",
},
{
"check_id": "HW-03",
"category": "传输安全",
"requirement": "必须使用HTTPS协议",
"severity": "high",
"check_method": "检查是否强制HTTPS,禁止HTTP访问",
},
{
"check_id": "HW-04",
"category": "传输安全",
"requirement": "TLS版本不低于1.2",
"severity": "medium",
"check_method": "检查TLS版本配置",
},
{
"check_id": "HW-05",
"category": "鉴权机制",
"requirement": "Token有效期不应超过24小时",
"severity": "medium",
"check_method": "检查JWT/Token过期时间配置",
},
{
"check_id": "HW-06",
"category": "鉴权机制",
"requirement": "注销后Token必须立即失效",
"severity": "high",
"check_method": "注销后使用旧Token访问接口,应返回401",
},
{
"check_id": "HW-07",
"category": "鉴权机制",
"requirement": "验证码必须随机生成",
"severity": "medium",
"check_method": "检查验证码生成逻辑",
},
{
"check_id": "HW-08",
"category": "鉴权机制",
"requirement": "登录失败5次后应锁定账号15分钟",
"severity": "medium",
"check_method": "连续错误登录测试",
},
{
"check_id": "HW-09",
"category": "配置管理",
"requirement": "中间件管理界面不应对外开放",
"severity": "high",
"check_method": "检查/nacos/、/actuator/等路径是否可外部访问",
},
{
"check_id": "HW-10",
"category": "配置管理",
"requirement": "Swagger/API文档不应在生产环境暴露",
"severity": "medium",
"check_method": "检查/swagger-ui.html、/v2/api-docs等路径",
},
{
"check_id": "HW-11",
"category": "配置管理",
"requirement": "调试接口和测试端点应关闭",
"severity": "medium",
"check_method": "检查/test/、/debug/等路径",
},
{
"check_id": "HW-12",
"category": "输入验证",
"requirement": "所有用户输入必须进行服务端校验",
"severity": "high",
"check_method": "SQL注入、XSS、NoSQL注入测试",
},
{
"check_id": "HW-13",
"category": "权限控制",
"requirement": "每个接口必须校验用户权限",
"severity": "high",
"check_method": "越权测试:普通用户访问管理员接口",
},
{
"check_id": "HW-14",
"category": "权限控制",
"requirement": "用户只能访问自己的数据",
"severity": "high",
"check_method": "水平越权测试:遍历ID访问其他用户数据",
},
{
"check_id": "HW-15",
"category": "日志审计",
"requirement": "敏感操作必须记录日志",
"severity": "medium",
"check_method": "检查登录、权限变更等操作是否有日志",
},
],
# ==================== SQL注入测试载荷 ====================
"sql_injection_payloads": [
"' OR '1'='1",
"' OR '1'='1' --",
"' OR '1'='1' /*",
"1' AND '1'='1",
"1' AND '1'='2",
"1 OR 1=1",
"1 AND 1=2",
"'; DROP TABLE users--",
"' UNION SELECT NULL--",
"' UNION SELECT 1,2,3--",
"admin'--",
"admin' #",
"1; EXEC xp_cmdshell('dir')--",
"' AND SLEEP(5)--",
"' AND BENCHMARK(10000000,SHA1('test'))--",
"1' ORDER BY 1--",
"1' GROUP BY 1--",
"' HAVING 1=1--",
"-1' UNION SELECT table_name FROM information_schema.tables--",
"-1' UNION SELECT column_name FROM information_schema.columns--",
],
# ==================== 敏感路径探测字典 ====================
"sensitive_paths": [
# 配置文件
"/config.json",
"/static/config.json",
"/config.yaml",
"/.env",
"/.git/config",
"/WEB-INF/web.xml",
# 管理界面
"/nacos/",
"/actuator/",
"/swagger-ui.html",
"/swagger-resources",
"/v2/api-docs",
"/druid/",
"/admin/",
"/manager/",
# 测试调试
"/test/",
"/debug/",
"/health",
"/info",
"/trace",
"/metrics",
# 备份文件
"/backup.zip",
"/backup.sql",
"/db.sql",
"/dump.sql",
# 内部接口
"/api/internal/",
"/api/admin/",
"/system/inner/",
"/noLogin/",
],
}
def get_historical_vulns():
"""获取历史漏洞列表"""
return KNOWLEDGE_BASE.get("historical_vulns", [])
def get_huawei_redlines():
"""获取华为安全红线列表"""
return KNOWLEDGE_BASE.get("huawei_redlines", [])
def get_sql_payloads():
"""获取SQL注入载荷列表"""
return KNOWLEDGE_BASE.get("sql_injection_payloads", [])
def get_sensitive_paths():
"""获取敏感路径字典"""
return KNOWLEDGE_BASE.get("sensitive_paths", [])
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:security_service.py
模块描述:安全测试服务层,提供安全测试配置管理、执行调度、结果查询等业务逻辑
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
import logging
from datetime import datetime
from typing import Optional, List, Tuple, Dict, Any
from concurrent.futures import ThreadPoolExecutor
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, desc, or_
from app.models.security_config import SecurityConfig
from app.models.vulnerability_result import VulnerabilityResult
from app.models.execution import Execution
from app.models.test_case import TestCase
from app.models.module import Module
from app.executors.security_executor import SecurityExecutor, SecurityCaseResult
from app.utils.id_generator import generate_id
from app.config import settings
logger = logging.getLogger(__name__)
class SecurityService:
"""
安全测试服务类
提供安全测试配置管理、执行调度、结果查询等功能。
"""
def __init__(self, db: AsyncSession):
"""
初始化安全测试服务
Args:
db: 数据库会话
"""
self.db = db
# ==================== 配置管理 ====================
async def get_config(self, config_id: str) -> Optional[SecurityConfig]:
"""
获取安全测试配置
Args:
config_id: 配置ID
Returns:
SecurityConfig: 配置对象,不存在返回 None
"""
result = await self.db.execute(
select(SecurityConfig).where(SecurityConfig.id == config_id)
)
return result.scalar_one_or_none()
async def get_default_config(self) -> Optional[SecurityConfig]:
"""
获取默认安全测试配置
Returns:
SecurityConfig: 默认配置,不存在返回 None
"""
result = await self.db.execute(
select(SecurityConfig).where(SecurityConfig.is_default == True)
)
return result.scalar_one_or_none()
async def list_configs(
self,
skip: int = 0,
limit: int = 20
) -> Tuple[List[SecurityConfig], int]:
"""
获取安全测试配置列表
Args:
skip: 跳过记录数
limit: 返回记录数
Returns:
tuple: (配置列表, 总数)
"""
# 查询总数
count_result = await self.db.execute(
select(func.count(SecurityConfig.id))
)
total = count_result.scalar() or 0
# 查询列表
result = await self.db.execute(
select(SecurityConfig)
.order_by(desc(SecurityConfig.is_default), desc(SecurityConfig.updated_at))
.offset(skip)
.limit(limit)
)
configs = list(result.scalars().all())
return configs, total
async def create_config(self, config_data: Dict[str, Any]) -> SecurityConfig:
"""
创建安全测试配置
Args:
config_data: 配置数据
Returns:
SecurityConfig: 创建的配置对象
"""
config_id = generate_id("sec_cfg")
config = SecurityConfig(
id=config_id,
name=config_data.get("name", ""),
target_url=config_data.get("target_url", ""),
server_ip=config_data.get("server_ip", ""),
verify_ssl=config_data.get("verify_ssl", False),
timeout=config_data.get("timeout", 30),
accounts=config_data.get("accounts", {}),
auth_config=config_data.get("auth_config", {}),
rate_limits=config_data.get("rate_limits", {}),
api_prefixes=config_data.get("api_prefixes", {}),
is_default=config_data.get("is_default", False),
description=config_data.get("description", ""),
)
self.db.add(config)
await self.db.commit()
await self.db.refresh(config)
logger.info(f"创建安全测试配置: {config_id} - {config.name}")
return config
async def update_config(
self,
config_id: str,
config_data: Dict[str, Any]
) -> Optional[SecurityConfig]:
"""
更新安全测试配置
Args:
config_id: 配置ID
config_data: 更新数据
Returns:
SecurityConfig: 更新后的配置,不存在返回 None
"""
config = await self.get_config(config_id)
if not config:
return None
# 更新字段
for key, value in config_data.items():
if value is not None and hasattr(config, key):
setattr(config, key, value)
config.updated_at = datetime.utcnow()
await self.db.commit()
await self.db.refresh(config)
logger.info(f"更新安全测试配置: {config_id}")
return config
async def delete_config(self, config_id: str) -> bool:
"""
删除安全测试配置
Args:
config_id: 配置ID
Returns:
bool: 是否成功删除
"""
config = await self.get_config(config_id)
if not config:
return False
await self.db.delete(config)
await self.db.commit()
logger.info(f"删除安全测试配置: {config_id}")
return True
# ==================== 账号测试 ====================
async def test_account(
self,
config_id: str,
account_key: str
) -> Dict[str, Any]:
"""
测试账号连接
Args:
config_id: 配置ID
account_key: 账号键名
Returns:
dict: 测试结果
"""
config = await self.get_config(config_id)
if not config:
return {
"success": False,
"account_key": account_key,
"token_preview": "",
"error_message": "配置不存在"
}
# 创建临时执行器测试登录
executor = SecurityExecutor(config.to_client_config())
result = executor.auth.test_account(account_key)
return {
"success": result["success"],
"account_key": account_key,
"token_preview": result["token_preview"],
"error_message": result["error"]
}
# ==================== 执行管理 ====================
async def create_execution(
self,
config_id: str,
module_ids: List[str] = None,
case_ids: List[str] = None,
name: str = ""
) -> Execution:
"""
创建安全测试执行记录
Args:
config_id: 安全测试配置ID
module_ids: 要执行的模块ID列表
case_ids: 要执行的用例ID列表
name: 执行名称
Returns:
Execution: 执行记录
"""
execution_id = generate_id("exec")
# 如果没有指定用例,查询所有安全测试用例
if not case_ids and not module_ids:
case_ids = await self._get_all_security_case_ids()
elif module_ids and not case_ids:
case_ids = await self._get_case_ids_by_modules(module_ids)
total_cases = len(case_ids)
execution = Execution(
id=execution_id,
name=name or f"安全测试-{datetime.now().strftime('%Y%m%d_%H%M%S')}",
trigger_type="manual",
trigger_by="security_test",
total_cases=total_cases,
status="pending",
case_type="security",
config={"config_id": config_id, "case_ids": case_ids},
)
self.db.add(execution)
await self.db.commit()
await self.db.refresh(execution)
logger.info(f"创建安全测试执行: {execution_id}, 用例数: {total_cases}")
return execution
async def _get_all_security_case_ids(self) -> List[str]:
"""获取所有安全测试用例ID"""
result = await self.db.execute(
select(TestCase.id).where(TestCase.case_type == "security")
)
return [row[0] for row in result.all()]
async def _get_case_ids_by_modules(self, module_ids: List[str]) -> List[str]:
"""获取指定模块的安全测试用例ID"""
result = await self.db.execute(
select(TestCase.id)
.where(TestCase.module_id.in_(module_ids))
.where(TestCase.case_type == "security")
)
return [row[0] for row in result.all()]
async def run_execution(self, execution_id: str, progress_callback=None):
"""
执行安全测试
Args:
execution_id: 执行ID
progress_callback: 进度回调函数
"""
execution = await self._get_execution(execution_id)
if not execution:
logger.error(f"执行记录不存在: {execution_id}")
return
config_id = execution.config.get("config_id")
case_ids = execution.config.get("case_ids", [])
# 获取配置
config = await self.get_config(config_id)
if not config:
logger.error(f"安全测试配置不存在: {config_id}")
execution.status = "failed"
await self.db.commit()
return
# 获取用例
cases = await self._get_cases_by_ids(case_ids)
if not cases:
logger.warning(f"没有找到要执行的用例")
execution.status = "completed"
await self.db.commit()
return
# 更新执行状态
execution.status = "running"
execution.start_time = datetime.utcnow()
await self.db.commit()
try:
# 在线程池中执行(避免阻塞 asyncio)
loop = asyncio.get_event_loop()
# 在 async 上下文中预先把 ORM 对象转成纯字典,避免:
# 1. SQLAlchemy JSON 字段在离开 session 上下文后变为 str
# 2. ORM 对象在 run_in_executor 中脱钩(detached)引发异常
import json as _json
def _to_plain_steps(steps_val):
if steps_val is None:
return {}
if isinstance(steps_val, str):
try:
parsed = _json.loads(steps_val)
return parsed if isinstance(parsed, dict) else {}
except Exception:
return {}
if isinstance(steps_val, dict):
return steps_val
return {}
plain_cases = [
{
"id": c.id,
"name": c.name,
"steps": _to_plain_steps(c.steps),
}
for c in cases
]
client_config = config.to_client_config()
# 同步执行函数
def sync_run():
try:
executor = SecurityExecutor(client_config)
executor.start()
return executor.execute_cases(
plain_cases,
progress_callback=lambda cid, st, r: None # 同步回调先不处理
)
except Exception as e:
import traceback
logger.error(f"sync_run 异常: {e}")
logger.error(traceback.format_exc())
raise
# 在线程池中执行
results: List[SecurityCaseResult] = await loop.run_in_executor(
None, sync_run
)
# 保存结果
passed = 0
failed = 0
for r in results:
await self._save_vulnerability_result(execution_id, r)
if r.is_vulnerable:
failed += 1
else:
passed += 1
# 更新执行统计
execution.passed = passed
execution.failed = failed
execution.status = "completed"
execution.end_time = datetime.utcnow()
execution.duration = (execution.end_time - execution.start_time).total_seconds()
execution.pass_rate = round((passed / execution.total_cases) * 100, 2) if execution.total_cases > 0 else 0
await self.db.commit()
logger.info(f"安全测试执行完成: {execution_id}, 通过: {passed}, 失败: {failed}")
except Exception as e:
logger.error(f"安全测试执行异常: {e}")
execution.status = "failed"
execution.end_time = datetime.utcnow()
await self.db.commit()
async def _save_vulnerability_result(
self,
execution_id: str,
result: SecurityCaseResult
):
"""保存漏洞测试结果"""
vuln_result = VulnerabilityResult(
id=generate_id("vuln"),
execution_id=execution_id,
case_id=result.case_id,
test_id=result.test_id,
name=result.name,
level=result.level,
description=result.description,
request_info=result.request_info,
response_info=result.response_info,
is_vulnerable=result.is_vulnerable,
fix_suggestion=result.fix_suggestion,
duration=result.duration,
status=result.status,
error_message=result.error,
extra_data=result.metadata,
)
self.db.add(vuln_result)
# 不在这里 commit,由调用方统一处理
async def _get_execution(self, execution_id: str) -> Optional[Execution]:
"""获取执行记录"""
result = await self.db.execute(
select(Execution).where(Execution.id == execution_id)
)
return result.scalar_one_or_none()
async def _get_cases_by_ids(self, case_ids: List[str]) -> List[TestCase]:
"""获取指定ID的用例"""
if not case_ids:
return []
result = await self.db.execute(
select(TestCase).where(TestCase.id.in_(case_ids))
)
return list(result.scalars().all())
# ==================== 结果查询 ====================
async def get_vulnerability_results(
self,
execution_id: str,
skip: int = 0,
limit: int = 50,
level: Optional[str] = None,
is_vulnerable: Optional[bool] = None
) -> Tuple[List[VulnerabilityResult], int]:
"""
获取漏洞测试结果列表
Args:
execution_id: 执行ID
skip: 跳过记录数
limit: 返回记录数
level: 风险等级筛选
is_vulnerable: 是否存在漏洞筛选
Returns:
tuple: (结果列表, 总数)
"""
# 构建查询条件
conditions = [VulnerabilityResult.execution_id == execution_id]
if level:
conditions.append(VulnerabilityResult.level == level)
if is_vulnerable is not None:
conditions.append(VulnerabilityResult.is_vulnerable == is_vulnerable)
# 查询总数
count_query = select(func.count(VulnerabilityResult.id)).where(*conditions)
count_result = await self.db.execute(count_query)
total = count_result.scalar() or 0
# 查询列表(按风险等级排序)
result = await self.db.execute(
select(VulnerabilityResult)
.where(*conditions)
.order_by(VulnerabilityResult.level, VulnerabilityResult.test_id)
.offset(skip)
.limit(limit)
)
results = list(result.scalars().all())
return results, total
async def get_execution_summary(self, execution_id: str) -> Dict[str, Any]:
"""
获取执行摘要
Args:
execution_id: 执行ID
Returns:
dict: 摘要数据
"""
# 统计各风险等级数量
result = await self.db.execute(
select(
VulnerabilityResult.level,
func.count(VulnerabilityResult.id),
func.sum(
func.case((VulnerabilityResult.is_vulnerable == True, 1), else_=0)
)
)
.where(VulnerabilityResult.execution_id == execution_id)
.group_by(VulnerabilityResult.level)
)
level_stats = {}
for row in result.all():
level, total, vuln_count = row
level_stats[level] = {
"total": total,
"vulnerable": vuln_count or 0,
}
return {
"execution_id": execution_id,
"level_stats": level_stats,
}
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
创建默认安全测试配置(直接操作数据库,避免 HTTP 编码问题)
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.models.security_config import SecurityConfig
def main():
db_path = os.path.join(os.path.dirname(__file__), "..", "data", "test_platform.db")
db_url = f"sqlite:///{os.path.abspath(db_path)}"
engine = create_engine(db_url)
Session = sessionmaker(bind=engine)
session = Session()
try:
# 检查是否已存在默认配置
existing = session.query(SecurityConfig).filter(SecurityConfig.is_default == True).first()
if existing:
print(f"默认配置已存在: {existing.name} ({existing.id})")
return
config = SecurityConfig(
id="sec_cfg_default",
name="新统一平台安全测试配置",
target_url="https://192.168.5.44",
server_ip="192.168.5.44",
verify_ssl=False,
timeout=30,
accounts={
"superadmin": {"username": "superadmin", "password": "Ubains@1357"},
"admin": {"username": "admin@aq", "password": "Ubains@1357"},
"user": {"username": "user@aq", "password": "Ubains@1357"},
"captcha": "csba",
},
auth_config={
"token_type": "accessToken",
"mechanism": "JWT",
"login_path": "/platform/api/auth/login",
"token_header": "accessToken",
"captcha_path": "/platform/api/code",
},
rate_limits={
"brute_force_max": 20,
"rate_limit_max": 100,
"request_interval": 0.5,
},
api_prefixes={
"meeting": "/api/",
"monitor": "/monitor/api2/api/",
"platform": "/platform/api/",
},
is_default=True,
description="默认安全测试配置,用于新统一平台(192.168.5.44)",
)
session.add(config)
session.commit()
print(f"✅ 默认配置创建成功: {config.id}")
print(f" 名称: {config.name}")
print(f" 目标: {config.target_url}")
except Exception as e:
session.rollback()
print(f"❌ 错误: {e}")
finally:
session.close()
if __name__ == "__main__":
main()
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称:create_security_cases.py
模块描述:创建安全测试模块和用例
作者:czj
创建日期:2026-07-21
最后修改:2026-07-21
"""
import sys
import os
from datetime import datetime
# 添加项目路径
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.database import Base
from app.models import Module, TestCase
from app.utils.id_generator import generate_id
# ==================== 安全测试模块定义 ====================
SECURITY_MODULES = [
{
"id": "sec_api01",
"name": "API1 - 对象级别授权失效",
"description": "测试用户是否能够访问或操作其他用户的数据资源(水平越权、IDOR)",
"icon": "Lock",
"order": 1,
},
{
"id": "sec_api02",
"name": "API2 - 身份认证失效",
"description": "测试系统身份认证机制是否存在绕过、暴力破解等安全风险",
"icon": "Key",
"order": 2,
},
{
"id": "sec_api03",
"name": "API3 - 对象属性级别授权失效",
"description": "测试用户是否能够访问或修改不应被授权的对象属性(垂直越权、成批分配)",
"icon": "UserFilled",
"order": 3,
},
{
"id": "sec_api04",
"name": "API4 - 资源消耗不受限",
"description": "测试系统是否存在速率限制、文件上传、分页滥用等资源消耗漏洞",
"icon": "Timer",
"order": 4,
},
{
"id": "sec_api05",
"name": "API5 - 功能级别授权失效",
"description": "测试普通用户是否能够访问管理员功能接口",
"icon": "Operation",
"order": 5,
},
{
"id": "sec_api06",
"name": "API6 - 无限制访问敏感业务流",
"description": "测试业务流程是否存在批量注册、短信轰炸、接口滥用等风险",
"icon": "Connection",
"order": 6,
},
{
"id": "sec_api07",
"name": "API7 - 服务器端请求伪造",
"description": "测试系统是否存在SSRF漏洞,可探测内网服务",
"icon": "Monitor",
"order": 7,
},
{
"id": "sec_api08",
"name": "API8 - 安全配置错误",
"description": "检测系统是否存在CORS配置错误、敏感路径暴露、调试接口开启等配置问题",
"icon": "Setting",
"order": 8,
},
{
"id": "sec_api09",
"name": "API9 - 库存管理不当",
"description": "测试是否存在隐藏接口、旧版API、内部API暴露等资产管理问题",
"icon": "Files",
"order": 9,
},
{
"id": "sec_api10",
"name": "API10 - 不安全的第三方API集成",
"description": "测试第三方API集成是否存在凭证泄露、权限过度等安全问题",
"icon": "Link",
"order": 10,
},
{
"id": "sec_regression",
"name": "历史漏洞回归测试",
"description": "基于历史漏洞报告的回归验证测试用例",
"icon": "RefreshRight",
"order": 11,
},
{
"id": "sec_redline",
"name": "华为安全红线检查",
"description": "基于华为安全红线规范的合规性检查",
"icon": "Warning",
"order": 12,
},
]
# ==================== 安全测试用例定义 ====================
SECURITY_CASES = [
# ==================== API1: 对象级别授权失效 ====================
{
"id": "sec_2_1_1",
"module_id": "sec_api01",
"name": "水平越权访问其他用户会议详情",
"description": "用普通用户Token访问超级管理员的会议详情,验证是否存在越权漏洞",
"priority": "high",
"tags": ["OWASP-API1", "水平越权", "IDOR"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/getMessageById", "method": "GET"},
"auth": {"required": True, "account": "user", "expect_different_account": "superadmin"},
"request": {"params": {"id": "${meeting_id}"}},
"pre_steps": [
{
"action": "get_resource_id",
"account": "superadmin",
"path": "/api/message/getMeetingList",
"params": {"pageNo": 1, "pageSize": 5},
"extract": {"meeting_id": "data.records[0].id"}
}
],
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "越权访问应被拒绝"},
{"type": "response_code_contain", "expect": ["401", "403", "B0027"], "description": "应返回权限不足"}
],
"vulnerability": {
"id": "2.1.1",
"name": "水平越权访问其他用户会议详情",
"level": "high",
"description": "user使用Token成功访问了superadmin的会议详情,存在水平越权漏洞",
"fix_suggestion": "在接口中增加用户身份校验,确保用户只能访问自己创建的会议数据"
}
}
},
{
"id": "sec_2_1_2a",
"module_id": "sec_api01",
"name": "水平越权修改其他用户会议",
"description": "用普通用户Token尝试修改超级管理员的会议",
"priority": "high",
"tags": ["OWASP-API1", "水平越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/update", "method": "POST"},
"auth": {"required": True, "account": "user"},
"request": {"body": {"id": "${meeting_id}", "title": "安全测试-越权修改尝试"}},
"pre_steps": [
{"action": "get_resource_id", "account": "superadmin", "path": "/api/message/getMeetingList", "params": {"pageNo": 1, "pageSize": 5}, "extract": {"meeting_id": "data.records[0].id"}}
],
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "越权修改应被拒绝"}
],
"vulnerability": {
"id": "2.1.2",
"name": "水平越权修改其他用户会议",
"level": "high",
"description": "user可修改superadmin的会议,存在严重水平越权漏洞",
"fix_suggestion": "修改会议接口必须校验当前用户是否为会议创建者"
}
}
},
{
"id": "sec_2_1_2b",
"module_id": "sec_api01",
"name": "水平越权取消其他用户会议",
"description": "用普通用户Token尝试取消超级管理员的会议",
"priority": "high",
"tags": ["OWASP-API1", "水平越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/cancel/", "method": "PUT"},
"auth": {"required": True, "account": "user"},
"request": {"body": {"id": "${meeting_id}"}},
"pre_steps": [
{"action": "get_resource_id", "account": "superadmin", "path": "/api/message/getMeetingList", "params": {"pageNo": 1, "pageSize": 5}, "extract": {"meeting_id": "data.records[0].id"}}
],
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "越权取消应被拒绝"}
],
"vulnerability": {
"id": "2.1.2",
"name": "水平越权取消其他用户会议",
"level": "high",
"description": "user可取消superadmin的会议,存在严重水平越权漏洞",
"fix_suggestion": "取消会议接口必须校验当前用户是否为会议创建者或管理员"
}
}
},
{
"id": "sec_2_1_3",
"module_id": "sec_api01",
"name": "水平越权访问其他用户收藏列表",
"description": "通过修改userId参数访问其他用户的收藏列表",
"priority": "high",
"tags": ["OWASP-API1", "水平越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/userCollection/listUserCollectionPage", "method": "GET"},
"auth": {"required": True, "account": "user"},
"request": {"params": {"userId": "1", "pageNo": 1, "pageSize": 10}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "访问其他用户收藏应被拒绝"}
],
"vulnerability": {
"id": "2.1.3",
"name": "水平越权访问其他用户收藏列表",
"level": "high",
"description": "通过修改userId参数可访问其他用户收藏",
"fix_suggestion": "收藏列表接口应从Token中提取用户ID,忽略客户端传入的userId参数"
}
}
},
{
"id": "sec_2_1_4",
"module_id": "sec_api01",
"name": "水平越权跨公司创建会议",
"description": "用普通用户Token修改companyNumber参数跨公司创建会议",
"priority": "high",
"tags": ["OWASP-API1", "水平越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/conference/insert", "method": "POST"},
"auth": {"required": True, "account": "user"},
"request": {"body": {"title": "安全测试", "companyNumber": "OTHER_COMPANY_001", "roomId": "1"}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "跨公司创建应被拒绝"}
],
"vulnerability": {
"id": "2.1.4",
"name": "水平越权跨公司创建会议",
"level": "high",
"description": "通过修改companyNumber可跨公司创建会议,数据隔离被破坏",
"fix_suggestion": "创建会议接口应从Token中提取用户的companyNumber"
}
}
},
{
"id": "sec_2_1_5",
"module_id": "sec_api01",
"name": "水平越权访问运维集控设备接口",
"description": "用普通用户Token访问运维集控设备管理接口",
"priority": "high",
"tags": ["OWASP-API1", "水平越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/monitor/api2/api/roommaster/", "method": "GET"},
"auth": {"required": True, "account": "user"},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "普通用户不应能访问运维集控"}
],
"vulnerability": {
"id": "2.1.5",
"name": "水平越权访问运维集控设备接口",
"level": "high",
"description": "普通用户Token可访问运维集控设备管理接口",
"fix_suggestion": "运维集控接口应增加角色权限校验"
}
}
},
{
"id": "sec_2_1_7",
"module_id": "sec_api01",
"name": "IDOR遍历访问签到记录",
"description": "遍历签到ID访问其他用户的签到记录",
"priority": "high",
"tags": ["OWASP-API1", "IDOR"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/signs/getSigns", "method": "GET"},
"auth": {"required": True, "account": "user"},
"request": {"params": {"id": "1"}},
"assertions": [
{"type": "is_success", "expect": False, "description": "遍历ID访问应被拒绝"}
],
"vulnerability": {
"id": "2.1.7",
"name": "IDOR遍历访问签到记录",
"level": "high",
"description": "通过遍历ID可访问其他用户签到记录",
"fix_suggestion": "签到记录接口应校验当前用户权限"
}
}
},
{
"id": "sec_2_1_8",
"module_id": "sec_api01",
"name": "IDOR遍历访问其他用户通知",
"description": "遍历通知ID访问其他用户的通知",
"priority": "high",
"tags": ["OWASP-API1", "IDOR"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/notification/getNotificationByNoticeId", "method": "GET"},
"auth": {"required": True, "account": "user"},
"request": {"params": {"noticeId": "1"}},
"assertions": [
{"type": "is_success", "expect": False, "description": "遍历ID访问应被拒绝"}
],
"vulnerability": {
"id": "2.1.8",
"name": "IDOR遍历访问其他用户通知",
"level": "high",
"description": "通过遍历ID可访问其他用户通知",
"fix_suggestion": "通知接口应校验当前用户是否为通知接收者"
}
}
},
# ==================== API2: 身份认证失效 ====================
{
"id": "sec_2_2_1",
"module_id": "sec_api02",
"name": "Token伪造测试",
"description": "使用伪造的Token访问需要认证的接口",
"priority": "high",
"tags": ["OWASP-API2", "认证绕过"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/getMeetingList", "method": "GET"},
"auth": {"required": True, "account": "user", "use_fake_token": True},
"request": {"params": {"pageNo": 1, "pageSize": 10}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "伪造Token应被拒绝"}
],
"vulnerability": {
"id": "2.2.1",
"name": "Token伪造测试",
"level": "high",
"description": "伪造的Token未被正确校验",
"fix_suggestion": "加强Token校验机制,增加签名验证"
}
}
},
{
"id": "sec_2_2_2",
"module_id": "sec_api02",
"name": "NoSQL注入-登录接口",
"description": "在登录接口username参数注入MongoDB操作符",
"priority": "critical",
"tags": ["OWASP-API2", "NoSQL注入", "Critical"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/platform/api/auth/login", "method": "POST"},
"auth": {"required": False},
"request": {"body": {"username": {"$ne": "1"}, "password": "test", "code": "csba"}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "NoSQL注入应被拦截"}
],
"vulnerability": {
"id": "2.2.2",
"name": "NoSQL注入-登录接口",
"level": "critical",
"description": "登录接口存在NoSQL注入漏洞,可绕过认证(CVSS 9.4)",
"fix_suggestion": "对username参数进行类型校验,确保为字符串类型"
}
}
},
{
"id": "sec_2_2_3",
"module_id": "sec_api02",
"name": "暴力破解-登录接口",
"description": "测试登录接口是否存在速率限制",
"priority": "high",
"tags": ["OWASP-API2", "暴力破解"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/platform/api/auth/login", "method": "POST"},
"auth": {"required": False},
"request": {"body": {"username": "admin", "password": "wrong_password", "code": "csba"}},
"assertions": [
{"type": "body_not_contains", "expect": "用户名或密码错误", "description": "连续错误登录应被限流"}
],
"vulnerability": {
"id": "2.2.3",
"name": "暴力破解-登录接口",
"level": "high",
"description": "登录接口无速率限制,可被暴力破解",
"fix_suggestion": "实现登录失败计数与账号临时锁定机制"
}
}
},
{
"id": "sec_2_2_4",
"module_id": "sec_api02",
"name": "注销后Token未失效",
"description": "注销后使用旧Token访问接口",
"priority": "high",
"tags": ["OWASP-API2", "会话管理"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/getMeetingList", "method": "GET"},
"auth": {"required": True, "account": "user", "after_logout": True},
"request": {"params": {"pageNo": 1, "pageSize": 10}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "注销后Token应失效"}
],
"vulnerability": {
"id": "2.2.4",
"name": "注销后Token未失效",
"level": "high",
"description": "注销后旧Token仍可使用",
"fix_suggestion": "实现Token黑名单机制"
}
}
},
# ==================== API5: 功能级别授权失效 ====================
{
"id": "sec_2_5_1",
"module_id": "sec_api05",
"name": "普通用户访问管理员接口-用户管理",
"description": "普通用户尝试访问管理员用户管理接口",
"priority": "high",
"tags": ["OWASP-API5", "垂直越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/manageUser/getManagerPage", "method": "POST"},
"auth": {"required": True, "account": "user"},
"request": {"body": {"pageNo": 1, "pageSize": 10}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "普通用户不应能访问管理员接口"}
],
"vulnerability": {
"id": "2.5.1",
"name": "普通用户访问管理员接口",
"level": "high",
"description": "普通用户可访问管理员接口",
"fix_suggestion": "接口增加RBAC权限校验"
}
}
},
{
"id": "sec_2_5_2",
"module_id": "sec_api05",
"name": "普通用户访问权限组管理接口",
"description": "普通用户尝试访问权限组管理接口",
"priority": "high",
"tags": ["OWASP-API5", "垂直越权"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/permissionGroup/add", "method": "POST"},
"auth": {"required": True, "account": "user"},
"request": {"body": {"name": "测试权限组"}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "普通用户不应能访问权限管理接口"}
],
"vulnerability": {
"id": "2.5.2",
"name": "普通用户访问权限组管理接口",
"level": "high",
"description": "普通用户可操作权限组管理",
"fix_suggestion": "权限组接口增加管理员角色校验"
}
}
},
# ==================== API8: 安全配置错误 ====================
{
"id": "sec_2_8_1",
"module_id": "sec_api08",
"name": "Nacos未授权访问",
"description": "检测Nacos控制台是否可匿名访问",
"priority": "high",
"tags": ["OWASP-API8", "配置错误"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/nacos/", "method": "GET"},
"auth": {"required": False},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "Nacos不应匿名访问"}
],
"vulnerability": {
"id": "2.8.1",
"name": "Nacos未授权访问",
"level": "high",
"description": "Nacos控制台可匿名访问",
"fix_suggestion": "Nginx层面限制/nacos路径仅内网可访问"
}
}
},
{
"id": "sec_2_8_2",
"module_id": "sec_api08",
"name": "Swagger文档暴露",
"description": "检测Swagger文档是否对外暴露",
"priority": "medium",
"tags": ["OWASP-API8", "信息泄露"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/swagger-ui.html", "method": "GET"},
"auth": {"required": False},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "Swagger文档不应暴露"}
],
"vulnerability": {
"id": "2.8.2",
"name": "Swagger文档暴露",
"level": "medium",
"description": "Swagger API文档对外暴露",
"fix_suggestion": "生产环境禁用Swagger或限制内网访问"
}
}
},
{
"id": "sec_2_8_3",
"module_id": "sec_api08",
"name": "SQL注入-会议查询接口",
"description": "测试会议查询接口是否存在SQL注入",
"priority": "high",
"tags": ["OWASP-API8", "SQL注入"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/getMeetingList", "method": "GET"},
"auth": {"required": True, "account": "user"},
"request": {"params": {"title": "' OR '1'='1", "pageNo": 1, "pageSize": 10}},
"assertions": [
{"type": "body_not_contains", "expect": "SQL", "description": "SQL注入应被拦截"}
],
"vulnerability": {
"id": "2.8.3",
"name": "SQL注入-会议查询接口",
"level": "high",
"description": "会议查询接口存在SQL注入漏洞",
"fix_suggestion": "使用参数化查询,禁止直接拼接SQL"
}
}
},
{
"id": "sec_2_8_4",
"module_id": "sec_api08",
"name": "敏感配置文件泄露",
"description": "检测config.json等敏感配置文件是否可访问",
"priority": "high",
"tags": ["OWASP-API8", "信息泄露"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/static/config.json", "method": "GET"},
"auth": {"required": False},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "配置文件不应泄露"}
],
"vulnerability": {
"id": "2.8.4",
"name": "敏感配置文件泄露",
"level": "high",
"description": "config.json含敏感信息可被外部访问",
"fix_suggestion": "删除前端敏感配置或限制访问"
}
}
},
# ==================== 历史漏洞回归 ====================
{
"id": "sec_hv_001",
"module_id": "sec_regression",
"name": "HV-001 SQL注入-会议预定接口",
"description": "历史漏洞回归:会议预定接口SQL注入(长安深蓝汽车)",
"priority": "high",
"tags": ["历史漏洞", "SQL注入", "长安深蓝汽车"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/book", "method": "POST"},
"auth": {"required": True, "account": "user"},
"request": {"body": {"title": "' OR '1'='1--"}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "SQL注入应被拦截"}
],
"vulnerability": {
"id": "HV-001",
"name": "SQL注入-会议预定接口",
"level": "high",
"description": "历史漏洞:会议预定接口SQL注入",
"fix_suggestion": "使用预编译语句"
}
}
},
{
"id": "sec_hv_005",
"module_id": "sec_regression",
"name": "HV-005 Nacos未授权访问",
"description": "历史漏洞回归:Nacos配置中心未授权访问(南山区委)",
"priority": "high",
"tags": ["历史漏洞", "未授权访问", "南山区委"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/nacos/v1/cs/configs", "method": "GET"},
"auth": {"required": False},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "Nacos应需认证"}
],
"vulnerability": {
"id": "HV-005",
"name": "Nacos未授权访问",
"level": "high",
"description": "历史漏洞:Nacos配置列表可匿名访问",
"fix_suggestion": "开启Nacos鉴权"
}
}
},
{
"id": "sec_hv_011",
"module_id": "sec_regression",
"name": "HV-011 Nacos serverIdentity权限绕过",
"description": "历史漏洞回归:Nacos用户列表接口权限绕过",
"priority": "high",
"tags": ["历史漏洞", "权限绕过", "长安深蓝汽车"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/nacos/v1/auth/users", "method": "GET"},
"auth": {"required": False},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "用户列表不应匿名访问"}
],
"vulnerability": {
"id": "HV-011",
"name": "Nacos serverIdentity权限绕过",
"level": "high",
"description": "历史漏洞:Nacos用户列表可匿名访问",
"fix_suggestion": "升级Nacos版本并开启鉴权"
}
}
},
# ==================== 华为安全红线 ====================
{
"id": "sec_hw_01",
"module_id": "sec_redline",
"name": "HW-01 密码存储加盐哈希",
"description": "华为红线:密码存储必须使用加盐哈希",
"priority": "high",
"tags": ["华为红线", "加密规范"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/platform/api/auth/login", "method": "POST"},
"auth": {"required": False},
"request": {"body": {"username": "test", "password": "test", "code": "csba"}},
"assertions": [
{"type": "body_not_contains", "expect": "SHA256", "description": "不应暴露哈希算法"}
],
"vulnerability": {
"id": "HW-01",
"name": "密码存储加盐哈希",
"level": "high",
"description": "华为红线检查:密码存储方式",
"fix_suggestion": "使用bcrypt/argon2等加盐哈希"
}
}
},
{
"id": "sec_hw_06",
"module_id": "sec_redline",
"name": "HW-06 注销后Token立即失效",
"description": "华为红线:注销后Token必须立即失效",
"priority": "high",
"tags": ["华为红线", "会话管理"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/api/message/getMeetingList", "method": "GET"},
"auth": {"required": True, "account": "user", "after_logout": True},
"request": {"params": {"pageNo": 1, "pageSize": 10}},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "注销后Token应失效"}
],
"vulnerability": {
"id": "HW-06",
"name": "注销后Token立即失效",
"level": "high",
"description": "华为红线检查:Token失效机制",
"fix_suggestion": "实现Token黑名单"
}
}
},
{
"id": "sec_hw_09",
"module_id": "sec_redline",
"name": "HW-09 中间件管理界面不对外开放",
"description": "华为红线:中间件管理界面不应暴露",
"priority": "high",
"tags": ["华为红线", "配置管理"],
"steps": {
"test_type": "api_security",
"target": {"base_url": "", "path": "/nacos/", "method": "GET"},
"auth": {"required": False},
"request": {},
"assertions": [
{"type": "status_code_not_equal", "expect": 200, "description": "Nacos不应外部访问"}
],
"vulnerability": {
"id": "HW-09",
"name": "中间件管理界面不对外开放",
"level": "high",
"description": "华为红线检查:中间件暴露",
"fix_suggestion": "Nginx限制中间件路径"
}
}
},
]
def create_security_modules_and_cases():
"""创建安全测试模块和用例"""
# 连接数据库
db_path = os.path.join(os.path.dirname(__file__), "..", "data", "test_platform.db")
db_url = f"sqlite:///{os.path.abspath(db_path)}"
engine = create_engine(db_url)
Session = sessionmaker(bind=engine)
session = Session()
try:
# 创建模块
print("=" * 60)
print("创建安全测试模块...")
print("=" * 60)
module_count = 0
for mod_data in SECURITY_MODULES:
existing = session.query(Module).filter_by(id=mod_data["id"]).first()
if existing:
print(f" 模块已存在: {mod_data['name']}")
continue
module = Module(
id=mod_data["id"],
name=mod_data["name"],
description=mod_data["description"],
icon=mod_data["icon"],
order=mod_data["order"],
module_type="security",
)
session.add(module)
module_count += 1
print(f" ✅ 创建模块: {mod_data['name']}")
session.commit()
print(f"\n模块创建完成: {module_count} 个新模块")
# 创建用例
print("\n" + "=" * 60)
print("创建安全测试用例...")
print("=" * 60)
case_count = 0
for case_data in SECURITY_CASES:
existing = session.query(TestCase).filter_by(id=case_data["id"]).first()
if existing:
print(f" 用例已存在: {case_data['name']}")
continue
case = TestCase(
id=case_data["id"],
module_id=case_data["module_id"],
name=case_data["name"],
description=case_data.get("description", ""),
priority=case_data.get("priority", "medium"),
tags=case_data.get("tags", []),
steps=case_data["steps"],
config={},
case_type="security",
status="active",
)
session.add(case)
case_count += 1
print(f" ✅ 创建用例: {case_data['id']} - {case_data['name']}")
session.commit()
print(f"\n用例创建完成: {case_count} 个新用例")
# 统计
print("\n" + "=" * 60)
print("安全测试模块统计")
print("=" * 60)
total_modules = session.query(Module).filter(Module.module_type == "security").count()
total_cases = session.query(TestCase).filter(TestCase.case_type == "security").count()
print(f" 安全测试模块总数: {total_modules}")
print(f" 安全测试用例总数: {total_cases}")
# 按模块统计
for mod in SECURITY_MODULES:
count = session.query(TestCase).filter(
TestCase.module_id == mod["id"],
TestCase.case_type == "security"
).count()
if count > 0:
print(f" - {mod['name']}: {count} 个用例")
except Exception as e:
session.rollback()
print(f"❌ 错误: {e}")
raise
finally:
session.close()
if __name__ == "__main__":
create_security_modules_and_cases()
\ No newline at end of file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""端到端测试安全测试执行(模拟前端从 Cases.vue 执行)"""
import sys
import os
import time
import requests
BASE = "http://127.0.0.1:8001"
# 1. 创建执行(模拟前端 executionApi.create)
print("=== 1. 创建执行 ===")
resp = requests.post(f"{BASE}/api/executions", json={
"case_ids": ["sec_2_8_1", "sec_2_8_2", "sec_2_8_3"],
"name": "安全测试端到端验证",
})
print(f"创建状态码: {resp.status_code}")
exec_data = resp.json()
exec_id = exec_data.get("id")
print(f"执行ID: {exec_id}")
print(f"case_type: {exec_data.get('case_type')}")
if not exec_id:
print("❌ 创建失败:", exec_data)
sys.exit(1)
# 2. 触发执行(模拟前端 executionApi.run)
print("\n=== 2. 触发执行 ===")
resp = requests.post(f"{BASE}/api/executions/{exec_id}/run")
print(f"触发状态码: {resp.status_code}")
print(f"触发响应: {resp.text[:200]}")
# 3. 轮询等待完成
print("\n=== 3. 等待执行完成 ===")
for i in range(30):
time.sleep(2)
resp = requests.get(f"{BASE}/api/executions/{exec_id}/results")
if resp.status_code == 200:
data = resp.json()
results = data.get("items", [])
done = [r for r in results if r.get("status") in ("passed", "failed", "error", "skipped")]
print(f" 轮询 {i+1}: {len(done)}/{len(results)} 完成")
if len(done) == len(results) and results:
print("\n=== 4. 执行结果 ===")
for r in results:
status = r.get("status")
err = r.get("error_message") or ""
print(f" {r.get('case_name')}: {status} | {err[:80]}")
break
# 检查执行记录状态
resp2 = requests.get(f"{BASE}/api/executions?skip=0&limit=1&case_type=security")
if resp2.status_code == 200:
items = resp2.json().get("items", [])
if items:
e = items[0]
if e.get("status") in ("completed", "failed"):
print(f"\n执行状态: {e.get('status')}, 通过: {e.get('passed')}, 失败: {e.get('failed')}")
else:
print("⚠️ 30秒内未完成,检查后端日志")
/**
* 安全测试 API 封装
*
* @author czj
* @date 2026-07-21
*/
import request from '@/utils/request'
import type {
SecurityConfig,
SecurityConfigFormData,
VulnerabilityResult,
SecurityExecutionCreate,
SecurityExecutionResponse,
KnowledgeBaseResponse,
AccountTestResponse,
ExecutionSummary,
} from '@/types/security'
// ==================== 配置管理 ====================
/** 获取安全测试配置列表 */
export function getSecurityConfigs(skip = 0, limit = 20) {
return request.get<{ items: SecurityConfig[]; total: number }>('/api/security/config', {
params: { skip, limit }
})
}
/** 获取默认配置 */
export function getDefaultConfig() {
return request.get<SecurityConfig>('/api/security/config/default')
}
/** 获取指定配置 */
export function getSecurityConfig(configId: string) {
return request.get<SecurityConfig>(`/api/security/config/${configId}`)
}
/** 创建安全测试配置 */
export function createSecurityConfig(data: SecurityConfigFormData) {
return request.post<SecurityConfig>('/api/security/config', data)
}
/** 更新安全测试配置 */
export function updateSecurityConfig(configId: string, data: Partial<SecurityConfigFormData>) {
return request.put<SecurityConfig>(`/api/security/config/${configId}`, data)
}
/** 删除安全测试配置 */
export function deleteSecurityConfig(configId: string) {
return request.delete(`/api/security/config/${configId}`)
}
// ==================== 账号测试 ====================
/** 测试账号连接 */
export function testAccount(configId: string, accountKey: string) {
return request.post<AccountTestResponse>('/api/security/test-account', {
configId,
accountKey
})
}
// ==================== 执行管理 ====================
/** 创建安全测试执行 */
export function createSecurityExecution(data: SecurityExecutionCreate) {
return request.post<SecurityExecutionResponse>('/api/security/executions', data)
}
/** 触发执行 */
export function runSecurityExecution(executionId: string) {
return request.post<{ execution_id: string; status: string; message: string }>(
`/api/security/executions/${executionId}/run`
)
}
/** 获取执行结果 */
export function getSecurityResults(
executionId: string,
params?: { skip?: number; limit?: number; level?: string; isVulnerable?: boolean }
) {
return request.get<{ items: VulnerabilityResult[]; total: number }>(
`/api/security/executions/${executionId}/results`,
{ params }
)
}
/** 获取执行摘要 */
export function getSecuritySummary(executionId: string) {
return request.get<ExecutionSummary>(
`/api/security/executions/${executionId}/summary`
)
}
// ==================== 知识库 ====================
/** 获取安全知识库 */
export function getKnowledgeBase() {
return request.get<KnowledgeBaseResponse>('/api/security/knowledge-base')
}
/**
* 安全测试相关类型定义
*
* @author czj
* @date 2026-07-21
*/
/**
* 安全测试配置
*/
export interface SecurityConfig {
id: string
name: string
targetUrl: string
serverIp: string
verifySsl: boolean
timeout: number
accounts: Record<string, any>
authConfig: Record<string, any>
rateLimits: Record<string, any>
apiPrefixes: Record<string, any>
isDefault: boolean
description: string
createdAt?: string
updatedAt?: string
}
/**
* 创建/更新安全测试配置
*/
export interface SecurityConfigFormData {
name: string
targetUrl: string
serverIp?: string
verifySsl?: boolean
timeout?: number
accounts?: Record<string, any>
authConfig?: Record<string, any>
rateLimits?: Record<string, any>
apiPrefixes?: Record<string, any>
isDefault?: boolean
description?: string
}
/**
* 漏洞测试结果
*/
export interface VulnerabilityResult {
id: string
executionId: string
caseId: string
testId: string
name: string
level: 'critical' | 'high' | 'medium' | 'low' | 'info'
description: string
requestInfo: string
responseInfo: string
isVulnerable: boolean
fixSuggestion: string
duration: number
status: string
errorMessage?: string
extraData: Record<string, any>
createdAt?: string
}
/**
* 安全测试执行创建参数
*/
export interface SecurityExecutionCreate {
configId: string
moduleIds?: string[]
caseIds?: string[]
includeRegression?: boolean
includeRedline?: boolean
name?: string
}
/**
* 安全测试执行响应
*/
export interface SecurityExecutionResponse {
id: string
name: string
status: string
totalCases: number
passed: number
failed: number
skipped: number
duration: number
configId: string
startTime?: string
endTime?: string
}
/**
* 历史漏洞条目
*/
export interface HistoricalVuln {
vulnId: string
title: string
severity: string
vulnType: string
sourceProject: string
detectionMethod: string
regressionNote: string
mappedPaths: string[]
}
/**
* 华为安全红线检查项
*/
export interface HuaweiRedlineCheck {
checkId: string
category: string
requirement: string
severity: string
checkMethod: string
}
/**
* 知识库响应
*/
export interface KnowledgeBaseResponse {
historicalVulns: HistoricalVuln[]
huaweiRedlines: HuaweiRedlineCheck[]
totalVulns: number
totalRedlines: number
}
/**
* 账号测试结果
*/
export interface AccountTestResponse {
accountKey: string
success: boolean
tokenPreview: string
errorMessage: string
}
/**
* 执行摘要
*/
export interface ExecutionSummary {
executionId: string
levelStats: Record<string, { total: number; vulnerable: number }>
}
......@@ -100,6 +100,9 @@
<template #default="{ row }">
<div class="case-name-cell">
<el-icon :size="18" class="case-icon"><Document /></el-icon>
<span v-if="row.caseType === 'security'" class="security-level-emoji">
{{ getSecurityLevelEmoji(row.steps?.vulnerability?.level) }}
</span>
<span>{{ row.name }}</span>
</div>
</template>
......@@ -135,8 +138,19 @@
</el-table-column>
<el-table-column label="步骤数" width="80" align="center">
<template #default="{ row }">
<template v-if="row.caseType === 'security'">
<el-tag
:type="getSecurityLevelType(row.steps?.vulnerability?.level)"
size="small"
effect="light"
>
{{ getSecurityLevelText(row.steps?.vulnerability?.level) }}
</el-tag>
</template>
<template v-else>
<span class="step-count">{{ row.steps?.length || 0 }}</span>
</template>
</template>
</el-table-column>
<el-table-column label="标签" min-width="150">
<template #default="{ row }">
......@@ -311,8 +325,8 @@
</div>
</div>
<!-- 步骤列表 -->
<div class="steps-section">
<!-- 步骤列表(UI用例) -->
<div class="steps-section" v-if="currentCase.caseType !== 'security'">
<h4>测试步骤 ({{ currentCase.steps?.length || 0 }})</h4>
<el-table
:data="currentCase.steps || []"
......@@ -335,6 +349,57 @@
</el-table>
<el-empty v-else description="暂无步骤" :image-size="60" />
</div>
<!-- 安全测试详情 -->
<div class="steps-section" v-if="currentCase.caseType === 'security' && currentCase.steps">
<h4>安全测试配置</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="测试类型">
<el-tag size="small" effect="plain">{{ currentCase.steps.test_type || '-' }}</el-tag>
</el-descriptions-item>
<el-descriptions-item label="风险等级">
<el-tag
:type="getSecurityLevelType(currentCase.steps.vulnerability?.level)"
size="small"
effect="light"
>
{{ getSecurityLevelEmoji(currentCase.steps.vulnerability?.level) }}
{{ getSecurityLevelText(currentCase.steps.vulnerability?.level) }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="请求方法" v-if="currentCase.steps.target">
{{ currentCase.steps.target?.method }} {{ currentCase.steps.target?.path }}
</el-descriptions-item>
<el-descriptions-item label="测试账号" v-if="currentCase.steps.auth">
{{ currentCase.steps.auth?.account || '-' }}
</el-descriptions-item>
</el-descriptions>
<div v-if="currentCase.steps.vulnerability" style="margin-top: 12px">
<h4>漏洞信息</h4>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="漏洞编号">
{{ currentCase.steps.vulnerability.id || '-' }}
</el-descriptions-item>
<el-descriptions-item label="漏洞描述">
{{ currentCase.steps.vulnerability.description || '-' }}
</el-descriptions-item>
<el-descriptions-item label="修复建议">
{{ currentCase.steps.vulnerability.fix_suggestion || '-' }}
</el-descriptions-item>
</el-descriptions>
</div>
<div v-if="currentCase.steps.assertions?.length" style="margin-top: 12px">
<h4>断言规则 ({{ currentCase.steps.assertions.length }})</h4>
<el-table :data="currentCase.steps.assertions" size="small" stripe>
<el-table-column label="#" width="50" type="index" />
<el-table-column label="类型" prop="type" width="180" />
<el-descriptions-item label="预期值" prop="expect" width="100" />
<el-table-column label="说明" prop="description" min-width="200" />
</el-table>
</div>
</div>
</div>
</el-dialog>
......@@ -533,6 +598,42 @@ const getPriorityText = (priority: string) => {
return map[priority] || priority
}
/** 安全测试风险等级 emoji */
const getSecurityLevelEmoji = (level?: string) => {
const map: Record<string, string> = {
critical: '🔴🔴🔴',
high: '🔴',
medium: '🟠',
low: '🟡',
info: '🔵',
}
return map[level || ''] || '⚪'
}
/** 安全测试风险等级 Tag 类型 */
const getSecurityLevelType = (level?: string) => {
const map: Record<string, string> = {
critical: 'danger',
high: 'danger',
medium: 'warning',
low: 'info',
info: 'info',
}
return map[level || ''] || 'info'
}
/** 安全测试风险等级文本 */
const getSecurityLevelText = (level?: string) => {
const map: Record<string, string> = {
critical: '严重',
high: '高危',
medium: '中危',
low: '低危',
info: '信息',
}
return map[level || ''] || '-'
}
const formatTime = (time: string) => {
if (!time) return '-'
return new Date(time).toLocaleString('zh-CN', {
......
......@@ -271,16 +271,28 @@
<el-form-item label="执行名称">
<el-input v-model="runConfig.name" placeholder="可选,留空自动生成" />
</el-form-item>
<el-form-item label="执行环境">
<!-- UI用例显示浏览器模式选项,安全测试不显示 -->
<el-form-item label="浏览器模式" v-if="currentType !== 'security'">
<el-switch v-model="runConfig.headless" active-text="无头" inactive-text="有头" />
</el-form-item>
<!-- 安全测试显示配置选择 -->
<el-form-item label="测试配置" v-if="currentType === 'security'">
<el-select v-model="runConfig.configId" placeholder="选择安全测试配置" style="width: 100%">
<el-option
v-for="cfg in securityConfigs"
:key="cfg.id"
:label="cfg.name"
:value="cfg.id"
/>
</el-select>
</el-form-item>
<el-form-item label="执行环境" v-if="currentType !== 'security'">
<el-select v-model="runConfig.environment" style="width: 100%">
<el-option label="默认环境" value="default" />
<el-option label="测试环境" value="test" />
<el-option label="开发环境" value="dev" />
</el-select>
</el-form-item>
<el-form-item label="浏览器模式">
<el-switch v-model="runConfig.headless" active-text="无头" inactive-text="有头" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="runDialogVisible = false">取消</el-button>
......@@ -325,9 +337,20 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { VideoPlay, Refresh, Delete } from '@element-plus/icons-vue'
import { executionApi } from '@/api/executions'
import { caseApi } from '@/api/cases'
import {
getSecurityConfigs,
createSecurityExecution,
runSecurityExecution,
} from '@/api/security'
import { WebSocketClient } from '@/utils/websocket'
import StepDetailPanel from '@/components/StepDetailPanel.vue'
// 安全测试 API 简易封装(与 executionApi 接口风格保持一致)
const securityApi = {
create: createSecurityExecution,
run: runSecurityExecution,
}
const route = useRoute()
const router = useRouter()
......@@ -362,8 +385,12 @@ const runConfig = ref({
name: '',
environment: 'default',
headless: true,
configId: '', // 安全测试配置ID
})
// 安全测试配置列表
const securityConfigs = ref<any[]>([])
// 步骤详情弹窗相关
const stepDetailVisible = ref(false)
const currentStepResult = ref<any[]>([])
......@@ -632,25 +659,71 @@ const loadAllCases = async () => {
}
}
// 加载安全测试配置列表
const loadSecurityConfigs = async () => {
if (currentType.value !== 'security') return
try {
const response = await getSecurityConfigs(0, 100)
const data = (response as any).data || response
securityConfigs.value = data.items || []
// 默认选中第一个配置
if (securityConfigs.value.length > 0 && !runConfig.value.configId) {
runConfig.value.configId = securityConfigs.value[0].id
}
} catch (error: any) {
console.error('加载安全测试配置失败:', error)
}
}
const createAndRunExecution = async () => {
executing.value = true
try {
// 创建执行任务
const execution = await executionApi.create({
let execution: any
if (currentType.value === 'security') {
// 安全测试走 securityApi
if (!runConfig.value.configId) {
ElMessage.warning('请先选择安全测试配置')
executing.value = false
return
}
execution = await securityApi.create({
configId: runConfig.value.configId,
caseIds: selectedCases.value,
name: runConfig.value.name || `安全测试-${new Date().toLocaleString()}`,
})
activeExecution.value = execution
runDialogVisible.value = false
selectedCases.value = []
runConfig.value.name = ''
runConfig.value.configId = ''
// 触发安全测试执行
securityApi.run(execution.id).then(() => {
ElMessage.success('安全测试执行完成')
loadExecutions()
}).catch((error: any) => {
console.warn('安全测试执行异常:', error.message)
loadExecutions()
})
ElMessage.info('安全测试已启动,请等待...')
startPolling()
await loadExecutions()
} else {
// UI用例走 executionApi
execution = await executionApi.create({
case_ids: selectedCases.value,
name: runConfig.value.name,
environment: runConfig.value.environment,
config: { headless: runConfig.value.headless },
})
// 设置当前执行并连接 WebSocket
activeExecution.value = execution
connectWebSocket(execution.id)
runDialogVisible.value = false
selectedCases.value = []
runConfig.value.name = ''
// 异步触发执行(不等待完成,通过轮询/WebSocket 更新状态)
executionApi.run(execution.id, { headless: runConfig.value.headless }).then(() => {
ElMessage.success('执行完成')
loadExecutions()
......@@ -662,6 +735,7 @@ const createAndRunExecution = async () => {
ElMessage.info('执行任务已启动,请等待...')
startPolling()
await loadExecutions()
}
} catch (error: any) {
ElMessage.error('启动执行失败: ' + error.message)
} finally {
......@@ -861,6 +935,7 @@ const stopPolling = () => {
onMounted(async () => {
await loadExecutions()
loadAllCases()
loadSecurityConfigs()
// 检测从用例管理页跳转过来的自动执行请求
const runId = route.query.run as string
......@@ -876,6 +951,9 @@ onMounted(async () => {
watch(currentType, () => {
currentPage.value = 1
loadExecutions()
if (currentType.value === 'security') {
loadSecurityConfigs()
}
})
onUnmounted(() => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论