@h-ai/audit 0.1.0-alpha8 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +199 -0
- package/README.ja.md +199 -0
- package/README.md +108 -22
- package/dist/index.d.ts +359 -76
- package/dist/index.js +1163 -144
- package/dist/index.js.map +1 -1
- package/package.json +35 -10
package/dist/index.js
CHANGED
|
@@ -1,9 +1,95 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { core,
|
|
3
|
-
import { validateIdentifiers,
|
|
2
|
+
import { core, ok, err } from '@h-ai/core';
|
|
3
|
+
import { migrateSchema, reldb, validateIdentifiers, schemaColumns, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
6
|
+
import { Buffer } from 'buffer';
|
|
4
7
|
|
|
5
8
|
// src/audit-config.ts
|
|
6
9
|
var AuditInitConfigSchema = z.object({
|
|
10
|
+
/** 以固定时间窗口统计已提交事件;目标和事件由应用配置,不内置业务角色。 */
|
|
11
|
+
alerts: z.object({
|
|
12
|
+
/** 是否检查异常事件并发送告警;开启前配置至少一条规则和真实 Reach 通道,关闭不影响审计入库。 */
|
|
13
|
+
enabled: z.boolean().default(false),
|
|
14
|
+
/** 每次检查最多尝试的通知数;超过的目标留待后续检查,避免突发告警挤占业务资源。 */
|
|
15
|
+
maxNotificationsPerCheck: z.number().int().min(1).max(1e3).default(100),
|
|
16
|
+
/** 告警规则列表;每条按全站匹配事件的固定窗口累计计数,不是每个账号各自计数。 */
|
|
17
|
+
rules: z.array(z.object({
|
|
18
|
+
/** 稳定且唯一的规则编号,用于共享去重;更改编号会视为新规则。 */
|
|
19
|
+
id: z.string().regex(/^[\w-]{1,64}$/),
|
|
20
|
+
/** 匹配的审计 action 全名,例如 auth.login;从实际记录复制,不支持 auth.* 通配。 */
|
|
21
|
+
actions: z.array(z.string().trim().min(1).max(128)).min(1).max(50),
|
|
22
|
+
/** 只统计此资源类型的事件;省略时不按资源过滤,不填写业务敏感正文。 */
|
|
23
|
+
resource: z.string().trim().min(1).max(256).optional(),
|
|
24
|
+
/** 要统计的执行结果;默认失败和拒绝,空数组表示所有结果均参与计数。 */
|
|
25
|
+
outcomes: z.array(z.enum(["success", "failure", "denied"])).max(3).default(["failure", "denied"]),
|
|
26
|
+
/** 参与计数的事件最低等级;低于此级别的记录仍保留,但不触发这条告警。 */
|
|
27
|
+
minimumSeverity: z.enum(["info", "warning", "error", "critical"]).default("info"),
|
|
28
|
+
/** 发出通知时的告警等级;不修改原始审计事件的等级。 */
|
|
29
|
+
severity: z.enum(["warning", "error", "critical"]).default("warning"),
|
|
30
|
+
/** 同一窗口内累计达到此次数才通知;与 IAM 登录失败锁定阈值分别配置。 */
|
|
31
|
+
threshold: z.number().int().min(1).max(1e9).default(1),
|
|
32
|
+
/** 固定计数窗口长度,单位秒;例如 300 表示按五分钟窗口统计,不追补长期停机历史。 */
|
|
33
|
+
windowSeconds: z.number().int().min(60).max(86400).default(300),
|
|
34
|
+
/** 通知接收目标;每个目标独立记录投递结果,渠道受理不等于人员已读。 */
|
|
35
|
+
targets: z.array(z.object({
|
|
36
|
+
/** 本规则内稳定且唯一的目标编号,用于防止多节点重复投递。 */
|
|
37
|
+
id: z.string().regex(/^[\w-]{1,64}$/),
|
|
38
|
+
/** 已注册的 Reach Provider 名称,例如 email;不是 smtp/api 等 Provider 类型。 */
|
|
39
|
+
provider: z.string().trim().min(1).max(128),
|
|
40
|
+
/** 该通道接受的收件地址,例如邮箱或手机号;填写真实值,开发 Console 不会投递。 */
|
|
41
|
+
to: z.string().trim().min(1).max(1e3)
|
|
42
|
+
})).min(1).max(20).refine((targets) => new Set(targets.map((target) => target.id)).size === targets.length)
|
|
43
|
+
})).max(20).default([]).refine((rules) => new Set(rules.map((rule) => rule.id)).size === rules.length),
|
|
44
|
+
/** 自动告警检查计划;宿主需接入 audit.schedulerPlan,只开启告警功能不会自动轮询。 */
|
|
45
|
+
schedule: z.object({
|
|
46
|
+
/** 是否让 Scheduler 自动检查;需同时开启 alerts.enabled,否则仅保留计划配置。 */
|
|
47
|
+
enabled: z.boolean().default(false),
|
|
48
|
+
/** 检查频率的 Cron 表达式;默认每分钟检查,窗口阈值仍由规则决定。 */
|
|
49
|
+
cron: z.string().trim().min(1).max(128).default("* * * * *"),
|
|
50
|
+
/** Cron 使用的 IANA 时区,例如 Asia/Shanghai;UTC 不等于北京时间。 */
|
|
51
|
+
timezone: z.string().trim().min(1).max(128).default("UTC")
|
|
52
|
+
}).default({ enabled: false, cron: "* * * * *", timezone: "UTC" })
|
|
53
|
+
}).default({ enabled: false, maxNotificationsPerCheck: 100, rules: [], schedule: { enabled: false, cron: "* * * * *", timezone: "UTC" } }).refine((alerts) => !alerts.enabled || alerts.rules.length > 0),
|
|
54
|
+
/** 可选的流式归档;自动执行由宿主现有 Scheduler 装配。 */
|
|
55
|
+
backup: z.object({
|
|
56
|
+
/** 是否允许手动/计划归档审计;需已初始化的私有 Storage,关闭不影响审计采集。 */
|
|
57
|
+
enabled: z.boolean().default(false),
|
|
58
|
+
/** 每批读取的记录数;较小更省内存但请求更多,不改变归档内容。 */
|
|
59
|
+
batchSize: z.number().int().min(1).max(1e3).default(200),
|
|
60
|
+
/** 单条归档记录的 UTF-8 字节上限;默认 1 MiB,超限报错而不截断原审计。 */
|
|
61
|
+
maxRecordBytes: z.number().int().min(1024).max(64 * 1024 * 1024).default(1024 * 1024),
|
|
62
|
+
/** 自动备份计划;与手动备份开关独立,备份不会删除源日志,也不是原子数据库快照。 */
|
|
63
|
+
schedule: z.object({
|
|
64
|
+
/** 是否由 Scheduler 自动备份;还需 backup.enabled=true,关闭后仍可手动备份。 */
|
|
65
|
+
enabled: z.boolean().default(false),
|
|
66
|
+
/** 备份 Cron 表达式;0 2 * * * 表示在下方时区每天 02:00 执行。 */
|
|
67
|
+
cron: z.string().trim().min(1).max(128).default("0 2 * * *"),
|
|
68
|
+
/** 定时备份采用的 IANA 时区;Asia/Shanghai 为北京时间,UTC 的 02:00 对应北京时间 10:00。 */
|
|
69
|
+
timezone: z.string().trim().min(1).max(128).default("UTC")
|
|
70
|
+
}).default({ enabled: false, cron: "0 2 * * *", timezone: "UTC" })
|
|
71
|
+
}).default({ enabled: false, batchSize: 200, maxRecordBytes: 1024 * 1024, schedule: { enabled: false, cron: "0 2 * * *", timezone: "UTC" } }),
|
|
72
|
+
/** 框架入口自动采集;显式 audit.log 不受此开关影响。 */
|
|
73
|
+
capture: z.object({
|
|
74
|
+
/** 是否自动记录已接入框架入口的重要操作、失败和拒绝;关闭不阻止显式 audit.log 写入。 */
|
|
75
|
+
enabled: z.boolean().default(true),
|
|
76
|
+
/** 是否也采集成功读取事件;开启可追溯查询行为,但会显著增加日志量与存储开销。 */
|
|
77
|
+
includeReads: z.boolean().default(false)
|
|
78
|
+
}).default({ enabled: true, includeReads: false }),
|
|
79
|
+
/** 未传时沿用数据库保留设置;显式关闭才解除保护。 */
|
|
80
|
+
retention: z.object({
|
|
81
|
+
/** 是否启用共享数据库的审计修改/删除/覆盖保护;配置会持久化,移除配置不会解除已有保护。 */
|
|
82
|
+
enabled: z.boolean().default(false),
|
|
83
|
+
/** 保护期天数;例如 90 天内 cleanup(0) 也不能删除,到期不会自动清理日志。 */
|
|
84
|
+
minDays: z.number().int().min(1).max(36500).default(90)
|
|
85
|
+
}).optional(),
|
|
86
|
+
/** 凭据始终脱敏;此开关仅控制业务隐私字段。 */
|
|
87
|
+
privacy: z.object({
|
|
88
|
+
/** 是否对业务隐私字段脱敏;关闭也不会暴露密码、token 等凭据,脱敏前的内容不会恢复。 */
|
|
89
|
+
enabled: z.boolean().default(true),
|
|
90
|
+
/** 额外脱敏的详情字段名,不区分大小写;例如 customerPhone,不要将秘密放进 action/resourceId。 */
|
|
91
|
+
fields: z.array(z.string().trim().min(1).max(256)).max(1e3).default([])
|
|
92
|
+
}).default({ enabled: true, fields: [] }),
|
|
7
93
|
/** 用户表名,用于 list 查询时 LEFT JOIN 获取用户名 */
|
|
8
94
|
userTable: z.string().default("hai_iam_users"),
|
|
9
95
|
/** 用户表主键列名,用于 JOIN 条件 */
|
|
@@ -11,51 +97,25 @@ var AuditInitConfigSchema = z.object({
|
|
|
11
97
|
/** 用户表用户名列名,用于 SELECT 输出 */
|
|
12
98
|
userNameColumn: z.string().default("username")
|
|
13
99
|
});
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
return ok(void 0);
|
|
100
|
+
var storage = new AsyncLocalStorage();
|
|
101
|
+
function getAuditContext() {
|
|
102
|
+
return storage.getStore() ?? {};
|
|
19
103
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
},
|
|
26
|
-
async logout(userId, ip, ua) {
|
|
27
|
-
const result = await logFn({ userId, action: "logout", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
28
|
-
return toVoid(result);
|
|
29
|
-
},
|
|
30
|
-
async register(userId, ip, ua) {
|
|
31
|
-
const result = await logFn({ userId, action: "register", resource: "auth", resourceId: userId, ipAddress: ip, userAgent: ua });
|
|
32
|
-
return toVoid(result);
|
|
33
|
-
},
|
|
34
|
-
async passwordResetRequest(email, ip, ua) {
|
|
35
|
-
const result = await logFn({ action: "password_reset_request", resource: "auth", details: { email }, ipAddress: ip, userAgent: ua });
|
|
36
|
-
return toVoid(result);
|
|
37
|
-
},
|
|
38
|
-
async passwordResetComplete(userId, ip, ua) {
|
|
39
|
-
const result = await logFn({ userId, action: "password_reset", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
40
|
-
return toVoid(result);
|
|
41
|
-
},
|
|
42
|
-
async crud(input) {
|
|
43
|
-
const result = await logFn({
|
|
44
|
-
userId: input.userId,
|
|
45
|
-
action: input.action,
|
|
46
|
-
resource: input.resource,
|
|
47
|
-
resourceId: input.resourceId,
|
|
48
|
-
details: input.details,
|
|
49
|
-
ipAddress: input.ip,
|
|
50
|
-
userAgent: input.ua
|
|
51
|
-
});
|
|
52
|
-
return toVoid(result);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
104
|
+
function withAuditContext(context, operation) {
|
|
105
|
+
const previous = getAuditContext();
|
|
106
|
+
const actor = Object.hasOwn(context, "actor") ? context.actor : previous.actor;
|
|
107
|
+
const snapshot = actor ? { ...actor, ...actor.roles ? { roles: [...actor.roles] } : {} } : actor;
|
|
108
|
+
return storage.run({ ...previous, ...context, actor: snapshot }, operation);
|
|
55
109
|
}
|
|
56
110
|
|
|
57
111
|
// messages/en-US.json
|
|
58
112
|
var en_US_default = {
|
|
113
|
+
audit_eventFieldsInvalid: "Invalid event fields",
|
|
114
|
+
audit_identityMismatch: "Actor snapshot does not match the event identity",
|
|
115
|
+
audit_queryInvalid: "Invalid filters or dates",
|
|
116
|
+
audit_eventProcessingFailed: "Event processing failed",
|
|
117
|
+
audit_sortInvalid: "Invalid sort field or direction",
|
|
118
|
+
audit_retentionPolicyFailed: "Unable to read or update the audit retention policy",
|
|
59
119
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
60
120
|
audit_notInitialized: "Audit module not initialized, call audit.init() first",
|
|
61
121
|
audit_initFailed: "Audit module initialization failed: {error}",
|
|
@@ -63,13 +123,71 @@ var en_US_default = {
|
|
|
63
123
|
audit_queryFailed: "Failed to query audit logs: {error}",
|
|
64
124
|
audit_cleanupFailed: "Failed to cleanup audit logs: {error}",
|
|
65
125
|
audit_statsFailed: "Failed to query audit statistics: {error}",
|
|
126
|
+
audit_invalidStatCount: "Audit count is outside the valid range and cannot be displayed safely",
|
|
66
127
|
audit_configError: "Audit config validation failed: {error}",
|
|
67
128
|
audit_initInProgress: "Audit module initialization is already in progress",
|
|
68
|
-
audit_invalidInput: "Invalid audit input for {field}: {reason}"
|
|
129
|
+
audit_invalidInput: "Invalid audit input for {field}: {reason}",
|
|
130
|
+
audit_backupDisabled: "Audit backup is disabled",
|
|
131
|
+
audit_backupFailed: "Audit backup operation failed",
|
|
132
|
+
audit_backupInvalid: "Invalid audit backup input",
|
|
133
|
+
audit_backupConflict: "This backup ID belongs to a different export request",
|
|
134
|
+
audit_backupStorageRequired: "Audit backups require initialized storage",
|
|
135
|
+
audit_backupPrivateStorageRequired: "Audit backups require private storage; remove the public base URL and restrict archive reads",
|
|
136
|
+
audit_backupVerificationFailed: "Audit archive verification could not complete",
|
|
137
|
+
audit_backupTaskName: "Audit log backup",
|
|
138
|
+
audit_alertsDisabled: "Audit alerts are disabled",
|
|
139
|
+
audit_alertsFailed: "Audit alert check or notification failed",
|
|
140
|
+
audit_alertReachRequired: "Alerts require initialized Reach and valid notification providers",
|
|
141
|
+
audit_alertSubject: "[{severity}] Security alert: {rule}",
|
|
142
|
+
audit_alertTaskName: "Audit anomaly alert check",
|
|
143
|
+
audit_unknownTask: "Unknown audit scheduled task",
|
|
144
|
+
audit_eventTimeUnavailable: "A valid database audit time is unavailable"
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// messages/ja-JP.json
|
|
148
|
+
var ja_JP_default = {
|
|
149
|
+
audit_eventFieldsInvalid: "\u30A4\u30D9\u30F3\u30C8\u9805\u76EE\u304C\u7121\u52B9\u3067\u3059",
|
|
150
|
+
audit_identityMismatch: "\u8EAB\u5143\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3068\u64CD\u4F5C\u8005\u304C\u4E00\u81F4\u3057\u307E\u305B\u3093",
|
|
151
|
+
audit_queryInvalid: "\u691C\u7D22\u6761\u4EF6\u307E\u305F\u306F\u65E5\u4ED8\u304C\u7121\u52B9\u3067\u3059",
|
|
152
|
+
audit_eventProcessingFailed: "\u30A4\u30D9\u30F3\u30C8\u51E6\u7406\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
153
|
+
audit_sortInvalid: "\u4E26\u3079\u66FF\u3048\u9805\u76EE\u307E\u305F\u306F\u65B9\u5411\u304C\u7121\u52B9\u3067\u3059",
|
|
154
|
+
audit_retentionPolicyFailed: "\u76E3\u67FB\u306E\u6700\u4F4E\u4FDD\u6301\u30DD\u30EA\u30B7\u30FC\u3092\u8AAD\u307F\u53D6\u308A\u307E\u305F\u306F\u66F4\u65B0\u3067\u304D\u307E\u305B\u3093",
|
|
155
|
+
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
156
|
+
audit_notInitialized: "\u76E3\u67FB\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Baudit.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
157
|
+
audit_initFailed: "\u76E3\u67FB\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
158
|
+
audit_logFailed: "\u76E3\u67FB\u30ED\u30B0\u306E\u8A18\u9332\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
159
|
+
audit_queryFailed: "\u76E3\u67FB\u30ED\u30B0\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
160
|
+
audit_cleanupFailed: "\u76E3\u67FB\u30ED\u30B0\u306E\u30AF\u30EA\u30FC\u30F3\u30A2\u30C3\u30D7\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
161
|
+
audit_statsFailed: "\u76E3\u67FB\u7D71\u8A08\u306E\u53D6\u5F97\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
162
|
+
audit_invalidStatCount: "\u76E3\u67FB\u4EF6\u6570\u304C\u6709\u52B9\u7BC4\u56F2\u3092\u8D85\u3048\u3066\u3044\u308B\u305F\u3081\u3001\u5B89\u5168\u306B\u8868\u793A\u3067\u304D\u307E\u305B\u3093",
|
|
163
|
+
audit_configError: "\u76E3\u67FB\u8A2D\u5B9A\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
164
|
+
audit_initInProgress: "\u76E3\u67FB\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306F\u65E2\u306B\u5B9F\u884C\u4E2D\u3067\u3059",
|
|
165
|
+
audit_invalidInput: "{field}\u306E\u76E3\u67FB\u5165\u529B\u304C\u7121\u52B9\u3067\u3059: {reason}",
|
|
166
|
+
audit_backupDisabled: "\u76E3\u67FB\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u306F\u7121\u52B9\u3067\u3059",
|
|
167
|
+
audit_backupFailed: "\u76E3\u67FB\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u64CD\u4F5C\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
168
|
+
audit_backupInvalid: "\u76E3\u67FB\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u306E\u5165\u529B\u304C\u7121\u52B9\u3067\u3059",
|
|
169
|
+
audit_backupConflict: "\u3053\u306E\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7 ID \u306F\u7570\u306A\u308B\u30A8\u30AF\u30B9\u30DD\u30FC\u30C8\u6761\u4EF6\u3067\u4F7F\u7528\u3055\u308C\u3066\u3044\u307E\u3059",
|
|
170
|
+
audit_backupStorageRequired: "\u76E3\u67FB\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u306B\u306F\u521D\u671F\u5316\u6E08\u307F\u30B9\u30C8\u30EC\u30FC\u30B8\u304C\u5FC5\u8981\u3067\u3059",
|
|
171
|
+
audit_backupPrivateStorageRequired: "\u76E3\u67FB\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7\u306B\u306F\u975E\u516C\u958B\u30B9\u30C8\u30EC\u30FC\u30B8\u304C\u5FC5\u8981\u3067\u3059\u3002\u516C\u958B URL \u3092\u524A\u9664\u3057\u3001\u30A2\u30FC\u30AB\u30A4\u30D6\u306E\u8AAD\u53D6\u3092\u5236\u9650\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
172
|
+
audit_backupVerificationFailed: "\u76E3\u67FB\u30A2\u30FC\u30AB\u30A4\u30D6\u306E\u691C\u8A3C\u3092\u5B8C\u4E86\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F",
|
|
173
|
+
audit_backupTaskName: "\u76E3\u67FB\u30ED\u30B0\u306E\u30D0\u30C3\u30AF\u30A2\u30C3\u30D7",
|
|
174
|
+
audit_alertsDisabled: "\u76E3\u67FB\u30A2\u30E9\u30FC\u30C8\u306F\u7121\u52B9\u3067\u3059",
|
|
175
|
+
audit_alertsFailed: "\u76E3\u67FB\u30A2\u30E9\u30FC\u30C8\u306E\u78BA\u8A8D\u307E\u305F\u306F\u901A\u77E5\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
176
|
+
audit_alertReachRequired: "\u521D\u671F\u5316\u6E08\u307F\u306E Reach \u3068\u6709\u52B9\u306A\u901A\u77E5\u30C1\u30E3\u30CD\u30EB\u304C\u5FC5\u8981\u3067\u3059",
|
|
177
|
+
audit_alertSubject: "[{severity}] \u30BB\u30AD\u30E5\u30EA\u30C6\u30A3\u30A2\u30E9\u30FC\u30C8\uFF1A{rule}",
|
|
178
|
+
audit_alertTaskName: "\u76E3\u67FB\u7570\u5E38\u30A2\u30E9\u30FC\u30C8\u78BA\u8A8D",
|
|
179
|
+
audit_unknownTask: "\u4E0D\u660E\u306A\u76E3\u67FB\u5B9A\u671F\u30BF\u30B9\u30AF\u3067\u3059",
|
|
180
|
+
audit_eventTimeUnavailable: "\u6709\u52B9\u306A\u76E3\u67FB\u7528 DB \u6642\u523B\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093"
|
|
69
181
|
};
|
|
70
182
|
|
|
71
183
|
// messages/zh-CN.json
|
|
72
184
|
var zh_CN_default = {
|
|
185
|
+
audit_eventFieldsInvalid: "\u4E8B\u4EF6\u5B57\u6BB5\u65E0\u6548",
|
|
186
|
+
audit_identityMismatch: "\u8EAB\u4EFD\u5FEB\u7167\u4E0E\u64CD\u4F5C\u8005\u4E0D\u4E00\u81F4",
|
|
187
|
+
audit_queryInvalid: "\u7B5B\u9009\u6761\u4EF6\u6216\u65E5\u671F\u65E0\u6548",
|
|
188
|
+
audit_eventProcessingFailed: "\u4E8B\u4EF6\u5904\u7406\u5931\u8D25",
|
|
189
|
+
audit_sortInvalid: "\u6392\u5E8F\u5B57\u6BB5\u6216\u65B9\u5411\u65E0\u6548",
|
|
190
|
+
audit_retentionPolicyFailed: "\u65E0\u6CD5\u8BFB\u53D6\u6216\u66F4\u65B0\u5BA1\u8BA1\u6700\u4F4E\u4FDD\u7559\u7B56\u7565",
|
|
73
191
|
$schema: "https://inlang.com/schema/inlang-message-format",
|
|
74
192
|
audit_notInitialized: "\u5BA1\u8BA1\u6A21\u5757\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 audit.init()",
|
|
75
193
|
audit_initFailed: "\u5BA1\u8BA1\u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25\uFF1A{error}",
|
|
@@ -77,15 +195,32 @@ var zh_CN_default = {
|
|
|
77
195
|
audit_queryFailed: "\u5BA1\u8BA1\u65E5\u5FD7\u67E5\u8BE2\u5931\u8D25\uFF1A{error}",
|
|
78
196
|
audit_cleanupFailed: "\u5BA1\u8BA1\u65E5\u5FD7\u6E05\u7406\u5931\u8D25\uFF1A{error}",
|
|
79
197
|
audit_statsFailed: "\u5BA1\u8BA1\u7EDF\u8BA1\u67E5\u8BE2\u5931\u8D25\uFF1A{error}",
|
|
198
|
+
audit_invalidStatCount: "\u5BA1\u8BA1\u7EDF\u8BA1\u6570\u91CF\u8D85\u51FA\u6709\u6548\u8303\u56F4\uFF0C\u65E0\u6CD5\u5B89\u5168\u663E\u793A",
|
|
80
199
|
audit_configError: "\u5BA1\u8BA1\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
|
|
81
200
|
audit_initInProgress: "\u5BA1\u8BA1\u6A21\u5757\u6B63\u5728\u521D\u59CB\u5316\u4E2D\uFF0C\u8BF7\u52FF\u5E76\u53D1\u8C03\u7528",
|
|
82
|
-
audit_invalidInput: "\u5BA1\u8BA1\u8F93\u5165\u53C2\u6570\u65E0\u6548\uFF08{field}\uFF09\uFF1A{reason}"
|
|
201
|
+
audit_invalidInput: "\u5BA1\u8BA1\u8F93\u5165\u53C2\u6570\u65E0\u6548\uFF08{field}\uFF09\uFF1A{reason}",
|
|
202
|
+
audit_backupDisabled: "\u5BA1\u8BA1\u5907\u4EFD\u672A\u542F\u7528",
|
|
203
|
+
audit_backupFailed: "\u5BA1\u8BA1\u5907\u4EFD\u64CD\u4F5C\u5931\u8D25",
|
|
204
|
+
audit_backupInvalid: "\u5BA1\u8BA1\u5907\u4EFD\u53C2\u6570\u65E0\u6548",
|
|
205
|
+
audit_backupConflict: "\u6B64\u5907\u4EFD\u7F16\u53F7\u5DF2\u7528\u4E8E\u4E0D\u540C\u7684\u5BFC\u51FA\u6761\u4EF6",
|
|
206
|
+
audit_backupStorageRequired: "\u542F\u7528\u5BA1\u8BA1\u5907\u4EFD\u9700\u8981\u5DF2\u521D\u59CB\u5316\u7684\u5B58\u50A8",
|
|
207
|
+
audit_backupPrivateStorageRequired: "\u5BA1\u8BA1\u5907\u4EFD\u9700\u8981\u79C1\u6709\u5B58\u50A8\uFF0C\u8BF7\u79FB\u9664\u516C\u5F00\u8BBF\u95EE\u57FA\u7840 URL \u5E76\u9650\u5236\u5F52\u6863\u8BFB\u53D6",
|
|
208
|
+
audit_backupVerificationFailed: "\u65E0\u6CD5\u5B8C\u6210\u5BA1\u8BA1\u5F52\u6863\u6821\u9A8C",
|
|
209
|
+
audit_backupTaskName: "\u5BA1\u8BA1\u65E5\u5FD7\u5907\u4EFD",
|
|
210
|
+
audit_alertsDisabled: "\u5BA1\u8BA1\u544A\u8B66\u672A\u542F\u7528",
|
|
211
|
+
audit_alertsFailed: "\u5BA1\u8BA1\u544A\u8B66\u68C0\u67E5\u6216\u901A\u77E5\u5931\u8D25",
|
|
212
|
+
audit_alertReachRequired: "\u544A\u8B66\u9700\u8981\u5DF2\u521D\u59CB\u5316\u7684 Reach \u548C\u6709\u6548\u901A\u77E5\u6E20\u9053",
|
|
213
|
+
audit_alertSubject: "[{severity}] \u5B89\u5168\u544A\u8B66\uFF1A{rule}",
|
|
214
|
+
audit_alertTaskName: "\u5BA1\u8BA1\u5F02\u5E38\u544A\u8B66\u68C0\u67E5",
|
|
215
|
+
audit_unknownTask: "\u672A\u77E5\u5BA1\u8BA1\u5B9A\u65F6\u4EFB\u52A1",
|
|
216
|
+
audit_eventTimeUnavailable: "\u65E0\u6CD5\u53D6\u5F97\u6709\u6548\u7684\u6570\u636E\u5E93\u5BA1\u8BA1\u65F6\u95F4"
|
|
83
217
|
};
|
|
84
218
|
|
|
85
219
|
// src/audit-i18n.ts
|
|
86
220
|
var auditM = core.i18n.createMessageGetter({
|
|
87
221
|
"zh-CN": zh_CN_default,
|
|
88
|
-
"en-US": en_US_default
|
|
222
|
+
"en-US": en_US_default,
|
|
223
|
+
"ja-JP": ja_JP_default
|
|
89
224
|
});
|
|
90
225
|
var AuditErrorInfo = {
|
|
91
226
|
LOG_FAILED: "001:500",
|
|
@@ -94,13 +229,721 @@ var AuditErrorInfo = {
|
|
|
94
229
|
STATS_FAILED: "004:500",
|
|
95
230
|
INIT_IN_PROGRESS: "005:409",
|
|
96
231
|
NOT_INITIALIZED: "010:500",
|
|
97
|
-
CONFIG_ERROR: "012:500"
|
|
232
|
+
CONFIG_ERROR: "012:500",
|
|
233
|
+
BACKUP_DISABLED: "020:403",
|
|
234
|
+
BACKUP_FAILED: "021:500",
|
|
235
|
+
BACKUP_INVALID: "022:400",
|
|
236
|
+
BACKUP_CONFLICT: "023:409",
|
|
237
|
+
ALERTS_DISABLED: "024:403",
|
|
238
|
+
ALERTS_FAILED: "025:500"
|
|
98
239
|
};
|
|
99
240
|
var HaiAuditError = core.error.buildHaiErrorsDef("audit", AuditErrorInfo);
|
|
241
|
+
var AUDIT_BACKUP_PREFIX = "hai-private/audit-backups/";
|
|
242
|
+
|
|
243
|
+
// src/audit-database-time.ts
|
|
244
|
+
function auditClockSql() {
|
|
245
|
+
return reldb.config?.type === "sqlite" ? "CAST(ROUND((julianday('now')-2440587.5)*86400000) AS INTEGER)" : reldb.config?.type === "postgresql" ? "CAST(EXTRACT(EPOCH FROM clock_timestamp())*1000 AS BIGINT)" : "CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3))*1000 AS SIGNED)";
|
|
246
|
+
}
|
|
247
|
+
async function readAuditTime(sql = reldb.sql) {
|
|
248
|
+
const result = await sql.get(`SELECT ${auditClockSql()} AS now_ms`);
|
|
249
|
+
if (!result.success)
|
|
250
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_eventTimeUnavailable"), result.error);
|
|
251
|
+
const value = Number(result.data?.now_ms);
|
|
252
|
+
if (!Number.isSafeInteger(value) || value < 0 || !Number.isFinite(new Date(value).getTime()))
|
|
253
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_eventTimeUnavailable"));
|
|
254
|
+
return ok(new Date(value));
|
|
255
|
+
}
|
|
100
256
|
|
|
101
|
-
// src/audit-
|
|
102
|
-
var logger = core.logger.child({ module: "audit", scope: "
|
|
257
|
+
// src/audit-alert-functions.ts
|
|
258
|
+
var logger = core.logger.child({ module: "audit", scope: "alerts" });
|
|
259
|
+
var levels = ["info", "warning", "error", "critical"];
|
|
260
|
+
var table = "hai_audit_alert_claims";
|
|
261
|
+
var hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
262
|
+
function createAuditAlerts(input) {
|
|
263
|
+
let active = true;
|
|
264
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
265
|
+
const pending = /* @__PURE__ */ new Set();
|
|
266
|
+
async function run(signal) {
|
|
267
|
+
if (!input.config.enabled)
|
|
268
|
+
return err(HaiAuditError.ALERTS_DISABLED, auditM("audit_alertsDisabled"));
|
|
269
|
+
signal.throwIfAborted();
|
|
270
|
+
const type = reldb.config?.type;
|
|
271
|
+
const clock = auditClockSql();
|
|
272
|
+
const time = await readAuditTime();
|
|
273
|
+
if (!time.success)
|
|
274
|
+
return time;
|
|
275
|
+
const now = time.data.getTime();
|
|
276
|
+
const report = { submitted: 0, queued: 0, failed: 0, more: false };
|
|
277
|
+
let attempted = 0;
|
|
278
|
+
for (const rule of input.config.rules) {
|
|
279
|
+
const duration = rule.windowSeconds * 1e3;
|
|
280
|
+
const current = Math.floor(now / duration) * duration;
|
|
281
|
+
for (const start of [current - duration, current]) {
|
|
282
|
+
signal.throwIfAborted();
|
|
283
|
+
const end = Math.min(start + duration, now + 1);
|
|
284
|
+
const actions = [...new Set(rule.actions)].sort();
|
|
285
|
+
const outcomes = [...new Set(rule.outcomes)].sort();
|
|
286
|
+
const severities = levels.slice(levels.indexOf(rule.minimumSeverity));
|
|
287
|
+
const date = (value) => type === "sqlite" ? value : new Date(value);
|
|
288
|
+
const conditions = ["created_at >= ?", "created_at < ?", "resource <> ?", `action IN (${actions.map(() => "?").join(",")})`];
|
|
289
|
+
const params = [date(start), date(end), "audit.alert", ...actions];
|
|
290
|
+
if (outcomes.length) {
|
|
291
|
+
conditions.push(`outcome IN (${outcomes.map(() => "?").join(",")})`);
|
|
292
|
+
params.push(...outcomes);
|
|
293
|
+
}
|
|
294
|
+
if (rule.minimumSeverity !== "info") {
|
|
295
|
+
conditions.push(`severity IN (${severities.map(() => "?").join(",")})`);
|
|
296
|
+
params.push(...severities);
|
|
297
|
+
}
|
|
298
|
+
if (rule.resource) {
|
|
299
|
+
conditions.push("resource = ?");
|
|
300
|
+
params.push(rule.resource);
|
|
301
|
+
}
|
|
302
|
+
const counted = await reldb.sql.get(`SELECT COUNT(*) AS count FROM hai_audit_logs WHERE ${conditions.join(" AND ")}`, params);
|
|
303
|
+
if (!counted.success)
|
|
304
|
+
return counted;
|
|
305
|
+
const count = Number(counted.data?.count);
|
|
306
|
+
if (!Number.isSafeInteger(count) || count < 0)
|
|
307
|
+
return err(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"));
|
|
308
|
+
if (count < rule.threshold)
|
|
309
|
+
continue;
|
|
310
|
+
for (const target of rule.targets) {
|
|
311
|
+
signal.throwIfAborted();
|
|
312
|
+
const id = hash({ rule: { ...rule, actions, outcomes, targets: void 0 }, target, start });
|
|
313
|
+
if (attempted >= input.config.maxNotificationsPerCheck) {
|
|
314
|
+
const existing = await reldb.sql.get(`SELECT id FROM ${table} WHERE id=?`, [id]);
|
|
315
|
+
if (!existing.success)
|
|
316
|
+
return existing;
|
|
317
|
+
if (!existing.data)
|
|
318
|
+
report.more = true;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const selection = `SELECT ?, ?, ?, ?, 'claimed' WHERE ? <= ${clock} AND ? > ${clock}`;
|
|
322
|
+
const statement = type === "mysql" ? `INSERT IGNORE INTO ${table} (id, rule_id, target_id, window_start, status) ${selection}` : `INSERT INTO ${table} (id, rule_id, target_id, window_start, status) ${selection} ON CONFLICT(id) DO NOTHING`;
|
|
323
|
+
const claimed = await reldb.sql.execute(statement, [id, rule.id, target.id, start, start, start + duration * 2]);
|
|
324
|
+
if (!claimed.success)
|
|
325
|
+
return claimed;
|
|
326
|
+
if (claimed.data.changes === 0)
|
|
327
|
+
continue;
|
|
328
|
+
attempted++;
|
|
329
|
+
const details = { ruleId: rule.id, targetId: target.id, count, windowStart: new Date(start).toISOString(), windowEnd: new Date(start + duration).toISOString(), level: rule.severity };
|
|
330
|
+
const recorded = await input.log({ action: "audit.alert.claimed", resource: "audit.alert", resourceId: id, category: "system", severity: rule.severity, outcome: "success", details });
|
|
331
|
+
if (!recorded.success)
|
|
332
|
+
return recorded;
|
|
333
|
+
signal.throwIfAborted();
|
|
334
|
+
const sending = await reldb.sql.execute(`UPDATE ${table} SET status='sending' WHERE id=? AND status='claimed' AND ? > ${clock}`, [id, start + duration * 2]);
|
|
335
|
+
if (!sending.success)
|
|
336
|
+
return sending;
|
|
337
|
+
if (sending.data.changes !== 1)
|
|
338
|
+
continue;
|
|
339
|
+
const sent = await input.reach.send({ provider: target.provider, to: target.to, subject: auditM("audit_alertSubject", { params: { severity: rule.severity, rule: rule.id } }), body: JSON.stringify(details), idempotencyKey: id });
|
|
340
|
+
const accepted = sent.success && sent.data.success;
|
|
341
|
+
const status = accepted ? sent.data.deferred ? "queued" : "submitted" : "unknown";
|
|
342
|
+
const updated = await reldb.sql.execute(`UPDATE ${table} SET status=?, queue_id=?, error_code=? WHERE id=? AND status='sending'`, [status, sent.success && sent.data.deferred ? sent.data.queueId : null, sent.success ? null : sent.error.code, id]);
|
|
343
|
+
if (!updated.success || updated.data.changes !== 1)
|
|
344
|
+
return err(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"), updated.success ? void 0 : updated.error);
|
|
345
|
+
if (status === "queued")
|
|
346
|
+
report.queued++;
|
|
347
|
+
else if (status === "submitted")
|
|
348
|
+
report.submitted++;
|
|
349
|
+
else
|
|
350
|
+
report.failed++;
|
|
351
|
+
const outcome = await input.log({ action: "audit.alert.notification", resource: "audit.alert", resourceId: id, category: "system", severity: rule.severity, outcome: accepted ? "success" : "failure", details: { ...details, status, ...sent.success ? {} : { reasonCode: sent.error.code }, ...sent.success && sent.data.deferred ? { queueId: sent.data.queueId } : {} } });
|
|
352
|
+
if (!outcome.success)
|
|
353
|
+
return outcome;
|
|
354
|
+
if (!accepted)
|
|
355
|
+
logger.error("Audit alert notification outcome requires review", { id, ruleId: rule.id, targetId: target.id, errorCode: sent.success ? void 0 : sent.error.code });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const oldest = now - Math.max(...input.config.rules.map((rule) => rule.windowSeconds * 1e3)) * 3;
|
|
360
|
+
const cleaned = await reldb.sql.execute(`DELETE FROM ${table} WHERE window_start < ? AND status IN ('submitted', 'queued')`, [oldest]);
|
|
361
|
+
return cleaned.success ? ok(report) : cleaned;
|
|
362
|
+
}
|
|
363
|
+
const check = (options) => {
|
|
364
|
+
if (!active)
|
|
365
|
+
return Promise.resolve(err(HaiAuditError.NOT_INITIALIZED, auditM("audit_notInitialized")));
|
|
366
|
+
if (options?.signal !== void 0 && !(options.signal instanceof AbortSignal))
|
|
367
|
+
return Promise.resolve(err(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed")));
|
|
368
|
+
const controller = new AbortController();
|
|
369
|
+
const cancel = () => controller.abort();
|
|
370
|
+
options?.signal?.addEventListener("abort", cancel, { once: true });
|
|
371
|
+
if (options?.signal?.aborted)
|
|
372
|
+
cancel();
|
|
373
|
+
controllers.add(controller);
|
|
374
|
+
const operation = withAuditContext({ actor: { id: "system:audit-alerts", username: "scheduler" }, category: "system" }, () => run(controller.signal)).catch((error) => {
|
|
375
|
+
logger.error("Audit alert check failed", { error });
|
|
376
|
+
return err(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"), error);
|
|
377
|
+
});
|
|
378
|
+
pending.add(operation);
|
|
379
|
+
void operation.finally(() => {
|
|
380
|
+
controllers.delete(controller);
|
|
381
|
+
pending.delete(operation);
|
|
382
|
+
options?.signal?.removeEventListener("abort", cancel);
|
|
383
|
+
});
|
|
384
|
+
return operation;
|
|
385
|
+
};
|
|
386
|
+
let schedule = null;
|
|
387
|
+
if (input.config.enabled && input.config.schedule.enabled) {
|
|
388
|
+
schedule = {
|
|
389
|
+
tasks: [{ id: "hai-audit-alerts", name: auditM("audit_alertTaskName"), idempotent: true, cron: input.config.schedule.cron, timezone: input.config.schedule.timezone }],
|
|
390
|
+
hooks: { onTaskExecute: async ({ context, signal }) => {
|
|
391
|
+
if (context.taskId !== "hai-audit-alerts")
|
|
392
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"));
|
|
393
|
+
const result = await check({ signal });
|
|
394
|
+
if (!result.success || result.data.failed > 0)
|
|
395
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"), result.success ? void 0 : result.error);
|
|
396
|
+
return result.data;
|
|
397
|
+
} }
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
check,
|
|
402
|
+
schedule,
|
|
403
|
+
async close() {
|
|
404
|
+
active = false;
|
|
405
|
+
controllers.forEach((controller) => controller.abort());
|
|
406
|
+
await Promise.allSettled([...pending]);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
var logger2 = core.logger.child({ module: "audit", scope: "writer" });
|
|
411
|
+
var actorSchema = z.object({ id: z.string().min(1).max(256), username: z.string().max(256).optional(), displayName: z.string().max(256).optional(), roles: z.array(z.string().max(256)).max(1e3).optional() });
|
|
412
|
+
var detailsSchema = z.record(z.string(), z.unknown());
|
|
413
|
+
var inputSchema = z.object({
|
|
414
|
+
userId: z.string().min(1).max(256).nullable().optional(),
|
|
415
|
+
actorSnapshot: actorSchema.nullable().optional(),
|
|
416
|
+
action: z.string().trim().min(1).max(256),
|
|
417
|
+
resource: z.string().trim().min(1).max(256),
|
|
418
|
+
resourceId: z.string().max(256).nullable().optional(),
|
|
419
|
+
category: z.enum(["system", "business"]).nullable().optional(),
|
|
420
|
+
outcome: z.enum(["success", "failure", "denied"]).nullable().optional(),
|
|
421
|
+
severity: z.enum(["info", "warning", "error", "critical"]).nullable().optional(),
|
|
422
|
+
requestId: z.string().max(256).nullable().optional(),
|
|
423
|
+
traceId: z.string().max(256).nullable().optional(),
|
|
424
|
+
scopeId: z.string().max(256).nullable().optional(),
|
|
425
|
+
ipAddress: z.string().max(256).nullable().optional(),
|
|
426
|
+
userAgent: z.string().max(4096).nullable().optional(),
|
|
427
|
+
details: detailsSchema.nullable().optional()
|
|
428
|
+
});
|
|
429
|
+
var querySchema = z.object({
|
|
430
|
+
requestId: z.string().min(1).max(256).optional(),
|
|
431
|
+
traceId: z.string().min(1).max(256).optional(),
|
|
432
|
+
userId: z.string().min(1).max(256).optional(),
|
|
433
|
+
resource: z.string().min(1).max(256).optional(),
|
|
434
|
+
action: z.string().min(1).max(256).optional(),
|
|
435
|
+
scopeId: z.string().min(1).max(256).optional(),
|
|
436
|
+
category: z.enum(["system", "business"]).optional(),
|
|
437
|
+
outcome: z.enum(["success", "failure", "denied"]).optional(),
|
|
438
|
+
severity: z.enum(["info", "warning", "error", "critical"]).optional(),
|
|
439
|
+
sortBy: z.enum(["createdAt", "action", "resource", "category", "outcome", "severity"]).optional(),
|
|
440
|
+
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
441
|
+
startDate: z.date().optional(),
|
|
442
|
+
endDate: z.date().optional()
|
|
443
|
+
});
|
|
444
|
+
function validAuditQuery(options) {
|
|
445
|
+
return querySchema.safeParse(options ?? {}).success;
|
|
446
|
+
}
|
|
447
|
+
function readAuditActor(value) {
|
|
448
|
+
if (value == null)
|
|
449
|
+
return null;
|
|
450
|
+
const parsed = actorSchema.safeParse(typeof value === "string" ? JSON.parse(value) : value);
|
|
451
|
+
if (!parsed.success)
|
|
452
|
+
throw new Error("Invalid audit actor snapshot");
|
|
453
|
+
return parsed.data;
|
|
454
|
+
}
|
|
455
|
+
function createAuditWriter(repository, config) {
|
|
456
|
+
const fields = new Set(config.privacy.fields.map((field) => field.toLowerCase()));
|
|
457
|
+
const businessPrivacy = /email|phone|mobile|idcard|identitynumber|bankaccount|shippingaddress|billingaddress|realname|identifier|recipient|邮箱|手机|电话|身份证|银行卡|地址|真实姓名|客户姓名/i;
|
|
458
|
+
return async (input, tx) => {
|
|
459
|
+
try {
|
|
460
|
+
const parsed = inputSchema.safeParse(input);
|
|
461
|
+
if (!parsed.success) {
|
|
462
|
+
logger2.error("Audit input rejected", { issues: parsed.error.issues.map((issue) => issue.path.join(".")) });
|
|
463
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_invalidInput", { params: { field: "event", reason: auditM("audit_eventFieldsInvalid") } }));
|
|
464
|
+
}
|
|
465
|
+
const value = parsed.data;
|
|
466
|
+
const context = getAuditContext();
|
|
467
|
+
const correlation = core.context.get();
|
|
468
|
+
const actor = Object.hasOwn(value, "actorSnapshot") ? value.actorSnapshot : context.actor;
|
|
469
|
+
const userId = value.userId !== void 0 ? value.userId : actor?.id ?? correlation.userId ?? null;
|
|
470
|
+
if (value.actorSnapshot && value.actorSnapshot.id !== userId) {
|
|
471
|
+
logger2.error("Audit actor snapshot rejected", { reason: auditM("audit_identityMismatch") });
|
|
472
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_invalidInput", { params: { field: "actorSnapshot", reason: auditM("audit_identityMismatch") } }));
|
|
473
|
+
}
|
|
474
|
+
const snapshot = actor?.id === userId ? readAuditActor(actor) : userId ? await repository.captureActor(userId, tx) : null;
|
|
475
|
+
const outcome = value.outcome ?? "success";
|
|
476
|
+
const jsonDetails = value.details == null ? null : JSON.parse(JSON.stringify(value.details, (key, entry) => {
|
|
477
|
+
const sensitive = core.sanitize.isSensitiveKey(key) || config.privacy.enabled && (businessPrivacy.test(key) || fields.has(key.toLowerCase()));
|
|
478
|
+
return sensitive ? "[REDACTED]" : entry;
|
|
479
|
+
}));
|
|
480
|
+
const normalizedDetails = jsonDetails == null ? null : detailsSchema.parse(jsonDetails);
|
|
481
|
+
const credentialSafe = normalizedDetails == null ? null : core.sanitize.sanitizeSensitiveFields(normalizedDetails, { replacement: "[REDACTED]" });
|
|
482
|
+
const details = !config.privacy.enabled || credentialSafe == null ? credentialSafe : core.sanitize.sanitizeSensitiveFields(credentialSafe, {
|
|
483
|
+
matcher: (key) => businessPrivacy.test(key) || fields.has(key.toLowerCase()),
|
|
484
|
+
replacement: "[REDACTED]"
|
|
485
|
+
});
|
|
486
|
+
const result = await repository.log({
|
|
487
|
+
...value,
|
|
488
|
+
category: value.category ?? context.category ?? "business",
|
|
489
|
+
outcome,
|
|
490
|
+
severity: value.severity ?? (outcome === "denied" ? "warning" : outcome === "failure" ? "error" : "info"),
|
|
491
|
+
userId,
|
|
492
|
+
actorSnapshot: snapshot,
|
|
493
|
+
requestId: value.requestId ?? context.requestId ?? correlation.requestId ?? null,
|
|
494
|
+
traceId: value.traceId ?? context.traceId ?? correlation.traceId ?? correlation.requestId ?? null,
|
|
495
|
+
scopeId: value.scopeId ?? context.scopeId ?? correlation.scopeId ?? null,
|
|
496
|
+
ipAddress: value.ipAddress ?? context.ipAddress ?? null,
|
|
497
|
+
userAgent: value.userAgent ?? context.userAgent ?? null,
|
|
498
|
+
details
|
|
499
|
+
}, tx);
|
|
500
|
+
if (!result.success)
|
|
501
|
+
logger2.error("Audit event persistence failed", { action: value.action, resource: value.resource, requestId: value.requestId ?? context.requestId, error: result.error });
|
|
502
|
+
return result;
|
|
503
|
+
} catch (error) {
|
|
504
|
+
logger2.error("Audit event processing failed", { requestId: getAuditContext().requestId, error });
|
|
505
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_logFailed", { params: { error: auditM("audit_eventProcessingFailed") } }), error);
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/audit-backup-format.ts
|
|
511
|
+
var backupIdSchema = z.string().regex(/^[a-z0-9][\w-]{0,127}$/i);
|
|
512
|
+
var digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
513
|
+
var backupHeaderSchema = z.object({
|
|
514
|
+
type: z.literal("header"),
|
|
515
|
+
format: z.literal("hai-audit"),
|
|
516
|
+
version: z.literal(1),
|
|
517
|
+
id: backupIdSchema,
|
|
518
|
+
createdAt: z.iso.datetime(),
|
|
519
|
+
cutoff: z.iso.datetime(),
|
|
520
|
+
from: z.iso.datetime().nullable(),
|
|
521
|
+
requestHash: digestSchema
|
|
522
|
+
});
|
|
523
|
+
var footerSchema = z.object({ type: z.literal("footer"), recordCount: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER), sha256: digestSchema });
|
|
524
|
+
var entrySchema = z.object({ type: z.literal("entry"), record: z.object({ id: z.string().min(1), action: z.string().min(1), resource: z.string().min(1), createdAt: z.iso.datetime(), actorSnapshot: z.unknown().optional() }).passthrough() });
|
|
525
|
+
function archiveLine(value, maxRecordBytes) {
|
|
526
|
+
const encoded = Buffer.from(`${JSON.stringify(value)}
|
|
527
|
+
`, "utf8");
|
|
528
|
+
if (encoded.byteLength > maxRecordBytes)
|
|
529
|
+
throw new Error("Audit archive record exceeds size limit");
|
|
530
|
+
return encoded;
|
|
531
|
+
}
|
|
532
|
+
async function* lines(source, maxBytes, signal) {
|
|
533
|
+
let fragments = [];
|
|
534
|
+
let length = 0;
|
|
535
|
+
for await (const chunk of source) {
|
|
536
|
+
signal?.throwIfAborted();
|
|
537
|
+
const buffer = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
538
|
+
let offset = 0;
|
|
539
|
+
while (offset < buffer.byteLength) {
|
|
540
|
+
const newline = buffer.indexOf(10, offset);
|
|
541
|
+
const end = newline === -1 ? buffer.byteLength : newline + 1;
|
|
542
|
+
const part = buffer.subarray(offset, end);
|
|
543
|
+
length += part.byteLength;
|
|
544
|
+
if (length > maxBytes)
|
|
545
|
+
throw new Error("Audit archive record exceeds size limit");
|
|
546
|
+
fragments.push(part);
|
|
547
|
+
offset = end;
|
|
548
|
+
if (newline !== -1) {
|
|
549
|
+
yield Buffer.concat(fragments, length);
|
|
550
|
+
fragments = [];
|
|
551
|
+
length = 0;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
signal?.throwIfAborted();
|
|
556
|
+
if (length !== 0)
|
|
557
|
+
throw new Error("Audit archive is truncated");
|
|
558
|
+
}
|
|
559
|
+
async function verifyArchive(source, options) {
|
|
560
|
+
const hash2 = createHash("sha256");
|
|
561
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
562
|
+
let header;
|
|
563
|
+
let footer;
|
|
564
|
+
let count = 0;
|
|
565
|
+
for await (const line of lines(source, options.maxRecordBytes, options.signal)) {
|
|
566
|
+
if (footer)
|
|
567
|
+
throw new Error("Audit archive has trailing records");
|
|
568
|
+
const value = JSON.parse(decoder.decode(line));
|
|
569
|
+
if (!header) {
|
|
570
|
+
header = backupHeaderSchema.parse(value);
|
|
571
|
+
if (options.key !== `${AUDIT_BACKUP_PREFIX}${header.id}.jsonl`)
|
|
572
|
+
throw new Error("Audit archive identity mismatch");
|
|
573
|
+
hash2.update(line);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const end = footerSchema.safeParse(value);
|
|
577
|
+
if (end.success) {
|
|
578
|
+
footer = end.data;
|
|
579
|
+
if (footer.recordCount !== count || footer.sha256 !== hash2.digest("hex"))
|
|
580
|
+
throw new Error("Audit archive checksum or count mismatch");
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const entry = entrySchema.parse(value);
|
|
584
|
+
readAuditActor(entry.record.actorSnapshot);
|
|
585
|
+
if (!Number.isSafeInteger(++count))
|
|
586
|
+
throw new Error("Audit archive record count exceeds safe range");
|
|
587
|
+
hash2.update(line);
|
|
588
|
+
}
|
|
589
|
+
if (!header || !footer)
|
|
590
|
+
throw new Error("Audit archive is incomplete");
|
|
591
|
+
return { info: { id: header.id, key: options.key, recordCount: footer.recordCount, sha256: footer.sha256, createdAt: new Date(header.createdAt), cutoff: new Date(header.cutoff) }, requestHash: header.requestHash };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/audit-backup-functions.ts
|
|
595
|
+
var logger3 = core.logger.child({ module: "audit", scope: "backup" });
|
|
596
|
+
var optionsSchema = z.object({ id: backupIdSchema.optional(), from: z.date().optional(), to: z.date().optional(), signal: z.instanceof(AbortSignal).optional() });
|
|
597
|
+
function createAuditBackup(input) {
|
|
598
|
+
let active = true;
|
|
599
|
+
const controllers = /* @__PURE__ */ new Set();
|
|
600
|
+
const pending = /* @__PURE__ */ new Set();
|
|
601
|
+
function track(operation, event, parent) {
|
|
602
|
+
if (!active)
|
|
603
|
+
return Promise.resolve(err(HaiAuditError.NOT_INITIALIZED, auditM("audit_notInitialized")));
|
|
604
|
+
const controller = new AbortController();
|
|
605
|
+
const signal = parent ? AbortSignal.any([parent, controller.signal]) : controller.signal;
|
|
606
|
+
controllers.add(controller);
|
|
607
|
+
const promise = Promise.resolve().then(async () => {
|
|
608
|
+
if (!input.config.enabled || !input.storage?.isInitialized)
|
|
609
|
+
return err(HaiAuditError.BACKUP_DISABLED, auditM("audit_backupDisabled"));
|
|
610
|
+
signal.throwIfAborted();
|
|
611
|
+
return operation(signal);
|
|
612
|
+
}).catch((error) => {
|
|
613
|
+
logger3.error("Audit backup operation failed", { error });
|
|
614
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), error);
|
|
615
|
+
}).then(async (result) => {
|
|
616
|
+
const recorded = await input.capture({
|
|
617
|
+
action: event.action,
|
|
618
|
+
resource: "audit",
|
|
619
|
+
resourceId: result.success ? result.data.id : event.resourceId,
|
|
620
|
+
category: "system",
|
|
621
|
+
outcome: result.success ? "success" : result.error.httpStatus === 403 ? "denied" : "failure",
|
|
622
|
+
details: result.success ? { key: result.data.key, recordCount: result.data.recordCount, sha256: result.data.sha256 } : { reasonCode: result.error.code }
|
|
623
|
+
}, { read: event.read });
|
|
624
|
+
if (!recorded.success)
|
|
625
|
+
logger3.error("Audit backup event failed", { action: event.action, error: recorded.error });
|
|
626
|
+
return result;
|
|
627
|
+
}).finally(() => {
|
|
628
|
+
controllers.delete(controller);
|
|
629
|
+
pending.delete(promise);
|
|
630
|
+
});
|
|
631
|
+
pending.add(promise);
|
|
632
|
+
return promise;
|
|
633
|
+
}
|
|
634
|
+
async function read(key, signal) {
|
|
635
|
+
const opened = await input.storage.file.getStream(key, { signal });
|
|
636
|
+
if (!opened.success)
|
|
637
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), opened.error);
|
|
638
|
+
try {
|
|
639
|
+
return ok(await verifyArchive(opened.data, { key, maxRecordBytes: input.config.maxRecordBytes, signal }));
|
|
640
|
+
} catch (error) {
|
|
641
|
+
logger3.error("Audit archive verification failed", { error });
|
|
642
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupVerificationFailed"), error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function reuse(key, requestHash, signal) {
|
|
646
|
+
const previous = await read(key, signal);
|
|
647
|
+
if (!previous.success)
|
|
648
|
+
return previous;
|
|
649
|
+
return previous.data.requestHash === requestHash ? ok(previous.data.info) : err(HaiAuditError.BACKUP_CONFLICT, auditM("audit_backupConflict"));
|
|
650
|
+
}
|
|
651
|
+
const operations = {
|
|
652
|
+
run(options) {
|
|
653
|
+
const parsed = optionsSchema.safeParse(options ?? {});
|
|
654
|
+
if (!parsed.success)
|
|
655
|
+
return Promise.resolve(err(HaiAuditError.BACKUP_INVALID, auditM("audit_backupInvalid")));
|
|
656
|
+
const values = parsed.data;
|
|
657
|
+
const id = values.id ?? core.id.uuid();
|
|
658
|
+
const from = values.from ? new Date(values.from.getTime()) : void 0;
|
|
659
|
+
const key = `${AUDIT_BACKUP_PREFIX}${id}.jsonl`;
|
|
660
|
+
const database = reldb.config;
|
|
661
|
+
const source = database?.type === "sqlite" ? { type: database.type, database: database.database } : { type: database?.type, host: database?.host, port: database?.port, database: database?.database };
|
|
662
|
+
const requestHash = createHash("sha256").update(JSON.stringify({ source, from: from?.toISOString() ?? null, to: values.to?.toISOString() ?? null })).digest("hex");
|
|
663
|
+
return track(async (signal) => {
|
|
664
|
+
const time = await readAuditTime();
|
|
665
|
+
if (!time.success)
|
|
666
|
+
return time;
|
|
667
|
+
const createdAt = time.data;
|
|
668
|
+
const cutoff = new Date(values.to?.getTime() ?? createdAt.getTime());
|
|
669
|
+
if (cutoff > createdAt || from && from > cutoff)
|
|
670
|
+
return err(HaiAuditError.BACKUP_INVALID, auditM("audit_backupInvalid"));
|
|
671
|
+
const exists = await input.storage.file.exists(key);
|
|
672
|
+
if (!exists.success)
|
|
673
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), exists.error);
|
|
674
|
+
if (exists.data)
|
|
675
|
+
return reuse(key, requestHash, signal);
|
|
676
|
+
let completed;
|
|
677
|
+
async function* content() {
|
|
678
|
+
const hash2 = createHash("sha256");
|
|
679
|
+
const header = archiveLine({ type: "header", format: "hai-audit", version: 1, id, createdAt: createdAt.toISOString(), cutoff: cutoff.toISOString(), from: from?.toISOString() ?? null, requestHash }, input.config.maxRecordBytes);
|
|
680
|
+
hash2.update(header);
|
|
681
|
+
yield header;
|
|
682
|
+
let afterId;
|
|
683
|
+
let count = 0;
|
|
684
|
+
while (true) {
|
|
685
|
+
signal.throwIfAborted();
|
|
686
|
+
const page = await input.repository.archivePage({ afterId, from, cutoff, limit: input.config.batchSize });
|
|
687
|
+
if (!page.success)
|
|
688
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), page.error);
|
|
689
|
+
for (const record of page.data) {
|
|
690
|
+
signal.throwIfAborted();
|
|
691
|
+
const line = archiveLine({ type: "entry", record }, input.config.maxRecordBytes);
|
|
692
|
+
hash2.update(line);
|
|
693
|
+
yield line;
|
|
694
|
+
if (!Number.isSafeInteger(++count))
|
|
695
|
+
throw new Error("Audit archive record count exceeds safe range");
|
|
696
|
+
afterId = record.id;
|
|
697
|
+
}
|
|
698
|
+
if (page.data.length < input.config.batchSize)
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
const sha256 = hash2.digest("hex");
|
|
702
|
+
yield archiveLine({ type: "footer", recordCount: count, sha256 }, input.config.maxRecordBytes);
|
|
703
|
+
completed = { id, key, recordCount: count, sha256, createdAt, cutoff };
|
|
704
|
+
}
|
|
705
|
+
const stream = content();
|
|
706
|
+
let uploaded;
|
|
707
|
+
try {
|
|
708
|
+
uploaded = await input.storage.file.put(key, stream, { ifNoneMatch: "*", contentType: "application/x-ndjson", cacheControl: "private, no-store", signal });
|
|
709
|
+
} finally {
|
|
710
|
+
await stream.return(void 0);
|
|
711
|
+
}
|
|
712
|
+
if (!uploaded.success) {
|
|
713
|
+
const stored = await input.storage.file.exists(key);
|
|
714
|
+
if (stored.success && stored.data)
|
|
715
|
+
return reuse(key, requestHash, signal);
|
|
716
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), uploaded.error);
|
|
717
|
+
}
|
|
718
|
+
if (!completed)
|
|
719
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"));
|
|
720
|
+
return ok(completed);
|
|
721
|
+
}, { action: "audit.backup", resourceId: id }, values.signal);
|
|
722
|
+
},
|
|
723
|
+
verify(key, options) {
|
|
724
|
+
if (typeof key !== "string" || !key.startsWith(AUDIT_BACKUP_PREFIX) || !key.endsWith(".jsonl") || !backupIdSchema.safeParse(key.slice(AUDIT_BACKUP_PREFIX.length, -6)).success)
|
|
725
|
+
return Promise.resolve(err(HaiAuditError.BACKUP_INVALID, auditM("audit_backupInvalid")));
|
|
726
|
+
const parsed = optionsSchema.pick({ signal: true }).safeParse(options ?? {});
|
|
727
|
+
if (!parsed.success)
|
|
728
|
+
return Promise.resolve(err(HaiAuditError.BACKUP_INVALID, auditM("audit_backupInvalid")));
|
|
729
|
+
return track(async (signal) => {
|
|
730
|
+
const result = await read(key, signal);
|
|
731
|
+
return result.success ? ok(result.data.info) : result;
|
|
732
|
+
}, { action: "audit.backup.verify", read: true }, parsed.data.signal);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
let schedule = null;
|
|
736
|
+
if (input.config.enabled && input.config.schedule.enabled) {
|
|
737
|
+
schedule = {
|
|
738
|
+
tasks: [{ id: "hai-audit-backup", name: auditM("audit_backupTaskName"), idempotent: true, cron: input.config.schedule.cron, timezone: input.config.schedule.timezone }],
|
|
739
|
+
hooks: { onTaskExecute: async ({ context, signal }) => {
|
|
740
|
+
if (context.taskId !== "hai-audit-backup" || !context.occurrenceId)
|
|
741
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.BACKUP_INVALID, auditM("audit_backupInvalid"));
|
|
742
|
+
const result = await withAuditContext({ actor: { id: "system:audit-backup", username: "scheduler" }, requestId: context.occurrenceId }, () => operations.run({ id: context.occurrenceId, signal }));
|
|
743
|
+
if (!result.success)
|
|
744
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.BACKUP_FAILED, result.error.message, result.error);
|
|
745
|
+
return result.data;
|
|
746
|
+
} }
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
operations,
|
|
751
|
+
schedule,
|
|
752
|
+
async close() {
|
|
753
|
+
active = false;
|
|
754
|
+
for (const controller of controllers)
|
|
755
|
+
controller.abort();
|
|
756
|
+
await Promise.allSettled([...pending]);
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
function toVoid(result) {
|
|
761
|
+
if (!result.success) {
|
|
762
|
+
return result;
|
|
763
|
+
}
|
|
764
|
+
return ok(void 0);
|
|
765
|
+
}
|
|
766
|
+
function createHelper(logFn) {
|
|
767
|
+
return {
|
|
768
|
+
async login(userId, ip, ua) {
|
|
769
|
+
const result = await logFn({ userId, category: "system", action: "login", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
770
|
+
return toVoid(result);
|
|
771
|
+
},
|
|
772
|
+
async logout(userId, ip, ua) {
|
|
773
|
+
const result = await logFn({ userId, category: "system", action: "logout", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
774
|
+
return toVoid(result);
|
|
775
|
+
},
|
|
776
|
+
async register(userId, ip, ua) {
|
|
777
|
+
const result = await logFn({ userId, category: "system", action: "register", resource: "auth", resourceId: userId, ipAddress: ip, userAgent: ua });
|
|
778
|
+
return toVoid(result);
|
|
779
|
+
},
|
|
780
|
+
async passwordResetRequest(email, ip, ua) {
|
|
781
|
+
const result = await logFn({
|
|
782
|
+
action: "password_reset_request",
|
|
783
|
+
category: "system",
|
|
784
|
+
resource: "auth",
|
|
785
|
+
details: { email },
|
|
786
|
+
ipAddress: ip,
|
|
787
|
+
userAgent: ua
|
|
788
|
+
});
|
|
789
|
+
return toVoid(result);
|
|
790
|
+
},
|
|
791
|
+
async passwordResetComplete(userId, ip, ua) {
|
|
792
|
+
const result = await logFn({ userId, category: "system", action: "password_reset", resource: "auth", ipAddress: ip, userAgent: ua });
|
|
793
|
+
return toVoid(result);
|
|
794
|
+
},
|
|
795
|
+
async crud(input) {
|
|
796
|
+
const { ip, ua, ...fields } = input;
|
|
797
|
+
const result = await logFn({ ...fields, ipAddress: ip, userAgent: ua });
|
|
798
|
+
return toVoid(result);
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
var columns = { category: "VARCHAR(16)", outcome: "VARCHAR(16)", severity: "VARCHAR(16)", request_id: "TEXT", trace_id: "TEXT", scope_id: "TEXT", actor_snapshot: "JSON" };
|
|
803
|
+
var triggers = ["hai_audit_protect_update", "hai_audit_protect_delete", "hai_audit_protect_insert"];
|
|
804
|
+
async function existingTriggers(sql) {
|
|
805
|
+
const result = reldb.config?.type === "sqlite" ? await sql.query("SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='hai_audit_logs'") : await sql.query(`SELECT trigger_name AS name FROM information_schema.triggers WHERE trigger_schema=${reldb.config?.type === "mysql" ? "DATABASE()" : "current_schema()"} AND event_object_table='hai_audit_logs'`);
|
|
806
|
+
return result.success ? ok(result.data.map((row) => row.name)) : result;
|
|
807
|
+
}
|
|
808
|
+
var steps = [{
|
|
809
|
+
version: 1,
|
|
810
|
+
name: "event-context-and-identity",
|
|
811
|
+
async needed(sql) {
|
|
812
|
+
const found = await schemaColumns(reldb, "hai_audit_logs", sql);
|
|
813
|
+
return found.success ? ok(found.data.length > 0 && Object.keys(columns).some((name) => !found.data.includes(name))) : found;
|
|
814
|
+
},
|
|
815
|
+
async apply(sql) {
|
|
816
|
+
const found = await schemaColumns(reldb, "hai_audit_logs", sql);
|
|
817
|
+
if (!found.success)
|
|
818
|
+
return found;
|
|
819
|
+
for (const [name, definition] of Object.entries(columns)) {
|
|
820
|
+
if (found.data.includes(name))
|
|
821
|
+
continue;
|
|
822
|
+
const added = await sql.execute(`ALTER TABLE hai_audit_logs ADD COLUMN ${name} ${definition}`);
|
|
823
|
+
if (!added.success)
|
|
824
|
+
return added;
|
|
825
|
+
}
|
|
826
|
+
return ok(void 0);
|
|
827
|
+
}
|
|
828
|
+
}, {
|
|
829
|
+
version: 2,
|
|
830
|
+
name: "persistent-retention-policy",
|
|
831
|
+
async needed(sql) {
|
|
832
|
+
const found = await schemaColumns(reldb, "hai_audit_policy", sql);
|
|
833
|
+
if (!found.success || found.data.length === 0)
|
|
834
|
+
return found.success ? ok(true) : found;
|
|
835
|
+
const row = await sql.get("SELECT id FROM hai_audit_policy WHERE id=1");
|
|
836
|
+
return row.success ? ok(!row.data) : row;
|
|
837
|
+
},
|
|
838
|
+
async apply(sql) {
|
|
839
|
+
const created = await sql.execute("CREATE TABLE IF NOT EXISTS hai_audit_policy (id INTEGER PRIMARY KEY, enabled INTEGER NOT NULL, min_days INTEGER NOT NULL)");
|
|
840
|
+
if (!created.success)
|
|
841
|
+
return created;
|
|
842
|
+
return sql.execute(reldb.config?.type === "mysql" ? "INSERT IGNORE INTO hai_audit_policy (id, enabled, min_days) VALUES (1, 0, 90)" : "INSERT INTO hai_audit_policy (id, enabled, min_days) VALUES (1, 0, 90) ON CONFLICT(id) DO NOTHING");
|
|
843
|
+
}
|
|
844
|
+
}, {
|
|
845
|
+
version: 3,
|
|
846
|
+
name: "retention-write-guards",
|
|
847
|
+
async needed(sql) {
|
|
848
|
+
const table2 = await schemaColumns(reldb, "hai_audit_logs", sql);
|
|
849
|
+
if (!table2.success || !table2.data.length)
|
|
850
|
+
return table2.success ? ok(false) : table2;
|
|
851
|
+
const found = await existingTriggers(sql);
|
|
852
|
+
return found.success ? ok(triggers.some((name) => !found.data.includes(name))) : found;
|
|
853
|
+
},
|
|
854
|
+
async apply(sql) {
|
|
855
|
+
const found = await existingTriggers(sql);
|
|
856
|
+
if (!found.success)
|
|
857
|
+
return found;
|
|
858
|
+
const enabled = "COALESCE((SELECT enabled FROM hai_audit_policy WHERE id=1),1)=1";
|
|
859
|
+
const days = "COALESCE((SELECT min_days FROM hai_audit_policy WHERE id=1),90)";
|
|
860
|
+
const cutoff = reldb.config?.type === "sqlite" ? `CAST((julianday('now')-2440587.5)*86400000 AS INTEGER) - ${days}*86400000` : reldb.config?.type === "postgresql" ? `CURRENT_TIMESTAMP - (${days} * INTERVAL '1 day')` : `TIMESTAMPADD(DAY, -${days}, CURRENT_TIMESTAMP(3))`;
|
|
861
|
+
const existingProtected = `${enabled} AND EXISTS(SELECT 1 FROM hai_audit_logs a WHERE a.id=NEW.id AND a.created_at >= ${cutoff})`;
|
|
862
|
+
if (reldb.config?.type === "postgresql") {
|
|
863
|
+
const fn = await sql.execute(`CREATE OR REPLACE FUNCTION hai_audit_protect() RETURNS trigger AS $$ BEGIN IF TG_OP='INSERT' THEN IF ${existingProtected} THEN RAISE EXCEPTION 'Audit record is protected by retention policy'; END IF; RETURN NEW; END IF; IF ${enabled} AND OLD.created_at >= ${cutoff} THEN RAISE EXCEPTION 'Audit record is protected by retention policy'; END IF; IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql`);
|
|
864
|
+
if (!fn.success)
|
|
865
|
+
return fn;
|
|
866
|
+
}
|
|
867
|
+
for (const [index, operation] of ["UPDATE", "DELETE", "INSERT"].entries()) {
|
|
868
|
+
const name = triggers[index];
|
|
869
|
+
if (found.data.includes(name))
|
|
870
|
+
continue;
|
|
871
|
+
const protectedRow = operation === "INSERT" ? existingProtected : `${enabled} AND OLD.created_at >= ${cutoff}`;
|
|
872
|
+
const statement = reldb.config?.type === "sqlite" ? `CREATE TRIGGER ${name} BEFORE ${operation} ON hai_audit_logs WHEN ${protectedRow} BEGIN SELECT RAISE(ABORT, 'Audit record is protected by retention policy'); END` : reldb.config?.type === "postgresql" ? `CREATE TRIGGER ${name} BEFORE ${operation} ON hai_audit_logs FOR EACH ROW EXECUTE FUNCTION hai_audit_protect()` : `CREATE TRIGGER ${name} BEFORE ${operation} ON hai_audit_logs FOR EACH ROW BEGIN IF ${protectedRow} THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='Audit record is protected by retention policy'; END IF; END`;
|
|
873
|
+
const created = reldb.config?.type === "mysql" ? await sql.query(statement) : await sql.execute(statement);
|
|
874
|
+
if (!created.success)
|
|
875
|
+
return created;
|
|
876
|
+
}
|
|
877
|
+
return ok(void 0);
|
|
878
|
+
}
|
|
879
|
+
}, {
|
|
880
|
+
version: 4,
|
|
881
|
+
name: "millisecond-event-time",
|
|
882
|
+
async needed(sql) {
|
|
883
|
+
if (reldb.config?.type !== "mysql")
|
|
884
|
+
return ok(false);
|
|
885
|
+
const column = await sql.get("SELECT DATETIME_PRECISION AS fractional_digits FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name=? AND column_name=?", ["hai_audit_logs", "created_at"]);
|
|
886
|
+
return column.success ? ok(Boolean(column.data && Number(column.data.fractional_digits ?? 0) < 3)) : column;
|
|
887
|
+
},
|
|
888
|
+
async apply(sql) {
|
|
889
|
+
const changed = await sql.execute("ALTER TABLE hai_audit_logs MODIFY COLUMN created_at DATETIME(3)");
|
|
890
|
+
return changed.success ? ok(void 0) : changed;
|
|
891
|
+
}
|
|
892
|
+
}, {
|
|
893
|
+
version: 5,
|
|
894
|
+
name: "alert-notification-claims",
|
|
895
|
+
async needed(sql) {
|
|
896
|
+
const found = await schemaColumns(reldb, "hai_audit_alert_claims", sql);
|
|
897
|
+
return found.success ? ok(found.data.length === 0) : found;
|
|
898
|
+
},
|
|
899
|
+
async apply(sql) {
|
|
900
|
+
const created = await sql.execute("CREATE TABLE IF NOT EXISTS hai_audit_alert_claims (id VARCHAR(64) PRIMARY KEY, rule_id VARCHAR(64) NOT NULL, target_id VARCHAR(64) NOT NULL, window_start BIGINT NOT NULL, status VARCHAR(16) NOT NULL, queue_id VARCHAR(64), error_code VARCHAR(128))");
|
|
901
|
+
return created.success ? ok(void 0) : created;
|
|
902
|
+
}
|
|
903
|
+
}, {
|
|
904
|
+
version: 6,
|
|
905
|
+
name: "event-time-index",
|
|
906
|
+
async needed(sql) {
|
|
907
|
+
const table2 = await schemaColumns(reldb, "hai_audit_logs", sql);
|
|
908
|
+
if (!table2.success || !table2.data.length)
|
|
909
|
+
return table2.success ? ok(false) : table2;
|
|
910
|
+
const found = reldb.config?.type === "sqlite" ? await sql.get("SELECT name FROM sqlite_master WHERE type='index' AND name='hai_audit_event_time_idx'") : reldb.config?.type === "postgresql" ? await sql.get("SELECT indexname FROM pg_indexes WHERE schemaname=current_schema() AND tablename='hai_audit_logs' AND indexname='hai_audit_event_time_idx'") : await sql.get("SELECT index_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='hai_audit_logs' AND index_name='hai_audit_event_time_idx' LIMIT 1");
|
|
911
|
+
return found.success ? ok(!found.data) : found;
|
|
912
|
+
},
|
|
913
|
+
async apply(sql) {
|
|
914
|
+
const created = await sql.execute("CREATE INDEX hai_audit_event_time_idx ON hai_audit_logs (created_at)");
|
|
915
|
+
return created.success ? ok(void 0) : created;
|
|
916
|
+
}
|
|
917
|
+
}];
|
|
918
|
+
function migrateAudit(options = {}) {
|
|
919
|
+
return migrateSchema(reldb, "audit", steps, options);
|
|
920
|
+
}
|
|
921
|
+
async function configureAuditRetention(policy) {
|
|
922
|
+
if (!policy)
|
|
923
|
+
return ok(void 0);
|
|
924
|
+
const result = await reldb.sql.execute("UPDATE hai_audit_policy SET enabled=?, min_days=? WHERE id=1", [Number(policy.enabled), policy.minDays]);
|
|
925
|
+
return result.success && result.data.changes === 1 ? ok(void 0) : err(HaiAuditError.CONFIG_ERROR, auditM("audit_retentionPolicyFailed"), result.success ? void 0 : result.error);
|
|
926
|
+
}
|
|
927
|
+
async function auditRetentionDays() {
|
|
928
|
+
const result = await reldb.sql.get("SELECT enabled, min_days FROM hai_audit_policy WHERE id=1");
|
|
929
|
+
if (!result.success || !result.data || ![0, 1].includes(Number(result.data.enabled)) || !Number.isInteger(Number(result.data.min_days)) || Number(result.data.min_days) < 1)
|
|
930
|
+
return err(HaiAuditError.CLEANUP_FAILED, auditM("audit_retentionPolicyFailed"));
|
|
931
|
+
return ok(Number(result.data.enabled) === 1 ? Number(result.data.min_days) : 0);
|
|
932
|
+
}
|
|
933
|
+
var logger4 = core.logger.child({ module: "audit", scope: "repository" });
|
|
103
934
|
var AUDIT_TABLE = "hai_audit_logs";
|
|
935
|
+
function normalizeAuditLog(row) {
|
|
936
|
+
return {
|
|
937
|
+
...row,
|
|
938
|
+
actorSnapshot: readAuditActor(row.actorSnapshot),
|
|
939
|
+
userId: row.userId ?? null,
|
|
940
|
+
resourceId: row.resourceId ?? null,
|
|
941
|
+
details: row.details == null ? null : typeof row.details === "string" ? row.details : JSON.stringify(row.details) ?? null,
|
|
942
|
+
ipAddress: row.ipAddress ?? null,
|
|
943
|
+
userAgent: row.userAgent ?? null,
|
|
944
|
+
createdAt: row.createdAt instanceof Date ? row.createdAt : new Date(row.createdAt)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
104
947
|
var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
105
948
|
repoConfig;
|
|
106
949
|
isSqlite;
|
|
@@ -121,22 +964,68 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
121
964
|
{ fieldName: "details", columnName: "details", def: { type: "JSON" }, select: true, create: true, update: false },
|
|
122
965
|
{ fieldName: "ipAddress", columnName: "ip_address", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
123
966
|
{ fieldName: "userAgent", columnName: "user_agent", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
967
|
+
{ fieldName: "category", columnName: "category", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
968
|
+
{ fieldName: "outcome", columnName: "outcome", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
969
|
+
{ fieldName: "severity", columnName: "severity", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
970
|
+
{ fieldName: "requestId", columnName: "request_id", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
971
|
+
{ fieldName: "traceId", columnName: "trace_id", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
972
|
+
{ fieldName: "scopeId", columnName: "scope_id", def: { type: "TEXT" }, select: true, create: true, update: false },
|
|
973
|
+
{ fieldName: "actorSnapshot", columnName: "actor_snapshot", def: { type: "JSON" }, select: true, create: true, update: false },
|
|
124
974
|
{ fieldName: "createdAt", columnName: "created_at", def: { type: "TIMESTAMP" }, select: true, create: true, update: false }
|
|
125
975
|
]
|
|
126
976
|
});
|
|
127
977
|
this.repoConfig = config;
|
|
128
978
|
this.isSqlite = this.db.config?.type === "sqlite";
|
|
129
979
|
}
|
|
980
|
+
/**
|
|
981
|
+
* 等待基类完成建表及字段校验;init和自定义SQL共享此屏障,失败不发布仓库。
|
|
982
|
+
* @returns 建表结果,失败由调用方映射到对应审计错误
|
|
983
|
+
* @example const ready = await repository.ready(); if (!ready.success) return ready
|
|
984
|
+
*/
|
|
985
|
+
ready() {
|
|
986
|
+
return this.ensureReady();
|
|
987
|
+
}
|
|
130
988
|
/**
|
|
131
989
|
* 将 Date 转为适合当前数据库类型的 SQL 参数
|
|
132
990
|
*
|
|
133
|
-
* SQLite
|
|
991
|
+
* 与基类写入保持一致:SQLite 使用毫秒,其他数据库由驱动转换 Date。
|
|
134
992
|
*
|
|
135
993
|
* @param date - 要转换的日期
|
|
136
|
-
* @returns SQLite
|
|
994
|
+
* @returns SQLite 返回毫秒时间戳,其他返回 Date
|
|
137
995
|
*/
|
|
138
996
|
toDateParam(date) {
|
|
139
|
-
return this.isSqlite ? date.getTime() : date
|
|
997
|
+
return this.isSqlite ? date.getTime() : date;
|
|
998
|
+
}
|
|
999
|
+
/** 身份映射可选;名称读失败仍写入明确缺少名称的身份,并留下可观察诊断。 */
|
|
1000
|
+
async captureActor(userId, tx) {
|
|
1001
|
+
const quote = this.db.config?.type === "mysql" ? "`" : '"';
|
|
1002
|
+
const identifier = (name) => `${quote}${name}${quote}`;
|
|
1003
|
+
const { userTable, userIdColumn, userNameColumn } = this.repoConfig;
|
|
1004
|
+
const result = await this.sql(tx).get(`SELECT ${identifier(userNameColumn)} AS username FROM ${identifier(userTable)} WHERE ${identifier(userIdColumn)}=?`, [userId]);
|
|
1005
|
+
if (!result.success) {
|
|
1006
|
+
logger4.warn("Audit actor name unavailable", { userId, error: result.error });
|
|
1007
|
+
return { id: userId };
|
|
1008
|
+
}
|
|
1009
|
+
return { id: userId, ...result.data?.username ? { username: result.data.username } : {} };
|
|
1010
|
+
}
|
|
1011
|
+
filters(options) {
|
|
1012
|
+
const conditions = [];
|
|
1013
|
+
const params = [];
|
|
1014
|
+
for (const [field, column] of [["userId", "user_id"], ["action", "action"], ["resource", "resource"], ["category", "category"], ["outcome", "outcome"], ["severity", "severity"], ["scopeId", "scope_id"], ["requestId", "request_id"], ["traceId", "trace_id"]]) {
|
|
1015
|
+
if (options[field] !== void 0) {
|
|
1016
|
+
conditions.push(`a.${column} = ?`);
|
|
1017
|
+
params.push(options[field]);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (options.startDate) {
|
|
1021
|
+
conditions.push("a.created_at >= ?");
|
|
1022
|
+
params.push(this.toDateParam(options.startDate));
|
|
1023
|
+
}
|
|
1024
|
+
if (options.endDate) {
|
|
1025
|
+
conditions.push("a.created_at <= ?");
|
|
1026
|
+
params.push(this.toDateParam(options.endDate));
|
|
1027
|
+
}
|
|
1028
|
+
return { conditions, params };
|
|
140
1029
|
}
|
|
141
1030
|
/**
|
|
142
1031
|
* 记录一条审计日志
|
|
@@ -151,11 +1040,16 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
151
1040
|
* @param input.userAgent - 客户端 User-Agent(可选)
|
|
152
1041
|
* @returns 成功时返回创建的 AuditLog;失败时返回 LOG_FAILED
|
|
153
1042
|
*/
|
|
154
|
-
async log(input) {
|
|
155
|
-
|
|
1043
|
+
async log(input, tx) {
|
|
1044
|
+
logger4.trace("Recording audit log", { action: input.action, resource: input.resource });
|
|
156
1045
|
const id = core.id.withPrefix("audit_");
|
|
157
|
-
const
|
|
1046
|
+
const time = await readAuditTime(this.sql(tx));
|
|
1047
|
+
if (!time.success)
|
|
1048
|
+
return time;
|
|
1049
|
+
const now = time.data;
|
|
1050
|
+
const context = { category: input.category ?? null, outcome: input.outcome ?? null, severity: input.severity ?? null, requestId: input.requestId ?? null, traceId: input.traceId ?? null, scopeId: input.scopeId ?? null, actorSnapshot: input.actorSnapshot ?? null };
|
|
158
1051
|
const data = {
|
|
1052
|
+
...context,
|
|
159
1053
|
id,
|
|
160
1054
|
userId: input.userId ?? null,
|
|
161
1055
|
action: input.action,
|
|
@@ -167,9 +1061,9 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
167
1061
|
createdAt: now
|
|
168
1062
|
};
|
|
169
1063
|
try {
|
|
170
|
-
const createResult = await this.create(data);
|
|
1064
|
+
const createResult = await this.create(data, tx);
|
|
171
1065
|
if (!createResult.success) {
|
|
172
|
-
|
|
1066
|
+
logger4.error("Failed to record audit log", { action: input.action, error: createResult.error.message });
|
|
173
1067
|
return err(
|
|
174
1068
|
HaiAuditError.LOG_FAILED,
|
|
175
1069
|
auditM("audit_logFailed", { params: { error: createResult.error.message } }),
|
|
@@ -177,6 +1071,7 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
177
1071
|
);
|
|
178
1072
|
}
|
|
179
1073
|
const auditLog = {
|
|
1074
|
+
...context,
|
|
180
1075
|
id,
|
|
181
1076
|
userId: input.userId ?? null,
|
|
182
1077
|
action: input.action,
|
|
@@ -187,10 +1082,10 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
187
1082
|
userAgent: input.userAgent ?? null,
|
|
188
1083
|
createdAt: now
|
|
189
1084
|
};
|
|
190
|
-
|
|
1085
|
+
logger4.trace("Audit log recorded", { id, action: input.action, resource: input.resource });
|
|
191
1086
|
return ok(auditLog);
|
|
192
1087
|
} catch (error) {
|
|
193
|
-
|
|
1088
|
+
logger4.error("Failed to record audit log", { action: input.action, error });
|
|
194
1089
|
return err(
|
|
195
1090
|
HaiAuditError.LOG_FAILED,
|
|
196
1091
|
auditM("audit_logFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
@@ -202,58 +1097,48 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
202
1097
|
* 分页查询日志列表(含用户名 LEFT JOIN)
|
|
203
1098
|
*
|
|
204
1099
|
* @param options - 过滤条件与分页参数
|
|
205
|
-
* @returns 成功时返回
|
|
1100
|
+
* @returns 成功时返回 PaginatedResult(items/total/page/pageSize);失败时返回 QUERY_FAILED
|
|
206
1101
|
*/
|
|
207
1102
|
async listWithUser(options = {}) {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1103
|
+
const ready = await this.ready();
|
|
1104
|
+
if (!ready.success)
|
|
1105
|
+
return ready;
|
|
1106
|
+
logger4.debug("Querying audit logs", { userId: options.userId, action: options.action, resource: options.resource });
|
|
1107
|
+
const { conditions, params } = this.filters(options);
|
|
211
1108
|
const { userTable, userIdColumn, userNameColumn } = this.repoConfig;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
if (options.resource) {
|
|
221
|
-
conditions.push("a.resource = ?");
|
|
222
|
-
params.push(options.resource);
|
|
223
|
-
}
|
|
224
|
-
if (options.startDate) {
|
|
225
|
-
conditions.push("a.created_at >= ?");
|
|
226
|
-
params.push(this.toDateParam(options.startDate));
|
|
227
|
-
}
|
|
228
|
-
if (options.endDate) {
|
|
229
|
-
conditions.push("a.created_at <= ?");
|
|
230
|
-
params.push(this.toDateParam(options.endDate));
|
|
231
|
-
}
|
|
1109
|
+
const quote = this.db.config?.type === "mysql" ? "`" : '"';
|
|
1110
|
+
const identifier = (name) => `${quote}${name.replaceAll(quote, quote + quote)}${quote}`;
|
|
1111
|
+
const sortColumns = { createdAt: "created_at", action: "action", resource: "resource", category: "category", outcome: "outcome", severity: "severity" };
|
|
1112
|
+
const sortBy = options.sortBy ?? "createdAt";
|
|
1113
|
+
const direction = options.sortDirection ?? "desc";
|
|
1114
|
+
if (!Object.hasOwn(sortColumns, sortBy) || !["asc", "desc"].includes(direction))
|
|
1115
|
+
return err(HaiAuditError.QUERY_FAILED, auditM("audit_invalidInput", { params: { field: "sort", reason: auditM("audit_sortInvalid") } }));
|
|
232
1116
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
233
1117
|
try {
|
|
234
1118
|
const result = await this.sql().queryPage({
|
|
235
|
-
sql: `SELECT a.id, a.user_id AS userId, a.action, a.resource, a.resource_id AS resourceId,
|
|
236
|
-
a.details, a.ip_address AS ipAddress, a.user_agent AS userAgent,
|
|
237
|
-
a.created_at AS createdAt,
|
|
1119
|
+
sql: `SELECT a.id, a.user_id AS ${identifier("userId")}, a.action, a.resource, a.resource_id AS ${identifier("resourceId")},
|
|
1120
|
+
a.details, a.ip_address AS ${identifier("ipAddress")}, a.user_agent AS ${identifier("userAgent")},
|
|
1121
|
+
a.created_at AS ${identifier("createdAt")}, a.category, a.outcome, a.severity,
|
|
1122
|
+
a.request_id AS ${identifier("requestId")}, a.trace_id AS ${identifier("traceId")}, a.scope_id AS ${identifier("scopeId")}, a.actor_snapshot AS ${identifier("actorSnapshot")}, u.${identifier(userNameColumn)} AS username
|
|
238
1123
|
FROM ${AUDIT_TABLE} a
|
|
239
|
-
LEFT JOIN ${userTable} u ON a.user_id = u.${userIdColumn}
|
|
1124
|
+
LEFT JOIN ${identifier(userTable)} u ON a.user_id = u.${identifier(userIdColumn)}
|
|
240
1125
|
${whereClause}
|
|
241
|
-
ORDER BY a.
|
|
1126
|
+
ORDER BY a.${sortColumns[sortBy]} ${direction.toUpperCase()}, a.id ${direction.toUpperCase()}`,
|
|
242
1127
|
params,
|
|
243
1128
|
pagination: { page: options.page, pageSize: options.pageSize },
|
|
244
1129
|
overrides: { defaultPageSize: 20 }
|
|
245
1130
|
});
|
|
246
1131
|
if (!result.success) {
|
|
247
|
-
|
|
1132
|
+
logger4.error("Failed to query audit logs", { error: result.error.message });
|
|
248
1133
|
return err(
|
|
249
1134
|
HaiAuditError.QUERY_FAILED,
|
|
250
1135
|
auditM("audit_queryFailed", { params: { error: result.error.message } }),
|
|
251
1136
|
result.error
|
|
252
1137
|
);
|
|
253
1138
|
}
|
|
254
|
-
return ok({ items: result.data.items,
|
|
1139
|
+
return ok({ ...result.data, items: result.data.items.map((row) => ({ ...normalizeAuditLog(row), username: readAuditActor(row.actorSnapshot)?.username ?? (row.actorSnapshot == null ? row.username ?? null : null) })) });
|
|
255
1140
|
} catch (error) {
|
|
256
|
-
|
|
1141
|
+
logger4.error("Failed to query audit logs", { error });
|
|
257
1142
|
return err(
|
|
258
1143
|
HaiAuditError.QUERY_FAILED,
|
|
259
1144
|
auditM("audit_queryFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
@@ -261,6 +1146,50 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
261
1146
|
);
|
|
262
1147
|
}
|
|
263
1148
|
}
|
|
1149
|
+
/** 归档使用主键游标有界扫描,不关联当前用户名称或改写历史身份。 */
|
|
1150
|
+
async archivePage(options) {
|
|
1151
|
+
const ready = await this.ready();
|
|
1152
|
+
if (!ready.success)
|
|
1153
|
+
return ready;
|
|
1154
|
+
const quote = this.db.config?.type === "mysql" ? "`" : '"';
|
|
1155
|
+
const fields = [
|
|
1156
|
+
["id", "id"],
|
|
1157
|
+
["user_id", "userId"],
|
|
1158
|
+
["action", "action"],
|
|
1159
|
+
["resource", "resource"],
|
|
1160
|
+
["resource_id", "resourceId"],
|
|
1161
|
+
["details", "details"],
|
|
1162
|
+
["ip_address", "ipAddress"],
|
|
1163
|
+
["user_agent", "userAgent"],
|
|
1164
|
+
["created_at", "createdAt"],
|
|
1165
|
+
["category", "category"],
|
|
1166
|
+
["outcome", "outcome"],
|
|
1167
|
+
["severity", "severity"],
|
|
1168
|
+
["request_id", "requestId"],
|
|
1169
|
+
["trace_id", "traceId"],
|
|
1170
|
+
["scope_id", "scopeId"],
|
|
1171
|
+
["actor_snapshot", "actorSnapshot"]
|
|
1172
|
+
].map(([column, alias]) => `${column} AS ${quote}${alias}${quote}`).join(", ");
|
|
1173
|
+
const conditions = ["created_at <= ?"];
|
|
1174
|
+
const params = [this.toDateParam(options.cutoff)];
|
|
1175
|
+
if (options.from) {
|
|
1176
|
+
conditions.push("created_at >= ?");
|
|
1177
|
+
params.push(this.toDateParam(options.from));
|
|
1178
|
+
}
|
|
1179
|
+
if (options.afterId) {
|
|
1180
|
+
conditions.push("id > ?");
|
|
1181
|
+
params.push(options.afterId);
|
|
1182
|
+
}
|
|
1183
|
+
params.push(options.limit);
|
|
1184
|
+
const result = await this.sql().query(`SELECT ${fields} FROM ${AUDIT_TABLE} WHERE ${conditions.join(" AND ")} ORDER BY id ASC LIMIT ?`, params);
|
|
1185
|
+
if (!result.success)
|
|
1186
|
+
return result;
|
|
1187
|
+
try {
|
|
1188
|
+
return ok(result.data.map(normalizeAuditLog));
|
|
1189
|
+
} catch (error) {
|
|
1190
|
+
return err(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), error);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
264
1193
|
/**
|
|
265
1194
|
* 获取指定用户的最近活动
|
|
266
1195
|
*
|
|
@@ -269,7 +1198,7 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
269
1198
|
* @returns 成功时返回 AuditLog 数组(按时间倒序);失败时返回 QUERY_FAILED
|
|
270
1199
|
*/
|
|
271
1200
|
async getUserRecent(userId, limit = 10) {
|
|
272
|
-
|
|
1201
|
+
logger4.debug("Getting user recent activity", { userId, limit });
|
|
273
1202
|
try {
|
|
274
1203
|
const result = await this.findAll({
|
|
275
1204
|
where: "user_id = ?",
|
|
@@ -284,7 +1213,7 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
284
1213
|
result.error
|
|
285
1214
|
);
|
|
286
1215
|
}
|
|
287
|
-
return ok(result.data);
|
|
1216
|
+
return ok(result.data.map(normalizeAuditLog));
|
|
288
1217
|
} catch (error) {
|
|
289
1218
|
return err(
|
|
290
1219
|
HaiAuditError.QUERY_FAILED,
|
|
@@ -300,7 +1229,14 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
300
1229
|
* @returns 成功时返回删除的记录数;失败时返回 CLEANUP_FAILED
|
|
301
1230
|
*/
|
|
302
1231
|
async cleanupOld(olderThanDays = 90) {
|
|
303
|
-
|
|
1232
|
+
const ready = await this.ready();
|
|
1233
|
+
if (!ready.success)
|
|
1234
|
+
return ready;
|
|
1235
|
+
const minimum = await auditRetentionDays();
|
|
1236
|
+
if (!minimum.success)
|
|
1237
|
+
return minimum;
|
|
1238
|
+
olderThanDays = Math.max(olderThanDays, minimum.data);
|
|
1239
|
+
logger4.debug("Cleaning up old audit logs", { olderThanDays });
|
|
304
1240
|
const cutoff = /* @__PURE__ */ new Date();
|
|
305
1241
|
cutoff.setDate(cutoff.getDate() - olderThanDays);
|
|
306
1242
|
try {
|
|
@@ -309,7 +1245,7 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
309
1245
|
[this.toDateParam(cutoff)]
|
|
310
1246
|
);
|
|
311
1247
|
if (!result.success) {
|
|
312
|
-
|
|
1248
|
+
logger4.error("Failed to cleanup audit logs", { error: result.error.message });
|
|
313
1249
|
return err(
|
|
314
1250
|
HaiAuditError.CLEANUP_FAILED,
|
|
315
1251
|
auditM("audit_cleanupFailed", { params: { error: result.error.message } }),
|
|
@@ -317,10 +1253,13 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
317
1253
|
);
|
|
318
1254
|
}
|
|
319
1255
|
const deleted = result.data.changes;
|
|
320
|
-
|
|
1256
|
+
if (deleted > 0)
|
|
1257
|
+
logger4.info("Audit logs cleaned up", { olderThanDays, deleted });
|
|
1258
|
+
else
|
|
1259
|
+
logger4.trace("Audit cleanup found no expired records", { olderThanDays, deleted });
|
|
321
1260
|
return ok(deleted);
|
|
322
1261
|
} catch (error) {
|
|
323
|
-
|
|
1262
|
+
logger4.error("Failed to cleanup audit logs", { error });
|
|
324
1263
|
return err(
|
|
325
1264
|
HaiAuditError.CLEANUP_FAILED,
|
|
326
1265
|
auditM("audit_cleanupFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
@@ -334,30 +1273,37 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
334
1273
|
* @param days - 统计天数,默认 7
|
|
335
1274
|
* @returns 成功时返回 AuditStatItem 数组(按 count 倒序);失败时返回 STATS_FAILED
|
|
336
1275
|
*/
|
|
337
|
-
async getStats(days = 7) {
|
|
338
|
-
|
|
1276
|
+
async getStats(days = 7, filters) {
|
|
1277
|
+
const ready = await this.ready();
|
|
1278
|
+
if (!ready.success)
|
|
1279
|
+
return ready;
|
|
1280
|
+
logger4.debug("Getting audit statistics", { days });
|
|
339
1281
|
const cutoff = /* @__PURE__ */ new Date();
|
|
340
1282
|
cutoff.setDate(cutoff.getDate() - days);
|
|
1283
|
+
const { conditions, params } = this.filters({ ...filters, startDate: cutoff });
|
|
341
1284
|
try {
|
|
342
1285
|
const result = await this.sql().query(
|
|
343
1286
|
`SELECT action, COUNT(*) as count
|
|
344
|
-
FROM ${AUDIT_TABLE}
|
|
345
|
-
WHERE
|
|
1287
|
+
FROM ${AUDIT_TABLE} a
|
|
1288
|
+
WHERE ${conditions.join(" AND ")}
|
|
346
1289
|
GROUP BY action
|
|
347
1290
|
ORDER BY count DESC`,
|
|
348
|
-
|
|
1291
|
+
params
|
|
349
1292
|
);
|
|
350
1293
|
if (!result.success) {
|
|
351
|
-
|
|
1294
|
+
logger4.error("Failed to query audit statistics", { error: result.error.message });
|
|
352
1295
|
return err(
|
|
353
1296
|
HaiAuditError.STATS_FAILED,
|
|
354
1297
|
auditM("audit_statsFailed", { params: { error: result.error.message } }),
|
|
355
1298
|
result.error
|
|
356
1299
|
);
|
|
357
1300
|
}
|
|
358
|
-
|
|
1301
|
+
const statistics = result.data.map((item) => ({ action: item.action, count: Number(item.count) }));
|
|
1302
|
+
if (statistics.some((item) => !Number.isSafeInteger(item.count) || item.count < 0))
|
|
1303
|
+
return err(HaiAuditError.STATS_FAILED, auditM("audit_invalidStatCount"));
|
|
1304
|
+
return ok(statistics);
|
|
359
1305
|
} catch (error) {
|
|
360
|
-
|
|
1306
|
+
logger4.error("Failed to query audit statistics", { error });
|
|
361
1307
|
return err(
|
|
362
1308
|
HaiAuditError.STATS_FAILED,
|
|
363
1309
|
auditM("audit_statsFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
@@ -368,28 +1314,32 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
368
1314
|
};
|
|
369
1315
|
|
|
370
1316
|
// src/audit-main.ts
|
|
371
|
-
var
|
|
372
|
-
var MAX_TEXT_LENGTH = 256;
|
|
1317
|
+
var logger5 = core.logger.child({ module: "audit", scope: "main" });
|
|
373
1318
|
function isNonEmptyString(value) {
|
|
374
1319
|
return typeof value === "string" && value.trim().length > 0;
|
|
375
1320
|
}
|
|
376
|
-
function isValidTextLength(value, maxLength = MAX_TEXT_LENGTH) {
|
|
377
|
-
return value.length <= maxLength;
|
|
378
|
-
}
|
|
379
1321
|
var currentRepo = null;
|
|
1322
|
+
var currentWriter = null;
|
|
1323
|
+
var capturePolicy = null;
|
|
1324
|
+
var currentBackup = null;
|
|
1325
|
+
var currentAlerts = null;
|
|
1326
|
+
var inFlightWrites = /* @__PURE__ */ new Set();
|
|
380
1327
|
var currentHelper = null;
|
|
381
1328
|
var initInProgress = false;
|
|
1329
|
+
var lifecycleGeneration = 0;
|
|
382
1330
|
var notInitialized = core.module.createNotInitializedKit(
|
|
383
1331
|
HaiAuditError.NOT_INITIALIZED,
|
|
384
1332
|
() => auditM("audit_notInitialized")
|
|
385
1333
|
);
|
|
386
1334
|
var notInitializedHelper = notInitialized.proxy();
|
|
1335
|
+
var notInitializedBackup = notInitialized.proxy();
|
|
387
1336
|
var audit = {
|
|
1337
|
+
withContext: withAuditContext,
|
|
388
1338
|
/**
|
|
389
1339
|
* 初始化审计模块
|
|
390
1340
|
*
|
|
391
1341
|
* 会先关闭已有实例(如已初始化),再用新配置重新初始化。
|
|
392
|
-
*
|
|
1342
|
+
* 等待 AuditLogRepository / BaseReldbCrudRepository 完成建表后才发布实例。
|
|
393
1343
|
* 依赖 @h-ai/reldb 已初始化。
|
|
394
1344
|
*
|
|
395
1345
|
* @param config - 初始化配置(可选,所有字段均有默认值)
|
|
@@ -405,22 +1355,33 @@ var audit = {
|
|
|
405
1355
|
*/
|
|
406
1356
|
async init(config) {
|
|
407
1357
|
if (initInProgress) {
|
|
408
|
-
|
|
1358
|
+
logger5.warn("Audit init already in progress, skipping concurrent call");
|
|
409
1359
|
return err(
|
|
410
1360
|
HaiAuditError.INIT_IN_PROGRESS,
|
|
411
1361
|
auditM("audit_initInProgress")
|
|
412
1362
|
);
|
|
413
1363
|
}
|
|
414
1364
|
initInProgress = true;
|
|
1365
|
+
const generation = ++lifecycleGeneration;
|
|
415
1366
|
try {
|
|
416
1367
|
if (currentRepo) {
|
|
417
|
-
|
|
418
|
-
|
|
1368
|
+
logger5.warn("Audit module is already initialized, reinitializing");
|
|
1369
|
+
currentRepo = null;
|
|
1370
|
+
currentWriter = null;
|
|
1371
|
+
capturePolicy = null;
|
|
1372
|
+
currentHelper = null;
|
|
1373
|
+
const closingBackup = currentBackup;
|
|
1374
|
+
currentBackup = null;
|
|
1375
|
+
const closingAlerts = currentAlerts;
|
|
1376
|
+
currentAlerts = null;
|
|
1377
|
+
await closingAlerts?.close();
|
|
1378
|
+
await closingBackup?.close();
|
|
1379
|
+
await Promise.allSettled([...inFlightWrites]);
|
|
419
1380
|
}
|
|
420
|
-
|
|
1381
|
+
logger5.info("Initializing audit module");
|
|
421
1382
|
const parseResult = AuditInitConfigSchema.safeParse(config ?? {});
|
|
422
1383
|
if (!parseResult.success) {
|
|
423
|
-
|
|
1384
|
+
logger5.error("Audit config validation failed", { error: parseResult.error.message });
|
|
424
1385
|
return err(
|
|
425
1386
|
HaiAuditError.CONFIG_ERROR,
|
|
426
1387
|
auditM("audit_configError", { params: { error: parseResult.error.message } }),
|
|
@@ -428,25 +1389,51 @@ var audit = {
|
|
|
428
1389
|
);
|
|
429
1390
|
}
|
|
430
1391
|
const parsed = parseResult.data;
|
|
1392
|
+
if (parsed.alerts.enabled && (!config?.reach?.isInitialized || parsed.alerts.rules.some((rule) => rule.targets.some((target) => !config.reach?.config?.providers.some((provider) => provider.name === target.provider)))))
|
|
1393
|
+
return err(HaiAuditError.CONFIG_ERROR, auditM("audit_alertReachRequired"));
|
|
1394
|
+
if (parsed.backup.enabled && !config?.storage?.isInitialized)
|
|
1395
|
+
return err(HaiAuditError.CONFIG_ERROR, auditM("audit_backupStorageRequired"));
|
|
1396
|
+
if (parsed.backup.enabled && config?.storage?.config?.type === "s3" && config.storage.config.publicUrl)
|
|
1397
|
+
return err(HaiAuditError.CONFIG_ERROR, auditM("audit_backupPrivateStorageRequired"));
|
|
431
1398
|
const identifierResult = validateIdentifiers([parsed.userTable, parsed.userIdColumn, parsed.userNameColumn]);
|
|
432
1399
|
if (!identifierResult.success) {
|
|
433
|
-
|
|
1400
|
+
logger5.error("Audit config contains invalid identifiers", { error: identifierResult.error.message });
|
|
434
1401
|
return err(
|
|
435
1402
|
HaiAuditError.CONFIG_ERROR,
|
|
436
1403
|
auditM("audit_configError", { params: { error: identifierResult.error.message } }),
|
|
437
1404
|
identifierResult.error
|
|
438
1405
|
);
|
|
439
1406
|
}
|
|
440
|
-
|
|
1407
|
+
const migrated = await migrateAudit();
|
|
1408
|
+
if (!migrated.success)
|
|
1409
|
+
return migrated;
|
|
1410
|
+
const candidate = new AuditLogRepository({
|
|
441
1411
|
userTable: parsed.userTable,
|
|
442
1412
|
userIdColumn: parsed.userIdColumn,
|
|
443
1413
|
userNameColumn: parsed.userNameColumn
|
|
444
1414
|
});
|
|
445
|
-
|
|
446
|
-
|
|
1415
|
+
const ready = await candidate.ready();
|
|
1416
|
+
if (!ready.success) {
|
|
1417
|
+
return err(HaiAuditError.CONFIG_ERROR, auditM("audit_initFailed", { params: { error: ready.error.message } }), ready.error);
|
|
1418
|
+
}
|
|
1419
|
+
const protection = await migrateAudit();
|
|
1420
|
+
if (!protection.success)
|
|
1421
|
+
return protection;
|
|
1422
|
+
const configured = await configureAuditRetention(parsed.retention);
|
|
1423
|
+
if (!configured.success)
|
|
1424
|
+
return configured;
|
|
1425
|
+
if (generation !== lifecycleGeneration)
|
|
1426
|
+
return err(HaiAuditError.NOT_INITIALIZED, auditM("audit_notInitialized"));
|
|
1427
|
+
currentRepo = candidate;
|
|
1428
|
+
currentWriter = createAuditWriter(candidate, parsed);
|
|
1429
|
+
capturePolicy = parsed.capture;
|
|
1430
|
+
currentHelper = createHelper((input) => audit.log(input));
|
|
1431
|
+
currentBackup = createAuditBackup({ repository: candidate, config: parsed.backup, storage: config?.storage, capture: (input, options) => audit.capture(input, options) });
|
|
1432
|
+
currentAlerts = createAuditAlerts({ config: parsed.alerts, reach: config?.reach, log: currentWriter });
|
|
1433
|
+
logger5.info("Audit module initialized");
|
|
447
1434
|
return ok(void 0);
|
|
448
1435
|
} catch (error) {
|
|
449
|
-
|
|
1436
|
+
logger5.error("Audit module initialization failed", { error });
|
|
450
1437
|
return err(
|
|
451
1438
|
HaiAuditError.CONFIG_ERROR,
|
|
452
1439
|
auditM("audit_initFailed", { params: { error: error instanceof Error ? error.message : String(error) } }),
|
|
@@ -466,34 +1453,35 @@ var audit = {
|
|
|
466
1453
|
* @param input - 日志内容(action 和 resource 为必填)
|
|
467
1454
|
* @returns 成功时返回创建的 AuditLog;未初始化时返回 NOT_INITIALIZED
|
|
468
1455
|
*/
|
|
469
|
-
log(input) {
|
|
1456
|
+
log(input, tx) {
|
|
470
1457
|
if (!currentRepo) {
|
|
471
1458
|
return Promise.resolve(notInitialized.result());
|
|
472
1459
|
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (!
|
|
480
|
-
return
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
return currentRepo.log(input);
|
|
1460
|
+
const operation = currentWriter(input, tx);
|
|
1461
|
+
inFlightWrites.add(operation);
|
|
1462
|
+
void operation.then(() => inFlightWrites.delete(operation), () => inFlightWrites.delete(operation));
|
|
1463
|
+
return operation;
|
|
1464
|
+
},
|
|
1465
|
+
async capture(input, options) {
|
|
1466
|
+
if (!currentRepo)
|
|
1467
|
+
return notInitialized.result();
|
|
1468
|
+
if (!capturePolicy?.enabled || options?.read && !capturePolicy.includeReads && (!input.outcome || input.outcome === "success"))
|
|
1469
|
+
return ok(void 0);
|
|
1470
|
+
const written = await audit.log(input, options?.tx);
|
|
1471
|
+
return written.success ? ok(void 0) : written;
|
|
486
1472
|
},
|
|
487
1473
|
/**
|
|
488
1474
|
* 分页查询审计日志列表(含用户名 LEFT JOIN)
|
|
489
1475
|
*
|
|
490
1476
|
* @param options - 过滤条件与分页参数
|
|
491
|
-
* @returns 成功时返回
|
|
1477
|
+
* @returns 成功时返回 PaginatedResult(items/total/page/pageSize);未初始化时返回 NOT_INITIALIZED
|
|
492
1478
|
*/
|
|
493
1479
|
list(options) {
|
|
494
1480
|
if (!currentRepo) {
|
|
495
1481
|
return Promise.resolve(notInitialized.result());
|
|
496
1482
|
}
|
|
1483
|
+
if (!validAuditQuery(options))
|
|
1484
|
+
return Promise.resolve(err(HaiAuditError.QUERY_FAILED, auditM("audit_invalidInput", { params: { field: "query", reason: auditM("audit_queryInvalid") } })));
|
|
497
1485
|
if (options?.startDate && options?.endDate && options.startDate > options.endDate) {
|
|
498
1486
|
return Promise.resolve(err(
|
|
499
1487
|
HaiAuditError.QUERY_FAILED,
|
|
@@ -551,17 +1539,19 @@ var audit = {
|
|
|
551
1539
|
* @param days - 统计天数,默认 7
|
|
552
1540
|
* @returns 成功时返回 AuditStatItem 数组;未初始化时返回 NOT_INITIALIZED
|
|
553
1541
|
*/
|
|
554
|
-
getStats(days) {
|
|
1542
|
+
getStats(days, filters) {
|
|
555
1543
|
if (!currentRepo) {
|
|
556
1544
|
return Promise.resolve(notInitialized.result());
|
|
557
1545
|
}
|
|
1546
|
+
if (!validAuditQuery(filters))
|
|
1547
|
+
return Promise.resolve(err(HaiAuditError.STATS_FAILED, auditM("audit_invalidInput", { params: { field: "query", reason: auditM("audit_queryInvalid") } })));
|
|
558
1548
|
if (typeof days === "number" && (!Number.isInteger(days) || days < 0)) {
|
|
559
1549
|
return Promise.resolve(err(
|
|
560
1550
|
HaiAuditError.STATS_FAILED,
|
|
561
1551
|
auditM("audit_invalidInput", { params: { field: "days", reason: "must be a non-negative integer" } })
|
|
562
1552
|
));
|
|
563
1553
|
}
|
|
564
|
-
return currentRepo.getStats(days);
|
|
1554
|
+
return currentRepo.getStats(days, filters);
|
|
565
1555
|
},
|
|
566
1556
|
/**
|
|
567
1557
|
* 便捷记录器
|
|
@@ -574,21 +1564,50 @@ var audit = {
|
|
|
574
1564
|
}
|
|
575
1565
|
return currentHelper;
|
|
576
1566
|
},
|
|
1567
|
+
get backup() {
|
|
1568
|
+
return currentBackup?.operations ?? notInitializedBackup;
|
|
1569
|
+
},
|
|
1570
|
+
checkAlerts(options) {
|
|
1571
|
+
return currentAlerts ? currentAlerts.check(options) : Promise.resolve(notInitialized.result());
|
|
1572
|
+
},
|
|
1573
|
+
get schedulerPlan() {
|
|
1574
|
+
const backup = currentBackup?.schedule;
|
|
1575
|
+
const alerts = currentAlerts?.schedule;
|
|
1576
|
+
if (!backup && !alerts)
|
|
1577
|
+
return null;
|
|
1578
|
+
return {
|
|
1579
|
+
tasks: [...backup?.tasks ?? [], ...alerts?.tasks ?? []].map((task) => ({ ...task })),
|
|
1580
|
+
hooks: { onTaskExecute: async (input) => {
|
|
1581
|
+
if (input.context.taskId === "hai-audit-backup" && backup)
|
|
1582
|
+
return backup.hooks.onTaskExecute(input);
|
|
1583
|
+
if (input.context.taskId === "hai-audit-alerts" && alerts)
|
|
1584
|
+
return alerts.hooks.onTaskExecute(input);
|
|
1585
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.CONFIG_ERROR, auditM("audit_unknownTask"));
|
|
1586
|
+
} }
|
|
1587
|
+
};
|
|
1588
|
+
},
|
|
577
1589
|
/**
|
|
578
1590
|
* 关闭审计模块,释放内部状态
|
|
579
1591
|
*/
|
|
580
1592
|
async close() {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
logger2.info("Closing audit module");
|
|
1593
|
+
lifecycleGeneration++;
|
|
1594
|
+
logger5.info("Closing audit module");
|
|
586
1595
|
currentRepo = null;
|
|
1596
|
+
currentWriter = null;
|
|
1597
|
+
capturePolicy = null;
|
|
587
1598
|
currentHelper = null;
|
|
588
|
-
|
|
1599
|
+
const closingBackup = currentBackup;
|
|
1600
|
+
currentBackup = null;
|
|
1601
|
+
const closingAlerts = currentAlerts;
|
|
1602
|
+
currentAlerts = null;
|
|
1603
|
+
await closingAlerts?.close();
|
|
1604
|
+
await closingBackup?.close();
|
|
1605
|
+
await Promise.allSettled([...inFlightWrites]);
|
|
1606
|
+
logger5.info("Audit module closed");
|
|
1607
|
+
return ok(void 0);
|
|
589
1608
|
}
|
|
590
1609
|
};
|
|
591
1610
|
|
|
592
|
-
export { AuditInitConfigSchema, HaiAuditError, audit,
|
|
1611
|
+
export { AUDIT_BACKUP_PREFIX, AuditInitConfigSchema, HaiAuditError, audit, migrateAudit };
|
|
593
1612
|
//# sourceMappingURL=index.js.map
|
|
594
1613
|
//# sourceMappingURL=index.js.map
|