zhizhi/src/membrane/audit-trail.js

151 lines
4.2 KiB
JavaScript
Raw Normal View History

/**
* 全链路审计追溯系统 · 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,
};