/** * 系统设置 - 平台操作审计日志 */ let auditLogsPage = 1; let auditLogsPageSize = 20; let auditLogsTotal = 0; let auditLogsCache = []; 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: ['create', '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); } /** Stored DB messages that share category+action but need distinct i18n keys. */ const AUDIT_MSG_BY_STORED_TEXT = { '登录失败:密码错误': 'settingsAudit.msg.auth.login_failed', '修改密码失败:当前密码不正确': 'settingsAudit.msg.auth.change_password_failed', '应用配置失败:初始化知识库': 'settingsAudit.msg.config.apply_fail_kb_init', '应用配置失败:重新初始化知识库': 'settingsAudit.msg.config.apply_fail_kb_reinit', '应用配置失败:C2': 'settingsAudit.msg.config.apply_fail_c2' }; function auditMessageLabel(log) { if (!log) return ''; const raw = (log.message || '').trim(); if (raw && AUDIT_MSG_BY_STORED_TEXT[raw]) { return auditT(AUDIT_MSG_BY_STORED_TEXT[raw], null, raw); } const cat = (log.category || '').trim(); const act = (log.action || '').trim(); const res = (log.result || '').trim(); if (cat && act) { if (cat === 'auth' && act === 'login' && res === 'failure') { return auditT('settingsAudit.msg.auth.login_failed', null, raw); } if (cat === 'auth' && act === 'change_password' && res === 'failure') { return auditT('settingsAudit.msg.auth.change_password_failed', null, raw); } const key = 'settingsAudit.msg.' + cat + '.' + act; const translated = auditT(key, null, null); if (translated && translated !== key) return translated; } return raw; } function auditResultLabel(result) { if (!result) return ''; return auditT('settingsAudit.result.' + result, null, result); } function auditLocale() { if (typeof window.__locale === 'string' && window.__locale.length) { return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US'; } return (typeof navigator !== 'undefined' && navigator.language) ? navigator.language : 'en-US'; } function auditTimezoneShortLabel() { try { const parts = new Intl.DateTimeFormat(auditLocale(), { timeZoneName: 'short' }).formatToParts(new Date()); const tz = parts.find(function (p) { return p.type === 'timeZoneName'; }); return tz ? tz.value : ''; } catch (_) { return ''; } } function formatAuditTime(iso) { if (!iso) return ''; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return iso; return d.toLocaleString(auditLocale(), { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, timeZoneName: 'short' }); } catch (_) { return iso; } } /** Read stored local datetime (YYYY-MM-DDTHH:mm) from custom picker or raw input. */ function getAuditFilterDatetimeValue(inputId) { if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.getValue === 'function') { return window.AuditDatetimePicker.getValue(inputId) || ''; } var el = document.getElementById(inputId); return el ? (el.value || '') : ''; } /** datetime-local / picker storage -> UTC RFC3339 for API. */ function auditDatetimeLocalToRFC3339(value) { if (!value || !value.trim()) return ''; const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/.exec(value.trim()); if (!m) return ''; const d = new Date(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0, 0); if (Number.isNaN(d.getTime())) return ''; return d.toISOString(); } function updateAuditTimezoneHint() { const el = document.getElementById('audit-filter-timezone-hint'); if (!el) return; const tz = auditTimezoneShortLabel(); if (!tz) { el.hidden = true; el.textContent = ''; return; } el.hidden = false; el.textContent = auditT('settingsAudit.filterTimeZone', { tz: tz }, '时区:' + tz + '(筛选按浏览器本地时间,API 使用 UTC)'); } 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; syncAuditCustomSelect('audit-filter-action'); 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; } syncAuditCustomSelect('audit-filter-action'); } 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'); 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 = auditDatetimeLocalToRFC3339(getAuditFilterDatetimeValue('audit-filter-since')); const untilISO = auditDatetimeLocalToRFC3339(getAuditFilterDatetimeValue('audit-filter-until')); if (sinceISO) params.set('since', sinceISO); if (untilISO) params.set('until', untilISO); return params.toString(); } 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 elSuccess = document.getElementById('audit-stat-success'); const elFail = document.getElementById('audit-stat-failures'); const elRecent = document.getElementById('audit-stat-recent'); const total = data.total != null ? data.total : 0; const failures = data.failures != null ? data.failures : 0; if (elTotal) elTotal.textContent = String(total); if (elSuccess) elSuccess.textContent = String(Math.max(0, total - failures)); if (elFail) elFail.textContent = String(failures); 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 = '
| 时间 | ' + '说明 | ' + '类别 | ' + '操作 | ' + '结果 | ' + 'IP | ' + '资源 ID | ' + '
|---|---|---|---|---|---|---|
| ' + when + ' | ' + '' + (msg || dash) + ' | ' + '' + (catLabel ? '' + catLabel + '' : dash) + ' | ' + '' + (actionLabel ? '' + actionLabel + '' : dash) + ' | ' + '' + (res ? '' + res + '' : dash) + ' | ' + '' + (ip || dash) + ' | ' + '' + (rid || dash) + ' | ' + '
ID: ' + id + '
'); } async function auditOpenConversationChat(conversationId) { const id = String(conversationId || '').trim(); if (!id) return; if (typeof apiFetch === 'function') { try { const r = await apiFetch('/api/conversations/' + encodeURIComponent(id)); if (!r.ok) { if (typeof showToast === 'function') { showToast(auditT('settingsAudit.resourceRemoved', null, '(关联对象已删除)'), 'warning'); } return; } } catch (_) { return; } } closeAuditDetailModal(); if (typeof switchPage === 'function') { switchPage('chat'); } if (typeof loadConversation === 'function') { void loadConversation(id); } } window.auditOpenConversationChat = auditOpenConversationChat; function auditResourceLink(log) { if (!log || auditResourceUnavailable(log)) return ''; const type = log.resourceType || ''; const id = log.resourceId || ''; if (!id) return ''; const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); }; const label = esc(auditT('settingsAudit.openResource', null, '打开关联资源')); if (type === 'conversation' || (type === '' && id.length > 8 && !id.startsWith('c2_'))) { const chatLabel = esc(auditT('settingsAudit.openResourceChat', null, '打开关联资源(chat)')); return ''; } if (type === 'vulnerability' || type === 'batch_queue') { const page = type === 'batch_queue' ? 'tasks' : 'vulnerabilities'; return ''; } if (type === 'c2_listener' || type === 'c2_session' || type === 'c2_task') { const page = type === 'c2_listener' ? 'c2-listeners' : (type === 'c2_session' ? 'c2-sessions' : 'c2-tasks'); return ''; } if (type === 'webshell_connection') { return ''; } if (type === 'knowledge_item') { return ''; } if (type === 'chat_upload') { return ''; } if (type === 'tool_execution') { return ''; } if (type === 'role' || type === 'skill' || type === 'markdown_agent') { return ''; } return ''; } 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() { closeAppModal('audit-detail-modal'); const el = document.getElementById('audit-detail-modal'); if (el) el.remove(); syncAppModalBodyLock(); } async function showAuditLogDetail(id) { if (!id || typeof apiFetch !== 'function') return; const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); }; try { closeAuditDetailModal(); const overlay = document.createElement('div'); overlay.id = 'audit-detail-modal'; overlay.className = 'modal'; document.body.appendChild(overlay); openAppModal(overlay, { focus: false }); 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) : ''; const catAction = esc(auditCategoryLabel(log.category || '')) + ' / ' + esc(auditActionLabel(log.action || '')); deferModalContent(function () { overlay.innerHTML = ''; const chatBtn = overlay.querySelector('.audit-open-chat-btn'); if (chatBtn) { chatBtn.addEventListener('click', function () { auditOpenConversationChat(chatBtn.getAttribute('data-conversation-id')); }); } overlay.addEventListener('click', function (ev) { if (ev.target === overlay) closeAuditDetailModal(); }); }); } catch (e) { closeAuditDetailModal(); if (typeof showToast === 'function') { showToast(e.message || String(e), 'error'); } } } function initAuditLogsSection() { if (!document.getElementById('audit-log-list')) return; initAuditPageSizeFromStorage(); initAuditFilterSelects(); rebuildAuditActionSelect(); if (typeof window.AuditDatetimePicker !== 'undefined' && typeof window.AuditDatetimePicker.init === 'function') { window.AuditDatetimePicker.init(); } initAuditTimePresets(); updateAuditTimezoneHint(); loadAuditLogs(1); } function refreshAuditFilterI18n() { const section = document.getElementById('settings-section-audit'); if (section && typeof applyTranslations === 'function') { applyTranslations(section); } rebuildAuditActionSelect(); syncAuditCustomSelect('audit-filter-category'); syncAuditCustomSelect('audit-filter-action'); syncAuditCustomSelect('audit-filter-result'); updateAuditTimezoneHint(); } function refreshAuditLogsI18n() { if (!document.getElementById('audit-log-list')) return; refreshAuditFilterI18n(); if (auditLogsCache.length) { renderAuditLogs(auditLogsCache); renderAuditLogsPagination(); } } document.addEventListener('languagechange', function () { try { refreshAuditLogsI18n(); } catch (e) { console.warn('languagechange audit refresh failed', e); } }); var auditCustomSelectMap = {}; var auditFilterSelectsDocListener = false; function closeAllAuditCustomSelects() { Object.keys(auditCustomSelectMap).forEach(function (id) { auditCustomSelectMap[id].wrapper.classList.remove('open'); }); } function syncAuditCustomSelect(selectId) { var reg = auditCustomSelectMap[selectId]; if (!reg) return; var select = reg.select; var dropdown = reg.dropdown; var trigger = reg.trigger; var wrapper = reg.wrapper; var valueSpan = trigger.querySelector('.audit-custom-select-value'); dropdown.innerHTML = ''; Array.prototype.forEach.call(select.options, function (opt) { var item = document.createElement('div'); item.className = 'audit-custom-select-option'; item.setAttribute('role', 'option'); item.setAttribute('data-value', opt.value); if (opt.value === select.value) { item.classList.add('is-selected'); item.setAttribute('aria-selected', 'true'); } var check = document.createElement('span'); check.className = 'audit-custom-select-check'; check.setAttribute('aria-hidden', 'true'); check.textContent = '✓'; var label = document.createElement('span'); label.className = 'audit-custom-select-label'; label.textContent = opt.textContent; item.appendChild(check); item.appendChild(label); dropdown.appendChild(item); }); var selectedOpt = select.options[select.selectedIndex]; if (valueSpan) { valueSpan.textContent = selectedOpt ? selectedOpt.textContent : ''; } trigger.disabled = !!select.disabled; wrapper.classList.toggle('is-disabled', !!select.disabled); } function enhanceAuditFilterSelect(selectId) { var select = document.getElementById(selectId); if (!select) return; if (select.dataset.auditCustom === '1') { syncAuditCustomSelect(selectId); return; } select.dataset.auditCustom = '1'; select.classList.add('audit-native-select'); select.tabIndex = -1; select.setAttribute('aria-hidden', 'true'); var wrapper = document.createElement('div'); wrapper.className = 'audit-custom-select'; var trigger = document.createElement('button'); trigger.type = 'button'; trigger.className = 'audit-custom-select-trigger'; trigger.setAttribute('aria-haspopup', 'listbox'); var valueSpan = document.createElement('span'); valueSpan.className = 'audit-custom-select-value'; trigger.appendChild(valueSpan); var caret = document.createElement('span'); caret.className = 'audit-custom-select-caret'; caret.setAttribute('aria-hidden', 'true'); caret.textContent = '▾'; trigger.appendChild(caret); var dropdown = document.createElement('div'); dropdown.className = 'audit-custom-select-dropdown'; dropdown.setAttribute('role', 'listbox'); var parent = select.parentNode; parent.insertBefore(wrapper, select); wrapper.appendChild(trigger); wrapper.appendChild(dropdown); wrapper.appendChild(select); auditCustomSelectMap[selectId] = { wrapper: wrapper, trigger: trigger, dropdown: dropdown, select: select }; trigger.addEventListener('click', function (e) { e.stopPropagation(); if (select.disabled) return; var open = wrapper.classList.contains('open'); closeAllAuditCustomSelects(); if (!open) wrapper.classList.add('open'); }); dropdown.addEventListener('click', function (e) { var opt = e.target.closest('.audit-custom-select-option'); if (!opt) return; var val = opt.getAttribute('data-value'); if (val === null) val = ''; if (select.value !== val) { select.value = val; select.dispatchEvent(new Event('change', { bubbles: true })); } wrapper.classList.remove('open'); syncAuditCustomSelect(selectId); }); syncAuditCustomSelect(selectId); } function initAuditFilterSelects() { if (!document.getElementById('audit-filter-category')) return; if (!auditFilterSelectsDocListener) { document.addEventListener('click', function () { closeAllAuditCustomSelects(); }); auditFilterSelectsDocListener = true; } enhanceAuditFilterSelect('audit-filter-category'); enhanceAuditFilterSelect('audit-filter-action'); enhanceAuditFilterSelect('audit-filter-result'); }