@h-ai/audit 1.0.0-beta.2 → 1.0.0-beta.21
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 +120 -50
- package/README.ja.md +129 -61
- package/README.md +130 -62
- package/dist/index.d.ts +2 -1
- package/dist/index.js +727 -224
- package/dist/index.js.map +1 -1
- package/package.json +15 -15
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { core, ok, err } from '@h-ai/core';
|
|
3
|
-
import {
|
|
3
|
+
import { reldb, validateIdentifiers, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
4
4
|
import { createHash } from 'crypto';
|
|
5
5
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
6
6
|
import { Buffer } from 'buffer';
|
|
@@ -50,7 +50,12 @@ var AuditInitConfigSchema = z.object({
|
|
|
50
50
|
/** Cron 使用的 IANA 时区,例如 Asia/Shanghai;UTC 不等于北京时间。 */
|
|
51
51
|
timezone: z.string().trim().min(1).max(128).default("UTC")
|
|
52
52
|
}).default({ enabled: false, cron: "* * * * *", timezone: "UTC" })
|
|
53
|
-
}).default({
|
|
53
|
+
}).default({
|
|
54
|
+
enabled: false,
|
|
55
|
+
maxNotificationsPerCheck: 100,
|
|
56
|
+
rules: [],
|
|
57
|
+
schedule: { enabled: false, cron: "* * * * *", timezone: "UTC" }
|
|
58
|
+
}).refine((alerts) => !alerts.enabled || alerts.rules.length > 0),
|
|
54
59
|
/** 可选的流式归档;自动执行由宿主现有 Scheduler 装配。 */
|
|
55
60
|
backup: z.object({
|
|
56
61
|
/** 是否允许手动/计划归档审计;需已初始化的私有 Storage,关闭不影响审计采集。 */
|
|
@@ -68,7 +73,12 @@ var AuditInitConfigSchema = z.object({
|
|
|
68
73
|
/** 定时备份采用的 IANA 时区;Asia/Shanghai 为北京时间,UTC 的 02:00 对应北京时间 10:00。 */
|
|
69
74
|
timezone: z.string().trim().min(1).max(128).default("UTC")
|
|
70
75
|
}).default({ enabled: false, cron: "0 2 * * *", timezone: "UTC" })
|
|
71
|
-
}).default({
|
|
76
|
+
}).default({
|
|
77
|
+
enabled: false,
|
|
78
|
+
batchSize: 200,
|
|
79
|
+
maxRecordBytes: 1024 * 1024,
|
|
80
|
+
schedule: { enabled: false, cron: "0 2 * * *", timezone: "UTC" }
|
|
81
|
+
}),
|
|
72
82
|
/** 框架入口自动采集;显式 audit.log 不受此开关影响。 */
|
|
73
83
|
capture: z.object({
|
|
74
84
|
/** 是否自动记录已接入框架入口的重要操作、失败和拒绝;关闭不阻止显式 audit.log 写入。 */
|
|
@@ -104,8 +114,17 @@ function getAuditContext() {
|
|
|
104
114
|
function withAuditContext(context, operation) {
|
|
105
115
|
const previous = getAuditContext();
|
|
106
116
|
const actor = Object.hasOwn(context, "actor") ? context.actor : previous.actor;
|
|
107
|
-
const snapshot = actor ? {
|
|
108
|
-
|
|
117
|
+
const snapshot = actor ? {
|
|
118
|
+
...actor,
|
|
119
|
+
...actor.roles ? {
|
|
120
|
+
roles: [...actor.roles]
|
|
121
|
+
} : {}
|
|
122
|
+
} : actor;
|
|
123
|
+
return storage.run({
|
|
124
|
+
...previous,
|
|
125
|
+
...context,
|
|
126
|
+
actor: snapshot
|
|
127
|
+
}, operation);
|
|
109
128
|
}
|
|
110
129
|
|
|
111
130
|
// messages/en-US.json
|
|
@@ -273,7 +292,12 @@ function createAuditAlerts(input) {
|
|
|
273
292
|
if (!time.success)
|
|
274
293
|
return time;
|
|
275
294
|
const now = time.data.getTime();
|
|
276
|
-
const report = {
|
|
295
|
+
const report = {
|
|
296
|
+
submitted: 0,
|
|
297
|
+
queued: 0,
|
|
298
|
+
failed: 0,
|
|
299
|
+
more: false
|
|
300
|
+
};
|
|
277
301
|
let attempted = 0;
|
|
278
302
|
for (const rule of input.config.rules) {
|
|
279
303
|
const duration = rule.windowSeconds * 1e3;
|
|
@@ -281,12 +305,26 @@ function createAuditAlerts(input) {
|
|
|
281
305
|
for (const start of [current - duration, current]) {
|
|
282
306
|
signal.throwIfAborted();
|
|
283
307
|
const end = Math.min(start + duration, now + 1);
|
|
284
|
-
const actions = [
|
|
285
|
-
|
|
308
|
+
const actions = [
|
|
309
|
+
...new Set(rule.actions)
|
|
310
|
+
].sort();
|
|
311
|
+
const outcomes = [
|
|
312
|
+
...new Set(rule.outcomes)
|
|
313
|
+
].sort();
|
|
286
314
|
const severities = levels.slice(levels.indexOf(rule.minimumSeverity));
|
|
287
315
|
const date = (value) => type === "sqlite" ? value : new Date(value);
|
|
288
|
-
const conditions = [
|
|
289
|
-
|
|
316
|
+
const conditions = [
|
|
317
|
+
"created_at >= ?",
|
|
318
|
+
"created_at < ?",
|
|
319
|
+
"resource <> ?",
|
|
320
|
+
`action IN (${actions.map(() => "?").join(",")})`
|
|
321
|
+
];
|
|
322
|
+
const params = [
|
|
323
|
+
date(start),
|
|
324
|
+
date(end),
|
|
325
|
+
"audit.alert",
|
|
326
|
+
...actions
|
|
327
|
+
];
|
|
290
328
|
if (outcomes.length) {
|
|
291
329
|
conditions.push(`outcome IN (${outcomes.map(() => "?").join(",")})`);
|
|
292
330
|
params.push(...outcomes);
|
|
@@ -309,7 +347,16 @@ function createAuditAlerts(input) {
|
|
|
309
347
|
continue;
|
|
310
348
|
for (const target of rule.targets) {
|
|
311
349
|
signal.throwIfAborted();
|
|
312
|
-
const id = hash({
|
|
350
|
+
const id = hash({
|
|
351
|
+
rule: {
|
|
352
|
+
...rule,
|
|
353
|
+
actions,
|
|
354
|
+
outcomes,
|
|
355
|
+
targets: void 0
|
|
356
|
+
},
|
|
357
|
+
target,
|
|
358
|
+
start
|
|
359
|
+
});
|
|
313
360
|
if (attempted >= input.config.maxNotificationsPerCheck) {
|
|
314
361
|
const existing = await reldb.sql.get(`SELECT id FROM ${table} WHERE id=?`, [id]);
|
|
315
362
|
if (!existing.success)
|
|
@@ -320,14 +367,36 @@ function createAuditAlerts(input) {
|
|
|
320
367
|
}
|
|
321
368
|
const selection = `SELECT ?, ?, ?, ?, 'claimed' WHERE ? <= ${clock} AND ? > ${clock}`;
|
|
322
369
|
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, [
|
|
370
|
+
const claimed = await reldb.sql.execute(statement, [
|
|
371
|
+
id,
|
|
372
|
+
rule.id,
|
|
373
|
+
target.id,
|
|
374
|
+
start,
|
|
375
|
+
start,
|
|
376
|
+
start + duration * 2
|
|
377
|
+
]);
|
|
324
378
|
if (!claimed.success)
|
|
325
379
|
return claimed;
|
|
326
380
|
if (claimed.data.changes === 0)
|
|
327
381
|
continue;
|
|
328
382
|
attempted++;
|
|
329
|
-
const details = {
|
|
330
|
-
|
|
383
|
+
const details = {
|
|
384
|
+
ruleId: rule.id,
|
|
385
|
+
targetId: target.id,
|
|
386
|
+
count,
|
|
387
|
+
windowStart: new Date(start).toISOString(),
|
|
388
|
+
windowEnd: new Date(start + duration).toISOString(),
|
|
389
|
+
level: rule.severity
|
|
390
|
+
};
|
|
391
|
+
const recorded = await input.log({
|
|
392
|
+
action: "audit.alert.claimed",
|
|
393
|
+
resource: "audit.alert",
|
|
394
|
+
resourceId: id,
|
|
395
|
+
category: "system",
|
|
396
|
+
severity: rule.severity,
|
|
397
|
+
outcome: "success",
|
|
398
|
+
details
|
|
399
|
+
});
|
|
331
400
|
if (!recorded.success)
|
|
332
401
|
return recorded;
|
|
333
402
|
signal.throwIfAborted();
|
|
@@ -336,10 +405,23 @@ function createAuditAlerts(input) {
|
|
|
336
405
|
return sending;
|
|
337
406
|
if (sending.data.changes !== 1)
|
|
338
407
|
continue;
|
|
339
|
-
const sent = await input.reach.send({
|
|
408
|
+
const sent = await input.reach.send({
|
|
409
|
+
provider: target.provider,
|
|
410
|
+
to: target.to,
|
|
411
|
+
subject: auditM("audit_alertSubject", {
|
|
412
|
+
params: { severity: rule.severity, rule: rule.id }
|
|
413
|
+
}),
|
|
414
|
+
body: JSON.stringify(details),
|
|
415
|
+
idempotencyKey: id
|
|
416
|
+
});
|
|
340
417
|
const accepted = sent.success && sent.data.success;
|
|
341
418
|
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'`, [
|
|
419
|
+
const updated = await reldb.sql.execute(`UPDATE ${table} SET status=?, queue_id=?, error_code=? WHERE id=? AND status='sending'`, [
|
|
420
|
+
status,
|
|
421
|
+
sent.success && sent.data.deferred ? sent.data.queueId : null,
|
|
422
|
+
sent.success ? null : sent.error.code,
|
|
423
|
+
id
|
|
424
|
+
]);
|
|
343
425
|
if (!updated.success || updated.data.changes !== 1)
|
|
344
426
|
return err(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"), updated.success ? void 0 : updated.error);
|
|
345
427
|
if (status === "queued")
|
|
@@ -348,11 +430,30 @@ function createAuditAlerts(input) {
|
|
|
348
430
|
report.submitted++;
|
|
349
431
|
else
|
|
350
432
|
report.failed++;
|
|
351
|
-
const outcome = await input.log({
|
|
433
|
+
const outcome = await input.log({
|
|
434
|
+
action: "audit.alert.notification",
|
|
435
|
+
resource: "audit.alert",
|
|
436
|
+
resourceId: id,
|
|
437
|
+
category: "system",
|
|
438
|
+
severity: rule.severity,
|
|
439
|
+
outcome: accepted ? "success" : "failure",
|
|
440
|
+
details: {
|
|
441
|
+
...details,
|
|
442
|
+
status,
|
|
443
|
+
...sent.success ? {} : { reasonCode: sent.error.code },
|
|
444
|
+
...sent.success && sent.data.deferred ? { queueId: sent.data.queueId } : {}
|
|
445
|
+
}
|
|
446
|
+
});
|
|
352
447
|
if (!outcome.success)
|
|
353
448
|
return outcome;
|
|
354
|
-
if (!accepted)
|
|
355
|
-
logger.error("Audit alert notification outcome requires review", {
|
|
449
|
+
if (!accepted) {
|
|
450
|
+
logger.error("Audit alert notification outcome requires review", {
|
|
451
|
+
id,
|
|
452
|
+
ruleId: rule.id,
|
|
453
|
+
targetId: target.id,
|
|
454
|
+
errorCode: sent.success ? void 0 : sent.error.code
|
|
455
|
+
});
|
|
456
|
+
}
|
|
356
457
|
}
|
|
357
458
|
}
|
|
358
459
|
}
|
|
@@ -371,7 +472,10 @@ function createAuditAlerts(input) {
|
|
|
371
472
|
if (options?.signal?.aborted)
|
|
372
473
|
cancel();
|
|
373
474
|
controllers.add(controller);
|
|
374
|
-
const operation = withAuditContext({
|
|
475
|
+
const operation = withAuditContext({
|
|
476
|
+
actor: { id: "system:audit-alerts", username: "scheduler" },
|
|
477
|
+
category: "system"
|
|
478
|
+
}, () => run(controller.signal)).catch((error) => {
|
|
375
479
|
logger.error("Audit alert check failed", { error });
|
|
376
480
|
return err(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"), error);
|
|
377
481
|
});
|
|
@@ -386,15 +490,25 @@ function createAuditAlerts(input) {
|
|
|
386
490
|
let schedule = null;
|
|
387
491
|
if (input.config.enabled && input.config.schedule.enabled) {
|
|
388
492
|
schedule = {
|
|
389
|
-
tasks: [
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
493
|
+
tasks: [
|
|
494
|
+
{
|
|
495
|
+
id: "hai-audit-alerts",
|
|
496
|
+
name: auditM("audit_alertTaskName"),
|
|
497
|
+
idempotent: true,
|
|
498
|
+
cron: input.config.schedule.cron,
|
|
499
|
+
timezone: input.config.schedule.timezone
|
|
500
|
+
}
|
|
501
|
+
],
|
|
502
|
+
hooks: {
|
|
503
|
+
onTaskExecute: async ({ context, signal }) => {
|
|
504
|
+
if (context.taskId !== "hai-audit-alerts")
|
|
505
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"));
|
|
506
|
+
const result = await check({ signal });
|
|
507
|
+
if (!result.success || result.data.failed > 0)
|
|
508
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.ALERTS_FAILED, auditM("audit_alertsFailed"), result.success ? void 0 : result.error);
|
|
509
|
+
return result.data;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
398
512
|
};
|
|
399
513
|
}
|
|
400
514
|
return {
|
|
@@ -408,7 +522,12 @@ function createAuditAlerts(input) {
|
|
|
408
522
|
};
|
|
409
523
|
}
|
|
410
524
|
var logger2 = core.logger.child({ module: "audit", scope: "writer" });
|
|
411
|
-
var actorSchema = z.object({
|
|
525
|
+
var actorSchema = z.object({
|
|
526
|
+
id: z.string().min(1).max(256),
|
|
527
|
+
username: z.string().max(256).optional(),
|
|
528
|
+
displayName: z.string().max(256).optional(),
|
|
529
|
+
roles: z.array(z.string().max(256)).max(1e3).optional()
|
|
530
|
+
});
|
|
412
531
|
var detailsSchema = z.record(z.string(), z.unknown());
|
|
413
532
|
var inputSchema = z.object({
|
|
414
533
|
userId: z.string().min(1).max(256).nullable().optional(),
|
|
@@ -436,7 +555,14 @@ var querySchema = z.object({
|
|
|
436
555
|
category: z.enum(["system", "business"]).optional(),
|
|
437
556
|
outcome: z.enum(["success", "failure", "denied"]).optional(),
|
|
438
557
|
severity: z.enum(["info", "warning", "error", "critical"]).optional(),
|
|
439
|
-
sortBy: z.enum([
|
|
558
|
+
sortBy: z.enum([
|
|
559
|
+
"createdAt",
|
|
560
|
+
"action",
|
|
561
|
+
"resource",
|
|
562
|
+
"category",
|
|
563
|
+
"outcome",
|
|
564
|
+
"severity"
|
|
565
|
+
]).optional(),
|
|
440
566
|
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
441
567
|
startDate: z.date().optional(),
|
|
442
568
|
endDate: z.date().optional()
|
|
@@ -459,8 +585,15 @@ function createAuditWriter(repository, config) {
|
|
|
459
585
|
try {
|
|
460
586
|
const parsed = inputSchema.safeParse(input);
|
|
461
587
|
if (!parsed.success) {
|
|
462
|
-
logger2.error("Audit input rejected", {
|
|
463
|
-
|
|
588
|
+
logger2.error("Audit input rejected", {
|
|
589
|
+
issues: parsed.error.issues.map((issue) => issue.path.join("."))
|
|
590
|
+
});
|
|
591
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_invalidInput", {
|
|
592
|
+
params: {
|
|
593
|
+
field: "event",
|
|
594
|
+
reason: auditM("audit_eventFieldsInvalid")
|
|
595
|
+
}
|
|
596
|
+
}));
|
|
464
597
|
}
|
|
465
598
|
const value = parsed.data;
|
|
466
599
|
const context = getAuditContext();
|
|
@@ -468,8 +601,15 @@ function createAuditWriter(repository, config) {
|
|
|
468
601
|
const actor = Object.hasOwn(value, "actorSnapshot") ? value.actorSnapshot : context.actor;
|
|
469
602
|
const userId = value.userId !== void 0 ? value.userId : actor?.id ?? correlation.userId ?? null;
|
|
470
603
|
if (value.actorSnapshot && value.actorSnapshot.id !== userId) {
|
|
471
|
-
logger2.error("Audit actor snapshot rejected", {
|
|
472
|
-
|
|
604
|
+
logger2.error("Audit actor snapshot rejected", {
|
|
605
|
+
reason: auditM("audit_identityMismatch")
|
|
606
|
+
});
|
|
607
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_invalidInput", {
|
|
608
|
+
params: {
|
|
609
|
+
field: "actorSnapshot",
|
|
610
|
+
reason: auditM("audit_identityMismatch")
|
|
611
|
+
}
|
|
612
|
+
}));
|
|
473
613
|
}
|
|
474
614
|
const snapshot = actor?.id === userId ? readAuditActor(actor) : userId ? await repository.captureActor(userId, tx) : null;
|
|
475
615
|
const outcome = value.outcome ?? "success";
|
|
@@ -497,12 +637,22 @@ function createAuditWriter(repository, config) {
|
|
|
497
637
|
userAgent: value.userAgent ?? context.userAgent ?? null,
|
|
498
638
|
details
|
|
499
639
|
}, tx);
|
|
500
|
-
if (!result.success)
|
|
501
|
-
logger2.error("Audit event persistence failed", {
|
|
640
|
+
if (!result.success) {
|
|
641
|
+
logger2.error("Audit event persistence failed", {
|
|
642
|
+
action: value.action,
|
|
643
|
+
resource: value.resource,
|
|
644
|
+
requestId: value.requestId ?? context.requestId,
|
|
645
|
+
error: result.error
|
|
646
|
+
});
|
|
647
|
+
}
|
|
502
648
|
return result;
|
|
503
649
|
} catch (error) {
|
|
504
650
|
logger2.error("Audit event processing failed", { requestId: getAuditContext().requestId, error });
|
|
505
|
-
return err(HaiAuditError.LOG_FAILED, auditM("audit_logFailed", {
|
|
651
|
+
return err(HaiAuditError.LOG_FAILED, auditM("audit_logFailed", {
|
|
652
|
+
params: {
|
|
653
|
+
error: auditM("audit_eventProcessingFailed")
|
|
654
|
+
}
|
|
655
|
+
}), error);
|
|
506
656
|
}
|
|
507
657
|
};
|
|
508
658
|
}
|
|
@@ -520,8 +670,21 @@ var backupHeaderSchema = z.object({
|
|
|
520
670
|
from: z.iso.datetime().nullable(),
|
|
521
671
|
requestHash: digestSchema
|
|
522
672
|
});
|
|
523
|
-
var footerSchema = z.object({
|
|
524
|
-
|
|
673
|
+
var footerSchema = z.object({
|
|
674
|
+
type: z.literal("footer"),
|
|
675
|
+
recordCount: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
676
|
+
sha256: digestSchema
|
|
677
|
+
});
|
|
678
|
+
var entrySchema = z.object({
|
|
679
|
+
type: z.literal("entry"),
|
|
680
|
+
record: z.object({
|
|
681
|
+
id: z.string().min(1),
|
|
682
|
+
action: z.string().min(1),
|
|
683
|
+
resource: z.string().min(1),
|
|
684
|
+
createdAt: z.iso.datetime(),
|
|
685
|
+
actorSnapshot: z.unknown().optional()
|
|
686
|
+
}).passthrough()
|
|
687
|
+
});
|
|
525
688
|
function archiveLine(value, maxRecordBytes) {
|
|
526
689
|
const encoded = Buffer.from(`${JSON.stringify(value)}
|
|
527
690
|
`, "utf8");
|
|
@@ -583,17 +746,32 @@ async function verifyArchive(source, options) {
|
|
|
583
746
|
const entry = entrySchema.parse(value);
|
|
584
747
|
readAuditActor(entry.record.actorSnapshot);
|
|
585
748
|
if (!Number.isSafeInteger(++count))
|
|
586
|
-
throw new
|
|
749
|
+
throw new TypeError("Audit archive record count exceeds safe range");
|
|
587
750
|
hash2.update(line);
|
|
588
751
|
}
|
|
589
752
|
if (!header || !footer)
|
|
590
753
|
throw new Error("Audit archive is incomplete");
|
|
591
|
-
return {
|
|
754
|
+
return {
|
|
755
|
+
info: {
|
|
756
|
+
id: header.id,
|
|
757
|
+
key: options.key,
|
|
758
|
+
recordCount: footer.recordCount,
|
|
759
|
+
sha256: footer.sha256,
|
|
760
|
+
createdAt: new Date(header.createdAt),
|
|
761
|
+
cutoff: new Date(header.cutoff)
|
|
762
|
+
},
|
|
763
|
+
requestHash: header.requestHash
|
|
764
|
+
};
|
|
592
765
|
}
|
|
593
766
|
|
|
594
767
|
// src/audit-backup-functions.ts
|
|
595
768
|
var logger3 = core.logger.child({ module: "audit", scope: "backup" });
|
|
596
|
-
var optionsSchema = z.object({
|
|
769
|
+
var optionsSchema = z.object({
|
|
770
|
+
id: backupIdSchema.optional(),
|
|
771
|
+
from: z.date().optional(),
|
|
772
|
+
to: z.date().optional(),
|
|
773
|
+
signal: z.instanceof(AbortSignal).optional()
|
|
774
|
+
});
|
|
597
775
|
function createAuditBackup(input) {
|
|
598
776
|
let active = true;
|
|
599
777
|
const controllers = /* @__PURE__ */ new Set();
|
|
@@ -658,7 +836,12 @@ function createAuditBackup(input) {
|
|
|
658
836
|
const from = values.from ? new Date(values.from.getTime()) : void 0;
|
|
659
837
|
const key = `${AUDIT_BACKUP_PREFIX}${id}.jsonl`;
|
|
660
838
|
const database = reldb.config;
|
|
661
|
-
const source = database?.type === "sqlite" ? { type: database.type, database: database.database } : {
|
|
839
|
+
const source = database?.type === "sqlite" ? { type: database.type, database: database.database } : {
|
|
840
|
+
type: database?.type,
|
|
841
|
+
host: database?.host,
|
|
842
|
+
port: database?.port,
|
|
843
|
+
database: database?.database
|
|
844
|
+
};
|
|
662
845
|
const requestHash = createHash("sha256").update(JSON.stringify({ source, from: from?.toISOString() ?? null, to: values.to?.toISOString() ?? null })).digest("hex");
|
|
663
846
|
return track(async (signal) => {
|
|
664
847
|
const time = await readAuditTime();
|
|
@@ -676,14 +859,28 @@ function createAuditBackup(input) {
|
|
|
676
859
|
let completed;
|
|
677
860
|
async function* content() {
|
|
678
861
|
const hash2 = createHash("sha256");
|
|
679
|
-
const header = archiveLine({
|
|
862
|
+
const header = archiveLine({
|
|
863
|
+
type: "header",
|
|
864
|
+
format: "hai-audit",
|
|
865
|
+
version: 1,
|
|
866
|
+
id,
|
|
867
|
+
createdAt: createdAt.toISOString(),
|
|
868
|
+
cutoff: cutoff.toISOString(),
|
|
869
|
+
from: from?.toISOString() ?? null,
|
|
870
|
+
requestHash
|
|
871
|
+
}, input.config.maxRecordBytes);
|
|
680
872
|
hash2.update(header);
|
|
681
873
|
yield header;
|
|
682
874
|
let afterId;
|
|
683
875
|
let count = 0;
|
|
684
876
|
while (true) {
|
|
685
877
|
signal.throwIfAborted();
|
|
686
|
-
const page = await input.repository.archivePage({
|
|
878
|
+
const page = await input.repository.archivePage({
|
|
879
|
+
afterId,
|
|
880
|
+
from,
|
|
881
|
+
cutoff,
|
|
882
|
+
limit: input.config.batchSize
|
|
883
|
+
});
|
|
687
884
|
if (!page.success)
|
|
688
885
|
throw core.error.buildHaiErrorInst(HaiAuditError.BACKUP_FAILED, auditM("audit_backupFailed"), page.error);
|
|
689
886
|
for (const record of page.data) {
|
|
@@ -692,7 +889,7 @@ function createAuditBackup(input) {
|
|
|
692
889
|
hash2.update(line);
|
|
693
890
|
yield line;
|
|
694
891
|
if (!Number.isSafeInteger(++count))
|
|
695
|
-
throw new
|
|
892
|
+
throw new TypeError("Audit archive record count exceeds safe range");
|
|
696
893
|
afterId = record.id;
|
|
697
894
|
}
|
|
698
895
|
if (page.data.length < input.config.batchSize)
|
|
@@ -700,12 +897,24 @@ function createAuditBackup(input) {
|
|
|
700
897
|
}
|
|
701
898
|
const sha256 = hash2.digest("hex");
|
|
702
899
|
yield archiveLine({ type: "footer", recordCount: count, sha256 }, input.config.maxRecordBytes);
|
|
703
|
-
completed = {
|
|
900
|
+
completed = {
|
|
901
|
+
id,
|
|
902
|
+
key,
|
|
903
|
+
recordCount: count,
|
|
904
|
+
sha256,
|
|
905
|
+
createdAt,
|
|
906
|
+
cutoff
|
|
907
|
+
};
|
|
704
908
|
}
|
|
705
909
|
const stream = content();
|
|
706
910
|
let uploaded;
|
|
707
911
|
try {
|
|
708
|
-
uploaded = await input.storage.file.put(key, stream, {
|
|
912
|
+
uploaded = await input.storage.file.put(key, stream, {
|
|
913
|
+
ifNoneMatch: "*",
|
|
914
|
+
contentType: "application/x-ndjson",
|
|
915
|
+
cacheControl: "private, no-store",
|
|
916
|
+
signal
|
|
917
|
+
});
|
|
709
918
|
} finally {
|
|
710
919
|
await stream.return(void 0);
|
|
711
920
|
}
|
|
@@ -735,15 +944,28 @@ function createAuditBackup(input) {
|
|
|
735
944
|
let schedule = null;
|
|
736
945
|
if (input.config.enabled && input.config.schedule.enabled) {
|
|
737
946
|
schedule = {
|
|
738
|
-
tasks: [
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
947
|
+
tasks: [
|
|
948
|
+
{
|
|
949
|
+
id: "hai-audit-backup",
|
|
950
|
+
name: auditM("audit_backupTaskName"),
|
|
951
|
+
idempotent: true,
|
|
952
|
+
cron: input.config.schedule.cron,
|
|
953
|
+
timezone: input.config.schedule.timezone
|
|
954
|
+
}
|
|
955
|
+
],
|
|
956
|
+
hooks: {
|
|
957
|
+
onTaskExecute: async ({ context, signal }) => {
|
|
958
|
+
if (context.taskId !== "hai-audit-backup" || !context.occurrenceId)
|
|
959
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.BACKUP_INVALID, auditM("audit_backupInvalid"));
|
|
960
|
+
const result = await withAuditContext({
|
|
961
|
+
actor: { id: "system:audit-backup", username: "scheduler" },
|
|
962
|
+
requestId: context.occurrenceId
|
|
963
|
+
}, () => operations.run({ id: context.occurrenceId, signal }));
|
|
964
|
+
if (!result.success)
|
|
965
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.BACKUP_FAILED, result.error.message, result.error);
|
|
966
|
+
return result.data;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
747
969
|
};
|
|
748
970
|
}
|
|
749
971
|
return {
|
|
@@ -766,15 +988,37 @@ function toVoid(result) {
|
|
|
766
988
|
function createHelper(logFn) {
|
|
767
989
|
return {
|
|
768
990
|
async login(userId, ip, ua) {
|
|
769
|
-
const result = await logFn({
|
|
991
|
+
const result = await logFn({
|
|
992
|
+
userId,
|
|
993
|
+
category: "system",
|
|
994
|
+
action: "login",
|
|
995
|
+
resource: "auth",
|
|
996
|
+
ipAddress: ip,
|
|
997
|
+
userAgent: ua
|
|
998
|
+
});
|
|
770
999
|
return toVoid(result);
|
|
771
1000
|
},
|
|
772
1001
|
async logout(userId, ip, ua) {
|
|
773
|
-
const result = await logFn({
|
|
1002
|
+
const result = await logFn({
|
|
1003
|
+
userId,
|
|
1004
|
+
category: "system",
|
|
1005
|
+
action: "logout",
|
|
1006
|
+
resource: "auth",
|
|
1007
|
+
ipAddress: ip,
|
|
1008
|
+
userAgent: ua
|
|
1009
|
+
});
|
|
774
1010
|
return toVoid(result);
|
|
775
1011
|
},
|
|
776
1012
|
async register(userId, ip, ua) {
|
|
777
|
-
const result = await logFn({
|
|
1013
|
+
const result = await logFn({
|
|
1014
|
+
userId,
|
|
1015
|
+
category: "system",
|
|
1016
|
+
action: "register",
|
|
1017
|
+
resource: "auth",
|
|
1018
|
+
resourceId: userId,
|
|
1019
|
+
ipAddress: ip,
|
|
1020
|
+
userAgent: ua
|
|
1021
|
+
});
|
|
778
1022
|
return toVoid(result);
|
|
779
1023
|
},
|
|
780
1024
|
async passwordResetRequest(email, ip, ua) {
|
|
@@ -789,7 +1033,14 @@ function createHelper(logFn) {
|
|
|
789
1033
|
return toVoid(result);
|
|
790
1034
|
},
|
|
791
1035
|
async passwordResetComplete(userId, ip, ua) {
|
|
792
|
-
const result = await logFn({
|
|
1036
|
+
const result = await logFn({
|
|
1037
|
+
userId,
|
|
1038
|
+
category: "system",
|
|
1039
|
+
action: "password_reset",
|
|
1040
|
+
resource: "auth",
|
|
1041
|
+
ipAddress: ip,
|
|
1042
|
+
userAgent: ua
|
|
1043
|
+
});
|
|
793
1044
|
return toVoid(result);
|
|
794
1045
|
},
|
|
795
1046
|
async crud(input) {
|
|
@@ -799,129 +1050,150 @@ function createHelper(logFn) {
|
|
|
799
1050
|
}
|
|
800
1051
|
};
|
|
801
1052
|
}
|
|
802
|
-
var columns = {
|
|
1053
|
+
var columns = {
|
|
1054
|
+
category: "VARCHAR(16)",
|
|
1055
|
+
outcome: "VARCHAR(16)",
|
|
1056
|
+
severity: "VARCHAR(16)",
|
|
1057
|
+
request_id: "TEXT",
|
|
1058
|
+
trace_id: "TEXT",
|
|
1059
|
+
scope_id: "TEXT",
|
|
1060
|
+
actor_snapshot: "JSON"
|
|
1061
|
+
};
|
|
803
1062
|
var triggers = ["hai_audit_protect_update", "hai_audit_protect_delete", "hai_audit_protect_insert"];
|
|
804
1063
|
async function existingTriggers(sql) {
|
|
805
1064
|
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
1065
|
return result.success ? ok(result.data.map((row) => row.name)) : result;
|
|
807
1066
|
}
|
|
808
|
-
var steps = [
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1067
|
+
var steps = [
|
|
1068
|
+
{
|
|
1069
|
+
version: 1,
|
|
1070
|
+
name: "event-context-and-identity",
|
|
1071
|
+
async needed(sql) {
|
|
1072
|
+
const found = await reldb.schemaColumns("hai_audit_logs", sql);
|
|
1073
|
+
return found.success ? ok(found.data.length > 0 && Object.keys(columns).some((name) => !found.data.includes(name))) : found;
|
|
1074
|
+
},
|
|
1075
|
+
async apply(sql) {
|
|
1076
|
+
const found = await reldb.schemaColumns("hai_audit_logs", sql);
|
|
1077
|
+
if (!found.success)
|
|
1078
|
+
return found;
|
|
1079
|
+
for (const [name, definition] of Object.entries(columns)) {
|
|
1080
|
+
if (found.data.includes(name))
|
|
1081
|
+
continue;
|
|
1082
|
+
const added = await sql.execute(`ALTER TABLE hai_audit_logs ADD COLUMN ${name} ${definition}`);
|
|
1083
|
+
if (!added.success)
|
|
1084
|
+
return added;
|
|
1085
|
+
}
|
|
1086
|
+
return ok(void 0);
|
|
825
1087
|
}
|
|
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
1088
|
},
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
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);
|
|
1089
|
+
{
|
|
1090
|
+
version: 2,
|
|
1091
|
+
name: "persistent-retention-policy",
|
|
1092
|
+
async needed(sql) {
|
|
1093
|
+
const found = await reldb.schemaColumns("hai_audit_policy", sql);
|
|
1094
|
+
if (!found.success || found.data.length === 0) {
|
|
1095
|
+
return found.success ? ok(true) : found;
|
|
1096
|
+
}
|
|
1097
|
+
const row = await sql.get("SELECT id FROM hai_audit_policy WHERE id=1");
|
|
1098
|
+
return row.success ? ok(!row.data) : row;
|
|
1099
|
+
},
|
|
1100
|
+
async apply(sql) {
|
|
1101
|
+
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)");
|
|
874
1102
|
if (!created.success)
|
|
875
1103
|
return created;
|
|
1104
|
+
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");
|
|
876
1105
|
}
|
|
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
1106
|
},
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1107
|
+
{
|
|
1108
|
+
version: 3,
|
|
1109
|
+
name: "retention-write-guards",
|
|
1110
|
+
async needed(sql) {
|
|
1111
|
+
const table2 = await reldb.schemaColumns("hai_audit_logs", sql);
|
|
1112
|
+
if (!table2.success || !table2.data.length) {
|
|
1113
|
+
return table2.success ? ok(false) : table2;
|
|
1114
|
+
}
|
|
1115
|
+
const found = await existingTriggers(sql);
|
|
1116
|
+
return found.success ? ok(triggers.some((name) => !found.data.includes(name))) : found;
|
|
1117
|
+
},
|
|
1118
|
+
async apply(sql) {
|
|
1119
|
+
const found = await existingTriggers(sql);
|
|
1120
|
+
if (!found.success)
|
|
1121
|
+
return found;
|
|
1122
|
+
const enabled = "COALESCE((SELECT enabled FROM hai_audit_policy WHERE id=1),1)=1";
|
|
1123
|
+
const days = "COALESCE((SELECT min_days FROM hai_audit_policy WHERE id=1),90)";
|
|
1124
|
+
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))`;
|
|
1125
|
+
const existingProtected = `${enabled} AND EXISTS(SELECT 1 FROM hai_audit_logs a WHERE a.id=NEW.id AND a.created_at >= ${cutoff})`;
|
|
1126
|
+
if (reldb.config?.type === "postgresql") {
|
|
1127
|
+
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`);
|
|
1128
|
+
if (!fn.success)
|
|
1129
|
+
return fn;
|
|
1130
|
+
}
|
|
1131
|
+
for (const [index, operation] of ["UPDATE", "DELETE", "INSERT"].entries()) {
|
|
1132
|
+
const name = triggers[index];
|
|
1133
|
+
if (found.data.includes(name))
|
|
1134
|
+
continue;
|
|
1135
|
+
const protectedRow = operation === "INSERT" ? existingProtected : `${enabled} AND OLD.created_at >= ${cutoff}`;
|
|
1136
|
+
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`;
|
|
1137
|
+
const created = reldb.config?.type === "mysql" ? await sql.query(statement) : await sql.execute(statement);
|
|
1138
|
+
if (!created.success)
|
|
1139
|
+
return created;
|
|
1140
|
+
}
|
|
1141
|
+
return ok(void 0);
|
|
1142
|
+
}
|
|
898
1143
|
},
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1144
|
+
{
|
|
1145
|
+
version: 4,
|
|
1146
|
+
name: "millisecond-event-time",
|
|
1147
|
+
async needed(sql) {
|
|
1148
|
+
if (reldb.config?.type !== "mysql")
|
|
1149
|
+
return ok(false);
|
|
1150
|
+
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"]);
|
|
1151
|
+
return column.success ? ok(Boolean(column.data && Number(column.data.fractional_digits ?? 0) < 3)) : column;
|
|
1152
|
+
},
|
|
1153
|
+
async apply(sql) {
|
|
1154
|
+
const changed = await sql.execute("ALTER TABLE hai_audit_logs MODIFY COLUMN created_at DATETIME(3)");
|
|
1155
|
+
return changed.success ? ok(void 0) : changed;
|
|
1156
|
+
}
|
|
912
1157
|
},
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
1158
|
+
{
|
|
1159
|
+
version: 5,
|
|
1160
|
+
name: "alert-notification-claims",
|
|
1161
|
+
async needed(sql) {
|
|
1162
|
+
const found = await reldb.schemaColumns("hai_audit_alert_claims", sql);
|
|
1163
|
+
return found.success ? ok(found.data.length === 0) : found;
|
|
1164
|
+
},
|
|
1165
|
+
async apply(sql) {
|
|
1166
|
+
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))");
|
|
1167
|
+
return created.success ? ok(void 0) : created;
|
|
1168
|
+
}
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
version: 6,
|
|
1172
|
+
name: "event-time-index",
|
|
1173
|
+
async needed(sql) {
|
|
1174
|
+
const table2 = await reldb.schemaColumns("hai_audit_logs", sql);
|
|
1175
|
+
if (!table2.success || !table2.data.length) {
|
|
1176
|
+
return table2.success ? ok(false) : table2;
|
|
1177
|
+
}
|
|
1178
|
+
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");
|
|
1179
|
+
return found.success ? ok(!found.data) : found;
|
|
1180
|
+
},
|
|
1181
|
+
async apply(sql) {
|
|
1182
|
+
const created = await sql.execute("CREATE INDEX hai_audit_event_time_idx ON hai_audit_logs (created_at)");
|
|
1183
|
+
return created.success ? ok(void 0) : created;
|
|
1184
|
+
}
|
|
916
1185
|
}
|
|
917
|
-
|
|
1186
|
+
];
|
|
918
1187
|
function migrateAudit(options = {}) {
|
|
919
|
-
return migrateSchema(
|
|
1188
|
+
return reldb.migrateSchema("audit", steps, options);
|
|
920
1189
|
}
|
|
921
1190
|
async function configureAuditRetention(policy) {
|
|
922
1191
|
if (!policy)
|
|
923
1192
|
return ok(void 0);
|
|
924
|
-
const result = await reldb.sql.execute("UPDATE hai_audit_policy SET enabled=?, min_days=? WHERE id=1", [
|
|
1193
|
+
const result = await reldb.sql.execute("UPDATE hai_audit_policy SET enabled=?, min_days=? WHERE id=1", [
|
|
1194
|
+
Number(policy.enabled),
|
|
1195
|
+
policy.minDays
|
|
1196
|
+
]);
|
|
925
1197
|
return result.success && result.data.changes === 1 ? ok(void 0) : err(HaiAuditError.CONFIG_ERROR, auditM("audit_retentionPolicyFailed"), result.success ? void 0 : result.error);
|
|
926
1198
|
}
|
|
927
1199
|
async function auditRetentionDays() {
|
|
@@ -956,22 +1228,134 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
956
1228
|
idColumn: "id",
|
|
957
1229
|
generateId: () => core.id.withPrefix("audit_"),
|
|
958
1230
|
fields: [
|
|
959
|
-
{
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
{
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1231
|
+
{
|
|
1232
|
+
fieldName: "id",
|
|
1233
|
+
columnName: "id",
|
|
1234
|
+
def: { type: "TEXT", primaryKey: true },
|
|
1235
|
+
select: true,
|
|
1236
|
+
create: true,
|
|
1237
|
+
update: false
|
|
1238
|
+
},
|
|
1239
|
+
{
|
|
1240
|
+
fieldName: "userId",
|
|
1241
|
+
columnName: "user_id",
|
|
1242
|
+
def: { type: "TEXT" },
|
|
1243
|
+
select: true,
|
|
1244
|
+
create: true,
|
|
1245
|
+
update: false
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
fieldName: "action",
|
|
1249
|
+
columnName: "action",
|
|
1250
|
+
def: { type: "TEXT", notNull: true },
|
|
1251
|
+
select: true,
|
|
1252
|
+
create: true,
|
|
1253
|
+
update: false
|
|
1254
|
+
},
|
|
1255
|
+
{
|
|
1256
|
+
fieldName: "resource",
|
|
1257
|
+
columnName: "resource",
|
|
1258
|
+
def: { type: "TEXT", notNull: true },
|
|
1259
|
+
select: true,
|
|
1260
|
+
create: true,
|
|
1261
|
+
update: false
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
fieldName: "resourceId",
|
|
1265
|
+
columnName: "resource_id",
|
|
1266
|
+
def: { type: "TEXT" },
|
|
1267
|
+
select: true,
|
|
1268
|
+
create: true,
|
|
1269
|
+
update: false
|
|
1270
|
+
},
|
|
1271
|
+
{
|
|
1272
|
+
fieldName: "details",
|
|
1273
|
+
columnName: "details",
|
|
1274
|
+
def: { type: "JSON" },
|
|
1275
|
+
select: true,
|
|
1276
|
+
create: true,
|
|
1277
|
+
update: false
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
fieldName: "ipAddress",
|
|
1281
|
+
columnName: "ip_address",
|
|
1282
|
+
def: { type: "TEXT" },
|
|
1283
|
+
select: true,
|
|
1284
|
+
create: true,
|
|
1285
|
+
update: false
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
fieldName: "userAgent",
|
|
1289
|
+
columnName: "user_agent",
|
|
1290
|
+
def: { type: "TEXT" },
|
|
1291
|
+
select: true,
|
|
1292
|
+
create: true,
|
|
1293
|
+
update: false
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
fieldName: "category",
|
|
1297
|
+
columnName: "category",
|
|
1298
|
+
def: { type: "TEXT" },
|
|
1299
|
+
select: true,
|
|
1300
|
+
create: true,
|
|
1301
|
+
update: false
|
|
1302
|
+
},
|
|
1303
|
+
{
|
|
1304
|
+
fieldName: "outcome",
|
|
1305
|
+
columnName: "outcome",
|
|
1306
|
+
def: { type: "TEXT" },
|
|
1307
|
+
select: true,
|
|
1308
|
+
create: true,
|
|
1309
|
+
update: false
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
fieldName: "severity",
|
|
1313
|
+
columnName: "severity",
|
|
1314
|
+
def: { type: "TEXT" },
|
|
1315
|
+
select: true,
|
|
1316
|
+
create: true,
|
|
1317
|
+
update: false
|
|
1318
|
+
},
|
|
1319
|
+
{
|
|
1320
|
+
fieldName: "requestId",
|
|
1321
|
+
columnName: "request_id",
|
|
1322
|
+
def: { type: "TEXT" },
|
|
1323
|
+
select: true,
|
|
1324
|
+
create: true,
|
|
1325
|
+
update: false
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
fieldName: "traceId",
|
|
1329
|
+
columnName: "trace_id",
|
|
1330
|
+
def: { type: "TEXT" },
|
|
1331
|
+
select: true,
|
|
1332
|
+
create: true,
|
|
1333
|
+
update: false
|
|
1334
|
+
},
|
|
1335
|
+
{
|
|
1336
|
+
fieldName: "scopeId",
|
|
1337
|
+
columnName: "scope_id",
|
|
1338
|
+
def: { type: "TEXT" },
|
|
1339
|
+
select: true,
|
|
1340
|
+
create: true,
|
|
1341
|
+
update: false
|
|
1342
|
+
},
|
|
1343
|
+
{
|
|
1344
|
+
fieldName: "actorSnapshot",
|
|
1345
|
+
columnName: "actor_snapshot",
|
|
1346
|
+
def: { type: "JSON" },
|
|
1347
|
+
select: true,
|
|
1348
|
+
create: true,
|
|
1349
|
+
update: false
|
|
1350
|
+
},
|
|
1351
|
+
{
|
|
1352
|
+
fieldName: "createdAt",
|
|
1353
|
+
columnName: "created_at",
|
|
1354
|
+
def: { type: "TIMESTAMP" },
|
|
1355
|
+
select: true,
|
|
1356
|
+
create: true,
|
|
1357
|
+
update: false
|
|
1358
|
+
}
|
|
975
1359
|
]
|
|
976
1360
|
});
|
|
977
1361
|
this.repoConfig = config;
|
|
@@ -1006,12 +1390,25 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1006
1390
|
logger4.warn("Audit actor name unavailable", { userId, error: result.error });
|
|
1007
1391
|
return { id: userId };
|
|
1008
1392
|
}
|
|
1009
|
-
return {
|
|
1393
|
+
return {
|
|
1394
|
+
id: userId,
|
|
1395
|
+
...result.data?.username ? { username: result.data.username } : {}
|
|
1396
|
+
};
|
|
1010
1397
|
}
|
|
1011
1398
|
filters(options) {
|
|
1012
1399
|
const conditions = [];
|
|
1013
1400
|
const params = [];
|
|
1014
|
-
for (const [field, column] of [
|
|
1401
|
+
for (const [field, column] of [
|
|
1402
|
+
["userId", "user_id"],
|
|
1403
|
+
["action", "action"],
|
|
1404
|
+
["resource", "resource"],
|
|
1405
|
+
["category", "category"],
|
|
1406
|
+
["outcome", "outcome"],
|
|
1407
|
+
["severity", "severity"],
|
|
1408
|
+
["scopeId", "scope_id"],
|
|
1409
|
+
["requestId", "request_id"],
|
|
1410
|
+
["traceId", "trace_id"]
|
|
1411
|
+
]) {
|
|
1015
1412
|
if (options[field] !== void 0) {
|
|
1016
1413
|
conditions.push(`a.${column} = ?`);
|
|
1017
1414
|
params.push(options[field]);
|
|
@@ -1047,7 +1444,15 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1047
1444
|
if (!time.success)
|
|
1048
1445
|
return time;
|
|
1049
1446
|
const now = time.data;
|
|
1050
|
-
const context = {
|
|
1447
|
+
const context = {
|
|
1448
|
+
category: input.category ?? null,
|
|
1449
|
+
outcome: input.outcome ?? null,
|
|
1450
|
+
severity: input.severity ?? null,
|
|
1451
|
+
requestId: input.requestId ?? null,
|
|
1452
|
+
traceId: input.traceId ?? null,
|
|
1453
|
+
scopeId: input.scopeId ?? null,
|
|
1454
|
+
actorSnapshot: input.actorSnapshot ?? null
|
|
1455
|
+
};
|
|
1051
1456
|
const data = {
|
|
1052
1457
|
...context,
|
|
1053
1458
|
id,
|
|
@@ -1066,7 +1471,9 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1066
1471
|
logger4.error("Failed to record audit log", { action: input.action, error: createResult.error.message });
|
|
1067
1472
|
return err(
|
|
1068
1473
|
HaiAuditError.LOG_FAILED,
|
|
1069
|
-
auditM("audit_logFailed", {
|
|
1474
|
+
auditM("audit_logFailed", {
|
|
1475
|
+
params: { error: createResult.error.message }
|
|
1476
|
+
}),
|
|
1070
1477
|
createResult.error
|
|
1071
1478
|
);
|
|
1072
1479
|
}
|
|
@@ -1088,7 +1495,11 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1088
1495
|
logger4.error("Failed to record audit log", { action: input.action, error });
|
|
1089
1496
|
return err(
|
|
1090
1497
|
HaiAuditError.LOG_FAILED,
|
|
1091
|
-
auditM("audit_logFailed", {
|
|
1498
|
+
auditM("audit_logFailed", {
|
|
1499
|
+
params: {
|
|
1500
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1501
|
+
}
|
|
1502
|
+
}),
|
|
1092
1503
|
error
|
|
1093
1504
|
);
|
|
1094
1505
|
}
|
|
@@ -1108,11 +1519,24 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1108
1519
|
const { userTable, userIdColumn, userNameColumn } = this.repoConfig;
|
|
1109
1520
|
const quote = this.db.config?.type === "mysql" ? "`" : '"';
|
|
1110
1521
|
const identifier = (name) => `${quote}${name.replaceAll(quote, quote + quote)}${quote}`;
|
|
1111
|
-
const sortColumns = {
|
|
1522
|
+
const sortColumns = {
|
|
1523
|
+
createdAt: "created_at",
|
|
1524
|
+
action: "action",
|
|
1525
|
+
resource: "resource",
|
|
1526
|
+
category: "category",
|
|
1527
|
+
outcome: "outcome",
|
|
1528
|
+
severity: "severity"
|
|
1529
|
+
};
|
|
1112
1530
|
const sortBy = options.sortBy ?? "createdAt";
|
|
1113
1531
|
const direction = options.sortDirection ?? "desc";
|
|
1114
|
-
if (!Object.hasOwn(sortColumns, sortBy) || !["asc", "desc"].includes(direction))
|
|
1115
|
-
return err(HaiAuditError.QUERY_FAILED, auditM("audit_invalidInput", {
|
|
1532
|
+
if (!Object.hasOwn(sortColumns, sortBy) || !["asc", "desc"].includes(direction)) {
|
|
1533
|
+
return err(HaiAuditError.QUERY_FAILED, auditM("audit_invalidInput", {
|
|
1534
|
+
params: {
|
|
1535
|
+
field: "sort",
|
|
1536
|
+
reason: auditM("audit_sortInvalid")
|
|
1537
|
+
}
|
|
1538
|
+
}));
|
|
1539
|
+
}
|
|
1116
1540
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1117
1541
|
try {
|
|
1118
1542
|
const result = await this.sql().queryPage({
|
|
@@ -1132,16 +1556,28 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1132
1556
|
logger4.error("Failed to query audit logs", { error: result.error.message });
|
|
1133
1557
|
return err(
|
|
1134
1558
|
HaiAuditError.QUERY_FAILED,
|
|
1135
|
-
auditM("audit_queryFailed", {
|
|
1559
|
+
auditM("audit_queryFailed", {
|
|
1560
|
+
params: { error: result.error.message }
|
|
1561
|
+
}),
|
|
1136
1562
|
result.error
|
|
1137
1563
|
);
|
|
1138
1564
|
}
|
|
1139
|
-
return ok({
|
|
1565
|
+
return ok({
|
|
1566
|
+
...result.data,
|
|
1567
|
+
items: result.data.items.map((row) => ({
|
|
1568
|
+
...normalizeAuditLog(row),
|
|
1569
|
+
username: readAuditActor(row.actorSnapshot)?.username ?? (row.actorSnapshot == null ? row.username ?? null : null)
|
|
1570
|
+
}))
|
|
1571
|
+
});
|
|
1140
1572
|
} catch (error) {
|
|
1141
1573
|
logger4.error("Failed to query audit logs", { error });
|
|
1142
1574
|
return err(
|
|
1143
1575
|
HaiAuditError.QUERY_FAILED,
|
|
1144
|
-
auditM("audit_queryFailed", {
|
|
1576
|
+
auditM("audit_queryFailed", {
|
|
1577
|
+
params: {
|
|
1578
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1579
|
+
}
|
|
1580
|
+
}),
|
|
1145
1581
|
error
|
|
1146
1582
|
);
|
|
1147
1583
|
}
|
|
@@ -1171,7 +1607,9 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1171
1607
|
["actor_snapshot", "actorSnapshot"]
|
|
1172
1608
|
].map(([column, alias]) => `${column} AS ${quote}${alias}${quote}`).join(", ");
|
|
1173
1609
|
const conditions = ["created_at <= ?"];
|
|
1174
|
-
const params = [
|
|
1610
|
+
const params = [
|
|
1611
|
+
this.toDateParam(options.cutoff)
|
|
1612
|
+
];
|
|
1175
1613
|
if (options.from) {
|
|
1176
1614
|
conditions.push("created_at >= ?");
|
|
1177
1615
|
params.push(this.toDateParam(options.from));
|
|
@@ -1209,7 +1647,9 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1209
1647
|
if (!result.success) {
|
|
1210
1648
|
return err(
|
|
1211
1649
|
HaiAuditError.QUERY_FAILED,
|
|
1212
|
-
auditM("audit_queryFailed", {
|
|
1650
|
+
auditM("audit_queryFailed", {
|
|
1651
|
+
params: { error: result.error.message }
|
|
1652
|
+
}),
|
|
1213
1653
|
result.error
|
|
1214
1654
|
);
|
|
1215
1655
|
}
|
|
@@ -1217,7 +1657,11 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1217
1657
|
} catch (error) {
|
|
1218
1658
|
return err(
|
|
1219
1659
|
HaiAuditError.QUERY_FAILED,
|
|
1220
|
-
auditM("audit_queryFailed", {
|
|
1660
|
+
auditM("audit_queryFailed", {
|
|
1661
|
+
params: {
|
|
1662
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1663
|
+
}
|
|
1664
|
+
}),
|
|
1221
1665
|
error
|
|
1222
1666
|
);
|
|
1223
1667
|
}
|
|
@@ -1242,13 +1686,17 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1242
1686
|
try {
|
|
1243
1687
|
const result = await this.sql().execute(
|
|
1244
1688
|
`DELETE FROM ${AUDIT_TABLE} WHERE created_at < ?`,
|
|
1245
|
-
[
|
|
1689
|
+
[
|
|
1690
|
+
this.toDateParam(cutoff)
|
|
1691
|
+
]
|
|
1246
1692
|
);
|
|
1247
1693
|
if (!result.success) {
|
|
1248
1694
|
logger4.error("Failed to cleanup audit logs", { error: result.error.message });
|
|
1249
1695
|
return err(
|
|
1250
1696
|
HaiAuditError.CLEANUP_FAILED,
|
|
1251
|
-
auditM("audit_cleanupFailed", {
|
|
1697
|
+
auditM("audit_cleanupFailed", {
|
|
1698
|
+
params: { error: result.error.message }
|
|
1699
|
+
}),
|
|
1252
1700
|
result.error
|
|
1253
1701
|
);
|
|
1254
1702
|
}
|
|
@@ -1262,7 +1710,11 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1262
1710
|
logger4.error("Failed to cleanup audit logs", { error });
|
|
1263
1711
|
return err(
|
|
1264
1712
|
HaiAuditError.CLEANUP_FAILED,
|
|
1265
|
-
auditM("audit_cleanupFailed", {
|
|
1713
|
+
auditM("audit_cleanupFailed", {
|
|
1714
|
+
params: {
|
|
1715
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1716
|
+
}
|
|
1717
|
+
}),
|
|
1266
1718
|
error
|
|
1267
1719
|
);
|
|
1268
1720
|
}
|
|
@@ -1294,11 +1746,16 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1294
1746
|
logger4.error("Failed to query audit statistics", { error: result.error.message });
|
|
1295
1747
|
return err(
|
|
1296
1748
|
HaiAuditError.STATS_FAILED,
|
|
1297
|
-
auditM("audit_statsFailed", {
|
|
1749
|
+
auditM("audit_statsFailed", {
|
|
1750
|
+
params: { error: result.error.message }
|
|
1751
|
+
}),
|
|
1298
1752
|
result.error
|
|
1299
1753
|
);
|
|
1300
1754
|
}
|
|
1301
|
-
const statistics = result.data.map((item) => ({
|
|
1755
|
+
const statistics = result.data.map((item) => ({
|
|
1756
|
+
action: item.action,
|
|
1757
|
+
count: Number(item.count)
|
|
1758
|
+
}));
|
|
1302
1759
|
if (statistics.some((item) => !Number.isSafeInteger(item.count) || item.count < 0))
|
|
1303
1760
|
return err(HaiAuditError.STATS_FAILED, auditM("audit_invalidStatCount"));
|
|
1304
1761
|
return ok(statistics);
|
|
@@ -1306,7 +1763,11 @@ var AuditLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1306
1763
|
logger4.error("Failed to query audit statistics", { error });
|
|
1307
1764
|
return err(
|
|
1308
1765
|
HaiAuditError.STATS_FAILED,
|
|
1309
|
-
auditM("audit_statsFailed", {
|
|
1766
|
+
auditM("audit_statsFailed", {
|
|
1767
|
+
params: {
|
|
1768
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1769
|
+
}
|
|
1770
|
+
}),
|
|
1310
1771
|
error
|
|
1311
1772
|
);
|
|
1312
1773
|
}
|
|
@@ -1384,7 +1845,9 @@ var audit = {
|
|
|
1384
1845
|
logger5.error("Audit config validation failed", { error: parseResult.error.message });
|
|
1385
1846
|
return err(
|
|
1386
1847
|
HaiAuditError.CONFIG_ERROR,
|
|
1387
|
-
auditM("audit_configError", {
|
|
1848
|
+
auditM("audit_configError", {
|
|
1849
|
+
params: { error: parseResult.error.message }
|
|
1850
|
+
}),
|
|
1388
1851
|
parseResult.error
|
|
1389
1852
|
);
|
|
1390
1853
|
}
|
|
@@ -1400,7 +1863,9 @@ var audit = {
|
|
|
1400
1863
|
logger5.error("Audit config contains invalid identifiers", { error: identifierResult.error.message });
|
|
1401
1864
|
return err(
|
|
1402
1865
|
HaiAuditError.CONFIG_ERROR,
|
|
1403
|
-
auditM("audit_configError", {
|
|
1866
|
+
auditM("audit_configError", {
|
|
1867
|
+
params: { error: identifierResult.error.message }
|
|
1868
|
+
}),
|
|
1404
1869
|
identifierResult.error
|
|
1405
1870
|
);
|
|
1406
1871
|
}
|
|
@@ -1414,7 +1879,9 @@ var audit = {
|
|
|
1414
1879
|
});
|
|
1415
1880
|
const ready = await candidate.ready();
|
|
1416
1881
|
if (!ready.success) {
|
|
1417
|
-
return err(HaiAuditError.CONFIG_ERROR, auditM("audit_initFailed", {
|
|
1882
|
+
return err(HaiAuditError.CONFIG_ERROR, auditM("audit_initFailed", {
|
|
1883
|
+
params: { error: ready.error.message }
|
|
1884
|
+
}), ready.error);
|
|
1418
1885
|
}
|
|
1419
1886
|
const protection = await migrateAudit();
|
|
1420
1887
|
if (!protection.success)
|
|
@@ -1428,7 +1895,12 @@ var audit = {
|
|
|
1428
1895
|
currentWriter = createAuditWriter(candidate, parsed);
|
|
1429
1896
|
capturePolicy = parsed.capture;
|
|
1430
1897
|
currentHelper = createHelper((input) => audit.log(input));
|
|
1431
|
-
currentBackup = createAuditBackup({
|
|
1898
|
+
currentBackup = createAuditBackup({
|
|
1899
|
+
repository: candidate,
|
|
1900
|
+
config: parsed.backup,
|
|
1901
|
+
storage: config?.storage,
|
|
1902
|
+
capture: (input, options) => audit.capture(input, options)
|
|
1903
|
+
});
|
|
1432
1904
|
currentAlerts = createAuditAlerts({ config: parsed.alerts, reach: config?.reach, log: currentWriter });
|
|
1433
1905
|
logger5.info("Audit module initialized");
|
|
1434
1906
|
return ok(void 0);
|
|
@@ -1436,7 +1908,11 @@ var audit = {
|
|
|
1436
1908
|
logger5.error("Audit module initialization failed", { error });
|
|
1437
1909
|
return err(
|
|
1438
1910
|
HaiAuditError.CONFIG_ERROR,
|
|
1439
|
-
auditM("audit_initFailed", {
|
|
1911
|
+
auditM("audit_initFailed", {
|
|
1912
|
+
params: {
|
|
1913
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1914
|
+
}
|
|
1915
|
+
}),
|
|
1440
1916
|
error
|
|
1441
1917
|
);
|
|
1442
1918
|
} finally {
|
|
@@ -1480,12 +1956,20 @@ var audit = {
|
|
|
1480
1956
|
if (!currentRepo) {
|
|
1481
1957
|
return Promise.resolve(notInitialized.result());
|
|
1482
1958
|
}
|
|
1483
|
-
if (!validAuditQuery(options))
|
|
1484
|
-
return Promise.resolve(err(HaiAuditError.QUERY_FAILED, auditM("audit_invalidInput", {
|
|
1959
|
+
if (!validAuditQuery(options)) {
|
|
1960
|
+
return Promise.resolve(err(HaiAuditError.QUERY_FAILED, auditM("audit_invalidInput", {
|
|
1961
|
+
params: {
|
|
1962
|
+
field: "query",
|
|
1963
|
+
reason: auditM("audit_queryInvalid")
|
|
1964
|
+
}
|
|
1965
|
+
})));
|
|
1966
|
+
}
|
|
1485
1967
|
if (options?.startDate && options?.endDate && options.startDate > options.endDate) {
|
|
1486
1968
|
return Promise.resolve(err(
|
|
1487
1969
|
HaiAuditError.QUERY_FAILED,
|
|
1488
|
-
auditM("audit_invalidInput", {
|
|
1970
|
+
auditM("audit_invalidInput", {
|
|
1971
|
+
params: { field: "dateRange", reason: "startDate must be before endDate" }
|
|
1972
|
+
})
|
|
1489
1973
|
));
|
|
1490
1974
|
}
|
|
1491
1975
|
return currentRepo.listWithUser(options);
|
|
@@ -1504,13 +1988,17 @@ var audit = {
|
|
|
1504
1988
|
if (!isNonEmptyString(userId)) {
|
|
1505
1989
|
return Promise.resolve(err(
|
|
1506
1990
|
HaiAuditError.QUERY_FAILED,
|
|
1507
|
-
auditM("audit_invalidInput", {
|
|
1991
|
+
auditM("audit_invalidInput", {
|
|
1992
|
+
params: { field: "userId", reason: "must be non-empty string" }
|
|
1993
|
+
})
|
|
1508
1994
|
));
|
|
1509
1995
|
}
|
|
1510
1996
|
if (typeof limit === "number" && (!Number.isInteger(limit) || limit <= 0)) {
|
|
1511
1997
|
return Promise.resolve(err(
|
|
1512
1998
|
HaiAuditError.QUERY_FAILED,
|
|
1513
|
-
auditM("audit_invalidInput", {
|
|
1999
|
+
auditM("audit_invalidInput", {
|
|
2000
|
+
params: { field: "limit", reason: "must be a positive integer" }
|
|
2001
|
+
})
|
|
1514
2002
|
));
|
|
1515
2003
|
}
|
|
1516
2004
|
return currentRepo.getUserRecent(userId, limit);
|
|
@@ -1528,7 +2016,9 @@ var audit = {
|
|
|
1528
2016
|
if (typeof olderThanDays === "number" && (!Number.isInteger(olderThanDays) || olderThanDays < 0)) {
|
|
1529
2017
|
return Promise.resolve(err(
|
|
1530
2018
|
HaiAuditError.CLEANUP_FAILED,
|
|
1531
|
-
auditM("audit_invalidInput", {
|
|
2019
|
+
auditM("audit_invalidInput", {
|
|
2020
|
+
params: { field: "olderThanDays", reason: "must be a non-negative integer" }
|
|
2021
|
+
})
|
|
1532
2022
|
));
|
|
1533
2023
|
}
|
|
1534
2024
|
return currentRepo.cleanupOld(olderThanDays);
|
|
@@ -1543,12 +2033,20 @@ var audit = {
|
|
|
1543
2033
|
if (!currentRepo) {
|
|
1544
2034
|
return Promise.resolve(notInitialized.result());
|
|
1545
2035
|
}
|
|
1546
|
-
if (!validAuditQuery(filters))
|
|
1547
|
-
return Promise.resolve(err(HaiAuditError.STATS_FAILED, auditM("audit_invalidInput", {
|
|
2036
|
+
if (!validAuditQuery(filters)) {
|
|
2037
|
+
return Promise.resolve(err(HaiAuditError.STATS_FAILED, auditM("audit_invalidInput", {
|
|
2038
|
+
params: {
|
|
2039
|
+
field: "query",
|
|
2040
|
+
reason: auditM("audit_queryInvalid")
|
|
2041
|
+
}
|
|
2042
|
+
})));
|
|
2043
|
+
}
|
|
1548
2044
|
if (typeof days === "number" && (!Number.isInteger(days) || days < 0)) {
|
|
1549
2045
|
return Promise.resolve(err(
|
|
1550
2046
|
HaiAuditError.STATS_FAILED,
|
|
1551
|
-
auditM("audit_invalidInput", {
|
|
2047
|
+
auditM("audit_invalidInput", {
|
|
2048
|
+
params: { field: "days", reason: "must be a non-negative integer" }
|
|
2049
|
+
})
|
|
1552
2050
|
));
|
|
1553
2051
|
}
|
|
1554
2052
|
return currentRepo.getStats(days, filters);
|
|
@@ -1576,14 +2074,19 @@ var audit = {
|
|
|
1576
2074
|
if (!backup && !alerts)
|
|
1577
2075
|
return null;
|
|
1578
2076
|
return {
|
|
1579
|
-
tasks: [
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2077
|
+
tasks: [
|
|
2078
|
+
...backup?.tasks ?? [],
|
|
2079
|
+
...alerts?.tasks ?? []
|
|
2080
|
+
].map((task) => ({ ...task })),
|
|
2081
|
+
hooks: {
|
|
2082
|
+
onTaskExecute: async (input) => {
|
|
2083
|
+
if (input.context.taskId === "hai-audit-backup" && backup)
|
|
2084
|
+
return backup.hooks.onTaskExecute(input);
|
|
2085
|
+
if (input.context.taskId === "hai-audit-alerts" && alerts)
|
|
2086
|
+
return alerts.hooks.onTaskExecute(input);
|
|
2087
|
+
throw core.error.buildHaiErrorInst(HaiAuditError.CONFIG_ERROR, auditM("audit_unknownTask"));
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
1587
2090
|
};
|
|
1588
2091
|
},
|
|
1589
2092
|
/**
|