mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-21 15:16:55 +02:00
Add files via upload
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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": "修改登录密码后,需要使用新密码重新登录。",
|
||||
|
||||
@@ -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()">×</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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
// 打开设置
|
||||
|
||||
Reference in New Issue
Block a user