diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 571877cf..714f17c8 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -712,6 +712,49 @@ export const clientSiteResourcesAssociationsCache = pgTable( } ); +export const clientPostureSnapshots = pgTable("clientPostureSnapshots", { + snapshotId: serial("snapshotId").primaryKey(), + + clientId: integer("clientId").references(() => clients.clientId, { + onDelete: "cascade" + }), + + // Platform-agnostic checks + + biometricsEnabled: boolean("biometricsEnabled").notNull().default(false), + diskEncrypted: boolean("diskEncrypted").notNull().default(false), + firewallEnabled: boolean("firewallEnabled").notNull().default(false), + autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false), + tpmAvailable: boolean("tpmAvailable").notNull().default(false), + + // Windows-specific posture check information + + windowsDefenderEnabled: boolean("windowsDefenderEnabled") + .notNull() + .default(false), + + // macOS-specific posture check information + + macosSipEnabled: boolean("macosSipEnabled").notNull().default(false), + macosGatekeeperEnabled: boolean("macosGatekeeperEnabled") + .notNull() + .default(false), + macosFirewallStealthMode: boolean("macosFirewallStealthMode") + .notNull() + .default(false), + + // Linux-specific posture check information + + linuxAppArmorEnabled: boolean("linuxAppArmorEnabled") + .notNull() + .default(false), + linuxSELinuxEnabled: boolean("linuxSELinuxEnabled") + .notNull() + .default(false), + + collectedAt: integer("collectedAt").notNull() +}); + export const olms = pgTable("olms", { olmId: varchar("id").primaryKey(), secretHash: varchar("secretHash").notNull(), @@ -730,6 +773,26 @@ export const olms = pgTable("olms", { archived: boolean("archived").notNull().default(false) }); +export const fingerprints = pgTable("fingerprints", { + fingerprintId: serial("id").primaryKey(), + + olmId: text("olmId") + .references(() => olms.olmId, { onDelete: "cascade" }) + .notNull(), + + firstSeen: integer("firstSeen").notNull(), + lastSeen: integer("lastSeen").notNull(), + + username: text("username"), + hostname: text("hostname"), + platform: text("platform"), // macos | windows | linux | ios | android | unknown + osVersion: text("osVersion"), + kernelVersion: text("kernelVersion"), + arch: text("arch"), + deviceModel: text("deviceModel"), + serialNumber: text("serialNumber") +}); + export const olmSessions = pgTable("clientSession", { sessionId: varchar("id").primaryKey(), olmId: varchar("olmId") diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 69647229..f967b2d2 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -255,7 +255,9 @@ export const siteResources = sqliteTable("siteResources", { aliasAddress: text("aliasAddress"), tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"), udpPortRangeString: text("udpPortRangeString").notNull().default("*"), - disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false) + disableIcmp: integer("disableIcmp", { mode: "boolean" }) + .notNull() + .default(false) }); export const clientSiteResources = sqliteTable("clientSiteResources", { @@ -409,6 +411,69 @@ export const clientSiteResourcesAssociationsCache = sqliteTable( } ); +export const clientPostureSnapshots = sqliteTable("clientPostureSnapshots", { + snapshotId: integer("snapshotId").primaryKey({ autoIncrement: true }), + + clientId: integer("clientId").references(() => clients.clientId, { + onDelete: "cascade" + }), + + // Platform-agnostic checks + + biometricsEnabled: integer("biometricsEnabled", { mode: "boolean" }) + .notNull() + .default(false), + diskEncrypted: integer("diskEncrypted", { mode: "boolean" }) + .notNull() + .default(false), + firewallEnabled: integer("firewallEnabled", { mode: "boolean" }) + .notNull() + .default(false), + autoUpdatesEnabled: integer("autoUpdatesEnabled", { mode: "boolean" }) + .notNull() + .default(false), + tpmAvailable: integer("tpmAvailable", { mode: "boolean" }) + .notNull() + .default(false), + + // Windows-specific posture check information + + windowsDefenderEnabled: integer("windowsDefenderEnabled", { + mode: "boolean" + }) + .notNull() + .default(false), + + // macOS-specific posture check information + + macosSipEnabled: integer("macosSipEnabled", { mode: "boolean" }) + .notNull() + .default(false), + macosGatekeeperEnabled: integer("macosGatekeeperEnabled", { + mode: "boolean" + }) + .notNull() + .default(false), + macosFirewallStealthMode: integer("macosFirewallStealthMode", { + mode: "boolean" + }) + .notNull() + .default(false), + + // Linux-specific posture check information + + linuxAppArmorEnabled: integer("linuxAppArmorEnabled", { mode: "boolean" }) + .notNull() + .default(false), + linuxSELinuxEnabled: integer("linuxSELinuxEnabled", { + mode: "boolean" + }) + .notNull() + .default(false), + + collectedAt: integer("collectedAt").notNull() +}); + export const olms = sqliteTable("olms", { olmId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), @@ -427,6 +492,26 @@ export const olms = sqliteTable("olms", { archived: integer("archived", { mode: "boolean" }).notNull().default(false) }); +export const fingerprints = sqliteTable("fingerprints", { + fingerprintId: integer("id").primaryKey({ autoIncrement: true }), + + olmId: text("olmId") + .references(() => olms.olmId, { onDelete: "cascade" }) + .notNull(), + + firstSeen: integer("firstSeen").notNull(), + lastSeen: integer("lastSeen").notNull(), + + username: text("username"), + hostname: text("hostname"), + platform: text("platform"), // macos | windows | linux | ios | android | unknown + osVersion: text("osVersion"), + kernelVersion: text("kernelVersion"), + arch: text("arch"), + deviceModel: text("deviceModel"), + serialNumber: text("serialNumber") +}); + export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { codeId: integer("id").primaryKey({ autoIncrement: true }), userId: text("userId") diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 0fa490c8..ab300361 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -1,4 +1,4 @@ -import { db } from "@server/db"; +import { clientPostureSnapshots, db, fingerprints } from "@server/db"; import { disconnectClient } from "#dynamic/routers/ws"; import { MessageHandler } from "@server/routers/ws"; import { clients, Olm } from "@server/db"; @@ -101,7 +101,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { const { message, client: c, sendToClient } = context; const olm = c as Olm; - const { userToken } = message.data; + const { userToken, fingerprint, postures } = message.data; if (!olm) { logger.warn("Olm not found"); @@ -174,6 +174,72 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { 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 + }); + } 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 + }) + .where(eq(fingerprints.olmId, olm.olmId)); + } + } + + if (postures && olm.clientId) { + await db.insert(clientPostureSnapshots).values({ + clientId: olm.clientId, + + biometricsEnabled: postures?.biometricsEnabled, + diskEncrypted: postures?.diskEncrypted, + firewallEnabled: postures?.firewallEnabled, + autoUpdatesEnabled: postures?.autoUpdatesEnabled, + tpmAvailable: postures?.tpmAvailable, + + windowsDefenderEnabled: postures?.windowsDefenderEnabled, + + macosSipEnabled: postures?.macosSipEnabled, + macosGatekeeperEnabled: postures?.macosGatekeeperEnabled, + macosFirewallStealthMode: postures?.macosFirewallStealthMode, + + linuxAppArmorEnabled: postures?.linuxAppArmorEnabled, + linuxSELinuxEnabled: postures?.linuxSELinuxEnabled, + + collectedAt: now + }); + } + return { message: { type: "pong", diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 0f71ee8b..99f891d7 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,6 +1,8 @@ import { + clientPostureSnapshots, clientSiteResourcesAssociationsCache, db, + fingerprints, orgs, siteResources } from "@server/db"; @@ -36,8 +38,16 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - const { publicKey, relay, olmVersion, olmAgent, orgId, userToken } = - message.data; + const { + publicKey, + relay, + olmVersion, + olmAgent, + orgId, + userToken, + fingerprint, + postures + } = message.data; if (!olm.clientId) { logger.warn("Olm client ID not found"); @@ -289,6 +299,70 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { }); } + if (fingerprint) { + 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 + }); + } 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 + }) + .where(eq(fingerprints.olmId, olm.olmId)); + } + } + + if (postures && olm.clientId) { + await db.insert(clientPostureSnapshots).values({ + clientId: olm.clientId, + + biometricsEnabled: postures?.biometricsEnabled, + diskEncrypted: postures?.diskEncrypted, + firewallEnabled: postures?.firewallEnabled, + autoUpdatesEnabled: postures?.autoUpdatesEnabled, + tpmAvailable: postures?.tpmAvailable, + + windowsDefenderEnabled: postures?.windowsDefenderEnabled, + + macosSipEnabled: postures?.macosSipEnabled, + macosGatekeeperEnabled: postures?.macosGatekeeperEnabled, + macosFirewallStealthMode: postures?.macosFirewallStealthMode, + + linuxAppArmorEnabled: postures?.linuxAppArmorEnabled, + linuxSELinuxEnabled: postures?.linuxSELinuxEnabled, + + collectedAt: now + }); + } + // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES // if (siteConfigurations.length === 0) { // logger.warn("No valid site configurations found");