mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-19 10:28:09 +02:00
Add files via upload
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -360,6 +360,9 @@ const PAGE_PERMISSION_MAP = {
|
||||
chat: 'chat:read',
|
||||
hitl: 'hitl:read',
|
||||
'info-collect': 'fofa:execute',
|
||||
assets: 'asset:read',
|
||||
'asset-overview': 'asset:read',
|
||||
'asset-library': 'asset:read',
|
||||
tasks: 'tasks:read',
|
||||
workflows: 'workflow:read',
|
||||
projects: 'project:read',
|
||||
|
||||
+45
-13
@@ -3079,6 +3079,29 @@ function getCachedToolExecutionSummaries(messageElement) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 过程摘要中的早期/快速工具结果可能没有 executionId,但消息本身会按调用顺序保存 ID。
|
||||
* 合并两份数据,避免渲染摘要时丢失可用的弹窗详情入口。
|
||||
*/
|
||||
function mergeToolExecutionSummariesWithIds(summaries, executionIds) {
|
||||
const normalizedSummaries = Array.isArray(summaries)
|
||||
? summaries.map(normalizeToolExecutionSummaryForButton)
|
||||
: [];
|
||||
const normalizedIds = normalizeMcpExecutionIds(executionIds);
|
||||
const claimedIds = new Set(
|
||||
normalizedSummaries.map((item) => item.executionId).filter(Boolean)
|
||||
);
|
||||
const fallbackIds = normalizedIds.filter((id) => !claimedIds.has(id));
|
||||
let fallbackIndex = 0;
|
||||
return normalizedSummaries.map((item) => {
|
||||
if (item.executionId || fallbackIndex >= fallbackIds.length) return item;
|
||||
return {
|
||||
...item,
|
||||
executionId: fallbackIds[fallbackIndex++]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function setPendingToolExecutionSummaries(messageElement, summaries) {
|
||||
if (!messageElement || !messageElement.dataset || !Array.isArray(summaries)) return;
|
||||
const normalized = cacheToolExecutionSummaries(messageElement, summaries);
|
||||
@@ -3337,9 +3360,28 @@ window.setMcpExecutionSummaryCount = setMcpExecutionSummaryCount;
|
||||
window.setPendingMcpExecutionIds = setPendingMcpExecutionIds;
|
||||
window.setPendingToolExecutionSummaries = setPendingToolExecutionSummaries;
|
||||
|
||||
async function openTaskToolExecutionDetail(messageElement, item, index) {
|
||||
let detailItem = item;
|
||||
if (!detailItem.executionId) {
|
||||
const refreshedItem = await resolveToolExecutionSummaryForFocus(messageElement, '', index);
|
||||
const mergedItems = mergeToolExecutionSummariesWithIds(
|
||||
getCachedToolExecutionSummaries(messageElement),
|
||||
getCachedMcpExecutionIds(messageElement)
|
||||
);
|
||||
detailItem = mergedItems[index] || refreshedItem || detailItem;
|
||||
}
|
||||
if (detailItem.executionId) {
|
||||
await showMCPDetail(detailItem.executionId);
|
||||
return;
|
||||
}
|
||||
alert(typeof window.t === 'function'
|
||||
? window.t('chat.toolExecutionDetailPending')
|
||||
: '工具执行详情尚未同步,请稍后重试。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明式渲染工具调用列表。
|
||||
* 过程摘要是展示与定位的唯一模型;executionIds 仅在摘要尚未到达时提供占位。
|
||||
* 过程摘要是展示详情入口的唯一模型;executionIds 仅在摘要尚未到达时提供占位。
|
||||
* 每次更新整体替换列表,避免增量追加产生双重状态。
|
||||
*/
|
||||
function renderMcpCallButtons(messageElement) {
|
||||
@@ -3350,7 +3392,7 @@ function renderMcpCallButtons(messageElement) {
|
||||
const executionIds = getCachedMcpExecutionIds(messageElement);
|
||||
const summaries = getCachedToolExecutionSummaries(messageElement);
|
||||
const items = summaries.length > 0
|
||||
? summaries
|
||||
? mergeToolExecutionSummariesWithIds(summaries, executionIds)
|
||||
: executionIds.map((executionId) => normalizeToolExecutionSummaryForButton({ executionId }));
|
||||
|
||||
const renderVersion = String((parseInt(toolList.dataset.renderVersion, 10) || 0) + 1);
|
||||
@@ -3367,17 +3409,7 @@ function renderMcpCallButtons(messageElement) {
|
||||
if (item.toolCallId) {
|
||||
btn.dataset.toolCallId = item.toolCallId;
|
||||
}
|
||||
btn.onclick = async () => {
|
||||
let focusItem = item;
|
||||
if (!focusItem.processDetailId && !focusItem.toolCallId && focusItem.executionId) {
|
||||
focusItem = await resolveToolExecutionSummaryForFocus(
|
||||
messageElement,
|
||||
focusItem.executionId,
|
||||
index
|
||||
) || focusItem;
|
||||
}
|
||||
await focusToolExecutionInProcessDetails(messageElement, focusItem, index);
|
||||
};
|
||||
btn.onclick = () => openTaskToolExecutionDetail(messageElement, item, index);
|
||||
if (item.toolName) {
|
||||
renderToolExecutionButtonContent(btn, item.toolName, String(index + 1), item.status);
|
||||
} else {
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
const skipText = el.getAttribute('data-i18n-skip-text') === 'true';
|
||||
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
|
||||
const attrList = el.getAttribute('data-i18n-attr');
|
||||
const text = i18next.t(key);
|
||||
const translated = i18next.t(key);
|
||||
// 缺键时保留模板中的后备文案,避免页面直接显示 assets.project 一类内部键名。
|
||||
const text = translated && translated !== key ? translated : '';
|
||||
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;input/textarea 永不设置 textContent
|
||||
const hasNoElementChildren = !el.querySelector('*');
|
||||
if (!skipText && !isFormControl && hasNoElementChildren && text && typeof text === 'string') {
|
||||
@@ -80,7 +82,7 @@
|
||||
var val = text;
|
||||
if (attr === 'title' && titleKey) {
|
||||
var titleText = i18next.t(titleKey);
|
||||
if (titleText && typeof titleText === 'string') val = titleText;
|
||||
if (titleText && titleText !== titleKey && typeof titleText === 'string') val = titleText;
|
||||
}
|
||||
if (val && typeof val === 'string') {
|
||||
el.setAttribute(attr, val);
|
||||
@@ -233,4 +235,3 @@
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@@ -556,6 +556,9 @@ function renderFofaResults(payload) {
|
||||
<path d="M16 8h3a4 4 0 0 1 0 8h-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" data-require-permission="asset:write" onclick="importFofaRowAsset(${idx}); event.stopPropagation();" title="${escapeHtml(_t('assets.importOne'))}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none"><path d="M12 3v12m0 0 4-4m-4 4-4-4M4 19h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -566,6 +569,7 @@ function renderFofaResults(payload) {
|
||||
|
||||
// 更新全选框状态
|
||||
syncSelectAllCheckbox();
|
||||
if (typeof applyRBACToUI === 'function') applyRBACToUI(els.tbody);
|
||||
}
|
||||
|
||||
function inferTargetFromRow(row, fields) {
|
||||
@@ -1147,4 +1151,3 @@ if (document.readyState === 'loading') {
|
||||
} else {
|
||||
updateSelectedMeta();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const PROJECTS_LIST_PAGE_SIZE_KEY = 'cyberstrike.projects_list_page_size';
|
||||
let currentProjectId = null;
|
||||
let currentProjectUpdatedAt = null;
|
||||
let currentProjectTab = 'facts';
|
||||
let currentProjectAssets = [];
|
||||
const projectNameById = {};
|
||||
let _projectsListReady = false;
|
||||
let _projectsFetchPromise = null;
|
||||
@@ -1016,8 +1017,9 @@ async function selectProject(id) {
|
||||
}
|
||||
|
||||
function switchProjectTab(tab) {
|
||||
if (tab === 'assets' && typeof hasPermission === 'function' && !hasPermission('asset:read')) tab = 'facts';
|
||||
currentProjectTab = tab;
|
||||
['facts', 'graph', 'conversations', 'vulns', 'settings'].forEach((t) => {
|
||||
['facts', 'graph', 'assets', 'conversations', 'vulns', 'settings'].forEach((t) => {
|
||||
const btn = document.getElementById(`project-tab-${t}`);
|
||||
const panel = document.getElementById(`project-panel-${t}`);
|
||||
if (btn) btn.classList.toggle('is-active', t === tab);
|
||||
@@ -1025,10 +1027,54 @@ function switchProjectTab(tab) {
|
||||
});
|
||||
if (tab === 'facts') loadProjectFacts();
|
||||
if (tab === 'graph') loadProjectFactGraph();
|
||||
if (tab === 'assets') loadProjectAssets();
|
||||
if (tab === 'conversations') loadProjectConversations();
|
||||
if (tab === 'vulns') loadProjectVulnerabilities();
|
||||
}
|
||||
|
||||
async function loadProjectAssets() {
|
||||
const tbody = document.getElementById('project-assets-tbody');
|
||||
const countEl = document.getElementById('project-assets-count');
|
||||
if (!tbody || !currentProjectId) return;
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('common.loading', '加载中...'))}</td></tr>`;
|
||||
const qs = new URLSearchParams({ project_id: currentProjectId, page: '1', page_size: '100' });
|
||||
const res = await apiFetch(`/api/assets?${qs.toString()}`);
|
||||
if (!res.ok) {
|
||||
currentProjectAssets = [];
|
||||
if (countEl) countEl.textContent = '0';
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('common.loadFailed', '加载失败'))}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
currentProjectAssets = data.assets || [];
|
||||
if (countEl) countEl.textContent = tpFmt('projects.assetCount', `${data.total || 0} 个资产`, { count: data.total || 0 });
|
||||
if (!currentProjectAssets.length) {
|
||||
tbody.innerHTML = `<tr class="is-empty-row"><td colspan="7">${escapeHtml(tpFmt('projects.noBoundAssets', '暂无绑定到此项目的资产'))}</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tbody.innerHTML = currentProjectAssets.map((asset, index) => {
|
||||
const target = asset.host || asset.domain || asset.ip || '-';
|
||||
const service = [asset.protocol, asset.port ? ':' + asset.port : ''].join('') || '-';
|
||||
const fingerprint = [asset.title, asset.server].filter(Boolean).join(' · ') || '-';
|
||||
const updated = asset.last_seen_at ? new Date(asset.last_seen_at).toLocaleString() : '-';
|
||||
const status = asset.status === 'inactive' ? tpFmt('assets.statusInactive', '停用') : tpFmt('assets.statusActive', '活跃');
|
||||
return `<tr>
|
||||
<td class="cell-summary"><button type="button" class="projects-asset-target" onclick="openProjectAssetDetail(${index})" title="${escapeHtml(target)}">${escapeHtml(target)}</button></td>
|
||||
<td><code>${escapeHtml(service)}</code></td>
|
||||
<td class="cell-summary" title="${escapeHtml(fingerprint)}">${escapeHtml(fingerprint)}</td>
|
||||
<td>${escapeHtml(asset.source || '-')}</td>
|
||||
<td>${escapeHtml(updated)}</td>
|
||||
<td><span class="asset-status asset-status--${escapeHtml(asset.status || 'active')}">${escapeHtml(status)}</span></td>
|
||||
<td class="col-actions"><div class="projects-table-actions"><button type="button" class="projects-action-btn projects-action-btn--view" onclick="openProjectAssetDetail(${index})">${escapeHtml(tpFmt('common.view', '查看'))}</button></div></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function openProjectAssetDetail(index) {
|
||||
const asset = currentProjectAssets[Number(index)];
|
||||
if (asset && typeof window.openAssetDetailRecord === 'function') window.openAssetDetailRecord(asset);
|
||||
}
|
||||
|
||||
let _selectedGraphFactKey = null;
|
||||
let _selectedGraphEdgeId = null;
|
||||
let _currentGraphData = null;
|
||||
@@ -2759,6 +2805,8 @@ window.viewFactsForVulnerability = viewFactsForVulnerability;
|
||||
window.openProjectConversation = openProjectConversation;
|
||||
window.unbindConversationFromProject = unbindConversationFromProject;
|
||||
window.loadProjectConversations = loadProjectConversations;
|
||||
window.loadProjectAssets = loadProjectAssets;
|
||||
window.openProjectAssetDetail = openProjectAssetDetail;
|
||||
window.loadProjectFactGraph = loadProjectFactGraph;
|
||||
window.filterProjectFactGraph = filterProjectFactGraph;
|
||||
window.centerProjectFactGraph = centerProjectFactGraph;
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
scanFofaRow: 'fofa:execute',
|
||||
batchScanSelectedFofaRows: 'fofa:execute',
|
||||
exportFofaResults: 'fofa:execute',
|
||||
importSelectedFofaAssets: 'asset:write',
|
||||
importFofaRowAsset: 'asset:write',
|
||||
saveAsset: 'asset:write',
|
||||
deleteAsset: 'asset:delete',
|
||||
|
||||
// 任务队列
|
||||
showBatchImportModal: 'tasks:write',
|
||||
|
||||
@@ -47,7 +47,7 @@ const rbacScopeMeta = {
|
||||
const rbacResourceLabels = {
|
||||
user: '平台用户',
|
||||
project: '项目', conversation: '对话', vulnerability: '漏洞', webshell: 'WebShell 连接',
|
||||
batch_task: '批量任务', c2_listener: 'C2 监听器',
|
||||
batch_task: '批量任务', c2_listener: 'C2 监听器', asset: '资产',
|
||||
};
|
||||
|
||||
function rbacText(value, fallback = '') {
|
||||
|
||||
+17
-3
@@ -81,7 +81,7 @@ function initRouter() {
|
||||
const hashParts = hash.split('?');
|
||||
let pageId = hashParts[0];
|
||||
if (pageId === 'c2') pageId = 'c2-listeners';
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'workflows', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'tasks', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
if (pageId === 'chat') {
|
||||
scheduleChatConversationFromHash(500);
|
||||
@@ -141,7 +141,15 @@ function updateNavState(pageId) {
|
||||
});
|
||||
|
||||
// 设置活动状态
|
||||
if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
|
||||
if (pageId === 'asset-overview' || pageId === 'asset-library' || pageId === 'info-collect') {
|
||||
const assetItem = document.querySelector('.nav-item[data-page="assets"]');
|
||||
if (assetItem) {
|
||||
assetItem.classList.add('active');
|
||||
assetItem.classList.add('expanded');
|
||||
}
|
||||
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
|
||||
if (submenuItem) submenuItem.classList.add('active');
|
||||
} else if (pageId === 'mcp-monitor' || pageId === 'mcp-management') {
|
||||
// MCP子菜单项
|
||||
const mcpItem = document.querySelector('.nav-item[data-page="mcp"]');
|
||||
if (mcpItem) {
|
||||
@@ -374,6 +382,12 @@ async function initPage(pageId) {
|
||||
initInfoCollectPage();
|
||||
}
|
||||
break;
|
||||
case 'asset-overview':
|
||||
if (typeof loadAssetOverview === 'function') loadAssetOverview();
|
||||
break;
|
||||
case 'asset-library':
|
||||
if (typeof loadAssets === 'function') loadAssets();
|
||||
break;
|
||||
case 'tasks':
|
||||
// 初始化任务管理页面
|
||||
if (typeof initTasksPage === 'function') {
|
||||
@@ -549,7 +563,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
let pageId = hashParts[0];
|
||||
|
||||
if (pageId === 'c2') pageId = 'c2-listeners';
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
if (pageId && ['dashboard', 'chat', 'hitl', 'asset-overview', 'asset-library', 'info-collect', 'projects', 'tasks', 'workflows', 'vulnerabilities', 'webshell', 'chat-files', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'platform-rbac', 'skills-monitor', 'skills-management', 'agents-management', 'settings', 'c2-listeners', 'c2-sessions', 'c2-tasks', 'c2-payloads', 'c2-events', 'c2-profiles'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
if (pageId === 'chat') {
|
||||
scheduleChatConversationFromHash(200);
|
||||
|
||||
Reference in New Issue
Block a user