/** * 系统设置 - 平台操作审计日志 */ let auditLogsPage = 1; let auditLogsPageSize = 20; let auditLogsTotal = 0; const AUDIT_PAGE_SIZE_KEY = 'cyberstrike_audit_page_size'; /** 按类别列出的操作(用于 datalist 提示,避免超长下拉) */ const AUDIT_ACTIONS_BY_CATEGORY = { auth: ['login', 'logout', 'change_password'], config: ['apply', 'update'], c2: ['listener_create', 'listener_delete', 'listener_start', 'listener_stop', 'session_delete', 'task_create', 'task_cancel', 'task_delete'], webshell: ['connection_create', 'connection_delete'], knowledge: ['item_delete', 'index_rebuild'], conversation: ['delete', 'delete_turn'], vulnerability: ['create', 'update', 'delete'], external_mcp: ['upsert', 'delete'], task: ['create_queue', 'start_queue', 'delete_queue', 'pause_queue', 'rerun_queue', 'delete_batch_task'], tool: ['execution_delete', 'execution_delete_batch'], file: ['upload', 'delete'], hitl: ['decision'], role: ['create', 'update', 'delete'], skill: ['create', 'update', 'delete'], agent: ['markdown_create', 'markdown_update', 'markdown_delete'] }; function auditT(key, opts, fallback) { if (typeof t === 'function') { const v = t(key, opts); if (v && v !== key) return v; } return fallback != null ? fallback : key; } function auditCategoryI18nKey(category) { if (!category) return ''; if (category === 'external_mcp') return 'externalMcp'; return category; } function auditCategoryLabel(category) { if (!category) return ''; const key = 'settingsAudit.cat.' + auditCategoryI18nKey(category); return auditT(key, null, category); } function auditActionLabel(action) { if (!action) return ''; return auditT('settingsAudit.act.' + action, null, action); } function formatAuditTime(iso) { if (!iso) return ''; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleString(); } catch (_) { return iso; } } function auditDatetimeLocalToRFC3339(value) { if (!value || !value.trim()) return ''; const d = new Date(value); if (Number.isNaN(d.getTime())) return ''; return d.toISOString(); } function initAuditPageSizeFromStorage() { try { const saved = parseInt(localStorage.getItem(AUDIT_PAGE_SIZE_KEY), 10); if ([10, 20, 50, 100].indexOf(saved) >= 0) { auditLogsPageSize = saved; } } catch (_) { /* ignore */ } const sel = document.getElementById('audit-page-size'); if (sel) sel.value = String(auditLogsPageSize); } function onAuditPageSizeChange() { const sel = document.getElementById('audit-page-size'); if (!sel) return; const n = parseInt(sel.value, 10); if ([10, 20, 50, 100].indexOf(n) < 0) return; auditLogsPageSize = n; try { localStorage.setItem(AUDIT_PAGE_SIZE_KEY, String(n)); } catch (_) { /* ignore */ } auditLogsPage = 1; loadAuditLogs(1); } function rebuildAuditActionSelect() { const catEl = document.getElementById('audit-filter-category'); const actEl = document.getElementById('audit-filter-action'); if (!actEl) return; const category = catEl ? catEl.value : ''; const prev = actEl.value; const allLabel = auditT('settingsAudit.filterAllActions', null, '全部操作'); const hint = auditT('settingsAudit.filterCascadeHint', null, '选择类别后可筛选具体操作'); actEl.innerHTML = ''; const allOpt = document.createElement('option'); allOpt.value = ''; allOpt.textContent = allLabel; actEl.appendChild(allOpt); if (!category) { actEl.disabled = true; actEl.value = ''; actEl.title = hint; return; } actEl.disabled = false; actEl.title = ''; const actions = AUDIT_ACTIONS_BY_CATEGORY[category] || []; actions.forEach(function (action) { const opt = document.createElement('option'); opt.value = action; opt.textContent = auditActionLabel(action); actEl.appendChild(opt); }); if (prev && Array.prototype.some.call(actEl.options, function (o) { return o.value === prev; })) { actEl.value = prev; } } function onAuditCategoryFilterChange() { rebuildAuditActionSelect(); } function buildAuditQueryParams(forExport) { const params = new URLSearchParams(); if (!forExport) { params.set('page', String(auditLogsPage)); params.set('page_size', String(auditLogsPageSize)); } const cat = document.getElementById('audit-filter-category'); const act = document.getElementById('audit-filter-action'); const res = document.getElementById('audit-filter-result'); const q = document.getElementById('audit-filter-q'); const since = document.getElementById('audit-filter-since'); const until = document.getElementById('audit-filter-until'); if (cat && cat.value) params.set('category', cat.value); if (act && !act.disabled && act.value) params.set('action', act.value); if (res && res.value) params.set('result', res.value); if (q && q.value.trim()) params.set('q', q.value.trim()); const sinceISO = since ? auditDatetimeLocalToRFC3339(since.value) : ''; const untilISO = until ? auditDatetimeLocalToRFC3339(until.value) : ''; if (sinceISO) params.set('since', sinceISO); if (untilISO) params.set('until', untilISO); return params.toString(); } async function loadAuditMeta() { if (typeof apiFetch !== 'function') return; const hint = document.getElementById('audit-retention-hint'); try { const r = await apiFetch('/api/audit/meta'); if (!r.ok) return; const data = await r.json(); if (!hint) return; if (!data.enabled) { hint.hidden = false; hint.textContent = auditT('settingsAudit.disabledHint', null, '审计功能已关闭,新操作不会写入审计表。'); return; } const days = data.retention_days; if (days > 0) { hint.hidden = false; hint.textContent = auditT('settingsAudit.retentionHint', { days: days }, '审计记录保留 ' + days + ' 天,超期自动清理。'); } else { hint.hidden = true; } } catch (_) { /* ignore */ } } async function loadAuditSummary() { if (typeof apiFetch !== 'function') return; const wrap = document.getElementById('audit-summary-stats'); try { const r = await apiFetch('/api/audit/summary?' + buildAuditQueryParams(true)); if (!r.ok) return; const data = await r.json(); if (wrap) wrap.hidden = false; const elTotal = document.getElementById('audit-stat-total'); const elFail = document.getElementById('audit-stat-failures'); const elRecent = document.getElementById('audit-stat-recent'); if (elTotal) elTotal.textContent = String(data.total != null ? data.total : 0); if (elFail) elFail.textContent = String(data.failures != null ? data.failures : 0); if (elRecent) elRecent.textContent = String(data.recent_7d != null ? data.recent_7d : 0); } catch (_) { /* ignore */ } } async function loadAuditLogs(page) { if (typeof apiFetch !== 'function') return; auditLogsPage = page != null ? page : auditLogsPage; const listEl = document.getElementById('audit-log-list'); if (listEl) { listEl.innerHTML = '
ID: ' + esc(id) + '
' : ''; } function refreshAuditLogs() { loadAuditLogs(auditLogsPage); } async function downloadAuditExport(url, filename) { const r = await apiFetch(url); if (!r.ok) { const err = await r.json().catch(function () { return {}; }); throw new Error(err.error || r.statusText); } const blob = await r.blob(); const objectUrl = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = objectUrl; a.download = filename; a.click(); URL.revokeObjectURL(objectUrl); } function closeAuditExportMenu() { const menu = document.getElementById('audit-export-menu'); const trigger = document.getElementById('audit-export-trigger'); if (menu) menu.hidden = true; if (trigger) trigger.setAttribute('aria-expanded', 'false'); } function toggleAuditExportMenu(ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); const menu = document.getElementById('audit-export-menu'); const trigger = document.getElementById('audit-export-trigger'); if (!menu) return; const willOpen = menu.hidden; if (willOpen) { menu.hidden = false; if (trigger) trigger.setAttribute('aria-expanded', 'true'); if (!window._auditExportMenuDocBound) { window._auditExportMenuDocBound = true; document.addEventListener('click', function () { closeAuditExportMenu(); }); } } else { closeAuditExportMenu(); } } async function runAuditExport(format) { closeAuditExportMenu(); if (format === 'csv') { await exportAuditLogsCsv(); } else { await exportAuditLogs(); } } async function exportAuditLogs() { if (typeof apiFetch !== 'function') return; try { await downloadAuditExport( '/api/audit/logs/export?' + buildAuditQueryParams(true), 'audit-logs-' + new Date().toISOString().slice(0, 10) + '.json' ); if (typeof showToast === 'function') { showToast(auditT('settingsAudit.exportDone', null, '导出完成'), 'success'); } } catch (e) { if (typeof showToast === 'function') { showToast(e.message || String(e), 'error'); } } } async function exportAuditLogsCsv() { if (typeof apiFetch !== 'function') return; try { const qs = buildAuditQueryParams(true); await downloadAuditExport( '/api/audit/logs/export?' + (qs ? qs + '&' : '') + 'format=csv', 'audit-logs-' + new Date().toISOString().slice(0, 10) + '.csv' ); if (typeof showToast === 'function') { showToast(auditT('settingsAudit.exportDone', null, '导出完成'), 'success'); } } catch (e) { if (typeof showToast === 'function') { showToast(e.message || String(e), 'error'); } } } function closeAuditDetailModal() { const el = document.getElementById('audit-detail-modal'); if (el) el.remove(); } async function showAuditLogDetail(id) { if (!id || typeof apiFetch !== 'function') return; const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); }; try { const r = await apiFetch('/api/audit/logs/' + encodeURIComponent(id)); if (!r.ok) throw new Error('not found'); const data = await r.json(); const log = data.log || {}; const detail = log.detail ? JSON.stringify(log.detail, null, 2) : ''; closeAuditDetailModal(); const overlay = document.createElement('div'); overlay.id = 'audit-detail-modal'; overlay.className = 'modal'; overlay.style.display = 'block'; const catAction = esc(auditCategoryLabel(log.category || '')) + ' / ' + esc(auditActionLabel(log.action || '')); overlay.innerHTML = ''; document.body.appendChild(overlay); overlay.addEventListener('click', function (ev) { if (ev.target === overlay) closeAuditDetailModal(); }); } catch (e) { if (typeof showToast === 'function') { showToast(e.message || String(e), 'error'); } } } function initAuditLogsSection() { if (!document.getElementById('audit-log-list')) return; initAuditPageSizeFromStorage(); rebuildAuditActionSelect(); loadAuditMeta(); loadAuditLogs(1); }