From c41dfed5f393a7bed2cda5673846c337fbb3e7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sat, 27 Dec 2025 02:30:34 +0800 Subject: [PATCH] Add files via upload --- internal/attackchain/builder.go | 89 +++-- web/static/css/style.css | 653 +++++++++++++++++++++++++++----- web/static/js/chat.js | 230 ++++++----- web/templates/index.html | 129 ++++--- 4 files changed, 821 insertions(+), 280 deletions(-) diff --git a/internal/attackchain/builder.go b/internal/attackchain/builder.go index f85d75cf..7db3b82c 100644 --- a/internal/attackchain/builder.go +++ b/internal/attackchain/builder.go @@ -334,7 +334,7 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { ## 核心目标 -构建一个能够讲述完整攻击故事的攻击链(通常8-15个节点,必要时可适当增加),让学习者能够: +构建一个能够讲述完整攻击故事的攻击链让学习者能够: 1. 理解渗透测试的完整流程和思维逻辑(从目标识别到漏洞发现的每一步) 2. 学习如何从失败中获取线索并调整策略 3. 掌握工具使用的实际效果和局限性 @@ -358,11 +358,15 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { - **vulnerability节点**:每个真实确认的漏洞创建一个vulnerability节点 - **完整性检查**:对照ReAct输入中的工具调用序列,确保每个有意义的工具执行都被包含在攻击链中 -### 第三步:构建逻辑关系 -按照时间顺序和因果关系连接节点: +### 第三步:构建逻辑关系(树状结构) +**重要:必须构建树状结构,而不是简单的线性链。** +按照因果关系连接节点,形成树状图(因为是单agent执行,所以可以不按照时间顺序): +- **分支结构**:一个节点可以有多个后续节点(例如:端口扫描发现多个端口后,可以同时进行多个不同的测试) +- **汇聚结构**:多个节点可以指向同一个节点(例如:多个不同的测试都发现了同一个漏洞) - 识别哪些action是基于前面action的结果而执行的 - 识别哪些vulnerability是由哪些action发现的 - 识别失败节点如何为后续成功提供线索 +- **避免线性链**:不要将所有节点连成一条线,应该根据实际的并行测试和分支探索构建树状结构 ### 第四步:优化和精简 - **完整性检查**:确保所有有意义的工具执行都被包含,不要遗漏关键步骤 @@ -449,10 +453,12 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { ### 边的类型 - **leads_to**:表示"导致"或"引导到",用于action→action、target→action * 例如:端口扫描 → 目录扫描(因为发现了80端口,所以进行目录扫描) -- **discovers**:表示"发现",用于action→vulnerability +- **discovers**:表示"发现",**专门用于action→vulnerability** * 例如:SQL注入测试 → SQL注入漏洞 -- **enables**:表示"使能"或"促成",用于vulnerability→vulnerability、action→action(当后续行动依赖前面结果时) + * **重要**:所有action→vulnerability的边都必须使用discovers类型,即使多个action都指向同一个vulnerability,也应该统一使用discovers +- **enables**:表示"使能"或"促成",**仅用于vulnerability→vulnerability、action→action(当后续行动依赖前面结果时)** * 例如:信息泄露漏洞 → 权限提升漏洞(通过信息泄露获得的信息促成了权限提升) + * **重要**:enables不能用于action→vulnerability,action→vulnerability必须使用discovers ### 边的权重 - **权重1-2**:弱关联(如初步探测到进一步探测) @@ -460,10 +466,14 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { - **权重5-7**:强关联(如发现漏洞、关键信息泄露) - **权重8-10**:极强关联(如漏洞利用成功、权限提升) -### DAG结构要求 +### DAG结构要求(树状图) - 所有边的source节点id必须小于target节点id(确保无环) - 节点id从"node_1"开始递增 - 确保无孤立节点(每个节点至少有一条边连接) +- **树状结构要求**: + * 一个节点可以有多个后续节点(分支),例如:端口扫描节点可以同时连接到"Web服务识别"、"FTP服务识别"、"SSH服务识别"等多个节点 + * 多个节点可以汇聚到一个节点(汇聚),例如:多个不同的测试都指向同一个漏洞节点 + * 避免将所有节点连成一条线,应该根据实际的并行测试和分支探索构建树状结构 ## 攻击链逻辑连贯性要求 @@ -487,6 +497,8 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { 严格按照以下JSON格式输出,不要添加任何其他文字: +**重要:示例展示的是树状结构,注意node_2(端口扫描)同时连接到多个后续节点(node_3、node_4),形成分支结构。** + { "nodes": [ { @@ -513,30 +525,42 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { { "id": "node_3", "type": "action", - "label": "尝试SQL注入(被WAF拦截)", - "risk_score": 0, - "metadata": { - "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目录进行绕过测试。", + "ai_analysis": "使用dirsearch对目标进行目录扫描,发现/admin目录存在且可访问。该目录可能为管理后台,是重要的测试目标。", "findings": ["/admin目录存在", "返回200状态码", "疑似管理后台"] } }, + { + "id": "node_4", + "type": "action", + "label": "识别Web服务为Apache 2.4", + "risk_score": 0, + "metadata": { + "tool_name": "whatweb", + "tool_intent": "Web服务识别", + "ai_analysis": "识别出目标运行Apache 2.4服务器,这为后续的漏洞测试提供了重要信息。", + "findings": ["Apache 2.4", "PHP版本信息"] + } + }, { "id": "node_5", + "type": "action", + "label": "尝试SQL注入(被WAF拦截)", + "risk_score": 0, + "metadata": { + "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_6", "type": "vulnerability", "label": "SQL注入漏洞", "risk_score": 85, @@ -559,17 +583,23 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { "source": "node_2", "target": "node_3", "type": "leads_to", + "weight": 4 + }, + { + "source": "node_2", + "target": "node_4", + "type": "leads_to", "weight": 3 }, { "source": "node_3", - "target": "node_4", + "target": "node_5", "type": "leads_to", "weight": 4 }, { - "source": "node_4", - "target": "node_5", + "source": "node_5", + "target": "node_6", "type": "discovers", "weight": 7 } @@ -579,12 +609,13 @@ func (b *Builder) buildSimplePrompt(reactInput, modelOutput string) string { ## 重要提醒 1. **严禁杜撰**:只使用ReAct输入中实际执行的工具和实际返回的结果。如无实际数据,返回空的nodes和edges数组。 -2. **完整性优先**:必须包含所有有意义的工具执行和关键步骤,不要为了控制节点数量而删除重要节点。攻击链必须能够完整展现从目标识别到漏洞发现的完整过程。 -3. **逻辑连贯**:确保攻击链能够讲述一个完整、连贯的渗透测试故事,包括所有关键步骤和决策点。 -4. **教育价值**:优先保留有教育意义的节点,帮助学习者理解渗透测试思维和完整流程。 -5. **准确性**:所有节点信息必须基于实际数据,不要推测或假设。 -6. **完整性检查**:确保每个节点都有必要的metadata字段,每条边都有正确的source和target,没有孤立节点。 -7. **不要过度精简**:如果实际执行步骤较多,可以适当增加节点数量(最多20个),确保不遗漏关键步骤。 +2. **树状结构优先**:必须构建树状结构,而不是线性链。一个节点可以有多个后续节点(分支),多个节点可以指向同一个节点(汇聚)。避免将所有节点连成一条线。 +3. **完整性优先**:必须包含所有有意义的工具执行和关键步骤,不要为了控制节点数量而删除重要节点。攻击链必须能够完整展现从目标识别到漏洞发现的完整过程。 +4. **逻辑连贯**:确保攻击链能够讲述一个完整、连贯的渗透测试故事,包括所有关键步骤和决策点。 +5. **教育价值**:优先保留有教育意义的节点,帮助学习者理解渗透测试思维和完整流程。 +6. **准确性**:所有节点信息必须基于实际数据,不要推测或假设。 +7. **完整性检查**:确保每个节点都有必要的metadata字段,每条边都有正确的source和target,没有孤立节点。 +8. **不要过度精简**:如果实际执行步骤较多,可以适当增加节点数量(最多20个),确保不遗漏关键步骤。 现在开始分析并构建攻击链:`, reactInput, modelOutput) } diff --git a/web/static/css/style.css b/web/static/css/style.css index 9f58c8da..2b0834e7 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -1739,24 +1739,27 @@ header { } .modal-close { - width: 32px; - height: 32px; + width: 36px; + height: 36px; display: flex; align-items: center; justify-content: center; - border-radius: 6px; + border-radius: 10px; cursor: pointer; color: var(--text-secondary); font-size: 1.5rem; line-height: 1; - transition: all 0.2s; - border: none; + transition: all 0.25s ease; + border: 2px solid transparent; background: transparent; + font-weight: 300; } .modal-close:hover { - background: var(--bg-tertiary); - color: var(--text-primary); + background: linear-gradient(135deg, rgba(220, 53, 69, 0.1) 0%, rgba(220, 53, 69, 0.05) 100%); + color: #dc3545; + border-color: rgba(220, 53, 69, 0.2); + transform: scale(1.1); } .modal-body { @@ -3816,9 +3819,10 @@ header { max-height: 90vh; display: flex; flex-direction: column; - border-radius: 16px; + border-radius: 20px; overflow: hidden; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + box-shadow: 0 24px 80px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.05); + background: #ffffff; } .attack-chain-body { @@ -3827,73 +3831,160 @@ header { flex: 1; overflow: hidden; padding: 0; - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + background: linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%); } -.attack-chain-controls { - padding: 20px 24px; - border-bottom: 1px solid var(--border-color); +.attack-chain-main-layout { + display: flex; + flex: 1; + overflow: hidden; + min-height: 0; + gap: 0; +} + +.attack-chain-visualization-area { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + min-width: 0; + min-height: 0; +} + +.attack-chain-sidebar { + width: 280px; + flex-shrink: 0; + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + border-left: 1px solid rgba(0, 0, 0, 0.06); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.attack-chain-sidebar-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + will-change: scroll-position; + -webkit-overflow-scrolling: touch; +} + +.attack-chain-toolbar { + padding: 12px 16px; + border-bottom: 1px solid rgba(0, 0, 0, 0.06); + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); display: flex; - justify-content: space-between; align-items: center; - flex-wrap: wrap; - gap: 20px; - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + gap: 16px; + flex-shrink: 0; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); } .attack-chain-info { - font-size: 0.9375rem; + font-size: 0.8125rem; color: var(--text-primary); - font-weight: 500; - padding: 8px 16px; - background: linear-gradient(135deg, rgba(0, 102, 255, 0.08) 0%, rgba(0, 102, 255, 0.04) 100%); - border: 1px solid rgba(0, 102, 255, 0.15); - border-radius: 12px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + font-weight: 600; + padding: 6px 12px; + background: linear-gradient(135deg, rgba(0, 102, 255, 0.1) 0%, rgba(0, 102, 255, 0.06) 100%); + border: 1px solid rgba(0, 102, 255, 0.2); + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 102, 255, 0.08); + display: inline-flex; + align-items: center; + gap: 6px; + white-space: nowrap; + flex-shrink: 0; +} + +.attack-chain-info::before { + content: '📊'; + font-size: 0.8125rem; } .attack-chain-legend { display: flex; + flex-direction: column; gap: 20px; - flex-wrap: wrap; - padding: 12px 20px; - background: var(--bg-primary); - border-radius: 12px; - border: 1px solid var(--border-color); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + padding: 16px; + background: transparent; + flex-shrink: 0; } .legend-item { display: flex; align-items: center; - gap: 8px; - font-size: 0.875rem; + gap: 10px; + font-size: 0.8125rem; font-weight: 500; color: var(--text-primary); padding: 4px 0; + transition: all 0.2s ease; +} + +.legend-item:hover { + transform: translateX(2px); } .legend-color { width: 20px; height: 20px; border-radius: 6px; - border: 2px solid rgba(255, 255, 255, 0.8); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); - transition: transform 0.2s ease; + border: 1.5px solid rgba(255, 255, 255, 0.9); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.3); + transition: all 0.2s ease; + flex-shrink: 0; } .legend-item:hover .legend-color { - transform: scale(1.1); + transform: scale(1.15); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.3); +} + +.legend-section { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.legend-title { + font-size: 0.75rem; + font-weight: 700; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 6px; + padding-bottom: 6px; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); +} + +.legend-line { + display: inline-block; + width: 32px; + height: 0; + margin-right: 0; + vertical-align: middle; + border-radius: 2px; + flex-shrink: 0; + transition: all 0.2s ease; +} + +.legend-item:hover .legend-line { + transform: scaleX(1.2); } .attack-chain-container { flex: 1; min-height: 0; - background: linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%); + background: #ffffff; // 使用纯白色背景,提高节点对比度 border: none; position: relative; overflow: hidden; + margin: 12px; + border-radius: 12px; + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.06); } .attack-chain-container::before { @@ -3904,41 +3995,46 @@ header { right: 0; bottom: 0; background: - radial-gradient(circle at 20% 30%, rgba(0, 102, 255, 0.03) 0%, transparent 50%), - radial-gradient(circle at 80% 70%, rgba(0, 102, 255, 0.03) 0%, transparent 50%); + radial-gradient(circle at 20% 30%, rgba(0, 102, 255, 0.02) 0%, transparent 50%), + radial-gradient(circle at 80% 70%, rgba(0, 102, 255, 0.02) 0%, transparent 50%); pointer-events: none; z-index: 0; + border-radius: 12px; } /* 攻击链筛选器样式 */ .attack-chain-filters { display: flex; - gap: 12px; + gap: 8px; align-items: center; flex-wrap: wrap; - padding: 12px 16px; - background: var(--bg-primary); - border-radius: 12px; - border: 1px solid var(--border-color); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + flex: 1; + min-width: 0; } .attack-chain-filters input[type="text"] { - padding: 10px 16px; - border: 2px solid var(--border-color); - border-radius: 8px; - font-size: 0.875rem; - min-width: 220px; - background: var(--bg-primary); + padding: 6px 12px; + border: 1.5px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + font-size: 0.8125rem; + min-width: 180px; + flex: 1; + max-width: 300px; + background: #ffffff; color: var(--text-primary); - transition: all 0.2s ease; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + transition: all 0.25s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); } .attack-chain-filters input[type="text"]:focus { outline: none; border-color: var(--accent-color); - box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); + box-shadow: 0 0 0 4px rgba(0, 102, 255, 0.12), 0 2px 6px rgba(0, 102, 255, 0.1); + transform: translateY(-1px); +} + +.attack-chain-filters input[type="text"]:hover { + border-color: rgba(0, 102, 255, 0.3); } .attack-chain-filters input[type="text"]::placeholder { @@ -3946,74 +4042,267 @@ header { } .attack-chain-filters select { - padding: 10px 16px; - border: 2px solid var(--border-color); - border-radius: 8px; - font-size: 0.875rem; - background: var(--bg-primary); + padding: 6px 12px; + padding-right: 32px; + border: 1.5px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + font-size: 0.8125rem; + background: #ffffff; color: var(--text-primary); cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + transition: all 0.25s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); appearance: none; - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; - background-position: right 12px center; - padding-right: 40px; + background-position: right 10px center; + font-weight: 500; + min-width: 120px; } .attack-chain-filters select:focus { outline: none; border-color: var(--accent-color); - box-shadow: 0 0 0 3px rgba(0, 102, 255, 0.1); + box-shadow: 0 0 0 4px rgba(0, 102, 255, 0.12), 0 2px 6px rgba(0, 102, 255, 0.1); + transform: translateY(-1px); } .attack-chain-filters select:hover { - border-color: var(--accent-color); + border-color: rgba(0, 102, 255, 0.3); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); } .attack-chain-filters button.btn-secondary { - padding: 10px 18px; - font-size: 0.875rem; - font-weight: 500; - border: 2px solid var(--border-color); - border-radius: 8px; - background: var(--bg-primary); + padding: 6px 14px; + font-size: 0.8125rem; + font-weight: 600; + border: 1.5px solid rgba(0, 0, 0, 0.1); + border-radius: 6px; + background: #ffffff; color: var(--text-primary); cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + transition: all 0.25s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + white-space: nowrap; } .attack-chain-filters button.btn-secondary:hover { - background: var(--bg-tertiary); + background: linear-gradient(135deg, rgba(0, 102, 255, 0.08) 0%, rgba(0, 102, 255, 0.04) 100%); border-color: var(--accent-color); color: var(--accent-color); - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 102, 255, 0.15); +} + +.attack-chain-action-btn { + padding: 10px 18px !important; + font-size: 0.875rem !important; + font-weight: 600 !important; + border-radius: 10px !important; + transition: all 0.25s ease !important; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08) !important; +} + +.attack-chain-action-btn:hover { + transform: translateY(-2px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; +} + +.modal-header { + padding: 16px 20px !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.06) !important; + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important; + flex-shrink: 0; +} + +.modal-header h2 { + font-size: 1.25rem !important; + font-weight: 700 !important; + letter-spacing: -0.3px !important; + background: linear-gradient(135deg, #1a1a1a 0%, #4a4a4a 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } .attack-chain-details { - padding: 20px 24px; - border-top: 1px solid var(--border-color); - background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); - max-height: 200px; - overflow-y: auto; - box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.04); + display: flex !important; + flex-direction: column; + gap: 12px; + margin-top: 8px; + padding-top: 20px; + border-top: 1px solid rgba(0, 0, 0, 0.08); + transition: opacity 0.2s ease; + overflow: hidden; + will-change: opacity; } -.attack-chain-details h3 { - margin: 0 0 16px 0; - font-size: 1.125rem; - font-weight: 600; +.attack-chain-details[style*="display: none"] { + display: none !important; +} + +@keyframes slideDown { + from { + opacity: 0; + max-height: 0; + padding-top: 0; + margin-top: 0; + } + to { + opacity: 1; + max-height: 1500px; + padding-top: 20px; + margin-top: 8px; + } +} + +.attack-chain-details-title { + display: flex !important; + justify-content: space-between; + align-items: center; + margin-bottom: 0 !important; + padding-bottom: 8px !important; + border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important; +} + +.attack-chain-details-title span { + flex: 1; +} + +.attack-chain-details-close { + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + cursor: pointer; + color: var(--text-secondary); + border: 1px solid transparent; + background: transparent; + transition: all 0.2s ease; + padding: 0; + flex-shrink: 0; + opacity: 0.6; +} + +.attack-chain-details-close:hover { + background: rgba(0, 0, 0, 0.06); color: var(--text-primary); - padding-bottom: 12px; - border-bottom: 2px solid var(--border-color); + opacity: 1; +} + +.attack-chain-details-content { + display: flex; + flex-direction: column; + gap: 12px; + max-height: 600px; + overflow-y: auto; + overflow-x: hidden; + padding-right: 4px; + will-change: scroll-position; + -webkit-overflow-scrolling: touch; +} + +.attack-chain-details-content::-webkit-scrollbar { + width: 6px; +} + +.attack-chain-details-content::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.02); + border-radius: 3px; +} + +.attack-chain-details-content::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.15); + border-radius: 3px; +} + +.attack-chain-details-content::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.25); } .node-detail-item { - margin-bottom: 8px; - font-size: 0.875rem; + margin-bottom: 0; + font-size: 0.8125rem; + line-height: 1.6; + padding: 10px 12px; + background: rgba(0, 0, 0, 0.02); + border-radius: 6px; + border: 1px solid rgba(0, 0, 0, 0.05); + transition: all 0.2s ease; +} + +.node-detail-item:hover { + background: rgba(0, 0, 0, 0.04); + border-color: rgba(0, 102, 255, 0.2); +} + +.node-detail-item strong { + display: block; + margin-bottom: 6px; + color: var(--text-primary); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--text-secondary); +} + +.node-detail-item code { + background: rgba(0, 0, 0, 0.06); + padding: 3px 8px; + border-radius: 4px; + font-size: 0.8125rem; + word-break: break-all; + display: inline-block; + max-width: 100%; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + color: var(--text-primary); +} + +.node-detail-item pre { + background: rgba(0, 0, 0, 0.06); + padding: 10px; + border-radius: 6px; + font-size: 0.75rem; + overflow-x: auto; + margin: 8px 0 0 0; + line-height: 1.5; + max-width: 100%; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.node-detail-item ul { + margin: 8px 0 0 0; + padding-left: 20px; +} + +.node-detail-item li { + margin-bottom: 6px; + line-height: 1.5; +} + +.node-detail-item li:last-child { + margin-bottom: 0; +} + +.node-detail-item div[style*="background"] { + margin-top: 8px; + padding: 10px 12px !important; + border-radius: 6px; + line-height: 1.6; + border: 1px solid rgba(0, 0, 0, 0.08); +} + +.node-detail-item ul { + margin: 8px 0; + padding-left: 20px; +} + +.node-detail-item li { + margin-bottom: 4px; } .node-detail-item strong { @@ -4040,30 +4329,50 @@ header { .modal-header-actions { display: flex; - gap: 10px; + gap: 12px; align-items: center; flex-wrap: wrap; } .attack-chain-action-btn { - padding: 10px 18px; - font-size: 0.875rem; - font-weight: 500; - border-radius: 8px; - transition: all 0.2s ease; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - border: 2px solid transparent; + padding: 8px 16px !important; + font-size: 0.8125rem !important; + font-weight: 600 !important; + border-radius: 8px !important; + transition: all 0.25s ease !important; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08) !important; + border: 2px solid transparent !important; + display: inline-flex !important; + align-items: center !important; + gap: 6px !important; } .attack-chain-action-btn:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + transform: translateY(-2px) !important; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important; } .attack-chain-action-btn.btn-primary { - background: linear-gradient(135deg, #0066ff 0%, #0052cc 100%); - color: white; - border-color: #0066ff; + background: linear-gradient(135deg, #0066ff 0%, #0052cc 100%) !important; + color: white !important; + border-color: #0066ff !important; +} + +.attack-chain-action-btn.btn-primary:hover { + background: linear-gradient(135deg, #0052cc 0%, #0040a3 100%) !important; + box-shadow: 0 6px 20px rgba(0, 102, 255, 0.3) !important; +} + +.attack-chain-action-btn.btn-secondary { + background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important; + color: var(--text-primary) !important; + border-color: rgba(0, 0, 0, 0.1) !important; +} + +.attack-chain-action-btn.btn-secondary:hover { + background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f5 100%) !important; + border-color: var(--accent-color) !important; + color: var(--accent-color) !important; } .attack-chain-action-btn.btn-primary:hover { @@ -5980,3 +6289,137 @@ header { color: var(--text-secondary); font-size: 1rem; } + +/* ==================== 攻击链可视化响应式样式 ==================== */ +@media (max-width: 1200px) { + .attack-chain-modal-content { + width: 98vw; + height: 92vh; + max-height: 92vh; + } + + .attack-chain-sidebar { + width: 240px; + } + + .attack-chain-toolbar { + padding: 10px 12px; + gap: 12px; + } + + .attack-chain-filters { + gap: 6px; + } + + .attack-chain-filters input[type="text"] { + min-width: 150px; + max-width: 250px; + } + + .attack-chain-container { + margin: 10px; + } +} + +@media (max-width: 768px) { + .attack-chain-modal-content { + width: 100vw; + height: 100vh; + max-height: 100vh; + border-radius: 0; + } + + .attack-chain-main-layout { + flex-direction: column; + } + + .attack-chain-sidebar { + width: 100%; + max-height: 200px; + border-left: none; + border-top: 1px solid rgba(0, 0, 0, 0.06); + order: 2; + } + + .attack-chain-visualization-area { + order: 1; + } + + .attack-chain-toolbar { + flex-direction: column; + align-items: stretch; + gap: 10px; + padding: 10px 12px; + } + + .attack-chain-info { + text-align: center; + width: 100%; + } + + .attack-chain-filters { + flex-direction: column; + width: 100%; + } + + .attack-chain-filters input[type="text"], + .attack-chain-filters select, + .attack-chain-filters button { + width: 100%; + min-width: auto; + max-width: none; + } + + .attack-chain-legend { + flex-direction: row; + gap: 16px; + padding: 12px; + overflow-x: auto; + } + + .legend-section { + min-width: 160px; + flex-shrink: 0; + } + + .attack-chain-details { + min-width: 200px; + flex-shrink: 0; + } + + .attack-chain-details-content { + max-height: 400px; + } + + .attack-chain-container { + margin: 8px; + border-radius: 10px; + } + + .attack-chain-details { + margin: 0 8px 8px 8px; + border-radius: 10px; + padding: 12px 16px; + } + + .modal-header { + padding: 12px 16px !important; + } + + .modal-header h2 { + font-size: 1.125rem !important; + } + + .modal-header-actions { + flex-direction: column; + width: 100%; + gap: 6px; + } + + .attack-chain-action-btn { + width: 100%; + justify-content: center; + padding: 7px 14px !important; + font-size: 0.8125rem !important; + } +} diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 15bdb903..caafc6cc 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2026,13 +2026,13 @@ function renderAttackChain(chainData) { // 根据节点类型调整大小,target节点更大(增加高度以容纳类型标签) 'width': function(ele) { const type = ele.data('type'); - if (type === 'target') return isComplexGraph ? 240 : 260; - return isComplexGraph ? 220 : 240; + if (type === 'target') return isComplexGraph ? 380 : 420; + return isComplexGraph ? 360 : 400; }, 'height': function(ele) { const type = ele.data('type'); - if (type === 'target') return isComplexGraph ? 115 : 125; - return isComplexGraph ? 110 : 120; + if (type === 'target') return isComplexGraph ? 180 : 200; + return isComplexGraph ? 170 : 190; }, 'shape': function(ele) { // 所有节点都使用圆角矩形 @@ -2044,7 +2044,7 @@ function renderAttackChain(chainData) { // target节点使用更深的蓝色背景,增强对比度 if (type === 'target') { - return '#bbdefb'; // 更深的浅蓝色 + return '#e8f4fd'; // 更亮的蓝色背景,提高对比度 } // action节点根据执行有效性显示不同颜色 @@ -2058,30 +2058,30 @@ function renderAttackChain(chainData) { const isFailedInsight = status === 'failed_insight'; if (hasFindings && !isFailedInsight) { - return '#e8f5e9'; // 浅绿色:有效执行 + return '#f5fbf5'; // 更亮的绿色背景,提高对比度 } else { - return '#f5f5f5'; // 浅灰色:无效执行 + return '#fafafa'; // 浅灰色背景,与白色文字形成对比 } } - // vulnerability节点根据风险分数显示不同颜色 + // vulnerability节点根据风险分数显示不同颜色,使用更亮的背景 if (type === 'vulnerability') { - if (riskScore >= 80) return '#ffcdd2'; // 更饱和的浅红色 - if (riskScore >= 60) return '#ffe0b2'; // 更饱和的浅橙色 - if (riskScore >= 40) return '#fff9c4'; // 更饱和的浅黄色 - return '#dcedc8'; // 更饱和的浅绿色 + if (riskScore >= 80) return '#fff0f0'; // 更亮的红色背景 + if (riskScore >= 60) return '#fff5e6'; // 更亮的橙色背景 + if (riskScore >= 40) return '#fffef0'; // 更亮的黄色背景 + return '#f5fbf5'; // 更亮的绿色背景 } - return '#f5f5f5'; // 默认浅灰色 + return '#ffffff'; // 默认白色背景 }, - // 根据节点类型和风险分数设置文字颜色 + // 根据节点类型和风险分数设置文字颜色,使用更深的颜色提高对比度 // 注意:由于标签包含类型标签和内容,颜色适用于所有文本 'color': function(ele) { const type = ele.data('type'); const riskScore = ele.data('riskScore') || 0; if (type === 'target') { - return '#1976d2'; // 深蓝色文字 + return '#0d47a1'; // 更深的蓝色文字,提高对比度 } // action节点根据执行有效性显示不同文字颜色 @@ -2095,54 +2095,54 @@ function renderAttackChain(chainData) { const isFailedInsight = status === 'failed_insight'; if (hasFindings && !isFailedInsight) { - return '#2e7d32'; // 深绿色:有效执行 + return '#1b5e20'; // 更深的绿色:有效执行,提高对比度 } else { - return '#757575'; // 深灰色:无效执行 + return '#212121'; // 深灰色:无效执行,提高对比度 } } - // vulnerability节点根据风险分数显示不同文字颜色 + // vulnerability节点根据风险分数显示不同文字颜色,使用更深的颜色 if (type === 'vulnerability') { - if (riskScore >= 80) return '#c62828'; // 深红色 - if (riskScore >= 60) return '#e65100'; // 深橙色 - if (riskScore >= 40) return '#f57f17'; // 深黄色 - return '#558b2f'; // 深绿色 + if (riskScore >= 80) return '#b71c1c'; // 更深的红色,提高对比度 + if (riskScore >= 60) return '#bf360c'; // 更深的橙色,提高对比度 + if (riskScore >= 40) return '#e65100'; // 更深的黄色,提高对比度 + return '#33691e'; // 更深的绿色,提高对比度 } - return '#424242'; // 默认深灰色 + return '#000000'; // 黑色,最高对比度 }, 'font-size': function(ele) { - // 由于标签包含类型标签和内容,使用合适的字体大小 + // 进一步增大字体,提高可读性 const type = ele.data('type'); - if (type === 'target') return isComplexGraph ? '16px' : '18px'; - return isComplexGraph ? '15px' : '17px'; + if (type === 'target') return isComplexGraph ? '20px' : '22px'; + return isComplexGraph ? '19px' : '21px'; }, - 'font-weight': '600', + 'font-weight': 'bold', // 加粗字体,提高可读性 'font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 'text-valign': 'center', 'text-halign': 'center', 'text-wrap': 'wrap', 'text-max-width': function(ele) { const type = ele.data('type'); - if (type === 'target') return isComplexGraph ? '220px' : '240px'; - return isComplexGraph ? '200px' : '220px'; + if (type === 'target') return isComplexGraph ? '340px' : '380px'; + return isComplexGraph ? '320px' : '360px'; }, 'text-overflow-wrap': 'anywhere', - 'text-margin-y': 3, // 调整垂直边距以适应多行文本 - 'padding': '14px', // 增加内边距,使节点内容更有呼吸感和现代感 + 'text-margin-y': 5, // 调整垂直边距以适应多行文本 + 'padding': '18px', // 进一步增加内边距,使节点内容更有呼吸感 + 'line-height': 1.6, // 增加行高,提高可读性 // 根据节点类型设置边框样式,使用更粗的边框增强视觉效果 'border-width': function(ele) { const type = ele.data('type'); - if (type === 'target') return 5; - return 4; + if (type === 'target') return 6; + return 5; }, - 'border-radius': '12px', // 所有节点都使用圆角 'border-color': function(ele) { const type = ele.data('type'); const riskScore = ele.data('riskScore') || 0; if (type === 'target') { - return '#1976d2'; // 蓝色边框 + return '#1565c0'; // 更深的蓝色边框,提高对比度 } // action节点根据执行有效性显示不同边框颜色 @@ -2156,38 +2156,26 @@ function renderAttackChain(chainData) { const isFailedInsight = status === 'failed_insight'; if (hasFindings && !isFailedInsight) { - return '#66bb6a'; // 绿色边框:有效执行 + return '#4caf50'; // 更深的绿色边框:有效执行,提高对比度 } else { - return '#9e9e9e'; // 灰色边框:无效执行 + return '#757575'; // 更深的灰色边框:无效执行,提高对比度 } } - // vulnerability节点根据风险分数显示不同边框颜色 + // vulnerability节点根据风险分数显示不同边框颜色,使用更深的颜色 if (type === 'vulnerability') { - if (riskScore >= 80) return '#d32f2f'; // 红色边框 - if (riskScore >= 60) return '#f57c00'; // 橙色边框 - if (riskScore >= 40) return '#fbc02d'; // 黄色边框 - return '#689f38'; // 绿色边框 + if (riskScore >= 80) return '#b71c1c'; // 更深的红色边框,提高对比度 + if (riskScore >= 60) return '#bf360c'; // 更深的橙色边框,提高对比度 + if (riskScore >= 40) return '#e65100'; // 更深的黄色边框,提高对比度 + return '#33691e'; // 更深的绿色边框,提高对比度 } - return '#9e9e9e'; // 默认灰色边框 - }, - 'border-style': function(ele) { - const type = ele.data('type'); - // action节点使用虚线边框,其他使用实线 - if (type === 'action') return 'dashed'; - return 'solid'; + return '#616161'; // 更深的默认灰色边框,提高对比度 }, + 'border-style': 'solid', // 统一使用实线边框,提高可读性 'overlay-padding': '12px', // 移除文字轮廓,使用纯色文字 'text-outline-width': 0, - // 增强阴影效果,使节点更立体更有层次感 - // 增强阴影效果,使节点更立体更有层次感(使用更柔和的阴影) - 'shadow-blur': 20, - 'shadow-opacity': 0.25, - 'shadow-offset-x': 2, - 'shadow-offset-y': 6, - 'shadow-color': 'rgba(0, 0, 0, 0.15)', 'background-opacity': 1 } }, @@ -2213,7 +2201,6 @@ function renderAttackChain(chainData) { return '#9e9e9e'; }, 'target-arrow-shape': 'triangle', - 'target-arrow-size': 8, // 使用bezier曲线,更美观 'curve-style': 'bezier', 'control-point-step-size': 60, // 增加步长,让控制点分布更均匀 @@ -2231,13 +2218,7 @@ function renderAttackChain(chainData) { const type = ele.data('type'); if (type === 'targets') return [8, 4]; // 虚线模式 return []; - }, - // 添加边的阴影效果(浅色主题使用浅阴影) - 'shadow-blur': 3, - 'shadow-opacity': 0.1, - 'shadow-offset-x': 1, - 'shadow-offset-y': 1, - 'shadow-color': '#000000' + } } }, { @@ -2245,29 +2226,11 @@ function renderAttackChain(chainData) { style: { 'border-width': 5, 'border-color': '#0066ff', - 'shadow-blur': 16, - 'shadow-opacity': 0.6, - 'shadow-offset-x': 4, - 'shadow-offset-y': 5, - 'shadow-color': '#0066ff', 'z-index': 999, 'opacity': 1, 'overlay-opacity': 0.1, 'overlay-color': '#0066ff' } - }, - { - selector: 'node:hover', - style: { - 'border-width': 5, - 'shadow-blur': 14, - 'shadow-opacity': 0.5, - 'shadow-offset-x': 3, - 'shadow-offset-y': 4, - 'z-index': 998, - 'overlay-opacity': 0.05, - 'overlay-color': '#333333' - } } ], userPanningEnabled: true, @@ -2294,8 +2257,8 @@ function renderAttackChain(chainData) { const containerHeight = container ? container.offsetHeight : 800; // 计算平均节点宽度(考虑不同类型节点的平均尺寸) - const avgNodeWidth = isComplexGraph ? 230 : 250; // 基于新的节点尺寸 - const avgNodeHeight = isComplexGraph ? 97.5 : 107.5; + const avgNodeWidth = isComplexGraph ? 370 : 410; // 进一步增大节点尺寸 + const avgNodeHeight = isComplexGraph ? 175 : 195; // 计算图的层级深度(估算) const estimatedDepth = Math.ceil(Math.log2(Math.max(nodeCount, 2))) + 1; @@ -2317,13 +2280,13 @@ function renderAttackChain(chainData) { ); // 动态计算层级间距:基于容器高度和层级数 - // 大幅增加最小间距,避免节点重合 + // 减小垂直间距,让节点更紧凑,同时节点更大更易读 const targetGraphHeight = containerHeight * 0.85; const calculatedRankSep = Math.max( - avgNodeHeight * 2.5, // 最小为节点高度的2.5倍,确保垂直方向有足够间距 + avgNodeHeight * 1.3, // 减小到节点高度的1.3倍,让节点更紧凑 Math.min( targetGraphHeight / Math.max(estimatedDepth - 1, 1), - avgNodeHeight * 4.0 // 最大不超过节点高度的4.0倍 + avgNodeHeight * 2.0 // 最大不超过节点高度的2.0倍 ) ); @@ -2380,7 +2343,7 @@ function renderAttackChain(chainData) { // 动态计算节点间距,基于容器尺寸 const container = attackChainCytoscape.container(); const containerWidth = container ? container.offsetWidth : 1200; - const avgNodeWidth = isComplexGraph ? 230 : 250; + const avgNodeWidth = isComplexGraph ? 370 : 410; // 与布局计算保持一致 const estimatedDepth = Math.ceil(Math.log2(Math.max(nodeCount, 2))) + 1; const maxLevelWidth = Math.max(1, Math.ceil(nodeCount / estimatedDepth)); const targetGraphWidth = containerWidth * 0.95; // 与布局计算保持一致,使用95%宽度 @@ -2827,7 +2790,24 @@ function renderAttackChain(chainData) { showNodeDetails(node.data()); }); - // 悬停渐变效果已移除 + // 添加悬停效果(使用事件监听器替代CSS选择器) + attackChainCytoscape.on('mouseover', 'node', function(evt) { + const node = evt.target; + node.style('border-width', 5); + node.style('z-index', 998); + node.style('overlay-opacity', 0.05); + node.style('overlay-color', '#333333'); + }); + + attackChainCytoscape.on('mouseout', 'node', function(evt) { + const node = evt.target; + const type = node.data('type'); + // 恢复默认边框宽度 + const defaultBorderWidth = type === 'target' ? 5 : 4; + node.style('border-width', defaultBorderWidth); + node.style('z-index', 'auto'); + node.style('overlay-opacity', 0); + }); // 保存原始数据用于过滤 window.attackChainOriginalData = chainData; @@ -3046,7 +3026,14 @@ function showNodeDetails(nodeData) { return; } - detailsPanel.style.display = 'block'; + // 使用 requestAnimationFrame 优化显示动画 + requestAnimationFrame(() => { + detailsPanel.style.display = 'flex'; + // 在下一帧设置透明度,确保显示动画流畅 + requestAnimationFrame(() => { + detailsPanel.style.opacity = '1'; + }); + }); let html = `
${JSON.stringify(nodeData.metadata, null, 2)}
-