zhizhi/src/membrane/audit-trail.js

151 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 全链路审计追溯系统 · Audit Trail
* 语言膜核心组件
*
* 每一条通过语言膜的请求都会被记录:
* WHO — 谁发起的HLDP编号 / IP / 会话ID
* WHAT — 什么操作HLDP格式化的指令内容
* WHEN — 什么时间ISO时间戳
* WHERE — 在哪个频道/模块(频道编号/模块编号)
* HOW — 执行结果(成功/失败/异常)
* WHY — 什么触发的(上下文链路)
*
* 编号: SY-MEMBRANE-AUDIT-001
* 守护: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
*/
'use strict';
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// 审计日志存储目录
const AUDIT_DIR = process.env.ZY_AUDIT_DIR
|| path.join(process.env.ZY_ROOT || process.cwd(), 'data', 'audit-trail');
/**
* 生成审计事件ID
* 格式: AT-YYYYMMDD-HHmmss-XXXX
*/
function generateAuditId() {
const now = new Date();
const date = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
const rand = crypto.randomBytes(2).toString('hex').toUpperCase();
return `AT-${date}-${rand}`;
}
/**
* 创建审计条目
*
* @param {object} params
* @param {string} params.who — 请求发起者标识
* @param {string} params.what — 操作描述
* @param {string} [params.where] — 频道/模块标识
* @param {string} [params.why] — 触发原因/上下文
* @param {string} [params.sourceIp] — 来源IP
* @param {string} [params.sessionId] — 会话ID
* @returns {object} 审计条目
*/
function createEntry(params) {
return {
audit_id: generateAuditId(),
timestamp: new Date().toISOString(),
who: params.who || 'unknown',
what: params.what || '',
when: new Date().toISOString(),
where: params.where || 'membrane-gateway',
why: params.why || '',
how: 'pending',
source_ip: params.sourceIp || '',
session_id: params.sessionId || '',
responsibility: 'undetermined',
};
}
/**
* 完成审计条目(填入执行结果)
*
* @param {object} entry — createEntry 返回的条目
* @param {string} result — 'success' | 'failure' | 'error' | 'denied'
* @param {string} [responsibility] — 'system' | 'human' | 'persona'
* @param {string} [detail] — 附加信息
* @returns {object} 完成后的审计条目
*/
function completeEntry(entry, result, responsibility, detail) {
entry.how = result;
entry.responsibility = responsibility || 'system';
entry.completed_at = new Date().toISOString();
if (detail) {
entry.detail = detail;
}
return entry;
}
/**
* 持久化审计条目到磁盘
* 按日期分文件存储: data/audit-trail/YYYY-MM-DD.jsonl
*
* @param {object} entry — 审计条目
*/
function persist(entry) {
try {
if (!fs.existsSync(AUDIT_DIR)) {
fs.mkdirSync(AUDIT_DIR, { recursive: true });
}
const dateStr = new Date().toISOString().slice(0, 10);
const filePath = path.join(AUDIT_DIR, `${dateStr}.jsonl`);
fs.appendFileSync(filePath, JSON.stringify(entry) + '\n', 'utf8');
} catch (err) {
// 审计系统自身不能阻塞请求流,记录到 stderr
process.stderr.write(`[AUDIT] persist error: ${err.message}\n`);
}
}
/**
* 查询审计日志
*
* @param {object} [filter]
* @param {string} [filter.date] — 'YYYY-MM-DD'
* @param {string} [filter.who] — 按 who 过滤
* @param {number} [filter.limit] — 最大返回条数
* @returns {Array} 审计条目列表
*/
function query(filter) {
const opts = filter || {};
const dateStr = opts.date || new Date().toISOString().slice(0, 10);
const filePath = path.join(AUDIT_DIR, `${dateStr}.jsonl`);
if (!fs.existsSync(filePath)) {
return [];
}
const lines = fs.readFileSync(filePath, 'utf8')
.split('\n')
.filter(Boolean);
let entries = lines.map(line => {
try { return JSON.parse(line); } catch (_) { return null; }
}).filter(Boolean);
if (opts.who) {
entries = entries.filter(e => e.who === opts.who);
}
if (opts.limit && opts.limit > 0) {
entries = entries.slice(-opts.limit);
}
return entries;
}
module.exports = {
createEntry,
completeEntry,
persist,
query,
generateAuditId,
AUDIT_DIR,
};