/* * This file is part of a proprietary work. * * Copyright (c) 2025 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. * You may not use this file except in compliance with the License. * Unauthorized use, copying, modification, or distribution is strictly prohibited. * * This file is not licensed under the AGPLv3. */ import { connectionAuditLog, logsDb, siteResources, sites, clients, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; import { eq, gt, lt, and, count, desc, inArray } from "drizzle-orm"; import { OpenAPITags } from "@server/openApi"; import { z } from "zod"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { fromError } from "zod-validation-error"; import { QueryConnectionAuditLogResponse } from "@server/routers/auditLogs/types"; import response from "@server/lib/response"; import logger from "@server/logger"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; export const queryConnectionAuditLogsQuery = z.object({ // iso string just validate its a parseable date timeStart: z .string() .refine((val) => !isNaN(Date.parse(val)), { error: "timeStart must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .prefault(() => getSevenDaysAgo().toISOString()) .openapi({ type: "string", format: "date-time", description: "Start time as ISO date string (defaults to 7 days ago)" }), timeEnd: z .string() .refine((val) => !isNaN(Date.parse(val)), { error: "timeEnd must be a valid ISO date string" }) .transform((val) => Math.floor(new Date(val).getTime() / 1000)) .optional() .prefault(() => new Date().toISOString()) .openapi({ type: "string", format: "date-time", description: "End time as ISO date string (defaults to current time)" }), protocol: z.string().optional(), sourceAddr: z.string().optional(), destAddr: z.string().optional(), clientId: z .string() .optional() .transform(Number) .pipe(z.int().positive()) .optional(), siteId: z .string() .optional() .transform(Number) .pipe(z.int().positive()) .optional(), siteResourceId: z .string() .optional() .transform(Number) .pipe(z.int().positive()) .optional(), userId: z.string().optional(), limit: z .string() .optional() .default("1000") .transform(Number) .pipe(z.int().positive()), offset: z .string() .optional() .default("0") .transform(Number) .pipe(z.int().nonnegative()) }); export const queryConnectionAuditLogsParams = z.object({ orgId: z.string() }); export const queryConnectionAuditLogsCombined = queryConnectionAuditLogsQuery.merge(queryConnectionAuditLogsParams); type Q = z.infer; function getWhere(data: Q) { return and( gt(connectionAuditLog.startedAt, data.timeStart), lt(connectionAuditLog.startedAt, data.timeEnd), eq(connectionAuditLog.orgId, data.orgId), data.protocol ? eq(connectionAuditLog.protocol, data.protocol) : undefined, data.sourceAddr ? eq(connectionAuditLog.sourceAddr, data.sourceAddr) : undefined, data.destAddr ? eq(connectionAuditLog.destAddr, data.destAddr) : undefined, data.clientId ? eq(connectionAuditLog.clientId, data.clientId) : undefined, data.siteId ? eq(connectionAuditLog.siteId, data.siteId) : undefined, data.siteResourceId ? eq(connectionAuditLog.siteResourceId, data.siteResourceId) : undefined, data.userId ? eq(connectionAuditLog.userId, data.userId) : undefined ); } export function queryConnection(data: Q) { return logsDb .select({ sessionId: connectionAuditLog.sessionId, siteResourceId: connectionAuditLog.siteResourceId, orgId: connectionAuditLog.orgId, siteId: connectionAuditLog.siteId, clientId: connectionAuditLog.clientId, userId: connectionAuditLog.userId, sourceAddr: connectionAuditLog.sourceAddr, destAddr: connectionAuditLog.destAddr, protocol: connectionAuditLog.protocol, startedAt: connectionAuditLog.startedAt, endedAt: connectionAuditLog.endedAt, bytesTx: connectionAuditLog.bytesTx, bytesRx: connectionAuditLog.bytesRx }) .from(connectionAuditLog) .where(getWhere(data)) .orderBy( desc(connectionAuditLog.startedAt), desc(connectionAuditLog.id) ); } export function countConnectionQuery(data: Q) { const countQuery = logsDb .select({ count: count() }) .from(connectionAuditLog) .where(getWhere(data)); return countQuery; } async function enrichWithDetails( logs: Awaited> ) { // Collect unique IDs from logs const siteResourceIds = [ ...new Set( logs .map((log) => log.siteResourceId) .filter((id): id is number => id !== null && id !== undefined) ) ]; const siteIds = [ ...new Set( logs .map((log) => log.siteId) .filter((id): id is number => id !== null && id !== undefined) ) ]; const clientIds = [ ...new Set( logs .map((log) => log.clientId) .filter((id): id is number => id !== null && id !== undefined) ) ]; // Fetch resource details from main database const resourceMap = new Map< number, { name: string; niceId: string } >(); if (siteResourceIds.length > 0) { const resourceDetails = await primaryDb .select({ siteResourceId: siteResources.siteResourceId, name: siteResources.name, niceId: siteResources.niceId }) .from(siteResources) .where(inArray(siteResources.siteResourceId, siteResourceIds)); for (const r of resourceDetails) { resourceMap.set(r.siteResourceId, { name: r.name, niceId: r.niceId }); } } // Fetch site details from main database const siteMap = new Map(); if (siteIds.length > 0) { const siteDetails = await primaryDb .select({ siteId: sites.siteId, name: sites.name, niceId: sites.niceId }) .from(sites) .where(inArray(sites.siteId, siteIds)); for (const s of siteDetails) { siteMap.set(s.siteId, { name: s.name, niceId: s.niceId }); } } // Fetch client details from main database const clientMap = new Map(); if (clientIds.length > 0) { const clientDetails = await primaryDb .select({ clientId: clients.clientId, name: clients.name }) .from(clients) .where(inArray(clients.clientId, clientIds)); for (const c of clientDetails) { clientMap.set(c.clientId, { name: c.name }); } } // Enrich logs with details return logs.map((log) => ({ ...log, resourceName: log.siteResourceId ? resourceMap.get(log.siteResourceId)?.name ?? null : null, resourceNiceId: log.siteResourceId ? resourceMap.get(log.siteResourceId)?.niceId ?? null : null, siteName: log.siteId ? siteMap.get(log.siteId)?.name ?? null : null, siteNiceId: log.siteId ? siteMap.get(log.siteId)?.niceId ?? null : null, clientName: log.clientId ? clientMap.get(log.clientId)?.name ?? null : null })); } async function queryUniqueFilterAttributes( timeStart: number, timeEnd: number, orgId: string ) { const baseConditions = and( gt(connectionAuditLog.startedAt, timeStart), lt(connectionAuditLog.startedAt, timeEnd), eq(connectionAuditLog.orgId, orgId) ); // Get unique protocols const uniqueProtocols = await logsDb .selectDistinct({ protocol: connectionAuditLog.protocol }) .from(connectionAuditLog) .where(baseConditions); return { protocols: uniqueProtocols .map((row) => row.protocol) .filter((protocol): protocol is string => protocol !== null) }; } registry.registerPath({ method: "get", path: "/org/{orgId}/logs/connection", description: "Query the connection audit log for an organization", tags: [OpenAPITags.Logs], request: { query: queryConnectionAuditLogsQuery, params: queryConnectionAuditLogsParams }, responses: {} }); export async function queryConnectionAuditLogs( req: Request, res: Response, next: NextFunction ): Promise { try { const parsedQuery = queryConnectionAuditLogsQuery.safeParse(req.query); if (!parsedQuery.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedQuery.error) ) ); } const parsedParams = queryConnectionAuditLogsParams.safeParse( req.params ); if (!parsedParams.success) { return next( createHttpError( HttpCode.BAD_REQUEST, fromError(parsedParams.error) ) ); } const data = { ...parsedQuery.data, ...parsedParams.data }; const baseQuery = queryConnection(data); const logsRaw = await baseQuery.limit(data.limit).offset(data.offset); // Enrich with resource, site, and client details const log = await enrichWithDetails(logsRaw); const totalCountResult = await countConnectionQuery(data); const totalCount = totalCountResult[0].count; const filterAttributes = await queryUniqueFilterAttributes( data.timeStart, data.timeEnd, data.orgId ); return response(res, { data: { log: log, pagination: { total: totalCount, limit: data.limit, offset: data.offset }, filterAttributes }, success: true, error: false, message: "Connection audit logs retrieved successfully", status: HttpCode.OK }); } catch (error) { logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } }