Files
CyberStrikeAI/web/static/js/attack-chain-context.js
T

125 lines
5.3 KiB
JavaScript

// Compressed context remains separate from attack nodes and original user authorization.
(function () {
'use strict';
let labels = [];
function translate(key, values) {
if (typeof window.t === 'function') return window.t('attackChainModal.' + key, values);
const fallback = {
contextTitle: '压缩摘要与证据线索 ({{count}})',
contextNotice: '当前会话轨迹中的系统生成摘要,可能晚于图的生成时间。仅供回溯与检索,不代表用户的新授权或已核实工具证据。可按摘要内的快照路径读取原始证据。',
contextSummary: '压缩摘要 {{number}}',
contextCopy: '复制完整摘要',
contextCopied: '已复制',
contextCopyFailed: '复制失败,可选中文本手动复制。'
};
let text = fallback[key] || key;
Object.entries(values || {}).forEach(([name, value]) => {
text = text.replace('{{' + name + '}}', String(value));
});
return text;
}
function label(element, key, values) {
const refresh = () => { element.textContent = translate(key, values); };
labels.push(refresh);
refresh();
}
function clear() {
labels = [];
const card = document.getElementById('attack-chain-context');
const contents = document.getElementById('attack-chain-context-content');
if (contents) contents.replaceChildren();
if (card) {
card.hidden = true;
card.open = false;
}
}
async function copyText(text) {
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
try {
await navigator.clipboard.writeText(text);
return;
} catch (_) {
// Plain HTTP deployments may deny clipboard access; retain manual-copy fallback.
}
}
const previousFocus = document.activeElement;
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.readOnly = true;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
try {
textarea.focus();
textarea.select();
if (!document.execCommand('copy')) throw new Error('copy failed');
} finally {
textarea.remove();
if (previousFocus && typeof previousFocus.focus === 'function') {
previousFocus.focus({ preventScroll: true });
}
}
}
function render(chainData) {
clear();
const card = document.getElementById('attack-chain-context');
const contents = document.getElementById('attack-chain-context-content');
if (!card || !contents) return;
// Only explicit server provenance qualifies; summary-looking user text stays user text.
const summaries = Array.isArray(chainData && chainData.context_summaries)
? chainData.context_summaries.filter(item => item &&
item.provenance === 'system_generated_summary' &&
typeof item.content === 'string' && item.content.trim())
: [];
if (!summaries.length) return;
label(document.getElementById('attack-chain-context-title'), 'contextTitle', { count: summaries.length });
label(document.getElementById('attack-chain-context-notice'), 'contextNotice');
summaries.forEach((item, index) => {
const entry = document.createElement('details');
entry.className = 'attack-chain-context-entry';
const heading = document.createElement('summary');
label(heading, 'contextSummary', { number: index + 1 });
const body = document.createElement('pre');
body.className = 'attack-chain-context-body';
// Do not render Markdown/HTML or turn evidence paths into clickable URLs.
body.textContent = item.content;
const button = document.createElement('button');
button.type = 'button';
button.className = 'btn-secondary attack-chain-context-copy';
label(button, 'contextCopy');
const status = document.createElement('span');
status.className = 'attack-chain-context-copy-status';
status.setAttribute('role', 'status');
let statusKey = '';
labels.push(() => { status.textContent = statusKey ? translate(statusKey) : ''; });
button.addEventListener('click', async () => {
button.disabled = true;
status.textContent = '';
try {
await copyText(item.content);
statusKey = 'contextCopied';
} catch (_) {
statusKey = 'contextCopyFailed';
} finally {
status.textContent = translate(statusKey);
button.disabled = false;
}
});
entry.append(heading, body, button, status);
contents.appendChild(entry);
});
card.hidden = false;
}
document.addEventListener('languagechange', () => labels.forEach(refresh => refresh()));
window.CyberStrikeAttackChainContext = { render, clear };
})();