Files
Termix/src/backend/utils/audit-logger.ts
T
a89ccc6b07 v26.10.3 (#1692)
* feat: shell controller, TabStage, layout and theme plugins with repair view

* feat: move the classic layout into its own bundled plugin

* feat: move color themes into a bundled themes plugin

* fix: plugin tests and type checks work with a linked sdk

* fix: home tab, layout switch and missing layout fallbacks for looks

* chore: add classic layout and themes to the readme

* chore: drop example plugin links from the plugin guide

* feat: plugin filters by type, paged browse, hide plugins needing newer termix

* feat: move desktop, status, import, audit and branding features into plugins

* feat: host lock api so a plugin can load hosts from a config file (#1581)

* chore: add hosts config to the plugin table

* feat: login policy hook for plugins and optional https redirect (#1584)

* chore: add login security to the plugin table

* feat: api key socket tickets so a master can open remote sessions (#1654)

* feat: sync moves to a plugin, desktop gets multiple accounts (#1580, #1654)

* chore: add sync to the plugin table

* feat: move the sync screens and wizard out of core into the plugin (#1580)

* fix: add a neutral linked desktop hook for plugins (#1580)

* fix: show the linked account's name and admin flag in the app (#1580)

* feat: ssh transport providers, core ssh relay and plugin folders api

* feat: support FIDO2 security keys through ssh agent

* feat: add companion panels beside host tabs

* feat: mTLS client certificates for desktop server links

* feat: native web view for plugins in the desktop app

* fix: detect the desktop link window by Electron's default user agent

Fixes #1675

* fix: changelog for desktop link fix

* fix: keep text primary keys in the plugin data export

* fix: ship sharp binaries for Windows arm64 builds

* feat: client cert on the sync link types (sdk 1.0.18)

* chore: update translations

* feat: themed title bar for the desktop app (#1690)

* chore: changelog for the themed title bar

* fix: draw the title bar in the sidebar color

* feat: per host layout for the terminal panel bar

* chore: sdk 1.0.19 for the panel bar layout

* chore: bundle all 43 plugins for 26.10.3

* chore: update translations

---------

Co-authored-by: SirHephaistos <60616553+SirHephaistos@users.noreply.github.com>
Co-authored-by: Giovanny Aranda <garanda21@gmail.com>
2026-10-20 16:03:29 -05:00

111 lines
3.3 KiB
TypeScript

import type { Request } from "express";
import { emitAuditEntry } from "./audit-hooks.js";
import {
createCurrentAuditLogRepository,
createCurrentUserRepository,
} from "../database/repositories/factory.js";
import { getClientIp } from "./request-origin.js";
/**
* Resolves the display name to store alongside the entry. It is denormalised on
* purpose: the record has to stay readable after the account is gone.
*/
export async function getAuditUsername(userId: string): Promise<string> {
try {
const actor = await createCurrentUserRepository().findById(userId);
return actor?.username ?? userId;
} catch {
return userId;
}
}
export interface AuditLogParams {
// Null when no user acted (a plugin starting up, background work): the
// column references users.id, so anything else would be refused.
userId: string | null;
username: string;
action: string;
resourceType: string;
resourceId?: string;
resourceName?: string;
details?: string;
ipAddress?: string;
userAgent?: string;
success: boolean;
errorMessage?: string;
}
// Plugin routine calls (a headless viewer's heartbeat through ctx.asUser and a
// service) repeat identically every few seconds; written each time they push
// real security entries out of the capped log. Only successful plugin_as_user
// and plugin_service_* lines are coalesced; failures and everything else are
// always written.
const ROUTINE_COALESCE_MS = 10 * 60 * 1000;
const ROUTINE_COALESCE_MAX_KEYS = 5000;
const routineLastWritten = new Map<string, number>();
function isCoalescedRoutine(params: AuditLogParams): boolean {
if (
!params.success ||
!params.username.startsWith("plugin:") ||
!(
params.action === "plugin_as_user" ||
params.action.startsWith("plugin_service_")
)
) {
return false;
}
const key = JSON.stringify([
params.userId,
params.username,
params.action,
params.resourceType,
params.resourceId,
params.details,
]);
const now = Date.now();
const last = routineLastWritten.get(key);
if (last !== undefined && now - last < ROUTINE_COALESCE_MS) return true;
if (routineLastWritten.size >= ROUTINE_COALESCE_MAX_KEYS) {
for (const [k, at] of routineLastWritten) {
if (now - at >= ROUTINE_COALESCE_MS) routineLastWritten.delete(k);
}
}
routineLastWritten.set(key, now);
return false;
}
export async function logAudit(params: AuditLogParams): Promise<void> {
if (isCoalescedRoutine(params)) return;
// Hooks (plugins copying the log elsewhere) never delay or fail the
// audited operation.
emitAuditEntry(params);
try {
await createCurrentAuditLogRepository().create({
userId: params.userId,
username: params.username,
action: params.action,
resourceType: params.resourceType,
resourceId: params.resourceId ?? null,
resourceName: params.resourceName ?? null,
details: params.details ?? null,
ipAddress: params.ipAddress ?? null,
userAgent: params.userAgent ?? null,
success: params.success,
errorMessage: params.errorMessage ?? null,
});
} catch {
// audit logging must never throw and break the caller
}
}
export function getRequestMeta(req: Request): {
ipAddress: string;
userAgent: string;
} {
const userAgent = (req.headers["user-agent"] as string) || "";
return { ipAddress: getClientIp(req), userAgent };
}