Add files via upload

This commit is contained in:
公明
2026-05-20 16:05:40 +08:00
committed by GitHub
parent b1a620bfce
commit a69bc93fa1
6 changed files with 1006 additions and 0 deletions
+198
View File
@@ -4268,6 +4268,204 @@ header {
line-height: 1.6;
}
/* 系统设置 - 日志审计 */
.audit-logs-toolbar {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.audit-logs-filters {
display: flex;
flex-wrap: wrap;
align-items: flex-end;
gap: 12px;
}
.audit-logs-filters > .btn-secondary {
align-self: flex-end;
margin-bottom: 0;
}
.audit-logs-filters label {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 0.8125rem;
color: var(--text-secondary);
}
/* 事件类型:两个下拉与「结果」等控件同款边框,无外层套框 */
.audit-filter-cascade {
display: flex;
align-items: center;
gap: 8px;
}
.audit-filter-cascade select {
flex: 0 1 auto;
min-width: 120px;
max-width: 148px;
}
.audit-filter-cascade select:disabled {
opacity: 0.55;
cursor: not-allowed;
background: var(--bg-secondary, #f5f6f8);
}
.audit-filter-cascade-arrow {
flex-shrink: 0;
font-size: 0.8125rem;
color: var(--text-secondary);
line-height: 1;
user-select: none;
pointer-events: none;
}
.audit-logs-filters select,
.audit-logs-filters input[type="text"],
.audit-logs-filters input[type="datetime-local"] {
min-width: 140px;
padding: 0.35rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
}
.audit-logs-actions {
display: flex;
gap: 8px;
}
/* 列表 + 底部分页合并为一张卡片,避免双边框/底部分隔线 */
#settings-section-audit .audit-log-list.c2-event-list {
margin-bottom: 0;
border-bottom: none;
border-radius: 8px 8px 0 0;
}
#settings-section-audit .audit-logs-pagination {
margin-top: 0;
padding: 0;
border: none;
box-shadow: none;
background: transparent;
}
#settings-section-audit .audit-logs-pagination .monitor-pagination {
margin-top: 0;
border: 1px solid var(--border-color);
border-radius: 0 0 8px 8px;
}
.audit-log-item {
cursor: pointer;
}
.audit-detail-pre {
max-height: 320px;
overflow: auto;
font-size: 0.75rem;
background: var(--bg-secondary);
padding: 12px;
border-radius: 6px;
margin-top: 12px;
}
.audit-summary-stats {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin: 12px 0 16px;
}
.audit-stat-card {
flex: 1;
min-width: 120px;
padding: 12px 16px;
border-radius: 8px;
background: var(--bg-secondary, rgba(255, 255, 255, 0.04));
border: 1px solid var(--border-color, rgba(255, 255, 255, 0.08));
}
.audit-stat-card strong {
display: block;
font-size: 1.35rem;
margin-top: 4px;
}
.audit-stat-label {
font-size: 0.85rem;
opacity: 0.75;
}
.audit-retention-hint {
margin-top: 4px;
opacity: 0.85;
}
.audit-export-dropdown {
position: relative;
display: inline-block;
}
.audit-export-trigger {
display: inline-flex;
align-items: center;
gap: 4px;
}
.audit-export-caret {
font-size: 0.7rem;
opacity: 0.75;
}
.audit-export-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 140px;
padding: 4px 0;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
z-index: 50;
}
.audit-export-menu-item {
display: block;
width: 100%;
padding: 8px 14px;
border: none;
background: transparent;
color: var(--text-primary);
font-size: 0.875rem;
text-align: left;
cursor: pointer;
}
.audit-export-menu-item:hover {
background: var(--bg-secondary);
}
#settings-section-audit .audit-logs-pagination .pagination-info {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.audit-detail-body p {
margin: 0 0 8px;
font-size: 0.875rem;
}
/* 系统设置 - 终端 */
.terminal-wrapper {
border: 1px solid var(--border-color);
+98
View File
@@ -820,6 +820,7 @@
"robots": "Bots",
"terminal": "Terminal",
"security": "Security",
"audit": "Audit logs",
"infocollect": "Recon"
},
"infocollect": {
@@ -1782,6 +1783,103 @@
"close": "×",
"newTerminal": "+"
},
"settingsAudit": {
"title": "Audit logs",
"description": "Platform admin actions (login, config, deletes). Does not log chat content, per-command terminal/WebShell runs, or per-tool invocations.",
"filterCategory": "Category",
"filterAction": "Action",
"filterEvent": "Event type",
"filterAllCategories": "All categories",
"filterAllActions": "All actions",
"filterCascadeHint": "Select a category to filter by action",
"filterResult": "Result",
"pageSize": "Per page",
"statTotal": "Filtered total",
"statFailures": "Failures",
"statRecent7d": "Last 7 days",
"retentionHint": "Audit records are kept for {{days}} days, then purged automatically.",
"disabledHint": "Audit logging is disabled; new actions are not written.",
"filterSince": "From",
"filterUntil": "Until",
"filterQuery": "Keyword",
"filterQueryPlaceholder": "Message / resource ID / action",
"cat": {
"auth": "Auth",
"config": "Config",
"terminal": "Terminal",
"c2": "C2",
"webshell": "WebShell",
"knowledge": "Knowledge",
"conversation": "Conversation",
"vulnerability": "Vulnerability",
"externalMcp": "External MCP",
"task": "Tasks",
"tool": "Tools",
"file": "Files",
"hitl": "HITL",
"role": "Roles",
"skill": "Skills",
"agent": "Sub-agents"
},
"act": {
"login": "Login",
"logout": "Logout",
"login_failed": "Login failed",
"password_change": "Password change",
"change_password": "Change password",
"apply": "Apply config",
"update": "Update",
"exec": "Terminal exec",
"exec_stream": "Terminal stream",
"listener_create": "Create listener",
"listener_delete": "Delete listener",
"listener_start": "Start listener",
"listener_stop": "Stop listener",
"session_delete": "Delete session",
"task_create": "Create task",
"task_cancel": "Cancel task",
"task_delete": "Delete task",
"connection_create": "Create connection",
"connection_delete": "Delete connection",
"item_delete": "Delete knowledge item",
"index_rebuild": "Rebuild index",
"delete": "Delete",
"delete_turn": "Delete turn",
"create": "Create",
"upsert": "Upsert external MCP",
"create_queue": "Create batch queue",
"start_queue": "Start batch queue",
"delete_queue": "Delete batch queue",
"pause_queue": "Pause batch queue",
"rerun_queue": "Rerun batch queue",
"delete_batch_task": "Delete batch subtask",
"execution_delete": "Delete execution",
"execution_delete_batch": "Batch delete executions",
"upload": "Upload",
"decision": "HITL decision",
"markdown_create": "Create sub-agent",
"markdown_update": "Update sub-agent",
"markdown_delete": "Delete sub-agent"
},
"openResource": "Open linked resource",
"filterAll": "All",
"filterBtn": "Filter",
"resetBtn": "Reset",
"exportBtn": "Export",
"exportJson": "Export JSON",
"export": "Export JSON",
"exportCsv": "Export CSV",
"exportDone": "Export complete",
"loading": "Loading...",
"empty": "No audit records",
"paginationShow": "{{start}}-{{end}} of {{total}}",
"detailTitle": "Audit detail",
"detailTime": "Time",
"detailCategory": "Category",
"detailResult": "Result",
"detailMessage": "Message",
"detailSession": "Session"
},
"settingsSecurity": {
"changePasswordTitle": "Change password",
"changePasswordDesc": "After changing password, sign in again with the new password.",
+98
View File
@@ -809,6 +809,7 @@
"robots": "机器人设置",
"terminal": "终端",
"security": "安全设置",
"audit": "日志审计",
"infocollect": "信息收集"
},
"infocollect": {
@@ -1771,6 +1772,103 @@
"close": "×",
"newTerminal": "+"
},
"settingsAudit": {
"title": "日志审计",
"description": "记录平台管理类操作(登录、配置、删除等),不记录对话正文、终端/WebShell 每次命令与工具调用明细。",
"filterCategory": "类别",
"filterAction": "操作",
"filterEvent": "事件类型",
"filterAllCategories": "全部类别",
"filterAllActions": "全部操作",
"filterCascadeHint": "选择类别后可筛选具体操作",
"filterResult": "结果",
"pageSize": "每页",
"statTotal": "当前筛选",
"statFailures": "失败",
"statRecent7d": "近 7 天",
"retentionHint": "审计记录保留 {{days}} 天,超期自动清理。",
"disabledHint": "审计功能已关闭,新操作不会写入审计表。",
"filterSince": "开始时间",
"filterUntil": "结束时间",
"filterQuery": "关键词",
"filterQueryPlaceholder": "消息 / 资源 ID / 操作名",
"cat": {
"auth": "认证",
"config": "配置",
"terminal": "终端",
"c2": "C2",
"webshell": "WebShell",
"knowledge": "知识库",
"conversation": "对话",
"vulnerability": "漏洞",
"externalMcp": "外部 MCP",
"task": "任务",
"tool": "工具",
"file": "文件",
"hitl": "人机协同",
"role": "角色",
"skill": "Skill",
"agent": "子代理"
},
"act": {
"login": "登录",
"logout": "登出",
"login_failed": "登录失败",
"password_change": "修改密码",
"change_password": "修改密码",
"apply": "应用配置",
"update": "更新",
"exec": "终端执行",
"exec_stream": "终端流式执行",
"listener_create": "创建监听器",
"listener_delete": "删除监听器",
"listener_start": "启动监听器",
"listener_stop": "停止监听器",
"session_delete": "删除会话",
"task_create": "创建任务",
"task_cancel": "取消任务",
"task_delete": "删除任务",
"connection_create": "创建连接",
"connection_delete": "删除连接",
"item_delete": "删除知识项",
"index_rebuild": "重建索引",
"delete": "删除",
"delete_turn": "删除轮次",
"create": "创建",
"upsert": "保存外部 MCP",
"create_queue": "创建批量队列",
"start_queue": "启动批量队列",
"delete_queue": "删除批量队列",
"pause_queue": "暂停批量队列",
"rerun_queue": "重跑批量队列",
"delete_batch_task": "删除批量子任务",
"execution_delete": "删除执行记录",
"execution_delete_batch": "批量删除执行",
"upload": "上传",
"decision": "HITL 决策",
"markdown_create": "创建子代理",
"markdown_update": "更新子代理",
"markdown_delete": "删除子代理"
},
"openResource": "打开关联资源",
"filterAll": "全部",
"filterBtn": "筛选",
"resetBtn": "重置",
"exportBtn": "导出",
"exportJson": "导出 JSON",
"export": "导出 JSON",
"exportCsv": "导出 CSV",
"exportDone": "导出完成",
"loading": "加载中...",
"empty": "暂无审计记录",
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条",
"detailTitle": "审计详情",
"detailTime": "时间",
"detailCategory": "类别",
"detailResult": "结果",
"detailMessage": "说明",
"detailSession": "会话"
},
"settingsSecurity": {
"changePasswordTitle": "修改密码",
"changePasswordDesc": "修改登录密码后,需要使用新密码重新登录。",
+523
View File
@@ -0,0 +1,523 @@
/**
* 系统设置 - 平台操作审计日志
*/
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 = '<div class="loading-spinner">' + (typeof escapeHtml === 'function' ? escapeHtml(auditT('settingsAudit.loading', null, '加载中...')) : '加载中...') + '</div>';
}
try {
const qs = buildAuditQueryParams(false);
const r = await apiFetch('/api/audit/logs?' + qs);
if (!r.ok) {
const err = await r.json().catch(function () { return {}; });
throw new Error(err.error || r.statusText);
}
const data = await r.json();
renderAuditLogs(data.logs || []);
auditLogsTotal = typeof data.total === 'number' ? data.total : 0;
const maxPage = Math.max(1, Math.ceil(auditLogsTotal / auditLogsPageSize));
if (auditLogsPage > maxPage) {
loadAuditLogs(maxPage);
return;
}
renderAuditLogsPagination();
loadAuditSummary();
} catch (e) {
if (listEl) {
const msg = typeof escapeHtml === 'function' ? escapeHtml(e.message || String(e)) : (e.message || String(e));
listEl.innerHTML = '<div class="monitor-empty">' + msg + '</div>';
}
if (typeof showToast === 'function') {
showToast(e.message || String(e), 'error');
}
}
}
function renderAuditLogs(logs) {
const listEl = document.getElementById('audit-log-list');
if (!listEl) return;
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
if (!logs.length) {
listEl.innerHTML = '<div class="c2-empty">' + esc(auditT('settingsAudit.empty', null, '暂无审计记录')) + '</div>';
return;
}
listEl.innerHTML = logs.map(function (log) {
const lvl = log.result === 'failure' ? 'warn' : (log.level || 'info');
const catLabel = esc(auditCategoryLabel(log.category || ''));
const actionLabel = esc(auditActionLabel(log.action || ''));
const msg = esc(log.message || '');
const ip = esc(log.clientIp || '');
const when = esc(formatAuditTime(log.createdAt));
const res = esc(log.result || '');
const rid = log.resourceId || '';
const meta = rid ? (' · ' + esc(rid)) : '';
const eid = esc(log.id || '');
return (
'<div class="c2-event-item audit-log-item" role="button" tabindex="0" ' +
'onclick="showAuditLogDetail(\'' + eid + '\')" ' +
'onkeydown="if(event.key===\'Enter\'||event.key===\' \'){event.preventDefault();showAuditLogDetail(\'' + eid + '\')}">' +
'<div class="c2-event-level ' + esc(lvl) + '"></div>' +
'<div class="c2-event-content">' +
'<div class="c2-event-message">' + msg + '</div>' +
'<div class="c2-event-meta">' + when + ' · ' + catLabel + '/' + actionLabel + ' · ' + res + meta +
(ip ? ' · IP ' + ip : '') +
'</div></div></div>'
);
}).join('');
if (typeof applyTranslations === 'function') {
applyTranslations(listEl);
}
}
function renderAuditLogsPagination() {
const container = document.getElementById('audit-logs-pagination');
if (!container) return;
const esc = typeof escapeHtml === 'function' ? escapeHtml : function (s) { return String(s || ''); };
const total = auditLogsTotal || 0;
const currentPage = auditLogsPage || 1;
const pageSize = auditLogsPageSize || 20;
const totalPages = Math.max(1, Math.ceil(total / pageSize));
const start = total === 0 ? 0 : (currentPage - 1) * pageSize + 1;
const end = total === 0 ? 0 : Math.min(currentPage * pageSize, total);
const infoText = auditT('mcpMonitor.paginationInfo', { start: start, end: end, total: total },
'显示 ' + start + '-' + end + ' / 共 ' + total + ' 条记录');
const perPageLabel = auditT('mcpMonitor.perPageLabel', null, '每页显示');
const firstPageLabel = auditT('mcp.firstPage', null, '首页');
const prevPageLabel = auditT('mcp.prevPage', null, '上一页');
const pageInfoText = auditT('mcp.pageInfo', { page: currentPage, total: totalPages },
'第 ' + currentPage + ' / ' + totalPages + ' 页');
const nextPageLabel = auditT('mcp.nextPage', null, '下一页');
const lastPageLabel = auditT('mcp.lastPage', null, '末页');
const disabledFirst = currentPage === 1 || total === 0;
const disabledLast = currentPage >= totalPages || total === 0;
let html = '<div class="monitor-pagination">';
html += '<div class="pagination-info">';
html += '<span>' + esc(infoText) + '</span>';
html += '<label class="pagination-page-size">' + esc(perPageLabel);
html += '<select id="audit-page-size" onchange="onAuditPageSizeChange()">';
[10, 20, 50, 100].forEach(function (n) {
html += '<option value="' + n + '"' + (pageSize === n ? ' selected' : '') + '>' + n + '</option>';
});
html += '</select></label></div>';
html += '<div class="pagination-controls">';
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(1)"' + (disabledFirst ? ' disabled' : '') + '>' + esc(firstPageLabel) + '</button>';
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + (currentPage - 1) + ')"' + (disabledFirst ? ' disabled' : '') + '>' + esc(prevPageLabel) + '</button>';
html += '<span class="pagination-page">' + esc(pageInfoText) + '</span>';
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + (currentPage + 1) + ')"' + (disabledLast ? ' disabled' : '') + '>' + esc(nextPageLabel) + '</button>';
html += '<button type="button" class="btn-secondary" onclick="goAuditLogsPage(' + totalPages + ')"' + (disabledLast ? ' disabled' : '') + '>' + esc(lastPageLabel) + '</button>';
html += '</div></div>';
container.innerHTML = html;
}
function goAuditLogsPage(p) {
const totalPages = Math.max(1, Math.ceil((auditLogsTotal || 0) / (auditLogsPageSize || 20)));
if (p < 1 || p > totalPages) return;
loadAuditLogs(p);
}
function filterAuditLogs() {
auditLogsPage = 1;
loadAuditLogs(1);
}
function resetAuditLogFilters() {
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 = '';
if (res) res.value = '';
if (q) q.value = '';
if (since) since.value = '';
if (until) until.value = '';
rebuildAuditActionSelect();
filterAuditLogs();
}
function auditResourceLink(log) {
if (!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_'))) {
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'chat\');}">' + label + ' (chat)</button></p>';
}
if (type === 'vulnerability' || type === 'batch_queue') {
const page = type === 'batch_queue' ? 'tasks' : 'vulnerabilities';
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'' + page + '\');}">' + label + '</button></p>';
}
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 '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'' + page + '\');}">' + label + '</button></p>';
}
if (type === 'webshell_connection') {
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'webshell\');}">' + label + '</button></p>';
}
if (type === 'knowledge_item') {
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'knowledge-management\');}">' + label + '</button></p>';
}
if (type === 'chat_upload') {
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'chat-files\');}">' + label + '</button></p>';
}
if (type === 'tool_execution') {
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchPage===\'function\'){switchPage(\'mcp-monitor\');}">' + label + '</button></p>';
}
if (type === 'role' || type === 'skill' || type === 'markdown_agent') {
return '<p><button type="button" class="btn-secondary btn-small" onclick="closeAuditDetailModal();if(typeof switchSettingsSection===\'function\'){switchPage(\'settings\');switchSettingsSection(\'roles\');}">' + label + '</button></p>';
}
return id ? '<p><strong>ID:</strong> ' + esc(id) + '</p>' : '';
}
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 =
'<div class="modal-content" style="max-width: 720px;">' +
'<div class="modal-header">' +
'<h2>' + esc(auditT('settingsAudit.detailTitle', null, '审计详情')) + '</h2>' +
'<span class="modal-close" onclick="closeAuditDetailModal()">&times;</span>' +
'</div>' +
'<div class="modal-body audit-detail-body">' +
'<p><strong>' + esc(auditT('settingsAudit.detailTime', null, '时间')) + ':</strong> ' + esc(formatAuditTime(log.createdAt)) + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailCategory', null, '类别')) + ':</strong> ' + catAction + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailResult', null, '结果')) + ':</strong> ' + esc(log.result || '') + '</p>' +
'<p><strong>' + esc(auditT('settingsAudit.detailMessage', null, '说明')) + ':</strong> ' + esc(log.message || '') + '</p>' +
(log.clientIp ? '<p><strong>IP:</strong> ' + esc(log.clientIp) + '</p>' : '') +
(log.sessionHint ? '<p><strong>' + esc(auditT('settingsAudit.detailSession', null, '会话')) + ':</strong> ' + esc(log.sessionHint) + '</p>' : '') +
(log.userAgent ? '<p><strong>UA:</strong> ' + esc(log.userAgent) + '</p>' : '') +
auditResourceLink(log) +
(detail ? '<pre class="audit-detail-pre">' + esc(detail) + '</pre>' : '') +
'</div>' +
'<div class="modal-footer"><button type="button" class="btn-secondary" onclick="closeAuditDetailModal()">' +
esc(auditT('common.close', null, '关闭')) + '</button></div>' +
'</div>';
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);
}
+3
View File
@@ -87,6 +87,9 @@ function switchSettingsSection(section) {
if (section === 'terminal' && typeof initTerminal === 'function') {
setTimeout(initTerminal, 0);
}
if (section === 'audit' && typeof initAuditLogsSection === 'function') {
setTimeout(initAuditLogsSection, 0);
}
}
// 打开设置
+86
View File
@@ -2041,6 +2041,9 @@
<div class="settings-nav-item" data-section="security" onclick="switchSettingsSection('security')">
<span data-i18n="settings.nav.security">安全设置</span>
</div>
<div class="settings-nav-item" data-section="audit" onclick="switchSettingsSection('audit')">
<span data-i18n="settings.nav.audit">日志审计</span>
</div>
</nav>
</aside>
@@ -2572,6 +2575,88 @@
</div>
</div>
<!-- 日志审计 -->
<div id="settings-section-audit" class="settings-section-content">
<div class="settings-section-header">
<h3 data-i18n="settingsAudit.title">日志审计</h3>
<p class="settings-description" data-i18n="settingsAudit.description">记录平台管理类操作(登录、配置、删除等),不记录对话正文、终端/WebShell 每次命令与工具调用明细。</p>
<p id="audit-retention-hint" class="settings-description audit-retention-hint" hidden></p>
</div>
<div id="audit-summary-stats" class="audit-summary-stats" hidden>
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statTotal">当前筛选</span><strong id="audit-stat-total">0</strong></div>
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statFailures">失败</span><strong id="audit-stat-failures">0</strong></div>
<div class="audit-stat-card"><span class="audit-stat-label" data-i18n="settingsAudit.statRecent7d">近 7 天</span><strong id="audit-stat-recent">0</strong></div>
</div>
<div class="audit-logs-toolbar">
<div class="audit-logs-filters">
<label class="audit-filter-cascade-group">
<span data-i18n="settingsAudit.filterEvent">事件类型</span>
<div class="audit-filter-cascade">
<select id="audit-filter-category" onchange="onAuditCategoryFilterChange()" aria-label="类别">
<option value="" data-i18n="settingsAudit.filterAllCategories">全部类别</option>
<option value="auth" data-i18n="settingsAudit.cat.auth">认证</option>
<option value="config" data-i18n="settingsAudit.cat.config">配置</option>
<option value="c2" data-i18n="settingsAudit.cat.c2">C2</option>
<option value="webshell" data-i18n="settingsAudit.cat.webshell">WebShell</option>
<option value="knowledge" data-i18n="settingsAudit.cat.knowledge">知识库</option>
<option value="conversation" data-i18n="settingsAudit.cat.conversation">对话</option>
<option value="vulnerability" data-i18n="settingsAudit.cat.vulnerability">漏洞</option>
<option value="external_mcp" data-i18n="settingsAudit.cat.externalMcp">外部 MCP</option>
<option value="task" data-i18n="settingsAudit.cat.task">任务</option>
<option value="tool" data-i18n="settingsAudit.cat.tool">工具</option>
<option value="file" data-i18n="settingsAudit.cat.file">文件</option>
<option value="hitl" data-i18n="settingsAudit.cat.hitl">人机协同</option>
<option value="role" data-i18n="settingsAudit.cat.role">角色</option>
<option value="skill" data-i18n="settingsAudit.cat.skill">Skill</option>
<option value="agent" data-i18n="settingsAudit.cat.agent">子代理</option>
</select>
<span class="audit-filter-cascade-arrow" aria-hidden="true"></span>
<select id="audit-filter-action" disabled aria-label="操作">
<option value="" data-i18n="settingsAudit.filterAllActions">全部操作</option>
</select>
</div>
</label>
<label>
<span data-i18n="settingsAudit.filterResult">结果</span>
<select id="audit-filter-result">
<option value="" data-i18n="settingsAudit.filterAll">全部</option>
<option value="success">success</option>
<option value="failure">failure</option>
</select>
</label>
<label>
<span data-i18n="settingsAudit.filterSince">开始时间</span>
<input type="datetime-local" id="audit-filter-since" />
</label>
<label>
<span data-i18n="settingsAudit.filterUntil">结束时间</span>
<input type="datetime-local" id="audit-filter-until" />
</label>
<label>
<span data-i18n="settingsAudit.filterQuery">关键词</span>
<input type="text" id="audit-filter-q" data-i18n="settingsAudit.filterQueryPlaceholder" data-i18n-attr="placeholder" placeholder="消息 / 资源 ID / 操作名" />
</label>
<button type="button" class="btn-secondary" onclick="filterAuditLogs()" data-i18n="settingsAudit.filterBtn">筛选</button>
<button type="button" class="btn-secondary" onclick="resetAuditLogFilters()" data-i18n="settingsAudit.resetBtn">重置</button>
</div>
<div class="audit-logs-actions">
<button type="button" class="btn-secondary" onclick="refreshAuditLogs()" data-i18n="common.refresh">刷新</button>
<div class="audit-export-dropdown">
<button type="button" class="btn-secondary audit-export-trigger" id="audit-export-trigger" onclick="toggleAuditExportMenu(event)" aria-haspopup="true" aria-expanded="false">
<span data-i18n="settingsAudit.exportBtn">导出</span>
<span class="audit-export-caret" aria-hidden="true"></span>
</button>
<div id="audit-export-menu" class="audit-export-menu" role="menu" hidden>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('json')" data-i18n="settingsAudit.exportJson">导出 JSON</button>
<button type="button" class="audit-export-menu-item" role="menuitem" onclick="runAuditExport('csv')" data-i18n="settingsAudit.exportCsv">导出 CSV</button>
</div>
</div>
</div>
</div>
<div id="audit-log-list" class="audit-log-list c2-event-list"></div>
<div id="audit-logs-pagination" class="pagination-container audit-logs-pagination"></div>
</div>
<!-- 安全设置 -->
<div id="settings-section-security" class="settings-section-content">
<div class="settings-section-header">
@@ -3615,6 +3700,7 @@
<script src="/static/js/chat.js"></script>
<script src="/static/js/hitl.js"></script>
<script src="/static/js/settings.js"></script>
<script src="/static/js/audit.js"></script>
<script src="/static/js/wechat-robot.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm@4.19.0/lib/xterm.js"></script>
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.5.0/lib/xterm-addon-fit.js"></script>