Add files via upload

This commit is contained in:
公明
2025-12-24 23:14:37 +08:00
committed by GitHub
parent e860c84975
commit ef169ba307
8 changed files with 545 additions and 229 deletions
+185 -199
View File
@@ -330,161 +330,162 @@ func (b *Builder) formatReActInputFromJSON(reactInputJSON string) string {
// buildSimplePrompt 构建简化的prompt
func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string {
return fmt.Sprintf(`你是一个专业的安全测试分析师。请根据以下对话和工具执行记录,生成清晰、有教育意义的攻击链图
return fmt.Sprintf(`你是专业的安全测试分析师和攻击链构建专家。你的任务是根据对话记录和工具执行结果,构建一个逻辑清晰、有教育意义的攻击链图,完整展现渗透测试的思维过程和执行路径
## 核心原则
## 核心目标
**目标:让不懂渗透测试的同学可以通过这个攻击链路学习到知识,而不是无数个节点看花眼。**
**⚠️ 特别重要:失败路径和错误经验同样具有重要价值!**
构建一个能够讲述完整攻击故事的攻击链(通常8-15个节点,必要时可适当增加),让学习者能够:
1. 理解渗透测试的完整流程和思维逻辑(从目标识别到漏洞发现的每一步)
2. 学习如何从失败中获取线索并调整策略
3. 掌握工具使用的实际效果和局限性
4. 理解漏洞发现和利用的因果关系
**失败路径的价值:**
- **指引作用**:失败的尝试往往揭示了系统的防御机制、配置信息或攻击面边界
- **学习价值**:展示"为什么这条路走不通"、"遇到了什么障碍"、"如何绕过或解决"
- **完整还原**:真实的渗透测试过程包含大量失败尝试,只展示成功路径会误导学习者
- **关键线索**:即使工具执行失败,错误信息、超时、拒绝连接等都可能包含重要信息(如WAF类型、防护策略、端口状态等)
**关键原则**:完整性优先。必须包含所有有意义的工具执行和关键步骤,不要为了控制节点数量而遗漏重要信息。
**必须保留的失败路径类型:**
1. **工具执行失败但提供了线索**:如"工具未安装"、"权限不足"、"连接被拒绝"、"超时"等,这些信息有助于理解环境限制
2. **漏洞验证失败但指明了方向**:如"SQL注入尝试失败,但暴露了数据库类型"、"XSS尝试被WAF拦截,但暴露了WAF规则"
3. **扫描失败但揭示了防护**:如"端口扫描被防火墙拦截"、"目录枚举被限制"、"暴力破解被锁定"
4. **配置错误或环境问题**:如"工具配置错误"、"目标不可达"、"证书验证失败"等,这些可能揭示系统配置问题
5. **AI分析中的失败尝试**:如果AI在对话中明确提到"尝试了X但失败了,因为Y",这也应该被记录
## 构建流程(按此顺序思考)
**关键要求:**
1. **节点标签必须简洁明了**:每个节点标签控制在15-25个汉字以内,使用简洁的动宾结构
- action节点要描述"做了什么"和"发现了什么"(如"扫描端口发现22/80/443"、"验证SQL注入成功"、"WAF拦截暴露厂商"、"SQLMap扫描失败(工具未安装)"
- 失败路径的标签应明确标注失败原因(如"尝试SQL注入(被WAF拦截)"、"端口扫描(连接超时)")
- 避免冗长描述,关键信息放在metadata中详细说明
2. **严格控制节点数量**:优先保留关键步骤,避免生成过多细碎节点。理想情况下,单个目标的攻击链应控制在8-15个节点以内
- 如果节点太多(>20个),优先保留最重要的节点(包括重要的失败路径),合并或删除次要节点
- 合并相似的action节点(如同一工具的连续调用,如果结果相似)
- 对于同一类型的多个发现,考虑合并为一个节点(如"发现多个开放端口"而不是为每个端口创建节点)
- **但不要因为节点数量限制而删除有价值的失败路径**
3. **确保DAG结构**:生成的图必须是有向无环图(DAG),不允许出现循环。边的方向必须符合时间顺序和逻辑关系(从早期步骤指向后期步骤)
- 生成后必须检查:确保图中不存在循环(即不存在路径A→B→...→A)
- 如果发现循环,必须断开形成循环的边,保留最重要的连接
- **失败路径也应该正确连接到后续的成功路径**(如"尝试A失败" → "改用B方法成功"
4. **层次清晰**:攻击链应该呈现清晰的层次结构:目标 → 信息收集(包括失败的尝试) → 漏洞发现 → 漏洞利用 → 后续行动
### 第一步:理解上下文
仔细分析ReAct输入中的工具调用序列和大模型输出,识别:
- 测试目标(IP、域名、URL等
- 实际执行的工具和参数
- 工具返回的关键信息(成功结果、错误信息、超时等)
- AI的分析和决策过程
## ⚠️ 重要原则 - 严禁杜撰
### 第二步:提取关键节点
从工具执行记录中提取有意义的节点,**确保不遗漏任何关键步骤**:
- **target节点**:每个独立的测试目标创建一个target节点
- **action节点**:每个有意义的工具执行创建一个action节点(包括提供线索的失败、成功的信息收集、漏洞验证等)
- **vulnerability节点**:每个真实确认的漏洞创建一个vulnerability节点
- **完整性检查**:对照ReAct输入中的工具调用序列,确保每个有意义的工具执行都被包含在攻击链中
**严格禁止编造或推测任何内容!** 你必须:
1. **只使用实际发生的信息**:仅基于ReAct输入中实际执行的工具调用和实际返回的结果
2. **不要推测**:如果没有实际执行工具或发现漏洞,不要编造
3. **不要假设**:不能仅根据URL、目标名称等推断漏洞类型
4. **基于事实**:每个节点和边都必须有实际依据,来自工具执行结果或模型的实际输出
### 第三步:构建逻辑关系
按照时间顺序和因果关系连接节点:
- 识别哪些action是基于前面action的结果而执行的
- 识别哪些vulnerability是由哪些action发现的
- 识别失败节点如何为后续成功提供线索
如果ReAct输入中没有实际的工具执行记录,或者模型输出中明确表示任务未完成/被取消,必须返回空的攻击链(空的nodes和edges数组)。
### 第四步:优化和精简
- **完整性检查**:确保所有有意义的工具执行都被包含,不要遗漏关键步骤
- **合并规则**:只合并真正相似或重复的action节点(如多次相同工具的相似调用)
- **删除规则**:只删除完全无价值的失败节点(完全无输出、纯系统错误、重复的相同失败)
- **重要提醒**:宁可保留更多节点,也不要遗漏关键步骤。攻击链必须完整展现渗透测试过程
- 确保攻击链逻辑连贯,能够讲述完整故事
## 节点类型详解
### target(目标节点)
- **用途**:标识测试目标
- **创建规则**:每个独立目标(不同IP/域名)创建一个target节点
- **多目标处理**:不同目标的节点不相互连接,各自形成独立的子图
- **metadata.target**:精确记录目标标识(IP地址、域名、URL等)
### action(行动节点)
- **用途**:记录工具执行和AI分析结果
- **标签规则**
* 15-25个汉字,动宾结构
* 成功节点:描述执行结果(如"扫描端口发现80/443/8080"、"目录扫描发现/admin路径"
* 失败节点:描述失败原因(如"尝试SQL注入(被WAF拦截)"、"端口扫描超时(目标不可达)")
- **ai_analysis要求**
* 成功节点:总结工具执行的关键发现,说明这些发现的意义
* 失败节点:必须说明失败原因、获得的线索、这些线索如何指引后续行动
* 不超过150字,要具体、有信息量
- **findings要求**
* 提取工具返回结果中的关键信息点
* 每个finding应该是独立的、有价值的信息片段
* 成功节点:列出关键发现(如["80端口开放", "443端口开放", "HTTP服务为Apache 2.4"]
* 失败节点:列出失败线索(如["WAF拦截", "返回403", "检测到Cloudflare"]
- **status标记**
* 成功节点:不设置或设为"success"
* 提供线索的失败节点:必须设为"failed_insight"
- **risk_score**:始终为0action节点不评估风险)
### vulnerability(漏洞节点)
- **用途**:记录真实确认的安全漏洞
- **创建规则**
* 必须是真实确认的漏洞,不是所有发现都是漏洞
* 需要明确的漏洞证据(如SQL注入返回数据库错误、XSS成功执行等)
- **risk_score规则**
* critical90-100):可导致系统完全沦陷(RCE、SQL注入导致数据泄露等)
* high(80-89):可导致敏感信息泄露或权限提升
* medium(60-79):存在安全风险但影响有限
* low40-59):轻微安全问题
- **metadata要求**
* vulnerability_type:漏洞类型(SQL注入、XSS、RCE等)
* description:详细描述漏洞位置、原理、影响
* severitycritical/high/medium/low
* location:精确的漏洞位置(URL、参数、文件路径等)
## 节点过滤和合并规则
### 必须保留的失败节点
以下失败情况必须创建节点,因为它们提供了有价值的线索:
- 工具返回明确的错误信息(权限错误、连接拒绝、认证失败等)
- 超时或连接失败(可能表明防火墙、网络隔离等)
- WAF/防火墙拦截(返回403、406等,表明存在防护机制)
- 工具未安装或配置错误(但执行了调用)
- 目标不可达(DNS解析失败、网络不通等)
### 应该删除的失败节点
以下情况不应创建节点:
- 完全无输出的工具调用
- 纯系统错误(与目标无关,如本地环境问题)
- 重复的相同失败(多次相同错误只保留第一次)
### 节点合并规则
以下情况应合并节点:
- 同一工具的多次相似调用(如多次nmap扫描不同端口范围,合并为一个"端口扫描"节点)
- 同一目标的多个相似探测(如多个目录扫描工具,合并为一个"目录扫描"节点)
### 节点数量控制
- **完整性优先**:必须包含所有有意义的工具执行和关键步骤,不要为了控制数量而删除重要节点
- **建议范围**:单目标通常8-15个节点,但如果实际执行步骤较多,可以适当增加(最多20个节点)
- **优先保留**:关键成功步骤、提供线索的失败、发现的漏洞、重要的信息收集步骤
- **可以合并**:同一工具的多次相似调用(如多次nmap扫描不同端口范围,合并为一个"端口扫描"节点)
- **可以删除**:完全无输出的工具调用、纯系统错误、重复的相同失败(多次相同错误只保留第一次)
- **重要原则**:宁可节点稍多,也不要遗漏关键步骤。攻击链必须能够完整展现渗透测试的完整过程
## 边的类型和权重
### 边的类型
- **leads_to**:表示"导致"或"引导到",用于action→action、target→action
* 例如:端口扫描 → 目录扫描(因为发现了80端口,所以进行目录扫描)
- **discovers**:表示"发现",用于action→vulnerability
* 例如:SQL注入测试 → SQL注入漏洞
- **enables**:表示"使能"或"促成",用于vulnerability→vulnerability、action→action(当后续行动依赖前面结果时)
* 例如:信息泄露漏洞 → 权限提升漏洞(通过信息泄露获得的信息促成了权限提升)
### 边的权重
- **权重1-2**:弱关联(如初步探测到进一步探测)
- **权重3-4**:中等关联(如发现端口到服务识别)
- **权重5-7**:强关联(如发现漏洞、关键信息泄露)
- **权重8-10**:极强关联(如漏洞利用成功、权限提升)
### DAG结构要求
- 所有边的source节点id必须小于target节点id(确保无环)
- 节点id从"node_1"开始递增
- 确保无孤立节点(每个节点至少有一条边连接)
## 攻击链逻辑连贯性要求
构建的攻击链应该能够回答以下问题:
1. **起点**:测试从哪里开始?(target节点)
2. **探索过程**:如何逐步收集信息?(action节点序列)
3. **失败与调整**:遇到障碍时如何调整策略?(failed_insight节点)
4. **关键发现**:发现了哪些重要信息?(action的findings
5. **漏洞确认**:如何确认漏洞存在?(action→vulnerability
6. **攻击路径**:完整的攻击路径是什么?(从target到vulnerability的路径)
## 最后一轮ReAct输入
## 最后一轮ReAct的输入(历史对话上下文)
%s
## 大模型最后的输出
## 大模型输出
%s
## 任务要求
### 1. 节点类型(简化,只保留3种)
**target(目标)**:从用户输入中提取测试目标(IP、域名、URL等)
- **重要:如果对话中测试了多个不同的目标(如先测试A网页,后测试B网页),必须:**
- 为每个不同的目标创建独立的target节点
- 每个target节点只关联属于它的action和vulnerability节点
- 不同目标的节点之间**不应该**建立任何关联关系
- 这样会形成多个独立的攻击链分支,每个分支对应一个测试目标
**action(行动)****工具执行 + AI分析结果 = 一个action节点**
- 将每个工具执行和AI对该工具结果的分析合并为一个action节点
- **节点标签必须简洁**:控制在15-25个汉字,使用动宾结构,描述"做了什么"和"发现了什么"
- 成功示例:"扫描端口发现22/80/443"、"验证SQL注入成功"、"WAF拦截暴露厂商"
- **失败示例(必须保留)**:"尝试SQL注入(被WAF拦截)"、"端口扫描(连接超时)"、"SQLMap扫描(工具未安装)"、"目录枚举(权限不足)"
- 避免冗长描述,关键信息放在metadata中详细说明
- **⚠️ 失败路径处理规则**
- **必须创建节点**:如果工具执行失败但提供了任何线索、错误信息或指引,必须创建action节点
- **标记失败状态**:在metadata.status中标记为"failed_insight",并在findings中详细说明失败原因和获得的线索
- **说明线索价值**:在ai_analysis中明确说明"为什么这个失败很重要"、"提供了什么信息"、"如何指引了后续行动"
- **连接后续节点**:失败路径应该连接到后续的成功路径,展示"失败 → 调整策略 → 成功"的完整过程
- **重要:action节点必须关联到正确的target节点(通过工具执行参数判断目标)**
- **risk_score****action节点没有风险,risk_score必须设置为0**(只有vulnerability节点才有风险等级)
**vulnerability(漏洞)**:从工具执行结果和AI分析中提取的**真实漏洞**(不是所有发现都是漏洞)
- 若验证失败但能明确表明某个漏洞利用方向不可行,可作为行动节点的线索描述,而不是漏洞节点
- **risk_score**:反映实际发现的漏洞的风险等级(高危80-100,中危60-80,低危40-60
### 2. 简化结构
- 只创建target、action、vulnerability三种节点
- 不要创建discovery、decision等节点
- 让攻击链清晰、有教育意义
### 3. 过滤规则(重要!)
**必须忽略的失败执行(可以删除):**
- 完全没有输出、没有任何错误信息的失败
- 纯粹的系统错误(如"内存不足"、"磁盘满"等),且与测试目标无关
- 重复的、完全相同的失败尝试(只保留第一次)
**必须保留的失败执行(必须创建节点):**
- **工具执行失败但提供了线索**:如错误信息、超时、拒绝连接、权限错误等
- **漏洞验证失败但指明了方向**:如"SQL注入尝试失败,但暴露了数据库类型"、"XSS被WAF拦截,但暴露了WAF规则"
- **扫描失败但揭示了防护**:如"端口扫描被防火墙拦截"、"目录枚举被限制"、"暴力破解被锁定"
- **配置或环境问题**:如"工具未安装"、"目标不可达"、"证书验证失败"等,这些可能揭示系统配置问题
- **AI明确分析的失败尝试**:如果AI在对话中明确提到"尝试了X但失败了,因为Y",必须记录
**判断标准:**
- 如果失败提供了**任何**有助于理解系统、调整策略或学习的信息,就必须保留
- 如果失败揭示了**任何**关于目标系统、防护机制或环境配置的信息,就必须保留
- 如果失败指引了**后续的成功尝试**,就必须保留并建立连接关系
- **宁可多保留一些失败路径,也不要遗漏有价值的线索**
**只保留对学习或溯源有帮助的节点**:包括成功路径和重要的失败路径
### 4. 关联关系(确保DAG结构)
- target → action:目标指向属于它的所有行动(通过工具执行参数判断目标)
- action → action:按时间顺序连接,但只连接有逻辑关系的
- **重要:只连接属于同一目标的action节点,不同目标的action节点之间不应该连接**
- **必须确保无环**:只能从早期步骤指向后期步骤,不能形成循环
- 优先连接直接相关的步骤,避免过度连接
- action → vulnerability:行动发现的漏洞
- vulnerability → vulnerability:漏洞间的因果关系
- **重要:只连接属于同一目标的漏洞,不同目标的漏洞之间不应该连接**
- **必须确保无环**:漏洞间的因果关系也必须是单向的
### 5. 节点属性
每个节点需要:id, type, label, risk_score, metadata
**重要:risk_score规则**
- **target节点**:可以设置适当的risk_score(如40),表示目标本身的风险
- **action节点**:**必须设置为0**,因为行动本身没有风险,只有漏洞才有风险
- **vulnerability节点**:必须根据漏洞严重程度设置risk_score(高危80-100,中危60-80,低危40-60
**action节点metadata必须包含:**
- tool_name: 工具名称(必须与ReAct中的tool_calls一致)
- tool_intent: 工具调用意图(如"端口扫描"、"漏洞扫描"、"目录枚举"等)
- ai_analysis: AI对工具结果的分析总结(不超过150字)
- **成功节点**:总结关键发现和结果
- **失败节点**:**必须详细说明**:①失败的具体原因 ②获得了什么线索或信息 ③这些线索如何指引了后续行动 ④为什么这个失败很重要
- findings: 关键发现列表(数组)
- 成功节点:如["发现80端口开放", "检测到WAF"]
- **失败节点**:必须包含失败原因和获得的线索,如["WAF拦截SQL注入尝试", "返回403错误", "目标部署了Web应用防火墙"]
- status:
- 成功节点:可以不设置或设置为"success"
- **失败节点:必须标记为"failed_insight"**,表示失败但提供了有价值的线索
**target节点metadata必须包含:**
- target: 测试目标(URL、IP、域名等)
**vulnerability节点metadata必须包含:**
- vulnerability_type: 漏洞类型
- description: 实际发现的漏洞描述(必须与模型输出中明确提及的漏洞一致)
- severity: 严重程度("critical"|"high"|"medium"|"low"
- location: 漏洞位置
## 输出格式
请以JSON格式返回攻击链,严格按照以下格式
严格按照以下JSON格式输出,不要添加任何其他文字
{
"nodes": [
@@ -500,64 +501,50 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string {
{
"id": "node_2",
"type": "action",
"label": "扫描端口发现80/443",
"label": "扫描端口发现80/443/8080",
"risk_score": 0,
"metadata": {
"tool_name": "nmap",
"tool_intent": "端口扫描",
"ai_analysis": "使用nmap扫描发现80443端口开放,目标运行标准Web服务",
"findings": ["80端口开放", "443端口开放"]
"ai_analysis": "使用nmap对目标进行端口扫描发现80443、8080端口开放。80端口运行HTTP服务,443端口运行HTTPS服务,8080端口可能为管理后台。这些开放端口为后续Web应用测试提供了入口。",
"findings": ["80端口开放", "443端口开放", "8080端口开放", "HTTP服务为Apache 2.4"]
}
},
{
"id": "node_3",
"type": "action",
"label": "SQLMap扫描(工具未安装)",
"risk_score": 0,
"metadata": {
"tool_name": "sqlmap",
"tool_intent": "SQL注入测试",
"ai_analysis": "sqlmap工具未安装,无法进行SQL注入测试。这个失败揭示了测试环境的工具配置情况,需要先安装工具才能继续测试。",
"findings": ["工具未安装,需要先安装sqlmap工具", "环境配置限制:缺少SQL注入测试工具"],
"status": "failed_insight"
}
},
{
"id": "node_5",
"type": "action",
"label": "尝试SQL注入(被WAF拦截)",
"risk_score": 0,
"metadata": {
"tool_name": "manual_test",
"tool_intent": "SQL注入验证",
"ai_analysis": "尝试SQL注入攻击时被WAF拦截,返回403错误。这个失败提供了重要线索:目标部署了WAF防护,且WAF规则较为严格。后续可以尝试绕过WAF或寻找其他攻击面。",
"findings": ["WAF拦截SQL注入尝试", "返回403错误", "目标部署了Web应用防火墙"],
"status": "failed_insight"
}
},
{
"id": "node_6",
"type": "action",
"label": "端口扫描(连接超时)",
"risk_score": 0,
"metadata": {
"tool_name": "nmap",
"tool_intent": "端口扫描",
"ai_analysis": "对目标进行端口扫描时,多个端口连接超时。这个失败表明目标可能部署了防火墙或IDS,对扫描行为进行了检测和拦截。超时信息有助于了解目标的防护策略。",
"findings": ["多个端口连接超时", "可能部署了防火墙或IDS", "目标对扫描行为有防护"],
"tool_name": "sqlmap",
"tool_intent": "SQL注入检测",
"ai_analysis": "对/login.php进行SQL注入测试时被WAF拦截,返回403错误。错误信息显示检测到Cloudflare防护。这表明目标部署了WAF,需要调整测试策略,可尝试绕过技术或寻找其他未受保护的攻击面。",
"findings": ["WAF拦截", "返回403", "检测到Cloudflare", "目标部署WAF"],
"status": "failed_insight"
}
},
{
"id": "node_4",
"type": "action",
"label": "目录扫描发现/admin后台",
"risk_score": 0,
"metadata": {
"tool_name": "dirsearch",
"tool_intent": "目录扫描",
"ai_analysis": "使用dirsearch对目标进行目录扫描,发现/admin目录存在且可访问。该目录可能为管理后台,是重要的测试目标。结合之前发现的WAF防护,可以尝试对/admin目录进行绕过测试。",
"findings": ["/admin目录存在", "返回200状态码", "疑似管理后台"]
}
},
{
"id": "node_5",
"type": "vulnerability",
"label": "SQL注入漏洞",
"risk_score": 85,
"metadata": {
"vulnerability_type": "SQL注入",
"description": "在/admin/login.php发现SQL注入漏洞",
"description": "在/admin/login.php的username参数发现SQL注入漏洞,可通过注入payload绕过登录验证,直接获取管理员权限。漏洞返回数据库错误信息,确认存在注入点。",
"severity": "high",
"location": "/admin/login.php"
"location": "/admin/login.php?username="
}
}
],
@@ -570,37 +557,36 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string {
},
{
"source": "node_2",
"target": "node_3",
"type": "leads_to",
"weight": 3
},
{
"source": "node_3",
"target": "node_4",
"type": "leads_to",
"weight": 4
},
{
"source": "node_4",
"target": "node_5",
"type": "discovers",
"weight": 5
"weight": 7
}
]
}
}
**关键要求:**
- 节点id必须从"node_1"开始,按顺序递增(node_1, node_2, node_3, ...
- 所有边的source节点id必须小于target节点id(确保DAG无环)
- target节点必须是node_1(如果是多目标,第一个target是node_1,第二个target是node_2,以此类推)
- 节点之间必须形成清晰的路径,不能有孤立节点
- 如果有vulnerability节点,必须展示从target到vulnerability的完整路径
- 边的类型只能是:leads_to、discovers、enables
## 重要提醒
**再次强调:如果没有实际数据,返回空的nodes和edges数组。严禁杜撰!**
1. **严禁杜撰**:只使用ReAct输入中实际执行的工具和实际返回的结果。如无实际数据,返回空的nodes和edges数组。
2. **完整性优先**:必须包含所有有意义的工具执行和关键步骤,不要为了控制节点数量而删除重要节点。攻击链必须能够完整展现从目标识别到漏洞发现的完整过程。
3. **逻辑连贯**:确保攻击链能够讲述一个完整、连贯的渗透测试故事,包括所有关键步骤和决策点。
4. **教育价值**:优先保留有教育意义的节点,帮助学习者理解渗透测试思维和完整流程。
5. **准确性**:所有节点信息必须基于实际数据,不要推测或假设。
6. **完整性检查**:确保每个节点都有必要的metadata字段,每条边都有正确的source和target,没有孤立节点。
7. **不要过度精简**:如果实际执行步骤较多,可以适当增加节点数量(最多20个),确保不遗漏关键步骤。
## ⚠️ 关于失败路径的最后提醒
**请特别注意:在生成攻击链时,不要只关注成功的路径,也要仔细检查ReAct输入中的所有失败尝试。**
**检查清单(在生成节点前,请逐一检查):**
1. ✅ 是否所有工具执行失败都被检查过了?
2. ✅ 每个失败是否提供了线索、错误信息或指引?
3. ✅ 失败路径是否连接到了后续的成功路径?
4. ✅ 失败节点的metadata是否详细说明了线索价值?
5. ✅ 是否因为节点数量限制而误删了重要的失败路径?
**记住:一个完整的攻击链应该展示真实的渗透测试过程,包括成功和重要的失败尝试。失败路径不是噪音,而是宝贵的经验和学习材料!**
只返回JSON,不要包含其他解释文字。`, reactInput, modelOutput)
现在开始分析并构建攻击链:`, reactInput, modelOutput)
}
// saveChain 保存攻击链到数据库
+1
View File
@@ -105,6 +105,7 @@ type ToolConfig struct {
Enabled bool `yaml:"enabled"`
Parameters []ParameterConfig `yaml:"parameters,omitempty"` // 参数定义(可选)
ArgMapping string `yaml:"arg_mapping,omitempty"` // 参数映射方式: "auto", "manual", "template"(可选)
AllowedExitCodes []int `yaml:"allowed_exit_codes,omitempty"` // 允许的退出码列表(某些工具在成功时也返回非零退出码)
}
// ParameterConfig 参数配置
+24 -5
View File
@@ -129,11 +129,30 @@ func (db *DB) GetConversation(id string) (*Conversation, error) {
}
// ListConversations 列出所有对话
func (db *DB) ListConversations(limit, offset int) ([]*Conversation, error) {
rows, err := db.Query(
"SELECT id, title, created_at, updated_at FROM conversations ORDER BY updated_at DESC LIMIT ? OFFSET ?",
limit, offset,
)
func (db *DB) ListConversations(limit, offset int, search string) ([]*Conversation, error) {
var rows *sql.Rows
var err error
if search != "" {
// 使用LIKE进行模糊搜索,搜索标题和消息内容
searchPattern := "%" + search + "%"
// 使用DISTINCT避免重复,因为一个对话可能有多条消息匹配
rows, err = db.Query(
`SELECT DISTINCT c.id, c.title, c.created_at, c.updated_at
FROM conversations c
LEFT JOIN messages m ON c.id = m.conversation_id
WHERE c.title LIKE ? OR m.content LIKE ?
ORDER BY c.updated_at DESC
LIMIT ? OFFSET ?`,
searchPattern, searchPattern, limit, offset,
)
} else {
rows, err = db.Query(
"SELECT id, title, created_at, updated_at FROM conversations ORDER BY updated_at DESC LIMIT ? OFFSET ?",
limit, offset,
)
}
if err != nil {
return nil, fmt.Errorf("查询对话列表失败: %w", err)
}
+2 -1
View File
@@ -55,6 +55,7 @@ func (h *ConversationHandler) CreateConversation(c *gin.Context) {
func (h *ConversationHandler) ListConversations(c *gin.Context) {
limitStr := c.DefaultQuery("limit", "50")
offsetStr := c.DefaultQuery("offset", "0")
search := c.Query("search") // 获取搜索参数
limit, _ := strconv.Atoi(limitStr)
offset, _ := strconv.Atoi(offsetStr)
@@ -63,7 +64,7 @@ func (h *ConversationHandler) ListConversations(c *gin.Context) {
limit = 50
}
conversations, err := h.db.ListConversations(limit, offset)
conversations, err := h.db.ListConversations(limit, offset, search)
if err != nil {
h.logger.Error("获取对话列表失败", zap.Error(err))
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
+46
View File
@@ -145,9 +145,33 @@ func (e *Executor) ExecuteTool(ctx context.Context, toolName string, args map[st
output, err := cmd.CombinedOutput()
if err != nil {
// 检查退出码是否在允许列表中
exitCode := getExitCode(err)
if exitCode != nil && toolConfig.AllowedExitCodes != nil {
for _, allowedCode := range toolConfig.AllowedExitCodes {
if *exitCode == allowedCode {
e.logger.Info("工具执行完成(退出码在允许列表中)",
zap.String("tool", toolName),
zap.Int("exitCode", *exitCode),
zap.String("output", string(output)),
)
return &mcp.ToolResult{
Content: []mcp.Content{
{
Type: "text",
Text: string(output),
},
},
IsError: false,
}, nil
}
}
}
e.logger.Error("工具执行失败",
zap.String("tool", toolName),
zap.Error(err),
zap.Int("exitCode", getExitCodeValue(err)),
zap.String("output", string(output)),
)
return &mcp.ToolResult{
@@ -1217,3 +1241,25 @@ func (e *Executor) convertToOpenAIType(configType string) string {
return configType
}
}
// getExitCode 从错误中提取退出码,如果不是ExitError则返回nil
func getExitCode(err error) *int {
if err == nil {
return nil
}
if exitError, ok := err.(*exec.ExitError); ok {
if exitError.ProcessState != nil {
exitCode := exitError.ExitCode()
return &exitCode
}
}
return nil
}
// getExitCodeValue 从错误中提取退出码值,如果不是ExitError则返回-1
func getExitCodeValue(err error) int {
if code := getExitCode(err); code != nil {
return *code
}
return -1
}
+55
View File
@@ -748,6 +748,61 @@ header {
padding: 0 8px;
}
.conversation-search-box {
position: relative;
margin-bottom: 12px;
}
.conversation-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 ease;
}
.conversation-search-box input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1);
}
.conversation-search-box input::placeholder {
color: var(--text-muted);
}
.conversation-search-clear {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
padding: 0;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
transition: color 0.2s ease;
border-radius: 4px;
}
.conversation-search-clear:hover {
color: var(--text-primary);
background: var(--bg-tertiary);
}
.conversation-search-clear svg {
width: 14px;
height: 14px;
}
.conversations-list {
display: flex;
flex-direction: column;
+220 -24
View File
@@ -1185,9 +1185,13 @@ function startNewConversation() {
}
// 加载对话列表(按时间分组)
async function loadConversations() {
async function loadConversations(searchQuery = '') {
try {
const response = await apiFetch('/api/conversations?limit=50');
let url = '/api/conversations?limit=50';
if (searchQuery && searchQuery.trim()) {
url += '&search=' + encodeURIComponent(searchQuery.trim());
}
const response = await apiFetch(url);
const conversations = await response.json();
const listContainer = document.getElementById('conversations-list');
@@ -1315,6 +1319,45 @@ function createConversationListItem(conversation) {
return item;
}
// 处理历史记录搜索
let conversationSearchTimer = null;
function handleConversationSearch(query) {
// 防抖处理,避免频繁请求
if (conversationSearchTimer) {
clearTimeout(conversationSearchTimer);
}
const searchInput = document.getElementById('conversation-search-input');
const clearBtn = document.getElementById('conversation-search-clear');
if (clearBtn) {
if (query && query.trim()) {
clearBtn.style.display = 'block';
} else {
clearBtn.style.display = 'none';
}
}
conversationSearchTimer = setTimeout(() => {
loadConversations(query);
}, 300); // 300ms防抖延迟
}
// 清除搜索
function clearConversationSearch() {
const searchInput = document.getElementById('conversation-search-input');
const clearBtn = document.getElementById('conversation-search-clear');
if (searchInput) {
searchInput.value = '';
}
if (clearBtn) {
clearBtn.style.display = 'none';
}
loadConversations('');
}
function formatConversationTimestamp(dateObj, todayStart, yesterdayStart) {
if (!(dateObj instanceof Date) || isNaN(dateObj.getTime())) {
return '';
@@ -2070,8 +2113,10 @@ function renderAttackChain(chainData) {
'target-arrow-size': 8,
// 使用bezier曲线,更美观
'curve-style': 'bezier',
'control-point-step-size': 50,
'control-point-distance': 40,
'control-point-step-size': 60, // 增加步长,让控制点分布更均匀
// 大幅增加控制点距离,避免多条边指向同一节点时箭头重叠
// 使用更大的值确保箭头之间有足够的间距
'control-point-distance': isComplexGraph ? 180 : 150,
'opacity': 0.7,
// 根据边类型设置线条样式:targets使用虚线,其他使用实线
'line-style': function(ele) {
@@ -2153,43 +2198,47 @@ function renderAttackChain(chainData) {
const estimatedDepth = Math.ceil(Math.log2(Math.max(nodeCount, 2))) + 1;
// 动态计算节点水平间距:基于容器宽度和节点数量
// 目标:使用容器宽度的85-90%,让图充分展开
// 目标:使用容器宽度的95%,让图充分展开
const maxLevelWidth = Math.max(1, Math.ceil(nodeCount / estimatedDepth));
const targetGraphWidth = containerWidth * 0.88; // 使用88%的容器宽度
const minNodeSep = avgNodeWidth * 0.8; // 最小间距为节点宽度的80%(从60%增加到80%,增加水平间距
const targetGraphWidth = containerWidth * 0.95; // 使用95%的容器宽度,让图更宽
// 大幅增加最小间距,确保节点不重叠(考虑节点宽度和标签
const minNodeSep = avgNodeWidth * 1.5; // 最小间距为节点宽度的1.5倍,确保节点之间有足够空间
// 优化间距计算:确保即使节点很多时也有足够的间距
const availableWidth = targetGraphWidth - avgNodeWidth * maxLevelWidth;
const calculatedNodeSep = Math.max(
minNodeSep,
Math.min(
(targetGraphWidth - avgNodeWidth * maxLevelWidth) / Math.max(1, maxLevelWidth - 1),
avgNodeWidth * 2.0 // 最大间距不超过节点宽度的2.0倍(从1.5增加到2.0
availableWidth / Math.max(1, maxLevelWidth - 1),
avgNodeWidth * 3.0 // 最大间距不超过节点宽度的3.0倍,让图更宽
)
);
// 动态计算层级间距:基于容器高度和层级数
// 增加最小间距,避免节点重合
// 大幅增加最小间距,避免节点重合
const targetGraphHeight = containerHeight * 0.85;
const calculatedRankSep = Math.max(
avgNodeHeight * 1.8, // 最小为节点高度的1.8倍(从1.2增加到1.8,增加层级间距
avgNodeHeight * 2.5, // 最小为节点高度的2.5倍,确保垂直方向有足够间距
Math.min(
targetGraphHeight / Math.max(estimatedDepth - 1, 1),
avgNodeHeight * 3.5 // 最大不超过节点高度的3.5倍(从2.5增加到3.5
avgNodeHeight * 4.0 // 最大不超过节点高度的4.0倍
)
);
// 边间距:基于节点间距的合理比例
const calculatedEdgeSep = Math.max(40, calculatedNodeSep * 0.3); // 增加边间距(从30增加到40,从0.25增加到0.3
// 增加边间距,确保边之间有足够的空间,避免视觉混乱
const calculatedEdgeSep = Math.max(50, calculatedNodeSep * 0.4);
// 根据图的复杂度调整布局参数,优化可读性和空间利用率
layoutOptions = {
name: 'dagre',
rankDir: 'TB', // 从上到下
spacingFactor: 1.0, // 使用1.0,因为我们已经动态计算了具体间距
spacingFactor: 1.2, // 增加间距因子,让图更宽
nodeSep: Math.round(calculatedNodeSep), // 动态计算的节点间距
edgeSep: Math.round(calculatedEdgeSep), // 动态计算的边间距
rankSep: Math.round(calculatedRankSep), // 动态计算的层级间距
nodeDimensionsIncludeLabels: true, // 考虑标签大小
animate: false,
padding: Math.min(40, containerWidth * 0.02), // 动态边距,不超过容器宽度的2%
padding: Math.max(40, Math.min(60, containerWidth * 0.03)), // 减少边距,让图更宽
// 优化边的路由,减少交叉
edgeRouting: 'polyline',
// 对齐方式:使用上左对齐,然后手动居中
@@ -2205,11 +2254,14 @@ function renderAttackChain(chainData) {
// 应用布局,等待布局完成后再平衡和居中
const layout = attackChainCytoscape.layout(layoutOptions);
layout.one('layoutstop', () => {
// 布局完成后,先平衡分支,再居中显示
// 布局完成后,先平衡分支,再修复重叠,最后居中显示
setTimeout(() => {
balanceBranches();
setTimeout(() => {
centerAttackChain();
fixNodeOverlaps();
setTimeout(() => {
centerAttackChain();
}, 50);
}, 50);
}, 100);
});
@@ -2228,13 +2280,15 @@ function renderAttackChain(chainData) {
const avgNodeWidth = isComplexGraph ? 230 : 250;
const estimatedDepth = Math.ceil(Math.log2(Math.max(nodeCount, 2))) + 1;
const maxLevelWidth = Math.max(1, Math.ceil(nodeCount / estimatedDepth));
const targetGraphWidth = containerWidth * 0.88;
const minNodeSep = avgNodeWidth * 0.8; // 与布局计算保持一致
const targetGraphWidth = containerWidth * 0.95; // 与布局计算保持一致,使用95%宽度
// 与布局计算保持一致,使用更大的间距避免节点重叠
const minNodeSep = avgNodeWidth * 1.5; // 与布局计算保持一致
const availableWidth = targetGraphWidth - avgNodeWidth * maxLevelWidth;
const spacing = Math.max(
minNodeSep,
Math.min(
(targetGraphWidth - avgNodeWidth * maxLevelWidth) / Math.max(1, maxLevelWidth - 1),
avgNodeWidth * 2.0 // 与布局计算保持一致
availableWidth / Math.max(1, maxLevelWidth - 1),
avgNodeWidth * 3.0 // 与布局计算保持一致
)
);
@@ -2334,9 +2388,9 @@ function renderAttackChain(chainData) {
const leftTotalWidth = leftGroup.length > 0 ? leftTotal + (leftGroup.length - 1) * spacing : 0;
const rightTotalWidth = rightGroup.length > 0 ? rightTotal + (rightGroup.length - 1) * spacing : 0;
// 根据容器宽度动态调整,充分利用水平空间
// 使用更大的宽度系数,让图充分利用容器空间(使用88%的容器宽度以匹配布局算法)
// 使用更大的宽度系数,让图充分利用容器空间(使用95%的容器宽度以匹配布局算法)
const maxSideWidth = Math.max(leftTotalWidth, rightTotalWidth);
const targetWidth = Math.max(maxSideWidth * 1.2, containerWidth * 0.88); // 使用88%的容器宽度以匹配布局
const targetWidth = Math.max(maxSideWidth * 1.2, containerWidth * 0.95); // 使用95%的容器宽度以匹配布局
const maxWidth = Math.max(targetWidth, avgNodeWidth * 2);
// 递归调整子树位置
@@ -2437,6 +2491,148 @@ function renderAttackChain(chainData) {
}
}
// 修复节点重叠的函数
function fixNodeOverlaps() {
try {
if (!attackChainCytoscape) {
return;
}
const nodes = attackChainCytoscape.nodes();
const minSpacing = 40; // 节点之间的最小间距(像素),增加以确保不重叠
const overlapThreshold = 0.05; // 重叠阈值(5%),更敏感地检测重叠
// 按Y坐标分组节点(同一层级的节点)
const nodesByLevel = new Map();
nodes.forEach(node => {
const pos = node.position();
const y = Math.round(pos.y / 30) * 30; // 将相近的Y坐标归为同一层级(更精细的分组)
if (!nodesByLevel.has(y)) {
nodesByLevel.set(y, []);
}
nodesByLevel.get(y).push(node);
});
// 检查并修复同一层级内的重叠
nodesByLevel.forEach((levelNodes, levelY) => {
// 按X坐标排序
levelNodes.sort((a, b) => a.position().x - b.position().x);
// 检查相邻节点是否重叠
for (let i = 0; i < levelNodes.length - 1; i++) {
const node1 = levelNodes[i];
const node2 = levelNodes[i + 1];
const pos1 = node1.position();
const pos2 = node2.position();
const width1 = node1.width();
const width2 = node2.width();
const height1 = node1.height();
const height2 = node2.height();
// 计算节点边界
const left1 = pos1.x - width1 / 2;
const right1 = pos1.x + width1 / 2;
const top1 = pos1.y - height1 / 2;
const bottom1 = pos1.y + height1 / 2;
const left2 = pos2.x - width2 / 2;
const right2 = pos2.x + width2 / 2;
const top2 = pos2.y - height2 / 2;
const bottom2 = pos2.y + height2 / 2;
// 检查是否重叠
const horizontalOverlap = Math.max(0, Math.min(right1, right2) - Math.max(left1, left2));
const verticalOverlap = Math.max(0, Math.min(bottom1, bottom2) - Math.max(top1, top2));
const overlapArea = horizontalOverlap * verticalOverlap;
const node1Area = width1 * height1;
const node2Area = width2 * height2;
const minArea = Math.min(node1Area, node2Area);
// 如果重叠面积超过阈值,调整位置
if (overlapArea > minArea * overlapThreshold) {
// 计算需要的间距
const requiredSpacing = (width1 + width2) / 2 + minSpacing;
const currentSpacing = pos2.x - pos1.x;
const spacingDiff = requiredSpacing - currentSpacing;
if (spacingDiff > 0) {
// 向右移动第二个节点及其后续节点
const moveDistance = spacingDiff;
for (let j = i + 1; j < levelNodes.length; j++) {
const node = levelNodes[j];
const currentPos = node.position();
node.position({
x: currentPos.x + moveDistance,
y: currentPos.y
});
}
}
}
}
});
// 检查不同层级之间的重叠(垂直方向)- 简化处理
// 只处理明显的垂直重叠,通过增加层级间距来解决
const sortedLevels = Array.from(nodesByLevel.keys()).sort((a, b) => a - b);
for (let i = 0; i < sortedLevels.length - 1; i++) {
const level1Y = sortedLevels[i];
const level2Y = sortedLevels[i + 1];
const level1Nodes = nodesByLevel.get(level1Y);
const level2Nodes = nodesByLevel.get(level2Y);
// 检查两个层级之间的最小垂直间距
let minVerticalSpacing = Infinity;
level1Nodes.forEach(node1 => {
const pos1 = node1.position();
const height1 = node1.height();
const bottom1 = pos1.y + height1 / 2;
level2Nodes.forEach(node2 => {
const pos2 = node2.position();
const height2 = node2.height();
const top2 = pos2.y - height2 / 2;
const spacing = top2 - bottom1;
if (spacing < minVerticalSpacing) {
minVerticalSpacing = spacing;
}
});
});
// 如果垂直间距太小,向下移动第二个层级的所有节点
if (minVerticalSpacing < minSpacing) {
const moveDistance = minSpacing - minVerticalSpacing;
level2Nodes.forEach(node => {
const currentPos = node.position();
node.position({
x: currentPos.x,
y: currentPos.y + moveDistance
});
});
// 更新后续层级的Y坐标
for (let j = i + 2; j < sortedLevels.length; j++) {
const laterLevelY = sortedLevels[j];
const laterLevelNodes = nodesByLevel.get(laterLevelY);
laterLevelNodes.forEach(node => {
const currentPos = node.position();
node.position({
x: currentPos.x,
y: currentPos.y + moveDistance
});
});
}
}
}
} catch (error) {
console.warn('修复节点重叠时出错:', error);
}
}
// 居中攻击链的函数
function centerAttackChain() {
try {
@@ -2472,7 +2668,7 @@ function renderAttackChain(chainData) {
// 根据图的宽度和容器宽度,调整缩放以更好地利用水平空间
const graphWidth = extent.x2 - extent.x1;
const graphHeight = extent.y2 - extent.y1;
const availableWidth = containerWidth * 0.88; // 使用88%的容器宽度(与布局算法一致)
const availableWidth = containerWidth * 0.95; // 使用95%的容器宽度(与布局算法一致)
const availableHeight = containerHeight * 0.85; // 使用85%的容器高度
const currentZoom = attackChainCytoscape.zoom();
+12
View File
@@ -142,6 +142,18 @@
</div>
<div class="sidebar-content">
<div class="sidebar-title">历史对话</div>
<div class="conversation-search-box">
<input type="text" id="conversation-search-input" placeholder="搜索历史记录..."
oninput="handleConversationSearch(this.value)"
onkeypress="if(event.key === 'Enter') handleConversationSearch(this.value)" />
<button class="conversation-search-clear" id="conversation-search-clear"
onclick="clearConversationSearch()" 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 id="conversations-list" class="conversations-list"></div>
</div>
</aside>