mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-11-04 17:45:24 +00:00
* 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>
111 lines
3.3 KiB
TypeScript
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 };
|
|
}
|