diff --git a/messages/en-US.json b/messages/en-US.json index 827b8187..ea91bfda 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2489,6 +2489,8 @@ "logIn": "Log In", "deviceInformation": "Device Information", "deviceInformationDescription": "Information about the device and agent", + "deviceSecurity": "Device Security", + "deviceSecurityDescription": "Device security posture information", "platform": "Platform", "macosVersion": "macOS Version", "windowsVersion": "Windows Version", @@ -2501,6 +2503,17 @@ "hostname": "Hostname", "firstSeen": "First Seen", "lastSeen": "Last Seen", + "biometricsEnabled": "Biometrics Enabled", + "diskEncrypted": "Disk Encrypted", + "firewallEnabled": "Firewall Enabled", + "autoUpdatesEnabled": "Auto Updates Enabled", + "tpmAvailable": "TPM Available", + "windowsDefenderEnabled": "Windows Defender Enabled", + "macosSipEnabled": "System Integrity Protection (SIP)", + "macosGatekeeperEnabled": "Gatekeeper", + "macosFirewallStealthMode": "Firewall Stealth Mode", + "linuxAppArmorEnabled": "AppArmor", + "linuxSELinuxEnabled": "SELinux", "deviceSettingsDescription": "View device information and settings", "devicePendingApprovalDescription": "This device is waiting for approval", "deviceBlockedDescription": "This device is currently blocked. It won't be able to connect to any resources unless unblocked.", diff --git a/server/private/middlewares/verifyValidSubscription.ts b/server/private/middlewares/verifyValidSubscription.ts new file mode 100644 index 00000000..5e6a9ff5 --- /dev/null +++ b/server/private/middlewares/verifyValidSubscription.ts @@ -0,0 +1,51 @@ +/* + * 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 { Request, Response, NextFunction } from "express"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { build } from "@server/build"; +import { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; + +export async function verifyValidLicense( + req: Request, + res: Response, + next: NextFunction +) { + try { + if (build != "saas") { + return next(); + } + + const { tier, active } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + + return next(); + } catch (e) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying subscription" + ) + ); + } +} diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index b7f56640..d981cbd3 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -11,6 +11,7 @@ import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { getUserDeviceName } from "@server/db/names"; +import { build } from "@server/build"; const getClientSchema = z.strictObject({ clientId: z @@ -51,6 +52,106 @@ async function query(clientId?: number, niceId?: string, orgId?: string) { } } +type PostureData = { + biometricsEnabled?: boolean | null; + diskEncrypted?: boolean | null; + firewallEnabled?: boolean | null; + autoUpdatesEnabled?: boolean | null; + tpmAvailable?: boolean | null; + windowsDefenderEnabled?: boolean | null; + macosSipEnabled?: boolean | null; + macosGatekeeperEnabled?: boolean | null; + macosFirewallStealthMode?: boolean | null; + linuxAppArmorEnabled?: boolean | null; + linuxSELinuxEnabled?: boolean | null; +}; + +function getPlatformPostureData( + platform: string | null | undefined, + fingerprint: typeof currentFingerprint.$inferSelect | null +): PostureData | null { + if (!fingerprint) return null; + + const normalizedPlatform = platform?.toLowerCase() || "unknown"; + const posture: PostureData = {}; + + // Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender + if (normalizedPlatform === "windows") { + if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + posture.diskEncrypted = fingerprint.diskEncrypted; + } + if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { + posture.firewallEnabled = fingerprint.firewallEnabled; + } + if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) { + posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled; + } + if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { + posture.tpmAvailable = fingerprint.tpmAvailable; + } + if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) { + posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled; + } + } + // macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode + else if (normalizedPlatform === "macos") { + if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + posture.diskEncrypted = fingerprint.diskEncrypted; + } + if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { + posture.biometricsEnabled = fingerprint.biometricsEnabled; + } + if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { + posture.firewallEnabled = fingerprint.firewallEnabled; + } + if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) { + posture.macosSipEnabled = fingerprint.macosSipEnabled; + } + if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) { + posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled; + } + if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) { + posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode; + } + } + // Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability + else if (normalizedPlatform === "linux") { + if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + posture.diskEncrypted = fingerprint.diskEncrypted; + } + if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { + posture.firewallEnabled = fingerprint.firewallEnabled; + } + if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) { + posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled; + } + if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) { + posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled; + } + if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { + posture.tpmAvailable = fingerprint.tpmAvailable; + } + } + // iOS: Biometric configuration + else if (normalizedPlatform === "ios") { + if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { + posture.biometricsEnabled = fingerprint.biometricsEnabled; + } + } + // Android: Screen lock, Biometric configuration, Hard drive encryption + else if (normalizedPlatform === "android") { + if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { + posture.biometricsEnabled = fingerprint.biometricsEnabled; + } + if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { + posture.diskEncrypted = fingerprint.diskEncrypted; + } + } + + // Only return if we have at least one posture field + return Object.keys(posture).length > 0 ? posture : null; +} + export type GetClientResponse = NonNullable< Awaited> >["clients"] & { @@ -69,6 +170,7 @@ export type GetClientResponse = NonNullable< firstSeen: number | null; lastSeen: number | null; } | null; + posture: PostureData | null; }; registry.registerPath({ @@ -152,13 +254,23 @@ export async function getClient( } : null; + // Build posture data if available (platform-specific) + let postureData: PostureData | null = null; + if (build !== "oss") { + postureData = getPlatformPostureData( + client.currentFingerprint?.platform || null, + client.currentFingerprint + ); + } + const data: GetClientResponse = { ...client.clients, name: clientName, olmId: client.olms ? client.olms.olmId : null, agent: client.olms?.agent || null, olmVersion: client.olms?.version || null, - fingerprint: fingerprintData + fingerprint: fingerprintData, + posture: postureData }; return response(res, { diff --git a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx index daa668e6..ed9a5f49 100644 --- a/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/clients/user/[niceId]/general/page.tsx @@ -22,12 +22,13 @@ import { import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import ActionBanner from "@app/components/ActionBanner"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { toast } from "@app/hooks/useToast"; import { useRouter } from "next/navigation"; import { useState, useEffect, useTransition } from "react"; -import { Check, Ban, Shield, ShieldOff, Clock } from "lucide-react"; +import { Check, Ban, Shield, ShieldOff, Clock, CheckCircle2, XCircle } from "lucide-react"; import { useParams } from "next/navigation"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; import { SiAndroid } from "react-icons/si"; @@ -111,18 +112,12 @@ function getPlatformFieldConfig( kernelVersion: { show: false, labelKey: "kernelVersion" }, arch: { show: true, labelKey: "architecture" }, deviceModel: { show: true, labelKey: "deviceModel" }, - serialNumber: { show: true, labelKey: "serialNumber" }, - username: { show: true, labelKey: "username" }, - hostname: { show: true, labelKey: "hostname" } }, android: { osVersion: { show: true, labelKey: "androidVersion" }, kernelVersion: { show: true, labelKey: "kernelVersion" }, arch: { show: true, labelKey: "architecture" }, deviceModel: { show: true, labelKey: "deviceModel" }, - serialNumber: { show: true, labelKey: "serialNumber" }, - username: { show: true, labelKey: "username" }, - hostname: { show: true, labelKey: "hostname" } }, unknown: { osVersion: { show: true, labelKey: "osVersion" }, @@ -138,6 +133,7 @@ function getPlatformFieldConfig( return configs[normalizedPlatform] || configs.unknown; } + export default function GeneralPage() { const { client, updateClient } = useClientContext(); const { isPaidUser } = usePaidStatus(); @@ -152,6 +148,20 @@ export default function GeneralPage() { const showApprovalFeatures = build !== "oss" && isPaidUser; + const formatPostureValue = (value: boolean | null | undefined) => { + if (value === null || value === undefined) return "-"; + return ( +
+ {value ? ( + + ) : ( + + )} + {value ? t("enabled") : t("disabled")} +
+ ); + }; + // Fetch approval ID for this client if pending useEffect(() => { if ( @@ -407,13 +417,13 @@ export default function GeneralPage() { )} {client.fingerprint.osVersion && - fieldConfig.osVersion.show && ( + fieldConfig.osVersion?.show && ( {t( fieldConfig .osVersion - .labelKey + ?.labelKey || "osVersion" )} @@ -426,7 +436,7 @@ export default function GeneralPage() { )} {client.fingerprint.kernelVersion && - fieldConfig.kernelVersion.show && ( + fieldConfig.kernelVersion?.show && ( {t("kernelVersion")} @@ -456,7 +466,7 @@ export default function GeneralPage() { )} {client.fingerprint.deviceModel && - fieldConfig.deviceModel.show && ( + fieldConfig.deviceModel?.show && ( {t("deviceModel")} @@ -486,7 +496,7 @@ export default function GeneralPage() { )} {client.fingerprint.username && - fieldConfig.username.show && ( + fieldConfig.username?.show && ( {t("username")} @@ -501,7 +511,7 @@ export default function GeneralPage() { )} {client.fingerprint.hostname && - fieldConfig.hostname.show && ( + fieldConfig.hostname?.show && ( {t("hostname")} @@ -548,6 +558,218 @@ export default function GeneralPage() { )} + + {/* Device Security Section */} + {build !== "oss" && ( + + + + {t("deviceSecurity")} + + + {t("deviceSecurityDescription")} + + + + + {client.posture && Object.keys(client.posture).length > 0 ? ( + <> + {!isPaidUser && } + + {client.posture.biometricsEnabled !== null && + client.posture.biometricsEnabled !== undefined && ( + + + {t("biometricsEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture.biometricsEnabled + ) + : "-"} + + + )} + + {client.posture.diskEncrypted !== null && + client.posture.diskEncrypted !== undefined && ( + + + {t("diskEncrypted")} + + + {isPaidUser + ? formatPostureValue( + client.posture.diskEncrypted + ) + : "-"} + + + )} + + {client.posture.firewallEnabled !== null && + client.posture.firewallEnabled !== undefined && ( + + + {t("firewallEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture.firewallEnabled + ) + : "-"} + + + )} + + {client.posture.autoUpdatesEnabled !== null && + client.posture.autoUpdatesEnabled !== undefined && ( + + + {t("autoUpdatesEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture.autoUpdatesEnabled + ) + : "-"} + + + )} + + {client.posture.tpmAvailable !== null && + client.posture.tpmAvailable !== undefined && ( + + + {t("tpmAvailable")} + + + {isPaidUser + ? formatPostureValue( + client.posture.tpmAvailable + ) + : "-"} + + + )} + + {client.posture.windowsDefenderEnabled !== null && + client.posture.windowsDefenderEnabled !== undefined && ( + + + {t("windowsDefenderEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .windowsDefenderEnabled + ) + : "-"} + + + )} + + {client.posture.macosSipEnabled !== null && + client.posture.macosSipEnabled !== undefined && ( + + + {t("macosSipEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture.macosSipEnabled + ) + : "-"} + + + )} + + {client.posture.macosGatekeeperEnabled !== null && + client.posture.macosGatekeeperEnabled !== + undefined && ( + + + {t("macosGatekeeperEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .macosGatekeeperEnabled + ) + : "-"} + + + )} + + {client.posture.macosFirewallStealthMode !== null && + client.posture.macosFirewallStealthMode !== + undefined && ( + + + {t("macosFirewallStealthMode")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .macosFirewallStealthMode + ) + : "-"} + + + )} + + {client.posture.linuxAppArmorEnabled !== null && + client.posture.linuxAppArmorEnabled !== + undefined && ( + + + {t("linuxAppArmorEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .linuxAppArmorEnabled + ) + : "-"} + + + )} + + {client.posture.linuxSELinuxEnabled !== null && + client.posture.linuxSELinuxEnabled !== + undefined && ( + + + {t("linuxSELinuxEnabled")} + + + {isPaidUser + ? formatPostureValue( + client.posture + .linuxSELinuxEnabled + ) + : "-"} + + + )} + + + ) : ( +
+ {t("noData")} +
+ )} +
+
+ )} ); } diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 39ae601f..294fd54d 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -145,10 +145,10 @@ function CollapsibleNavItem({
{notificationCount !== undefined && notificationCount > 0 && ( - - {notificationCount > 99 ? "99+" : notificationCount} + + {notificationCount > 99 + ? "99+" + : notificationCount} )} {build === "enterprise" && @@ -321,9 +321,7 @@ export function SidebarNav({
{notificationCount !== undefined && notificationCount > 0 && ( - + {notificationCount > 99 ? "99+" : notificationCount} @@ -346,8 +344,8 @@ export function SidebarNav({ notificationCount !== undefined && notificationCount > 0 && ( {notificationCount > 99 ? "99+" : notificationCount} @@ -379,7 +377,7 @@ export function SidebarNav({ {notificationCount !== undefined && notificationCount > 0 && ( {notificationCount > 99 diff --git a/src/lib/formatDeviceFingerprint.ts b/src/lib/formatDeviceFingerprint.ts index 3bd4a99b..34686169 100644 --- a/src/lib/formatDeviceFingerprint.ts +++ b/src/lib/formatDeviceFingerprint.ts @@ -38,6 +38,7 @@ export function formatFingerprintInfo( ): string { if (!fingerprint) return ""; const parts: string[] = []; + const normalizedPlatform = fingerprint.platform?.toLowerCase() || "unknown"; if (fingerprint.platform) { parts.push( @@ -53,14 +54,17 @@ export function formatFingerprintInfo( if (fingerprint.arch) { parts.push(`${t("architecture")}: ${fingerprint.arch}`); } - if (fingerprint.hostname) { - parts.push(`${t("hostname")}: ${fingerprint.hostname}`); - } - if (fingerprint.username) { - parts.push(`${t("username")}: ${fingerprint.username}`); - } - if (fingerprint.serialNumber) { - parts.push(`${t("serialNumber")}: ${fingerprint.serialNumber}`); + + if (normalizedPlatform !== "ios" && normalizedPlatform !== "android") { + if (fingerprint.hostname) { + parts.push(`${t("hostname")}: ${fingerprint.hostname}`); + } + if (fingerprint.username) { + parts.push(`${t("username")}: ${fingerprint.username}`); + } + if (fingerprint.serialNumber) { + parts.push(`${t("serialNumber")}: ${fingerprint.serialNumber}`); + } } return parts.join("\n");