feat(logs): 审计扩展到关键只读操作(导出/查看报告/查看评估详情)

- 写操作(POST/PUT/DELETE)+登录原已全量记录;校准(应用预测校准)亦在内
- 新增 deriveReadActionLabel:对有审计价值的 GET 记录(导出报告/查看报告/查看评估详情),
  跳过看板轮询/列表/聚合等高频只读端点,避免日志噪音
- 修正审计中间件:GET 按只读白名单记录,非关键 GET 不记
This commit is contained in:
freedakgmail
2026-06-14 11:08:06 +08:00
parent 8bac14ef44
commit 5ae33502f9
+28 -3
View File
@@ -192,15 +192,40 @@ function deriveTargetId(path: string): string | null {
return m && m[1] !== undefined ? decodeURIComponent(m[1]) : null;
}
// 系统操作审计:记录全部写操作(POST/PUT/DELETE)+ 登录,供系统管理员审计。
/**
* 关键只读操作的审计标签(GET)。仅记录有审计价值的读操作(导出/查看报告/查看评估详情),
* 跳过看板轮询、列表与聚合等高频只读端点(summary/expiring/overdue/accuracy/calibration 等),
* 避免审计日志被噪音淹没。返回 null 表示该 GET 不记录。
*/
function deriveReadActionLabel(path: string): string | null {
if (/^\/api\/assessments\/[^/]+\/report\/export$/.test(path)) return '导出报告';
if (/^\/api\/assessments\/[^/]+\/report$/.test(path)) return '查看报告';
// 单条评估详情查看(排除聚合/列表类只读端点)。
if (/^\/api\/assessments\/[^/]+$/.test(path)) {
const id = path.split('/')[3] ?? '';
const skip = ['summary', 'expiring', 'overdue', 'run', 'profitability'];
if (id !== '' && !skip.includes(id)) return '查看评估详情';
}
return null;
}
// 系统操作审计:记录全部写操作(POST/PUT/DELETE)+ 登录 + 关键只读操作(导出/查看报告/查看详情),供系统管理员审计。
app.use('/api/*', async (c, next) => {
const method = c.req.method;
const start = Date.now();
await next();
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') return;
if (method === 'HEAD' || method === 'OPTIONS') return;
if (pool === null) return;
const path = c.req.path;
if (path === '/api/system-logs') return; // 不记录查询日志自身
// GET:仅记录有审计价值的关键只读操作,其余(看板轮询/列表/聚合)跳过。
let readLabel: string | null = null;
if (method === 'GET') {
readLabel = deriveReadActionLabel(path);
if (readLabel === null) return;
}
const payload = (c as import('hono').Context).get('user') as AuthPayload | undefined;
let actorId = payload?.uid ?? null;
let actorName = payload?.username ?? null;
@@ -218,7 +243,7 @@ app.use('/api/*', async (c, next) => {
}
// 业务动作增强:解析目标项目名 + 决策(通过/驳回等)。
let action = deriveActionLabel(method, path);
let action = method === 'GET' ? readLabel as string : deriveActionLabel(method, path);
let targetName: string | null = null;
const targetId = deriveTargetId(path);
const isAssessmentPath = /^\/api\/assessments\/[^/]+/.test(path) && targetId !== null && targetId !== 'run' && targetId !== 'profitability' && targetId !== 'summary' && targetId !== 'expiring' && targetId !== 'overdue';