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
+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();