Add files via upload

This commit is contained in:
公明
2026-06-20 19:28:34 +08:00
committed by GitHub
parent 46a7d338a4
commit 11ab5cde8f
6 changed files with 175 additions and 46 deletions
+60 -6
View File
@@ -24545,6 +24545,9 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
flex: 1 1 auto;
min-height: 0;
}
#project-panel-graph .project-fact-graph-footer {
flex: 0 0 auto;
}
.projects-graph-toolbar-row {
align-items: flex-end;
}
@@ -24603,8 +24606,8 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 14px;
padding-top: 2px;
justify-content: flex-end;
gap: 6px 12px;
}
.projects-graph-legend-item {
display: inline-flex;
@@ -24778,10 +24781,16 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
border: 1px solid #e2e8f0;
}
.project-fact-graph-node-category--target { color: #4338ca; background: #eef2ff; border-color: #c7d2fe; }
.project-fact-graph-node-category--finding { color: #be123c; background: #fff1f2; border-color: #fecdd3; }
.project-fact-graph-node-category--finding,
.project-fact-graph-node-category--vulnerability { color: #be123c; background: #fff1f2; border-color: #fecdd3; }
.project-fact-graph-node-category--exploit,
.project-fact-graph-node-category--poc { color: #c2410c; background: #ffedd5; border-color: #fdba74; }
.project-fact-graph-node-category--chain { color: #6d28d9; background: #f5f3ff; border-color: #ddd6fe; }
.project-fact-graph-node-category--auth { color: #0f766e; background: #f0fdfa; border-color: #99f6e4; }
.project-fact-graph-node-category--infra { color: #475569; background: #f1f5f9; border-color: #cbd5e1; }
.project-fact-graph-node-category--business { color: #0369a1; background: #f0f9ff; border-color: #bae6fd; }
.project-fact-graph-node-category--note { color: #64748b; background: #f8fafc; border-color: #e2e8f0; }
.project-fact-graph-node-category--missing { color: #94a3b8; background: #f1f5f9; border-color: #e2e8f0; font-style: italic; }
.project-fact-graph-sidebar-close {
flex-shrink: 0;
display: inline-flex;
@@ -24861,7 +24870,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
}
.project-fact-graph-edge-item {
display: grid;
grid-template-columns: auto auto auto 1fr auto;
grid-template-columns: auto auto 1fr auto;
align-items: center;
gap: 4px 6px;
padding: 6px 8px;
@@ -24931,6 +24940,42 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
color: #cbd5e1;
flex-shrink: 0;
}
.projects-incoming-links-readonly {
margin-top: 4px;
}
.projects-incoming-links-list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.projects-incoming-links-item {
padding: 8px 10px;
font-size: 0.8125rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
color: #334155;
word-break: break-all;
}
.projects-incoming-links-item code {
font-size: 0.75rem;
}
.projects-incoming-links-empty {
margin: 0;
font-size: 0.8125rem;
color: #94a3b8;
}
.projects-edge-type {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem;
color: #4338ca;
}
.projects-edge-arrow {
color: #94a3b8;
}
.project-fact-graph-sidebar-actions {
display: flex;
gap: 8px;
@@ -24938,13 +24983,22 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
padding-top: 4px;
border-top: 1px solid #f1f5f9;
}
.project-fact-graph-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 12px;
margin: 10px 0 0;
flex: 0 0 auto;
}
.project-fact-graph-stats {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin: 12px 0 0;
flex: 0 0 auto;
margin: 0;
flex: 0 1 auto;
}
.projects-graph-stat-badge {
display: inline-flex;
+10 -5
View File
@@ -263,7 +263,7 @@
"tabVulns": "Related vulnerabilities",
"tabSettings": "Settings",
"factToolbarHint": "Index includes key and summary only (must include what + where + how to verify); put attack chain / POC in body, and reproduce via get_project_fact.",
"graphToolbarHint": "Attack path graph shows target → findingexploit causality. Dashed edges are tentative. Click a node for details.",
"graphToolbarHint": "Graph arrows match stored fact links (source → target). Nodes are layered target→infra→findingexploit. Dashed edges are tentative.",
"graphView": "View",
"graphViewPath": "Attack path",
"graphViewFull": "Full graph",
@@ -284,9 +284,14 @@
"graphLegendLeads": "leads_to",
"graphLegendExploits": "exploits",
"graphLegendTentative": "Tentative (dashed)",
"factLinksLabel": "Outgoing links",
"factLinksPlaceholder": "discovered_on: target/primary_domain\nleads_to: finding/swagger",
"factLinksHint": "One per line: type: fact_key. Common types: discovered_on, depends_on, leads_to, enables, exploits. Saving replaces all outgoing links.",
"factLinksLabel": "Links (from → this fact)",
"factLinksPlaceholder": "discovered_on: target/primary_domain\nexploits: exploit/upload-rce",
"factLinksHint": "One per line: type: source_fact_key (source → this fact). Common types: discovered_on, depends_on, leads_to, enables, exploits. Saving replaces all links.",
"factIncomingLinksLabel": "Incoming links (read-only)",
"factIncomingLinksHint": "Derived from outgoing links on source facts. e.g. finding discovered_on → target/* appears as incoming on the target; edit the source fact's outgoing links.",
"factIncomingLinksEmpty": "No incoming links",
"graphEdgeFromSelf": "From this node",
"graphEdgeToSelf": "To this node",
"linksColumn": "Links",
"linkCountsTitle": "Outgoing / incoming edge counts",
"graphConnect": "Connect",
@@ -296,7 +301,7 @@
"graphConnectFailed": "Failed to create edge",
"graphConnectSuccess": "Edge created",
"graphEdgesTitle": "Links",
"graphEdgesHint": "Click an edge in the graph to focus it. Delete mistaken links here.",
"graphEdgesHint": "Arrow direction matches the database and edit modal (source → target). Click an edge to focus it.",
"graphEdgesEmpty": "No links yet",
"graphEdgeOutgoing": "Outgoing",
"graphEdgeIncoming": "Incoming",
+10 -5
View File
@@ -251,7 +251,7 @@
"tabVulns": "关联漏洞",
"tabSettings": "设置",
"factToolbarHint": "索引仅含 key 与摘要(须含「什么 + 在哪 + 如何验证」);攻击链 / POC 写在 bodyAgent 通过 get_project_fact 复现",
"graphToolbarHint": "攻击路径图:展示 target → findingexploit 因果关系;虚线边表示待确认。点击节点查看详情。",
"graphToolbarHint": "攻击路径图箭头与事实存储方向一致(source → target);节点按 target→infra→findingexploit 分层排布。虚线边待确认。",
"graphView": "视图",
"graphViewPath": "攻击路径",
"graphViewFull": "完整关系",
@@ -272,9 +272,14 @@
"graphLegendLeads": "leads_to",
"graphLegendExploits": "exploits",
"graphLegendTentative": "待确认(虚线)",
"factLinksLabel": "关系边(出边",
"factLinksPlaceholder": "discovered_on: target/primary_domain\nleads_to: finding/swagger",
"factLinksHint": "每行一条:type: fact_key。常用 typediscovered_on、depends_on、leads_to、enables、exploits。保存时替换全部边。",
"factLinksLabel": "关系边(from → 本事实",
"factLinksPlaceholder": "discovered_on: target/primary_domain\nexploits: exploit/upload-rce",
"factLinksHint": "每行一条:type: source_fact_key(来源 → 当前事实)。常用 typediscovered_on、depends_on、leads_to、enables、exploits。保存时替换全部关系边。",
"factIncomingLinksLabel": "入边(只读)",
"factIncomingLinksHint": "由来源事实的出边产生。例如 finding 的 discovered_on → target/*,在目标上会显示为入边;请编辑来源事实的出边。",
"factIncomingLinksEmpty": "暂无入边",
"graphEdgeFromSelf": "本节点指出",
"graphEdgeToSelf": "指向本节点",
"linksColumn": "关系",
"linkCountsTitle": "出边数 / 入边数",
"graphConnect": "连边",
@@ -284,7 +289,7 @@
"graphConnectFailed": "创建边失败",
"graphConnectSuccess": "边已创建",
"graphEdgesTitle": "关系边",
"graphEdgesHint": "点击图中连线可定位;误连可在此删除。",
"graphEdgesHint": "箭头方向与数据库/编辑弹窗一致(source → target);点击连线可定位。",
"graphEdgesEmpty": "暂无关系边",
"graphEdgeOutgoing": "出边",
"graphEdgeIncoming": "入边",
+54 -1
View File
@@ -45,12 +45,20 @@
return { typeLabel: '目标', typeEn: 'TARGET', accent: '#4F46E5', bgEnd: '#F5F3FF', icon: 'target' };
case 'finding':
return { typeLabel: '发现', typeEn: 'FINDING', accent: '#E11D48', bgEnd: '#FFF1F2', icon: 'vulnerability' };
case 'exploit':
return { typeLabel: '利用', typeEn: 'EXPLOIT', accent: '#B45309', bgEnd: '#FFFBEB', icon: 'vulnerability' };
case 'vulnerability':
return { typeLabel: '漏洞', typeEn: 'VULN', accent: '#BE123C', bgEnd: '#FFF1F2', icon: 'vulnerability' };
case 'auth':
return { typeLabel: '认证', typeEn: 'AUTH', accent: '#0D9488', bgEnd: '#F0FDFA', icon: 'default' };
case 'infra':
return { typeLabel: '基础设施', typeEn: 'INFRA', accent: '#64748B', bgEnd: '#F8FAFC', icon: 'default' };
case 'chain':
return { typeLabel: '攻击链', typeEn: 'CHAIN', accent: '#7C3AED', bgEnd: '#F5F3FF', icon: 'vulnerability' };
case 'poc':
return { typeLabel: 'POC', typeEn: 'POC', accent: '#C2410C', bgEnd: '#FFEDD5', icon: 'vulnerability' };
case 'business':
return { typeLabel: '业务', typeEn: 'BUSINESS', accent: '#0369A1', bgEnd: '#F0F9FF', icon: 'default' };
case 'missing':
return { typeLabel: '缺失', typeEn: 'MISSING', accent: '#CBD5E1', bgEnd: '#F1F5F9', icon: 'default' };
default:
@@ -261,6 +269,24 @@
}
}
// ELK 分层(仅影响节点纵向位置,不修改边的 source/target
function pathGraphNodeLayer(type, factKey) {
const key = (factKey || '').toLowerCase();
if (key.startsWith('vuln:')) return '4';
if (key.startsWith('target/')) return '0';
if (key.startsWith('infra/') || key.startsWith('auth/') || key.startsWith('business/')) return '1';
if (key.startsWith('exploit/') || key.startsWith('evidence/')) return '3';
if (key.startsWith('poc/')) return '3';
if (key.startsWith('chain/')) return '2';
if (key.startsWith('finding/')) return '2';
const t = (type || '').toLowerCase();
if (t === 'target') return '0';
if (t === 'infra' || t === 'auth') return '1';
if (t === 'exploit' || t === 'poc') return '3';
if (t === 'chain' || t === 'finding' || t === 'vulnerability') return '2';
return '2';
}
function applyElkLayout(validEdges, isComplex) {
const layoutOptions = {
name: 'breadthfirst',
@@ -290,7 +316,15 @@
const n = _cy ? _cy.getElementById(node.id) : null;
const w = n.length ? n.data('nodeWidth') : node.type === 'target' ? CARD_TARGET_W : CARD_MIN_W;
const h = n.length ? n.data('nodeHeight') : CARD_MIN_H;
return { id: node.id, width: w, height: h };
const nodeKey = node.fact_key || node.id;
return {
id: node.id,
width: w,
height: h,
layoutOptions: {
'org.eclipse.elk.layered.layering.layerId': pathGraphNodeLayer(node.type, nodeKey),
},
};
}),
edges: validEdges.map((edge) => ({
id: edge.id,
@@ -543,6 +577,23 @@
}
}
/** 与后端 GraphNodeType 一致:优先 fact_key 前缀,再 category/type。 */
function resolveGraphNodeType(node) {
if (!node) return 'note';
const key = String(node.fact_key || node.id || '').toLowerCase();
if (key.startsWith('target/')) return 'target';
if (key.startsWith('exploit/') || key.startsWith('poc/') || key.startsWith('evidence/')) return 'exploit';
if (key.startsWith('chain/')) return 'chain';
if (key.startsWith('finding/')) return 'finding';
if (key.startsWith('auth/')) return 'auth';
if (key.startsWith('infra/')) return 'infra';
if (key.startsWith('business/')) return 'business';
if (key.startsWith('vuln:')) return 'vulnerability';
const t = String(node.type || node.category || 'note').toLowerCase();
if (t === 'vuln') return 'vulnerability';
return t || 'note';
}
global.ProjectFactGraph = {
render,
destroy,
@@ -551,5 +602,7 @@
setConnectMode,
selectEdge,
clearEdgeSelection,
nodeTheme,
resolveGraphNodeType,
};
})(typeof window !== 'undefined' ? window : globalThis);
+27 -17
View File
@@ -64,7 +64,7 @@ Host: ...
## 关联
- related_vulnerability_id: <可选>
- 依赖事实: <fact_key,如 auth/session_cookie>
- 结构化边(自动同步):
- 结构化关系边(自动同步links 文本格式 type: source_fact_key:
- discovered_on: target/primary_domain
## 备注与不确定性
@@ -798,13 +798,14 @@ async function handleGraphConnectNodePick(factKey) {
loadProjectFacts();
}
function formatOutgoingLinksForModal(links) {
function formatIncomingLinksForModal(links) {
if (!links || !links.length) return '';
return links
.map((e) => `${e.edge_type || e.type}: ${e.target_fact_key || e.to}`)
.map((e) => `${e.edge_type || e.type}: ${e.source_fact_key || e.from}`)
.join('\n');
}
async function loadProjectFactGraph() {
const container = document.getElementById('project-fact-graph-container');
const statsEl = document.getElementById('project-fact-graph-stats');
@@ -887,9 +888,9 @@ function renderGraphEdgesListHtml(factKey, graphData, selectedEdgeId) {
return edges
.map((e) => {
const isOut = e.source === factKey;
const dirLabel = isOut ? tp('projects.graphEdgeOutgoing') : tp('projects.graphEdgeIncoming');
const other = isOut ? e.target : e.source;
const arrow = isOut ? '→' : '';
const dirLabel = isOut ? tp('projects.graphEdgeFromSelf') : tp('projects.graphEdgeToSelf');
const src = e.source || '';
const tgt = e.target || '';
const selected = e.id === selectedEdgeId ? ' is-selected' : '';
const synthetic = isSyntheticGraphEdge(e);
const deleteBtn = synthetic
@@ -898,8 +899,7 @@ function renderGraphEdgesListHtml(factKey, graphData, selectedEdgeId) {
return `<div class="project-fact-graph-edge-item${selected}" data-edge-id="${escapeHtml(e.id)}" onclick="focusProjectFactGraphEdge(${JSON.stringify(e.id)})">
<span class="project-fact-graph-edge-dir">${escapeHtml(dirLabel)}</span>
<span class="project-fact-graph-edge-type">${escapeHtml(e.type || '')}</span>
<span class="project-fact-graph-edge-arrow">${arrow}</span>
<span class="project-fact-graph-edge-peer" title="${escapeHtml(other)}">${escapeHtml(other)}</span>
<span class="project-fact-graph-edge-peer" title="${escapeHtml(src + ' → ' + tgt)}">${escapeHtml(src)}${escapeHtml(tgt)}</span>
${deleteBtn}
</div>`;
})
@@ -935,17 +935,25 @@ function showProjectFactGraphNode(factKey, graphData, selectedEdgeId) {
if (!sidebar || !titleEl || !metaEl) return;
titleEl.textContent = factKey;
if (categoryEl) {
const cat = node?.category || node?.type || '';
categoryEl.textContent = cat;
categoryEl.hidden = !cat;
categoryEl.className = 'project-fact-graph-node-category project-fact-graph-node-category--' + (cat || 'note');
const visualType =
typeof ProjectFactGraph !== 'undefined' && ProjectFactGraph.resolveGraphNodeType
? ProjectFactGraph.resolveGraphNodeType(node)
: node?.type || node?.category || 'note';
const theme =
typeof ProjectFactGraph !== 'undefined' && ProjectFactGraph.nodeTheme
? ProjectFactGraph.nodeTheme(visualType)
: { typeEn: String(visualType).toUpperCase(), typeLabel: visualType };
categoryEl.textContent = theme.typeEn || String(visualType).toUpperCase();
categoryEl.hidden = false;
categoryEl.className = 'project-fact-graph-node-category project-fact-graph-node-category--' + visualType;
categoryEl.title = theme.typeLabel || visualType;
}
const conf = node?.confidence || '';
const label = node?.label || '';
if (label || conf) {
const summary = (node?.summary || node?.label || '').trim();
if (summary || conf) {
const parts = [];
if (label) {
parts.push(`<span class="project-fact-graph-node-summary">${escapeHtml(label)}</span>`);
if (summary) {
parts.push(`<span class="project-fact-graph-node-summary">${escapeHtml(summary)}</span>`);
}
if (conf) {
parts.push(formatConfidenceBadge(conf));
@@ -1806,6 +1814,8 @@ function resetFactModalForm() {
if (rel) rel.value = '';
const linksEl = document.getElementById('fact-modal-links');
if (linksEl) linksEl.value = '';
const incomingWrap = document.getElementById('fact-modal-incoming-links-wrap');
if (incomingWrap) incomingWrap.hidden = true;
updateFactFormHints();
}
@@ -1838,7 +1848,7 @@ function fillFactModalForm(f) {
const rel = document.getElementById('fact-modal-related-vuln');
if (rel) rel.value = f.related_vulnerability_id || '';
const linksEl = document.getElementById('fact-modal-links');
if (linksEl) linksEl.value = formatOutgoingLinksForModal(f.outgoing_links);
if (linksEl) linksEl.value = formatIncomingLinksForModal(f.incoming_links);
const pinEl = document.getElementById('fact-modal-pinned');
if (pinEl) pinEl.checked = !!f.pinned;
updateFactFormHints();
+14 -12
View File
@@ -1607,7 +1607,7 @@
<circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/>
<path d="M12 10v6M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span data-i18n="projects.graphToolbarHint">攻击路径图:展示 target → findingexploit 因果关系;虚线边表示待确认。点击节点查看详情。</span>
<span data-i18n="projects.graphToolbarHint">攻击路径图箭头与事实存储方向一致(source → target);节点按 target→infra→findingexploit 分层排布。虚线边待确认。</span>
</p>
<div class="projects-fact-toolbar-filters projects-graph-toolbar-row">
<label class="projects-fact-filter-field">
@@ -1637,12 +1637,6 @@
<button type="button" class="projects-graph-action-btn projects-graph-action-btn--connect" id="project-graph-connect-btn" onclick="toggleProjectFactGraphConnectMode()" data-i18n="projects.graphConnect">连边</button>
</div>
</div>
<div class="projects-graph-legend" aria-hidden="true">
<span class="projects-graph-legend-item"><i style="--legend-color:#4F46E5"></i><span data-i18n="projects.graphLegendDiscovered">discovered_on</span></span>
<span class="projects-graph-legend-item"><i style="--legend-color:#64748B"></i><span data-i18n="projects.graphLegendLeads">leads_to</span></span>
<span class="projects-graph-legend-item"><i style="--legend-color:#DC2626"></i><span data-i18n="projects.graphLegendExploits">exploits</span></span>
<span class="projects-graph-legend-item projects-graph-legend-item--dashed"><i style="--legend-color:#94A3B8"></i><span data-i18n="projects.graphLegendTentative">待确认</span></span>
</div>
</div>
<div class="project-fact-graph-layout">
<div id="project-fact-graph-container" class="project-fact-graph-container"></div>
@@ -1659,7 +1653,7 @@
<p id="project-fact-graph-node-meta" class="project-fact-graph-node-meta"></p>
<div id="project-fact-graph-edges-wrap" class="project-fact-graph-edges-wrap" hidden>
<h5 class="project-fact-graph-edges-title" data-i18n="projects.graphEdgesTitle">关系边</h5>
<p class="project-fact-graph-edges-hint" data-i18n="projects.graphEdgesHint">点击图中连线可定位;误连可在此删除</p>
<p class="project-fact-graph-edges-hint" data-i18n="projects.graphEdgesHint">箭头方向与数据库/编辑弹窗一致(source → target);点击连线可定位</p>
<div id="project-fact-graph-edges-list" class="project-fact-graph-edges-list"></div>
</div>
<div class="project-fact-graph-sidebar-actions">
@@ -1668,7 +1662,15 @@
</div>
</aside>
</div>
<div id="project-fact-graph-stats" class="project-fact-graph-stats"></div>
<div class="project-fact-graph-footer">
<div id="project-fact-graph-stats" class="project-fact-graph-stats"></div>
<div class="projects-graph-legend" aria-hidden="true">
<span class="projects-graph-legend-item"><i style="--legend-color:#4F46E5"></i><span data-i18n="projects.graphLegendDiscovered">discovered_on</span></span>
<span class="projects-graph-legend-item"><i style="--legend-color:#64748B"></i><span data-i18n="projects.graphLegendLeads">leads_to</span></span>
<span class="projects-graph-legend-item"><i style="--legend-color:#DC2626"></i><span data-i18n="projects.graphLegendExploits">exploits</span></span>
<span class="projects-graph-legend-item projects-graph-legend-item--dashed"><i style="--legend-color:#94A3B8"></i><span data-i18n="projects.graphLegendTentative">待确认</span></span>
</div>
</div>
</div>
<div id="project-panel-conversations" class="projects-panel" role="tabpanel" hidden>
<div class="projects-panel-toolbar projects-panel-toolbar--hint">
@@ -4418,9 +4420,9 @@
<input type="text" id="fact-modal-related-vuln" class="form-input" placeholder="可选" data-i18n="projects.optional" data-i18n-attr="placeholder">
</div>
<div class="form-group">
<label for="fact-modal-links" data-i18n="projects.factLinksLabel">关系边(出边</label>
<textarea id="fact-modal-links" class="form-input" rows="4" placeholder="discovered_on: target/primary_domain&#10;leads_to: finding/swagger" data-i18n="projects.factLinksPlaceholder" data-i18n-attr="placeholder"></textarea>
<p class="projects-field-hint" data-i18n="projects.factLinksHint">每行一条:type: fact_key。常用 typediscovered_on、depends_on、leads_to、enables、exploits。保存时替换全部边。</p>
<label for="fact-modal-links" data-i18n="projects.factLinksLabel">关系边(from → 本事实</label>
<textarea id="fact-modal-links" class="form-input" rows="4" placeholder="discovered_on: target/primary_domain&#10;exploits: exploit/upload-rce" data-i18n="projects.factLinksPlaceholder" data-i18n-attr="placeholder"></textarea>
<p class="projects-field-hint" data-i18n="projects.factLinksHint">每行一条:type: source_fact_key(来源 → 当前事实)。常用 typediscovered_on、depends_on、leads_to、enables、exploits。保存时替换全部关系边。</p>
</div>
</div>
<div class="projects-modal-footer">