From 6235a6fcd2ff71645b7da55e581914579288ad06 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 30 Dec 2025 17:59:18 +0100 Subject: [PATCH] Report updates --- .../CippComponents/AuthMethodSankey.jsx | 14 +- src/components/CippComponents/CaSankey.jsx | 26 +- src/pages/dashboardv2/index.js | 1153 +++++++++++------ 3 files changed, 800 insertions(+), 393 deletions(-) diff --git a/src/components/CippComponents/AuthMethodSankey.jsx b/src/components/CippComponents/AuthMethodSankey.jsx index 6ef4e61e666d..200cb4274766 100644 --- a/src/components/CippComponents/AuthMethodSankey.jsx +++ b/src/components/CippComponents/AuthMethodSankey.jsx @@ -13,17 +13,21 @@ export const AuthMethodSankey = ({ data }) => { id: "Single factor", nodeColor: "hsl(0, 100%, 50%)", }, + { + id: "Multi factor", + nodeColor: "hsl(200, 70%, 50%)", + }, { id: "Phishable", - nodeColor: "hsl(12, 76%, 61%)", + nodeColor: "hsl(39, 100%, 50%)", }, { id: "Phone", - nodeColor: "hsl(12, 76%, 61%)", + nodeColor: "hsl(39, 100%, 45%)", }, { id: "Authenticator", - nodeColor: "hsl(12, 76%, 61%)", + nodeColor: "hsl(39, 100%, 55%)", }, { id: "Phish resistant", @@ -31,11 +35,11 @@ export const AuthMethodSankey = ({ data }) => { }, { id: "Passkey", - nodeColor: "hsl(99, 70%, 50%)", + nodeColor: "hsl(140, 70%, 50%)", }, { id: "WHfB", - nodeColor: "hsl(99, 70%, 50%)", + nodeColor: "hsl(160, 70%, 50%)", }, ], links: data, diff --git a/src/components/CippComponents/CaSankey.jsx b/src/components/CippComponents/CaSankey.jsx index 5b860e45dda5..ffad546d8738 100644 --- a/src/components/CippComponents/CaSankey.jsx +++ b/src/components/CippComponents/CaSankey.jsx @@ -6,24 +6,32 @@ export const CaSankey = ({ data }) => { data={{ nodes: [ { - id: "User sign in", + id: "Enabled users", nodeColor: "hsl(28, 100%, 53%)", }, { - id: "No CA applied", - nodeColor: "hsl(0, 100%, 50%)", + id: "MFA registered", + nodeColor: "hsl(99, 70%, 50%)", }, { - id: "CA applied", - nodeColor: "hsl(12, 76%, 61%)", + id: "Not registered", + nodeColor: "hsl(39, 100%, 50%)", }, { - id: "No MFA", - nodeColor: "hsl(0, 69%, 50%)", + id: "CA policy", + nodeColor: "hsl(99, 70%, 50%)", }, { - id: "MFA", - nodeColor: "hsl(99, 70%, 50%)", + id: "Security defaults", + nodeColor: "hsl(140, 70%, 50%)", + }, + { + id: "Per-user MFA", + nodeColor: "hsl(200, 70%, 50%)", + }, + { + id: "No enforcement", + nodeColor: "hsl(0, 100%, 50%)", }, ], links: data, diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 90b78b4f2e3d..9dbcf827492c 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -64,6 +64,211 @@ import { Work as BriefcaseIcon, } from "@mui/icons-material"; +// Helper function to process MFAState data into Sankey chart format +const processMFAStateData = (mfaState) => { + if (!mfaState || !Array.isArray(mfaState) || mfaState.length === 0) { + return null; + } + + // Count enabled users only + const enabledUsers = mfaState.filter((user) => user.AccountEnabled === true); + + if (enabledUsers.length === 0) { + return null; + } + + // Split by MFA registration status + let registeredUsers = 0; + let notRegisteredUsers = 0; + + // For registered users, split by protection method + let registeredCA = 0; + let registeredSD = 0; + let registeredPerUser = 0; + let registeredNone = 0; + + // For not registered users, split by protection method + let notRegisteredCA = 0; + let notRegisteredSD = 0; + let notRegisteredPerUser = 0; + let notRegisteredNone = 0; + + enabledUsers.forEach((user) => { + const hasRegistered = user.MFARegistration === true; + const coveredByCA = user.CoveredByCA?.startsWith("Enforced") || false; + const coveredBySD = user.CoveredBySD === true; + const perUserEnabled = user.PerUser === "enforced" || user.PerUser === "enabled"; + + // Consider PerUser as MFA enabled/registered + if (hasRegistered || perUserEnabled) { + registeredUsers++; + // Per-User gets its own separate terminal path + if (perUserEnabled) { + registeredPerUser++; + } else if (coveredByCA) { + registeredCA++; + } else if (coveredBySD) { + registeredSD++; + } else { + registeredNone++; + } + } else { + notRegisteredUsers++; + if (coveredByCA) { + notRegisteredCA++; + } else if (coveredBySD) { + notRegisteredSD++; + } else { + notRegisteredNone++; + } + } + }); + + const registeredPercentage = ((registeredUsers / enabledUsers.length) * 100).toFixed(1); + const protectedPercentage = ( + ((registeredCA + registeredSD + registeredPerUser) / enabledUsers.length) * + 100 + ).toFixed(1); + + const nodes = [ + { source: "Enabled users", target: "MFA registered", value: registeredUsers }, + { source: "Enabled users", target: "Not registered", value: notRegisteredUsers }, + ]; + + // Add protection methods for registered users + if (registeredCA > 0) + nodes.push({ source: "MFA registered", target: "CA policy", value: registeredCA }); + if (registeredSD > 0) + nodes.push({ source: "MFA registered", target: "Security defaults", value: registeredSD }); + if (registeredPerUser > 0) + nodes.push({ source: "MFA registered", target: "Per-user MFA", value: registeredPerUser }); + if (registeredNone > 0) + nodes.push({ source: "MFA registered", target: "No enforcement", value: registeredNone }); + + // Add protection methods for not registered users + if (notRegisteredCA > 0) + nodes.push({ source: "Not registered", target: "CA policy", value: notRegisteredCA }); + if (notRegisteredSD > 0) + nodes.push({ source: "Not registered", target: "Security defaults", value: notRegisteredSD }); + if (notRegisteredPerUser > 0) + nodes.push({ source: "Not registered", target: "Per-user MFA", value: notRegisteredPerUser }); + if (notRegisteredNone > 0) + nodes.push({ source: "Not registered", target: "No enforcement", value: notRegisteredNone }); + + return { + description: `${registeredPercentage}% of enabled users have registered MFA methods. ${protectedPercentage}% are protected by policies requiring MFA.`, + nodes: nodes, + }; +}; + +// Helper function to process MFAState data into Auth Methods Sankey chart format +const processAuthMethodsData = (mfaState) => { + if (!mfaState || !Array.isArray(mfaState) || mfaState.length === 0) { + return null; + } + + // Count enabled users only + const enabledUsers = mfaState.filter((user) => user.AccountEnabled === true); + + if (enabledUsers.length === 0) { + return null; + } + + // Categorize MFA methods as phishable or phish-resistant + const phishableMethods = ["mobilePhone", "email", "microsoftAuthenticatorPush"]; + const phishResistantMethods = ["fido2", "windowsHelloForBusiness", "x509Certificate"]; + + let singleFactor = 0; + let phishableCount = 0; + let phishResistantCount = 0; + let perUserMFA = 0; + + // Breakdown of phishable methods + let phoneCount = 0; + let authenticatorCount = 0; + + // Breakdown of phish-resistant methods + let passkeyCount = 0; + let whfbCount = 0; + + enabledUsers.forEach((user) => { + const methods = user.MFAMethods || []; + const perUser = user.PerUser === "enforced" || user.PerUser === "enabled"; + const hasRegistered = user.MFARegistration === true; + + // If user has per-user MFA enforced but no specific methods, count as generic MFA + if (perUser && !hasRegistered && methods.length === 0) { + perUserMFA++; + return; + } + + // Check if user has any MFA methods + if (!hasRegistered || methods.length === 0) { + singleFactor++; + return; + } + + // Categorize by method type + const hasPhishResistant = methods.some((m) => phishResistantMethods.includes(m)); + const hasPhishable = methods.some((m) => phishableMethods.includes(m)); + + if (hasPhishResistant) { + phishResistantCount++; + // Count specific phish-resistant methods + if (methods.includes("fido2") || methods.includes("x509Certificate")) { + passkeyCount++; + } + if (methods.includes("windowsHelloForBusiness")) { + whfbCount++; + } + } else if (hasPhishable) { + phishableCount++; + // Count specific phishable methods + if (methods.includes("mobilePhone") || methods.includes("email")) { + phoneCount++; + } + if ( + methods.includes("microsoftAuthenticatorPush") || + methods.includes("softwareOneTimePasscode") + ) { + authenticatorCount++; + } + } else { + // Has MFA methods but not in our categorized lists + phishableCount++; + authenticatorCount++; + } + }); + + const mfaPercentage = ( + ((phishableCount + phishResistantCount + perUserMFA) / enabledUsers.length) * + 100 + ).toFixed(1); + const phishResistantPercentage = ((phishResistantCount / enabledUsers.length) * 100).toFixed(1); + + const nodes = [ + { source: "Users", target: "Single factor", value: singleFactor }, + { source: "Users", target: "Multi factor", value: perUserMFA }, + { source: "Users", target: "Phishable", value: phishableCount }, + { source: "Users", target: "Phish resistant", value: phishResistantCount }, + ]; + + // Add phishable method breakdowns + if (phoneCount > 0) nodes.push({ source: "Phishable", target: "Phone", value: phoneCount }); + if (authenticatorCount > 0) + nodes.push({ source: "Phishable", target: "Authenticator", value: authenticatorCount }); + + // Add phish-resistant method breakdowns + if (passkeyCount > 0) + nodes.push({ source: "Phish resistant", target: "Passkey", value: passkeyCount }); + if (whfbCount > 0) nodes.push({ source: "Phish resistant", target: "WHfB", value: whfbCount }); + + return { + description: `${mfaPercentage}% of enabled users have MFA configured. ${phishResistantPercentage}% use phish-resistant authentication methods.`, + nodes: nodes, + }; +}; + const Page = () => { const settings = useSettings(); const { currentTenant } = settings; @@ -127,11 +332,11 @@ const Page = () => { DeviceCount: testsApi.data.TenantCounts.Devices || 0, ManagedDeviceCount: testsApi.data.TenantCounts.ManagedDevices || 0, }, - OverviewCaMfaAllUsers: dashboardDemoData.TenantInfo.OverviewCaMfaAllUsers, + OverviewCaMfaAllUsers: processMFAStateData(testsApi.data.MFAState), OverviewCaDevicesAllUsers: dashboardDemoData.TenantInfo.OverviewCaDevicesAllUsers, OverviewAuthMethodsPrivilegedUsers: dashboardDemoData.TenantInfo.OverviewAuthMethodsPrivilegedUsers, - OverviewAuthMethodsAllUsers: dashboardDemoData.TenantInfo.OverviewAuthMethodsAllUsers, + OverviewAuthMethodsAllUsers: processAuthMethodsData(testsApi.data.MFAState), DeviceOverview: dashboardDemoData.TenantInfo.DeviceOverview, }, } @@ -269,11 +474,13 @@ const Page = () => { Name - - {organization.isFetching - ? "Loading..." - : organization.data?.displayName || "Not Available"} - + {organization.isFetching ? ( + + ) : ( + + {organization.data?.displayName || "Not Available"} + + )} @@ -281,9 +488,7 @@ const Page = () => { {organization.isFetching ? ( - - Loading... - + ) : organization.data?.id ? ( ) : ( @@ -299,9 +504,7 @@ const Page = () => { {organization.isFetching ? ( - - Loading... - + ) : organization.data?.verifiedDomains?.find((d) => d.isDefault)?.name || currentTenant ? ( { /> - {reportData.SecureScore && reportData.SecureScore.length > 0 ? ( + {testsApi.isFetching ? ( + + + + ) : reportData.SecureScore && reportData.SecureScore.length > 0 ? ( { - {reportData.SecureScore && reportData.SecureScore.length > 0 ? ( + {testsApi.isFetching ? ( + + + + + + + + + + + + + + ) : reportData.SecureScore && reportData.SecureScore.length > 0 ? ( @@ -831,10 +1052,25 @@ const Page = () => { /> - {reportData.TenantInfo.OverviewAuthMethodsAllUsers?.nodes && ( + {testsApi.isFetching ? ( + + ) : reportData.TenantInfo.OverviewAuthMethodsAllUsers?.nodes ? ( + ) : ( + + + No authentication method data available + + )} @@ -862,8 +1098,23 @@ const Page = () => { /> - {reportData.TenantInfo.OverviewCaMfaAllUsers?.nodes && ( + {testsApi.isFetching ? ( + + ) : reportData.TenantInfo.OverviewCaMfaAllUsers?.nodes ? ( + ) : ( + + + No MFA data available + + )} @@ -886,10 +1137,25 @@ const Page = () => { /> - {reportData.TenantInfo.OverviewCaDevicesAllUsers?.nodes && ( + {testsApi.isFetching ? ( + + ) : reportData.TenantInfo.OverviewCaDevicesAllUsers?.nodes ? ( + ) : ( + + + No device sign-in data available + + )} @@ -919,96 +1185,116 @@ const Page = () => { sx={{ pb: 1 }} /> - - - - - - - - - - + {testsApi.isFetching ? ( + + ) : ( + + + + + + + + + + + )} - - - - Desktops - - - {Math.round( - ((reportData.TenantInfo.DeviceOverview.ManagedDevices.desktopCount || 0) / - (reportData.TenantInfo.DeviceOverview.ManagedDevices.totalCount || 1)) * - 100 - )} - % - + {testsApi.isFetching ? ( + + + + + + + + - - - - Mobiles - - - {Math.round( - ((reportData.TenantInfo.DeviceOverview.ManagedDevices.mobileCount || 0) / - (reportData.TenantInfo.DeviceOverview.ManagedDevices.totalCount || 1)) * - 100 - )} - % - + ) : ( + + + + Desktops + + + {Math.round( + ((reportData.TenantInfo.DeviceOverview?.ManagedDevices?.desktopCount || + 0) / + (reportData.TenantInfo.DeviceOverview?.ManagedDevices?.totalCount || + 1)) * + 100 + )} + % + + + + + + Mobiles + + + {Math.round( + ((reportData.TenantInfo.DeviceOverview?.ManagedDevices?.mobileCount || + 0) / + (reportData.TenantInfo.DeviceOverview?.ManagedDevices?.totalCount || + 1)) * + 100 + )} + % + + - + )} @@ -1026,86 +1312,104 @@ const Page = () => { sx={{ pb: 1 }} /> - - - - - - + {testsApi.isFetching ? ( + + ) : ( + + + + + + + )} - - - - - - Compliant - + {testsApi.isFetching ? ( + + + + + + + - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .compliantDeviceCount / - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .compliantDeviceCount + - reportData.TenantInfo.DeviceOverview.DeviceCompliance - .nonCompliantDeviceCount)) * - 100 - )} - % - - - - - - - Non-compliant + ) : ( + + + + + + Compliant + + + + {(() => { + const compliant = + reportData.TenantInfo.DeviceOverview?.DeviceCompliance + ?.compliantDeviceCount || 0; + const nonCompliant = + reportData.TenantInfo.DeviceOverview?.DeviceCompliance + ?.nonCompliantDeviceCount || 0; + const total = compliant + nonCompliant; + return total > 0 ? Math.round((compliant / total) * 100) : 0; + })()} + % + + + + + + + + Non-compliant + + + + {(() => { + const compliant = + reportData.TenantInfo.DeviceOverview?.DeviceCompliance + ?.compliantDeviceCount || 0; + const nonCompliant = + reportData.TenantInfo.DeviceOverview?.DeviceCompliance + ?.nonCompliantDeviceCount || 0; + const total = compliant + nonCompliant; + return total > 0 ? Math.round((nonCompliant / total) * 100) : 0; + })()} + % - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .nonCompliantDeviceCount / - (reportData.TenantInfo.DeviceOverview.DeviceCompliance - .compliantDeviceCount + - reportData.TenantInfo.DeviceOverview.DeviceCompliance - .nonCompliantDeviceCount)) * - 100 - )} - % - - + )} @@ -1123,78 +1427,104 @@ const Page = () => { sx={{ pb: 1 }} /> - - - - - - + {testsApi.isFetching ? ( + + ) : ( + + + + + + + )} - - - - - - Corporate - + {testsApi.isFetching ? ( + + + + + + + - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.corporateCount / - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.corporateCount + - reportData.TenantInfo.DeviceOverview.DeviceOwnership.personalCount)) * - 100 - )} - % - - - - - - - Personal + ) : ( + + + + + + Corporate + + + + {(() => { + const corporate = + reportData.TenantInfo.DeviceOverview?.DeviceOwnership + ?.corporateCount || 0; + const personal = + reportData.TenantInfo.DeviceOverview?.DeviceOwnership + ?.personalCount || 0; + const total = corporate + personal; + return total > 0 ? Math.round((corporate / total) * 100) : 0; + })()} + % + + + + + + + + Personal + + + + {(() => { + const corporate = + reportData.TenantInfo.DeviceOverview?.DeviceOwnership + ?.corporateCount || 0; + const personal = + reportData.TenantInfo.DeviceOverview?.DeviceOwnership + ?.personalCount || 0; + const total = corporate + personal; + return total > 0 ? Math.round((personal / total) * 100) : 0; + })()} + % - - {Math.round( - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.personalCount / - (reportData.TenantInfo.DeviceOverview.DeviceOwnership.corporateCount + - reportData.TenantInfo.DeviceOverview.DeviceOwnership.personalCount)) * - 100 - )} - % - - + )} @@ -1213,10 +1543,25 @@ const Page = () => { /> - {reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes && ( + {testsApi.isFetching ? ( + + ) : reportData.TenantInfo.DeviceOverview?.DesktopDevicesSummary?.nodes ? ( + ) : ( + + + No desktop device data available + + )} @@ -1226,82 +1571,101 @@ const Page = () => { - - - - Entra joined - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes || []; - const entraJoined = - nodes.find((n) => n.target === "Entra joined")?.value || 0; - const windowsDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "Windows" - )?.value || 0; - const macOSDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "macOS" - )?.value || 0; - const total = windowsDevices + macOSDevices; - return Math.round((entraJoined / (total || 1)) * 100); - })()} - % - - - - - - Entra hybrid joined - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes || []; - const entraHybrid = - nodes.find((n) => n.target === "Entra hybrid joined")?.value || 0; - const windowsDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "Windows" - )?.value || 0; - const macOSDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "macOS" - )?.value || 0; - const total = windowsDevices + macOSDevices; - return Math.round((entraHybrid / (total || 1)) * 100); - })()} - % - + {testsApi.isFetching ? ( + + + + + + + + + + + + - - - - Entra registered - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.nodes || []; - const entraRegistered = - nodes.find((n) => n.target === "Entra registered")?.value || 0; - const windowsDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "Windows" - )?.value || 0; - const macOSDevices = - nodes.find( - (n) => n.source === "Desktop devices" && n.target === "macOS" - )?.value || 0; - const total = windowsDevices + macOSDevices; - return Math.round((entraRegistered / (total || 1)) * 100); - })()} - % - + ) : ( + + + + Entra joined + + + {(() => { + const nodes = + reportData.TenantInfo.DeviceOverview?.DesktopDevicesSummary?.nodes || + []; + const entraJoined = + nodes.find((n) => n.target === "Entra joined")?.value || 0; + const windowsDevices = + nodes.find( + (n) => n.source === "Desktop devices" && n.target === "Windows" + )?.value || 0; + const macOSDevices = + nodes.find( + (n) => n.source === "Desktop devices" && n.target === "macOS" + )?.value || 0; + const total = windowsDevices + macOSDevices; + return Math.round((entraJoined / (total || 1)) * 100); + })()} + % + + + + + + Entra hybrid joined + + + {(() => { + const nodes = + reportData.TenantInfo.DeviceOverview?.DesktopDevicesSummary?.nodes || + []; + const entraHybrid = + nodes.find((n) => n.target === "Entra hybrid joined")?.value || 0; + const windowsDevices = + nodes.find( + (n) => n.source === "Desktop devices" && n.target === "Windows" + )?.value || 0; + const macOSDevices = + nodes.find( + (n) => n.source === "Desktop devices" && n.target === "macOS" + )?.value || 0; + const total = windowsDevices + macOSDevices; + return Math.round((entraHybrid / (total || 1)) * 100); + })()} + % + + + + + + Entra registered + + + {(() => { + const nodes = + reportData.TenantInfo.DeviceOverview?.DesktopDevicesSummary?.nodes || + []; + const entraRegistered = + nodes.find((n) => n.target === "Entra registered")?.value || 0; + const windowsDevices = + nodes.find( + (n) => n.source === "Desktop devices" && n.target === "Windows" + )?.value || 0; + const macOSDevices = + nodes.find( + (n) => n.source === "Desktop devices" && n.target === "macOS" + )?.value || 0; + const total = windowsDevices + macOSDevices; + return Math.round((entraRegistered / (total || 1)) * 100); + })()} + % + + - + )} @@ -1320,10 +1684,25 @@ const Page = () => { /> - {reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes && ( + {testsApi.isFetching ? ( + + ) : reportData.TenantInfo.DeviceOverview?.MobileSummary?.nodes ? ( + ) : ( + + + No mobile device data available + + )} @@ -1333,72 +1712,88 @@ const Page = () => { - - - - Android compliant - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes || []; - const androidCompliant = nodes - .filter( - (n) => n.source?.includes("Android") && n.target === "Compliant" - ) - .reduce((sum, n) => sum + (n.value || 0), 0); - const androidTotal = - nodes.find( - (n) => n.source === "Mobile devices" && n.target === "Android" - )?.value || 0; - return androidTotal > 0 - ? Math.round((androidCompliant / androidTotal) * 100) - : 0; - })()} - % - - - - - - iOS compliant - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes || []; - const iosCompliant = nodes - .filter((n) => n.source?.includes("iOS") && n.target === "Compliant") - .reduce((sum, n) => sum + (n.value || 0), 0); - const iosTotal = - nodes.find((n) => n.source === "Mobile devices" && n.target === "iOS") - ?.value || 0; - return iosTotal > 0 ? Math.round((iosCompliant / iosTotal) * 100) : 0; - })()} - % - + {testsApi.isFetching ? ( + + + + + + + + + + + + - - - - Total devices - - - {(() => { - const nodes = - reportData.TenantInfo.DeviceOverview.MobileSummary?.nodes || []; - const androidTotal = - nodes.find( - (n) => n.source === "Mobile devices" && n.target === "Android" - )?.value || 0; - const iosTotal = - nodes.find((n) => n.source === "Mobile devices" && n.target === "iOS") - ?.value || 0; - return androidTotal + iosTotal; - })()} - + ) : ( + + + + Android compliant + + + {(() => { + const nodes = + reportData.TenantInfo.DeviceOverview?.MobileSummary?.nodes || []; + const androidCompliant = nodes + .filter( + (n) => n.source?.includes("Android") && n.target === "Compliant" + ) + .reduce((sum, n) => sum + (n.value || 0), 0); + const androidTotal = + nodes.find( + (n) => n.source === "Mobile devices" && n.target === "Android" + )?.value || 0; + return androidTotal > 0 + ? Math.round((androidCompliant / androidTotal) * 100) + : 0; + })()} + % + + + + + + iOS compliant + + + {(() => { + const nodes = + reportData.TenantInfo.DeviceOverview?.MobileSummary?.nodes || []; + const iosCompliant = nodes + .filter((n) => n.source?.includes("iOS") && n.target === "Compliant") + .reduce((sum, n) => sum + (n.value || 0), 0); + const iosTotal = + nodes.find((n) => n.source === "Mobile devices" && n.target === "iOS") + ?.value || 0; + return iosTotal > 0 ? Math.round((iosCompliant / iosTotal) * 100) : 0; + })()} + % + + + + + + Total devices + + + {(() => { + const nodes = + reportData.TenantInfo.DeviceOverview?.MobileSummary?.nodes || []; + const androidTotal = + nodes.find( + (n) => n.source === "Mobile devices" && n.target === "Android" + )?.value || 0; + const iosTotal = + nodes.find((n) => n.source === "Mobile devices" && n.target === "iOS") + ?.value || 0; + return androidTotal + iosTotal; + })()} + + - + )}