refactor(fingerprint): start taking fingerprint snapshots in new table

This commit is contained in:
Varun Narravula
2026-01-20 06:48:40 -08:00
committed by Owen Schwartz
parent adf3d0347b
commit 1f077d7ec2
11 changed files with 307 additions and 131 deletions

View File

@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, olms } from "@server/db";
import { clients, fingerprints } from "@server/db";
import { clients, currentFingerprint } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -30,7 +30,10 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
.from(clients)
.where(eq(clients.clientId, clientId))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.leftJoin(
currentFingerprint,
eq(olms.olmId, currentFingerprint.olmId)
)
.limit(1);
return res;
} else if (niceId && orgId) {
@@ -39,7 +42,10 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
.from(clients)
.where(and(eq(clients.niceId, niceId), eq(clients.orgId, orgId)))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.leftJoin(
currentFingerprint,
eq(olms.olmId, currentFingerprint.olmId)
)
.limit(1);
return res;
}
@@ -125,24 +131,25 @@ export async function getClient(
// Replace name with device name if OLM exists
let clientName = client.clients.name;
if (client.olms) {
const model = client.fingerprints?.deviceModel || null;
const model = client.currentFingerprint?.deviceModel || null;
clientName = getUserDeviceName(model, client.clients.name);
}
// Build fingerprint data if available
const fingerprintData = client.fingerprints
const fingerprintData = client.currentFingerprint
? {
username: client.fingerprints.username || null,
hostname: client.fingerprints.hostname || null,
platform: client.fingerprints.platform || null,
osVersion: client.fingerprints.osVersion || null,
kernelVersion: client.fingerprints.kernelVersion || null,
arch: client.fingerprints.arch || null,
deviceModel: client.fingerprints.deviceModel || null,
serialNumber: client.fingerprints.serialNumber || null,
firstSeen: client.fingerprints.firstSeen || null,
lastSeen: client.fingerprints.lastSeen || null
}
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
: null;
const data: GetClientResponse = {

View File

@@ -6,7 +6,7 @@ import {
sites,
userClients,
clientSitesAssociationsCache,
fingerprints
currentFingerprint
} from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
@@ -143,20 +143,20 @@ function queryClients(
olmArchived: olms.archived,
archived: clients.archived,
blocked: clients.blocked,
deviceModel: fingerprints.deviceModel,
fingerprintPlatform: fingerprints.platform,
fingerprintOsVersion: fingerprints.osVersion,
fingerprintKernelVersion: fingerprints.kernelVersion,
fingerprintArch: fingerprints.arch,
fingerprintSerialNumber: fingerprints.serialNumber,
fingerprintUsername: fingerprints.username,
fingerprintHostname: fingerprints.hostname
deviceModel: currentFingerprint.deviceModel,
fingerprintPlatform: currentFingerprint.platform,
fingerprintOsVersion: currentFingerprint.osVersion,
fingerprintKernelVersion: currentFingerprint.kernelVersion,
fingerprintArch: currentFingerprint.arch,
fingerprintSerialNumber: currentFingerprint.serialNumber,
fingerprintUsername: currentFingerprint.username,
fingerprintHostname: currentFingerprint.hostname
})
.from(clients)
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
.leftJoin(olms, eq(clients.clientId, olms.clientId))
.leftJoin(users, eq(clients.userId, users.userId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.leftJoin(currentFingerprint, eq(olms.olmId, currentFingerprint.olmId))
.where(and(...conditions));
}

View File

@@ -0,0 +1,132 @@
import { sha256 } from "@oslojs/crypto/sha2";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { currentFingerprint, db, fingerprintSnapshots, Olm } from "@server/db";
import { desc, eq } from "drizzle-orm";
function fingerprintHash(fp: any): string {
const canonical = {
username: fp.username ?? null,
hostname: fp.hostname ?? null,
platform: fp.platform ?? null,
osVersion: fp.osVersion ?? null,
kernelVersion: fp.kernelVersion ?? null,
arch: fp.arch ?? null,
deviceModel: fp.deviceModel ?? null,
serialNumber: fp.serialNumber ?? null,
platformFingerprint: fp.platformFingerprint ?? null
};
return encodeHexLowerCase(
sha256(new TextEncoder().encode(JSON.stringify(canonical)))
);
}
export async function handleFingerprintInsertion(olm: Olm, fingerprint: any) {
if (!fingerprint || !olm.olmId || Object.keys(fingerprint).length < 1) {
return;
}
const hash = fingerprintHash(fingerprint);
const now = Math.floor(Date.now() / 1000);
const [current] = await db
.select()
.from(currentFingerprint)
.where(eq(currentFingerprint.olmId, olm.olmId))
.limit(1);
if (!current) {
const [inserted] = await db
.insert(currentFingerprint)
.values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.returning();
await db.insert(fingerprintSnapshots).values({
fingerprintId: inserted.fingerprintId,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint,
hash,
collectedAt: now
});
return;
}
// Get most recent snapshot hash
const [latestSnapshot] = await db
.select({ hash: fingerprintSnapshots.hash })
.from(fingerprintSnapshots)
.where(eq(fingerprintSnapshots.fingerprintId, current.fingerprintId))
.orderBy(desc(fingerprintSnapshots.collectedAt))
.limit(1);
const changed = !latestSnapshot || latestSnapshot.hash !== hash;
if (changed) {
// Insert snapshot if it has changed
await db.insert(fingerprintSnapshots).values({
fingerprintId: current.fingerprintId,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint,
hash,
collectedAt: now
});
// Update current fingerprint fully
await db
.update(currentFingerprint)
.set({
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(currentFingerprint.fingerprintId, current.fingerprintId));
} else {
// No change, so only bump lastSeen
await db
.update(currentFingerprint)
.set({ lastSeen: now })
.where(eq(currentFingerprint.fingerprintId, current.fingerprintId));
}
}

View File

@@ -1,6 +1,6 @@
import { NextFunction, Request, Response } from "express";
import { db } from "@server/db";
import { olms, clients, fingerprints } from "@server/db";
import { olms, clients, currentFingerprint } from "@server/db";
import { eq, and } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -66,16 +66,14 @@ export async function getUserOlm(
.select()
.from(olms)
.where(and(eq(olms.userId, userId), eq(olms.olmId, olmId)))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.leftJoin(
currentFingerprint,
eq(olms.olmId, currentFingerprint.olmId)
)
.limit(1);
if (!result || !result.olms) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Olm not found"
)
);
return next(createHttpError(HttpCode.NOT_FOUND, "Olm not found"));
}
const olm = result.olms;
@@ -98,12 +96,13 @@ export async function getUserOlm(
}
// Replace name with device name
const model = result.fingerprints?.deviceModel || null;
const model = result.currentFingerprint?.deviceModel || null;
const newName = getUserDeviceName(model, olm.name);
const responseData = blocked !== undefined
? { ...olm, name: newName, blocked }
: { ...olm, name: newName };
const responseData =
blocked !== undefined
? { ...olm, name: newName, blocked }
: { ...olm, name: newName };
return response(res, {
data: responseData,

View File

@@ -1,5 +1,5 @@
import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws";
import { clientPostureSnapshots, db, fingerprints } from "@server/db";
import { clientPostureSnapshots, db, currentFingerprint } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { clients, olms, Olm } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm";
@@ -11,6 +11,7 @@ import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { sendOlmSyncMessage } from "./sync";
import { OlmErrorCodes } from "./error";
import { handleFingerprintInsertion } from "./fingerprintingUtils";
// Track if the offline checker interval is running
let offlineCheckerInterval: NodeJS.Timeout | null = null;
@@ -173,15 +174,25 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
}
// get the version
logger.debug(`handleOlmPingMessage: About to get config version for olmId: ${olm.olmId}`);
logger.debug(
`handleOlmPingMessage: About to get config version for olmId: ${olm.olmId}`
);
const configVersion = await getClientConfigVersion(olm.olmId);
logger.debug(`handleOlmPingMessage: Got config version: ${configVersion} (type: ${typeof configVersion})`);
logger.debug(
`handleOlmPingMessage: Got config version: ${configVersion} (type: ${typeof configVersion})`
);
if (configVersion == null || configVersion === undefined) {
logger.debug(`handleOlmPingMessage: could not get config version from server for olmId: ${olm.olmId}`);
logger.debug(
`handleOlmPingMessage: could not get config version from server for olmId: ${olm.olmId}`
);
}
if (message.configVersion != null && configVersion != null && configVersion != message.configVersion) {
if (
message.configVersion != null &&
configVersion != null &&
configVersion != message.configVersion
) {
logger.debug(
`handleOlmPingMessage: Olm ping with outdated config version: ${message.configVersion} (current: ${configVersion})`
);
@@ -204,55 +215,14 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
.set({ archived: false })
.where(eq(olms.olmId, olm.olmId));
}
await handleFingerprintInsertion(olm, fingerprint);
} catch (error) {
logger.error("Error handling ping message", { error });
}
const now = Math.floor(Date.now() / 1000);
if (fingerprint && olm.olmId) {
const [existingFingerprint] = await db
.select()
.from(fingerprints)
.where(eq(fingerprints.olmId, olm.olmId))
.limit(1);
if (!existingFingerprint) {
await db.insert(fingerprints).values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
});
} else {
await db
.update(fingerprints)
.set({
lastSeen: now,
username: fingerprint.username,
hostname: fingerprint.hostname,
platform: fingerprint.platform,
osVersion: fingerprint.osVersion,
kernelVersion: fingerprint.kernelVersion,
arch: fingerprint.arch,
deviceModel: fingerprint.deviceModel,
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(fingerprints.olmId, olm.olmId));
}
}
if (postures && olm.clientId) {
await db.insert(clientPostureSnapshots).values({
clientId: olm.clientId,

View File

@@ -1,4 +1,9 @@
import { clientPostureSnapshots, db, fingerprints, orgs } from "@server/db";
import {
clientPostureSnapshots,
db,
currentFingerprint,
orgs
} from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import {
clients,
@@ -48,12 +53,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
if (fingerprint) {
const [existingFingerprint] = await db
.select()
.from(fingerprints)
.where(eq(fingerprints.olmId, olm.olmId))
.from(currentFingerprint)
.where(eq(currentFingerprint.olmId, olm.olmId))
.limit(1);
if (!existingFingerprint) {
await db.insert(fingerprints).values({
await db.insert(currentFingerprint).values({
olmId: olm.olmId,
firstSeen: now,
lastSeen: now,
@@ -75,16 +80,16 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
existingFingerprint.platform !== fingerprint.platform ||
existingFingerprint.osVersion !== fingerprint.osVersion ||
existingFingerprint.kernelVersion !==
fingerprint.kernelVersion ||
fingerprint.kernelVersion ||
existingFingerprint.arch !== fingerprint.arch ||
existingFingerprint.deviceModel !== fingerprint.deviceModel ||
existingFingerprint.serialNumber !== fingerprint.serialNumber ||
existingFingerprint.platformFingerprint !==
fingerprint.platformFingerprint;
fingerprint.platformFingerprint;
if (hasChanges) {
await db
.update(fingerprints)
.update(currentFingerprint)
.set({
lastSeen: now,
username: fingerprint.username,
@@ -97,7 +102,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
serialNumber: fingerprint.serialNumber,
platformFingerprint: fingerprint.platformFingerprint
})
.where(eq(fingerprints.olmId, olm.olmId));
.where(eq(currentFingerprint.olmId, olm.olmId));
}
}
}

View File

@@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express";
import { db, fingerprints } from "@server/db";
import { db, currentFingerprint } from "@server/db";
import { olms } from "@server/db";
import { eq, count, desc } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
@@ -104,13 +104,16 @@ export async function listUserOlms(
.select()
.from(olms)
.where(eq(olms.userId, userId))
.leftJoin(fingerprints, eq(olms.olmId, fingerprints.olmId))
.leftJoin(
currentFingerprint,
eq(olms.olmId, currentFingerprint.olmId)
)
.orderBy(desc(olms.dateCreated))
.limit(limit)
.offset(offset);
const userOlms = list.map((item) => {
const model = item.fingerprints?.deviceModel || null;
const model = item.currentFingerprint?.deviceModel || null;
const newName = getUserDeviceName(model, item.olms.name);
return {

View File

@@ -1,4 +1,4 @@
import { db, fingerprints, olms } from "@server/db";
import { db, currentFingerprint, olms } from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { and, eq } from "drizzle-orm";
@@ -55,18 +55,24 @@ export async function recoverOlmWithFingerprint(
const result = await db
.select({
olm: olms,
fingerprint: fingerprints
fingerprint: currentFingerprint
})
.from(olms)
.innerJoin(fingerprints, eq(fingerprints.olmId, olms.olmId))
.innerJoin(
currentFingerprint,
eq(currentFingerprint.olmId, olms.olmId)
)
.where(
and(
eq(olms.userId, userId),
eq(olms.archived, false),
eq(fingerprints.platformFingerprint, platformFingerprint)
eq(
currentFingerprint.platformFingerprint,
platformFingerprint
)
)
)
.orderBy(fingerprints.lastSeen);
.orderBy(currentFingerprint.lastSeen);
if (!result || result.length == 0) {
return next(