Stand vor Einführung des Nacht-Agenten
This commit is contained in:
commit
4763548bfb
168 changed files with 12726 additions and 0 deletions
81
packages/platform/src/audit.ts
Normal file
81
packages/platform/src/audit.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import type { PoolConnection } from 'mysql2/promise';
|
||||
import { pool, one, query } from './db.js';
|
||||
|
||||
const SENSITIVE = /pass|secret|token|hash|code|key|totp/i;
|
||||
/** Maskiert Geheimnisse rekursiv, bevor Zustände in das Audit-Protokoll gelangen. */
|
||||
export function mask(v: unknown): unknown {
|
||||
if (Array.isArray(v)) return v.map(mask);
|
||||
if (v && typeof v === 'object') {
|
||||
return Object.fromEntries(Object.entries(v as Record<string, unknown>).map(([k, val]) => [k, SENSITIVE.test(k) ? '***' : mask(val)]));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
export interface AuditInput {
|
||||
actorType: 'user' | 'system' | 'anonymous';
|
||||
actorId?: string | null;
|
||||
orgId?: string | null;
|
||||
action: string;
|
||||
resourceType?: string;
|
||||
resourceId?: string;
|
||||
result?: 'success' | 'denied' | 'failure';
|
||||
errorClass?: string;
|
||||
connector?: string;
|
||||
correlationId?: string;
|
||||
ip?: string;
|
||||
before?: unknown;
|
||||
after?: unknown;
|
||||
}
|
||||
|
||||
const canon = (o: unknown) => JSON.stringify(o);
|
||||
|
||||
/** Hängt ein Ereignis an die Hash-Kette an. Serialisiert über Zeilensperre auf dem letzten Eintrag. */
|
||||
export async function audit(e: AuditInput, conn?: PoolConnection): Promise<void> {
|
||||
const own = !conn;
|
||||
const c = conn ?? (await pool.getConnection());
|
||||
try {
|
||||
if (own) await c.beginTransaction();
|
||||
await c.query('SELECT GET_LOCK(?, 10)', ['kc_audit_chain']);
|
||||
const last = await one<{ hash: string } & import('mysql2').RowDataPacket>('SELECT hash FROM audit_events ORDER BY id DESC LIMIT 1', [], c);
|
||||
const prev = last?.hash ?? '0'.repeat(64);
|
||||
const ts = new Date();
|
||||
const body = {
|
||||
ts: ts.toISOString(), actor_type: e.actorType, actor_id: e.actorId ?? null, org_id: e.orgId ?? null, action: e.action,
|
||||
resource_type: e.resourceType ?? null, resource_id: e.resourceId ?? null, result: e.result ?? 'success',
|
||||
error_class: e.errorClass ?? null, correlation_id: e.correlationId ?? null,
|
||||
before: e.before === undefined ? null : mask(e.before), after: e.after === undefined ? null : mask(e.after),
|
||||
};
|
||||
const hash = createHash('sha256').update(prev + canon(body)).digest('hex');
|
||||
await c.execute(
|
||||
`INSERT INTO audit_events (ts, actor_type, actor_id, org_id, action, resource_type, resource_id, result, error_class, connector, correlation_id, ip, before_json, after_json, prev_hash, hash)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
[ts, e.actorType, body.actor_id, body.org_id, e.action, body.resource_type, body.resource_id, body.result, body.error_class, e.connector ?? null,
|
||||
body.correlation_id, e.ip ?? null, body.before === null ? null : JSON.stringify(body.before), body.after === null ? null : JSON.stringify(body.after), prev, hash],
|
||||
);
|
||||
if (own) await c.commit();
|
||||
} catch (err) {
|
||||
if (own) await c.rollback();
|
||||
throw err;
|
||||
} finally {
|
||||
await c.query('SELECT RELEASE_LOCK(?)', ['kc_audit_chain']).catch(() => undefined);
|
||||
if (own) c.release();
|
||||
}
|
||||
}
|
||||
|
||||
/** Prüft die Hash-Kette. Liefert die erste fehlerhafte ID oder null. */
|
||||
export async function verifyAuditChain(q: (sql: string) => Promise<any[]> = (sql) => query(sql)): Promise<{ checked: number; brokenAt: number | null }> {
|
||||
const rows = await q('SELECT id, ts, actor_type, actor_id, org_id, action, resource_type, resource_id, result, error_class, correlation_id, before_json, after_json, prev_hash, hash FROM audit_events ORDER BY id');
|
||||
let prev = '0'.repeat(64);
|
||||
for (const r of rows) {
|
||||
const body = {
|
||||
ts: (r.ts as Date).toISOString(), actor_type: r.actor_type, actor_id: r.actor_id, org_id: r.org_id, action: r.action,
|
||||
resource_type: r.resource_type, resource_id: r.resource_id, result: r.result, error_class: r.error_class, correlation_id: r.correlation_id,
|
||||
before: r.before_json ?? null, after: r.after_json ?? null,
|
||||
};
|
||||
const expect = createHash('sha256').update(prev + canon(body)).digest('hex');
|
||||
if (r.prev_hash !== prev || r.hash !== expect) return { checked: rows.length, brokenAt: r.id as number };
|
||||
prev = r.hash as string;
|
||||
}
|
||||
return { checked: rows.length, brokenAt: null };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue