mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-21 05:20:08 +02:00
Add files via upload
This commit is contained in:
@@ -24536,17 +24536,30 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
padding-bottom: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
#project-panel-graph .projects-graph-toolbar {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
#project-panel-graph .project-fact-graph-layout {
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#project-panel-graph .project-fact-graph-container {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
}
|
||||
#project-panel-graph .project-fact-graph-footer {
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
margin: 0;
|
||||
padding: 10px 0 12px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eef2f7;
|
||||
}
|
||||
.projects-graph-toolbar-row {
|
||||
align-items: flex-end;
|
||||
@@ -24607,7 +24620,27 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px 12px;
|
||||
gap: 8px 14px;
|
||||
}
|
||||
.projects-graph-legend-group {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 6px 10px;
|
||||
}
|
||||
.projects-graph-legend-heading {
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.projects-graph-legend-divider {
|
||||
display: inline-block;
|
||||
width: 1px;
|
||||
height: 18px;
|
||||
background: #e2e8f0;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.projects-graph-legend-item {
|
||||
display: inline-flex;
|
||||
@@ -24616,27 +24649,40 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
font-size: 0.75rem;
|
||||
color: #64748b;
|
||||
}
|
||||
.projects-graph-legend-item i {
|
||||
.projects-graph-legend-item--edge i {
|
||||
display: inline-block;
|
||||
width: 22px;
|
||||
height: 0;
|
||||
border-top: 2.5px solid var(--legend-color, #cbd5e1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
.projects-graph-legend-item--dashed i {
|
||||
.projects-graph-legend-item--edge.projects-graph-legend-item--dashed i {
|
||||
border-top-style: dashed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.projects-graph-legend-item--node i {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1.5px solid var(--legend-color, #cbd5e1);
|
||||
border-radius: 4px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, var(--legend-bg, #f8fafc) 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
.projects-graph-legend-item--node-dashed i {
|
||||
border-style: dashed;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.project-fact-graph-layout {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 480px;
|
||||
min-height: 0;
|
||||
align-items: stretch;
|
||||
}
|
||||
.project-fact-graph-container {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
min-height: 480px;
|
||||
min-height: 240px;
|
||||
border: 1px solid var(--border-color, #e2e8f0);
|
||||
border-radius: 14px;
|
||||
background-color: #f8fafc;
|
||||
@@ -24781,8 +24827,8 @@ 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,
|
||||
.project-fact-graph-node-category--vulnerability { color: #be123c; background: #fff1f2; border-color: #fecdd3; }
|
||||
.project-fact-graph-node-category--finding { color: #be123c; background: #fff1f2; border-color: #fecdd3; }
|
||||
.project-fact-graph-node-category--vulnerability { color: #7e22ce; background: #f5f3ff; border-color: #ddd6fe; }
|
||||
.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; }
|
||||
@@ -24831,6 +24877,13 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
min-width: 0;
|
||||
color: #475569;
|
||||
}
|
||||
.project-fact-graph-node-vuln-hint {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
color: #64748b;
|
||||
}
|
||||
.project-fact-graph-edges-wrap {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -24991,6 +25044,7 @@ button.chat-files-dropdown-item:hover:not(:disabled) {
|
||||
gap: 8px 12px;
|
||||
margin: 10px 0 0;
|
||||
flex: 0 0 auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.project-fact-graph-stats {
|
||||
display: flex;
|
||||
|
||||
@@ -280,6 +280,14 @@
|
||||
"graphStats": "Nodes: {{nodes}} | Edges: {{edges}}",
|
||||
"graphStatsNodes": "Nodes",
|
||||
"graphStatsEdges": "Edges",
|
||||
"graphLegendNodes": "Nodes",
|
||||
"graphLegendEdges": "Edges",
|
||||
"graphLegendNodeTarget": "TARGET",
|
||||
"graphLegendNodeInfra": "INFRA",
|
||||
"graphLegendNodeFinding": "FINDING",
|
||||
"graphLegendNodeVuln": "VULN",
|
||||
"graphLegendNodeExploit": "EXPLOIT",
|
||||
"graphLegendNodeMissing": "MISSING",
|
||||
"graphLegendDiscovered": "discovered_on",
|
||||
"graphLegendLeads": "leads_to",
|
||||
"graphLegendExploits": "exploits",
|
||||
@@ -310,6 +318,8 @@
|
||||
"graphEdgeDeleteFailed": "Failed to delete edge",
|
||||
"graphEdgeDeleteSuccess": "Edge deleted",
|
||||
"graphDeleteEdge": "Delete",
|
||||
"viewVulnerability": "View vulnerability",
|
||||
"graphVulnSidebarHint": "Linked vulnerability node. Use the button below to open it in Vulnerability Management.",
|
||||
"promoteAttackChain": "Promote chain",
|
||||
"promoteAttackChainTitle": "Promote conversation attack chain to project facts",
|
||||
"confirmPromoteAttackChain": "Promote this conversation's attack chain into the project? Facts and edges will be created or updated.",
|
||||
|
||||
@@ -268,6 +268,14 @@
|
||||
"graphStats": "节点: {{nodes}} | 边: {{edges}}",
|
||||
"graphStatsNodes": "节点",
|
||||
"graphStatsEdges": "边",
|
||||
"graphLegendNodes": "节点",
|
||||
"graphLegendEdges": "连线",
|
||||
"graphLegendNodeTarget": "TARGET · 目标",
|
||||
"graphLegendNodeInfra": "INFRA · 基础设施",
|
||||
"graphLegendNodeFinding": "FINDING · 发现",
|
||||
"graphLegendNodeVuln": "VULN · 漏洞",
|
||||
"graphLegendNodeExploit": "EXPLOIT · 利用",
|
||||
"graphLegendNodeMissing": "MISSING · 缺失",
|
||||
"graphLegendDiscovered": "discovered_on",
|
||||
"graphLegendLeads": "leads_to",
|
||||
"graphLegendExploits": "exploits",
|
||||
@@ -298,6 +306,8 @@
|
||||
"graphEdgeDeleteFailed": "删除边失败",
|
||||
"graphEdgeDeleteSuccess": "边已删除",
|
||||
"graphDeleteEdge": "删边",
|
||||
"viewVulnerability": "查看漏洞",
|
||||
"graphVulnSidebarHint": "关联漏洞节点,点击下方按钮在漏洞管理中查看详情。",
|
||||
"promoteAttackChain": "沉淀攻击链",
|
||||
"promoteAttackChainTitle": "将对话攻击链沉淀为项目事实与边",
|
||||
"confirmPromoteAttackChain": "将该对话的攻击链沉淀到本项目?会创建/更新事实与关系边。",
|
||||
|
||||
+103
-31
@@ -9,6 +9,7 @@
|
||||
let _graphData = null;
|
||||
let _onNodeSelect = null;
|
||||
let _onEdgeSelect = null;
|
||||
let _resizeObs = null;
|
||||
|
||||
const EDGE_COLORS = {
|
||||
discovered_on: '#4F46E5',
|
||||
@@ -30,25 +31,29 @@
|
||||
const CARD_MIN_W = 300;
|
||||
const CARD_TARGET_W = 360;
|
||||
const CARD_MIN_H = 88;
|
||||
const CARD_MAX_H = 152;
|
||||
const CARD_MAX_H = 176;
|
||||
const CARD_HEADER_FS = 11;
|
||||
const CARD_HEADER_LH = 16;
|
||||
const CARD_KEY_FS = 10;
|
||||
const CARD_KEY_LH = 14;
|
||||
const CARD_SUMMARY_FS = 13;
|
||||
const CARD_SUMMARY_LH = 18;
|
||||
const CARD_SECTION_GAP = 6;
|
||||
const CARD_FONT =
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif';
|
||||
const CARD_KEY_FONT =
|
||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace';
|
||||
|
||||
function nodeTheme(type) {
|
||||
switch (type) {
|
||||
case 'target':
|
||||
return { typeLabel: '目标', typeEn: 'TARGET', accent: '#4F46E5', bgEnd: '#F5F3FF', icon: 'target' };
|
||||
case 'finding':
|
||||
return { typeLabel: '发现', typeEn: 'FINDING', accent: '#E11D48', bgEnd: '#FFF1F2', icon: 'vulnerability' };
|
||||
return { typeLabel: '发现', typeEn: 'FINDING', accent: '#E11D48', bgEnd: '#FFF1F2', icon: 'finding', cardStyle: 'default' };
|
||||
case 'exploit':
|
||||
return { typeLabel: '利用', typeEn: 'EXPLOIT', accent: '#B45309', bgEnd: '#FFFBEB', icon: 'vulnerability' };
|
||||
return { typeLabel: '利用', typeEn: 'EXPLOIT', accent: '#B45309', bgEnd: '#FFFBEB', icon: 'vulnerability', cardStyle: 'default' };
|
||||
case 'vulnerability':
|
||||
return { typeLabel: '漏洞', typeEn: 'VULN', accent: '#BE123C', bgEnd: '#FFF1F2', icon: 'vulnerability' };
|
||||
return { typeLabel: '漏洞', typeEn: 'VULN', accent: '#9333EA', bgEnd: '#F5F3FF', icon: 'vuln', cardStyle: 'default' };
|
||||
case 'auth':
|
||||
return { typeLabel: '认证', typeEn: 'AUTH', accent: '#0D9488', bgEnd: '#F0FDFA', icon: 'default' };
|
||||
case 'infra':
|
||||
@@ -148,19 +153,24 @@
|
||||
return nodeWidth - CARD_TEXT_X - CARD_PAD - CARD_TEXT_PAD_RIGHT;
|
||||
}
|
||||
|
||||
function computeNodeLayout(type, summary, statusBadge, theme) {
|
||||
function computeNodeLayout(type, summary, statusBadge, theme, factKey) {
|
||||
const width = type === 'target' ? CARD_TARGET_W : CARD_MIN_W;
|
||||
const textW = cardTextWidth(width);
|
||||
const t = theme || nodeTheme(type);
|
||||
const headerLines = wrapTextLines(buildHeaderText(t, statusBadge), textW, CARD_HEADER_FS, 2, true);
|
||||
const summaryLines = wrapTextLines(summary, textW, CARD_SUMMARY_FS, 4, true);
|
||||
const keyText = String(factKey || '').trim();
|
||||
const keyLines = keyText ? wrapTextLines(keyText, textW, CARD_KEY_FS, 2, false) : [];
|
||||
const summaryLines = wrapTextLines(summary, textW, CARD_SUMMARY_FS, keyLines.length ? 3 : 4, true);
|
||||
const keyBlockHeight = keyLines.length
|
||||
? CARD_SECTION_GAP + keyLines.length * CARD_KEY_LH + CARD_SECTION_GAP
|
||||
: CARD_SECTION_GAP;
|
||||
const height = Math.min(
|
||||
CARD_MAX_H,
|
||||
Math.max(
|
||||
CARD_MIN_H,
|
||||
CARD_PAD +
|
||||
headerLines.length * CARD_HEADER_LH +
|
||||
CARD_SECTION_GAP +
|
||||
keyBlockHeight +
|
||||
summaryLines.length * CARD_SUMMARY_LH +
|
||||
CARD_PAD,
|
||||
),
|
||||
@@ -169,8 +179,11 @@
|
||||
width,
|
||||
height,
|
||||
headerLines,
|
||||
keyLines,
|
||||
summaryLines,
|
||||
searchLabel: headerLines.join(' ') + '\n' + summaryLines.join(' '),
|
||||
searchLabel: [headerLines.join(' '), keyLines.join(' '), summaryLines.join(' ')]
|
||||
.filter(Boolean)
|
||||
.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -183,6 +196,21 @@
|
||||
`<circle cx="12" cy="12" r="2.5" fill="${color}"/></g>`
|
||||
);
|
||||
}
|
||||
if (kind === 'finding') {
|
||||
return (
|
||||
`<g transform="translate(${x}, ${y}) scale(${scale})">` +
|
||||
`<circle cx="10" cy="10" r="6" fill="none" stroke="${color}" stroke-width="2"/>` +
|
||||
`<line x1="14.5" y1="14.5" x2="19" y2="19" stroke="${color}" stroke-width="2" stroke-linecap="round"/></g>`
|
||||
);
|
||||
}
|
||||
if (kind === 'vuln') {
|
||||
return (
|
||||
`<g transform="translate(${x}, ${y}) scale(${scale})">` +
|
||||
`<path d="M12 2.5l7.5 3v6.2c0 4.6-3.1 8.1-7.5 9.3-4.4-1.2-7.5-4.7-7.5-9.3V5.5z" fill="${color}" fill-opacity="0.12" stroke="${color}" stroke-width="2"/>` +
|
||||
`<line x1="12" y1="8.5" x2="12" y2="12.5" stroke="${color}" stroke-width="2" stroke-linecap="round"/>` +
|
||||
`<circle cx="12" cy="15.5" r="1.1" fill="${color}"/></g>`
|
||||
);
|
||||
}
|
||||
if (kind === 'vulnerability') {
|
||||
return (
|
||||
`<g transform="translate(${x}, ${y}) scale(${scale})">` +
|
||||
@@ -198,7 +226,7 @@
|
||||
}
|
||||
|
||||
function buildNodeCardSvgUrl(theme, layout, confidence) {
|
||||
const { width, height, headerLines, summaryLines } = layout;
|
||||
const { width, height, headerLines, keyLines, summaryLines } = layout;
|
||||
const accent = theme.accent;
|
||||
const bgEnd = theme.bgEnd;
|
||||
const conf = (confidence || '').toLowerCase();
|
||||
@@ -207,7 +235,14 @@
|
||||
const iconX = CARD_PAD;
|
||||
const iconY = (height - CARD_ICON) / 2;
|
||||
const headerY = CARD_PAD + CARD_HEADER_FS;
|
||||
const summaryY = CARD_PAD + headerLines.length * CARD_HEADER_LH + CARD_SECTION_GAP + CARD_SUMMARY_FS;
|
||||
const keyY = CARD_PAD + headerLines.length * CARD_HEADER_LH + CARD_SECTION_GAP + CARD_KEY_FS;
|
||||
const summaryY =
|
||||
CARD_PAD +
|
||||
headerLines.length * CARD_HEADER_LH +
|
||||
(keyLines.length
|
||||
? CARD_SECTION_GAP + keyLines.length * CARD_KEY_LH + CARD_SECTION_GAP
|
||||
: CARD_SECTION_GAP) +
|
||||
CARD_SUMMARY_FS;
|
||||
|
||||
const stroke = isTentative
|
||||
? `stroke="${accent}" stroke-width="1.5" stroke-dasharray="8 5" stroke-opacity="0.9"`
|
||||
@@ -220,6 +255,13 @@
|
||||
)
|
||||
.join('');
|
||||
|
||||
const keySvg = keyLines
|
||||
.map(
|
||||
(line, i) =>
|
||||
`<text x="${CARD_TEXT_X}" y="${keyY + i * CARD_KEY_LH}" font-size="${CARD_KEY_FS}" font-weight="500" fill="#64748b" font-family='${CARD_KEY_FONT}'>${escapeXml(line)}</text>`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
const summarySvg = summaryLines
|
||||
.map(
|
||||
(line, i) =>
|
||||
@@ -238,7 +280,7 @@
|
||||
`<g${isDeprecated ? ' opacity="0.55"' : ''}>` +
|
||||
`<rect x="0.75" y="0.75" width="${width - 1.5}" height="${height - 1.5}" rx="12" fill="url(#bg)" ${stroke}/>` +
|
||||
svgIconGroup(theme.icon, accent, iconX, iconY) +
|
||||
`<g clip-path="url(#textClip)">${headerSvg}${summarySvg}</g>` +
|
||||
`<g clip-path="url(#textClip)">${headerSvg}${keySvg}${summarySvg}</g>` +
|
||||
`</g></svg>`;
|
||||
|
||||
try {
|
||||
@@ -249,6 +291,10 @@
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (_resizeObs) {
|
||||
_resizeObs.disconnect();
|
||||
_resizeObs = null;
|
||||
}
|
||||
if (_cy) {
|
||||
_cy.destroy();
|
||||
_cy = null;
|
||||
@@ -256,9 +302,28 @@
|
||||
_graphData = null;
|
||||
}
|
||||
|
||||
function observeContainerResize(container) {
|
||||
if (_resizeObs) {
|
||||
_resizeObs.disconnect();
|
||||
_resizeObs = null;
|
||||
}
|
||||
if (!container || typeof ResizeObserver === 'undefined') return;
|
||||
_resizeObs = new ResizeObserver(() => {
|
||||
if (_cy) {
|
||||
try {
|
||||
_cy.resize();
|
||||
} catch (e) {
|
||||
console.warn('graph resize', e);
|
||||
}
|
||||
}
|
||||
});
|
||||
_resizeObs.observe(container);
|
||||
}
|
||||
|
||||
function centerGraph() {
|
||||
if (!_cy) return;
|
||||
try {
|
||||
_cy.resize();
|
||||
_cy.fit(undefined, 56);
|
||||
if (_cy.zoom() < 0.65) {
|
||||
_cy.zoom(0.65);
|
||||
@@ -273,17 +338,13 @@
|
||||
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 === 'infra' || t === 'auth' || t === 'business') return '1';
|
||||
if (t === 'exploit' || t === 'poc') return '3';
|
||||
if (t === 'chain' || t === 'finding' || t === 'vulnerability') return '2';
|
||||
if (t === 'vulnerability' || t === 'vuln') return '3';
|
||||
if (t === 'chain' || t === 'finding') return '2';
|
||||
if (t === 'note') return '2';
|
||||
return '2';
|
||||
}
|
||||
|
||||
@@ -408,16 +469,19 @@
|
||||
|
||||
nodes.forEach((node) => {
|
||||
nodeIds.add(node.id);
|
||||
const theme = nodeTheme(node.type || node.category || 'note');
|
||||
const label = node.label || node.fact_key || node.id;
|
||||
const visualType = resolveGraphNodeType(node);
|
||||
const theme = nodeTheme(visualType);
|
||||
const factKey = node.fact_key || node.id;
|
||||
const summary = (node.summary || node.label || '').trim() || '—';
|
||||
const statusBadge = buildStatusBadge(node.confidence);
|
||||
const layout = computeNodeLayout(node.type || node.category || 'note', label, statusBadge, theme);
|
||||
const layout = computeNodeLayout(visualType, summary, statusBadge, theme, factKey);
|
||||
elements.push({
|
||||
data: {
|
||||
id: node.id,
|
||||
label: layout.searchLabel,
|
||||
factKey: node.fact_key || node.id,
|
||||
type: node.type || 'note',
|
||||
category: node.category || '',
|
||||
type: visualType,
|
||||
typeLabel: theme.typeLabel,
|
||||
typeEn: theme.typeEn,
|
||||
accentColor: theme.accent,
|
||||
@@ -529,6 +593,7 @@
|
||||
});
|
||||
|
||||
applyElkLayout(validEdges, isComplex);
|
||||
observeContainerResize(container);
|
||||
return _cy;
|
||||
}
|
||||
|
||||
@@ -577,21 +642,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** 与后端 GraphNodeType 一致:优先 fact_key 前缀,再 category/type。 */
|
||||
/** 与后端 GraphNodeType 一致:优先 category,vuln: 合成节点例外;无 category 时回退 type/key。 */
|
||||
function resolveGraphNodeType(node) {
|
||||
if (!node) return 'note';
|
||||
const key = String(node.fact_key || node.id || '').toLowerCase();
|
||||
if (key.startsWith('vuln:')) return 'vulnerability';
|
||||
const cat = String(node.category || '').toLowerCase();
|
||||
if (cat) {
|
||||
if (cat === 'vuln') return 'vulnerability';
|
||||
if (cat === 'missing') return 'missing';
|
||||
return cat;
|
||||
}
|
||||
const t = String(node.type || '').toLowerCase();
|
||||
if (t === 'vuln') return 'vulnerability';
|
||||
if (t) return t;
|
||||
if (key.startsWith('target/')) return 'target';
|
||||
if (key.startsWith('exploit/') || key.startsWith('poc/') || key.startsWith('evidence/')) return 'exploit';
|
||||
if (key.startsWith('exploit/') || key.startsWith('evidence/')) return 'exploit';
|
||||
if (key.startsWith('poc/')) return 'poc';
|
||||
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';
|
||||
if (key.startsWith('infra/') || key.startsWith('business/')) return 'infra';
|
||||
return 'note';
|
||||
}
|
||||
|
||||
global.ProjectFactGraph = {
|
||||
|
||||
@@ -920,20 +920,31 @@ function renderProjectFactGraphEdges(factKey, graphData, selectedEdgeId) {
|
||||
if (!edges.length) wrap.hidden = false;
|
||||
}
|
||||
|
||||
function graphVulnIdFromKey(factKey) {
|
||||
const key = String(factKey || '');
|
||||
if (!key.startsWith('vuln:')) return null;
|
||||
return key.slice(5);
|
||||
}
|
||||
|
||||
function showProjectFactGraphNode(factKey, graphData, selectedEdgeId) {
|
||||
if (!factKey || String(factKey).startsWith('vuln:')) {
|
||||
if (!factKey) {
|
||||
closeProjectFactGraphSidebar();
|
||||
return;
|
||||
}
|
||||
_selectedGraphFactKey = factKey;
|
||||
_selectedGraphEdgeId = selectedEdgeId || null;
|
||||
const node = (graphData?.nodes || []).find((n) => n.fact_key === factKey || n.id === factKey);
|
||||
const vulnId = graphVulnIdFromKey(factKey);
|
||||
const isVulnNode = !!vulnId;
|
||||
const sidebar = document.getElementById('project-fact-graph-sidebar');
|
||||
const titleEl = document.getElementById('project-fact-graph-node-title');
|
||||
const metaEl = document.getElementById('project-fact-graph-node-meta');
|
||||
const categoryEl = document.getElementById('project-fact-graph-node-category');
|
||||
const detailBtn = document.getElementById('project-fact-graph-detail-btn');
|
||||
const editBtn = document.getElementById('project-fact-graph-edit-btn');
|
||||
if (!sidebar || !titleEl || !metaEl) return;
|
||||
titleEl.textContent = factKey;
|
||||
titleEl.textContent = isVulnNode ? vulnId : factKey;
|
||||
titleEl.title = isVulnNode ? vulnId : factKey;
|
||||
if (categoryEl) {
|
||||
const visualType =
|
||||
typeof ProjectFactGraph !== 'undefined' && ProjectFactGraph.resolveGraphNodeType
|
||||
@@ -950,11 +961,16 @@ function showProjectFactGraphNode(factKey, graphData, selectedEdgeId) {
|
||||
}
|
||||
const conf = node?.confidence || '';
|
||||
const summary = (node?.summary || node?.label || '').trim();
|
||||
if (summary || conf) {
|
||||
if (summary || conf || isVulnNode) {
|
||||
const parts = [];
|
||||
if (summary) {
|
||||
parts.push(`<span class="project-fact-graph-node-summary">${escapeHtml(summary)}</span>`);
|
||||
}
|
||||
if (isVulnNode) {
|
||||
parts.push(
|
||||
`<span class="project-fact-graph-node-vuln-hint">${escapeHtml(tp('projects.graphVulnSidebarHint'))}</span>`,
|
||||
);
|
||||
}
|
||||
if (conf) {
|
||||
parts.push(formatConfidenceBadge(conf));
|
||||
}
|
||||
@@ -962,6 +978,12 @@ function showProjectFactGraphNode(factKey, graphData, selectedEdgeId) {
|
||||
} else {
|
||||
metaEl.textContent = '';
|
||||
}
|
||||
if (detailBtn) {
|
||||
detailBtn.textContent = isVulnNode ? tp('projects.viewVulnerability') : tp('projects.details');
|
||||
}
|
||||
if (editBtn) {
|
||||
editBtn.hidden = isVulnNode;
|
||||
}
|
||||
renderProjectFactGraphEdges(factKey, graphData, _selectedGraphEdgeId);
|
||||
if (_selectedGraphEdgeId && typeof ProjectFactGraph !== 'undefined') {
|
||||
ProjectFactGraph.selectEdge(_selectedGraphEdgeId);
|
||||
@@ -1003,7 +1025,13 @@ async function deleteProjectFactEdge(edgeId) {
|
||||
}
|
||||
|
||||
function openSelectedGraphFactDetail() {
|
||||
if (_selectedGraphFactKey) viewProjectFactBody(_selectedGraphFactKey);
|
||||
if (!_selectedGraphFactKey) return;
|
||||
const vulnId = graphVulnIdFromKey(_selectedGraphFactKey);
|
||||
if (vulnId) {
|
||||
openVulnerabilityDetail(vulnId);
|
||||
return;
|
||||
}
|
||||
viewProjectFactBody(_selectedGraphFactKey);
|
||||
}
|
||||
|
||||
function editSelectedGraphFact() {
|
||||
|
||||
@@ -1664,11 +1664,24 @@
|
||||
</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 class="projects-graph-legend" role="group" aria-label="Graph legend">
|
||||
<div class="projects-graph-legend-group">
|
||||
<span class="projects-graph-legend-heading" data-i18n="projects.graphLegendNodes">节点</span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--node"><i style="--legend-color:#4F46E5;--legend-bg:#F5F3FF"></i><span data-i18n="projects.graphLegendNodeTarget">TARGET · 目标</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--node"><i style="--legend-color:#64748B;--legend-bg:#F8FAFC"></i><span data-i18n="projects.graphLegendNodeInfra">INFRA · 基础设施</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--node"><i style="--legend-color:#E11D48;--legend-bg:#FFF1F2"></i><span data-i18n="projects.graphLegendNodeFinding">FINDING · 发现</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--node"><i style="--legend-color:#9333EA;--legend-bg:#F5F3FF"></i><span data-i18n="projects.graphLegendNodeVuln">VULN · 漏洞</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--node"><i style="--legend-color:#B45309;--legend-bg:#FFFBEB"></i><span data-i18n="projects.graphLegendNodeExploit">EXPLOIT · 利用</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--node projects-graph-legend-item--node-dashed"><i style="--legend-color:#CBD5E1;--legend-bg:#F1F5F9"></i><span data-i18n="projects.graphLegendNodeMissing">MISSING · 缺失</span></span>
|
||||
</div>
|
||||
<span class="projects-graph-legend-divider" aria-hidden="true"></span>
|
||||
<div class="projects-graph-legend-group">
|
||||
<span class="projects-graph-legend-heading" data-i18n="projects.graphLegendEdges">连线</span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--edge"><i style="--legend-color:#4F46E5"></i><span data-i18n="projects.graphLegendDiscovered">discovered_on</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--edge"><i style="--legend-color:#64748B"></i><span data-i18n="projects.graphLegendLeads">leads_to</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--edge"><i style="--legend-color:#DC2626"></i><span data-i18n="projects.graphLegendExploits">exploits</span></span>
|
||||
<span class="projects-graph-legend-item projects-graph-legend-item--edge projects-graph-legend-item--dashed"><i style="--legend-color:#94A3B8"></i><span data-i18n="projects.graphLegendTentative">待确认(虚线)</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user