From 3729bce938e9368c0a447a6584a3725477045cfc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 21 Dec 2025 00:23:30 +0100 Subject: [PATCH 001/111] small updates --- src/pages/dashboardv2/index.js | 272 ++++++++++++++++++++++++++++++--- 1 file changed, 252 insertions(+), 20 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 3b29d86dcaac..1a5bbde6cedf 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -8,8 +8,17 @@ import { Avatar, Divider, Tooltip, + Autocomplete, + TextField, + Button, } from "@mui/material"; +import { useState, useEffect } from "react"; import { Grid } from "@mui/system"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall.jsx"; +import Portals from "/src/data/portals"; +import { BulkActionsMenu } from "/src/components/bulk-actions-menu.js"; +import { ExecutiveReportButton } from "/src/components/ExecutiveReportButton.js"; import { BarChart, Bar, @@ -34,6 +43,8 @@ import { CaDeviceSankey } from "/src/components/CippComponents/CaDeviceSankey"; import { AuthMethodSankey } from "/src/components/CippComponents/AuthMethodSankey"; import { DesktopDevicesSankey } from "/src/components/CippComponents/DesktopDevicesSankey"; import { MobileSankey } from "/src/components/CippComponents/MobileSankey"; +import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; +import { CippCopyToClipBoard } from "/src/components/CippComponents/CippCopyToClipboard.jsx"; import { People as UsersIcon, Person as UserIcon, @@ -51,6 +62,72 @@ import { const Page = () => { const reportData = dashboardDemoData; + const settings = useSettings(); + const { currentTenant } = settings; + const [portalMenuItems, setPortalMenuItems] = useState([]); + const [selectedReport, setSelectedReport] = useState(null); + + const reportOptions = [ + "Select a report", + "Executive Summary Report", + "Security Assessment Report", + "Compliance Report", + "Device Inventory Report", + ]; + + const organization = ApiGetCall({ + url: "/api/ListOrg", + queryKey: `${currentTenant}-ListOrg`, + data: { tenantFilter: currentTenant }, + }); + + const dashboard = ApiGetCall({ + url: "/api/ListuserCounts", + data: { tenantFilter: currentTenant }, + queryKey: `${currentTenant}-ListuserCounts`, + }); + + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + TenantFilter: currentTenant, + }, + queryKey: `TenantDrift-${currentTenant}`, + }); + + const currentTenantInfo = ApiGetCall({ + url: "/api/ListTenants", + queryKey: `ListTenants`, + }); + + useEffect(() => { + if (currentTenantInfo.isSuccess) { + const menuItems = Portals.map((portal) => ({ + label: portal.label, + link: portal.url + .replace( + "%%tenantid%%", + currentTenantInfo.data + ?.find((tenant) => tenant.defaultDomainName === currentTenant) + ?.customerId?.toLowerCase() + ) + .replace( + "%%customername%%", + currentTenantInfo.data?.find((tenant) => tenant.defaultDomainName === currentTenant) + ?.displayName + ), + external: portal.external, + target: settings.UserSpecificSettings?.portalLinks || portal.target, + icon: portal.icon, + })); + setPortalMenuItems(menuItems); + } + }, [ + currentTenantInfo.isSuccess, + currentTenant, + settings.portalLinks, + settings.UserSpecificSettings, + ]); const formatNumber = (num) => { if (!num && num !== 0) return "0"; @@ -72,6 +149,72 @@ const Page = () => { return ( + {/* Dashboard Bar with Portals, Executive Report, and Universal Search */} + + + + + + + + + + + + + + + + setSelectedReport(newValue)} + sx={{ flex: 1 }} + renderInput={(params) => ( + + )} + /> + + + + + + + {/* Tenant Overview Section - 3 Column Layout */} {/* Column 1: Tenant Information */} @@ -93,24 +236,53 @@ const Page = () => { Name - {reportData.TenantName || "Not Available"} + {organization.isFetching + ? "Loading..." + : organization.data?.displayName || "Not Available"} Tenant ID - - {reportData.TenantId || "Not Available"} - + + {organization.isFetching ? ( + + Loading... + + ) : organization.data?.id ? ( + + ) : ( + + Not Available + + )} + Primary Domain - - {reportData.Domain || "Not Available"} - + + {organization.isFetching ? ( + + Loading... + + ) : organization.data?.verifiedDomains?.find((d) => d.isDefault)?.name || + currentTenant ? ( + d.isDefault)?.name || + currentTenant + } + type="chip" + /> + ) : ( + + Not Available + + )} + @@ -133,8 +305,15 @@ const Page = () => { borderRadius: 1, }} > - - + + @@ -160,8 +339,15 @@ const Page = () => { borderRadius: 1, }} > - - + + @@ -187,8 +373,15 @@ const Page = () => { borderRadius: 1, }} > - - + + @@ -214,8 +407,15 @@ const Page = () => { borderRadius: 1, }} > - - + + @@ -241,8 +441,15 @@ const Page = () => { borderRadius: 1, }} > - - + + @@ -268,8 +475,15 @@ const Page = () => { borderRadius: 1, }} > - - + + @@ -317,7 +531,7 @@ const Page = () => { - + Devices @@ -334,6 +548,24 @@ const Page = () => { + + + Last Data Collection + + + {currentTenantInfo.isFetching + ? "Loading..." + : currentTenantInfo.data?.find( + (t) => t.defaultDomainName === currentTenant + )?.LastRefresh + ? new Date( + currentTenantInfo.data?.find( + (t) => t.defaultDomainName === currentTenant + )?.LastRefresh + ).toLocaleString() + : "Not Available"} + + From 08c8326e48e18c5b706088db2de5141b73744b53 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 00:29:28 +0100 Subject: [PATCH 002/111] dashv2 updates --- src/pages/dashboardv2/index.js | 195 +++++++++++++++++++++++---------- 1 file changed, 138 insertions(+), 57 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 1a5bbde6cedf..cfdc502fd868 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -11,6 +11,7 @@ import { Autocomplete, TextField, Button, + Skeleton, } from "@mui/material"; import { useState, useEffect } from "react"; import { Grid } from "@mui/system"; @@ -45,6 +46,7 @@ import { DesktopDevicesSankey } from "/src/components/CippComponents/DesktopDevi import { MobileSankey } from "/src/components/CippComponents/MobileSankey"; import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; import { CippCopyToClipBoard } from "/src/components/CippComponents/CippCopyToClipboard.jsx"; +import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo.jsx"; import { People as UsersIcon, Person as UserIcon, @@ -61,7 +63,6 @@ import { } from "@mui/icons-material"; const Page = () => { - const reportData = dashboardDemoData; const settings = useSettings(); const { currentTenant } = settings; const [portalMenuItems, setPortalMenuItems] = useState([]); @@ -81,10 +82,10 @@ const Page = () => { data: { tenantFilter: currentTenant }, }); - const dashboard = ApiGetCall({ - url: "/api/ListuserCounts", + const testsApi = ApiGetCall({ + url: "/api/ListTests", data: { tenantFilter: currentTenant }, - queryKey: `${currentTenant}-ListuserCounts`, + queryKey: `${currentTenant}-ListTests`, }); const driftApi = ApiGetCall({ @@ -100,6 +101,39 @@ const Page = () => { queryKey: `ListTenants`, }); + const reportData = + testsApi.isSuccess && testsApi.data?.TenantCounts + ? { + ExecutedAt: testsApi.data.LatestReportTimeStamp || new Date().toISOString(), + TenantName: organization.data?.displayName || "", + Domain: currentTenant || "", + TestResultSummary: { + IdentityPassed: testsApi.data.TestCounts?.Successful || 0, + IdentityTotal: testsApi.data.TestCounts?.Total || 0, + DevicesPassed: testsApi.data.TestCounts?.Successful || 0, + DevicesTotal: testsApi.data.TestCounts?.Total || 0, + DataPassed: 0, + DataTotal: 0, + }, + TenantInfo: { + TenantOverview: { + UserCount: testsApi.data.TenantCounts.Users || 0, + GuestCount: testsApi.data.TenantCounts.Guests || 0, + GroupCount: testsApi.data.TenantCounts.Groups || 0, + ApplicationCount: testsApi.data.TenantCounts.ServicePrincipals || 0, + DeviceCount: testsApi.data.TenantCounts.Devices || 0, + ManagedDeviceCount: testsApi.data.TenantCounts.ManagedDevices || 0, + }, + OverviewCaMfaAllUsers: dashboardDemoData.TenantInfo.OverviewCaMfaAllUsers, + OverviewCaDevicesAllUsers: dashboardDemoData.TenantInfo.OverviewCaDevicesAllUsers, + OverviewAuthMethodsPrivilegedUsers: + dashboardDemoData.TenantInfo.OverviewAuthMethodsPrivilegedUsers, + OverviewAuthMethodsAllUsers: dashboardDemoData.TenantInfo.OverviewAuthMethodsAllUsers, + DeviceOverview: dashboardDemoData.TenantInfo.DeviceOverview, + }, + } + : dashboardDemoData; + useEffect(() => { if (currentTenantInfo.isSuccess) { const menuItems = Portals.map((portal) => ({ @@ -141,7 +175,7 @@ const Page = () => { users: "Total number of users in your tenant", guests: "External users with guest access", groups: "Microsoft 365 and security groups", - apps: "Registered applications", + apps: "Service principals in your tenant", devices: "All devices accessing tenant resources", managed: "Devices enrolled in Intune", }; @@ -163,17 +197,14 @@ const Page = () => { tenantName={organization.data?.displayName} tenantId={organization.data?.id} userStats={{ - licensedUsers: dashboard.data?.LicUsers || 0, - unlicensedUsers: - dashboard.data?.Users && dashboard.data?.LicUsers - ? dashboard.data?.Users - dashboard.data?.LicUsers - : 0, - guests: dashboard.data?.Guests || 0, - globalAdmins: dashboard.data?.Gas || 0, + licensedUsers: 0, + unlicensedUsers: 0, + guests: testsApi.data?.TenantCounts?.Guests || 0, + globalAdmins: 0, }} standardsData={driftApi.data} organizationData={organization.data} - disabled={organization.isFetching || dashboard.isFetching} + disabled={organization.isFetching || testsApi.isFetching} /> @@ -293,7 +324,10 @@ const Page = () => { - + { Users - {formatNumber(reportData.TenantInfo.TenantOverview.UserCount)} + {testsApi.isFetching ? ( + + ) : ( + formatNumber(reportData.TenantInfo.TenantOverview.UserCount) + )} - + { Guests - {formatNumber(reportData.TenantInfo.TenantOverview.GuestCount)} + {testsApi.isFetching ? ( + + ) : ( + formatNumber(reportData.TenantInfo.TenantOverview.GuestCount) + )} - + { Groups - {formatNumber(reportData.TenantInfo.TenantOverview.GroupCount)} + {testsApi.isFetching ? ( + + ) : ( + formatNumber(reportData.TenantInfo.TenantOverview.GroupCount) + )} - + { - Apps + Service Principals - {formatNumber(reportData.TenantInfo.TenantOverview.ApplicationCount)} + {testsApi.isFetching ? ( + + ) : ( + formatNumber(reportData.TenantInfo.TenantOverview.ApplicationCount) + )} - + { Devices - {formatNumber(reportData.TenantInfo.TenantOverview.DeviceCount)} + {testsApi.isFetching ? ( + + ) : ( + formatNumber(reportData.TenantInfo.TenantOverview.DeviceCount) + )} - + { Managed - {formatNumber(reportData.TenantInfo.TenantOverview.ManagedDeviceCount)} + {testsApi.isFetching ? ( + + ) : ( + formatNumber(reportData.TenantInfo.TenantOverview.ManagedDeviceCount) + )} @@ -519,16 +592,22 @@ const Page = () => { Identity - {reportData.TestResultSummary.IdentityPassed}/ - {reportData.TestResultSummary.IdentityTotal} - - tests - + {testsApi.isFetching ? ( + + ) : ( + <> + {reportData.TestResultSummary.IdentityPassed}/ + {reportData.TestResultSummary.IdentityTotal} + + tests + + + )} @@ -536,16 +615,22 @@ const Page = () => { Devices - {reportData.TestResultSummary.DevicesPassed}/ - {reportData.TestResultSummary.DevicesTotal} - - tests - + {testsApi.isFetching ? ( + + ) : ( + <> + {reportData.TestResultSummary.DevicesPassed}/ + {reportData.TestResultSummary.DevicesTotal} + + tests + + + )} @@ -553,17 +638,13 @@ const Page = () => { Last Data Collection - {currentTenantInfo.isFetching - ? "Loading..." - : currentTenantInfo.data?.find( - (t) => t.defaultDomainName === currentTenant - )?.LastRefresh - ? new Date( - currentTenantInfo.data?.find( - (t) => t.defaultDomainName === currentTenant - )?.LastRefresh - ).toLocaleString() - : "Not Available"} + {testsApi.isFetching ? ( + + ) : testsApi.data?.LatestReportTimeStamp ? ( + + ) : ( + "Not Available" + )} From cf9da22f914d4151041e9acc6bd74a0c60c4a8bc Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 01:42:16 +0100 Subject: [PATCH 003/111] reporting template tests --- src/pages/dashboardv2/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index cfdc502fd868..3eb6718fa40c 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -84,8 +84,8 @@ const Page = () => { const testsApi = ApiGetCall({ url: "/api/ListTests", - data: { tenantFilter: currentTenant }, - queryKey: `${currentTenant}-ListTests`, + data: { tenantFilter: currentTenant, reportId: "d5d1e123-bce0-482d-971f-be6ed820dd92" }, + queryKey: `${currentTenant}-ListTests-d5d1e123-bce0-482d-971f-be6ed820dd92`, }); const driftApi = ApiGetCall({ From 8159090843dc7bfab712e1fc4e2f2d8686d67ffb Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:13:20 +0100 Subject: [PATCH 004/111] reporting updates --- src/pages/dashboardv2/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 3eb6718fa40c..654f8d8370f1 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -108,10 +108,10 @@ const Page = () => { TenantName: organization.data?.displayName || "", Domain: currentTenant || "", TestResultSummary: { - IdentityPassed: testsApi.data.TestCounts?.Successful || 0, - IdentityTotal: testsApi.data.TestCounts?.Total || 0, - DevicesPassed: testsApi.data.TestCounts?.Successful || 0, - DevicesTotal: testsApi.data.TestCounts?.Total || 0, + IdentityPassed: testsApi.data.TestCounts?.Identity?.Passed || 0, + IdentityTotal: testsApi.data.TestCounts?.Identity?.Total || 0, + DevicesPassed: testsApi.data.TestCounts?.Devices?.Passed || 0, + DevicesTotal: testsApi.data.TestCounts?.Devices?.Total || 0, DataPassed: 0, DataTotal: 0, }, From b0d7b6dd02d7d83479dc164a8ccc6b2f66f38493 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 22 Dec 2025 14:41:30 +0100 Subject: [PATCH 005/111] Reporting --- src/components/CippTable/CippDataTable.js | 16 ++ src/pages/dashboardv2/devices/index.js | 329 ++++++++++++++++++++-- src/pages/dashboardv2/identity/index.js | 329 ++++++++++++++++++++-- src/pages/dashboardv2/index.js | 1 - 4 files changed, 638 insertions(+), 37 deletions(-) diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 5a1590ac1fde..7322168d728e 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -97,6 +97,7 @@ export const CippDataTable = (props) => { simple = false, cardButton, offCanvas = false, + offCanvasOnRowClick = false, noCard = false, hideTitle = false, refreshFunction, @@ -286,6 +287,21 @@ export const CippDataTable = (props) => { top: table.getState().isFullScreen ? 64 : undefined, }, }), + muiTableBodyRowProps: + offCanvasOnRowClick && offCanvas + ? ({ row }) => ({ + onClick: () => { + setOffCanvasData(row.original); + setOffcanvasVisible(true); + }, + sx: { + cursor: "pointer", + "&:hover": { + backgroundColor: "action.hover", + }, + }, + }) + : undefined, // Add global styles to target the specific filter components enableColumnFilterModes: true, muiTableHeadCellProps: { diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index a4c96d300f0d..13de6730b648 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -1,28 +1,321 @@ -import { Container, Typography, Card, CardContent, CardHeader, Box } from "@mui/material"; +import { + Container, + Typography, + Card, + CardContent, + CardHeader, + Box, + Stack, + Chip, +} from "@mui/material"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall.jsx"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import ReactMarkdown from "react-markdown"; +import { Grid } from "@mui/system"; const Page = () => { + const settings = useSettings(); + const { currentTenant } = settings; + + const testsApi = ApiGetCall({ + url: "/api/ListTests", + data: { tenantFilter: currentTenant, reportId: "d5d1e123-bce0-482d-971f-be6ed820dd92" }, + queryKey: `${currentTenant}-ListTests-d5d1e123-bce0-482d-971f-be6ed820dd92`, + }); + + const deviceTests = + testsApi.data?.TestResults?.filter((test) => test.TestType === "Devices") || []; + + const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case "passed": + return "success"; + case "failed": + return "error"; + case "investigate": + return "warning"; + case "skipped": + return "default"; + default: + return "default"; + } + }; + + const getRiskColor = (risk) => { + switch (risk?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } + }; + + const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } + }; + + const offCanvas = { + extendedInfoFields: ["Name", "Risk", "Status", "Category"], + size: "lg", + children: (row) => { + return ( + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + Risk Level + + + + + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + User Impact + + + + + + + + + + + + Implementation Effort + + + + + + + + + + + {row.ResultMarkdown && ( + + + + {row.Name} + + + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + "& p": { + my: 1, + }, + "& ul": { + my: 1, + pl: 2, + }, + "& li": { + my: 0.5, + }, + "& h1, & h2, & h3, & h4, & h5, & h6": { + mt: 2, + mb: 1, + fontWeight: "bold", + }, + "& code": { + backgroundColor: "action.hover", + padding: "2px 6px", + borderRadius: 1, + fontSize: "0.85em", + }, + "& pre": { + backgroundColor: "action.hover", + padding: 2, + borderRadius: 1, + overflow: "auto", + }, + }} + > + ( + + {children} + + ), + }} + > + {row.ResultMarkdown} + + + + + )} + + + + + + What did we check + + + {row.Category && ( + + + Category + + {row.Category} + + )} + + + + This test verifies that device compliance policies are properly configured in + Microsoft Intune. Compliance policies define the requirements that devices must + meet to access corporate resources, such as encryption, password requirements, + and operating system versions. + + + Why this matters: Non-compliant devices pose significant + security risks to your organization. They may lack critical security updates, + have weak authentication, or be missing essential security features like + encryption. Properly configured compliance policies ensure only secure devices + can access sensitive data. + + + Recommendation: Create comprehensive compliance policies that + cover all device platforms (Windows, iOS, Android, macOS). Configure Conditional + Access to block non-compliant devices and set up automated remediation actions + to help users bring their devices back into compliance. + + + + + + + ); + }, + }; + + const filters = [ + { + filterName: "Passed", + value: [{ id: "Status", value: "Passed" }], + type: "column", + }, + { + filterName: "Failed", + value: [{ id: "Status", value: "Failed" }], + type: "column", + }, + { + filterName: "Investigate", + value: [{ id: "Status", value: "Investigate" }], + type: "column", + }, + { + filterName: "Skipped", + value: [{ id: "Status", value: "Skipped" }], + type: "column", + }, + { + filterName: "High Risk", + value: [{ id: "Risk", value: "High" }], + type: "column", + }, + { + filterName: "Medium Risk", + value: [{ id: "Risk", value: "Medium" }], + type: "column", + }, + { + filterName: "Low Risk", + value: [{ id: "Risk", value: "Low" }], + type: "column", + }, + ]; + return ( - - - - - - Device Test Results - - - This tab will display detailed device test results and recommendations. - - - Review device compliance policies, enrollment restrictions, and management - configurations to enhance your device security posture. - - - - + ); }; diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index 78b76b13e131..81870c3172b8 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -1,28 +1,321 @@ -import { Container, Typography, Card, CardContent, CardHeader, Box } from "@mui/material"; +import { + Container, + Typography, + Card, + CardContent, + CardHeader, + Box, + Stack, + Chip, +} from "@mui/material"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; +import { useSettings } from "/src/hooks/use-settings"; +import { ApiGetCall } from "/src/api/ApiCall.jsx"; +import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { Info } from "@mui/icons-material"; +import ReactMarkdown from "react-markdown"; +import { Grid } from "@mui/system"; const Page = () => { + const settings = useSettings(); + const { currentTenant } = settings; + + const testsApi = ApiGetCall({ + url: "/api/ListTests", + data: { tenantFilter: currentTenant, reportId: "d5d1e123-bce0-482d-971f-be6ed820dd92" }, + queryKey: `${currentTenant}-ListTests-d5d1e123-bce0-482d-971f-be6ed820dd92`, + }); + + const identityTests = + testsApi.data?.TestResults?.filter((test) => test.TestType === "Identity") || []; + + const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case "passed": + return "success"; + case "failed": + return "error"; + case "investigate": + return "warning"; + case "skipped": + return "default"; + default: + return "default"; + } + }; + + const getRiskColor = (risk) => { + switch (risk?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } + }; + + const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } + }; + + const offCanvas = { + size: "lg", + children: (row) => { + return ( + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + Risk Level + + + + + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + User Impact + + + + + + + + + + + + Implementation Effort + + + + + + + + + + + {row.ResultMarkdown && ( + + + + {row.Name} + + + theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + "& p": { + my: 1, + }, + "& ul": { + my: 1, + pl: 2, + }, + "& li": { + my: 0.5, + }, + "& h1, & h2, & h3, & h4, & h5, & h6": { + mt: 2, + mb: 1, + fontWeight: "bold", + }, + "& code": { + backgroundColor: "action.hover", + padding: "2px 6px", + borderRadius: 1, + fontSize: "0.85em", + }, + "& pre": { + backgroundColor: "action.hover", + padding: 2, + borderRadius: 1, + overflow: "auto", + }, + }} + > + ( + + {children} + + ), + }} + > + {row.ResultMarkdown} + + + + + )} + + + + + + What did we check + + + {row.Category && ( + + + Category + + {row.Category} + + )} + + + + This test verifies that Multi-Factor Authentication (MFA) is enabled for all + administrative accounts in your Microsoft 365 tenant. Administrative accounts + have elevated privileges and are prime targets for attackers. Enabling MFA adds + an additional layer of security by requiring a second form of verification + beyond just a password. + + + Why this matters: According to Microsoft, MFA can block over + 99.9% of account compromise attacks. Without MFA, compromised administrator + credentials can lead to complete tenant takeover, data breaches, and significant + business disruption. + + + Recommendation: Ensure all users with administrative roles have + MFA enabled through Conditional Access policies or per-user MFA settings. + Consider using stronger authentication methods like FIDO2 security keys or the + Microsoft Authenticator app for your most privileged accounts. + + + + + + + ); + }, + }; + + const filters = [ + { + filterName: "Passed", + value: [{ id: "Status", value: "Passed" }], + type: "column", + }, + { + filterName: "Failed", + value: [{ id: "Status", value: "Failed" }], + type: "column", + }, + { + filterName: "Investigate", + value: [{ id: "Status", value: "Investigate" }], + type: "column", + }, + { + filterName: "Skipped", + value: [{ id: "Status", value: "Skipped" }], + type: "column", + }, + { + filterName: "High Risk", + value: [{ id: "Risk", value: "High" }], + type: "column", + }, + { + filterName: "Medium Risk", + value: [{ id: "Risk", value: "Medium" }], + type: "column", + }, + { + filterName: "Low Risk", + value: [{ id: "Risk", value: "Low" }], + type: "column", + }, + ]; + return ( - - - - - - Identity Test Results - - - This tab will display detailed identity test results and recommendations. - - - Configure your identity policies and authentication methods to improve your zero trust - posture. - - - - + ); }; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 654f8d8370f1..a74e4be1cf39 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -25,7 +25,6 @@ import { Bar, PieChart, Pie, - Cell, RadialBarChart, RadialBar, PolarAngleAxis, From 7aa24227d04e8db6cfe95751f0bd4289c6dc8051 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 23 Dec 2025 00:46:49 +0100 Subject: [PATCH 006/111] Interface changes --- src/pages/dashboardv2/identity/index.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index 81870c3172b8..d31f48e770ba 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -14,9 +14,10 @@ import tabOptions from "../tabOptions"; import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall.jsx"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; -import { Info } from "@mui/icons-material"; +import { ArrowRight, Info, KeyboardArrowRight } from "@mui/icons-material"; import ReactMarkdown from "react-markdown"; import { Grid } from "@mui/system"; +import { ArrowLongRightIcon } from "@heroicons/react/24/outline"; const Page = () => { const settings = useSettings(); @@ -91,12 +92,12 @@ const Page = () => { }), }} > - + Risk Level - + @@ -114,7 +115,7 @@ const Page = () => { }), }} > - + User Impact @@ -135,7 +136,7 @@ const Page = () => { borderBottom: "none", }} > - + Implementation Effort @@ -157,7 +158,7 @@ const Page = () => { - {row.Name} + {row.Name} Date: Tue, 23 Dec 2025 12:01:37 +0100 Subject: [PATCH 007/111] add table support for tests --- package.json | 2 +- src/pages/dashboardv2/devices/index.js | 2 + src/pages/dashboardv2/identity/index.js | 2 + yarn.lock | 617 ++++++------------------ 4 files changed, 149 insertions(+), 474 deletions(-) diff --git a/package.json b/package.json index 6d01e9706b32..8edee7aa5549 100644 --- a/package.json +++ b/package.json @@ -106,7 +106,7 @@ "redux-persist": "^6.0.0", "redux-thunk": "3.1.0", "rehype-raw": "^7.0.0", - "remark-gfm": "^3.0.1", + "remark-gfm": "^4.0.0", "simplebar": "6.3.2", "simplebar-react": "3.3.2", "stylis-plugin-rtl": "2.1.1", diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index 13de6730b648..9d0dc4cbf6dd 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -15,6 +15,7 @@ import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall.jsx"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { Grid } from "@mui/system"; const Page = () => { @@ -206,6 +207,7 @@ const Page = () => { }} > ( diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index d31f48e770ba..6a0c74001b02 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -16,6 +16,7 @@ import { ApiGetCall } from "/src/api/ApiCall.jsx"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; import { ArrowRight, Info, KeyboardArrowRight } from "@mui/icons-material"; import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; import { Grid } from "@mui/system"; import { ArrowLongRightIcon } from "@heroicons/react/24/outline"; @@ -207,6 +208,7 @@ const Page = () => { }} > ( diff --git a/yarn.lock b/yarn.lock index e1d176d44d7a..5e07ec26f491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2466,13 +2466,6 @@ "@types/linkify-it" "^5" "@types/mdurl" "^2" -"@types/mdast@^3.0.0": - version "3.0.15" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" - integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== - dependencies: - "@types/unist" "^2" - "@types/mdast@^4.0.0": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-4.0.4.tgz#7ccf72edd2f1aa7dd3437e180c64373585804dd6" @@ -3678,11 +3671,6 @@ dfa@^1.2.0: resolved "https://registry.yarnpkg.com/dfa/-/dfa-1.2.0.tgz#96ac3204e2d29c49ea5b57af8d92c2ae12790657" integrity sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q== -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4929,11 +4917,6 @@ is-boolean-object@^1.2.1: call-bound "^1.0.3" has-tostringtag "^1.0.2" -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - is-bun-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-2.0.0.tgz#4d7859a87c0fcac950c95e666730e745eae8bddd" @@ -5273,11 +5256,6 @@ kind-of@^6.0.0, kind-of@^6.0.2: resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - language-subtag-registry@^0.3.20: version "0.3.23" resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" @@ -5436,33 +5414,15 @@ math-intrinsics@^1.1.0: resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== -mdast-util-find-and-replace@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" - integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== +mdast-util-find-and-replace@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz#70a3174c894e14df722abf43bc250cbae44b11df" + integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" escape-string-regexp "^5.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.0.0" - -mdast-util-from-markdown@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" + unist-util-is "^6.0.0" + unist-util-visit-parents "^6.0.0" mdast-util-from-markdown@^2.0.0: version "2.0.2" @@ -5482,63 +5442,70 @@ mdast-util-from-markdown@^2.0.0: micromark-util-types "^2.0.0" unist-util-stringify-position "^4.0.0" -mdast-util-gfm-autolink-literal@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" - integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== +mdast-util-gfm-autolink-literal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz#abd557630337bd30a6d5a4bd8252e1c2dc0875d5" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" ccount "^2.0.0" - mdast-util-find-and-replace "^2.0.0" - micromark-util-character "^1.0.0" + devlop "^1.0.0" + mdast-util-find-and-replace "^3.0.0" + micromark-util-character "^2.0.0" -mdast-util-gfm-footnote@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" - integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== +mdast-util-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz#7778e9d9ca3df7238cc2bd3fa2b1bf6a65b19403" + integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - micromark-util-normalize-identifier "^1.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.1.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" -mdast-util-gfm-strikethrough@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" - integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== +mdast-util-gfm-strikethrough@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz#d44ef9e8ed283ac8c1165ab0d0dfd058c2764c16" + integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" + "@types/mdast" "^4.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" - integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== +mdast-util-gfm-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz#7a435fb6223a72b0862b33afbd712b6dae878d38" + integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg== dependencies: - "@types/mdast" "^3.0.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" markdown-table "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.3.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm-task-list-item@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" - integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== +mdast-util-gfm-task-list-item@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz#e68095d2f8a4303ef24094ab642e1047b991a936" + integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" + "@types/mdast" "^4.0.0" + devlop "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-to-markdown "^2.0.0" -mdast-util-gfm@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" - integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== +mdast-util-gfm@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz#2cdf63b92c2a331406b0fb0db4c077c1b0331751" + integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ== dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-gfm-autolink-literal "^1.0.0" - mdast-util-gfm-footnote "^1.0.0" - mdast-util-gfm-strikethrough "^1.0.0" - mdast-util-gfm-table "^1.0.0" - mdast-util-gfm-task-list-item "^1.0.0" - mdast-util-to-markdown "^1.0.0" + mdast-util-from-markdown "^2.0.0" + mdast-util-gfm-autolink-literal "^2.0.0" + mdast-util-gfm-footnote "^2.0.0" + mdast-util-gfm-strikethrough "^2.0.0" + mdast-util-gfm-table "^2.0.0" + mdast-util-gfm-task-list-item "^2.0.0" + mdast-util-to-markdown "^2.0.0" mdast-util-mdx-expression@^2.0.0: version "2.0.1" @@ -5582,14 +5549,6 @@ mdast-util-mdxjs-esm@^2.0.0: mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - mdast-util-phrasing@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz#7cc0a8dec30eaf04b7b1a9661a92adb3382aa6e3" @@ -5613,20 +5572,6 @@ mdast-util-to-hast@^13.0.0: unist-util-visit "^5.0.0" vfile "^6.0.0" -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - mdast-util-to-markdown@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz#f910ffe60897f04bb4b7e7ee434486f76288361b" @@ -5642,13 +5587,6 @@ mdast-util-to-markdown@^2.0.0: unist-util-visit "^5.0.0" zwitch "^2.0.0" -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-string@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz#7a5121475556a04e7eddeb67b264aae79d312814" @@ -5691,28 +5629,6 @@ merge2@^1.3.0: resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - micromark-core-commonmark@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" @@ -5735,93 +5651,84 @@ micromark-core-commonmark@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-extension-gfm-autolink-literal@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" - integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== +micromark-extension-gfm-autolink-literal@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz#6286aee9686c4462c1e3552a9d505feddceeb935" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== dependencies: - micromark-util-character "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + micromark-util-character "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-footnote@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" - integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== - dependencies: - micromark-core-commonmark "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-strikethrough@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" - integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== +micromark-extension-gfm-footnote@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz#4dab56d4e398b9853f6fe4efac4fc9361f3e0750" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-core-commonmark "^2.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-normalize-identifier "^2.0.0" + micromark-util-sanitize-uri "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" - integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== +micromark-extension-gfm-strikethrough@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz#86106df8b3a692b5f6a92280d3879be6be46d923" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + devlop "^1.0.0" + micromark-util-chunked "^2.0.0" + micromark-util-classify-character "^2.0.0" + micromark-util-resolve-all "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-tagfilter@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" - integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== +micromark-extension-gfm-table@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz#fac70bcbf51fe65f5f44033118d39be8a9b5940b" + integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== dependencies: - micromark-util-types "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm-task-list-item@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" - integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== +micromark-extension-gfm-tagfilter@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz#f26d8a7807b5985fba13cf61465b58ca5ff7dc57" + integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg== dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" + micromark-util-types "^2.0.0" -micromark-extension-gfm@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" - integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== - dependencies: - micromark-extension-gfm-autolink-literal "^1.0.0" - micromark-extension-gfm-footnote "^1.0.0" - micromark-extension-gfm-strikethrough "^1.0.0" - micromark-extension-gfm-table "^1.0.0" - micromark-extension-gfm-tagfilter "^1.0.0" - micromark-extension-gfm-task-list-item "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== +micromark-extension-gfm-task-list-item@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz#bcc34d805639829990ec175c3eea12bb5b781f2c" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" + devlop "^1.0.0" + micromark-factory-space "^2.0.0" + micromark-util-character "^2.0.0" + micromark-util-symbol "^2.0.0" + micromark-util-types "^2.0.0" + +micromark-extension-gfm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz#3e13376ab95dd7a5cfd0e29560dfe999657b3c5b" + integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w== + dependencies: + micromark-extension-gfm-autolink-literal "^2.0.0" + micromark-extension-gfm-footnote "^2.0.0" + micromark-extension-gfm-strikethrough "^2.0.0" + micromark-extension-gfm-table "^2.0.0" + micromark-extension-gfm-tagfilter "^2.0.0" + micromark-extension-gfm-task-list-item "^2.0.0" + micromark-util-combine-extensions "^2.0.0" + micromark-util-types "^2.0.0" micromark-factory-destination@^2.0.0: version "2.0.1" @@ -5832,16 +5739,6 @@ micromark-factory-destination@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - micromark-factory-label@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" @@ -5852,14 +5749,6 @@ micromark-factory-label@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-space@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" @@ -5868,16 +5757,6 @@ micromark-factory-space@^2.0.0: micromark-util-character "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-title@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" @@ -5888,16 +5767,6 @@ micromark-factory-title@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-factory-whitespace@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" @@ -5908,14 +5777,6 @@ micromark-factory-whitespace@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-character@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-character@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" @@ -5924,13 +5785,6 @@ micromark-util-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-chunked@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" @@ -5938,15 +5792,6 @@ micromark-util-chunked@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-classify-character@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" @@ -5956,14 +5801,6 @@ micromark-util-classify-character@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - micromark-util-combine-extensions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" @@ -5972,13 +5809,6 @@ micromark-util-combine-extensions@^2.0.0: micromark-util-chunked "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-decode-numeric-character-reference@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" @@ -5986,16 +5816,6 @@ micromark-util-decode-numeric-character-reference@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-decode-string@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz#6cb99582e5d271e84efca8e61a807994d7161eb2" @@ -6006,33 +5826,16 @@ micromark-util-decode-string@^2.0.0: micromark-util-decode-numeric-character-reference "^2.0.0" micromark-util-symbol "^2.0.0" -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== - micromark-util-encode@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== - micromark-util-html-tag-name@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-normalize-identifier@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" @@ -6040,13 +5843,6 @@ micromark-util-normalize-identifier@^2.0.0: dependencies: micromark-util-symbol "^2.0.0" -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== - dependencies: - micromark-util-types "^1.0.0" - micromark-util-resolve-all@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" @@ -6054,15 +5850,6 @@ micromark-util-resolve-all@^2.0.0: dependencies: micromark-util-types "^2.0.0" -micromark-util-sanitize-uri@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-sanitize-uri@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" @@ -6072,16 +5859,6 @@ micromark-util-sanitize-uri@^2.0.0: micromark-util-encode "^2.0.0" micromark-util-symbol "^2.0.0" -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - micromark-util-subtokenize@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" @@ -6092,49 +5869,16 @@ micromark-util-subtokenize@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromark-util-symbol@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - micromark-util-symbol@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - micromark-util-types@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - micromark@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" @@ -6204,11 +5948,6 @@ monaco-editor@^0.53.0: dependencies: "@types/trusted-types" "^1.0.6" -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -7247,15 +6986,17 @@ relative-time-format@^1.1.7: resolved "https://registry.yarnpkg.com/relative-time-format/-/relative-time-format-1.1.11.tgz#b193d5192434e7c1c6a53e362f811c68a4f18c45" integrity sha512-TH+oV/w77hjaB9xCzoFYJ/Icmr/12+02IAoCI/YGS2UBTbjCbBjHGEBxGnVy4EJvOR1qadGzyFRI6hGaJJG93Q== -remark-gfm@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" - integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== +remark-gfm@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-4.0.1.tgz#33227b2a74397670d357bf05c098eaf8513f0d6b" + integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== dependencies: - "@types/mdast" "^3.0.0" - mdast-util-gfm "^2.0.0" - micromark-extension-gfm "^2.0.0" - unified "^10.0.0" + "@types/mdast" "^4.0.0" + mdast-util-gfm "^3.0.0" + micromark-extension-gfm "^3.0.0" + remark-parse "^11.0.0" + remark-stringify "^11.0.0" + unified "^11.0.0" remark-parse@^11.0.0: version "11.0.0" @@ -7278,6 +7019,15 @@ remark-rehype@^11.0.0: unified "^11.0.0" vfile "^6.0.0" +remark-stringify@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-11.0.0.tgz#4c5b01dd711c269df1aaae11743eb7e2e7636fd3" + integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw== + dependencies: + "@types/mdast" "^4.0.0" + mdast-util-to-markdown "^2.0.0" + unified "^11.0.0" + remove-accents@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/remove-accents/-/remove-accents-0.5.0.tgz#77991f37ba212afba162e375b627631315bed687" @@ -7353,13 +7103,6 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - safe-array-concat@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" @@ -8027,19 +7770,6 @@ unicode-trie@^2.0.0: pako "^0.2.5" tiny-inflate "^1.0.0" -unified@^10.0.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== - dependencies: - "@types/unist" "^2.0.0" - bail "^2.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^5.0.0" - unified@^11.0.0: version "11.0.5" resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" @@ -8053,13 +7783,6 @@ unified@^11.0.0: trough "^2.0.0" vfile "^6.0.0" -unist-util-is@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" - integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-6.0.1.tgz#d0a3f86f2dd0db7acd7d8c2478080b5c67f9c6a9" @@ -8074,13 +7797,6 @@ unist-util-position@^5.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" @@ -8088,14 +7804,6 @@ unist-util-stringify-position@^4.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents@^6.0.0: version "6.0.2" resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz#777df7fb98652ce16b4b7cd999d0a1a40efa3a02" @@ -8104,15 +7812,6 @@ unist-util-visit-parents@^6.0.0: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - unist-util-visit@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" @@ -8191,16 +7890,6 @@ utrie@^1.0.2: dependencies: base64-arraybuffer "^1.0.2" -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - vfile-location@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3" @@ -8209,14 +7898,6 @@ vfile-location@^5.0.0: "@types/unist" "^3.0.0" vfile "^6.0.0" -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message@^4.0.0: version "4.0.3" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4" @@ -8225,16 +7906,6 @@ vfile-message@^4.0.0: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" -vfile@^5.0.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - vfile@^6.0.0: version "6.0.3" resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" From 79b5f929e88c8423588d6d9a0efab54c0fed9db6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 23 Dec 2025 12:01:46 +0100 Subject: [PATCH 008/111] table support update --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8edee7aa5549..50baa212456d 100644 --- a/package.json +++ b/package.json @@ -118,4 +118,4 @@ "eslint": "9.35.0", "eslint-config-next": "15.5.2" } -} \ No newline at end of file +} From 85dd0b41b8102d231099486c27fed443d916af27 Mon Sep 17 00:00:00 2001 From: kakaiwa Date: Tue, 23 Dec 2025 23:40:08 -0500 Subject: [PATCH 009/111] Added overwrite toggle for transport rule standard --- AzuriteConfig | 1 + __azurite_db_blob__.json | 1 + __azurite_db_blob_extent__.json | 1 + __azurite_db_queue__.json | 1 + __azurite_db_queue_extent__.json | 1 + __azurite_db_table__.json | 1 + src/data/standards.json | 6 ++++++ 7 files changed, 12 insertions(+) create mode 100644 AzuriteConfig create mode 100644 __azurite_db_blob__.json create mode 100644 __azurite_db_blob_extent__.json create mode 100644 __azurite_db_queue__.json create mode 100644 __azurite_db_queue_extent__.json create mode 100644 __azurite_db_table__.json diff --git a/AzuriteConfig b/AzuriteConfig new file mode 100644 index 000000000000..6154d4706dff --- /dev/null +++ b/AzuriteConfig @@ -0,0 +1 @@ +{"instaceID":"7f12013a-93e6-4b9a-bdde-1592c81950ff"} \ No newline at end of file diff --git a/__azurite_db_blob__.json b/__azurite_db_blob__.json new file mode 100644 index 000000000000..1835fd8c3e2a --- /dev/null +++ b/__azurite_db_blob__.json @@ -0,0 +1 @@ +{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_blob__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$CONTAINERS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$CONTAINERS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOBS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]},"snapshot":{"name":"snapshot","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOBS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOCKS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"blobName":{"name":"blobName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOCKS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_blob_extent__.json b/__azurite_db_blob_extent__.json new file mode 100644 index 000000000000..b11420afc0cb --- /dev/null +++ b/__azurite_db_blob_extent__.json @@ -0,0 +1 @@ +{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_blob_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_queue__.json b/__azurite_db_queue__.json new file mode 100644 index 000000000000..181568ace20d --- /dev/null +++ b/__azurite_db_queue__.json @@ -0,0 +1 @@ +{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_queue__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$QUEUES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$QUEUES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$MESSAGES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"queueName":{"name":"queueName","dirty":false,"values":[]},"messageId":{"name":"messageId","dirty":false,"values":[]},"visibleTime":{"name":"visibleTime","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$MESSAGES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_queue_extent__.json b/__azurite_db_queue_extent__.json new file mode 100644 index 000000000000..d9abd4dd85f8 --- /dev/null +++ b/__azurite_db_queue_extent__.json @@ -0,0 +1 @@ +{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_queue_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_table__.json b/__azurite_db_table__.json new file mode 100644 index 000000000000..88c5d464b727 --- /dev/null +++ b/__azurite_db_table__.json @@ -0,0 +1 @@ +{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_table__.json","collections":[{"name":"$TABLES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"account":{"name":"account","dirty":false,"values":[]},"table":{"name":"table","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$TABLES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/src/data/standards.json b/src/data/standards.json index 92d605cf2036..e430a741ced9 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -5149,6 +5149,12 @@ "valueField": "GUID", "queryKey": "ListTransportRulesTemplates" } + }, + { + "type": "switch", + "label": "Overwrite existing transport rules", + "name": "overwrite", + "defaultValue": true } ] }, From 892223be420fa8bdc54ddfadc38355837a081084 Mon Sep 17 00:00:00 2001 From: kakaiwa Date: Tue, 23 Dec 2025 23:52:30 -0500 Subject: [PATCH 010/111] Removed Azurite files --- AzuriteConfig | 1 - __azurite_db_blob__.json | 1 - __azurite_db_blob_extent__.json | 1 - __azurite_db_queue__.json | 1 - __azurite_db_queue_extent__.json | 1 - __azurite_db_table__.json | 1 - 6 files changed, 6 deletions(-) delete mode 100644 AzuriteConfig delete mode 100644 __azurite_db_blob__.json delete mode 100644 __azurite_db_blob_extent__.json delete mode 100644 __azurite_db_queue__.json delete mode 100644 __azurite_db_queue_extent__.json delete mode 100644 __azurite_db_table__.json diff --git a/AzuriteConfig b/AzuriteConfig deleted file mode 100644 index 6154d4706dff..000000000000 --- a/AzuriteConfig +++ /dev/null @@ -1 +0,0 @@ -{"instaceID":"7f12013a-93e6-4b9a-bdde-1592c81950ff"} \ No newline at end of file diff --git a/__azurite_db_blob__.json b/__azurite_db_blob__.json deleted file mode 100644 index 1835fd8c3e2a..000000000000 --- a/__azurite_db_blob__.json +++ /dev/null @@ -1 +0,0 @@ -{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_blob__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$CONTAINERS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$CONTAINERS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOBS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]},"snapshot":{"name":"snapshot","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOBS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$BLOCKS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"containerName":{"name":"containerName","dirty":false,"values":[]},"blobName":{"name":"blobName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$BLOCKS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_blob_extent__.json b/__azurite_db_blob_extent__.json deleted file mode 100644 index b11420afc0cb..000000000000 --- a/__azurite_db_blob_extent__.json +++ /dev/null @@ -1 +0,0 @@ -{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_blob_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_queue__.json b/__azurite_db_queue__.json deleted file mode 100644 index 181568ace20d..000000000000 --- a/__azurite_db_queue__.json +++ /dev/null @@ -1 +0,0 @@ -{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_queue__.json","collections":[{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$QUEUES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"name":{"name":"name","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$QUEUES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$MESSAGES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"accountName":{"name":"accountName","dirty":false,"values":[]},"queueName":{"name":"queueName","dirty":false,"values":[]},"messageId":{"name":"messageId","dirty":false,"values":[]},"visibleTime":{"name":"visibleTime","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$MESSAGES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_queue_extent__.json b/__azurite_db_queue_extent__.json deleted file mode 100644 index d9abd4dd85f8..000000000000 --- a/__azurite_db_queue_extent__.json +++ /dev/null @@ -1 +0,0 @@ -{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_queue_extent__.json","collections":[{"name":"$EXTENTS_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"id":{"name":"id","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$EXTENTS_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file diff --git a/__azurite_db_table__.json b/__azurite_db_table__.json deleted file mode 100644 index 88c5d464b727..000000000000 --- a/__azurite_db_table__.json +++ /dev/null @@ -1 +0,0 @@ -{"filename":"C:\\Users\\kakaiwa_milesit\\Development\\CIPP-Project\\CIPP\\__azurite_db_table__.json","collections":[{"name":"$TABLES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{"account":{"name":"account","dirty":false,"values":[]},"table":{"name":"table","dirty":false,"values":[]}},"constraints":null,"uniqueNames":[],"transforms":{},"objType":"$TABLES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]},{"name":"$SERVICES_COLLECTION$","data":[],"idIndex":null,"binaryIndices":{},"constraints":null,"uniqueNames":["accountName"],"transforms":{},"objType":"$SERVICES_COLLECTION$","dirty":false,"cachedIndex":null,"cachedBinaryIndex":null,"cachedData":null,"adaptiveBinaryIndices":true,"transactional":false,"cloneObjects":false,"cloneMethod":"parse-stringify","asyncListeners":false,"disableMeta":false,"disableChangesApi":true,"disableDeltaChangesApi":true,"autoupdate":false,"serializableIndices":true,"disableFreeze":true,"ttl":null,"maxId":0,"DynamicViews":[],"events":{"insert":[],"update":[],"pre-insert":[],"pre-update":[],"close":[],"flushbuffer":[],"error":[],"delete":[null],"warning":[null]},"changes":[],"dirtyIds":[]}],"databaseVersion":1.5,"engineVersion":1.5,"autosave":true,"autosaveInterval":5000,"autosaveHandle":null,"throttledSaves":true,"options":{"persistenceMethod":"fs","autosave":true,"autosaveInterval":5000,"serializationMethod":"normal","destructureDelimiter":"$<\n"},"persistenceMethod":"fs","persistenceAdapter":null,"verbose":false,"events":{"init":[null],"loaded":[],"flushChanges":[],"close":[],"changes":[],"warning":[]},"ENV":"NODEJS"} \ No newline at end of file From 0437492e05ec38f80408fc8bea84203b177e0a9b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:55:03 +0100 Subject: [PATCH 011/111] Update the ability to browse items --- .../CippComponents/CippOffCanvas.jsx | 34 +++++++++++++++++-- src/components/CippTable/CippDataTable.js | 23 ++++++++++++- src/pages/dashboardv2/devices/index.js | 1 + src/pages/dashboardv2/identity/index.js | 1 + 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index 25b05ed69a28..cf55f054420e 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -4,6 +4,8 @@ import { getCippTranslation } from "../../utils/get-cipp-translation"; import { getCippFormatting } from "../../utils/get-cipp-formatting"; import { useMediaQuery, Grid } from "@mui/system"; import CloseIcon from "@mui/icons-material/Close"; +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; export const CippOffCanvas = (props) => { const { @@ -17,6 +19,10 @@ export const CippOffCanvas = (props) => { children, size = "sm", footer, + onNavigateUp, + onNavigateDown, + canNavigateUp = false, + canNavigateDown = false, } = props; const mdDown = useMediaQuery((theme) => theme.breakpoints.down("md")); @@ -84,9 +90,31 @@ export const CippOffCanvas = (props) => { sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", p: 1.5 }} > {title} - - - + + {(canNavigateUp || canNavigateDown) && ( + <> + + + + + + + + )} + + + + { const [usedColumns, setUsedColumns] = useState([]); const [offcanvasVisible, setOffcanvasVisible] = useState(false); const [offCanvasData, setOffCanvasData] = useState({}); + const [offCanvasRowIndex, setOffCanvasRowIndex] = useState(0); const [customComponentData, setCustomComponentData] = useState({}); const [customComponentVisible, setCustomComponentVisible] = useState(false); const [actionData, setActionData] = useState({ data: {}, action: {}, ready: false }); @@ -292,6 +293,7 @@ export const CippDataTable = (props) => { ? ({ row }) => ({ onClick: () => { setOffCanvasData(row.original); + setOffCanvasRowIndex(row.index); setOffcanvasVisible(true); }, sx: { @@ -453,6 +455,7 @@ export const CippDataTable = (props) => { onClick={() => { closeMenu(); setOffCanvasData(row.original); + setOffCanvasRowIndex(row.index); setOffcanvasVisible(true); }} > @@ -468,6 +471,7 @@ export const CippDataTable = (props) => { onClick={() => { closeMenu(); setOffCanvasData(row.original); + setOffCanvasRowIndex(row.index); setOffcanvasVisible(true); }} > @@ -758,8 +762,25 @@ export const CippDataTable = (props) => { extendedData={offCanvasData} extendedInfoFields={offCanvas?.extendedInfoFields} actions={actions} - children={offCanvas?.children} + title={offCanvasData?.Name || offCanvas?.title || "Extended Info"} + children={offCanvas?.children ? (row) => offCanvas.children(row, offCanvasRowIndex) : undefined} customComponent={offCanvas?.customComponent} + onNavigateUp={() => { + const newIndex = offCanvasRowIndex - 1; + if (newIndex >= 0 && memoizedData && memoizedData[newIndex]) { + setOffCanvasRowIndex(newIndex); + setOffCanvasData(memoizedData[newIndex]); + } + }} + onNavigateDown={() => { + const newIndex = offCanvasRowIndex + 1; + if (memoizedData && newIndex < memoizedData.length) { + setOffCanvasRowIndex(newIndex); + setOffCanvasData(memoizedData[newIndex]); + } + }} + canNavigateUp={offCanvasRowIndex > 0} + canNavigateDown={memoizedData && offCanvasRowIndex < memoizedData.length - 1} {...offCanvas} /> {/* Render custom component */} diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index 9d0dc4cbf6dd..ca747bc47bfb 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -1,3 +1,4 @@ +import React from "react"; import { Container, Typography, diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index 6a0c74001b02..d2fa1fcd0d51 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -1,3 +1,4 @@ +import React from "react"; import { Container, Typography, From 9c2c5641787e7b06ae6a2f492c7a855a10d353a0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 26 Dec 2025 21:55:10 +0100 Subject: [PATCH 012/111] update browsing --- src/components/CippComponents/CippOffCanvas.jsx | 8 ++++---- src/components/CippTable/CippDataTable.js | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippOffCanvas.jsx b/src/components/CippComponents/CippOffCanvas.jsx index cf55f054420e..b8e5b548e94d 100644 --- a/src/components/CippComponents/CippOffCanvas.jsx +++ b/src/components/CippComponents/CippOffCanvas.jsx @@ -93,16 +93,16 @@ export const CippOffCanvas = (props) => { {(canNavigateUp || canNavigateDown) && ( <> - - { extendedInfoFields={offCanvas?.extendedInfoFields} actions={actions} title={offCanvasData?.Name || offCanvas?.title || "Extended Info"} - children={offCanvas?.children ? (row) => offCanvas.children(row, offCanvasRowIndex) : undefined} + children={ + offCanvas?.children ? (row) => offCanvas.children(row, offCanvasRowIndex) : undefined + } customComponent={offCanvas?.customComponent} onNavigateUp={() => { const newIndex = offCanvasRowIndex - 1; From ac8483889a8998fba36bddc9347e8408d899fb74 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 26 Dec 2025 23:55:27 +0100 Subject: [PATCH 013/111] bug fixes for tests --- src/pages/dashboardv2/devices/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index ca747bc47bfb..2d9e6220bf03 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -15,6 +15,7 @@ import tabOptions from "../tabOptions"; import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall.jsx"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; +import { KeyboardArrowRight } from "@mui/icons-material"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Grid } from "@mui/system"; @@ -74,7 +75,6 @@ const Page = () => { }; const offCanvas = { - extendedInfoFields: ["Name", "Risk", "Status", "Category"], size: "lg", children: (row) => { return ( @@ -93,12 +93,12 @@ const Page = () => { }), }} > - + Risk Level - + @@ -116,7 +116,7 @@ const Page = () => { }), }} > - + User Impact @@ -137,7 +137,7 @@ const Page = () => { borderBottom: "none", }} > - + Implementation Effort @@ -159,7 +159,7 @@ const Page = () => { - {row.Name} + {row.Name} Date: Tue, 30 Dec 2025 11:53:31 +0100 Subject: [PATCH 014/111] Add secure score linegrid. --- src/pages/dashboardv2/index.js | 122 ++++++++++++++++++++++++++++++--- 1 file changed, 113 insertions(+), 9 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index a74e4be1cf39..90b78b4f2e3d 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -28,6 +28,9 @@ import { RadialBarChart, RadialBar, PolarAngleAxis, + LineChart, + Line, + CartesianGrid, XAxis, YAxis, ResponsiveContainer, @@ -114,6 +117,7 @@ const Page = () => { DataPassed: 0, DataTotal: 0, }, + SecureScore: testsApi.data.SecureScore || [], TenantInfo: { TenantOverview: { UserCount: testsApi.data.TenantCounts.Users || 0, @@ -688,30 +692,130 @@ const Page = () => { {/* Left Column */} - {/* Privileged users auth methods */} + {/* Secure Score */} - Privileged users auth methods + Secure Score } sx={{ pb: 1 }} /> - - {reportData.TenantInfo.OverviewAuthMethodsPrivilegedUsers?.nodes && ( - + + {reportData.SecureScore && reportData.SecureScore.length > 0 ? ( + + new Date(a.createdDateTime) - new Date(b.createdDateTime) + ).map((score) => ({ + date: new Date(score.createdDateTime).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + score: score.currentScore, + percentage: Math.round((score.currentScore / score.maxScore) * 100), + }))} + margin={{ left: 12, right: 12, top: 10, bottom: 10 }} + > + + + + { + if (name === "score") return [value.toFixed(2), "Score"]; + if (name === "percentage") return [value + "%", "Percentage"]; + return value; + }} + /> + + + + ) : ( + + + No secure score data available + + )} - {reportData.TenantInfo.OverviewAuthMethodsPrivilegedUsers?.description || - "No description available"} + The Secure Score measures your security posture across your tenant. + + + {reportData.SecureScore && reportData.SecureScore.length > 0 ? ( + + + + Latest % + + + {Math.round( + (reportData.SecureScore[reportData.SecureScore.length - 1] + .currentScore / + reportData.SecureScore[reportData.SecureScore.length - 1] + .maxScore) * + 100 + )} + % + + + + + + Current Score + + + {reportData.SecureScore[ + reportData.SecureScore.length - 1 + ].currentScore.toFixed(2)} + + + + + + Max Score + + + {reportData.SecureScore[ + reportData.SecureScore.length - 1 + ].maxScore.toFixed(2)} + + + + ) : ( + + Enable secure score monitoring in your tenant + + )} + {/* All Users Auth Methods */} 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 015/111] 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; + })()} + + - + )} From d2c70ddf0e2c1c43b0876e4c224f0185658c9e6f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 30 Dec 2025 23:12:24 +0100 Subject: [PATCH 016/111] New report creation ability --- .../CippAddTestReportDrawer.jsx | 171 ++++++++++++++++++ src/layouts/TabbedLayout.jsx | 8 +- src/pages/dashboardv2/devices/index.js | 12 +- src/pages/dashboardv2/identity/index.js | 8 +- src/pages/dashboardv2/index.js | 122 ++++++++++--- 5 files changed, 284 insertions(+), 37 deletions(-) create mode 100644 src/components/CippComponents/CippAddTestReportDrawer.jsx diff --git a/src/components/CippComponents/CippAddTestReportDrawer.jsx b/src/components/CippComponents/CippAddTestReportDrawer.jsx new file mode 100644 index 000000000000..e5d76ee9554e --- /dev/null +++ b/src/components/CippComponents/CippAddTestReportDrawer.jsx @@ -0,0 +1,171 @@ +import React, { useState, useEffect } from "react"; +import { Button } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState } from "react-hook-form"; +import { Add } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall, ApiGetCall } from "../../api/ApiCall"; + +export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) => { + const [drawerVisible, setDrawerVisible] = useState(false); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + name: "", + description: "", + IdentityTests: [], + DevicesTests: [], + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const createReport = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: ["ListTestReports"], + }); + + // Fetch available tests for the form + const availableTestsApi = ApiGetCall({ + url: "/api/ListAvailableTests", + queryKey: ["ListAvailableTests"], + }); + + const availableTests = availableTestsApi.data || { IdentityTests: [], DevicesTests: [] }; + + // Reset form fields on successful creation + useEffect(() => { + if (createReport.isSuccess) { + formControl.reset({ + name: "", + description: "", + IdentityTests: [], + DevicesTests: [], + }); + } + }, [createReport.isSuccess, formControl]); + + const handleSubmit = () => { + formControl.trigger(); + if (!isValid) { + return; + } + + const values = formControl.getValues(); + Object.keys(values).forEach((key) => { + if (values[key] === "" || values[key] === null) { + delete values[key]; + } + }); + + createReport.mutate({ + url: "/api/AddTestReport", + data: values, + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + name: "", + description: "", + IdentityTests: [], + DevicesTests: [], + }); + }; + + return ( + <> + + + +
+ + +
+ + } + > + + + + + + + + + ({ + value: test.id, + label: `${test.id} - ${test.name}`, + }))} + isFetching={availableTestsApi.isFetching} + /> + + + ({ + value: test.id, + label: `${test.id} - ${test.name}`, + }))} + isFetching={availableTestsApi.isFetching} + /> + + +
+ + ); +}; diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index f92712a89127..b268d6680cd5 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -1,13 +1,19 @@ import { usePathname, useRouter } from "next/navigation"; import { Box, Divider, Stack, Tab, Tabs } from "@mui/material"; +import { useSearchParams } from "next/navigation"; export const TabbedLayout = (props) => { const { tabOptions, children } = props; const router = useRouter(); const pathname = usePathname(); + const searchParams = useSearchParams(); const handleTabsChange = (event, value) => { - router.push(value); + // Preserve existing query parameters when changing tabs + const currentParams = new URLSearchParams(searchParams.toString()); + const queryString = currentParams.toString(); + const newPath = queryString ? `${value}?${queryString}` : value; + router.push(newPath); }; const currentTab = tabOptions.find((option) => option.path === pathname); diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index 2d9e6220bf03..56e932811359 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -19,18 +19,22 @@ import { KeyboardArrowRight } from "@mui/icons-material"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { Grid } from "@mui/system"; +import { useRouter } from "next/router"; const Page = () => { const settings = useSettings(); const { currentTenant } = settings; + const router = useRouter(); + const selectedReport = router.query.reportId || "ztna"; const testsApi = ApiGetCall({ url: "/api/ListTests", - data: { tenantFilter: currentTenant, reportId: "d5d1e123-bce0-482d-971f-be6ed820dd92" }, - queryKey: `${currentTenant}-ListTests-d5d1e123-bce0-482d-971f-be6ed820dd92`, + data: { tenantFilter: currentTenant, reportId: selectedReport }, + queryKey: `${currentTenant}-ListTests-${selectedReport}`, + waiting: !!currentTenant && !!selectedReport, }); - const deviceTests = + const DevicesTests = testsApi.data?.TestResults?.filter((test) => test.TestType === "Devices") || []; const getStatusColor = (status) => { @@ -311,7 +315,7 @@ const Page = () => { { const settings = useSettings(); const { currentTenant } = settings; + const router = useRouter(); + const selectedReport = router.query.reportId || "ztna"; const testsApi = ApiGetCall({ url: "/api/ListTests", - data: { tenantFilter: currentTenant, reportId: "d5d1e123-bce0-482d-971f-be6ed820dd92" }, - queryKey: `${currentTenant}-ListTests-d5d1e123-bce0-482d-971f-be6ed820dd92`, + data: { tenantFilter: currentTenant, reportId: selectedReport }, + queryKey: `${currentTenant}-ListTests-${selectedReport}`, + waiting: !!currentTenant && !!selectedReport, }); const identityTests = diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 9dbcf827492c..a69228be6a6e 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -8,12 +8,13 @@ import { Avatar, Divider, Tooltip, - Autocomplete, - TextField, Button, Skeleton, + Stack, } from "@mui/material"; import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { useForm, useWatch } from "react-hook-form"; import { Grid } from "@mui/system"; import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall.jsx"; @@ -47,8 +48,11 @@ import { AuthMethodSankey } from "/src/components/CippComponents/AuthMethodSanke import { DesktopDevicesSankey } from "/src/components/CippComponents/DesktopDevicesSankey"; import { MobileSankey } from "/src/components/CippComponents/MobileSankey"; import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; +import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; +import { CippAddTestReportDrawer } from "/src/components/CippComponents/CippAddTestReportDrawer"; import { CippCopyToClipBoard } from "/src/components/CippComponents/CippCopyToClipboard.jsx"; import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo.jsx"; +import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { People as UsersIcon, Person as UserIcon, @@ -271,17 +275,45 @@ const processAuthMethodsData = (mfaState) => { const Page = () => { const settings = useSettings(); + const router = useRouter(); const { currentTenant } = settings; const [portalMenuItems, setPortalMenuItems] = useState([]); - const [selectedReport, setSelectedReport] = useState(null); + const [deleteDialog, setDeleteDialog] = useState({ open: false }); - const reportOptions = [ - "Select a report", - "Executive Summary Report", - "Security Assessment Report", - "Compliance Report", - "Device Inventory Report", - ]; + // Get reportId from query params or default to "ztna" + const selectedReport = router.query.reportId || "ztna"; + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + reportId: selectedReport, + }, + }); + + const reportIdValue = useWatch({ control: formControl.control }); + + // Update URL when form value changes (e.g., user selects different report from dropdown) + useEffect(() => { + console.log("reportIdValue changed:", reportIdValue); + if (reportIdValue && reportIdValue.reportId?.value !== selectedReport) { + router.push( + { + pathname: router.pathname, + query: { ...router.query, reportId: reportIdValue.reportId?.value }, + }, + undefined, + { shallow: true } + ); + } + }, [reportIdValue]); + + // Fetch available reports + const reportsApi = ApiGetCall({ + url: "/api/ListTestReports", + queryKey: "ListTestReports", + }); + + const reports = reportsApi.data || []; const organization = ApiGetCall({ url: "/api/ListOrg", @@ -291,8 +323,9 @@ const Page = () => { const testsApi = ApiGetCall({ url: "/api/ListTests", - data: { tenantFilter: currentTenant, reportId: "d5d1e123-bce0-482d-971f-be6ed820dd92" }, - queryKey: `${currentTenant}-ListTests-d5d1e123-bce0-482d-971f-be6ed820dd92`, + data: { tenantFilter: currentTenant, reportId: selectedReport }, + queryKey: `${currentTenant}-ListTests-${selectedReport}`, + waiting: !!currentTenant && !!selectedReport, }); const driftApi = ApiGetCall({ @@ -423,29 +456,41 @@ const Page = () => { - setSelectedReport(newValue)} - sx={{ flex: 1 }} - renderInput={(params) => ( - - )} - /> - + + ({ + label: r.name, + value: r.id, + description: r.description, + }))} + placeholder="Choose a report" + /> + + @@ -1800,6 +1845,23 @@ const Page = () => { + + {/* Delete Report Dialog */} + ); }; From fa3bbb7555216c1314585bb5dd0a5c11e2e5c90c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 31 Dec 2025 01:54:20 +0100 Subject: [PATCH 017/111] License Sankey --- .../CippComponents/LicenseSankey.jsx | 80 +++++++ src/pages/dashboardv2/index.js | 207 ++++++++++-------- 2 files changed, 201 insertions(+), 86 deletions(-) create mode 100644 src/components/CippComponents/LicenseSankey.jsx diff --git a/src/components/CippComponents/LicenseSankey.jsx b/src/components/CippComponents/LicenseSankey.jsx new file mode 100644 index 000000000000..d1c74668aeb6 --- /dev/null +++ b/src/components/CippComponents/LicenseSankey.jsx @@ -0,0 +1,80 @@ +import { CippSankey } from "./CippSankey"; + +export const LicenseSankey = ({ data }) => { + // Null safety checks + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + // Calculate aggregated license data with null safety + let totalLicenses = 0; + let totalAssigned = 0; + let totalAvailable = 0; + + data.forEach((license) => { + if (license) { + totalLicenses += parseInt(license?.TotalLicenses || 0) || 0; + totalAssigned += parseInt(license?.CountUsed || 0) || 0; + totalAvailable += parseInt(license?.CountAvailable || 0) || 0; + } + }); + + // If no valid data, return null + if (totalLicenses === 0 && totalAssigned === 0 && totalAvailable === 0) { + return null; + } + + // Create Sankey flow: Total -> Assigned/Available -> Top 5 licenses + const nodes = [ + { id: "Total Licenses", nodeColor: "hsl(210, 100%, 56%)" }, + { id: "Assigned", nodeColor: "hsl(99, 70%, 50%)" }, + { id: "Available", nodeColor: "hsl(28, 100%, 53%)" }, + ]; + + const links = [ + { + source: "Total Licenses", + target: "Assigned", + value: totalAssigned > 0 ? totalAssigned : 0, + }, + { + source: "Total Licenses", + target: "Available", + value: totalAvailable > 0 ? totalAvailable : 0, + }, + ]; + + // Add top 5 most used licenses with null safety + const topLicenses = data + .filter((license) => license && parseInt(license?.CountUsed || 0) > 0) + .sort((a, b) => parseInt(b?.CountUsed || 0) - parseInt(a?.CountUsed || 0)) + .slice(0, 5); + + topLicenses.forEach((license, index) => { + if (license) { + const licenseName = + license.License || license.skuPartNumber || license.SkuPartNumber || "Unknown License"; + const shortName = + licenseName.length > 30 ? licenseName.substring(0, 27) + "..." : licenseName; + + nodes.push({ + id: shortName, + nodeColor: `hsl(${120 + index * 30}, 70%, 50%)`, + }); + + links.push({ + source: "Assigned", + target: shortName, + value: parseInt(license?.CountUsed || 0) || 0, + }); + } + }); + + // Only render if we have valid data + if (nodes.length === 3 && links.length === 2) { + // No licenses to show besides the base nodes + return null; + } + + return ; +}; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index a69228be6a6e..eab2425fcd4e 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -46,6 +46,7 @@ import { CaSankey } from "/src/components/CippComponents/CaSankey"; import { CaDeviceSankey } from "/src/components/CippComponents/CaDeviceSankey"; import { AuthMethodSankey } from "/src/components/CippComponents/AuthMethodSankey"; import { DesktopDevicesSankey } from "/src/components/CippComponents/DesktopDevicesSankey"; +import { LicenseSankey } from "/src/components/CippComponents/LicenseSankey"; import { MobileSankey } from "/src/components/CippComponents/MobileSankey"; import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; @@ -66,6 +67,7 @@ import { CheckCircle as CheckCircleIcon, Laptop as MonitorIcon, Work as BriefcaseIcon, + CardMembership as CardMembershipIcon, } from "@mui/icons-material"; // Helper function to process MFAState data into Sankey chart format @@ -464,7 +466,7 @@ const Page = () => { multiple={false} formControl={formControl} options={reports.map((r) => ({ - label: r.name, + label: r.description ? `${r.name} - ${r.description}` : r.name, value: r.id, description: r.description, }))} @@ -477,10 +479,6 @@ const Page = () => { color="error" size="small" sx={{ minHeight: 40 }} - disabled={ - !selectedReport || - reports.find((r) => r.id === selectedReport)?.source === "file" - } onClick={() => { const report = reports.find((r) => r.id === selectedReport); if (report && report.source !== "file") { @@ -1169,13 +1167,13 @@ const Page = () => { - {/* Device Sign-ins */} + {/* License Overview */} - - Device sign-ins + + License Overview } sx={{ pb: 1 }} @@ -1184,10 +1182,8 @@ const Page = () => { {testsApi.isFetching ? ( - ) : reportData.TenantInfo.OverviewCaDevicesAllUsers?.nodes ? ( - + ) : testsApi.data?.LicenseData && Array.isArray(testsApi.data.LicenseData) ? ( + ) : ( { }} > - No device sign-in data available + No license data available )} - - {reportData.TenantInfo.OverviewCaDevicesAllUsers?.description || - "No description available"} - + + + + {testsApi.isFetching ? ( + + + + + + + + + + + + + + + + + ) : testsApi.data?.LicenseData && Array.isArray(testsApi.data.LicenseData) ? ( + + + + Total Licenses + + + {testsApi.data.LicenseData.reduce( + (sum, lic) => sum + (parseInt(lic?.TotalLicenses || 0) || 0), + 0 + ).toLocaleString()} + + + + + + Assigned + + + {testsApi.data.LicenseData.reduce( + (sum, lic) => sum + (parseInt(lic?.CountUsed || 0) || 0), + 0 + ).toLocaleString()} + + + + + + Available + + + {testsApi.data.LicenseData.reduce( + (sum, lic) => sum + (parseInt(lic?.CountAvailable || 0) || 0), + 0 + ).toLocaleString()} + + + + ) : ( + + + No license statistics available + + + )} @@ -1581,7 +1645,7 @@ const Page = () => { title={ - Desktop devices + License Overview } sx={{ pb: 1 }} @@ -1590,10 +1654,8 @@ const Page = () => { {testsApi.isFetching ? ( - ) : reportData.TenantInfo.DeviceOverview?.DesktopDevicesSummary?.nodes ? ( - + ) : testsApi.data?.LicenseData ? ( + ) : ( { }} > - No desktop device data available + No license data available )} - {reportData.TenantInfo.DeviceOverview.DesktopDevicesSummary?.description || - "No description available"} + Overview of license assignments and availability @@ -1619,97 +1680,71 @@ const Page = () => { {testsApi.isFetching ? ( - + + - + + - + + - ) : ( + ) : testsApi.data?.LicenseData && Array.isArray(testsApi.data.LicenseData) ? ( - Entra joined + Total Licenses - {(() => { - 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); - })()} - % + {testsApi.data.LicenseData.reduce( + (sum, lic) => sum + (parseInt(lic?.TotalLicenses || 0) || 0), + 0 + ).toLocaleString()} - Entra hybrid joined + Assigned - {(() => { - 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.data.LicenseData.reduce( + (sum, lic) => sum + (parseInt(lic?.CountUsed || 0) || 0), + 0 + ).toLocaleString()} - Entra registered + Available - {(() => { - 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); - })()} - % + {testsApi.data.LicenseData.reduce( + (sum, lic) => sum + (parseInt(lic?.CountAvailable || 0) || 0), + 0 + ).toLocaleString()} + ) : ( + + + No license statistics available + + )} From d8a8282ca51a8ced862767ffab4e1eee55f158c5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 31 Dec 2025 02:29:32 +0100 Subject: [PATCH 018/111] License sankey --- .../CippComponents/LicenseSankey.jsx | 91 +++++++++---------- 1 file changed, 44 insertions(+), 47 deletions(-) diff --git a/src/components/CippComponents/LicenseSankey.jsx b/src/components/CippComponents/LicenseSankey.jsx index d1c74668aeb6..fd4e1763f260 100644 --- a/src/components/CippComponents/LicenseSankey.jsx +++ b/src/components/CippComponents/LicenseSankey.jsx @@ -6,49 +6,19 @@ export const LicenseSankey = ({ data }) => { return null; } - // Calculate aggregated license data with null safety - let totalLicenses = 0; - let totalAssigned = 0; - let totalAvailable = 0; - - data.forEach((license) => { - if (license) { - totalLicenses += parseInt(license?.TotalLicenses || 0) || 0; - totalAssigned += parseInt(license?.CountUsed || 0) || 0; - totalAvailable += parseInt(license?.CountAvailable || 0) || 0; - } - }); + // Get top 5 licenses by total count with null safety + const topLicenses = data + .filter((license) => license && parseInt(license?.TotalLicenses || 0) > 0) + .sort((a, b) => parseInt(b?.TotalLicenses || 0) - parseInt(a?.TotalLicenses || 0)) + .slice(0, 5); - // If no valid data, return null - if (totalLicenses === 0 && totalAssigned === 0 && totalAvailable === 0) { + if (topLicenses.length === 0) { return null; } - // Create Sankey flow: Total -> Assigned/Available -> Top 5 licenses - const nodes = [ - { id: "Total Licenses", nodeColor: "hsl(210, 100%, 56%)" }, - { id: "Assigned", nodeColor: "hsl(99, 70%, 50%)" }, - { id: "Available", nodeColor: "hsl(28, 100%, 53%)" }, - ]; - - const links = [ - { - source: "Total Licenses", - target: "Assigned", - value: totalAssigned > 0 ? totalAssigned : 0, - }, - { - source: "Total Licenses", - target: "Available", - value: totalAvailable > 0 ? totalAvailable : 0, - }, - ]; - - // Add top 5 most used licenses with null safety - const topLicenses = data - .filter((license) => license && parseInt(license?.CountUsed || 0) > 0) - .sort((a, b) => parseInt(b?.CountUsed || 0) - parseInt(a?.CountUsed || 0)) - .slice(0, 5); + // Create Sankey flow: Top 5 Licenses -> Assigned/Available for each + const nodes = []; + const links = []; topLicenses.forEach((license, index) => { if (license) { @@ -57,22 +27,49 @@ export const LicenseSankey = ({ data }) => { const shortName = licenseName.length > 30 ? licenseName.substring(0, 27) + "..." : licenseName; + const assigned = parseInt(license?.CountUsed || 0) || 0; + const available = parseInt(license?.CountAvailable || 0) || 0; + + // Add license node nodes.push({ id: shortName, - nodeColor: `hsl(${120 + index * 30}, 70%, 50%)`, + nodeColor: `hsl(${210 + index * 30}, 70%, 50%)`, }); - links.push({ - source: "Assigned", - target: shortName, - value: parseInt(license?.CountUsed || 0) || 0, - }); + // Add Assigned and Available nodes for this license + const assignedId = `${shortName} - Assigned`; + const availableId = `${shortName} - Available`; + + if (assigned > 0) { + nodes.push({ + id: assignedId, + nodeColor: "hsl(99, 70%, 50%)", + }); + + links.push({ + source: shortName, + target: assignedId, + value: assigned, + }); + } + + if (available > 0) { + nodes.push({ + id: availableId, + nodeColor: "hsl(28, 100%, 53%)", + }); + + links.push({ + source: shortName, + target: availableId, + value: available, + }); + } } }); // Only render if we have valid data - if (nodes.length === 3 && links.length === 2) { - // No licenses to show besides the base nodes + if (nodes.length === 0 || links.length === 0) { return null; } From f2dacd1c3d0f4bc87a7b1b6b01c82c4590878dfa Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 31 Dec 2025 12:56:08 +0100 Subject: [PATCH 019/111] prettification --- .../CippComponents/AuthMethodCard.jsx | 155 ++++ .../CippComponents/AuthMethodSankey.jsx | 196 ++++- src/components/CippComponents/LicenseCard.jsx | 190 +++++ src/components/CippComponents/MFACard.jsx | 151 ++++ src/components/CippComponents/MFASankey.jsx | 140 +++ .../CippComponents/SecureScoreCard.jsx | 162 ++++ .../CippComponents/SecureScoreChart.jsx | 153 ++++ src/pages/dashboardv2/index.js | 805 +----------------- 8 files changed, 1116 insertions(+), 836 deletions(-) create mode 100644 src/components/CippComponents/AuthMethodCard.jsx create mode 100644 src/components/CippComponents/LicenseCard.jsx create mode 100644 src/components/CippComponents/MFACard.jsx create mode 100644 src/components/CippComponents/MFASankey.jsx create mode 100644 src/components/CippComponents/SecureScoreCard.jsx create mode 100644 src/components/CippComponents/SecureScoreChart.jsx diff --git a/src/components/CippComponents/AuthMethodCard.jsx b/src/components/CippComponents/AuthMethodCard.jsx new file mode 100644 index 000000000000..a50baef5b6f2 --- /dev/null +++ b/src/components/CippComponents/AuthMethodCard.jsx @@ -0,0 +1,155 @@ +import { Box, Card, CardHeader, CardContent, Typography, Skeleton } from "@mui/material"; +import { People as UsersIcon } from "@mui/icons-material"; +import { CippSankey } from "./CippSankey"; + +export const AuthMethodCard = ({ data, isLoading }) => { + const processData = () => { + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + const enabledUsers = data.filter((user) => user.AccountEnabled === true); + if (enabledUsers.length === 0) { + return null; + } + + const phishableMethods = ["mobilePhone", "email", "microsoftAuthenticatorPush"]; + const phishResistantMethods = ["fido2", "windowsHelloForBusiness", "x509Certificate"]; + + let singleFactor = 0; + let phishableCount = 0; + let phishResistantCount = 0; + let perUserMFA = 0; + let phoneCount = 0; + let authenticatorCount = 0; + 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 (perUser && !hasRegistered && methods.length === 0) { + perUserMFA++; + return; + } + + if (!hasRegistered || methods.length === 0) { + singleFactor++; + return; + } + + const hasPhishResistant = methods.some((m) => phishResistantMethods.includes(m)); + const hasPhishable = methods.some((m) => phishableMethods.includes(m)); + + if (hasPhishResistant) { + phishResistantCount++; + if (methods.includes("fido2") || methods.includes("x509Certificate")) { + passkeyCount++; + } + if (methods.includes("windowsHelloForBusiness")) { + whfbCount++; + } + } else if (hasPhishable) { + phishableCount++; + if (methods.includes("mobilePhone") || methods.includes("email")) { + phoneCount++; + } + if ( + methods.includes("microsoftAuthenticatorPush") || + methods.includes("softwareOneTimePasscode") + ) { + authenticatorCount++; + } + } else { + phishableCount++; + authenticatorCount++; + } + }); + + const mfaPercentage = ( + ((phishableCount + phishResistantCount + perUserMFA) / enabledUsers.length) * + 100 + ).toFixed(1); + const phishResistantPercentage = ((phishResistantCount / enabledUsers.length) * 100).toFixed(1); + + const links = [ + { 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 }, + ]; + + if (phoneCount > 0) links.push({ source: "Phishable", target: "Phone", value: phoneCount }); + if (authenticatorCount > 0) + links.push({ source: "Phishable", target: "Authenticator", value: authenticatorCount }); + + if (passkeyCount > 0) + links.push({ source: "Phish resistant", target: "Passkey", value: passkeyCount }); + if (whfbCount > 0) links.push({ source: "Phish resistant", target: "WHfB", value: whfbCount }); + + const description = `${mfaPercentage}% of enabled users have MFA configured. ${phishResistantPercentage}% use phish-resistant authentication methods.`; + + return { + nodes: [ + { id: "Users", nodeColor: "hsl(28, 100%, 53%)" }, + { id: "Single factor", nodeColor: "hsl(0, 100%, 50%)" }, + { id: "Multi factor", nodeColor: "hsl(200, 70%, 50%)" }, + { id: "Phishable", nodeColor: "hsl(39, 100%, 50%)" }, + { id: "Phone", nodeColor: "hsl(39, 100%, 45%)" }, + { id: "Authenticator", nodeColor: "hsl(39, 100%, 55%)" }, + { id: "Phish resistant", nodeColor: "hsl(99, 70%, 50%)" }, + { id: "Passkey", nodeColor: "hsl(140, 70%, 50%)" }, + { id: "WHfB", nodeColor: "hsl(160, 70%, 50%)" }, + ], + links, + description, + }; + }; + + const processedData = processData(); + + return ( + + + + All users auth methods + + } + sx={{ pb: 1 }} + /> + + + {isLoading ? ( + + ) : processedData ? ( + + ) : ( + + + No authentication method data available + + + )} + + + {!isLoading && processedData?.description && ( + + + {processedData.description} + + + )} + + ); +}; diff --git a/src/components/CippComponents/AuthMethodSankey.jsx b/src/components/CippComponents/AuthMethodSankey.jsx index 200cb4274766..f57c42573c52 100644 --- a/src/components/CippComponents/AuthMethodSankey.jsx +++ b/src/components/CippComponents/AuthMethodSankey.jsx @@ -1,49 +1,159 @@ import { CippSankey } from "./CippSankey"; export const AuthMethodSankey = ({ data }) => { + // Null safety checks + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + // Count enabled users only + const enabledUsers = data.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 links = [ + { 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) links.push({ source: "Phishable", target: "Phone", value: phoneCount }); + if (authenticatorCount > 0) + links.push({ source: "Phishable", target: "Authenticator", value: authenticatorCount }); + + // Add phish-resistant method breakdowns + if (passkeyCount > 0) + links.push({ source: "Phish resistant", target: "Passkey", value: passkeyCount }); + if (whfbCount > 0) links.push({ source: "Phish resistant", target: "WHfB", value: whfbCount }); + + const description = `${mfaPercentage}% of enabled users have MFA configured. ${phishResistantPercentage}% use phish-resistant authentication methods.`; + return ( - + <> + + {description && ( +
+ {description} +
+ )} + ); }; diff --git a/src/components/CippComponents/LicenseCard.jsx b/src/components/CippComponents/LicenseCard.jsx new file mode 100644 index 000000000000..d6762196bc9b --- /dev/null +++ b/src/components/CippComponents/LicenseCard.jsx @@ -0,0 +1,190 @@ +import { Box, Card, CardHeader, CardContent, Typography, Divider, Skeleton } from "@mui/material"; +import { CardMembership as CardMembershipIcon } from "@mui/icons-material"; +import { CippSankey } from "./CippSankey"; + +export const LicenseCard = ({ data, isLoading }) => { + const processData = () => { + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + const topLicenses = data + .filter((license) => license && parseInt(license?.TotalLicenses || 0) > 0) + .sort((a, b) => parseInt(b?.TotalLicenses || 0) - parseInt(a?.TotalLicenses || 0)) + .slice(0, 5); + + if (topLicenses.length === 0) { + return null; + } + + const nodes = []; + const links = []; + + topLicenses.forEach((license, index) => { + if (license) { + const licenseName = + license.License || license.skuPartNumber || license.SkuPartNumber || "Unknown License"; + const shortName = + licenseName.length > 30 ? licenseName.substring(0, 27) + "..." : licenseName; + + const assigned = parseInt(license?.CountUsed || 0) || 0; + const available = parseInt(license?.CountAvailable || 0) || 0; + + nodes.push({ + id: shortName, + nodeColor: `hsl(${210 + index * 30}, 70%, 50%)`, + }); + + const assignedId = `${shortName} - Assigned`; + const availableId = `${shortName} - Available`; + + if (assigned > 0) { + nodes.push({ + id: assignedId, + nodeColor: "hsl(99, 70%, 50%)", + }); + + links.push({ + source: shortName, + target: assignedId, + value: assigned, + }); + } + + if (available > 0) { + nodes.push({ + id: availableId, + nodeColor: "hsl(28, 100%, 53%)", + }); + + links.push({ + source: shortName, + target: availableId, + value: available, + }); + } + } + }); + + if (nodes.length === 0 || links.length === 0) { + return null; + } + + return { nodes, links }; + }; + + const processedData = processData(); + + const calculateStats = () => { + if (!data || !Array.isArray(data)) { + return { total: 0, assigned: 0, available: 0 }; + } + + return { + total: data.reduce((sum, lic) => sum + (parseInt(lic?.TotalLicenses || 0) || 0), 0), + assigned: data.reduce((sum, lic) => sum + (parseInt(lic?.CountUsed || 0) || 0), 0), + available: data.reduce((sum, lic) => sum + (parseInt(lic?.CountAvailable || 0) || 0), 0), + }; + }; + + const stats = calculateStats(); + + return ( + + + + License Overview + + } + sx={{ pb: 1 }} + /> + + + {isLoading ? ( + + ) : processedData ? ( + + ) : ( + + + No license data available + + + )} + + + + + {isLoading ? ( + + + + + + + + + + + + + + + + + ) : data && Array.isArray(data) && data.length > 0 ? ( + + + + Total Licenses + + + {stats.total.toLocaleString()} + + + + + + Assigned + + + {stats.assigned.toLocaleString()} + + + + + + Available + + + {stats.available.toLocaleString()} + + + + ) : ( + + + No license statistics available + + + )} + + + ); +}; diff --git a/src/components/CippComponents/MFACard.jsx b/src/components/CippComponents/MFACard.jsx new file mode 100644 index 000000000000..3b2468415958 --- /dev/null +++ b/src/components/CippComponents/MFACard.jsx @@ -0,0 +1,151 @@ +import { Box, Card, CardHeader, CardContent, Typography, Skeleton } from "@mui/material"; +import { Person as UserIcon } from "@mui/icons-material"; +import { CippSankey } from "./CippSankey"; + +export const MFACard = ({ data, isLoading }) => { + // Process data inside component + const processData = () => { + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + const enabledUsers = data.filter((user) => user.AccountEnabled === true); + if (enabledUsers.length === 0) { + return null; + } + + let registeredUsers = 0; + let notRegisteredUsers = 0; + let registeredCA = 0; + let registeredSD = 0; + let registeredPerUser = 0; + let registeredNone = 0; + 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"; + + if (hasRegistered || perUserEnabled) { + registeredUsers++; + 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 links = [ + { source: "Enabled users", target: "MFA registered", value: registeredUsers }, + { source: "Enabled users", target: "Not registered", value: notRegisteredUsers }, + ]; + + if (registeredCA > 0) + links.push({ source: "MFA registered", target: "CA policy", value: registeredCA }); + if (registeredSD > 0) + links.push({ source: "MFA registered", target: "Security defaults", value: registeredSD }); + if (registeredPerUser > 0) + links.push({ source: "MFA registered", target: "Per-user MFA", value: registeredPerUser }); + if (registeredNone > 0) + links.push({ source: "MFA registered", target: "No enforcement", value: registeredNone }); + + if (notRegisteredCA > 0) + links.push({ source: "Not registered", target: "CA policy", value: notRegisteredCA }); + if (notRegisteredSD > 0) + links.push({ + source: "Not registered", + target: "Security defaults", + value: notRegisteredSD, + }); + if (notRegisteredPerUser > 0) + links.push({ source: "Not registered", target: "Per-user MFA", value: notRegisteredPerUser }); + if (notRegisteredNone > 0) + links.push({ source: "Not registered", target: "No enforcement", value: notRegisteredNone }); + + const description = `${registeredPercentage}% of enabled users have registered MFA methods. ${protectedPercentage}% are protected by policies requiring MFA.`; + + return { + nodes: [ + { id: "Enabled users", nodeColor: "hsl(28, 100%, 53%)" }, + { id: "MFA registered", nodeColor: "hsl(99, 70%, 50%)" }, + { id: "Not registered", nodeColor: "hsl(39, 100%, 50%)" }, + { id: "CA policy", 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, + description, + }; + }; + + const processedData = processData(); + + return ( + + + + User authentication + + } + sx={{ pb: 1 }} + /> + + + {isLoading ? ( + + ) : processedData ? ( + + ) : ( + + + No MFA data available + + + )} + + + {!isLoading && processedData?.description && ( + + + {processedData.description} + + + )} + + ); +}; diff --git a/src/components/CippComponents/MFASankey.jsx b/src/components/CippComponents/MFASankey.jsx new file mode 100644 index 000000000000..9fb387cefa9d --- /dev/null +++ b/src/components/CippComponents/MFASankey.jsx @@ -0,0 +1,140 @@ +import { CippSankey } from "./CippSankey"; + +export const MFASankey = ({ data }) => { + // Null safety checks + if (!data || !Array.isArray(data) || data.length === 0) { + return null; + } + + // Count enabled users only + const enabledUsers = data.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 links = [ + { 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) + links.push({ source: "MFA registered", target: "CA policy", value: registeredCA }); + if (registeredSD > 0) + links.push({ source: "MFA registered", target: "Security defaults", value: registeredSD }); + if (registeredPerUser > 0) + links.push({ source: "MFA registered", target: "Per-user MFA", value: registeredPerUser }); + if (registeredNone > 0) + links.push({ source: "MFA registered", target: "No enforcement", value: registeredNone }); + + // Add protection methods for not registered users + if (notRegisteredCA > 0) + links.push({ source: "Not registered", target: "CA policy", value: notRegisteredCA }); + if (notRegisteredSD > 0) + links.push({ source: "Not registered", target: "Security defaults", value: notRegisteredSD }); + if (notRegisteredPerUser > 0) + links.push({ source: "Not registered", target: "Per-user MFA", value: notRegisteredPerUser }); + if (notRegisteredNone > 0) + links.push({ source: "Not registered", target: "No enforcement", value: notRegisteredNone }); + + const description = `${registeredPercentage}% of enabled users have registered MFA methods. ${protectedPercentage}% are protected by policies requiring MFA.`; + + return ( + <> + + {description && ( +
+ {description} +
+ )} + + ); +}; diff --git a/src/components/CippComponents/SecureScoreCard.jsx b/src/components/CippComponents/SecureScoreCard.jsx new file mode 100644 index 000000000000..da849c069b3c --- /dev/null +++ b/src/components/CippComponents/SecureScoreCard.jsx @@ -0,0 +1,162 @@ +import { Box, Card, CardHeader, CardContent, Typography, Divider, Skeleton } from "@mui/material"; +import { Security as SecurityIcon } from "@mui/icons-material"; +import { + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + ResponsiveContainer, + Tooltip as RechartsTooltip, +} from "recharts"; + +export const SecureScoreCard = ({ data, isLoading }) => { + return ( + + + + Secure Score + + } + sx={{ pb: 1 }} + /> + + {isLoading ? ( + <> + + + + + + + The Secure Score measures your security posture across your tenant. + + + ) : !data || !Array.isArray(data) || data.length === 0 ? ( + <> + + + + No secure score data available + + + + + The Secure Score measures your security posture across your tenant. + + + ) : ( + <> + + + new Date(a.createdDateTime) - new Date(b.createdDateTime)) + .map((score) => ({ + date: new Date(score.createdDateTime).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + score: score.currentScore, + percentage: Math.round((score.currentScore / score.maxScore) * 100), + }))} + margin={{ left: 12, right: 12, top: 10, bottom: 10 }} + > + + + + { + if (name === "score") return [value.toFixed(2), "Score"]; + if (name === "percentage") return [value + "%", "Percentage"]; + return value; + }} + /> + + + + + + The Secure Score measures your security posture across your tenant. + + + )} + + + + {isLoading ? ( + + + + + + + + + + + + + + ) : !data || !Array.isArray(data) || data.length === 0 ? ( + + Enable secure score monitoring in your tenant + + ) : ( + + + + Latest % + + + {Math.round( + (data[data.length - 1].currentScore / data[data.length - 1].maxScore) * 100 + )} + % + + + + + + Current Score + + + {data[data.length - 1].currentScore.toFixed(2)} + + + + + + Max Score + + + {data[data.length - 1].maxScore.toFixed(2)} + + + + )} + + + ); +}; diff --git a/src/components/CippComponents/SecureScoreChart.jsx b/src/components/CippComponents/SecureScoreChart.jsx new file mode 100644 index 000000000000..f9830d128481 --- /dev/null +++ b/src/components/CippComponents/SecureScoreChart.jsx @@ -0,0 +1,153 @@ +import { Box, Typography, Divider, Skeleton } from "@mui/material"; +import { + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + ResponsiveContainer, + Tooltip as RechartsTooltip, +} from "recharts"; + +export const SecureScoreChart = ({ data, isLoading }) => { + if (isLoading) { + return ( + <> + + + + + + + The Secure Score measures your security posture across your tenant. + + + + + + + + + + + + + + + + + ); + } + + if (!data || !Array.isArray(data) || data.length === 0) { + return ( + <> + + + + No secure score data available + + + + + The Secure Score measures your security posture across your tenant. + + + + + Enable secure score monitoring in your tenant + + + + ); + } + + const sortedData = [...data].sort( + (a, b) => new Date(a.createdDateTime) - new Date(b.createdDateTime) + ); + + const chartData = sortedData.map((score) => ({ + date: new Date(score.createdDateTime).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }), + score: score.currentScore, + percentage: Math.round((score.currentScore / score.maxScore) * 100), + })); + + const latestScore = sortedData[sortedData.length - 1]; + const latestPercentage = Math.round((latestScore.currentScore / latestScore.maxScore) * 100); + + return ( + <> + + + + + + + { + if (name === "score") return [value.toFixed(2), "Score"]; + if (name === "percentage") return [value + "%", "Percentage"]; + return value; + }} + /> + + + + + + The Secure Score measures your security posture across your tenant. + + + + + + Latest % + + + {latestPercentage}% + + + + + + Current Score + + + {latestScore.currentScore.toFixed(2)} + + + + + + Max Score + + + {latestScore.maxScore.toFixed(2)} + + + + + ); +}; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index eab2425fcd4e..49768b605e55 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -10,7 +10,6 @@ import { Tooltip, Button, Skeleton, - Stack, } from "@mui/material"; import { useState, useEffect } from "react"; import { useRouter } from "next/router"; @@ -42,12 +41,10 @@ import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; import { dashboardDemoData } from "/src/data/dashboardv2-demo-data"; -import { CaSankey } from "/src/components/CippComponents/CaSankey"; -import { CaDeviceSankey } from "/src/components/CippComponents/CaDeviceSankey"; -import { AuthMethodSankey } from "/src/components/CippComponents/AuthMethodSankey"; -import { DesktopDevicesSankey } from "/src/components/CippComponents/DesktopDevicesSankey"; -import { LicenseSankey } from "/src/components/CippComponents/LicenseSankey"; -import { MobileSankey } from "/src/components/CippComponents/MobileSankey"; +import { SecureScoreCard } from "/src/components/CippComponents/SecureScoreCard"; +import { MFACard } from "/src/components/CippComponents/MFACard"; +import { AuthMethodCard } from "/src/components/CippComponents/AuthMethodCard"; +import { LicenseCard } from "/src/components/CippComponents/LicenseCard"; import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; import { CippAddTestReportDrawer } from "/src/components/CippComponents/CippAddTestReportDrawer"; @@ -70,211 +67,6 @@ import { CardMembership as CardMembershipIcon, } 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 router = useRouter(); @@ -367,11 +159,10 @@ const Page = () => { DeviceCount: testsApi.data.TenantCounts.Devices || 0, ManagedDeviceCount: testsApi.data.TenantCounts.ManagedDevices || 0, }, - OverviewCaMfaAllUsers: processMFAStateData(testsApi.data.MFAState), + MFAState: testsApi.data.MFAState, OverviewCaDevicesAllUsers: dashboardDemoData.TenantInfo.OverviewCaDevicesAllUsers, OverviewAuthMethodsPrivilegedUsers: dashboardDemoData.TenantInfo.OverviewAuthMethodsPrivilegedUsers, - OverviewAuthMethodsAllUsers: processAuthMethodsData(testsApi.data.MFAState), DeviceOverview: dashboardDemoData.TenantInfo.DeviceOverview, }, } @@ -414,19 +205,9 @@ const Page = () => { return num.toLocaleString(); }; - const metricDescriptions = { - users: "Total number of users in your tenant", - guests: "External users with guest access", - groups: "Microsoft 365 and security groups", - apps: "Service principals in your tenant", - devices: "All devices accessing tenant resources", - managed: "Devices enrolled in Intune", - }; - return ( - {/* Dashboard Bar with Portals, Executive Report, and Universal Search */} @@ -938,341 +719,19 @@ const Page = () => { {/* Left Column */} - {/* Secure Score */} - - - - Secure Score - - } - sx={{ pb: 1 }} - /> - - - {testsApi.isFetching ? ( - - - - ) : reportData.SecureScore && reportData.SecureScore.length > 0 ? ( - - new Date(a.createdDateTime) - new Date(b.createdDateTime) - ).map((score) => ({ - date: new Date(score.createdDateTime).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }), - score: score.currentScore, - percentage: Math.round((score.currentScore / score.maxScore) * 100), - }))} - margin={{ left: 12, right: 12, top: 10, bottom: 10 }} - > - - - - { - if (name === "score") return [value.toFixed(2), "Score"]; - if (name === "percentage") return [value + "%", "Percentage"]; - return value; - }} - /> - - - - ) : ( - - - No secure score data available - - - )} - - - The Secure Score measures your security posture across your tenant. - - - - - {testsApi.isFetching ? ( - - - - - - - - - - - - - - ) : reportData.SecureScore && reportData.SecureScore.length > 0 ? ( - - - - Latest % - - - {Math.round( - (reportData.SecureScore[reportData.SecureScore.length - 1] - .currentScore / - reportData.SecureScore[reportData.SecureScore.length - 1] - .maxScore) * - 100 - )} - % - - - - - - Current Score - - - {reportData.SecureScore[ - reportData.SecureScore.length - 1 - ].currentScore.toFixed(2)} - - - - - - Max Score - - - {reportData.SecureScore[ - reportData.SecureScore.length - 1 - ].maxScore.toFixed(2)} - - - - ) : ( - - Enable secure score monitoring in your tenant - - )} - - - - {/* All Users Auth Methods */} - - - - All users auth methods - - } - sx={{ pb: 1 }} - /> - - - {testsApi.isFetching ? ( - - ) : reportData.TenantInfo.OverviewAuthMethodsAllUsers?.nodes ? ( - - ) : ( - - - No authentication method data available - - - )} - - - {reportData.TenantInfo.OverviewAuthMethodsAllUsers?.description || - "No description available"} - - - + + {/* Right Column */} - {/* User Authentication */} - - - - User authentication - - } - sx={{ pb: 1 }} - /> - - - {testsApi.isFetching ? ( - - ) : reportData.TenantInfo.OverviewCaMfaAllUsers?.nodes ? ( - - ) : ( - - - No MFA data available - - - )} - - - {reportData.TenantInfo.OverviewCaMfaAllUsers?.description || - "No description available"} - - - - - {/* License Overview */} - - - - License Overview - - } - sx={{ pb: 1 }} - /> - - - {testsApi.isFetching ? ( - - ) : testsApi.data?.LicenseData && Array.isArray(testsApi.data.LicenseData) ? ( - - ) : ( - - - No license data available - - - )} - - - - - {testsApi.isFetching ? ( - - - - - - - - - - - - - - - - - ) : testsApi.data?.LicenseData && Array.isArray(testsApi.data.LicenseData) ? ( - - - - Total Licenses - - - {testsApi.data.LicenseData.reduce( - (sum, lic) => sum + (parseInt(lic?.TotalLicenses || 0) || 0), - 0 - ).toLocaleString()} - - - - - - Assigned - - - {testsApi.data.LicenseData.reduce( - (sum, lic) => sum + (parseInt(lic?.CountUsed || 0) || 0), - 0 - ).toLocaleString()} - - - - - - Available - - - {testsApi.data.LicenseData.reduce( - (sum, lic) => sum + (parseInt(lic?.CountAvailable || 0) || 0), - 0 - ).toLocaleString()} - - - - ) : ( - - - No license statistics available - - - )} - - + + @@ -1637,246 +1096,6 @@ const Page = () => { - - {/* Desktop Devices - Full Width */} - - - - - License Overview - - } - sx={{ pb: 1 }} - /> - - - {testsApi.isFetching ? ( - - ) : testsApi.data?.LicenseData ? ( - - ) : ( - - - No license data available - - - )} - - - Overview of license assignments and availability - - - - - {testsApi.isFetching ? ( - - - - - - - - - - - - - - - - - ) : testsApi.data?.LicenseData && Array.isArray(testsApi.data.LicenseData) ? ( - - - - Total Licenses - - - {testsApi.data.LicenseData.reduce( - (sum, lic) => sum + (parseInt(lic?.TotalLicenses || 0) || 0), - 0 - ).toLocaleString()} - - - - - - Assigned - - - {testsApi.data.LicenseData.reduce( - (sum, lic) => sum + (parseInt(lic?.CountUsed || 0) || 0), - 0 - ).toLocaleString()} - - - - - - Available - - - {testsApi.data.LicenseData.reduce( - (sum, lic) => sum + (parseInt(lic?.CountAvailable || 0) || 0), - 0 - ).toLocaleString()} - - - - ) : ( - - - No license statistics available - - - )} - - - - - {/* Mobile Devices - Full Width */} - - - - - Mobile devices - - } - sx={{ pb: 1 }} - /> - - - {testsApi.isFetching ? ( - - ) : reportData.TenantInfo.DeviceOverview?.MobileSummary?.nodes ? ( - - ) : ( - - - No mobile device data available - - - )} - - - {reportData.TenantInfo.DeviceOverview.MobileSummary?.description || - "No description available"} - - - - - {testsApi.isFetching ? ( - - - - - - - - - - - - - - ) : ( - - - - 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; - })()} - - - - )} - - - From 908c62fa7982f09bc6f1e8ec6db0bdaa71035906 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 31 Dec 2025 13:08:54 +0100 Subject: [PATCH 020/111] Index updates --- .../CippComponents/AssessmentCard.jsx | 125 +++++ .../CippComponents/TenantInfoCard.jsx | 70 +++ .../CippComponents/TenantMetricsGrid.jsx | 100 ++++ src/pages/dashboardv2/index.js | 442 +----------------- 4 files changed, 304 insertions(+), 433 deletions(-) create mode 100644 src/components/CippComponents/AssessmentCard.jsx create mode 100644 src/components/CippComponents/TenantInfoCard.jsx create mode 100644 src/components/CippComponents/TenantMetricsGrid.jsx diff --git a/src/components/CippComponents/AssessmentCard.jsx b/src/components/CippComponents/AssessmentCard.jsx new file mode 100644 index 000000000000..06cb5777c6b2 --- /dev/null +++ b/src/components/CippComponents/AssessmentCard.jsx @@ -0,0 +1,125 @@ +import React from "react"; +import { Card, CardHeader, CardContent, Box, Typography, Skeleton } from "@mui/material"; +import { Security as SecurityIcon } from "@mui/icons-material"; +import { ResponsiveContainer, RadialBarChart, RadialBar, PolarAngleAxis } from "recharts"; +import { CippTimeAgo } from "../CippComponents/CippTimeAgo"; + +export const AssessmentCard = ({ data, isLoading }) => { + // Extract data with null safety + const identityPassed = data?.TestResultSummary?.IdentityPassed || 0; + const identityTotal = data?.TestResultSummary?.IdentityTotal || 1; + const devicesPassed = data?.TestResultSummary?.DevicesPassed || 0; + const devicesTotal = data?.TestResultSummary?.DevicesTotal || 1; + const latestReportTimeStamp = data?.ExecutedAt; + + // Calculate percentages for the radial chart + const devicesPercentage = (devicesPassed / devicesTotal) * 100; + const identityPercentage = (identityPassed / identityTotal) * 100; + + const chartData = [ + { + value: devicesPercentage, + fill: "#22c55e", + }, + { + value: identityPercentage, + fill: "#3b82f6", + }, + ]; + + return ( + + + + Assessment + + } + sx={{ pb: 1.5 }} + /> + + + + + + Identity + + + {isLoading ? ( + + ) : ( + <> + {identityPassed}/{identityTotal} + + tests + + + )} + + + + + Devices + + + {isLoading ? ( + + ) : ( + <> + {devicesPassed}/{devicesTotal} + + tests + + + )} + + + + + Last Data Collection + + + {isLoading ? ( + + ) : latestReportTimeStamp ? ( + + ) : ( + "Not Available" + )} + + + + + {isLoading ? ( + + ) : ( + + + + + + + )} + + + + + ); +}; diff --git a/src/components/CippComponents/TenantInfoCard.jsx b/src/components/CippComponents/TenantInfoCard.jsx new file mode 100644 index 000000000000..cd4b753e8f2c --- /dev/null +++ b/src/components/CippComponents/TenantInfoCard.jsx @@ -0,0 +1,70 @@ +import { Box, Card, CardHeader, CardContent, Typography, Skeleton } from "@mui/material"; +import { Business as BuildingIcon } from "@mui/icons-material"; +import { CippCopyToClipBoard } from "./CippCopyToClipboard"; + +export const TenantInfoCard = ({ data, isLoading }) => { + return ( + + + + Tenant + + } + sx={{ pb: 1.5 }} + /> + + + + + Name + + {isLoading ? ( + + ) : ( + + {data?.displayName || "Not Available"} + + )} + + + + Tenant ID + + + {isLoading ? ( + + ) : data?.id ? ( + + ) : ( + + Not Available + + )} + + + + + Primary Domain + + + {isLoading ? ( + + ) : data?.verifiedDomains?.find((d) => d.isDefault)?.name ? ( + d.isDefault).name} + type="chip" + /> + ) : ( + + Not Available + + )} + + + + + + ); +}; diff --git a/src/components/CippComponents/TenantMetricsGrid.jsx b/src/components/CippComponents/TenantMetricsGrid.jsx new file mode 100644 index 000000000000..b8b0cfacc272 --- /dev/null +++ b/src/components/CippComponents/TenantMetricsGrid.jsx @@ -0,0 +1,100 @@ +import { Box, Grid, Tooltip, Avatar, Typography, Skeleton } from "@mui/material"; +import { + Person as UserIcon, + PersonOutline as GuestIcon, + Group as GroupIcon, + Apps as AppsIcon, + Devices as DevicesIcon, + PhoneAndroid as ManagedIcon, +} from "@mui/icons-material"; + +const formatNumber = (num) => { + if (num >= 1000000) return (num / 1000000).toFixed(1) + "M"; + if (num >= 1000) return (num / 1000).toFixed(1) + "K"; + return num?.toString() || "0"; +}; + +export const TenantMetricsGrid = ({ data, isLoading }) => { + const metrics = [ + { + label: "Users", + value: data?.UserCount || 0, + icon: UserIcon, + color: "primary", + }, + { + label: "Guests", + value: data?.GuestCount || 0, + icon: GuestIcon, + color: "info", + }, + { + label: "Groups", + value: data?.GroupCount || 0, + icon: GroupIcon, + color: "secondary", + }, + { + label: "Service Principals", + value: data?.ApplicationCount || 0, + icon: AppsIcon, + color: "error", + }, + { + label: "Devices", + value: data?.DeviceCount || 0, + icon: DevicesIcon, + color: "warning", + }, + { + label: "Managed", + value: data?.ManagedDeviceCount || 0, + icon: ManagedIcon, + color: "success", + }, + ]; + + return ( + + {metrics.map((metric) => { + const IconComponent = metric.icon; + return ( + + + + + + + + + {metric.label} + + + {isLoading ? : formatNumber(metric.value)} + + + + + + ); + })} + + ); +}; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 49768b605e55..0fcbf6027446 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -28,9 +28,6 @@ import { RadialBarChart, RadialBar, PolarAngleAxis, - LineChart, - Line, - CartesianGrid, XAxis, YAxis, ResponsiveContainer, @@ -45,26 +42,19 @@ import { SecureScoreCard } from "/src/components/CippComponents/SecureScoreCard" import { MFACard } from "/src/components/CippComponents/MFACard"; import { AuthMethodCard } from "/src/components/CippComponents/AuthMethodCard"; import { LicenseCard } from "/src/components/CippComponents/LicenseCard"; +import { TenantInfoCard } from "/src/components/CippComponents/TenantInfoCard"; +import { TenantMetricsGrid } from "/src/components/CippComponents/TenantMetricsGrid"; +import { AssessmentCard } from "/src/components/CippComponents/AssessmentCard"; import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; import { CippAddTestReportDrawer } from "/src/components/CippComponents/CippAddTestReportDrawer"; -import { CippCopyToClipBoard } from "/src/components/CippComponents/CippCopyToClipboard.jsx"; import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo.jsx"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { - People as UsersIcon, - Person as UserIcon, - PersonOutline as GuestIcon, - Group as GroupIcon, - Apps as AppsIcon, Devices as DevicesIcon, - PhoneAndroid as ManagedIcon, Security as SecurityIcon, - Business as BuildingIcon, CheckCircle as CheckCircleIcon, - Laptop as MonitorIcon, Work as BriefcaseIcon, - CardMembership as CardMembershipIcon, } from "@mui/icons-material"; const Page = () => { @@ -282,434 +272,20 @@ const Page = () => { {/* Column 1: Tenant Information */} - - - - Tenant - - } - sx={{ pb: 1.5 }} - /> - - - - - Name - - {organization.isFetching ? ( - - ) : ( - - {organization.data?.displayName || "Not Available"} - - )} - - - - Tenant ID - - - {organization.isFetching ? ( - - ) : organization.data?.id ? ( - - ) : ( - - Not Available - - )} - - - - - Primary Domain - - - {organization.isFetching ? ( - - ) : organization.data?.verifiedDomains?.find((d) => d.isDefault)?.name || - currentTenant ? ( - d.isDefault)?.name || - currentTenant - } - type="chip" - /> - ) : ( - - Not Available - - )} - - - - - + {/* Column 2: Tenant Metrics - 2x3 Grid */} - - - - - - - - - - Users - - - {testsApi.isFetching ? ( - - ) : ( - formatNumber(reportData.TenantInfo.TenantOverview.UserCount) - )} - - - - - - - - - - - - - - Guests - - - {testsApi.isFetching ? ( - - ) : ( - formatNumber(reportData.TenantInfo.TenantOverview.GuestCount) - )} - - - - - - - - - - - - - - Groups - - - {testsApi.isFetching ? ( - - ) : ( - formatNumber(reportData.TenantInfo.TenantOverview.GroupCount) - )} - - - - - - - - - - - - - - Service Principals - - - {testsApi.isFetching ? ( - - ) : ( - formatNumber(reportData.TenantInfo.TenantOverview.ApplicationCount) - )} - - - - - - - - - - - - - - Devices - - - {testsApi.isFetching ? ( - - ) : ( - formatNumber(reportData.TenantInfo.TenantOverview.DeviceCount) - )} - - - - - - - - - - - - - - Managed - - - {testsApi.isFetching ? ( - - ) : ( - formatNumber(reportData.TenantInfo.TenantOverview.ManagedDeviceCount) - )} - - - - - - + {/* Column 3: Assessment Results */} - - - - Assessment - - } - sx={{ pb: 1.5 }} - /> - - - - - - Identity - - - {testsApi.isFetching ? ( - - ) : ( - <> - {reportData.TestResultSummary.IdentityPassed}/ - {reportData.TestResultSummary.IdentityTotal} - - tests - - - )} - - - - - Devices - - - {testsApi.isFetching ? ( - - ) : ( - <> - {reportData.TestResultSummary.DevicesPassed}/ - {reportData.TestResultSummary.DevicesTotal} - - tests - - - )} - - - - - Last Data Collection - - - {testsApi.isFetching ? ( - - ) : testsApi.data?.LatestReportTimeStamp ? ( - - ) : ( - "Not Available" - )} - - - - - - - - - - - - - - + From 7d0d726635453d46c9b986c2b3c0532daef2e183 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 31 Dec 2025 16:09:11 -0500 Subject: [PATCH 021/111] Reverse spinner animation direction Changed the keyframes for the spinner animation to rotate -360deg instead of 360deg, reversing the direction of the spin. fixes #5146 --- src/components/CippTable/CIPPTableToptoolbar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 3004db8ebf66..f7442220a3f7 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -652,7 +652,7 @@ export const CIPPTableToptoolbar = ({ : "none", "@keyframes spin": { "0%": { transform: "rotate(0deg)" }, - "100%": { transform: "rotate(360deg)" }, + "100%": { transform: "rotate(-360deg)" }, }, }} > From e911d25fd423a6e24bb41fb67858fda35b76ebb6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 1 Jan 2026 16:04:13 +0100 Subject: [PATCH 022/111] dashboard updates --- src/pages/dashboardv2/index.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 0fcbf6027446..1fb8334c6274 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -5,9 +5,7 @@ import { CardHeader, Container, Typography, - Avatar, Divider, - Tooltip, Button, Skeleton, } from "@mui/material"; @@ -25,9 +23,6 @@ import { Bar, PieChart, Pie, - RadialBarChart, - RadialBar, - PolarAngleAxis, XAxis, YAxis, ResponsiveContainer, @@ -48,11 +43,9 @@ import { AssessmentCard } from "/src/components/CippComponents/AssessmentCard"; import { CippUniversalSearch } from "/src/components/CippCards/CippUniversalSearch.jsx"; import { CippApiDialog } from "/src/components/CippComponents/CippApiDialog"; import { CippAddTestReportDrawer } from "/src/components/CippComponents/CippAddTestReportDrawer"; -import { CippTimeAgo } from "/src/components/CippComponents/CippTimeAgo.jsx"; import CippFormComponent from "/src/components/CippComponents/CippFormComponent"; import { Devices as DevicesIcon, - Security as SecurityIcon, CheckCircle as CheckCircleIcon, Work as BriefcaseIcon, } from "@mui/icons-material"; From 578e07010756879deac0b5c5066269b103dee858 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 00:07:36 +0100 Subject: [PATCH 023/111] Design updates --- src/utils/get-cipp-formatting.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 36eb740bbe15..2450fc4eb297 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -796,6 +796,38 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr /> ); } + if (cellName === "Status" || cellName === "Risk" || cellName === "UserImpact") { + let color = "default"; + let label = data; + + switch (data.toLowerCase()) { + case "success": + color = "success"; + break; + case "passed": + color = "success"; + break; + case "failed": + case "high": + color = "error"; + break; + case "in progress": + color = "info"; + break; + case "not started": + color = "default"; + break; + case "investigate": + case "medium": + case "warning": + case "skipped": + color = "warning"; + break; + default: + color = "default"; + } + return isText ? label : ; + } // ISO 8601 Duration Formatting // Add property names here to automatically format ISO 8601 duration strings (e.g., "PT1H23M30S") From f17a8bfa6613bfab015a6d04c0e5b34d31a3dd44 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:24:47 +0100 Subject: [PATCH 024/111] up --- src/components/CippComponents/AssessmentCard.jsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/CippComponents/AssessmentCard.jsx b/src/components/CippComponents/AssessmentCard.jsx index 06cb5777c6b2..a1b1b5e03e27 100644 --- a/src/components/CippComponents/AssessmentCard.jsx +++ b/src/components/CippComponents/AssessmentCard.jsx @@ -10,7 +10,6 @@ export const AssessmentCard = ({ data, isLoading }) => { const identityTotal = data?.TestResultSummary?.IdentityTotal || 1; const devicesPassed = data?.TestResultSummary?.DevicesPassed || 0; const devicesTotal = data?.TestResultSummary?.DevicesTotal || 1; - const latestReportTimeStamp = data?.ExecutedAt; // Calculate percentages for the radial chart const devicesPercentage = (devicesPassed / devicesTotal) * 100; @@ -92,8 +91,8 @@ export const AssessmentCard = ({ data, isLoading }) => { {isLoading ? ( - ) : latestReportTimeStamp ? ( - + ) : data?.ExecutedAt ? ( + ) : ( "Not Available" )} From e2f4122fc7bada41bdd12cb7b46fe5620c85891d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 02:44:41 +0100 Subject: [PATCH 025/111] Fix silly bug --- src/components/CippComponents/AssessmentCard.jsx | 1 - src/pages/dashboardv2/index.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/CippComponents/AssessmentCard.jsx b/src/components/CippComponents/AssessmentCard.jsx index a1b1b5e03e27..957e57320e7e 100644 --- a/src/components/CippComponents/AssessmentCard.jsx +++ b/src/components/CippComponents/AssessmentCard.jsx @@ -1,4 +1,3 @@ -import React from "react"; import { Card, CardHeader, CardContent, Box, Typography, Skeleton } from "@mui/material"; import { Security as SecurityIcon } from "@mui/icons-material"; import { ResponsiveContainer, RadialBarChart, RadialBar, PolarAngleAxis } from "recharts"; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 1fb8334c6274..0ec20ec2d71c 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -121,7 +121,7 @@ const Page = () => { const reportData = testsApi.isSuccess && testsApi.data?.TenantCounts ? { - ExecutedAt: testsApi.data.LatestReportTimeStamp || new Date().toISOString(), + ExecutedAt: testsApi.data?.LatestReportTimeStamp || null, TenantName: organization.data?.displayName || "", Domain: currentTenant || "", TestResultSummary: { From 28084ff28197b60370255efcfc29618a66a30e30 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:59:02 +0100 Subject: [PATCH 026/111] frontend updates --- .../CippTestDetailOffCanvas.jsx | 258 ++++++++++++++++++ src/pages/dashboardv2/devices/index.js | 197 +------------ src/pages/dashboardv2/identity/index.js | 198 +------------- 3 files changed, 262 insertions(+), 391 deletions(-) create mode 100644 src/components/CippTestDetail/CippTestDetailOffCanvas.jsx diff --git a/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx b/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx new file mode 100644 index 000000000000..a6571350e3e7 --- /dev/null +++ b/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx @@ -0,0 +1,258 @@ +import React from "react"; +import { + Card, + CardContent, + Box, + Stack, + Chip, + Typography, +} from "@mui/material"; +import { KeyboardArrowRight } from "@mui/icons-material"; +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { Grid } from "@mui/system"; + +const getStatusColor = (status) => { + switch (status?.toLowerCase()) { + case "passed": + return "success"; + case "failed": + return "error"; + case "investigate": + return "warning"; + case "skipped": + return "default"; + default: + return "default"; + } +}; + +const getRiskColor = (risk) => { + switch (risk?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } +}; + +const getImpactColor = (impact) => { + switch (impact?.toLowerCase()) { + case "high": + return "error"; + case "medium": + return "warning"; + case "low": + return "info"; + default: + return "default"; + } +}; + +// Shared markdown styling for consistent rendering +const markdownStyles = { + "& a": { + color: (theme) => theme.palette.primary.main, + textDecoration: "underline", + "&:hover": { + textDecoration: "none", + }, + }, + color: "text.secondary", + fontSize: "0.875rem", + lineHeight: 1.43, + "& p": { + my: 1, + }, + "& ul": { + my: 1, + pl: 2, + }, + "& li": { + my: 0.5, + }, + "& h1, & h2, & h3, & h4, & h5, & h6": { + mt: 2, + mb: 1, + fontWeight: "bold", + }, + "& table": { + width: "100%", + borderCollapse: "collapse", + marginTop: 2, + marginBottom: 2, + }, + "& th, & td": { + border: 1, + borderColor: "divider", + padding: 1, + textAlign: "left", + }, + "& th": { + backgroundColor: "action.hover", + fontWeight: "bold", + }, + "& code": { + backgroundColor: "action.hover", + padding: "2px 6px", + borderRadius: 1, + fontSize: "0.85em", + }, + "& pre": { + backgroundColor: "action.hover", + padding: 2, + borderRadius: 1, + overflow: "auto", + }, +}; + +export const CippTestDetailOffCanvas = ({ row }) => { + return ( + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + Risk Level + + + + + + + + ({ + xs: `1px solid ${theme.palette.divider}`, + md: "none", + }), + borderRight: (theme) => ({ + md: `1px solid ${theme.palette.divider}`, + }), + }} + > + + + + User Impact + + + + + + + + + + + + Implementation Effort + + + + + + + + + + + {row.ResultMarkdown && ( + + + + {row.Name} + + + + ( +
+ {children} + + ), + }} + > + {row.ResultMarkdown} + + + + + )} + + + + + + What did we check + + + {row.Category && ( + + + Category + + {row.Category} + + )} + + {row.Description && ( + + ( + + {children} + + ), + }} + > + {row.Description} + + + )} + + + + + ); +}; diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index 56e932811359..e68373218bc9 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -15,10 +15,7 @@ import tabOptions from "../tabOptions"; import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall.jsx"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; -import { KeyboardArrowRight } from "@mui/icons-material"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import { Grid } from "@mui/system"; +import { CippTestDetailOffCanvas } from "/src/components/CippTestDetail/CippTestDetailOffCanvas"; import { useRouter } from "next/router"; const Page = () => { @@ -80,197 +77,7 @@ const Page = () => { const offCanvas = { size: "lg", - children: (row) => { - return ( - - - - ({ - xs: `1px solid ${theme.palette.divider}`, - md: "none", - }), - borderRight: (theme) => ({ - md: `1px solid ${theme.palette.divider}`, - }), - }} - > - - - - Risk Level - - - - - - - - ({ - xs: `1px solid ${theme.palette.divider}`, - md: "none", - }), - borderRight: (theme) => ({ - md: `1px solid ${theme.palette.divider}`, - }), - }} - > - - - - User Impact - - - - - - - - - - - - Implementation Effort - - - - - - - - - - - {row.ResultMarkdown && ( - - - - {row.Name} - - - theme.palette.primary.main, - textDecoration: "underline", - "&:hover": { - textDecoration: "none", - }, - }, - color: "text.secondary", - fontSize: "0.875rem", - lineHeight: 1.43, - "& p": { - my: 1, - }, - "& ul": { - my: 1, - pl: 2, - }, - "& li": { - my: 0.5, - }, - "& h1, & h2, & h3, & h4, & h5, & h6": { - mt: 2, - mb: 1, - fontWeight: "bold", - }, - "& code": { - backgroundColor: "action.hover", - padding: "2px 6px", - borderRadius: 1, - fontSize: "0.85em", - }, - "& pre": { - backgroundColor: "action.hover", - padding: 2, - borderRadius: 1, - overflow: "auto", - }, - }} - > - ( - - {children} - - ), - }} - > - {row.ResultMarkdown} - - - - - )} - - - - - - What did we check - - - {row.Category && ( - - - Category - - {row.Category} - - )} - - - - This test verifies that device compliance policies are properly configured in - Microsoft Intune. Compliance policies define the requirements that devices must - meet to access corporate resources, such as encryption, password requirements, - and operating system versions. - - - Why this matters: Non-compliant devices pose significant - security risks to your organization. They may lack critical security updates, - have weak authentication, or be missing essential security features like - encryption. Properly configured compliance policies ensure only secure devices - can access sensitive data. - - - Recommendation: Create comprehensive compliance policies that - cover all device platforms (Windows, iOS, Android, macOS). Configure Conditional - Access to block non-compliant devices and set up automated remediation actions - to help users bring their devices back into compliance. - - - - - - - ); - }, + children: (row) => , }; const filters = [ diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index 463427213efa..fc735085a08c 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -15,11 +15,7 @@ import tabOptions from "../tabOptions"; import { useSettings } from "/src/hooks/use-settings"; import { ApiGetCall } from "/src/api/ApiCall.jsx"; import { CippDataTable } from "/src/components/CippTable/CippDataTable"; -import { ArrowRight, Info, KeyboardArrowRight } from "@mui/icons-material"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; -import { Grid } from "@mui/system"; -import { ArrowLongRightIcon } from "@heroicons/react/24/outline"; +import { CippTestDetailOffCanvas } from "/src/components/CippTestDetail/CippTestDetailOffCanvas"; import { useRouter } from "next/router"; const Page = () => { @@ -81,197 +77,7 @@ const Page = () => { const offCanvas = { size: "lg", - children: (row) => { - return ( - - - - ({ - xs: `1px solid ${theme.palette.divider}`, - md: "none", - }), - borderRight: (theme) => ({ - md: `1px solid ${theme.palette.divider}`, - }), - }} - > - - - - Risk Level - - - - - - - - ({ - xs: `1px solid ${theme.palette.divider}`, - md: "none", - }), - borderRight: (theme) => ({ - md: `1px solid ${theme.palette.divider}`, - }), - }} - > - - - - User Impact - - - - - - - - - - - - Implementation Effort - - - - - - - - - - - {row.ResultMarkdown && ( - - - - {row.Name} - - - theme.palette.primary.main, - textDecoration: "underline", - "&:hover": { - textDecoration: "none", - }, - }, - color: "text.secondary", - fontSize: "0.875rem", - lineHeight: 1.43, - "& p": { - my: 1, - }, - "& ul": { - my: 1, - pl: 2, - }, - "& li": { - my: 0.5, - }, - "& h1, & h2, & h3, & h4, & h5, & h6": { - mt: 2, - mb: 1, - fontWeight: "bold", - }, - "& code": { - backgroundColor: "action.hover", - padding: "2px 6px", - borderRadius: 1, - fontSize: "0.85em", - }, - "& pre": { - backgroundColor: "action.hover", - padding: 2, - borderRadius: 1, - overflow: "auto", - }, - }} - > - ( - - {children} - - ), - }} - > - {row.ResultMarkdown} - - - - - )} - - - - - - What did we check - - - {row.Category && ( - - - Category - - {row.Category} - - )} - - - - This test verifies that Multi-Factor Authentication (MFA) is enabled for all - administrative accounts in your Microsoft 365 tenant. Administrative accounts - have elevated privileges and are prime targets for attackers. Enabling MFA adds - an additional layer of security by requiring a second form of verification - beyond just a password. - - - Why this matters: According to Microsoft, MFA can block over - 99.9% of account compromise attacks. Without MFA, compromised administrator - credentials can lead to complete tenant takeover, data breaches, and significant - business disruption. - - - Recommendation: Ensure all users with administrative roles have - MFA enabled through Conditional Access policies or per-user MFA settings. - Consider using stronger authentication methods like FIDO2 security keys or the - Microsoft Authenticator app for your most privileged accounts. - - - - - - - ); - }, + children: (row) => , }; const filters = [ From 847f13796d10784f414daa55038baba98132884f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:59:06 +0100 Subject: [PATCH 027/111] frontend updates --- .../CippTestDetail/CippTestDetailOffCanvas.jsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx b/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx index a6571350e3e7..2d20e2294c1b 100644 --- a/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx +++ b/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx @@ -1,12 +1,5 @@ import React from "react"; -import { - Card, - CardContent, - Box, - Stack, - Chip, - Typography, -} from "@mui/material"; +import { Card, CardContent, Box, Stack, Chip, Typography } from "@mui/material"; import { KeyboardArrowRight } from "@mui/icons-material"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; From e41086195ba4fcfa502448acd1840ed2c5a222bf Mon Sep 17 00:00:00 2001 From: Logan Cook <2997336+MWG-Logan@users.noreply.github.com> Date: Fri, 2 Jan 2026 11:43:28 -0500 Subject: [PATCH 028/111] feat(alerts): add Intune policy conflict alert configuration Closes #5149 --- src/data/alerts.json | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index 497436c7d6a3..7c9917eecba3 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -187,6 +187,41 @@ "label": "Alert on device compliance issues", "recommendedRunInterval": "4h" }, + { + "name": "IntunePolicyConflicts", + "label": "Alert on Intune policy or app conflicts/errors", + "recommendedRunInterval": "4h", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "switch", + "inputLabel": "Alert per issue (off = aggregated)", + "inputName": "AlertEachIssue" + }, + { + "inputType": "switch", + "inputLabel": "Include policy status issues", + "inputName": "IncludePolicies" + }, + { + "inputType": "switch", + "inputLabel": "Include app install issues", + "inputName": "IncludeApplications" + }, + { + "inputType": "switch", + "inputLabel": "Alert on conflicts", + "inputName": "AlertConflicts" + }, + { + "inputType": "switch", + "inputLabel": "Alert on errors/failures", + "inputName": "AlertErrors" + } + ], + "description": "Monitors Intune policy assignment states and app install statuses for conflicts or errors. Defaults to aggregated alerts with all mechanisms enabled and both conflicts and errors included." + }, { "name": "BreachAlert", "label": "Alert on (new) potentially breached passwords. Generates an alert if a password is found to be breached.", From 8f718861cc3da3119f4bc647fe811f9e3b0c9c47 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:55:34 +0100 Subject: [PATCH 029/111] update url. --- src/data/Extensions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index c6d8ced44146..21471f5e295c 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -274,7 +274,7 @@ "links": [ { "name": "HaloPSA Documentation", - "url": "https://halopsa.com/guides/" + "url": "https://usehalo.com/halopsa/guides/2697" } ], "SettingOptions": [ From b70a305381684a494f2174db36e67a8ececa31f3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:58:04 +0100 Subject: [PATCH 030/111] minor updates --- src/pages/dashboardv2/identity/index.js | 53 +------------------------ 1 file changed, 1 insertion(+), 52 deletions(-) diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index fc735085a08c..7849bb0d6079 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -1,14 +1,4 @@ -import React from "react"; -import { - Container, - Typography, - Card, - CardContent, - CardHeader, - Box, - Stack, - Chip, -} from "@mui/material"; +import { Container } from "@mui/material"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "../tabOptions"; @@ -34,47 +24,6 @@ const Page = () => { const identityTests = testsApi.data?.TestResults?.filter((test) => test.TestType === "Identity") || []; - const getStatusColor = (status) => { - switch (status?.toLowerCase()) { - case "passed": - return "success"; - case "failed": - return "error"; - case "investigate": - return "warning"; - case "skipped": - return "default"; - default: - return "default"; - } - }; - - const getRiskColor = (risk) => { - switch (risk?.toLowerCase()) { - case "high": - return "error"; - case "medium": - return "warning"; - case "low": - return "info"; - default: - return "default"; - } - }; - - const getImpactColor = (impact) => { - switch (impact?.toLowerCase()) { - case "high": - return "error"; - case "medium": - return "warning"; - case "low": - return "info"; - default: - return "default"; - } - }; - const offCanvas = { size: "lg", children: (row) => , From f0fc90be90efaffba0a709257703b6cd57613356 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 00:55:36 +0100 Subject: [PATCH 031/111] Updates for tests --- .../CippAddTestReportDrawer.jsx | 315 +++++++++++++++--- src/pages/dashboardv2/index.js | 33 +- 2 files changed, 294 insertions(+), 54 deletions(-) diff --git a/src/components/CippComponents/CippAddTestReportDrawer.jsx b/src/components/CippComponents/CippAddTestReportDrawer.jsx index e5d76ee9554e..ec63590e3ce3 100644 --- a/src/components/CippComponents/CippAddTestReportDrawer.jsx +++ b/src/components/CippComponents/CippAddTestReportDrawer.jsx @@ -1,8 +1,23 @@ import React, { useState, useEffect } from "react"; -import { Button } from "@mui/material"; +import { + Button, + Card, + CardContent, + Checkbox, + FormControlLabel, + TextField, + Typography, + Box, + Chip, + Tab, + Tabs, + InputAdornment, + Paper, + Stack, +} from "@mui/material"; import { Grid } from "@mui/system"; -import { useForm, useFormState } from "react-hook-form"; -import { Add } from "@mui/icons-material"; +import { useForm, useFormState, useWatch } from "react-hook-form"; +import { Add, Search, CheckBox, CheckBoxOutlineBlank } from "@mui/icons-material"; import { CippOffCanvas } from "./CippOffCanvas"; import CippFormComponent from "./CippFormComponent"; import { CippApiResults } from "./CippApiResults"; @@ -10,6 +25,8 @@ import { ApiPostCall, ApiGetCall } from "../../api/ApiCall"; export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) => { const [drawerVisible, setDrawerVisible] = useState(false); + const [activeTab, setActiveTab] = useState(0); + const [searchTerm, setSearchTerm] = useState(""); const formControl = useForm({ mode: "onChange", @@ -22,6 +39,8 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) }); const { isValid } = useFormState({ control: formControl.control }); + const selectedIdentityTests = useWatch({ control: formControl.control, name: "IdentityTests" }) || []; + const selectedDeviceTests = useWatch({ control: formControl.control, name: "DevicesTests" }) || []; const createReport = ApiPostCall({ urlFromData: true, @@ -69,6 +88,8 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) const handleCloseDrawer = () => { setDrawerVisible(false); + setSearchTerm(""); + setActiveTab(0); formControl.reset({ name: "", description: "", @@ -77,6 +98,43 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) }); }; + const toggleTest = (testId, testType) => { + const fieldName = testType === "Identity" ? "IdentityTests" : "DevicesTests"; + const currentTests = formControl.getValues(fieldName) || []; + + if (currentTests.includes(testId)) { + formControl.setValue( + fieldName, + currentTests.filter((id) => id !== testId), + { shouldValidate: true } + ); + } else { + formControl.setValue(fieldName, [...currentTests, testId], { shouldValidate: true }); + } + }; + + const isTestSelected = (testId, testType) => { + return testType === "Identity" + ? selectedIdentityTests.includes(testId) + : selectedDeviceTests.includes(testId); + }; + + const filterTests = (tests) => { + if (!searchTerm) return tests; + return tests.filter( + (test) => + test.id.toLowerCase().includes(searchTerm.toLowerCase()) || + test.name.toLowerCase().includes(searchTerm.toLowerCase()) + ); + }; + + const currentTests = + activeTab === 0 + ? filterTests(availableTests.IdentityTests || []) + : filterTests(availableTests.DevicesTests || []); + + const currentTestType = activeTab === 0 ? "Identity" : "Devices"; + return ( <> - + @@ -239,10 +249,15 @@ const Page = () => { From 1fe9dfb4da01054444d302f98d03649917c405de Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 3 Jan 2026 02:37:30 +0100 Subject: [PATCH 034/111] Remove checkboxes --- .../CippAddTestReportDrawer.jsx | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/components/CippComponents/CippAddTestReportDrawer.jsx b/src/components/CippComponents/CippAddTestReportDrawer.jsx index 2f9a2536c436..64a2af507867 100644 --- a/src/components/CippComponents/CippAddTestReportDrawer.jsx +++ b/src/components/CippComponents/CippAddTestReportDrawer.jsx @@ -3,21 +3,18 @@ import { Button, Card, CardContent, - Checkbox, - FormControlLabel, TextField, Typography, Box, Chip, Tab, Tabs, - InputAdornment, Paper, Stack, } from "@mui/material"; import { Grid } from "@mui/system"; import { useForm, useFormState, useWatch } from "react-hook-form"; -import { Add, Search, CheckBox, CheckBoxOutlineBlank } from "@mui/icons-material"; +import { Add } from "@mui/icons-material"; import { CippOffCanvas } from "./CippOffCanvas"; import CippFormComponent from "./CippFormComponent"; import { CippApiResults } from "./CippApiResults"; @@ -277,13 +274,6 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) placeholder={`Search ${currentTestType} tests...`} value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - }} /> @@ -326,13 +316,7 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) onClick={() => toggleTest(test.id, currentTestType)} > - - } - checkedIcon={} - sx={{ p: 0, mt: -0.5 }} - /> + Date: Sat, 3 Jan 2026 03:36:56 +0100 Subject: [PATCH 035/111] create ability to run tests --- src/pages/dashboardv2/index.js | 47 +++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 4ac8030048bc..c61935b6f39f 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -48,6 +48,7 @@ import { CheckCircle as CheckCircleIcon, Work as BriefcaseIcon, Assessment as AssessmentIcon, + Refresh as RefreshIcon, } from "@mui/icons-material"; const Page = () => { @@ -56,6 +57,7 @@ const Page = () => { const { currentTenant } = settings; const [portalMenuItems, setPortalMenuItems] = useState([]); const [deleteDialog, setDeleteDialog] = useState({ open: false }); + const [refreshDialog, setRefreshDialog] = useState({ open: false }); // Get reportId from query params or default to "ztna" const selectedReport = router.query.reportId || "ztna"; @@ -108,7 +110,7 @@ const Page = () => { const driftApi = ApiGetCall({ url: "/api/listTenantDrift", data: { - TenantFilter: currentTenant, + tenantFilter: currentTenant, }, queryKey: `TenantDrift-${currentTenant}`, }); @@ -248,6 +250,35 @@ const Page = () => { /> + - setBulkActionsAnchorEl(null)} - > - handleBulkAction("accept-all-customer-specific")}> - - Accept All Deviations - Customer Specific - - handleBulkAction("accept-all")}> - - Accept All Deviations - - {/* Only show delete option if there are template deviations that support deletion */} - {processedDriftData.currentDeviations.some( - (deviation) => - (deviation.standardName?.includes("ConditionalAccessTemplate") || - deviation.standardName?.includes("IntuneTemplate")) && - deviation.expectedValue === - "This policy only exists in the tenant, not in the template." - ) && ( - handleBulkAction("deny-all-delete")}> - - Deny All Deviations - Delete - - )} - handleBulkAction("deny-all-remediate")}> - - Deny All Deviations - Remediate to align with template - - - - Remove Drift Customization - - - - - - - - {/* Accepted Deviations Section */} - {acceptedDeviationItemsWithActions.length > 0 && ( + {filterStatus.some((f) => f.value === "all" || f.value === "current") && ( - - Accepted Deviations - + {/* Header with bulk actions */} + + Current Deviations + {selectedItems.length > 0 && ( + + {/* Bulk Actions Dropdown */} + + setBulkActionsAnchorEl(null)} + > + handleBulkAction("accept-all-customer-specific")} + > + + Accept All Deviations - Customer Specific + + handleBulkAction("accept-all")}> + + Accept All Deviations + + {/* Only show delete option if there are template deviations that support deletion */} + {processedDriftData.currentDeviations.some( + (deviation) => + (deviation.standardName?.includes("ConditionalAccessTemplate") || + deviation.standardName?.includes("IntuneTemplate")) && + deviation.expectedValue === + "This policy only exists in the tenant, not in the template." + ) && ( + handleBulkAction("deny-all-delete")}> + + Deny All Deviations - Delete + + )} + handleBulkAction("deny-all-remediate")}> + + Deny All Deviations - Remediate to align with template + + + + Remove Drift Customization + + + + )} + { )} + {/* Accepted Deviations Section */} + {filterStatus.some((f) => f.value === "all" || f.value === "accepted") && + filteredAcceptedItems.length > 0 && ( + + + Accepted Deviations + + + + )} + {/* Customer Specific Deviations Section */} - {customerSpecificDeviationItemsWithActions.length > 0 && ( - - - Accepted Deviations - Customer Specific - - - - )} + {filterStatus.some((f) => f.value === "all" || f.value === "customerspecific") && + filteredCustomerSpecificItems.length > 0 && ( + + + Accepted Deviations - Customer Specific + + + + )} {/* Denied Deviations Section */} - {deniedDeviationItemsWithActions.length > 0 && ( - - - Denied Deviations - - - - )} + {filterStatus.some((f) => f.value === "all" || f.value === "denied") && + filteredDeniedItems.length > 0 && ( + + + Denied Deviations + + + + )} From e36c360237ffe4082b361beeedbbaa858f1009ec Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 02:39:31 +0100 Subject: [PATCH 041/111] improvements to UX for bannerlist card and drift --- .../CippCards/CippBannerListCard.jsx | 36 ++++++++++++++++++- src/pages/tenant/manage/drift.js | 10 +++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/components/CippCards/CippBannerListCard.jsx b/src/components/CippCards/CippBannerListCard.jsx index c1fe171cd4e2..55f5c2ecbae6 100644 --- a/src/components/CippCards/CippBannerListCard.jsx +++ b/src/components/CippCards/CippBannerListCard.jsx @@ -3,6 +3,7 @@ import { useState, useCallback } from "react"; import { Box, Card, + Checkbox, Collapse, Divider, IconButton, @@ -16,13 +17,34 @@ import { CippPropertyListCard } from "./CippPropertyListCard"; import { CippDataTable } from "../CippTable/CippDataTable"; export const CippBannerListCard = (props) => { - const { items = [], isCollapsible = false, isFetching = false, children, ...other } = props; + const { + items = [], + isCollapsible = false, + isFetching = false, + children, + onSelectionChange, + selectedItems = [], + ...other + } = props; const [expanded, setExpanded] = useState(null); const handleExpand = useCallback((itemId) => { setExpanded((prevState) => (prevState === itemId ? null : itemId)); }, []); + const handleCheckboxChange = useCallback( + (itemId, checked) => { + if (onSelectionChange) { + if (checked) { + onSelectionChange([...selectedItems, itemId]); + } else { + onSelectionChange(selectedItems.filter((id) => id !== itemId)); + } + } + }, + [onSelectionChange, selectedItems] + ); + const hasItems = items.length > 0; if (isFetching) { @@ -91,6 +113,16 @@ export const CippBannerListCard = (props) => { alignItems="center" sx={{ flex: 1, minWidth: 0 }} > + {onSelectionChange && ( + { + e.stopPropagation(); + handleCheckboxChange(item.id, e.target.checked); + }} + onClick={(e) => e.stopPropagation()} + /> + )} { "No description available"; return { - id: index + 1, + id: statusOverride ? `${statusOverride}-${index + 1}` : `current-${index + 1}`, cardLabelBox: { cardLabelBoxHeader: getDeviationIcon( statusOverride || deviation.Status || deviation.state @@ -1121,6 +1121,8 @@ const ManageDriftPage = () => { isCollapsible={true} layout={"single"} isFetching={driftApi.isFetching} + onSelectionChange={setSelectedItems} + selectedItems={selectedItems} /> )} @@ -1137,6 +1139,8 @@ const ManageDriftPage = () => { isCollapsible={true} layout={"single"} isFetching={driftApi.isFetching} + onSelectionChange={setSelectedItems} + selectedItems={selectedItems} /> )} @@ -1153,6 +1157,8 @@ const ManageDriftPage = () => { isCollapsible={true} layout={"single"} isFetching={driftApi.isFetching} + onSelectionChange={setSelectedItems} + selectedItems={selectedItems} /> )} @@ -1169,6 +1175,8 @@ const ManageDriftPage = () => { isCollapsible={true} layout={"single"} isFetching={driftApi.isFetching} + onSelectionChange={setSelectedItems} + selectedItems={selectedItems} /> )} From 426be866d5c4002455d79174bc440cc42678499f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 14:27:03 +0100 Subject: [PATCH 042/111] updates to interface --- src/pages/tenant/manage/drift.js | 155 ++++++++++++++++++++++++------- 1 file changed, 123 insertions(+), 32 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 75912315cca5..c4d611ef17a5 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -24,10 +24,6 @@ import { Chip, SvgIcon, TextField, - InputAdornment, - Card, - CardContent, - CardHeader, Divider, } from "@mui/material"; import { Grid } from "@mui/system"; @@ -135,6 +131,61 @@ const ManageDriftPage = () => { acc.deniedDeviationsList.push(...item.deniedDeviations.filter((dev) => dev !== null)); } + // Extract compliant standards from ComparisonDetails in driftSettings + if ( + item.driftSettings?.ComparisonDetails && + Array.isArray(item.driftSettings.ComparisonDetails) + ) { + const compliantStandards = item.driftSettings.ComparisonDetails.filter( + (detail) => detail.Compliant === true + ).map((detail) => { + // Strip "standards." prefix if present + let standardName = detail.StandardName; + if (standardName.startsWith("standards.")) { + standardName = standardName.substring("standards.".length); + } + + let displayName = null; + + // For template types, extract the display name from standardSettings + if (standardName.startsWith("IntuneTemplate.")) { + const guid = standardName.substring("IntuneTemplate.".length); + const intuneTemplates = item.driftSettings?.standardSettings?.IntuneTemplate; + if (Array.isArray(intuneTemplates)) { + const template = intuneTemplates.find((t) => t.TemplateList?.value === guid); + if (template?.TemplateList?.label) { + displayName = template.TemplateList.label; + } + } + } else if (standardName.startsWith("ConditionalAccessTemplate.")) { + const guid = standardName.substring("ConditionalAccessTemplate.".length); + const caTemplates = item.driftSettings?.standardSettings?.ConditionalAccessTemplate; + if (Array.isArray(caTemplates)) { + const template = caTemplates.find((t) => t.TemplateList?.value === guid); + if (template?.TemplateList?.label) { + displayName = template.TemplateList.label; + } + } + } else { + // For non-template standards, keep the "standards." prefix for lookup + standardName = detail.StandardName; + } + + return { + standardName: standardName, + standardDisplayName: displayName, // Set display name if found from templates + state: "aligned", + Status: "Aligned", + ComplianceStatus: detail.ComplianceStatus, + StandardValue: detail.StandardValue, + ReportingDisabled: detail.ReportingDisabled, + expectedValue: "Compliant with template", + receivedValue: detail.StandardValue, + }; + }); + acc.alignedStandards.push(...compliantStandards); + } + // Use the latest data collection timestamp if ( item.latestDataCollection && @@ -156,23 +207,11 @@ const ManageDriftPage = () => { acceptedDeviations: [], customerSpecificDeviationsList: [], deniedDeviationsList: [], + alignedStandards: [], latestDataCollection: null, } ); - const chartLabels = [ - "Aligned Policies", - "Accepted Deviations", - "Current Deviations", - "Customer Specific Deviations", - ]; - const chartSeries = [ - processedDriftData.alignedCount || 0, - processedDriftData.acceptedDeviationsCount || 0, - processedDriftData.currentDeviationsCount || 0, - processedDriftData.customerSpecificDeviations || 0, - ]; - // Transform currentDeviations into deviation items for display const getDeviationIcon = (state) => { switch (state?.toLowerCase()) { @@ -190,6 +229,9 @@ const ManageDriftPage = () => { return ; case "customerspecific": return ; + case "aligned": + case "compliant": + return ; default: return ; } @@ -211,6 +253,9 @@ const ManageDriftPage = () => { return "success.main"; case "customerspecific": return "info.main"; + case "aligned": + case "compliant": + return "success.main"; default: return "warning.main"; } @@ -232,6 +277,9 @@ const ManageDriftPage = () => { return "Accepted Deviation"; case "customerspecific": return "Customer Specific"; + case "aligned": + case "compliant": + return "Compliant"; default: return "Deviation"; } @@ -362,6 +410,7 @@ const ManageDriftPage = () => { processedDriftData.deniedDeviationsList, "denied" ); + const alignedStandardItems = createDeviationItems(processedDriftData.alignedStandards, "aligned"); const handleMenuClick = (event, itemId) => { setAnchorEl((prev) => ({ ...prev, [itemId]: event.currentTarget })); @@ -474,10 +523,7 @@ const ManageDriftPage = () => { }; const handleBulkAction = (action) => { - if ( - !processedDriftData.currentDeviations || - processedDriftData.currentDeviations.length === 0 - ) { + if (!selectedItems || selectedItems.length === 0) { setBulkActionsAnchorEl(null); return; } @@ -487,30 +533,51 @@ const ManageDriftPage = () => { switch (action) { case "accept-all-customer-specific": status = "CustomerSpecific"; - actionText = "accept all deviations as customer specific"; + actionText = "accept selected deviations as customer specific"; break; case "accept-all": status = "Accepted"; - actionText = "accept all deviations"; + actionText = "accept selected deviations"; break; case "deny-all": status = "Denied"; - actionText = "deny all deviations"; + actionText = "deny selected deviations"; break; case "deny-all-delete": status = "DeniedDelete"; - actionText = "deny all deviations and delete"; + actionText = "deny selected deviations and delete"; break; case "deny-all-remediate": status = "DeniedRemediate"; - actionText = "deny all deviations and remediate to align with template"; + actionText = "deny selected deviations and remediate to align with template"; break; default: setBulkActionsAnchorEl(null); return; } - const deviations = processedDriftData.currentDeviations.map((deviation) => ({ + // Map selected item IDs back to their deviation data + // IDs are in format: "current-1", "accepted-2", etc. + const allDeviations = [ + ...deviationItemsWithActions, + ...acceptedDeviationItemsWithActions, + ...customerSpecificDeviationItemsWithActions, + ...deniedDeviationItemsWithActions, + ]; + + const selectedDeviations = selectedItems + .map((itemId) => { + const item = allDeviations.find((d) => d.id === itemId); + return item ? item.originalDeviation : null; + }) + .filter(Boolean); + + if (selectedDeviations.length === 0) { + setBulkActionsAnchorEl(null); + return; + } + + const deviations = selectedDeviations.map((deviation) => ({ standardName: deviation.standardName, status: status, receivedValue: deviation.receivedValue, @@ -805,6 +872,7 @@ const ManageDriftPage = () => { const filteredAcceptedItems = applyFilters(acceptedDeviationItemsWithActions); const filteredCustomerSpecificItems = applyFilters(customerSpecificDeviationItemsWithActions); const filteredDeniedItems = applyFilters(deniedDeviationItemsWithActions); + const filteredAlignedItems = applyFilters(alignedStandardItems); // Simple filter for drift templates const driftTemplateOptions = standardsApi.data @@ -1055,7 +1123,9 @@ const ManageDriftPage = () => { {/* Current Deviations Section */} - {filterStatus.some((f) => f.value === "all" || f.value === "current") && ( + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "current")) && ( {/* Header with bulk actions */} { alignItems="center" sx={{ mb: 2 }} > - Current Deviations + New Deviations {selectedItems.length > 0 && ( {/* Bulk Actions Dropdown */} @@ -1128,7 +1198,9 @@ const ManageDriftPage = () => { )} {/* Accepted Deviations Section */} - {filterStatus.some((f) => f.value === "all" || f.value === "accepted") && + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "accepted")) && filteredAcceptedItems.length > 0 && ( @@ -1146,7 +1218,9 @@ const ManageDriftPage = () => { )} {/* Customer Specific Deviations Section */} - {filterStatus.some((f) => f.value === "all" || f.value === "customerspecific") && + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "customerspecific")) && filteredCustomerSpecificItems.length > 0 && ( @@ -1164,7 +1238,9 @@ const ManageDriftPage = () => { )} {/* Denied Deviations Section */} - {filterStatus.some((f) => f.value === "all" || f.value === "denied") && + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "denied")) && filteredDeniedItems.length > 0 && ( @@ -1180,6 +1256,21 @@ const ManageDriftPage = () => { /> )} + + {/* Compliant Standards Section - Always shown, not affected by status filter */} + {filteredAlignedItems.length > 0 && ( + + + Compliant Standards + + + + )} From 875e03075ec3546a7e33e60ebe382c1d982c55c3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:11:40 +0100 Subject: [PATCH 043/111] Implement suggested policies by Jon --- src/pages/tenant/manage/drift.js | 139 +++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 34 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index c4d611ef17a5..4e4e00cea9cf 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -834,11 +834,13 @@ const ManageDriftPage = () => { })); // Calculate compliance metrics for badges + // Denied deviations are included in total but not in compliant count (they haven't been fixed yet) const totalPolicies = processedDriftData.alignedCount + processedDriftData.currentDeviationsCount + processedDriftData.acceptedDeviationsCount + - processedDriftData.customerSpecificDeviations; + processedDriftData.customerSpecificDeviations + + processedDriftData.deniedDeviationsCount; const compliancePercentage = totalPolicies > 0 ? Math.round((processedDriftData.alignedCount / totalPolicies) * 100) : 0; @@ -846,6 +848,21 @@ const ManageDriftPage = () => { const missingLicensePercentage = 0; // This would need to be calculated from actual license data const combinedScore = compliancePercentage + missingLicensePercentage; + // Helper function to get category from standardName + const getCategory = (standardName) => { + if (!standardName) return "Other Standards"; + if (standardName.includes("ConditionalAccessTemplate")) return "Conditional Access Policies"; + if (standardName.includes("IntuneTemplate")) return "Intune Policies"; + + // For other standards, look up category in standards.json + const standard = standardsData.find((s) => s.name === standardName); + if (standard && standard.cat) { + return standard.cat; + } + + return "Other Standards"; + }; + // Apply search and sort filters const applyFilters = (items) => { let filtered = [...items]; @@ -863,6 +880,16 @@ const ManageDriftPage = () => { filtered.sort((a, b) => (a.text || "").localeCompare(b.text || "")); } else if (sortBy === "status") { filtered.sort((a, b) => (a.statusText || "").localeCompare(b.statusText || "")); + } else if (sortBy === "category") { + // Sort by category, then by name within each category + filtered.sort((a, b) => { + const catA = getCategory(a.standardName); + const catB = getCategory(b.standardName); + if (catA !== catB) { + return catA.localeCompare(catB); + } + return (a.text || "").localeCompare(b.text || ""); + }); } return filtered; @@ -874,6 +901,58 @@ const ManageDriftPage = () => { const filteredDeniedItems = applyFilters(deniedDeviationItemsWithActions); const filteredAlignedItems = applyFilters(alignedStandardItems); + // Helper function to render items grouped by category when category sort is active + const renderItemsByCategory = (items) => { + if (sortBy !== "category" || items.length === 0) { + return ( + + ); + } + + // Group items by category and collect unique categories + const groupedItems = {}; + items.forEach((item) => { + const category = getCategory(item.standardName); + if (!groupedItems[category]) { + groupedItems[category] = []; + } + groupedItems[category].push(item); + }); + + // Sort categories alphabetically + const categories = Object.keys(groupedItems).sort(); + + return ( + + {categories.map((category) => { + if (groupedItems[category].length === 0) return null; + return ( + + + {category} + + + + ); + })} + + ); + }; + // Simple filter for drift templates const driftTemplateOptions = standardsApi.data ? standardsApi.data @@ -1053,6 +1132,17 @@ const ManageDriftPage = () => { variant="outlined" /> + + + Denied + + + @@ -1104,11 +1194,20 @@ const ManageDriftPage = () => { options={[ { label: "Name", value: "name" }, { label: "Status", value: "status" }, + { label: "Category", value: "category" }, ]} label="Sort by" value={ sortBy - ? { label: sortBy === "name" ? "Name" : "Status", value: sortBy } + ? { + label: + sortBy === "name" + ? "Name" + : sortBy === "status" + ? "Status" + : "Category", + value: sortBy, + } : null } onChange={(newValue) => setSortBy(newValue?.value || "name")} @@ -1186,14 +1285,7 @@ const ManageDriftPage = () => { )} - + {renderItemsByCategory(filteredDeviationItems)} )} @@ -1206,14 +1298,7 @@ const ManageDriftPage = () => { Accepted Deviations - + {renderItemsByCategory(filteredAcceptedItems)} )} @@ -1226,14 +1311,7 @@ const ManageDriftPage = () => { Accepted Deviations - Customer Specific - + {renderItemsByCategory(filteredCustomerSpecificItems)} )} @@ -1246,14 +1324,7 @@ const ManageDriftPage = () => { Denied Deviations - + {renderItemsByCategory(filteredDeniedItems)} )} From b88e20a8a66dfa7adcfd358cd7acfbac92adf56a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 15:11:53 +0100 Subject: [PATCH 044/111] Jons suggested sort --- src/pages/tenant/manage/drift.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 4e4e00cea9cf..153cc7915fe4 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -853,13 +853,13 @@ const ManageDriftPage = () => { if (!standardName) return "Other Standards"; if (standardName.includes("ConditionalAccessTemplate")) return "Conditional Access Policies"; if (standardName.includes("IntuneTemplate")) return "Intune Policies"; - + // For other standards, look up category in standards.json const standard = standardsData.find((s) => s.name === standardName); if (standard && standard.cat) { return standard.cat; } - + return "Other Standards"; }; From 89883132ae85be9b2f8f1d5497228695fbb87e45 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:41:46 +0100 Subject: [PATCH 045/111] Drift mgmgnt --- src/pages/tenant/manage/drift.js | 129 ++++++++++++++++++++++--------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 153cc7915fe4..c06cf61ddc5a 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -369,30 +369,79 @@ const ManageDriftPage = () => { receivedValue: deviation.receivedValue, // Store the original receivedValue for action handlers expectedValue: deviation.expectedValue, // Store the original expectedValue for action handlers originalDeviation: deviation, // Store the complete original deviation object for reference - propertyItems: [ - { label: "Standard Name", value: prettyName }, - { label: "Description", value: description }, - { label: "Expected Value", value: deviation.expectedValue || "N/A" }, - { label: "Current Value", value: formatPolicyValue(deviation.receivedValue) }, - { - label: "Status", - value: getDeviationStatusText(statusOverride || deviation.Status || deviation.state), - }, - { - label: "Reason", - value: deviation.Reason || "N/A", - }, - { - label: "User", - value: deviation.lastChangedByUser || "N/A", - }, - { - label: "Last Updated", - value: processedDriftData.latestDataCollection - ? new Date(processedDriftData.latestDataCollection).toLocaleString() - : "N/A", - }, - ].filter((item) => item.value !== "N/A" && item.value !== "No description available"), // Filter out N/A values and empty descriptions + children: ( + + {description && description !== "No description available" && ( + + {description} + + )} + + {(deviation.expectedValue && deviation.expectedValue !== "Compliant with template") || deviation.receivedValue ? ( + + {deviation.expectedValue && deviation.expectedValue !== "Compliant with template" && ( + + + Expected + + + + {deviation.expectedValue} + + + + )} + + {deviation.receivedValue && ( + + + Current + + + + {formatPolicyValue(deviation.receivedValue)} + + + + )} + + ) : null} + + {(deviation.Reason || deviation.lastChangedByUser || processedDriftData.latestDataCollection) && ( + <> + + + {deviation.Reason && ( + + + Reason + + {deviation.Reason} + + )} + {deviation.lastChangedByUser && ( + + + Changed By + + {deviation.lastChangedByUser} + + )} + {processedDriftData.latestDataCollection && ( + + + Last Updated + + + {new Date(processedDriftData.latestDataCollection).toLocaleString()} + + + )} + + + )} + + ), }; }); }; @@ -1186,6 +1235,7 @@ const ManageDriftPage = () => { { label: "Accepted", value: "accepted" }, { label: "Customer Specific", value: "customerspecific" }, { label: "Denied", value: "denied" }, + { label: "Compliant", value: "compliant" }, ]} multiple={true} /> @@ -1328,20 +1378,23 @@ const ManageDriftPage = () => { )} - {/* Compliant Standards Section - Always shown, not affected by status filter */} - {filteredAlignedItems.length > 0 && ( - - - Compliant Standards - - - - )} + {/* Compliant Standards Section - Only shown when filtered by All or Compliant */} + {(!filterStatus || + filterStatus.length === 0 || + filterStatus.some((f) => f.value === "all" || f.value === "compliant")) && + filteredAlignedItems.length > 0 && ( + + + Compliant Standards + + + + )} From 22c07295adaa07ebe345c243f3cfeff6ee8fdf84 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 4 Jan 2026 20:41:54 +0100 Subject: [PATCH 046/111] drift management --- src/pages/tenant/manage/drift.js | 99 ++++++++++++++++++++++++++------ 1 file changed, 81 insertions(+), 18 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index c06cf61ddc5a..744e9f7a909a 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -377,28 +377,80 @@ const ManageDriftPage = () => { )} - {(deviation.expectedValue && deviation.expectedValue !== "Compliant with template") || deviation.receivedValue ? ( + {(deviation.expectedValue && deviation.expectedValue !== "Compliant with template") || + deviation.receivedValue ? ( - {deviation.expectedValue && deviation.expectedValue !== "Compliant with template" && ( - - - Expected - - - - {deviation.expectedValue} + {deviation.expectedValue && + deviation.expectedValue !== "Compliant with template" && ( + + + Expected + + + {deviation.expectedValue} + + - - )} + )} {deviation.receivedValue && ( - + Current - - + + {formatPolicyValue(deviation.receivedValue)} @@ -407,13 +459,18 @@ const ManageDriftPage = () => { ) : null} - {(deviation.Reason || deviation.lastChangedByUser || processedDriftData.latestDataCollection) && ( + {(deviation.Reason || + deviation.lastChangedByUser || + processedDriftData.latestDataCollection) && ( <> {deviation.Reason && ( - + Reason {deviation.Reason} @@ -421,7 +478,10 @@ const ManageDriftPage = () => { )} {deviation.lastChangedByUser && ( - + Changed By {deviation.lastChangedByUser} @@ -429,7 +489,10 @@ const ManageDriftPage = () => { )} {processedDriftData.latestDataCollection && ( - + Last Updated From ffc874589f7bb3b60a3d6caffe97047bd50a0dd7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 5 Jan 2026 00:12:17 +0100 Subject: [PATCH 047/111] updates to compares and prettification --- src/pages/tenant/manage/drift.js | 218 ++++++++++++++++++++++++------- 1 file changed, 173 insertions(+), 45 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 744e9f7a909a..cd1e243a3294 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -313,6 +313,42 @@ const ManageDriftPage = () => { return null; }; + // Helper function to compare JSON objects and find differences + const compareJsonObjects = (expected, current) => { + if (!expected || !current) return null; + + try { + const expectedObj = typeof expected === "string" ? JSON.parse(expected) : expected; + const currentObj = typeof current === "string" ? JSON.parse(current) : current; + + // Deep comparison - if they're equal, return null (no diff) + if (JSON.stringify(expectedObj) === JSON.stringify(currentObj)) { + return null; // No differences + } + + // Find differences + const differences = {}; + const allKeys = new Set([...Object.keys(expectedObj), ...Object.keys(currentObj)]); + + allKeys.forEach((key) => { + const expectedVal = expectedObj[key]; + const currentVal = currentObj[key]; + + if (JSON.stringify(expectedVal) !== JSON.stringify(currentVal)) { + differences[key] = { + expected: expectedVal, + current: currentVal, + }; + } + }); + + return Object.keys(differences).length > 0 ? differences : null; + } catch (e) { + console.error("Error comparing JSON objects:", e); + return null; + } + }; + // Helper function to format policy objects for display const formatPolicyValue = (value) => { if (!value) return "N/A"; @@ -340,6 +376,21 @@ const ManageDriftPage = () => { // Helper function to create deviation items const createDeviationItems = (deviations, statusOverride = null) => { return (deviations || []).map((deviation, index) => { + // Check if this should be skipped due to missing license + const isLicenseSkipped = deviation.LicenseAvailable === false; + + // Check if we have both ExpectedValue and CurrentValue for comparison + let isActuallyCompliant = false; + let jsonDifferences = null; + + if (deviation.ExpectedValue && deviation.CurrentValue) { + jsonDifferences = compareJsonObjects(deviation.ExpectedValue, deviation.CurrentValue); + // If there are no differences, this is actually compliant + if (jsonDifferences === null) { + isActuallyCompliant = true; + } + } + // Prioritize standardDisplayName from drift data (which has user-friendly names for templates) // then fallback to standards.json lookup, then raw name const prettyName = @@ -354,21 +405,46 @@ const ManageDriftPage = () => { deviation.standardDescription || "No description available"; + // Determine the actual status + // If actually compliant (values match), mark as aligned regardless of input status + // If license is skipped, mark as skipped + // Otherwise use the provided status + const actualStatus = isActuallyCompliant + ? "aligned" + : isLicenseSkipped + ? "skipped" + : statusOverride || deviation.Status || deviation.state; + const actualStatusText = isActuallyCompliant + ? "Compliant" + : isLicenseSkipped + ? "Skipped - No License Available" + : getDeviationStatusText(actualStatus); + + // For skipped items, show different expected/received values + let displayExpectedValue = deviation.ExpectedValue || deviation.expectedValue; + let displayReceivedValue = deviation.CurrentValue || deviation.receivedValue; + + // If we have JSON differences, show only the differences + if (jsonDifferences && !isLicenseSkipped && !isActuallyCompliant) { + displayExpectedValue = JSON.stringify(jsonDifferences, null, 2); + displayReceivedValue = "See differences in Expected column"; + } + return { id: statusOverride ? `${statusOverride}-${index + 1}` : `current-${index + 1}`, cardLabelBox: { - cardLabelBoxHeader: getDeviationIcon( - statusOverride || deviation.Status || deviation.state - ), + cardLabelBoxHeader: getDeviationIcon(actualStatus), }, text: prettyName, subtext: description, - statusColor: getDeviationColor(statusOverride || deviation.Status || deviation.state), - statusText: getDeviationStatusText(statusOverride || deviation.Status || deviation.state), + statusColor: isLicenseSkipped ? "text.secondary" : getDeviationColor(actualStatus), + statusText: actualStatusText, standardName: deviation.standardName, // Store the original standardName for action handlers receivedValue: deviation.receivedValue, // Store the original receivedValue for action handlers expectedValue: deviation.expectedValue, // Store the original expectedValue for action handlers originalDeviation: deviation, // Store the complete original deviation object for reference + isLicenseSkipped: isLicenseSkipped, // Flag for filtering and disabling actions + isActuallyCompliant: isActuallyCompliant, // Flag to move to compliant section children: ( {description && description !== "No description available" && ( @@ -377,49 +453,65 @@ const ManageDriftPage = () => { )} - {(deviation.expectedValue && deviation.expectedValue !== "Compliant with template") || - deviation.receivedValue ? ( + {isLicenseSkipped && ( + + + ⚠️ This standard was skipped because the required license is not available for + this tenant. + + + )} + + {(displayExpectedValue && displayExpectedValue !== "Compliant with template") || + displayReceivedValue ? ( - {deviation.expectedValue && - deviation.expectedValue !== "Compliant with template" && ( - + {displayExpectedValue && displayExpectedValue !== "Compliant with template" && ( + + + {jsonDifferences ? "Differences" : "Expected"} + + - Expected + {displayExpectedValue} - - - {deviation.expectedValue} - - - )} + + )} - {deviation.receivedValue && ( + {displayReceivedValue && !jsonDifferences && ( { sx={{ mt: 0.5, p: 1.5, - bgcolor: "action.hover", + bgcolor: isActuallyCompliant ? "success.lighter" : "action.hover", borderRadius: 1, border: "1px solid", - borderColor: "divider", + borderColor: isActuallyCompliant ? "success.main" : "divider", }} > { wordBreak: "break-word", }} > - {formatPolicyValue(deviation.receivedValue)} + {displayReceivedValue} @@ -524,6 +616,16 @@ const ManageDriftPage = () => { ); const alignedStandardItems = createDeviationItems(processedDriftData.alignedStandards, "aligned"); + // Separate items by their actual status + const licenseSkippedItems = deviationItems.filter((item) => item.isLicenseSkipped); + const compliantFromDeviations = deviationItems.filter((item) => item.isActuallyCompliant); + const actualDeviationItems = deviationItems.filter( + (item) => !item.isLicenseSkipped && !item.isActuallyCompliant + ); + + // Combine compliant items from both sources + const allAlignedItems = [...alignedStandardItems, ...compliantFromDeviations]; + const handleMenuClick = (event, itemId) => { setAnchorEl((prev) => ({ ...prev, [itemId]: event.currentTarget })); }; @@ -776,7 +878,7 @@ const ManageDriftPage = () => { }, [templateId]); // Add action buttons to each deviation item - const deviationItemsWithActions = deviationItems.map((item) => { + const deviationItemsWithActions = actualDeviationItems.map((item) => { // Check if this is a template that supports delete action const supportsDelete = (item.standardName?.includes("ConditionalAccessTemplate") || @@ -1011,7 +1113,8 @@ const ManageDriftPage = () => { const filteredAcceptedItems = applyFilters(acceptedDeviationItemsWithActions); const filteredCustomerSpecificItems = applyFilters(customerSpecificDeviationItemsWithActions); const filteredDeniedItems = applyFilters(deniedDeviationItemsWithActions); - const filteredAlignedItems = applyFilters(alignedStandardItems); + const filteredAlignedItems = applyFilters(allAlignedItems); + const filteredLicenseSkippedItems = applyFilters(licenseSkippedItems); // Helper function to render items grouped by category when category sort is active const renderItemsByCategory = (items) => { @@ -1255,6 +1358,12 @@ const ManageDriftPage = () => { variant="outlined" /> + + + Skipped (No License) + + + @@ -1458,6 +1567,25 @@ const ManageDriftPage = () => { /> )} + + {/* License Skipped Section - Always at the end */} + {filteredLicenseSkippedItems.length > 0 && ( + + + Skipped - No License Available + + + These standards were skipped because the required licenses are not available + for this tenant. + + + + )} From 558efa3facda6d005b07a881eea9291ecdb0129e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 15 Dec 2025 23:14:16 +0100 Subject: [PATCH 048/111] Feat: Add trusted and blocked senders card and action --- .../administration/users/user/exchange.jsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 61013199368c..73de33d77cbb 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -107,6 +107,12 @@ const Page = () => { waiting: waiting, }); + const junkEmailConfigRequest = ApiGetCall({ + url: `/api/ListUserTrustedBlockedSenders?UserId=${userId}&userPrincipalName=${graphUserRequest.data?.[0]?.userPrincipalName}&tenantFilter=${userSettingsDefaults.currentTenant}`, + queryKey: `TrustedBlockedSenders-${userId}`, + waiting: waiting && !!graphUserRequest.data?.[0]?.userPrincipalName, + }); + const groupsList = ApiGetCall({ url: "/api/ListGraphRequest", data: { @@ -1086,6 +1092,81 @@ const Page = () => { }, ]; + const junkEmailConfigActions = [ + { + label: "Remove Entry", + type: "POST", + icon: , + url: "/api/RemoveTrustedBlockedSender", + customDataformatter: (row, action, formData) => { + return { + userPrincipalName: row?.userPrincipalName, + typeProperty: row?.TypeProperty, + value: row?.Value, + tenantFilter: userSettingsDefaults.currentTenant, + }; + }, + confirmText: + "Are you sure you want to remove [Value] from the [Type] list for [UserPrincipalName]?", + multiPost: false, + relatedQueryKeys: `JunkEmailConfig-${userId}`, + }, + ]; + + const junkEmailConfigCard = [ + { + id: 1, + cardLabelBox: { + cardLabelBoxHeader: junkEmailConfigRequest.isFetching ? ( + + ) : junkEmailConfigRequest.data?.length !== 0 ? ( + + ) : ( + + ), + }, + text: "Trusted and Blocked Senders/Domains", + subtext: junkEmailConfigRequest.data?.length + ? "Trusted/Blocked senders and domains are configured for this user" + : "No trusted or blocked senders/domains entries for this user", + statusColor: "green.main", + table: { + title: "Trusted and Blocked Senders/Domains", + hideTitle: true, + data: junkEmailConfigRequest.data || [], + refreshFunction: () => junkEmailConfigRequest.refetch(), + isFetching: junkEmailConfigRequest.isFetching, + simpleColumns: ["Type", "Value"], + actions: junkEmailConfigActions, + offCanvas: { + children: (data) => { + return ( + + ); + }, + }, + }, + }, + ]; + const proxyAddressActions = [ { label: "Make Primary", @@ -1268,6 +1349,11 @@ const Page = () => { items={mailboxRulesCard} isCollapsible={true} /> + Date: Mon, 15 Dec 2025 23:36:06 +0100 Subject: [PATCH 049/111] damn you typo --- src/pages/identity/administration/users/user/exchange.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 73de33d77cbb..258a46db771e 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -1100,7 +1100,7 @@ const Page = () => { url: "/api/RemoveTrustedBlockedSender", customDataformatter: (row, action, formData) => { return { - userPrincipalName: row?.userPrincipalName, + userPrincipalName: row?.UserPrincipalName, typeProperty: row?.TypeProperty, value: row?.Value, tenantFilter: userSettingsDefaults.currentTenant, From 55548ad0c2e0d369176a46482ecf9de9d050e7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 5 Jan 2026 20:10:05 +0100 Subject: [PATCH 050/111] Fix: Add requestDate default column and make it a pretty DateTime in the table --- src/pages/tenant/administration/app-consent-requests/index.js | 2 ++ src/utils/get-cipp-formatting.js | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pages/tenant/administration/app-consent-requests/index.js b/src/pages/tenant/administration/app-consent-requests/index.js index 5aafe8c75958..3127fdfd8dd1 100644 --- a/src/pages/tenant/administration/app-consent-requests/index.js +++ b/src/pages/tenant/administration/app-consent-requests/index.js @@ -86,6 +86,7 @@ const Page = () => { ]; const simpleColumns = [ + "requestDate", // Request Date "requestUser", // Requester "appDisplayName", // Application Name "appId", // Application ID @@ -116,6 +117,7 @@ const Page = () => { const offCanvas = { extendedInfoFields: [ + "requestDate", // Request Date "requestUser", // Requester "appDisplayName", // Application Name "appId", // Application ID diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 2450fc4eb297..c222f6418bfb 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -181,6 +181,8 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr "NotBefore", "NotAfter", "latestDataCollection", + "requestDate", // App Consent Requests + "reviewedDate", // App Consent Requests ]; const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration)/; From d3d3a7174a7b4be0e55a2cf2cb0423d88a150271 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 13:41:57 -0500 Subject: [PATCH 051/111] Update API config to post entire row in drift management Replaces the static 'deviations' data payload with 'postEntireRow: true' in the API configuration for updating drift deviations. This prevents errors when deviations are not provided in the post payload (e.g. removing all customizations) --- src/pages/tenant/manage/drift.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index cd1e243a3294..225345045697 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -1605,9 +1605,7 @@ const ManageDriftPage = () => { api={{ url: "/api/ExecUpdateDriftDeviation", type: "POST", - data: { - deviations: "deviations", - }, + postEntireRow: true, confirmText: `Are you sure you'd like to ${actionData.action?.text || "update"} ${ actionData.action?.type === "single" ? "this deviation" From 1b782bbd50c60d16fc16c8548375bb8a9e550f30 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 13:42:49 -0500 Subject: [PATCH 052/111] Add IP range support to roles Deleted the CippCustomRoles component and enhanced CippRoleAddEdit to support allowed IP ranges for roles, including UI and data handling for IP/CIDR input. Also updated unauthenticated page to display custom error messages from orgData. --- .../CippSettings/CippCustomRoles.jsx | 516 ------------------ .../CippSettings/CippRoleAddEdit.jsx | 45 ++ src/pages/unauthenticated.js | 9 +- 3 files changed, 50 insertions(+), 520 deletions(-) delete mode 100644 src/components/CippSettings/CippCustomRoles.jsx diff --git a/src/components/CippSettings/CippCustomRoles.jsx b/src/components/CippSettings/CippCustomRoles.jsx deleted file mode 100644 index 56a7ddf651ea..000000000000 --- a/src/components/CippSettings/CippCustomRoles.jsx +++ /dev/null @@ -1,516 +0,0 @@ -import { useEffect, useState } from "react"; - -import { - Box, - Button, - Alert, - Typography, - Accordion, - AccordionSummary, - AccordionDetails, - Stack, - SvgIcon, - Skeleton, -} from "@mui/material"; - -import { Grid } from "@mui/system"; -import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "../../api/ApiCall"; -import { CippOffCanvas } from "/src/components/CippComponents/CippOffCanvas"; -import { CippFormTenantSelector } from "/src/components/CippComponents/CippFormTenantSelector"; -import { Save } from "@mui/icons-material"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import CippFormComponent from "../CippComponents/CippFormComponent"; -import { useForm, useWatch } from "react-hook-form"; -import { InformationCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; -import { CippApiDialog } from "../CippComponents/CippApiDialog"; -import { useDialog } from "../../hooks/use-dialog"; -import { CippApiResults } from "../CippComponents/CippApiResults"; - -export const CippCustomRoles = () => { - const updatePermissions = ApiPostCall({ - urlFromData: true, - relatedQueryKeys: ["customRoleList"], - }); - - const [allTenantSelected, setAllTenantSelected] = useState(false); - const [cippApiRoleSelected, setCippApiRoleSelected] = useState(false); - const [selectedRole, setSelectedRole] = useState(null); - const [updateDefaults, setUpdateDefaults] = useState(false); - - const formControl = useForm({ - mode: "onBlur", - }); - - const createDialog = useDialog(); - const currentRole = useWatch({ control: formControl.control, name: "RoleName" }); - const selectedTenant = useWatch({ control: formControl.control, name: "allowedTenants" }); - const blockedTenants = useWatch({ control: formControl.control, name: "blockedTenants" }); - const setDefaults = useWatch({ control: formControl.control, name: "Defaults" }); - const selectedPermissions = useWatch({ control: formControl.control, name: "Permissions" }); - - const { - data: apiPermissions = [], - isFetching: apiPermissionFetching, - isSuccess: apiPermissionSuccess, - } = ApiGetCall({ - url: "/api/ExecAPIPermissionList", - queryKey: "apiPermissions", - }); - - const { - data: customRoleList = [], - isFetching: customRoleListFetching, - isSuccess: customRoleListSuccess, - refetch: refetchCustomRoleList, - } = ApiGetCall({ - url: "/api/ExecCustomRole", - queryKey: "customRoleList", - }); - - const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ - url: "/api/ListTenants?AllTenantSelector=true", - queryKey: "ListTenants-AllTenantSelector", - }); - const tenants = pages[0] || []; - - useEffect(() => { - if (customRoleListSuccess && tenantsSuccess && selectedRole !== currentRole?.value) { - setSelectedRole(currentRole?.value); - if (currentRole?.value === "cipp-api") { - setCippApiRoleSelected(true); - } else { - setCippApiRoleSelected(false); - } - - var currentPermissions = customRoleList.find((role) => role.RowKey === currentRole?.value); - - var newAllowedTenants = []; - currentPermissions?.AllowedTenants.map((tenant) => { - var tenantInfo = tenants.find((t) => t.customerId === tenant); - var label = `${tenantInfo?.displayName} (${tenantInfo?.defaultDomainName})`; - if (tenantInfo?.displayName) { - newAllowedTenants.push({ - label: label, - value: tenantInfo.defaultDomainName, - }); - } - }); - - var newBlockedTenants = []; - currentPermissions?.BlockedTenants.map((tenant) => { - var tenantInfo = tenants.find((t) => t.customerId === tenant); - var label = `${tenantInfo?.displayName} (${tenantInfo?.defaultDomainName})`; - if (tenantInfo?.displayName) { - newBlockedTenants.push({ - label: label, - value: tenantInfo.defaultDomainName, - }); - } - }); - - formControl.reset({ - Permissions: currentPermissions?.Permissions, - RoleName: currentRole, - allowedTenants: newAllowedTenants, - blockedTenants: newBlockedTenants, - }); - } - }, [currentRole, customRoleList, customRoleListSuccess, tenantsSuccess]); - - useEffect(() => { - if (updateDefaults !== setDefaults) { - setUpdateDefaults(setDefaults); - var newPermissions = {}; - Object.keys(apiPermissions).forEach((cat) => { - Object.keys(apiPermissions[cat]).forEach((obj) => { - var newval = ""; - if (cat == "CIPP" && obj == "Core" && setDefaults == "None") { - newval = "Read"; - } else { - newval = setDefaults; - } - newPermissions[`${cat}${obj}`] = `${cat}.${obj}.${newval}`; - }); - }); - formControl.setValue("Permissions", newPermissions); - } - }, [setDefaults, updateDefaults]); - - useEffect(() => { - var alltenant = false; - selectedTenant?.map((tenant) => { - if (tenant?.value === "AllTenants") { - alltenant = true; - } - }); - if (alltenant) { - setAllTenantSelected(true); - } else { - setAllTenantSelected(false); - } - }, [selectedTenant, blockedTenants]); - - const handleSubmit = () => { - var allowedTenantIds = []; - selectedTenant.map((tenant) => { - var tenant = tenants.find((t) => t.defaultDomainName === tenant?.value); - if (tenant?.customerId) { - allowedTenantIds.push(tenant.customerId); - } - }); - - var blockedTenantIds = []; - blockedTenants.map((tenant) => { - var tenant = tenants.find((t) => t.defaultDomainName === tenant?.value); - if (tenant?.customerId) { - blockedTenantIds.push(tenant.customerId); - } - }); - - updatePermissions.mutate({ - url: "/api/ExecCustomRole?Action=AddUpdate", - data: { - RoleName: currentRole.value, - Permissions: selectedPermissions, - AllowedTenants: allowedTenantIds, - BlockedTenants: blockedTenantIds, - }, - }); - }; - - const ApiPermissionRow = ({ obj, cat }) => { - const [offcanvasVisible, setOffcanvasVisible] = useState(false); - const [descriptionOffcanvasVisible, setDescriptionOffcanvasVisible] = useState(false); - const [selectedDescription, setSelectedDescription] = useState({ name: '', description: '' }); - - const handleDescriptionClick = (name, description) => { - setSelectedDescription({ name, description }); - setDescriptionOffcanvasVisible(true); - }; - - return ( - - {obj} - - - - - - {/* Main offcanvas */} - setOffcanvasVisible(false)} - title={`${cat}.${obj} Endpoints`} - > - - - Listed below are the available API endpoints based on permission level. - ReadWrite level includes endpoints under Read. - - {Object.keys(apiPermissions[cat][obj]).map((type, typeIndex) => { - var items = []; - for (var api in apiPermissions[cat][obj][type]) { - const apiFunction = apiPermissions[cat][obj][type][api]; - items.push({ - name: apiFunction.Name, - description: apiFunction.Description?.[0]?.Text || null - }); - } - return ( - - {type} - - {items.map((item, idx) => ( - - - {item.name} - - {item.description && ( - - )} - - ))} - - - ); - })} - - - - {/* Description offcanvas */} - setDescriptionOffcanvasVisible(false)} - title="Function Description" - > - - - {selectedDescription.name} - - - {selectedDescription.description} - - - - - ); - }; - - return ( - <> - - - - ({ - label: role.RowKey, - value: role.RowKey, - }))} - isFetching={customRoleListFetching} - refreshFunction={() => refetchCustomRoleList()} - creatable={true} - formControl={formControl} - multiple={false} - fullWidth={true} - /> - {cippApiRoleSelected && ( - - This is the default role for all API clients in the CIPP-API integration. If you - would like different permissions for specific applications, create a role per - application and select it from the CIPP-API integrations page. - - )} - - - - {allTenantSelected && blockedTenants?.length == 0 && ( - - All tenants selected, no tenant restrictions will be applied unless blocked tenants - are specified. - - )} - - {allTenantSelected && ( - - - - )} - - {currentRole && ( - <> - {apiPermissionFetching && } - {apiPermissionSuccess && ( - <> - API Permissions - - Set All Permissions - - - - - - - <> - {Object.keys(apiPermissions) - .sort() - .map((cat, catIndex) => ( - - }> - {cat} - - - {Object.keys(apiPermissions[cat]) - .sort() - .map((obj, index) => { - return ( - - - - ); - })} - - - ))} - - - - )} - - )} - - - - {selectedRole && selectedTenant?.length > 0 && ( - <> -
Allowed Tenants
-
    - {selectedTenant.map((tenant, idx) => ( -
  • {tenant?.label}
  • - ))} -
- - )} - {selectedRole && blockedTenants?.length > 0 && ( - <> -
Blocked Tenants
-
    - {blockedTenants.map((tenant, idx) => ( -
  • {tenant?.label}
  • - ))} -
- - )} - {selectedRole && selectedPermissions && ( - <> -
Selected Permissions
-
    - {selectedPermissions && - Object.keys(selectedPermissions) - ?.sort() - .map((cat, idx) => ( - <> - {selectedPermissions?.[cat] && - !selectedPermissions?.[cat]?.includes("None") && ( -
  • {selectedPermissions[cat]}
  • - )} - - ))} -
- - )} -
-
- - - - - {currentRole && ( - - )} - - - - ); -}; - -export default CippCustomRoles; diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index b2212bb39303..1ddbf19a3db1 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -68,6 +68,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const setDefaults = useWatch({ control: formControl.control, name: "Defaults" }); const selectedPermissions = useWatch({ control: formControl.control, name: "Permissions" }); const selectedEntraGroup = useWatch({ control: formControl.control, name: "EntraGroup" }); + const ipRanges = useWatch({ control: formControl.control, name: "IPRange" }); const { data: apiPermissions = [], @@ -240,6 +241,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { value: endpoint, })) || []; + // Process IP ranges + const processedIPRanges = + currentPermissions?.IPRange?.map((ip) => ({ + label: ip, + value: ip, + })) || []; + formControl.reset({ Permissions: basePermissions && Object.keys(basePermissions).length > 0 @@ -249,6 +257,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { allowedTenants: newAllowedTenants, blockedTenants: newBlockedTenants, BlockedEndpoints: processedBlockedEndpoints, + IPRange: processedIPRanges, EntraGroup: currentPermissions?.EntraGroup, }); } @@ -340,6 +349,11 @@ export const CippRoleAddEdit = ({ selectedRole }) => { return endpoint.value || endpoint; }) || []; + const processedIPRanges = + ipRanges?.map((ip) => { + return ip?.value || ip; + }) || []; + updatePermissions.mutate({ url: "/api/ExecCustomRole?Action=AddUpdate", data: { @@ -349,6 +363,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { AllowedTenants: processedAllowedTenants, BlockedTenants: processedBlockedTenants, BlockedEndpoints: processedBlockedEndpoints, + IPRange: processedIPRanges, }, }); }; @@ -612,6 +627,26 @@ export const CippRoleAddEdit = ({ selectedRole }) => {
)} + + + {apiPermissionFetching && ( <> @@ -821,6 +856,16 @@ export const CippRoleAddEdit = ({ selectedRole }) => { )} + {ipRanges?.length > 0 && ( + <> +
Allowed IP Ranges
+
    + {ipRanges.map((ip, idx) => ( +
  • {ip?.value || ip?.label || ip}
  • + ))} +
+ + )} {selectedPermissions && apiPermissionSuccess && ( <>
Selected Permissions
diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 5a1d385a4c4c..a544ff4c5aa4 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -22,9 +22,7 @@ const Page = () => { // Use useMemo to derive userRoles directly const userRoles = useMemo(() => { if (orgData.isSuccess && orgData.data?.clientPrincipal?.userRoles) { - return orgData.data.clientPrincipal.userRoles.filter( - (role) => !blockedRoles.includes(role) - ); + return orgData.data.clientPrincipal.userRoles.filter((role) => !blockedRoles.includes(role)); } return []; }, [orgData.isSuccess, orgData.data?.clientPrincipal?.userRoles]); @@ -54,7 +52,10 @@ const Page = () => { 0 From b9256ac39d60e772b29af9123d7cb1ed981e23a2 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 13:42:59 -0500 Subject: [PATCH 053/111] Add $orderby support to Graph Explorer filter Introduced a new $orderby field in the filter form and parameter handling to allow users to specify sort order for queries. This enhances query customization in the Graph Explorer. --- .../CippTable/CippGraphExplorerFilter.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/components/CippTable/CippGraphExplorerFilter.js b/src/components/CippTable/CippGraphExplorerFilter.js index 3167e5b7ca91..6ce020886005 100644 --- a/src/components/CippTable/CippGraphExplorerFilter.js +++ b/src/components/CippTable/CippGraphExplorerFilter.js @@ -47,6 +47,7 @@ const CippGraphExplorerFilter = ({ $expand: "", $top: "", $search: "", + $orderby: "", $format: "", NoPagination: false, ReverseTenantLookup: false, @@ -326,6 +327,10 @@ const CippGraphExplorerFilter = ({ Key: "$expand", Value: formParameters.$expand, }, + { + Key: "$orderby", + Value: formParameters.$orderby, + }, { Key: "$format", Value: formParameters.$format, @@ -722,6 +727,17 @@ const CippGraphExplorerFilter = ({ /> + {/* OrderBy Field */} + + + + {/* Format Field */} Date: Wed, 7 Jan 2026 15:18:50 -0500 Subject: [PATCH 054/111] fix group type in edit user --- src/components/CippFormPages/CippAddEditUser.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index bfc03594152e..eeddd9a075ca 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -606,7 +606,7 @@ const CippAddEditUser = (props) => { label: tenantGroup.displayName, value: tenantGroup.id, addedFields: { - calculatedGroupType: tenantGroup.calculatedGroupType, + groupType: tenantGroup.groupType, }, }))} formControl={formControl} @@ -624,7 +624,7 @@ const CippAddEditUser = (props) => { label: userGroups.DisplayName, value: userGroups.id, addedFields: { - calculatedGroupType: userGroups.calculatedGroupType, + groupType: userGroups.groupType, }, }))} formControl={formControl} From 9e1df4da7f4f598261d1c4a1dc7917d35de6b2c6 Mon Sep 17 00:00:00 2001 From: Brandon Martinez Date: Wed, 7 Jan 2026 15:50:28 -0800 Subject: [PATCH 055/111] Refactor tenant selection effects to include additional dependencies and simplify logic --- .../CippComponents/CippTenantSelector.jsx | 47 +++++-------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index 688afab83bb8..e4f7326a4f60 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -196,7 +196,7 @@ export const CippTenantSelector = (props) => { ); } } - }, [currentTenant?.value]); + }, [currentTenant?.value, router, queryClient]); // This effect handles when the URL parameter changes (from deep link or user selection) // This is the single source of truth for tenant changes @@ -235,7 +235,7 @@ export const CippTenantSelector = (props) => { } } } - }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess]); + }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess, tenantList.data, currentTenant, settings]); // This effect ensures the tenant filter parameter is included in the URL when missing useEffect(() => { @@ -253,39 +253,13 @@ export const CippTenantSelector = (props) => { { shallow: true } ); } - }, [router.isReady, router.query.tenantFilter, settings.currentTenant]); + }, [router.isReady, router.query.tenantFilter, settings.currentTenant, router]); useEffect(() => { - if (tenant && currentTenant?.value && currentTenant?.value !== "AllTenants") { + if (offcanvasVisible && currentTenant?.value && currentTenant?.value !== "AllTenants") { tenantDetails.refetch(); } - }, [tenant, offcanvasVisible]); - - // We can simplify this effect since we now have the new effect above to handle URL changes - useEffect(() => { - if (tenant && tenantList.isSuccess && !currentTenant) { - const matchingTenant = tenantList.data.find( - ({ defaultDomainName }) => defaultDomainName === tenant - ); - setSelectedTenant( - matchingTenant - ? { - value: tenant, - label: `${matchingTenant.displayName} (${tenant})`, - addedFields: { - defaultDomainName: matchingTenant.defaultDomainName, - displayName: matchingTenant.displayName, - customerId: matchingTenant.customerId, - initialDomainName: matchingTenant.initialDomainName, - }, - } - : { - value: null, - label: "Invalid Tenant", - } - ); - } - }, [tenant, tenantList.isSuccess, currentTenant]); + }, [offcanvasVisible, currentTenant?.value]); // Cleanup on unmount useEffect(() => { @@ -342,13 +316,14 @@ export const CippTenantSelector = (props) => { onChange={(nv) => setSelectedTenant(nv)} options={ tenantList.isSuccess && tenantList.data && tenantList.data.length > 0 - ? tenantList.data.map(({ customerId, displayName, defaultDomainName }) => ({ + ? tenantList.data.map(({ customerId, displayName, defaultDomainName, initialDomainName }) => ({ value: defaultDomainName, label: `${displayName} (${defaultDomainName})`, - addedField: { - defaultDomainName: "defaultDomainName", - displayName: "displayName", - customerId: "customerId", + addedFields: { + defaultDomainName: defaultDomainName, + displayName: displayName, + customerId: customerId, + initialDomainName: initialDomainName, }, })) : [] From 3a3f9a94ad30f7d0166b4c41da92b28946fe6170 Mon Sep 17 00:00:00 2001 From: Brandon Martinez Date: Wed, 7 Jan 2026 15:56:30 -0800 Subject: [PATCH 056/111] oops I changed too much --- .../CippComponents/CippTenantSelector.jsx | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index e4f7326a4f60..ab9e50ea8fcf 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -196,7 +196,7 @@ export const CippTenantSelector = (props) => { ); } } - }, [currentTenant?.value, router, queryClient]); + }, [currentTenant?.value]); // This effect handles when the URL parameter changes (from deep link or user selection) // This is the single source of truth for tenant changes @@ -235,7 +235,7 @@ export const CippTenantSelector = (props) => { } } } - }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess, tenantList.data, currentTenant, settings]); + }, [router.isReady, router.query.tenantFilter, tenantList.isSuccess]); // This effect ensures the tenant filter parameter is included in the URL when missing useEffect(() => { @@ -253,13 +253,39 @@ export const CippTenantSelector = (props) => { { shallow: true } ); } - }, [router.isReady, router.query.tenantFilter, settings.currentTenant, router]); + }, [router.isReady, router.query.tenantFilter, settings.currentTenant]); useEffect(() => { - if (offcanvasVisible && currentTenant?.value && currentTenant?.value !== "AllTenants") { + if (tenant && currentTenant?.value && currentTenant?.value !== "AllTenants") { tenantDetails.refetch(); } - }, [offcanvasVisible, currentTenant?.value]); + }, [tenant, offcanvasVisible]); + + // We can simplify this effect since we now have the new effect above to handle URL changes + useEffect(() => { + if (tenant && tenantList.isSuccess && !currentTenant) { + const matchingTenant = tenantList.data.find( + ({ defaultDomainName }) => defaultDomainName === tenant + ); + setSelectedTenant( + matchingTenant + ? { + value: tenant, + label: `${matchingTenant.displayName} (${tenant})`, + addedFields: { + defaultDomainName: matchingTenant.defaultDomainName, + displayName: matchingTenant.displayName, + customerId: matchingTenant.customerId, + initialDomainName: matchingTenant.initialDomainName, + }, + } + : { + value: null, + label: "Invalid Tenant", + } + ); + } + }, [tenant, tenantList.isSuccess, currentTenant]); // Cleanup on unmount useEffect(() => { From 37d28c6929db9d50141fe6147ad6b2921be43e68 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 19:23:06 -0500 Subject: [PATCH 057/111] Update CippAddEditUser.jsx --- src/components/CippFormPages/CippAddEditUser.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index eeddd9a075ca..79b42de33f6f 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -609,6 +609,7 @@ const CippAddEditUser = (props) => { groupType: tenantGroup.groupType, }, }))} + creatable={false} formControl={formControl} /> @@ -627,6 +628,7 @@ const CippAddEditUser = (props) => { groupType: userGroups.groupType, }, }))} + creatable={false} formControl={formControl} /> From aa27f3378ae944c8e2e98a1d026f4fc2e9051c8c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 7 Jan 2026 19:23:16 -0500 Subject: [PATCH 058/111] Handle null items in standards template arrays Added checks to skip null items when processing IntuneTemplate and ConditionalAccessTemplate arrays. Also updated template tag value access to use optional chaining for robustness. --- src/pages/tenant/manage/applied-standards.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 71e4865d8771..a4cf1a470593 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -145,6 +145,7 @@ const Page = () => { Object.entries(selectedTemplate.standards).forEach(([standardKey, standardConfig]) => { if (standardKey === "IntuneTemplate" && Array.isArray(standardConfig)) { standardConfig.forEach((templateItem, index) => { + if (!templateItem) return; // Skip null items console.log("Processing IntuneTemplate item:", templateItem); if ( templateItem["TemplateList-Tags"]?.value && @@ -214,7 +215,7 @@ const Page = () => { standardId, standardName: `Intune Template: ${ expandedTemplate.displayName || expandedTemplate.name || itemTemplateId - } (via ${templateItem["TemplateList-Tags"].value})`, + } (via ${templateItem["TemplateList-Tags"]?.value})`, currentTenantValue: standardObject !== undefined ? { @@ -367,6 +368,7 @@ const Page = () => { ) { // Process each ConditionalAccessTemplate item separately standardConfig.forEach((templateItem, index) => { + if (!templateItem) return; // Skip null items // Check if this item has TemplateList-Tags and expand them if ( templateItem["TemplateList-Tags"]?.value && @@ -423,7 +425,7 @@ const Page = () => { standardId, standardName: `Conditional Access Template: ${ expandedTemplate.displayName || expandedTemplate.name || itemTemplateId - } (via ${templateItem["TemplateList-Tags"].value})`, + } (via ${templateItem["TemplateList-Tags"]?.value})`, currentTenantValue: standardObject !== undefined ? { From 8e190b5d6176e63152c458010fcf5f2668374f45 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 8 Jan 2026 00:17:19 -0500 Subject: [PATCH 059/111] Update edit.jsx --- src/pages/identity/administration/groups/edit.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index bde4e212c864..304a1792d51b 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -330,6 +330,7 @@ const EditGroup = () => { }, })) || [] } + sortOptions={true} /> @@ -353,6 +354,7 @@ const EditGroup = () => { }, })) || [] } + sortOptions={true} /> @@ -374,6 +376,7 @@ const EditGroup = () => { addedFields: { id: m.id }, })) || [] } + sortOptions={true} /> From 467e624be5181141c69ff8817783f8868f851372 Mon Sep 17 00:00:00 2001 From: Luke Steward <29278153+LukeSteward@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:22:14 +0000 Subject: [PATCH 060/111] Add Dependabot configuration for npm updates Signed-off-by: Luke Steward <29278153+LukeSteward@users.noreply.github.com> --- .github/dependabot.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..e8faf3f82477 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "npm" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + target-branch: "dev" From 15bae483bf9ae2e4b295af4eaafebf71ee18d63c Mon Sep 17 00:00:00 2001 From: Luke Steward <87503131+sfaxluke@users.noreply.github.com> Date: Fri, 9 Jan 2026 11:10:33 +0000 Subject: [PATCH 061/111] Update GDAP invite URLs to new Microsoft admin domain Replaced 'admin.microsoft.com/AdminPortal/Home' with 'admin.cloud.microsoft' in GDAP invite URLs for onboarding and relationship pages to reflect the updated Microsoft admin portal structure. --- src/pages/tenant/gdap-management/onboarding/start.js | 2 +- .../tenant/gdap-management/relationships/relationship/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/gdap-management/onboarding/start.js b/src/pages/tenant/gdap-management/onboarding/start.js index e6ae11357c1a..6d37fbf75650 100644 --- a/src/pages/tenant/gdap-management/onboarding/start.js +++ b/src/pages/tenant/gdap-management/onboarding/start.js @@ -507,7 +507,7 @@ const Page = () => { { label: "Invite URL", value: getCippFormatting( - "https://admin.microsoft.com/AdminPortal/Home#/partners/invitation/granularAdminRelationships/" + + "https://admin.cloud.microsoft/?#/partners/invitation/granularAdminRelationships/" + currentRelationship.value, "InviteUrl", "url" diff --git a/src/pages/tenant/gdap-management/relationships/relationship/index.js b/src/pages/tenant/gdap-management/relationships/relationship/index.js index c739ca3a0e21..40e01ba7c7e9 100644 --- a/src/pages/tenant/gdap-management/relationships/relationship/index.js +++ b/src/pages/tenant/gdap-management/relationships/relationship/index.js @@ -120,7 +120,7 @@ const Page = () => { properties.push({ label: "Invite URL", value: getCippFormatting( - "https://admin.microsoft.com/AdminPortal/Home#/partners/invitation/granularAdminRelationships/" + + "https://admin.cloud.microsoft/?#/partners/invitation/granularAdminRelationships/" + data?.id, "InviteUrl", "url" From 3f877211589b019c8d3e40e3bb38844602899315 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 9 Jan 2026 12:33:41 -0500 Subject: [PATCH 062/111] Add deprecated standard handling to UI Introduces UI indicators and restrictions for deprecated standards in CippStandardAccordion and CippStandardDialog components. Deprecated standards are now visually marked, cannot be added, and display explanatory messages. Also adds a new deprecated standard to standards.json. --- .../CippStandards/CippStandardAccordion.jsx | 43 +++++++- .../CippStandards/CippStandardDialog.jsx | 104 ++++++++++++++++-- src/data/standards.json | 16 +++ 3 files changed, 148 insertions(+), 15 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index afbd54594256..e5c90b075932 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -714,6 +714,14 @@ const CippStandardAccordion = ({ {accordionTitle} + {standard.deprecated && ( + + )} {/* Hide action chips in drift mode */} {!isDriftMode && selectedActions && selectedActions?.length > 0 && ( <> @@ -780,10 +788,21 @@ const CippStandardAccordion = ({ {standard.multiple && ( - - handleAddMultipleStandard(standardName)}> - - + + + handleAddMultipleStandard(standardName)} + disabled={standard.deprecated} + > + + + )} - + {standard.deprecated && ( + + + ⚠️ This standard is deprecated and cannot be configured. Please remove it + from your template and use an alternative standard if available. + + + )} + {isDriftMode ? ( /* Drift mode layout - full width with slider first */ diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index d74d6f4d3630..6873936d9cda 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -102,9 +102,6 @@ const StandardCard = memo( height: "100%", display: "flex", flexDirection: "column", - ...(isNewStandard(standard.addedDate) && { - mt: 1.2, // Add top margin to accommodate the "New" label - }), }} > {isNewStandard(standard.addedDate) && ( @@ -123,6 +120,22 @@ const StandardCard = memo( }} /> )} + {standard.deprecated && ( + + )} @@ -243,7 +262,34 @@ const StandardCard = memo( - {standard.multiple ? ( + {standard.deprecated ? ( + + + } + label={ + isSelected + ? "Remove this standard from the template" + : "This standard is deprecated" + } + /> + {!isSelected && ( + + This standard is deprecated and cannot be added. Please use an alternative + standard if available. + + )} + + ) : standard.multiple ? ( } label="Add this standard to the template" @@ -329,7 +376,7 @@ const VirtualizedStandardGrid = memo(({ items, renderItem }) => { overscan={5} defaultItemHeight={320} // Provide estimated row height for better virtualization itemContent={(index) => ( - + {standard.label}
+ {standard.deprecated && ( + + )} {isNewStandard(standard.addedDate) && ( - {standard.multiple ? ( + {standard.deprecated ? ( + isSelected ? ( + + } + label="Remove" + sx={{ mr: 1 }} + /> + ) : ( + + Deprecated - Cannot be added + + ) + ) : standard.multiple ? ( Date: Sun, 11 Jan 2026 12:48:16 +0100 Subject: [PATCH 063/111] Fix reporting --- src/pages/dashboardv2/devices/index.js | 4 +++- src/pages/dashboardv2/identity/index.js | 4 +++- src/pages/dashboardv2/index.js | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/dashboardv2/devices/index.js b/src/pages/dashboardv2/devices/index.js index e68373218bc9..09738bb8b994 100644 --- a/src/pages/dashboardv2/devices/index.js +++ b/src/pages/dashboardv2/devices/index.js @@ -22,7 +22,9 @@ const Page = () => { const settings = useSettings(); const { currentTenant } = settings; const router = useRouter(); - const selectedReport = router.query.reportId || "ztna"; + // Only use default if router is ready and reportId is still not present + const selectedReport = + router.isReady && !router.query.reportId ? "ztna" : router.query.reportId || "ztna"; const testsApi = ApiGetCall({ url: "/api/ListTests", diff --git a/src/pages/dashboardv2/identity/index.js b/src/pages/dashboardv2/identity/index.js index 7849bb0d6079..4f6b66856f9a 100644 --- a/src/pages/dashboardv2/identity/index.js +++ b/src/pages/dashboardv2/identity/index.js @@ -12,7 +12,9 @@ const Page = () => { const settings = useSettings(); const { currentTenant } = settings; const router = useRouter(); - const selectedReport = router.query.reportId || "ztna"; + // Only use default if router is ready and reportId is still not present + const selectedReport = + router.isReady && !router.query.reportId ? "ztna" : router.query.reportId || "ztna"; const testsApi = ApiGetCall({ url: "/api/ListTests", diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 6c0ec354df76..2faef215d07e 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -60,7 +60,9 @@ const Page = () => { const [refreshDialog, setRefreshDialog] = useState({ open: false }); // Get reportId from query params or default to "ztna" - const selectedReport = router.query.reportId || "ztna"; + // Only use default if router is ready and reportId is still not present + const selectedReport = + router.isReady && !router.query.reportId ? "ztna" : router.query.reportId || "ztna"; const formControl = useForm({ mode: "onChange", From 8647fe5edfd3ad818990a1c24b05bdc3601eecda Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 13:19:00 +0100 Subject: [PATCH 064/111] prettification --- .../CippComponents/AssessmentCard.jsx | 54 ++++++++++--------- src/pages/dashboardv2/index.js | 38 ++++++++----- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/components/CippComponents/AssessmentCard.jsx b/src/components/CippComponents/AssessmentCard.jsx index 957e57320e7e..e3f02e29f080 100644 --- a/src/components/CippComponents/AssessmentCard.jsx +++ b/src/components/CippComponents/AssessmentCard.jsx @@ -8,10 +8,14 @@ export const AssessmentCard = ({ data, isLoading }) => { const identityPassed = data?.TestResultSummary?.IdentityPassed || 0; const identityTotal = data?.TestResultSummary?.IdentityTotal || 1; const devicesPassed = data?.TestResultSummary?.DevicesPassed || 0; - const devicesTotal = data?.TestResultSummary?.DevicesTotal || 1; + const devicesTotal = data?.TestResultSummary?.DevicesTotal || 0; + + // Determine if we should show devices section + const hasDeviceTests = devicesTotal > 0; // Calculate percentages for the radial chart - const devicesPercentage = (devicesPassed / devicesTotal) * 100; + // If no device tests, set devices to 100% (complete) + const devicesPercentage = hasDeviceTests ? (devicesPassed / devicesTotal) * 100 : 100; const identityPercentage = (identityPassed / identityTotal) * 100; const chartData = [ @@ -61,28 +65,30 @@ export const AssessmentCard = ({ data, isLoading }) => { )}
- - - Devices - - - {isLoading ? ( - - ) : ( - <> - {devicesPassed}/{devicesTotal} - - tests - - - )} - - + {hasDeviceTests && ( + + + Devices + + + {isLoading ? ( + + ) : ( + <> + {devicesPassed}/{devicesTotal} + + tests + + + )} + + + )} Last Data Collection diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 2faef215d07e..3f060771bf8e 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -66,21 +66,41 @@ const Page = () => { const formControl = useForm({ mode: "onChange", - defaultValues: { - reportId: selectedReport, - }, }); const reportIdValue = useWatch({ control: formControl.control }); + // Fetch available reports + const reportsApi = ApiGetCall({ + url: "/api/ListTestReports", + queryKey: "ListTestReports", + }); + + const reports = reportsApi.data || []; + + // Update form when selectedReport changes (from URL) + useEffect(() => { + if (selectedReport && router.isReady && reports.length > 0) { + const matchingReport = reports.find((r) => r.id === selectedReport); + if (matchingReport) { + formControl.setValue("reportId", { + value: matchingReport.id, + label: matchingReport.description + ? `${matchingReport.name} - ${matchingReport.description}` + : matchingReport.name, + }); + } + } + }, [selectedReport, router.isReady, reports]); + // Update URL when form value changes (e.g., user selects different report from dropdown) useEffect(() => { console.log("reportIdValue changed:", reportIdValue); - if (reportIdValue && reportIdValue.reportId?.value !== selectedReport) { + if (reportIdValue?.reportId?.value && reportIdValue.reportId.value !== selectedReport) { router.push( { pathname: router.pathname, - query: { ...router.query, reportId: reportIdValue.reportId?.value }, + query: { ...router.query, reportId: reportIdValue.reportId.value }, }, undefined, { shallow: true } @@ -88,14 +108,6 @@ const Page = () => { } }, [reportIdValue]); - // Fetch available reports - const reportsApi = ApiGetCall({ - url: "/api/ListTestReports", - queryKey: "ListTestReports", - }); - - const reports = reportsApi.data || []; - const organization = ApiGetCall({ url: "/api/ListOrg", queryKey: `${currentTenant}-ListOrg`, From 458676f859f3f93003137de0659bcab550184071 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:26:19 +0100 Subject: [PATCH 065/111] Add description field --- .../CippComponents/CippAutocomplete.jsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index daaa6dda867a..ea5d9811bda8 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -6,6 +6,8 @@ import { TextField, IconButton, Tooltip, + Box, + Typography, } from "@mui/material"; import { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { useSettings } from "../../hooks/use-settings"; @@ -189,6 +191,12 @@ export const CippAutoComplete = (props) => { typeof api?.valueField === "function" ? api.valueField(option) : option[api?.valueField], + description: + typeof api?.descriptionField === "function" + ? api.descriptionField(option) + : api?.descriptionField + ? option[api?.descriptionField] + : undefined, addedFields, rawData: option, // Store the full original object }; @@ -545,6 +553,21 @@ export const CippAutoComplete = (props) => { )} groupBy={groupBy} renderGroup={renderGroup} + renderOption={(props, option) => { + const { key, ...optionProps } = props; + return ( + + + {option.label} + {option.description && ( + + {option.description} + + )} + + + ); + }} {...other} /> {api?.templateView && ( From 4a9905d3254dd11370499d0634c97d2f2c79a095 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:28:15 +0100 Subject: [PATCH 066/111] added field --- src/pages/dashboardv2/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 3f060771bf8e..68a7eafc1cc6 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -256,7 +256,7 @@ const Page = () => { multiple={false} formControl={formControl} options={reports.map((r) => ({ - label: r.description ? `${r.name} - ${r.description}` : r.name, + label: r.name, value: r.id, description: r.description, }))} From b73e47424a654082650fd54b61b1e79bc494e219 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:56:47 +0100 Subject: [PATCH 067/111] making buttons the same --- .../CippComponents/CippAddTestReportDrawer.jsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippAddTestReportDrawer.jsx b/src/components/CippComponents/CippAddTestReportDrawer.jsx index 64a2af507867..d7ee1646656f 100644 --- a/src/components/CippComponents/CippAddTestReportDrawer.jsx +++ b/src/components/CippComponents/CippAddTestReportDrawer.jsx @@ -138,8 +138,15 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) <> + } + offCanvas={offCanvas} + simpleColumns={[ + "templateName", + "defaultForTenant", + "tenantFilter", + "defaultDuration.label", + "defaultRoles", + "generateTAPByDefault", + "defaultExpireAction.label", + "defaultNotificationActions", + "reasonTemplate" + ]} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 48e9bf2255a0..289ee607fb59 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -3,14 +3,181 @@ import { Grid } from "@mui/system"; import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; -import { useForm } from "react-hook-form"; +import { useForm, useWatch } from "react-hook-form"; import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; import gdaproles from "/src/data/GDAPRoles.json"; import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { useEffect, useState } from "react"; + const Page = () => { const formControl = useForm({ mode: "onChange" }); + const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); + const [selectedTemplate, setSelectedTemplate] = useState(null); + + const jitAdminTemplates = ApiGetCall({ + url: selectedTenant ? `/api/ListJITAdminTemplates?TenantFilter=${selectedTenant.value}` : undefined, + queryKey: selectedTenant ? `JITAdminTemplates-${selectedTenant.value}` : undefined, + refetchOnMount: false, + refetchOnReconnect: false, + enabled: !!selectedTenant, + }); + + const watcher = useWatch({ control: formControl.control }); + + // Simple duration parser for basic ISO 8601 durations + const parseDuration = (duration) => { + if (!duration) return null; + const matches = duration.match(/P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/); + if (!matches) return null; + return { + years: parseInt(matches[1] || 0), + months: parseInt(matches[2] || 0), + weeks: parseInt(matches[3] || 0), + days: parseInt(matches[4] || 0), + hours: parseInt(matches[5] || 0), + minutes: parseInt(matches[6] || 0), + seconds: parseInt(matches[7] || 0), + }; + }; + + const addDurationToDate = (date, duration) => { + if (!date || !duration) return null; + const parsed = parseDuration(duration); + if (!parsed) return null; + + const result = new Date(date); + result.setFullYear(result.getFullYear() + parsed.years); + result.setMonth(result.getMonth() + parsed.months); + result.setDate(result.getDate() + parsed.weeks * 7); + result.setDate(result.getDate() + parsed.days); + result.setHours(result.getHours() + parsed.hours); + result.setMinutes(result.getMinutes() + parsed.minutes); + result.setSeconds(result.getSeconds() + parsed.seconds); + return result; + }; + + // Auto-select default template for tenant + // Priority: tenant-specific default > AllTenants default + useEffect(() => { + if (jitAdminTemplates.isSuccess && !watcher.jitAdminTemplate) { + const templates = jitAdminTemplates.data || []; + + // First, try to find a tenant-specific default template + let defaultTemplate = templates.find( + (template) => + template.defaultForTenant === true && + template.tenantFilter !== "AllTenants" && + template.tenantFilter === selectedTenant?.value + ); + + // If not found, fall back to AllTenants default template + if (!defaultTemplate) { + defaultTemplate = templates.find( + (template) => + template.defaultForTenant === true && + template.tenantFilter === "AllTenants" + ); + } + + if (defaultTemplate) { + formControl.setValue("jitAdminTemplate", { + label: defaultTemplate.templateName, + value: defaultTemplate.GUID, + addedFields: defaultTemplate, + }); + setSelectedTemplate(defaultTemplate); + } + } + }, [jitAdminTemplates.isSuccess, selectedTenant]); + + // Only set template-driven fields when the template actually changes + const [lastTemplate, setLastTemplate] = useState(null); + useEffect(() => { + const template = watcher.jitAdminTemplate?.addedFields; + if (!template || template.GUID === lastTemplate) return; + setSelectedTemplate(template); + setLastTemplate(template.GUID); + + // Helpers + const roundDown15 = (date) => { + const d = new Date(date); + d.setMilliseconds(0); + d.setSeconds(0); + d.setMinutes(Math.floor(d.getMinutes() / 15) * 15); + return d; + }; + const roundUp15 = (date) => { + const d = new Date(date); + d.setMilliseconds(0); + d.setSeconds(0); + let min = d.getMinutes(); + d.setMinutes(min % 15 === 0 ? min : Math.ceil(min / 15) * 15); + if (d.getMinutes() === 60) { + d.setHours(d.getHours() + 1); + d.setMinutes(0); + } + return d; + }; + + // Set all template-driven fields + formControl.setValue("adminRoles", template.defaultRoles || [], { shouldDirty: true }); + formControl.setValue("expireAction", template.defaultExpireAction || null, { shouldDirty: true }); + formControl.setValue("postExecution", template.defaultNotificationActions || [], { shouldDirty: true }); + formControl.setValue("UseTAP", template.generateTAPByDefault ?? false, { shouldDirty: true }); + formControl.setValue("reason", template.reasonTemplate || "", { shouldDirty: true }); + + // User action and user details + if (template.defaultUserAction) { + formControl.setValue("userAction", template.defaultUserAction, { shouldDirty: true }); + } + if (template.defaultFirstName) { + formControl.setValue("firstName", template.defaultFirstName, { shouldDirty: true }); + } + if (template.defaultLastName) { + formControl.setValue("lastName", template.defaultLastName, { shouldDirty: true }); + } + if (template.defaultUserName) { + formControl.setValue("userName", template.defaultUserName, { shouldDirty: true }); + } + if (template.defaultDomain) { + formControl.setValue("domain", template.defaultDomain, { shouldDirty: true }); + } + if (template.defaultExistingUser) { + formControl.setValue("existingUser", template.defaultExistingUser, { shouldDirty: true }); + } + + // Dates + if (template.defaultDuration) { + const duration = typeof template.defaultDuration === "object" && template.defaultDuration !== null + ? template.defaultDuration.value + : template.defaultDuration; + const start = roundDown15(new Date()); + const unixStart = Math.floor(start.getTime() / 1000); + formControl.setValue("startDate", unixStart, { shouldDirty: true }); + const end = roundUp15(addDurationToDate(start, duration)); + const unixEnd = Math.floor(end.getTime() / 1000); + formControl.setValue("endDate", unixEnd, { shouldDirty: true }); + } + }, [watcher.jitAdminTemplate, lastTemplate]); + + // Recalculate end date when start date changes and template has default duration + useEffect(() => { + if (watcher.startDate && selectedTemplate?.defaultDuration) { + const durationValue = typeof selectedTemplate.defaultDuration === 'object' && selectedTemplate.defaultDuration !== null + ? selectedTemplate.defaultDuration.value + : selectedTemplate.defaultDuration; + const startDateDate = new Date(watcher.startDate * 1000); + const endDateObj = addDurationToDate(startDateDate, durationValue); + if (endDateObj) { + const unixEnd = Math.floor(endDateObj.getTime() / 1000); + formControl.setValue("endDate", unixEnd); + } + } + }, [watcher.startDate]); + return ( <> { validators={{ required: "A tenant must be selected" }} /> + + ({ + label: template.templateName, + value: template.GUID, + addedFields: template, + })) + : [] + } + formControl={formControl} + /> + { label="Expiration Action" name="expireAction" multiple={false} + creatable={false} required={true} options={[ { label: "Delete User", value: "DeleteUser" }, @@ -216,6 +403,7 @@ const Page = () => { label="Notification Action" name="postExecution" multiple={true} + creatable={false} options={[ { label: "Webhook", value: "Webhook" }, { label: "Email", value: "email" }, From f7427797244823f982a375b45feec55b061dfe81 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 11:41:37 +0100 Subject: [PATCH 074/111] update standards with extra tags --- src/data/standards.json | 254 ++++++++++++++++++++++++++++++++-------- 1 file changed, 202 insertions(+), 52 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 9abfde56944f..c9a7f30e2683 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -116,7 +116,13 @@ { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], + "tag": [ + "CIS M365 5.0 (3.1.1)", + "mip_search_auditlog", + "NIST CSF 2.0 (DE.CM-09)", + "CISAMSEXO171", + "CISAMSEXO173" + ], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", "addedComponent": [], @@ -319,7 +325,10 @@ "EIDSCA.AP14", "EIDSCA.ST08", "EIDSCA.ST09", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "EIDSCAAP07", + "EIDSCAST08", + "EIDSCAST09" ], "helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.", "docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)", @@ -335,7 +344,7 @@ { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], + "tag": ["CIS M365 5.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)", "ZTNA21799", "CISAMSEXO51"], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", @@ -350,7 +359,14 @@ { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": ["CIS M365 5.0 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], + "tag": [ + "CIS M365 5.0 (1.3.2)", + "spo_idle_session_timeout", + "NIST CSF 2.0 (PR.AA-03)", + "ZTNA21813", + "ZTNA21814", + "ZTNA21815" + ], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", "addedComponent": [ @@ -394,7 +410,7 @@ { "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03"], + "tag": ["EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", "EIDSCAAG02", "EIDSCAAG03"], "helpText": "Configures the report suspicious activity settings and system credential preferences in the authentication methods policy.", "docsDescription": "Controls the authentication methods policy settings for reporting suspicious activity and system credential preferences. These settings help enhance the security of authentication in your organization.", "executiveText": "Configures security settings that allow users to report suspicious login attempts and manages how the system handles authentication credentials. This enhances overall security by enabling early detection of potential security threats and optimizing authentication processes.", @@ -454,7 +470,7 @@ { "name": "standards.AuthMethodsPolicyMigration", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["EIDSCAAG01"], "helpText": "Completes the migration of authentication methods policy to the new format", "docsDescription": "Sets the authentication methods policy migration state to complete. This is required when migrating from legacy authentication policies to the new unified authentication methods policy.", "executiveText": "Completes the transition from legacy authentication policies to Microsoft's modern unified authentication methods policy, ensuring the organization benefits from the latest security features and management capabilities. This migration enables enhanced security controls and simplified policy management.", @@ -533,7 +549,7 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21953", "ZTNA21955", "ZTNA24560"], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.", @@ -556,7 +572,14 @@ "EIDSCA.AM07", "EIDSCA.AM09", "EIDSCA.AM10", - "NIST CSF 2.0 (PR.AA-03)" + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAM01", + "EIDSCAAM03", + "EIDSCAAM04", + "EIDSCAAM06", + "EIDSCAAM07", + "EIDSCAAM09", + "EIDSCAAM10" ], "helpText": "Enables the MS authenticator app to display information about the app that is requesting authentication. This displays the application name.", "docsDescription": "Allows users to use Passwordless with Number Matching and adds location information from the last request", @@ -572,7 +595,7 @@ { "name": "standards.allowOTPTokens", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AM02"], + "tag": ["EIDSCA.AM02", "EIDSCAAM02"], "helpText": "Allows you to use MS authenticator OTP token generator", "docsDescription": "Allows you to use Microsoft Authenticator OTP token generator. Useful for using the NPS extension as MFA on VPN clients.", "executiveText": "Enables one-time password generation through Microsoft Authenticator app, providing an additional secure authentication method for employees. This is particularly useful for secure VPN access and other systems requiring multi-factor authentication.", @@ -631,7 +654,13 @@ "EIDSCA.AF04", "EIDSCA.AF05", "EIDSCA.AF06", - "NIST CSF 2.0 (PR.AA-03)" + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAF01", + "EIDSCAAF02", + "EIDSCAAF03", + "EIDSCAAF04", + "EIDSCAAF05", + "EIDSCAAF06" ], "helpText": "Enables the FIDO2 authenticationMethod for the tenant", "docsDescription": "Enables FIDO2 capabilities for the tenant. This allows users to use FIDO2 keys like a Yubikey for authentication.", @@ -692,7 +721,7 @@ { "name": "standards.TAP", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21845", "ZTNA21846", "EIDSCAAT01", "EIDSCAAT02"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", @@ -740,7 +769,17 @@ { "name": "standards.CustomBannedPasswordList", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (5.2.3.2)"], + "tag": [ + "CIS M365 5.0 (5.2.3.2)", + "ZTNA21848", + "ZTNA21849", + "ZTNA21850", + "EIDSCAPR01", + "EIDSCAPR02", + "EIDSCAPR03", + "EIDSCAPR05", + "EIDSCAPR06" + ], "helpText": "**Requires Entra ID P1.** Updates and enables the Entra ID custom banned password list with the supplied words. Enter words separated by commas or semicolons. Each word must be 4-16 characters long. Maximum 1,000 words allowed.", "docsDescription": "Updates and enables the Entra ID custom banned password list with the supplied words. This supplements the global banned password list maintained by Microsoft. The custom list is limited to 1,000 key base terms of 4-16 characters each. Entra ID will [block variations and common substitutions](https://learn.microsoft.com/en-us/entra/identity/authentication/tutorial-configure-custom-password-protection#configure-custom-banned-passwords) of these words in user passwords. [How are passwords evaluated?](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-password-ban-bad#score-calculation)", "addedComponent": [ @@ -762,7 +801,7 @@ { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21803", "ZTNA21804"], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.", "addedComponent": [ @@ -794,7 +833,7 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)"], + "tag": ["CIS M365 5.0 (1.2.3)", "CISA (MS.AAD.6.1v1)", "ZTNA21772", "ZTNA21787"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", @@ -818,7 +857,12 @@ "EIDSCA.CR03", "EIDSCA.CR04", "Essential 8 (1507)", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "ZTNA21869", + "EIDSCACR01", + "EIDSCACR02", + "EIDSCACR03", + "EIDSCACR04" ], "helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings", "docsDescription": "Enables the ability for users to request admin consent for applications. Should be used in conjunction with the \"Require admin consent for applications\" standards", @@ -840,7 +884,7 @@ { "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21889"], "helpText": "Sets the state of the registration campaign for the tenant", "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", @@ -883,7 +927,7 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.21.1v1)"], + "tag": ["CISA (MS.AAD.21.1v1)", "ZTNA21868"], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.", @@ -903,7 +947,8 @@ "CISA (MS.AAD.4.1v1)", "EIDSCA.AP10", "Essential 8 (1175)", - "NIST CSF 2.0 (PR.AA-05)" + "NIST CSF 2.0 (PR.AA-05)", + "EIDSCAAP10" ], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", @@ -919,7 +964,7 @@ { "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21954"], "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.", @@ -952,7 +997,7 @@ { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "tag": ["CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA21868"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], @@ -1001,7 +1046,7 @@ { "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["ZTNA21858"], "helpText": "Blocks login for guest users that have not logged in for a number of days", "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", "addedComponent": [ @@ -1030,7 +1075,14 @@ "EIDSCA.AP09", "Essential 8 (1175)", "NIST CSF 2.0 (PR.AA-05)", - "ZTNA21807" + "ZTNA21772", + "ZTNA21774", + "ZTNA21807", + "EIDSCAAP08", + "EIDSCAAP09", + "EIDSCACP01", + "EIDSCACP03", + "EIDSCACP04" ], "helpText": "Disables users from being able to consent to applications, except for those specified in the field below", "docsDescription": "Requires users to get administrator consent before sharing data with applications. You can preapprove specific applications.", @@ -1067,7 +1119,7 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07"], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "EIDSCAAP04"], "helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.", "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.", "addedComponent": [ @@ -1151,7 +1203,7 @@ { "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.11.1v1)"], + "tag": ["CISA (MS.AAD.11.1v1)", "ZTNA21843"], "helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.", "docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.", "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.", @@ -1166,7 +1218,7 @@ { "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)"], + "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAS04"], "helpText": "This blocks users from using SMS as an MFA method. If a user only has SMS as a MFA method, they will be unable to log in.", "docsDescription": "Disables SMS as an MFA method for the tenant. If a user only has SMS as a MFA method, they will be unable to sign in.", "executiveText": "Disables SMS text messages as a multi-factor authentication method due to security vulnerabilities like SIM swapping attacks. This forces users to adopt more secure authentication methods like authenticator apps or hardware tokens, significantly improving account security.", @@ -1181,7 +1233,7 @@ { "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)"], + "tag": ["CIS M365 5.0 (2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAV01"], "helpText": "This blocks users from using Voice call as an MFA method. If a user only has Voice as a MFA method, they will be unable to log in.", "docsDescription": "Disables Voice call as an MFA method for the tenant. If a user only has Voice call as a MFA method, they will be unable to sign in.", "executiveText": "Disables voice call authentication due to security vulnerabilities and social engineering risks. This forces users to adopt more secure authentication methods like authenticator apps, improving overall account security by eliminating phone-based attack vectors.", @@ -1249,7 +1301,10 @@ "Essential 8 (1504)", "Essential 8 (1173)", "Essential 8 (1401)", - "NIST CSF 2.0 (PR.AA-03)" + "NIST CSF 2.0 (PR.AA-03)", + "ZTNA21780", + "ZTNA21782", + "ZTNA21796" ], "helpText": "Enables per user MFA for all users.", "executiveText": "Requires all employees to use multi-factor authentication for enhanced account security, significantly reducing the risk of unauthorized access from compromised passwords. This fundamental security measure protects against the majority of account-based attacks and is essential for maintaining strong cybersecurity posture.", @@ -1522,7 +1577,7 @@ { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.2.3)"], + "tag": ["CIS M365 5.0 (6.2.3)", "ORCA111", "ORCA240", "CISAMSEXO71"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.", @@ -1645,7 +1700,7 @@ { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (2.1.9)"], + "tag": ["CIS M365 5.0 (2.1.9)", "ORCA108", "CISAMSEXO31"], "helpText": "Enables DKIM for all domains that currently support it", "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", "addedComponent": [], @@ -1697,7 +1752,8 @@ "exo_mailboxaudit", "Essential 8 (1509)", "Essential 8 (1683)", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "CISAMSEXO131" ], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", @@ -1914,7 +1970,7 @@ { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing"], + "tag": ["CIS M365 5.0 (1.3.3)", "exo_individualsharing", "ZTNA21803", "CISAMSEXO62"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", @@ -1949,7 +2005,7 @@ { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted"], + "tag": ["CIS M365 5.0 (6.5.3)", "exo_storageproviderrestricted", "ZTNA21817"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", @@ -2109,7 +2165,8 @@ "CIS M365 5.0 (6.3.1)", "exo_outlookaddins", "NIST CSF 2.0 (PR.AA-05)", - "NIST CSF 2.0 (PR.PS-05)" + "NIST CSF 2.0 (PR.PS-05)", + "ZTNA21817" ], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", @@ -2382,7 +2439,24 @@ "CIS M365 5.0 (2.1.1)", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA105", + "ORCA106", + "ORCA107", + "ORCA112", + "ORCA113", + "ORCA114", + "ORCA116", + "ORCA119", + "ORCA156", + "ORCA179", + "ORCA226", + "ORCA236", + "ORCA237", + "ORCA238", + "CISAMSEXO151", + "CISAMSEXO152", + "CISAMSEXO153" ], "helpText": "This creates a Safe Links policy that automatically scans, tracks, and and enables safe links for Email, Office, and Teams for both external and internal senders", "addedComponent": [ @@ -2436,7 +2510,30 @@ "mdo_antiphishingpolicies", "mdo_phishthresholdlevel", "CIS M365 5.0 (2.1.7)", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA104", + "ORCA115", + "ORCA180", + "ORCA220", + "ORCA221", + "ORCA222", + "ORCA223", + "ORCA228", + "ORCA229", + "ORCA230", + "ORCA233", + "ORCA234", + "ORCA235", + "ORCA239", + "ORCA242", + "ORCA243", + "ORCA244", + "ZTNA21784", + "ZTNA21817", + "ZTNA21819", + "CISAMSEXO111", + "CISAMSEXO112", + "CISAMSEXO113" ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ @@ -2657,7 +2754,9 @@ "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA158", + "ORCA227" ], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ @@ -2808,7 +2907,16 @@ "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware", - "NIST CSF 2.0 (DE.CM-09)" + "NIST CSF 2.0 (DE.CM-09)", + "ORCA121", + "ORCA124", + "ORCA232", + "ZTNA21817", + "ZTNA21819", + "CISAMSEXO95", + "CISAMSEXO101", + "CISAMSEXO102", + "CISAMSEXO103" ], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ @@ -2936,7 +3044,25 @@ { "name": "standards.SpamFilterPolicy", "cat": "Defender Standards", - "tag": [], + "tag": [ + "ORCA100", + "ORCA101", + "ORCA102", + "ORCA103", + "ORCA104", + "ORCA123", + "ORCA139", + "ORCA140", + "ORCA141", + "ORCA142", + "ORCA143", + "ORCA224", + "ORCA231", + "ORCA241", + "CISAMSEXO141", + "CISAMSEXO142", + "CISAMSEXO143" + ], "helpText": "This standard creates a Spam filter policy similar to the default strict policy.", "docsDescription": "This standard creates a Spam filter policy similar to the default strict policy, the following settings are configured to on by default: IncreaseScoreWithNumericIps, IncreaseScoreWithRedirectToOtherPort, MarkAsSpamEmptyMessages, MarkAsSpamJavaScriptInHtml, MarkAsSpamSpfRecordHardFail, MarkAsSpamFromAddressAuthFail, MarkAsSpamNdrBackscatter, MarkAsSpamBulkMail, InlineSafetyTipsEnabled, PhishZapEnabled, SpamZapEnabled", "addedComponent": [ @@ -3770,7 +3896,7 @@ { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", - "tag": ["CISA (MS.AAD.17.1v1)"], + "tag": ["CISA (MS.AAD.17.1v1)", "ZTNA21801", "ZTNA21802"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", "addedComponent": [ @@ -3791,7 +3917,7 @@ { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", - "tag": [], + "tag": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.", "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", @@ -3941,7 +4067,7 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], + "tag": ["CIS M365 5.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)", "ZTNA21817"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], @@ -3983,7 +4109,7 @@ { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)"], + "tag": ["CIS M365 5.0 (7.2.9)", "CISA (MS.SPO.1.5v1)", "ZTNA21803", "ZTNA21804", "ZTNA21858"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ @@ -4008,7 +4134,7 @@ { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)"], + "tag": ["CIS M365 5.0 (7.2.10)", "CISA (MS.SPO.1.6v1)", "ZTNA21803", "ZTNA21804"], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ @@ -4033,7 +4159,13 @@ { "name": "standards.DefaultSharingLink", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.7)", "CIS M365 5.0 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "tag": [ + "CIS M365 5.0 (7.2.7)", + "CIS M365 5.0 (7.2.11)", + "CISA (MS.SPO.1.4v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.", @@ -4136,7 +4268,9 @@ "CIS M365 5.0 (7.2.1)", "spo_legacy_auth", "CISA (MS.AAD.3.1v1)", - "NIST CSF 2.0 (PR.IR-01)" + "NIST CSF 2.0 (PR.IR-01)", + "ZTNA21776", + "ZTNA21797" ], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", @@ -4152,7 +4286,13 @@ { "name": "standards.sharingCapability", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)"], + "tag": [ + "CIS M365 5.0 (7.2.3)", + "CISA (MS.AAD.14.1v1)", + "CISA (MS.SPO.1.1v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.", "addedComponent": [ @@ -4191,7 +4331,13 @@ { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], + "tag": [ + "CIS M365 5.0 (7.2.5)", + "CISA (MS.AAD.14.2v1)", + "CISA (MS.SPO.1.2v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.", @@ -4255,7 +4401,7 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "tag": ["CIS M365 5.0 (7.2.3)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA24824"], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -4289,7 +4435,13 @@ { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": ["CIS M365 5.0 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], + "tag": [ + "CIS M365 5.0 (7.2.6)", + "CISA (MS.AAD.14.3v1)", + "CISA (MS.SPO.1.3v1)", + "ZTNA21803", + "ZTNA21804" + ], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", "addedComponent": [ @@ -5394,9 +5546,7 @@ "impactColour": "info", "addedDate": "2025-08-26", "powershellEquivalent": "None", - "recommendedBy": [ - "Microsoft" - ] + "recommendedBy": ["Microsoft"] }, { "name": "standards.DeployCheckChromeExtension", From e4aa1ac667d4a84f789151877ecef8aee3c86851 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:21:22 +0100 Subject: [PATCH 075/111] updates dashboard --- src/components/ExecutiveReportButton.js | 50 ++- src/pages/dashboardv1.js | 455 +++++++++++++++++++++++ src/pages/dashboardv2/index.js | 91 +++-- src/pages/index.js | 471 +----------------------- 4 files changed, 553 insertions(+), 514 deletions(-) create mode 100644 src/pages/dashboardv1.js diff --git a/src/components/ExecutiveReportButton.js b/src/components/ExecutiveReportButton.js index e7d0cdde65de..922a3c550850 100644 --- a/src/components/ExecutiveReportButton.js +++ b/src/components/ExecutiveReportButton.js @@ -2533,7 +2533,7 @@ const ExecutiveReportDocument = ({ }; export const ExecutiveReportButton = (props) => { - const { tenantName, tenantId, userStats, standardsData, organizationData, ...other } = props; + const { ...other } = props; const settings = useSettings(); const brandingSettings = settings.customBranding; @@ -2550,6 +2550,22 @@ export const ExecutiveReportButton = (props) => { infographics: true, }); + // Fetch organization data - only when preview is open + const organization = ApiGetCall({ + url: "/api/ListOrg", + queryKey: `${settings.currentTenant}-ListOrg-report`, + data: { tenantFilter: settings.currentTenant }, + waiting: previewOpen, + }); + + // Fetch user counts - only when preview is open + const dashboard = ApiGetCall({ + url: "/api/ListuserCounts", + data: { tenantFilter: settings.currentTenant }, + queryKey: `${settings.currentTenant}-ListuserCounts-report`, + waiting: previewOpen, + }); + // Only fetch additional data when preview dialog is opened const secureScore = useSecureScore({ waiting: previewOpen }); @@ -2606,7 +2622,9 @@ export const ExecutiveReportButton = (props) => { // Check if all data is loaded (either successful or failed) - only relevant when preview is open const isDataLoading = previewOpen && - (secureScore.isFetching || + (organization.isFetching || + dashboard.isFetching || + secureScore.isFetching || licenseData.isFetching || deviceData.isFetching || conditionalAccessData.isFetching || @@ -2615,7 +2633,9 @@ export const ExecutiveReportButton = (props) => { const hasAllDataFinished = !previewOpen || - ((secureScore.isSuccess || secureScore.isError) && + ((organization.isSuccess || organization.isError) && + (dashboard.isSuccess || dashboard.isError) && + (secureScore.isSuccess || secureScore.isError) && (licenseData.isSuccess || licenseData.isError) && (deviceData.isSuccess || deviceData.isError) && (conditionalAccessData.isSuccess || conditionalAccessData.isError) && @@ -2625,6 +2645,18 @@ export const ExecutiveReportButton = (props) => { // Button is always available now since we don't need to wait for data const shouldShowButton = true; + const tenantName = organization.data?.displayName || "Tenant"; + const tenantId = organization.data?.id; + const userStats = { + licensedUsers: dashboard.data?.LicUsers || 0, + unlicensedUsers: + dashboard.data?.Users && dashboard.data?.LicUsers + ? dashboard.data?.Users - dashboard.data?.LicUsers + : 0, + guests: dashboard.data?.Guests || 0, + globalAdmins: dashboard.data?.Gas || 0, + }; + const fileName = `Executive_Report_${tenantName?.replace(/[^a-zA-Z0-9]/g, "_") || "Tenant"}_${ new Date().toISOString().split("T")[0] }.pdf`; @@ -2655,8 +2687,8 @@ export const ExecutiveReportButton = (props) => { tenantName={tenantName} tenantId={tenantId} userStats={userStats} - standardsData={standardsData} - organizationData={organizationData} + standardsData={driftComplianceData.data} + organizationData={organization.data} brandingSettings={brandingSettings} secureScoreData={secureScore.isSuccess ? secureScore : null} licensingData={licenseData.isSuccess ? licenseData?.data : null} @@ -2687,8 +2719,8 @@ export const ExecutiveReportButton = (props) => { tenantName, tenantId, userStats, - standardsData, - organizationData, + organization.data, + dashboard.data, brandingSettings, secureScore?.isSuccess, licenseData?.isSuccess, @@ -3007,8 +3039,8 @@ export const ExecutiveReportButton = (props) => { tenantName={tenantName} tenantId={tenantId} userStats={userStats} - standardsData={standardsData} - organizationData={organizationData} + standardsData={driftComplianceData.data} + organizationData={organization.data} brandingSettings={brandingSettings} secureScoreData={secureScore.isSuccess ? secureScore : null} licensingData={licenseData.isSuccess ? licenseData?.data : null} diff --git a/src/pages/dashboardv1.js b/src/pages/dashboardv1.js new file mode 100644 index 000000000000..dd9f5300d8d9 --- /dev/null +++ b/src/pages/dashboardv1.js @@ -0,0 +1,455 @@ +import Head from "next/head"; +import { useEffect, useState } from "react"; +import { Box, Container, Button, Card, CardContent } from "@mui/material"; +import { Grid } from "@mui/system"; +import { CippInfoBar } from "../components/CippCards/CippInfoBar"; +import { CippChartCard } from "../components/CippCards/CippChartCard"; +import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; +import { Layout as DashboardLayout } from "../layouts/index.js"; +import { useSettings } from "../hooks/use-settings"; +import { getCippFormatting } from "../utils/get-cipp-formatting.js"; +import Portals from "../data/portals"; +import { BulkActionsMenu } from "../components/bulk-actions-menu.js"; +import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; +import { ApiGetCall } from "../api/ApiCall.jsx"; +import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; +import { ExecutiveReportButton } from "../components/ExecutiveReportButton.js"; + +const Page = () => { + const settings = useSettings(); + const { currentTenant } = settings; + const [domainVisible, setDomainVisible] = useState(false); + + const organization = ApiGetCall({ + url: "/api/ListOrg", + queryKey: `${currentTenant}-ListOrg`, + data: { tenantFilter: currentTenant }, + }); + + const dashboard = ApiGetCall({ + url: "/api/ListuserCounts", + data: { tenantFilter: currentTenant }, + queryKey: `${currentTenant}-ListuserCounts`, + }); + + const sharepoint = ApiGetCall({ + url: "/api/ListSharepointQuota", + queryKey: `${currentTenant}-ListSharepointQuota`, + data: { tenantFilter: currentTenant }, + }); + + const standards = ApiGetCall({ + url: "/api/ListStandardTemplates", + queryKey: `${currentTenant}-ListStandardTemplates`, + }); + + const driftApi = ApiGetCall({ + url: "/api/listTenantDrift", + data: { + TenantFilter: currentTenant, + }, + queryKey: `TenantDrift-${currentTenant}`, + }); + + const partners = ApiGetCall({ + url: "/api/ListGraphRequest", + queryKey: `${currentTenant}-ListPartners`, + data: { + Endpoint: "policies/crossTenantAccessPolicy/partners", + tenantFilter: currentTenant, + ReverseTenantLookup: true, + }, + }); + + const currentTenantInfo = ApiGetCall({ + url: "/api/ListTenants", + queryKey: `ListTenants`, + }); + + // Top bar data + const tenantInfo = [ + { name: "Tenant Name", data: organization.data?.displayName }, + { + name: "Tenant ID", + data: ( + <> + + + ), + }, + { + name: "Default Domain", + data: ( + <> + domain.isDefault === true)?.name + } + type="chip" + /> + + ), + }, + { + name: "AD Sync Enabled", + data: getCippFormatting(organization.data?.onPremisesSyncEnabled, "dirsync"), + }, + ]; + + // Process drift data for chart - filter by current tenant and aggregate + const processDriftDataForTenant = (driftData, currentTenant) => { + if (!driftData) { + return { + alignedCount: 0, + acceptedDeviationsCount: 0, + currentDeviationsCount: 0, + customerSpecificDeviations: 0, + hasData: false, + }; + } + + const rawDriftData = driftData || []; + const tenantDriftData = Array.isArray(rawDriftData) + ? rawDriftData.filter((item) => item.tenantFilter === currentTenant) + : []; + + const hasData = tenantDriftData.length > 0; + + // Aggregate data across all standards for this tenant + const aggregatedData = tenantDriftData.reduce( + (acc, item) => { + acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; + acc.currentDeviationsCount += item.currentDeviationsCount || 0; + acc.alignedCount += item.alignedCount || 0; + acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; + return acc; + }, + { + acceptedDeviationsCount: 0, + currentDeviationsCount: 0, + alignedCount: 0, + customerSpecificDeviations: 0, + } + ); + + return { ...aggregatedData, hasData }; + }; + + function getActionCountsForTenant(standardsData, currentTenant) { + if (!standardsData) { + return { + remediateCount: 0, + alertCount: 0, + reportCount: 0, + total: 0, + }; + } + + const applicableTemplates = standardsData.filter((template) => { + const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; + const excludedTenantsArr = Array.isArray(template?.excludedTenants) + ? template.excludedTenants + : []; + + const tenantInFilter = + tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); + + const allTenantsTemplate = + tenantFilterArr.some((tf) => tf.value === "AllTenants") && + (excludedTenantsArr.length === 0 || + !excludedTenantsArr.some((et) => et.value === currentTenant)); + + return tenantInFilter || allTenantsTemplate; + }); + + // Combine standards from all applicable templates: + let combinedStandards = {}; + for (const template of applicableTemplates) { + for (const [standardKey, standardValue] of Object.entries(template.standards)) { + combinedStandards[standardKey] = standardValue; + } + } + + // Count each action type: + let remediateCount = 0; + let alertCount = 0; + let reportCount = 0; + + for (const [, standard] of Object.entries(combinedStandards)) { + let actions = standard.action || []; + if (!Array.isArray(actions)) { + actions = [actions]; + } + actions.forEach((actionObj) => { + if (actionObj?.value === "Remediate") { + remediateCount++; + } else if (actionObj?.value === "Alert") { + alertCount++; + } else if (actionObj?.value === "Report") { + reportCount++; + } + }); + } + + const total = Object.keys(combinedStandards).length; + + return { remediateCount, alertCount, reportCount, total }; + } + + const driftData = processDriftDataForTenant(driftApi.data, currentTenant); + const { remediateCount, alertCount, reportCount, total } = getActionCountsForTenant( + standards.data, + currentTenant + ); + + const [PortalMenuItems, setPortalMenuItems] = useState([]); + const [partnersVisible, setPartnersVisible] = useState(false); + + const formatStorageSize = (sizeInMB) => { + if (sizeInMB >= 1024) { + return `${(sizeInMB / 1024).toFixed(2)}GB`; + } + return `${sizeInMB}MB`; + }; + + // Function to filter portals based on user preferences + const getFilteredPortals = () => { + const defaultLinks = { + M365_Portal: true, + Exchange_Portal: true, + Entra_Portal: true, + Teams_Portal: true, + Azure_Portal: true, + Intune_Portal: true, + SharePoint_Admin: true, + Security_Portal: true, + Compliance_Portal: true, + Power_Platform_Portal: true, + Power_BI_Portal: true, + }; + + let portalLinks; + if (settings.UserSpecificSettings?.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; + } else if (settings.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.portalLinks }; + } else { + portalLinks = defaultLinks; + } + + // Filter the portals based on user settings + return Portals.filter((portal) => { + const settingKey = portal.name; + return settingKey ? portalLinks[settingKey] === true : true; + }); + }; + + useEffect(() => { + if (currentTenantInfo.isSuccess) { + const tenantLookup = currentTenantInfo.data?.find( + (tenant) => tenant.defaultDomainName === currentTenant + ); + + // Get filtered portals based on user preferences + const filteredPortals = getFilteredPortals(); + + const menuItems = filteredPortals.map((portal) => ({ + label: portal.label, + target: "_blank", + link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), + icon: portal.icon, + })); + setPortalMenuItems(menuItems); + } + }, [ + currentTenantInfo.isSuccess, + currentTenant, + settings.portalLinks, + settings.UserSpecificSettings, + ]); + + return ( + <> + + Dashboard + + + + + + + + + + + {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} + + + + + + + + + + + + + + + + + + + + + {/* Converted Domain Names to Property List */} + + ({ + label: "", + value: domain.name, + }))} + actionButton={ + organization.data?.verifiedDomains?.length > 3 && ( + + ) + } + /> + + + + ({ + label: partner.TenantInfo?.displayName, + value: partner.TenantInfo?.defaultDomainName, + }))} + actionButton={ + partners.data?.Results?.length > 3 && ( + + ) + } + /> + + + + + plan.capabilityStatus === "Enabled" && + ["exchange", "AADPremiumService", "WindowsDefenderATP"].includes( + plan.service + ) + ) + .reduce((uniqueServices, curr) => { + const serviceLabel = + curr.service === "exchange" + ? "Exchange" + : curr.service === "AADPremiumService" + ? "AAD Premium" + : curr.service === "Windows Defender" + ? "Windows Defender" + : curr.service; + + if (!uniqueServices.includes(serviceLabel)) { + uniqueServices.push(serviceLabel); + } + return uniqueServices; + }, []) + .join(", "), + }, + ]} + /> + + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index b0ea5e336144..21b814672044 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -119,14 +119,6 @@ const Page = () => { waiting: !!currentTenant && !!selectedReport, }); - const driftApi = ApiGetCall({ - url: "/api/listTenantDrift", - data: { - tenantFilter: currentTenant, - }, - queryKey: `TenantDrift-${currentTenant}`, - }); - const currentTenantInfo = ApiGetCall({ url: "/api/ListTenants", queryKey: `ListTenants`, @@ -165,24 +157,51 @@ const Page = () => { } : dashboardDemoData; + // Function to filter portals based on user preferences + const getFilteredPortals = () => { + const defaultLinks = { + M365_Portal: true, + Exchange_Portal: true, + Entra_Portal: true, + Teams_Portal: true, + Azure_Portal: true, + Intune_Portal: true, + SharePoint_Admin: true, + Security_Portal: true, + Compliance_Portal: true, + Power_Platform_Portal: true, + Power_BI_Portal: true, + }; + + let portalLinks; + if (settings.UserSpecificSettings?.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; + } else if (settings.portalLinks) { + portalLinks = { ...defaultLinks, ...settings.portalLinks }; + } else { + portalLinks = defaultLinks; + } + + // Filter the portals based on user settings + return Portals.filter((portal) => { + const settingKey = portal.name; + return settingKey ? portalLinks[settingKey] === true : true; + }); + }; + useEffect(() => { if (currentTenantInfo.isSuccess) { - const menuItems = Portals.map((portal) => ({ + const tenantLookup = currentTenantInfo.data?.find( + (tenant) => tenant.defaultDomainName === currentTenant + ); + + // Get filtered portals based on user preferences + const filteredPortals = getFilteredPortals(); + + const menuItems = filteredPortals.map((portal) => ({ label: portal.label, - link: portal.url - .replace( - "%%tenantid%%", - currentTenantInfo.data - ?.find((tenant) => tenant.defaultDomainName === currentTenant) - ?.customerId?.toLowerCase() - ) - .replace( - "%%customername%%", - currentTenantInfo.data?.find((tenant) => tenant.defaultDomainName === currentTenant) - ?.displayName - ), - external: portal.external, - target: settings.UserSpecificSettings?.portalLinks || portal.target, + target: "_blank", + link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), icon: portal.icon, })); setPortalMenuItems(menuItems); @@ -214,19 +233,7 @@ const Page = () => { actions={portalMenuItems} disabled={!currentTenantInfo.isSuccess || portalMenuItems.length === 0} /> - + + diff --git a/src/pages/index.js b/src/pages/index.js index 576bf8d504ff..fd02b3802bc9 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,470 +1,3 @@ -import Head from "next/head"; -import { useEffect, useState } from "react"; -import { Box, Container, Button, Card, CardContent } from "@mui/material"; -import { Grid } from "@mui/system"; -import { CippInfoBar } from "../components/CippCards/CippInfoBar"; -import { CippChartCard } from "../components/CippCards/CippChartCard"; -import { CippPropertyListCard } from "../components/CippCards/CippPropertyListCard"; -import { Layout as DashboardLayout } from "../layouts/index.js"; -import { useSettings } from "../hooks/use-settings"; -import { getCippFormatting } from "../utils/get-cipp-formatting.js"; -import Portals from "../data/portals"; -import { BulkActionsMenu } from "../components/bulk-actions-menu.js"; -import { CippUniversalSearch } from "../components/CippCards/CippUniversalSearch.jsx"; -import { ApiGetCall } from "../api/ApiCall.jsx"; -import { CippCopyToClipBoard } from "../components/CippComponents/CippCopyToClipboard.jsx"; -import { ExecutiveReportButton } from "../components/ExecutiveReportButton.js"; +import DashboardV2 from "./dashboardv2"; -const Page = () => { - const settings = useSettings(); - const { currentTenant } = settings; - const [domainVisible, setDomainVisible] = useState(false); - - const organization = ApiGetCall({ - url: "/api/ListOrg", - queryKey: `${currentTenant}-ListOrg`, - data: { tenantFilter: currentTenant }, - }); - - const dashboard = ApiGetCall({ - url: "/api/ListuserCounts", - data: { tenantFilter: currentTenant }, - queryKey: `${currentTenant}-ListuserCounts`, - }); - - const sharepoint = ApiGetCall({ - url: "/api/ListSharepointQuota", - queryKey: `${currentTenant}-ListSharepointQuota`, - data: { tenantFilter: currentTenant }, - }); - - const standards = ApiGetCall({ - url: "/api/ListStandardTemplates", - queryKey: `${currentTenant}-ListStandardTemplates`, - }); - - const driftApi = ApiGetCall({ - url: "/api/listTenantDrift", - data: { - TenantFilter: currentTenant, - }, - queryKey: `TenantDrift-${currentTenant}`, - }); - - const partners = ApiGetCall({ - url: "/api/ListGraphRequest", - queryKey: `${currentTenant}-ListPartners`, - data: { - Endpoint: "policies/crossTenantAccessPolicy/partners", - tenantFilter: currentTenant, - ReverseTenantLookup: true, - }, - }); - - const currentTenantInfo = ApiGetCall({ - url: "/api/ListTenants", - queryKey: `ListTenants`, - }); - - // Top bar data - const tenantInfo = [ - { name: "Tenant Name", data: organization.data?.displayName }, - { - name: "Tenant ID", - data: ( - <> - - - ), - }, - { - name: "Default Domain", - data: ( - <> - domain.isDefault === true)?.name - } - type="chip" - /> - - ), - }, - { - name: "AD Sync Enabled", - data: getCippFormatting(organization.data?.onPremisesSyncEnabled, "dirsync"), - }, - ]; - - // Process drift data for chart - filter by current tenant and aggregate - const processDriftDataForTenant = (driftData, currentTenant) => { - if (!driftData) { - return { - alignedCount: 0, - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - customerSpecificDeviations: 0, - hasData: false, - }; - } - - const rawDriftData = driftData || []; - const tenantDriftData = Array.isArray(rawDriftData) - ? rawDriftData.filter((item) => item.tenantFilter === currentTenant) - : []; - - const hasData = tenantDriftData.length > 0; - - // Aggregate data across all standards for this tenant - const aggregatedData = tenantDriftData.reduce( - (acc, item) => { - acc.acceptedDeviationsCount += item.acceptedDeviationsCount || 0; - acc.currentDeviationsCount += item.currentDeviationsCount || 0; - acc.alignedCount += item.alignedCount || 0; - acc.customerSpecificDeviations += item.customerSpecificDeviationsCount || 0; - return acc; - }, - { - acceptedDeviationsCount: 0, - currentDeviationsCount: 0, - alignedCount: 0, - customerSpecificDeviations: 0, - } - ); - - return { ...aggregatedData, hasData }; - }; - - function getActionCountsForTenant(standardsData, currentTenant) { - if (!standardsData) { - return { - remediateCount: 0, - alertCount: 0, - reportCount: 0, - total: 0, - }; - } - - const applicableTemplates = standardsData.filter((template) => { - const tenantFilterArr = Array.isArray(template?.tenantFilter) ? template.tenantFilter : []; - const excludedTenantsArr = Array.isArray(template?.excludedTenants) - ? template.excludedTenants - : []; - - const tenantInFilter = - tenantFilterArr.length > 0 && tenantFilterArr.some((tf) => tf.value === currentTenant); - - const allTenantsTemplate = - tenantFilterArr.some((tf) => tf.value === "AllTenants") && - (excludedTenantsArr.length === 0 || - !excludedTenantsArr.some((et) => et.value === currentTenant)); - - return tenantInFilter || allTenantsTemplate; - }); - - // Combine standards from all applicable templates: - let combinedStandards = {}; - for (const template of applicableTemplates) { - for (const [standardKey, standardValue] of Object.entries(template.standards)) { - combinedStandards[standardKey] = standardValue; - } - } - - // Count each action type: - let remediateCount = 0; - let alertCount = 0; - let reportCount = 0; - - for (const [, standard] of Object.entries(combinedStandards)) { - let actions = standard.action || []; - if (!Array.isArray(actions)) { - actions = [actions]; - } - actions.forEach((actionObj) => { - if (actionObj?.value === "Remediate") { - remediateCount++; - } else if (actionObj?.value === "Alert") { - alertCount++; - } else if (actionObj?.value === "Report") { - reportCount++; - } - }); - } - - const total = Object.keys(combinedStandards).length; - - return { remediateCount, alertCount, reportCount, total }; - } - - const driftData = processDriftDataForTenant(driftApi.data, currentTenant); - const { remediateCount, alertCount, reportCount, total } = getActionCountsForTenant( - standards.data, - currentTenant - ); - - const [PortalMenuItems, setPortalMenuItems] = useState([]); - const [partnersVisible, setPartnersVisible] = useState(false); - - const formatStorageSize = (sizeInMB) => { - if (sizeInMB >= 1024) { - return `${(sizeInMB / 1024).toFixed(2)}GB`; - } - return `${sizeInMB}MB`; - }; - - // Function to filter portals based on user preferences - const getFilteredPortals = () => { - const defaultLinks = { - M365_Portal: true, - Exchange_Portal: true, - Entra_Portal: true, - Teams_Portal: true, - Azure_Portal: true, - Intune_Portal: true, - SharePoint_Admin: true, - Security_Portal: true, - Compliance_Portal: true, - Power_Platform_Portal: true, - Power_BI_Portal: true, - }; - - let portalLinks; - if (settings.UserSpecificSettings?.portalLinks) { - portalLinks = { ...defaultLinks, ...settings.UserSpecificSettings.portalLinks }; - } else if (settings.portalLinks) { - portalLinks = { ...defaultLinks, ...settings.portalLinks }; - } else { - portalLinks = defaultLinks; - } - - // Filter the portals based on user settings - return Portals.filter((portal) => { - const settingKey = portal.name; - return settingKey ? portalLinks[settingKey] === true : true; - }); - }; - - useEffect(() => { - if (currentTenantInfo.isSuccess) { - const tenantLookup = currentTenantInfo.data?.find( - (tenant) => tenant.defaultDomainName === currentTenant - ); - - // Get filtered portals based on user preferences - const filteredPortals = getFilteredPortals(); - - const menuItems = filteredPortals.map((portal) => ({ - label: portal.label, - target: "_blank", - link: portal.url.replace(portal.variable, tenantLookup?.[portal.variable]), - icon: portal.icon, - })); - setPortalMenuItems(menuItems); - } - }, [ - currentTenantInfo.isSuccess, - currentTenant, - settings.portalLinks, - settings.UserSpecificSettings, - ]); - - return ( - <> - - Dashboard - - - - - - - - - - - {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} - - - - - - - - - - - - - - - - - - - - - {/* Converted Domain Names to Property List */} - - ({ - label: "", - value: domain.name, - }))} - actionButton={ - organization.data?.verifiedDomains?.length > 3 && ( - - ) - } - /> - - - - ({ - label: partner.TenantInfo?.displayName, - value: partner.TenantInfo?.defaultDomainName, - }))} - actionButton={ - partners.data?.Results?.length > 3 && ( - - ) - } - /> - - - - - plan.capabilityStatus === "Enabled" && - ["exchange", "AADPremiumService", "WindowsDefenderATP"].includes( - plan.service - ) - ) - .reduce((uniqueServices, curr) => { - const serviceLabel = - curr.service === "exchange" - ? "Exchange" - : curr.service === "AADPremiumService" - ? "AAD Premium" - : curr.service === "Windows Defender" - ? "Windows Defender" - : curr.service; - - if (!uniqueServices.includes(serviceLabel)) { - uniqueServices.push(serviceLabel); - } - return uniqueServices; - }, []) - .join(", "), - }, - ]} - /> - - - - - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; +export default DashboardV2; From 431e8706d27a86d37a64635bdf7e5b61c924d320 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:21:28 +0100 Subject: [PATCH 076/111] updates dashboard --- src/pages/dashboardv1.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/dashboardv1.js b/src/pages/dashboardv1.js index dd9f5300d8d9..a2054d962e5e 100644 --- a/src/pages/dashboardv1.js +++ b/src/pages/dashboardv1.js @@ -284,7 +284,9 @@ const Page = () => { actions={PortalMenuItems} disabled={!currentTenantInfo.isSuccess || PortalMenuItems.length === 0} /> - + {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} From 824a9ed837a857de53105b37b4d0008abe2d9fb6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 13:38:09 +0100 Subject: [PATCH 077/111] fix report creation --- src/pages/dashboardv1.js | 4 +--- src/pages/dashboardv2/index.js | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/pages/dashboardv1.js b/src/pages/dashboardv1.js index a2054d962e5e..dd9f5300d8d9 100644 --- a/src/pages/dashboardv1.js +++ b/src/pages/dashboardv1.js @@ -284,9 +284,7 @@ const Page = () => { actions={PortalMenuItems} disabled={!currentTenantInfo.isSuccess || PortalMenuItems.length === 0} /> - + {/* TODO: Remove Card from inside CippUniversalSearch to avoid double border */} diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 21b814672044..eb145a6b8a7b 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -8,6 +8,7 @@ import { Divider, Button, Skeleton, + Tooltip, } from "@mui/material"; import { useState, useEffect } from "react"; import { useRouter } from "next/router"; @@ -234,19 +235,24 @@ const Page = () => { disabled={!currentTenantInfo.isSuccess || portalMenuItems.length === 0} /> - + + + + + + } + > + + + Configure maximum allowed duration for Just-In-Time (JIT) admin accounts. This setting + helps enforce security policies by preventing technicians from creating JIT admin accounts + with excessively long lifespans. + + + {/* Maximum Duration Section */} + + + Maximum Duration + + { + // Allow empty value (no limit) + if (!value || typeof value !== "string" || value.trim() === "") { + return true; + } + const iso8601Regex = + /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; + if (!iso8601Regex.test(value)) { + return "Invalid format. Use PT1H, P1D, P7D, P28D, etc."; + } + return true; + }, + }, + }} + formControl={formControl} + /> + + + + + Leave empty for no limit on JIT admin account duration. When set, technicians cannot + create JIT admin accounts with durations exceeding this limit. This setting applies + globally to all tenants. + + + + {/* API Results */} + + + + ); +}; + +export default CippJitAdminSettings; diff --git a/src/pages/cipp/settings/index.js b/src/pages/cipp/settings/index.js index e6e5447ba12f..487e9a1cc719 100644 --- a/src/pages/cipp/settings/index.js +++ b/src/pages/cipp/settings/index.js @@ -11,6 +11,7 @@ import CippCacheSettings from "/src/components/CippSettings/CippCacheSettings"; import CippBackupSettings from "/src/components/CippSettings/CippBackupSettings"; import CippBrandingSettings from "/src/components/CippSettings/CippBrandingSettings"; import CippBackupRetentionSettings from "/src/components/CippSettings/CippBackupRetentionSettings"; +import CippJitAdminSettings from "/src/components/CippSettings/CippJitAdminSettings"; const Page = () => { return ( @@ -36,6 +37,9 @@ const Page = () => { + + + ); diff --git a/src/pages/cipp/super-admin/tabOptions.json b/src/pages/cipp/super-admin/tabOptions.json index 565a28588500..aa8a260e614a 100644 --- a/src/pages/cipp/super-admin/tabOptions.json +++ b/src/pages/cipp/super-admin/tabOptions.json @@ -11,10 +11,6 @@ "label": "Time Settings", "path": "/cipp/super-admin/time-settings" }, - { - "label": "JIT Admin Settings", - "path": "/cipp/super-admin/jit-admin-settings" - }, { "label": "CIPP Roles", "path": "/cipp/super-admin/cipp-roles" From 990b0d14f8c906da777c8ed8d936cb235186c2b7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:15:19 +0100 Subject: [PATCH 080/111] height fix. --- src/components/CippSettings/CippBrandingSettings.jsx | 2 +- src/components/CippSettings/CippJitAdminSettings.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippSettings/CippBrandingSettings.jsx b/src/components/CippSettings/CippBrandingSettings.jsx index a75330da986b..88838995811a 100644 --- a/src/components/CippSettings/CippBrandingSettings.jsx +++ b/src/components/CippSettings/CippBrandingSettings.jsx @@ -95,7 +95,7 @@ const CippBrandingSettings = () => { return ( - diff --git a/src/pages/dashboardv2/tabOptions.json b/src/pages/dashboardv2/tabOptions.json index 4c2bb6411b4d..952c392e5c89 100644 --- a/src/pages/dashboardv2/tabOptions.json +++ b/src/pages/dashboardv2/tabOptions.json @@ -10,5 +10,9 @@ { "label": "Devices", "path": "/dashboardv2/devices" + }, + { + "label": "Previous Dashboard Experience", + "path": "/dashboardv1" } ] From 381ad92944d5b9f55ab9a1ab481bc49a32707461 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 14 Jan 2026 01:18:30 +0100 Subject: [PATCH 084/111] dashboard fix --- src/pages/dashboardv2/index.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index ee645d5d6f5a..6f2f7d524907 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -19,17 +19,6 @@ import { ApiGetCall } from "/src/api/ApiCall.jsx"; import Portals from "/src/data/portals"; import { BulkActionsMenu } from "/src/components/bulk-actions-menu.js"; import { ExecutiveReportButton } from "/src/components/ExecutiveReportButton.js"; -import { - BarChart, - Bar, - PieChart, - Pie, - XAxis, - YAxis, - ResponsiveContainer, - Tooltip as RechartsTooltip, - LabelList, -} from "recharts"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import tabOptions from "./tabOptions"; From 4933cd827dcb1950a017b5ae6d34b2b553857e04 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 14 Jan 2026 01:18:50 +0100 Subject: [PATCH 085/111] dashboardv2 fixes --- src/pages/dashboardv2/index.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 6f2f7d524907..3dc8784a0a9b 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -1,15 +1,4 @@ -import { - Box, - Card, - CardContent, - CardHeader, - Container, - Typography, - Divider, - Button, - Skeleton, - Tooltip, -} from "@mui/material"; +import { Box, Card, CardContent, Container, Button, Tooltip } from "@mui/material"; import { useState, useEffect } from "react"; import { useRouter } from "next/router"; import { useForm, useWatch } from "react-hook-form"; From 3ed3b57b2148e34185a812eac58512c669836138 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 14 Jan 2026 01:26:05 +0100 Subject: [PATCH 086/111] remove all tenant support --- src/pages/dashboardv2/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 3dc8784a0a9b..5f236257b5c4 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -391,7 +391,7 @@ const Page = () => { }; Page.getLayout = (page) => ( - + {page} ); From b55ac1a8131fb220cf548ee8b51e232f38b39f67 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 14 Jan 2026 11:28:20 -0500 Subject: [PATCH 087/111] Fix issue with undefined JIT templates --- .../identity/administration/jit-admin/add.jsx | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index 289ee607fb59..c1e60624caa4 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -16,13 +16,15 @@ const Page = () => { const formControl = useForm({ mode: "onChange" }); const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); const [selectedTemplate, setSelectedTemplate] = useState(null); - + const jitAdminTemplates = ApiGetCall({ - url: selectedTenant ? `/api/ListJITAdminTemplates?TenantFilter=${selectedTenant.value}` : undefined, - queryKey: selectedTenant ? `JITAdminTemplates-${selectedTenant.value}` : undefined, + url: selectedTenant + ? `/api/ListJITAdminTemplates?TenantFilter=${selectedTenant.value}` + : undefined, + queryKey: selectedTenant ? `JITAdminTemplates-${selectedTenant.value}` : "JITAdminTemplates", refetchOnMount: false, refetchOnReconnect: false, - enabled: !!selectedTenant, + waiting: !!selectedTenant, }); const watcher = useWatch({ control: formControl.control }); @@ -30,7 +32,9 @@ const Page = () => { // Simple duration parser for basic ISO 8601 durations const parseDuration = (duration) => { if (!duration) return null; - const matches = duration.match(/P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/); + const matches = duration.match( + /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/ + ); if (!matches) return null; return { years: parseInt(matches[1] || 0), @@ -47,7 +51,7 @@ const Page = () => { if (!date || !duration) return null; const parsed = parseDuration(duration); if (!parsed) return null; - + const result = new Date(date); result.setFullYear(result.getFullYear() + parsed.years); result.setMonth(result.getMonth() + parsed.months); @@ -64,24 +68,22 @@ const Page = () => { useEffect(() => { if (jitAdminTemplates.isSuccess && !watcher.jitAdminTemplate) { const templates = jitAdminTemplates.data || []; - + // First, try to find a tenant-specific default template let defaultTemplate = templates.find( - (template) => - template.defaultForTenant === true && + (template) => + template.defaultForTenant === true && template.tenantFilter !== "AllTenants" && template.tenantFilter === selectedTenant?.value ); - + // If not found, fall back to AllTenants default template if (!defaultTemplate) { defaultTemplate = templates.find( - (template) => - template.defaultForTenant === true && - template.tenantFilter === "AllTenants" + (template) => template.defaultForTenant === true && template.tenantFilter === "AllTenants" ); } - + if (defaultTemplate) { formControl.setValue("jitAdminTemplate", { label: defaultTemplate.templateName, @@ -124,8 +126,12 @@ const Page = () => { // Set all template-driven fields formControl.setValue("adminRoles", template.defaultRoles || [], { shouldDirty: true }); - formControl.setValue("expireAction", template.defaultExpireAction || null, { shouldDirty: true }); - formControl.setValue("postExecution", template.defaultNotificationActions || [], { shouldDirty: true }); + formControl.setValue("expireAction", template.defaultExpireAction || null, { + shouldDirty: true, + }); + formControl.setValue("postExecution", template.defaultNotificationActions || [], { + shouldDirty: true, + }); formControl.setValue("UseTAP", template.generateTAPByDefault ?? false, { shouldDirty: true }); formControl.setValue("reason", template.reasonTemplate || "", { shouldDirty: true }); @@ -151,9 +157,10 @@ const Page = () => { // Dates if (template.defaultDuration) { - const duration = typeof template.defaultDuration === "object" && template.defaultDuration !== null - ? template.defaultDuration.value - : template.defaultDuration; + const duration = + typeof template.defaultDuration === "object" && template.defaultDuration !== null + ? template.defaultDuration.value + : template.defaultDuration; const start = roundDown15(new Date()); const unixStart = Math.floor(start.getTime() / 1000); formControl.setValue("startDate", unixStart, { shouldDirty: true }); @@ -166,9 +173,11 @@ const Page = () => { // Recalculate end date when start date changes and template has default duration useEffect(() => { if (watcher.startDate && selectedTemplate?.defaultDuration) { - const durationValue = typeof selectedTemplate.defaultDuration === 'object' && selectedTemplate.defaultDuration !== null - ? selectedTemplate.defaultDuration.value - : selectedTemplate.defaultDuration; + const durationValue = + typeof selectedTemplate.defaultDuration === "object" && + selectedTemplate.defaultDuration !== null + ? selectedTemplate.defaultDuration.value + : selectedTemplate.defaultDuration; const startDateDate = new Date(watcher.startDate * 1000); const endDateObj = addDurationToDate(startDateDate, durationValue); if (endDateObj) { @@ -207,7 +216,7 @@ const Page = () => { multiple={false} creatable={false} options={ - jitAdminTemplates.isSuccess + jitAdminTemplates.isSuccess && Array.isArray(jitAdminTemplates.data) ? jitAdminTemplates.data?.map((template) => ({ label: template.templateName, value: template.GUID, From 88c37f06d74969592a5c83796d19f3771ac68206 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:46:05 +0100 Subject: [PATCH 088/111] Fix tag monitoring --- src/pages/tenant/manage/drift.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 6f3aac1cf53c..eb8d8843b979 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -88,6 +88,12 @@ const ManageDriftPage = () => { queryKey: "ListDriftTemplates", }); + // API call to get all Intune templates for displayName lookup + const intuneTemplatesApi = ApiGetCall({ + url: "/api/ListIntuneTemplates", + queryKey: "ListIntuneTemplates", + }); + // API call for standards comparison (when templateId is available) const comparisonApi = ApiGetCall({ url: "/api/ListStandardsCompare", @@ -151,6 +157,8 @@ const ManageDriftPage = () => { // For template types, extract the display name from standardSettings if (standardName.startsWith("IntuneTemplate.")) { const guid = standardName.substring("IntuneTemplate.".length); + + // First try to find in standardSettings const intuneTemplates = item.driftSettings?.standardSettings?.IntuneTemplate; if (Array.isArray(intuneTemplates)) { const template = intuneTemplates.find((t) => t.TemplateList?.value === guid); @@ -158,6 +166,15 @@ const ManageDriftPage = () => { displayName = template.TemplateList.label; } } + + // If not found in standardSettings, look up in all Intune templates (for tag templates) + if (!displayName && intuneTemplatesApi.data) { + const template = intuneTemplatesApi.data.find((t) => t.GUID === guid); + if (template?.Displayname) { + displayName = template.Displayname; + } + } + // If template not found, return null to filter it out later if (!displayName) { return null; From 08b4a55766a233ca481a005fe2c0c2e3ef114a19 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 14 Jan 2026 19:46:25 +0100 Subject: [PATCH 089/111] Tenant drift management --- src/pages/tenant/manage/drift.js | 406 +++++++++++++++---------------- 1 file changed, 203 insertions(+), 203 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index eb8d8843b979..ac2ec90871f3 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -157,7 +157,7 @@ const ManageDriftPage = () => { // For template types, extract the display name from standardSettings if (standardName.startsWith("IntuneTemplate.")) { const guid = standardName.substring("IntuneTemplate.".length); - + // First try to find in standardSettings const intuneTemplates = item.driftSettings?.standardSettings?.IntuneTemplate; if (Array.isArray(intuneTemplates)) { @@ -166,7 +166,7 @@ const ManageDriftPage = () => { displayName = template.TemplateList.label; } } - + // If not found in standardSettings, look up in all Intune templates (for tag templates) if (!displayName && intuneTemplatesApi.data) { const template = intuneTemplatesApi.data.find((t) => t.GUID === guid); @@ -174,7 +174,7 @@ const ManageDriftPage = () => { displayName = template.Displayname; } } - + // If template not found, return null to filter it out later if (!displayName) { return null; @@ -418,229 +418,229 @@ const ManageDriftPage = () => { return true; }) .map((deviation, index) => { - // Check if this should be skipped due to missing license - const isLicenseSkipped = deviation.LicenseAvailable === false; - - // Check if we have both ExpectedValue and CurrentValue for comparison - let isActuallyCompliant = false; - let jsonDifferences = null; - - if (deviation.ExpectedValue && deviation.CurrentValue) { - jsonDifferences = compareJsonObjects(deviation.ExpectedValue, deviation.CurrentValue); - // If there are no differences, this is actually compliant - if (jsonDifferences === null) { - isActuallyCompliant = true; + // Check if this should be skipped due to missing license + const isLicenseSkipped = deviation.LicenseAvailable === false; + + // Check if we have both ExpectedValue and CurrentValue for comparison + let isActuallyCompliant = false; + let jsonDifferences = null; + + if (deviation.ExpectedValue && deviation.CurrentValue) { + jsonDifferences = compareJsonObjects(deviation.ExpectedValue, deviation.CurrentValue); + // If there are no differences, this is actually compliant + if (jsonDifferences === null) { + isActuallyCompliant = true; + } } - } - // Prioritize standardDisplayName from drift data (which has user-friendly names for templates) - // then fallback to standards.json lookup, then raw name - const prettyName = - deviation.standardDisplayName || - getStandardPrettyName(deviation.standardName) || - deviation.standardName || - "Unknown Standard"; - - // Get description from standards.json first, then fallback to standardDescription from deviation - const description = - getStandardDescription(deviation.standardName) || - deviation.standardDescription || - "No description available"; - - // Determine the actual status - // If actually compliant (values match), mark as aligned regardless of input status - // If license is skipped, mark as skipped - // Otherwise use the provided status - const actualStatus = isActuallyCompliant - ? "aligned" - : isLicenseSkipped - ? "skipped" - : statusOverride || deviation.Status || deviation.state; - const actualStatusText = isActuallyCompliant - ? "Compliant" - : isLicenseSkipped - ? "Skipped - No License Available" - : getDeviationStatusText(actualStatus); - - // For skipped items, show different expected/received values - let displayExpectedValue = deviation.ExpectedValue || deviation.expectedValue; - let displayReceivedValue = deviation.CurrentValue || deviation.receivedValue; - - // If we have JSON differences, show only the differences - if (jsonDifferences && !isLicenseSkipped && !isActuallyCompliant) { - displayExpectedValue = JSON.stringify(jsonDifferences, null, 2); - displayReceivedValue = "See differences in Expected column"; - } - - return { - id: statusOverride ? `${statusOverride}-${index + 1}` : `current-${index + 1}`, - cardLabelBox: { - cardLabelBoxHeader: getDeviationIcon(actualStatus), - }, - text: prettyName, - subtext: description, - statusColor: isLicenseSkipped ? "text.secondary" : getDeviationColor(actualStatus), - statusText: actualStatusText, - standardName: deviation.standardName, // Store the original standardName for action handlers - receivedValue: deviation.receivedValue, // Store the original receivedValue for action handlers - expectedValue: deviation.expectedValue, // Store the original expectedValue for action handlers - originalDeviation: deviation, // Store the complete original deviation object for reference - isLicenseSkipped: isLicenseSkipped, // Flag for filtering and disabling actions - isActuallyCompliant: isActuallyCompliant, // Flag to move to compliant section - children: ( - - {description && description !== "No description available" && ( - - {description} - - )} + // Prioritize standardDisplayName from drift data (which has user-friendly names for templates) + // then fallback to standards.json lookup, then raw name + const prettyName = + deviation.standardDisplayName || + getStandardPrettyName(deviation.standardName) || + deviation.standardName || + "Unknown Standard"; + + // Get description from standards.json first, then fallback to standardDescription from deviation + const description = + getStandardDescription(deviation.standardName) || + deviation.standardDescription || + "No description available"; + + // Determine the actual status + // If actually compliant (values match), mark as aligned regardless of input status + // If license is skipped, mark as skipped + // Otherwise use the provided status + const actualStatus = isActuallyCompliant + ? "aligned" + : isLicenseSkipped + ? "skipped" + : statusOverride || deviation.Status || deviation.state; + const actualStatusText = isActuallyCompliant + ? "Compliant" + : isLicenseSkipped + ? "Skipped - No License Available" + : getDeviationStatusText(actualStatus); + + // For skipped items, show different expected/received values + let displayExpectedValue = deviation.ExpectedValue || deviation.expectedValue; + let displayReceivedValue = deviation.CurrentValue || deviation.receivedValue; + + // If we have JSON differences, show only the differences + if (jsonDifferences && !isLicenseSkipped && !isActuallyCompliant) { + displayExpectedValue = JSON.stringify(jsonDifferences, null, 2); + displayReceivedValue = "See differences in Expected column"; + } - {isLicenseSkipped && ( - - - ⚠️ This standard was skipped because the required license is not available for - this tenant. + return { + id: statusOverride ? `${statusOverride}-${index + 1}` : `current-${index + 1}`, + cardLabelBox: { + cardLabelBoxHeader: getDeviationIcon(actualStatus), + }, + text: prettyName, + subtext: description, + statusColor: isLicenseSkipped ? "text.secondary" : getDeviationColor(actualStatus), + statusText: actualStatusText, + standardName: deviation.standardName, // Store the original standardName for action handlers + receivedValue: deviation.receivedValue, // Store the original receivedValue for action handlers + expectedValue: deviation.expectedValue, // Store the original expectedValue for action handlers + originalDeviation: deviation, // Store the complete original deviation object for reference + isLicenseSkipped: isLicenseSkipped, // Flag for filtering and disabling actions + isActuallyCompliant: isActuallyCompliant, // Flag to move to compliant section + children: ( + + {description && description !== "No description available" && ( + + {description} - - )} + )} + + {isLicenseSkipped && ( + + + ⚠️ This standard was skipped because the required license is not available for + this tenant. + + + )} - {(displayExpectedValue && displayExpectedValue !== "Compliant with template") || - displayReceivedValue ? ( - - {displayExpectedValue && displayExpectedValue !== "Compliant with template" && ( - - - {jsonDifferences ? "Differences" : "Expected"} - - + {(displayExpectedValue && displayExpectedValue !== "Compliant with template") || + displayReceivedValue ? ( + + {displayExpectedValue && displayExpectedValue !== "Compliant with template" && ( + - {displayExpectedValue} + {jsonDifferences ? "Differences" : "Expected"} - - - )} - - {displayReceivedValue && !jsonDifferences && ( - - - Current - - - - {displayReceivedValue} - - - - )} - - ) : null} - - {(deviation.Reason || - deviation.lastChangedByUser || - processedDriftData.latestDataCollection) && ( - <> - - - {deviation.Reason && ( - - - Reason - - {deviation.Reason} + + {displayExpectedValue} + + )} - {deviation.lastChangedByUser && ( - + + {displayReceivedValue && !jsonDifferences && ( + - Changed By + Current - {deviation.lastChangedByUser} - - )} - {processedDriftData.latestDataCollection && ( - - - Last Updated - - - {new Date(processedDriftData.latestDataCollection).toLocaleString()} - + + {displayReceivedValue} + + )} - - )} - - ), - }; - }); + ) : null} + + {(deviation.Reason || + deviation.lastChangedByUser || + processedDriftData.latestDataCollection) && ( + <> + + + {deviation.Reason && ( + + + Reason + + {deviation.Reason} + + )} + {deviation.lastChangedByUser && ( + + + Changed By + + {deviation.lastChangedByUser} + + )} + {processedDriftData.latestDataCollection && ( + + + Last Updated + + + {new Date(processedDriftData.latestDataCollection).toLocaleString()} + + + )} + + + )} + + ), + }; + }); }; const deviationItems = createDeviationItems(processedDriftData.currentDeviations); From d3a72fa32cb937dbaaa8a6e08ceadfcd79127402 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 01:26:59 +0100 Subject: [PATCH 090/111] Fix applied Standards --- src/pages/tenant/manage/applied-standards.js | 177 +++++++++++++++---- 1 file changed, 141 insertions(+), 36 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index a4cf1a470593..7879f24b17b7 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -146,22 +146,17 @@ const Page = () => { if (standardKey === "IntuneTemplate" && Array.isArray(standardConfig)) { standardConfig.forEach((templateItem, index) => { if (!templateItem) return; // Skip null items - console.log("Processing IntuneTemplate item:", templateItem); + + // Check for both addedFields.templates AND rawData.templates + const tagTemplates = templateItem["TemplateList-Tags"]?.addedFields?.templates || + templateItem["TemplateList-Tags"]?.rawData?.templates; + if ( templateItem["TemplateList-Tags"]?.value && - templateItem["TemplateList-Tags"]?.addedFields?.templates + tagTemplates ) { - console.log( - "Found TemplateList-Tags for IntuneTemplate:", - templateItem["TemplateList-Tags"] - ); - console.log( - "Templates to expand:", - templateItem["TemplateList-Tags"].addedFields.templates - ); - templateItem["TemplateList-Tags"].addedFields.templates.forEach( + tagTemplates.forEach( (expandedTemplate) => { - console.log("Expanding IntuneTemplate:", expandedTemplate); const itemTemplateId = expandedTemplate.GUID; const standardId = `standards.IntuneTemplate.${itemTemplateId}`; const standardInfo = standards.find( @@ -222,6 +217,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -321,6 +318,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -369,23 +368,18 @@ const Page = () => { // Process each ConditionalAccessTemplate item separately standardConfig.forEach((templateItem, index) => { if (!templateItem) return; // Skip null items + + // Check for both addedFields.templates AND rawData.templates + const tagTemplates = templateItem["TemplateList-Tags"]?.addedFields?.templates || + templateItem["TemplateList-Tags"]?.rawData?.templates; + // Check if this item has TemplateList-Tags and expand them if ( templateItem["TemplateList-Tags"]?.value && - templateItem["TemplateList-Tags"]?.addedFields?.templates + tagTemplates ) { - console.log( - "Found TemplateList-Tags for ConditionalAccessTemplate:", - templateItem["TemplateList-Tags"] - ); - console.log( - "Templates to expand:", - templateItem["TemplateList-Tags"].addedFields.templates - ); - // Expand TemplateList-Tags into multiple template items - templateItem["TemplateList-Tags"].addedFields.templates.forEach( + tagTemplates.forEach( (expandedTemplate) => { - console.log("Expanding ConditionalAccessTemplate:", expandedTemplate); const itemTemplateId = expandedTemplate.GUID; const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; const standardInfo = standards.find( @@ -432,6 +426,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -517,6 +513,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -639,6 +637,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -694,10 +694,6 @@ const Page = () => { (s) => s.standardId === standardId ); - // Determine compliance status - let isCompliant = false; - let reportingDisabled = !reportingEnabled; - // Check if the standard is directly in the tenant object (like "standards.AuditLog": {...}) const standardIdWithoutPrefix = standardId.replace("standards.", ""); const standardObject = currentTenantObj?.[standardId]; @@ -705,10 +701,19 @@ const Page = () => { // Extract the actual value from the standard object (new data structure includes .Value property) const directStandardValue = standardObject?.Value; - // Special case for boolean standards that are true in the tenant + // Determine compliance - use backend's logic: Value === true OR CurrentValue === ExpectedValue + let isCompliant = false; + let reportingDisabled = !reportingEnabled; + if (directStandardValue === true) { - // If the standard is directly in the tenant and is true, it's compliant + // Boolean true means compliant isCompliant = true; + } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { + // Compare CurrentValue and ExpectedValue (backend's comparison logic) + isCompliant = JSON.stringify(standardObject.CurrentValue) === JSON.stringify(standardObject.ExpectedValue); + } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { + // Compare CurrentValue and ExpectedValue (backend's comparison logic) + isCompliant = JSON.stringify(standardObject.CurrentValue) === JSON.stringify(standardObject.ExpectedValue); } else if (directStandardValue !== undefined) { // For non-boolean values, use strict equality isCompliant = @@ -748,6 +753,8 @@ const Page = () => { Value: directStandardValue, LastRefresh: standardObject?.LastRefresh, TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, } : currentTenantStandard?.value, standardValue: standardSettings, @@ -1697,13 +1704,109 @@ const Page = () => { standard.overridingTemplateId} ) : standard.complianceStatus === "Compliant" ? ( - - This setting is configured correctly - + <> + + This setting is configured correctly + + ) : standard.currentTenantValue?.Value === false ? ( - - This setting is not configured correctly - + <> + + This setting is not configured correctly + + {/* Show Current/Expected values for non-compliant standards */} + {standard.currentTenantValue?.CurrentValue && + standard.currentTenantValue?.ExpectedValue && ( + + + + Expected + + + + {JSON.stringify( + standard.currentTenantValue.ExpectedValue, + null, + 2 + )} + + + + + + Current + + + + {JSON.stringify( + standard.currentTenantValue.CurrentValue, + null, + 2 + )} + + + + + )} + ) : null} {/* Only show values if they're not simple true/false that's already covered by the alerts above */} @@ -1716,6 +1819,8 @@ const Page = () => { .filter( ([key]) => key !== "LastRefresh" && + key !== "CurrentValue" && + key !== "ExpectedValue" && // Skip showing the Value field separately if it's just true/false !( key === "Value" && From 76f2b7e4098d01810ef6b5ead9b12dd428c6ae21 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:57:38 +0100 Subject: [PATCH 091/111] imrovements to applied standards --- src/pages/tenant/manage/applied-standards.js | 406 +++++++++---------- 1 file changed, 199 insertions(+), 207 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 7879f24b17b7..57d7e657b539 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -146,118 +146,112 @@ const Page = () => { if (standardKey === "IntuneTemplate" && Array.isArray(standardConfig)) { standardConfig.forEach((templateItem, index) => { if (!templateItem) return; // Skip null items - + // Check for both addedFields.templates AND rawData.templates - const tagTemplates = templateItem["TemplateList-Tags"]?.addedFields?.templates || - templateItem["TemplateList-Tags"]?.rawData?.templates; - - if ( - templateItem["TemplateList-Tags"]?.value && - tagTemplates - ) { - tagTemplates.forEach( - (expandedTemplate) => { - const itemTemplateId = expandedTemplate.GUID; - const standardId = `standards.IntuneTemplate.${itemTemplateId}`; - const standardInfo = standards.find( - (s) => s.name === `standards.IntuneTemplate` - ); - - // Find the tenant's value for this specific template - const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId - ); - - // Get the standard object and its value from the tenant object - const standardObject = currentTenantObj?.[standardId]; - const directStandardValue = standardObject?.Value; - - // Determine compliance status - let isCompliant = false; - - // For IntuneTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { - isCompliant = true; - } else if ( - directStandardValue !== undefined && - typeof directStandardValue !== "object" - ) { - isCompliant = true; - } else if (currentTenantStandard) { - isCompliant = currentTenantStandard.value === true; - } + const tagTemplates = + templateItem["TemplateList-Tags"]?.addedFields?.templates || + templateItem["TemplateList-Tags"]?.rawData?.templates; + + if (templateItem["TemplateList-Tags"]?.value && tagTemplates) { + tagTemplates.forEach((expandedTemplate) => { + const itemTemplateId = expandedTemplate.GUID; + const standardId = `standards.IntuneTemplate.${itemTemplateId}`; + const standardInfo = standards.find( + (s) => s.name === `standards.IntuneTemplate` + ); + + // Find the tenant's value for this specific template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + + // Get the standard object and its value from the tenant object + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; - // Create a standardValue object that contains the template settings - const templateSettings = { - templateId, - Template: - expandedTemplate.displayName || - expandedTemplate.name || - "Unknown Template", - "Assign to": templateItem.AssignTo || "On", - "Excluded Group": templateItem.excludeGroup || "", - "Included Group": templateItem.customGroup || "", - }; - - // Check if this standard is overridden by another template - const tenantTemplateId = standardObject?.TemplateId; - const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; - const overridingTemplateName = isOverridden - ? getTemplateDisplayName(tenantTemplateId) - : null; - - allStandards.push({ - standardId, - standardName: `Intune Template: ${ - expandedTemplate.displayName || expandedTemplate.name || itemTemplateId - } (via ${templateItem["TemplateList-Tags"]?.value})`, - currentTenantValue: - standardObject !== undefined - ? { - Value: directStandardValue, - LastRefresh: standardObject?.LastRefresh, - TemplateId: tenantTemplateId, - CurrentValue: standardObject?.CurrentValue, - ExpectedValue: standardObject?.ExpectedValue, - } - : currentTenantStandard?.value, - standardValue: templateSettings, - complianceStatus: isOverridden - ? "Overridden" - : isCompliant - ? "Compliant" - : "Non-Compliant", - isOverridden, - overridingTemplateId: isOverridden ? tenantTemplateId : null, - overridingTemplateName, - complianceDetails: - standardInfo?.docsDescription || standardInfo?.helpText || "", - standardDescription: standardInfo?.helpText || "", - standardImpact: standardInfo?.impact || "Medium Impact", - standardImpactColour: standardInfo?.impactColour || "warning", - templateName: selectedTemplate?.templateName || "Standard Template", - templateActions: (() => { - const actions = templateItem.action || []; - const hasRemediate = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Remediate" || label === "remediate"; - }); - const hasReport = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Report" || label === "report"; - }); - if (hasRemediate && !hasReport) { - return [...actions, "Report"]; - } - return actions; - })(), - autoRemediate: - templateItem.autoRemediate || - templateItem.TemplateList?.autoRemediate || - false, - }); + // Determine compliance status + let isCompliant = false; + + // For IntuneTemplate, the value is true if compliant, or an object with comparison data if not compliant + if (directStandardValue === true) { + isCompliant = true; + } else if ( + directStandardValue !== undefined && + typeof directStandardValue !== "object" + ) { + isCompliant = true; + } else if (currentTenantStandard) { + isCompliant = currentTenantStandard.value === true; } - ); + + // Create a standardValue object that contains the template settings + const templateSettings = { + templateId, + Template: + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template", + "Assign to": templateItem.AssignTo || "On", + "Excluded Group": templateItem.excludeGroup || "", + "Included Group": templateItem.customGroup || "", + }; + + // Check if this standard is overridden by another template + const tenantTemplateId = standardObject?.TemplateId; + const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; + const overridingTemplateName = isOverridden + ? getTemplateDisplayName(tenantTemplateId) + : null; + + allStandards.push({ + standardId, + standardName: `Intune Template: ${ + expandedTemplate.displayName || expandedTemplate.name || itemTemplateId + } (via ${templateItem["TemplateList-Tags"]?.value})`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, + } + : currentTenantStandard?.value, + standardValue: templateSettings, + complianceStatus: isOverridden + ? "Overridden" + : isCompliant + ? "Compliant" + : "Non-Compliant", + isOverridden, + overridingTemplateId: isOverridden ? tenantTemplateId : null, + overridingTemplateName, + complianceDetails: + standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: (() => { + const actions = templateItem.action || []; + const hasRemediate = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Remediate" || label === "remediate"; + }); + const hasReport = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Report" || label === "report"; + }); + if (hasRemediate && !hasReport) { + return [...actions, "Report"]; + } + return actions; + })(), + autoRemediate: + templateItem.autoRemediate || + templateItem.TemplateList?.autoRemediate || + false, + }); + }); } else { // Regular TemplateList processing const itemTemplateId = templateItem.TemplateList?.value; @@ -368,105 +362,99 @@ const Page = () => { // Process each ConditionalAccessTemplate item separately standardConfig.forEach((templateItem, index) => { if (!templateItem) return; // Skip null items - + // Check for both addedFields.templates AND rawData.templates - const tagTemplates = templateItem["TemplateList-Tags"]?.addedFields?.templates || - templateItem["TemplateList-Tags"]?.rawData?.templates; - + const tagTemplates = + templateItem["TemplateList-Tags"]?.addedFields?.templates || + templateItem["TemplateList-Tags"]?.rawData?.templates; + // Check if this item has TemplateList-Tags and expand them - if ( - templateItem["TemplateList-Tags"]?.value && - tagTemplates - ) { - tagTemplates.forEach( - (expandedTemplate) => { - const itemTemplateId = expandedTemplate.GUID; - const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; - const standardInfo = standards.find( - (s) => s.name === `standards.ConditionalAccessTemplate` - ); - - // Find the tenant's value for this specific template - const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId - ); - const standardObject = currentTenantObj?.[standardId]; - const directStandardValue = standardObject?.Value; - const tenantTemplateId = standardObject?.TemplateId; - const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; - const overridingTemplateName = isOverridden - ? getTemplateDisplayName(tenantTemplateId) - : null; - let isCompliant = false; - - // For ConditionalAccessTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { - isCompliant = true; - } else { - isCompliant = false; - } + if (templateItem["TemplateList-Tags"]?.value && tagTemplates) { + tagTemplates.forEach((expandedTemplate) => { + const itemTemplateId = expandedTemplate.GUID; + const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; + const standardInfo = standards.find( + (s) => s.name === `standards.ConditionalAccessTemplate` + ); - // Create a standardValue object that contains the template settings - const templateSettings = { - templateId: itemTemplateId, - Template: - expandedTemplate.displayName || - expandedTemplate.name || - "Unknown Template", - }; - - allStandards.push({ - standardId, - standardName: `Conditional Access Template: ${ - expandedTemplate.displayName || expandedTemplate.name || itemTemplateId - } (via ${templateItem["TemplateList-Tags"]?.value})`, - currentTenantValue: - standardObject !== undefined - ? { - Value: directStandardValue, - LastRefresh: standardObject?.LastRefresh, - TemplateId: tenantTemplateId, - CurrentValue: standardObject?.CurrentValue, - ExpectedValue: standardObject?.ExpectedValue, - } - : currentTenantStandard?.value, - standardValue: templateSettings, - complianceStatus: isOverridden - ? "Overridden" - : isCompliant - ? "Compliant" - : "Non-Compliant", - complianceDetails: - standardInfo?.docsDescription || standardInfo?.helpText || "", - standardDescription: standardInfo?.helpText || "", - standardImpact: standardInfo?.impact || "Medium Impact", - standardImpactColour: standardInfo?.impactColour || "warning", - templateName: selectedTemplate?.templateName || "Standard Template", - templateActions: (() => { - const actions = templateItem.action || []; - const hasRemediate = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Remediate" || label === "remediate"; - }); - const hasReport = actions.some((a) => { - const label = typeof a === "object" ? a?.label || a?.value : a; - return label === "Report" || label === "report"; - }); - if (hasRemediate && !hasReport) { - return [...actions, "Report"]; - } - return actions; - })(), - autoRemediate: - templateItem.autoRemediate || - templateItem.TemplateList?.autoRemediate || - false, - isOverridden, - overridingTemplateId: isOverridden ? tenantTemplateId : null, - overridingTemplateName, - }); + // Find the tenant's value for this specific template + const currentTenantStandard = currentTenantData.find( + (s) => s.standardId === standardId + ); + const standardObject = currentTenantObj?.[standardId]; + const directStandardValue = standardObject?.Value; + const tenantTemplateId = standardObject?.TemplateId; + const isOverridden = tenantTemplateId && tenantTemplateId !== templateId; + const overridingTemplateName = isOverridden + ? getTemplateDisplayName(tenantTemplateId) + : null; + let isCompliant = false; + + // For ConditionalAccessTemplate, the value is true if compliant, or an object with comparison data if not compliant + if (directStandardValue === true) { + isCompliant = true; + } else { + isCompliant = false; } - ); + + // Create a standardValue object that contains the template settings + const templateSettings = { + templateId: itemTemplateId, + Template: + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template", + }; + + allStandards.push({ + standardId, + standardName: `Conditional Access Template: ${ + expandedTemplate.displayName || expandedTemplate.name || itemTemplateId + } (via ${templateItem["TemplateList-Tags"]?.value})`, + currentTenantValue: + standardObject !== undefined + ? { + Value: directStandardValue, + LastRefresh: standardObject?.LastRefresh, + TemplateId: tenantTemplateId, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, + } + : currentTenantStandard?.value, + standardValue: templateSettings, + complianceStatus: isOverridden + ? "Overridden" + : isCompliant + ? "Compliant" + : "Non-Compliant", + complianceDetails: + standardInfo?.docsDescription || standardInfo?.helpText || "", + standardDescription: standardInfo?.helpText || "", + standardImpact: standardInfo?.impact || "Medium Impact", + standardImpactColour: standardInfo?.impactColour || "warning", + templateName: selectedTemplate?.templateName || "Standard Template", + templateActions: (() => { + const actions = templateItem.action || []; + const hasRemediate = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Remediate" || label === "remediate"; + }); + const hasReport = actions.some((a) => { + const label = typeof a === "object" ? a?.label || a?.value : a; + return label === "Report" || label === "report"; + }); + if (hasRemediate && !hasReport) { + return [...actions, "Report"]; + } + return actions; + })(), + autoRemediate: + templateItem.autoRemediate || + templateItem.TemplateList?.autoRemediate || + false, + isOverridden, + overridingTemplateId: isOverridden ? tenantTemplateId : null, + overridingTemplateName, + }); + }); } else { // Regular TemplateList processing const itemTemplateId = templateItem.TemplateList?.value; @@ -704,16 +692,20 @@ const Page = () => { // Determine compliance - use backend's logic: Value === true OR CurrentValue === ExpectedValue let isCompliant = false; let reportingDisabled = !reportingEnabled; - + if (directStandardValue === true) { // Boolean true means compliant isCompliant = true; } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { // Compare CurrentValue and ExpectedValue (backend's comparison logic) - isCompliant = JSON.stringify(standardObject.CurrentValue) === JSON.stringify(standardObject.ExpectedValue); + isCompliant = + JSON.stringify(standardObject.CurrentValue) === + JSON.stringify(standardObject.ExpectedValue); } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { // Compare CurrentValue and ExpectedValue (backend's comparison logic) - isCompliant = JSON.stringify(standardObject.CurrentValue) === JSON.stringify(standardObject.ExpectedValue); + isCompliant = + JSON.stringify(standardObject.CurrentValue) === + JSON.stringify(standardObject.ExpectedValue); } else if (directStandardValue !== undefined) { // For non-boolean values, use strict equality isCompliant = From 6cbe2deeb0ce3aec6d8065326ec5106ed23510af Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 12:49:53 -0500 Subject: [PATCH 092/111] Add tenant fetching state and refresh to role form Introduced tenantsFetching state from ApiGetCallWithPagination and included it in the submit button's disabled logic. Also enabled the showRefresh option for the tenant select component to allow manual data refresh. --- src/components/CippSettings/CippRoleAddEdit.jsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index 1ddbf19a3db1..86bf06168ac9 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -88,7 +88,11 @@ export const CippRoleAddEdit = ({ selectedRole }) => { queryKey: "customRoleList", }); - const { data: { pages = [] } = {}, isSuccess: tenantsSuccess } = ApiGetCallWithPagination({ + const { + data: { pages = [] } = {}, + isSuccess: tenantsSuccess, + isFetching: tenantsFetching, + } = ApiGetCallWithPagination({ url: "/api/ListTenants?AllTenantSelector=true", queryKey: "ListTenants-All", }); @@ -524,6 +528,7 @@ export const CippRoleAddEdit = ({ selectedRole }) => { dataKey: "Results", labelField: "displayName", valueField: "id", + showRefresh: true, }} formControl={formControl} fullWidth={true} @@ -894,7 +899,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { className="me-2" type="submit" variant="contained" - disabled={updatePermissions.isPending || customRoleListFetching || !formState.isValid} + disabled={ + updatePermissions.isPending || + customRoleListFetching || + apiPermissionFetching || + tenantsFetching || + !formState.isValid + } startIcon={ From 48b9a450ecd442e9549150fca9ebe5c8c32d9622 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 15 Jan 2026 12:51:31 -0500 Subject: [PATCH 093/111] Update relatedQueryKeys for custom role actions Added 'customRoleTable' to the relatedQueryKeys array for clone and delete actions on custom roles to ensure proper query invalidation and data refresh. --- src/components/CippSettings/CippRoles.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CippSettings/CippRoles.jsx b/src/components/CippSettings/CippRoles.jsx index c155064b634a..34b1f08dcd78 100644 --- a/src/components/CippSettings/CippRoles.jsx +++ b/src/components/CippSettings/CippRoles.jsx @@ -44,7 +44,7 @@ const CippRoles = () => { disableVariables: true, }, ], - relatedQueryKeys: ["customRoleList"], + relatedQueryKeys: ["customRoleList", "customRoleTable"], confirmText: "Are you sure you want to clone this custom role?", condition: (row) => row?.Type === "Custom", }, @@ -63,7 +63,7 @@ const CippRoles = () => { RoleName: "RoleName", }, condition: (row) => row?.Type === "Custom", - relatedQueryKeys: ["customRoleList"], + relatedQueryKeys: ["customRoleList", "customRoleTable"], }, ]; From 841612e1aacf10386cdf91a8cc8774de2e72783f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:45:17 +0100 Subject: [PATCH 094/111] Fixed issue with buttons and improved design --- src/pages/tenant/manage/drift.js | 301 +++++++++++++++++-------------- 1 file changed, 161 insertions(+), 140 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index ac2ec90871f3..f685df9d47dd 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -921,171 +921,79 @@ const ManageDriftPage = () => { // Add action buttons to each deviation item const deviationItemsWithActions = actualDeviationItems.map((item) => { - // Check if this is a template that supports delete action - const supportsDelete = - (item.standardName?.includes("ConditionalAccessTemplate") || - item.standardName?.includes("IntuneTemplate")) && - item.expectedValue === "This policy only exists in the tenant, not in the template."; - return { ...item, - actionButton: ( - <> - - handleMenuClose(item.id)} - > - handleAction("accept-customer-specific", item.id)}> - - Accept Deviation - Customer Specific - - handleAction("accept", item.id)}> - - Accept Deviation - - {supportsDelete && ( - handleAction("deny-delete", item.id)}> - - Deny Deviation - Delete Policy - - )} - handleAction("deny-remediate", item.id)}> - - Deny Deviation - Remediate to align with template - - - + cardLabelBoxActions: ( + ), }; }); // Add action buttons to accepted deviation items const acceptedDeviationItemsWithActions = acceptedDeviationItems.map((item) => { - // Check if this is a template that supports delete action - const supportsDelete = - (item.standardName?.includes("ConditionalAccessTemplate") || - item.standardName?.includes("IntuneTemplate")) && - item.expectedValue === "This policy only exists in the tenant, not in the template."; - return { ...item, - actionButton: ( - <> - - handleMenuClose(`accepted-${item.id}`)} - > - {supportsDelete && ( - handleDeviationAction("deny-delete", item)}> - - Deny - Delete Policy - - )} - handleDeviationAction("deny-remediate", item)}> - - Deny - Remediate to align with template - - handleDeviationAction("accept-customer-specific", item)}> - - Accept - Customer Specific - - - + cardLabelBoxActions: ( + ), }; }); // Add action buttons to customer specific deviation items const customerSpecificDeviationItemsWithActions = customerSpecificDeviationItems.map((item) => { - // Check if this is a template that supports delete action - const supportsDelete = - (item.standardName?.includes("ConditionalAccessTemplate") || - item.standardName?.includes("IntuneTemplate")) && - item.expectedValue === "This policy only exists in the tenant, not in the template."; - return { ...item, - actionButton: ( - <> - - handleMenuClose(`customer-${item.id}`)} - > - {supportsDelete && ( - handleDeviationAction("deny-delete", item)}> - - Deny - Delete - - )} - handleDeviationAction("deny-remediate", item)}> - - Deny - Remediate to align with template - - handleDeviationAction("accept", item)}> - - Accept - - - - ), - }; - }); - - // Add action buttons to denied deviation items - const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ - ...item, - actionButton: ( - <> + cardLabelBoxActions: ( - handleMenuClose(`denied-${item.id}`)} - > - handleDeviationAction("accept", item)}> - - Accept - - handleDeviationAction("accept-customer-specific", item)}> - - Accept - Customer Specific - - - + ), + }; + }); + + // Add action buttons to denied deviation items + const deniedDeviationItemsWithActions = deniedDeviationItems.map((item) => ({ + ...item, + cardLabelBoxActions: ( + ), })); @@ -1663,6 +1571,119 @@ const ManageDriftPage = () => { /> )} + {/* Render all Menu components outside of card structure */} + {deviationItemsWithActions.map((item) => { + const supportsDelete = + (item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate")) && + item.expectedValue === "This policy only exists in the tenant, not in the template."; + return ( + handleMenuClose(item.id)} + > + { handleDeviationAction("accept-customer-specific", item); handleMenuClose(item.id); }}> + + Accept Deviation - Customer Specific + + { handleDeviationAction("accept", item); handleMenuClose(item.id); }}> + + Accept Deviation + + {supportsDelete && ( + { handleDeviationAction("deny-delete", item); handleMenuClose(item.id); }}> + + Deny Deviation - Delete Policy + + )} + { handleDeviationAction("deny-remediate", item); handleMenuClose(item.id); }}> + + Deny Deviation - Remediate to align with template + + + ); + })} + + {acceptedDeviationItemsWithActions.map((item) => { + const supportsDelete = + (item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate")) && + item.expectedValue === "This policy only exists in the tenant, not in the template."; + return ( + handleMenuClose(`accepted-${item.id}`)} + > + {supportsDelete && ( + { handleDeviationAction("deny-delete", item); handleMenuClose(`accepted-${item.id}`); }}> + + Deny - Delete Policy + + )} + { handleDeviationAction("deny-remediate", item); handleMenuClose(`accepted-${item.id}`); }}> + + Deny - Remediate to align with template + + { handleDeviationAction("accept-customer-specific", item); handleMenuClose(`accepted-${item.id}`); }}> + + Accept - Customer Specific + + + ); + })} + + {customerSpecificDeviationItemsWithActions.map((item) => { + const supportsDelete = + (item.standardName?.includes("ConditionalAccessTemplate") || + item.standardName?.includes("IntuneTemplate")) && + item.expectedValue === "This policy only exists in the tenant, not in the template."; + return ( + handleMenuClose(`customer-${item.id}`)} + > + {supportsDelete && ( + { handleDeviationAction("deny-delete", item); handleMenuClose(`customer-${item.id}`); }}> + + Deny - Delete + + )} + { handleDeviationAction("deny-remediate", item); handleMenuClose(`customer-${item.id}`); }}> + + Deny - Remediate to align with template + + { handleDeviationAction("accept", item); handleMenuClose(`customer-${item.id}`); }}> + + Accept + + + ); + })} + + {deniedDeviationItemsWithActions.map((item) => ( + handleMenuClose(`denied-${item.id}`)} + > + { handleDeviationAction("accept", item); handleMenuClose(`denied-${item.id}`); }}> + + Accept + + { handleDeviationAction("accept-customer-specific", item); handleMenuClose(`denied-${item.id}`); }}> + + Accept - Customer Specific + + + ))} + {/* Hidden ExecutiveReportButton that gets triggered programmatically */} Date: Thu, 15 Jan 2026 21:45:23 +0100 Subject: [PATCH 095/111] improve design --- src/pages/tenant/manage/drift.js | 84 +++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index f685df9d47dd..9d379e024d4b 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -1584,21 +1584,41 @@ const ManageDriftPage = () => { open={Boolean(anchorEl[item.id])} onClose={() => handleMenuClose(item.id)} > - { handleDeviationAction("accept-customer-specific", item); handleMenuClose(item.id); }}> + { + handleDeviationAction("accept-customer-specific", item); + handleMenuClose(item.id); + }} + > Accept Deviation - Customer Specific - { handleDeviationAction("accept", item); handleMenuClose(item.id); }}> + { + handleDeviationAction("accept", item); + handleMenuClose(item.id); + }} + > Accept Deviation {supportsDelete && ( - { handleDeviationAction("deny-delete", item); handleMenuClose(item.id); }}> + { + handleDeviationAction("deny-delete", item); + handleMenuClose(item.id); + }} + > Deny Deviation - Delete Policy )} - { handleDeviationAction("deny-remediate", item); handleMenuClose(item.id); }}> + { + handleDeviationAction("deny-remediate", item); + handleMenuClose(item.id); + }} + > Deny Deviation - Remediate to align with template @@ -1619,16 +1639,31 @@ const ManageDriftPage = () => { onClose={() => handleMenuClose(`accepted-${item.id}`)} > {supportsDelete && ( - { handleDeviationAction("deny-delete", item); handleMenuClose(`accepted-${item.id}`); }}> + { + handleDeviationAction("deny-delete", item); + handleMenuClose(`accepted-${item.id}`); + }} + > Deny - Delete Policy )} - { handleDeviationAction("deny-remediate", item); handleMenuClose(`accepted-${item.id}`); }}> + { + handleDeviationAction("deny-remediate", item); + handleMenuClose(`accepted-${item.id}`); + }} + > Deny - Remediate to align with template - { handleDeviationAction("accept-customer-specific", item); handleMenuClose(`accepted-${item.id}`); }}> + { + handleDeviationAction("accept-customer-specific", item); + handleMenuClose(`accepted-${item.id}`); + }} + > Accept - Customer Specific @@ -1649,16 +1684,31 @@ const ManageDriftPage = () => { onClose={() => handleMenuClose(`customer-${item.id}`)} > {supportsDelete && ( - { handleDeviationAction("deny-delete", item); handleMenuClose(`customer-${item.id}`); }}> + { + handleDeviationAction("deny-delete", item); + handleMenuClose(`customer-${item.id}`); + }} + > Deny - Delete )} - { handleDeviationAction("deny-remediate", item); handleMenuClose(`customer-${item.id}`); }}> + { + handleDeviationAction("deny-remediate", item); + handleMenuClose(`customer-${item.id}`); + }} + > Deny - Remediate to align with template - { handleDeviationAction("accept", item); handleMenuClose(`customer-${item.id}`); }}> + { + handleDeviationAction("accept", item); + handleMenuClose(`customer-${item.id}`); + }} + > Accept @@ -1673,11 +1723,21 @@ const ManageDriftPage = () => { open={Boolean(anchorEl[`denied-${item.id}`])} onClose={() => handleMenuClose(`denied-${item.id}`)} > - { handleDeviationAction("accept", item); handleMenuClose(`denied-${item.id}`); }}> + { + handleDeviationAction("accept", item); + handleMenuClose(`denied-${item.id}`); + }} + > Accept - { handleDeviationAction("accept-customer-specific", item); handleMenuClose(`denied-${item.id}`); }}> + { + handleDeviationAction("accept-customer-specific", item); + handleMenuClose(`denied-${item.id}`); + }} + > Accept - Customer Specific From d000695196cadcf7dae984f77301158837ce3a25 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:48:48 +0100 Subject: [PATCH 096/111] Fixed calculations --- src/pages/tenant/manage/drift.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 9d379e024d4b..b4579260f9a2 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -998,6 +998,7 @@ const ManageDriftPage = () => { })); // Calculate compliance metrics for badges + // Accepted and Customer Specific deviations count as compliant since they are user-approved // Denied deviations are included in total but not in compliant count (they haven't been fixed yet) const totalPolicies = processedDriftData.alignedCount + @@ -1006,8 +1007,13 @@ const ManageDriftPage = () => { processedDriftData.customerSpecificDeviations + processedDriftData.deniedDeviationsCount; + const compliantCount = + processedDriftData.alignedCount + + processedDriftData.acceptedDeviationsCount + + processedDriftData.customerSpecificDeviations; + const compliancePercentage = - totalPolicies > 0 ? Math.round((processedDriftData.alignedCount / totalPolicies) * 100) : 0; + totalPolicies > 0 ? Math.round((compliantCount / totalPolicies) * 100) : 0; const missingLicensePercentage = 0; // This would need to be calculated from actual license data const combinedScore = compliancePercentage + missingLicensePercentage; @@ -1392,7 +1398,7 @@ const ManageDriftPage = () => { {/* Right side - Deviation Management */} - + {/* Current Deviations Section */} {(!filterStatus || filterStatus.length === 0 || From cf1cfa76ec53aa017f2a8bdd016b923cc6de8803 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 21:53:09 +0100 Subject: [PATCH 097/111] improve padding --- src/pages/tenant/manage/applied-standards.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 57d7e657b539..573140da67f9 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -1370,7 +1370,7 @@ const Page = () => { {filteredGroupedStandards[category].map((standard, index) => ( - + Date: Thu, 15 Jan 2026 22:04:19 +0100 Subject: [PATCH 098/111] fixes with tags --- src/pages/tenant/manage/policies-deployed.js | 218 ++++++++++++++----- 1 file changed, 168 insertions(+), 50 deletions(-) diff --git a/src/pages/tenant/manage/policies-deployed.js b/src/pages/tenant/manage/policies-deployed.js index b6cca34dd67f..e0790feecdbf 100644 --- a/src/pages/tenant/manage/policies-deployed.js +++ b/src/pages/tenant/manage/policies-deployed.js @@ -57,6 +57,12 @@ const PoliciesDeployedPage = () => { enabled: !!templateId && !!tenantFilter, }); + // API call to get all Intune templates for displayName lookup + const intuneTemplatesApi = ApiGetCall({ + url: "/api/ListIntuneTemplates", + queryKey: "ListIntuneTemplates", + }); + // Find the current template from standards data const currentTemplate = (standardsApi.data || []).find( (template) => template.GUID === templateId @@ -67,42 +73,67 @@ const PoliciesDeployedPage = () => { // Helper function to get status from comparison data with deviation status const getStatus = (standardKey, templateValue = null, templateType = null) => { const comparisonKey = `standards.${standardKey}`; - const value = comparisonData[comparisonKey]?.Value; + const comparisonItem = comparisonData[comparisonKey]; + const value = comparisonItem?.Value; + // If value is true, it's deployed and compliant if (value === true) { return "Deployed"; - } else { - // Check if there's drift data for this standard to get the deviation status - const driftData = Array.isArray(driftApi.data) ? driftApi.data : []; - - // For templates, we need to match against the full template path - let searchKeys = [standardKey, `standards.${standardKey}`]; - - // Add template-specific search keys - if (templateValue && templateType) { - searchKeys.push( - `standards.${templateType}.${templateValue}`, - `${templateType}.${templateValue}`, - templateValue - ); + } + + // Check if ExpectedValue and CurrentValue match (like drift.js does) + if (comparisonItem?.ExpectedValue && comparisonItem?.CurrentValue) { + try { + const expectedStr = JSON.stringify(comparisonItem.ExpectedValue); + const currentStr = JSON.stringify(comparisonItem.CurrentValue); + if (expectedStr === currentStr) { + return "Deployed"; + } + } catch (e) { + console.error("Error comparing values:", e); } + } + + // If value is explicitly false, it means not deployed (not a deviation) + if (value === false) { + return "Not Deployed"; + } + + // If value is null/undefined, check drift data for deviation status + const driftData = Array.isArray(driftApi.data) ? driftApi.data : []; - const deviation = driftData.find((item) => - searchKeys.some( - (key) => - item.standardName === key || - item.policyName === key || - item.standardName?.includes(key) || - item.policyName?.includes(key) - ) + // For templates, we need to match against the full template path + let searchKeys = [standardKey, `standards.${standardKey}`]; + + // Add template-specific search keys + if (templateValue && templateType) { + searchKeys.push( + `standards.${templateType}.${templateValue}`, + `${templateType}.${templateValue}`, + templateValue ); + } - if (deviation && deviation.Status) { - return `Deviation - ${deviation.Status}`; - } + const deviation = driftData.find((item) => + searchKeys.some( + (key) => + item.standardName === key || + item.policyName === key || + item.standardName?.includes(key) || + item.policyName?.includes(key) + ) + ); + + if (deviation && deviation.Status) { + return `Deviation - ${deviation.Status}`; + } + // Only return "Deviation - New" if we have comparison data but value is null + if (comparisonItem) { return "Deviation - New"; } + + return "Not Configured"; }; // Helper function to get display name from drift data @@ -131,7 +162,20 @@ const PoliciesDeployedPage = () => { ) ); - return deviation?.standardDisplayName || null; + // If found in drift data, return the display name + if (deviation?.standardDisplayName) { + return deviation.standardDisplayName; + } + + // If not found in drift data and this is an Intune template, look it up in the Intune templates API + if (templateType === "IntuneTemplate" && templateValue && intuneTemplatesApi.data) { + const template = intuneTemplatesApi.data.find((t) => t.GUID === templateValue); + if (template?.Displayname) { + return template.Displayname; + } + } + + return null; }; // Helper function to get last refresh date @@ -195,19 +239,19 @@ const PoliciesDeployedPage = () => { (templateStandards.IntuneTemplate || []).forEach((template, index) => { console.log("Processing IntuneTemplate in policies-deployed:", template); + // Check if this template has TemplateList-Tags (try both property formats) + const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags; + // Check if this template has TemplateList-Tags and expand them - if ( - template["TemplateList-Tags"]?.value && - template["TemplateList-Tags"]?.addedFields?.templates - ) { + if (templateListTags?.value && templateListTags?.addedFields?.templates) { console.log( "Found TemplateList-Tags for IntuneTemplate in policies-deployed:", - template["TemplateList-Tags"] + templateListTags ); - console.log("Templates to expand:", template["TemplateList-Tags"].addedFields.templates); + console.log("Templates to expand:", templateListTags.addedFields.templates); // Expand TemplateList-Tags into multiple template items - template["TemplateList-Tags"].addedFields.templates.forEach( + templateListTags.addedFields.templates.forEach( (expandedTemplate, expandedIndex) => { console.log("Expanding IntuneTemplate in policies-deployed:", expandedTemplate); const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`; @@ -216,7 +260,7 @@ const PoliciesDeployedPage = () => { expandedTemplate.GUID, "IntuneTemplate" ); - const packageTagName = template["TemplateList-Tags"].value; + const packageTagName = templateListTags.value; const templateName = expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; @@ -234,45 +278,90 @@ const PoliciesDeployedPage = () => { ); } else { // Regular TemplateList processing - const standardKey = `IntuneTemplate.${template.TemplateList?.value}`; + const templateGuid = template.TemplateList?.value; + const standardKey = `IntuneTemplate.${templateGuid}`; const driftDisplayName = getDisplayNameFromDrift( standardKey, - template.TemplateList?.value, + templateGuid, "IntuneTemplate" ); - const templateLabel = getTemplateLabel(template.TemplateList?.value, "IntuneTemplate"); + + // Try multiple fallbacks for the name + let templateName = driftDisplayName; + if (!templateName) { + const templateLabel = getTemplateLabel(templateGuid, "IntuneTemplate"); + if (templateLabel !== "Unknown Template") { + templateName = `Intune - ${templateLabel}`; + } + } + // If still no name, try looking up directly in intuneTemplatesApi by GUID + if (!templateName && templateGuid && intuneTemplatesApi.data) { + const intuneTemplate = intuneTemplatesApi.data.find((t) => t.GUID === templateGuid); + if (intuneTemplate?.Displayname) { + templateName = intuneTemplate.Displayname; + } + } + // Final fallback + if (!templateName) { + templateName = `Intune - ${templateGuid || "Unknown Template"}`; + } intunePolices.push({ id: intunePolices.length + 1, - name: driftDisplayName || `Intune - ${templateLabel}`, + name: templateName, category: "Intune Template", platform: "Multi-Platform", - status: getStatus(standardKey, template.TemplateList?.value, "IntuneTemplate"), + status: getStatus(standardKey, templateGuid, "IntuneTemplate"), lastModified: getLastRefresh(standardKey), assignedGroups: template.AssignTo || "N/A", - templateValue: template.TemplateList?.value, + templateValue: templateGuid, }); } }); + // Add any templates from comparison data that weren't in template standards (e.g., from tags) + // Check for IntuneTemplate entries in comparison data + Object.keys(comparisonData).forEach((key) => { + if (key.startsWith("standards.IntuneTemplate.")) { + const guid = key.replace("standards.IntuneTemplate.", ""); + // Check if this GUID is already in our list + const alreadyExists = intunePolices.some((p) => p.templateValue === guid); + if (!alreadyExists && comparisonData[key]?.Value === true) { + const standardKey = `IntuneTemplate.${guid}`; + const driftDisplayName = getDisplayNameFromDrift(standardKey, guid, "IntuneTemplate"); + + intunePolices.push({ + id: intunePolices.length + 1, + name: driftDisplayName || `Intune - ${guid}`, + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(standardKey, guid, "IntuneTemplate"), + lastModified: getLastRefresh(standardKey), + assignedGroups: "N/A", + templateValue: guid, + }); + } + } + }); + // Process Conditional Access Templates const conditionalAccessPolicies = []; (templateStandards.ConditionalAccessTemplate || []).forEach((template, index) => { console.log("Processing ConditionalAccessTemplate in policies-deployed:", template); + // Check if this template has TemplateList-Tags (try both property formats) + const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags; + // Check if this template has TemplateList-Tags and expand them - if ( - template["TemplateList-Tags"]?.value && - template["TemplateList-Tags"]?.addedFields?.templates - ) { + if (templateListTags?.value && templateListTags?.addedFields?.templates) { console.log( "Found TemplateList-Tags for ConditionalAccessTemplate in policies-deployed:", - template["TemplateList-Tags"] + templateListTags ); - console.log("Templates to expand:", template["TemplateList-Tags"].addedFields.templates); + console.log("Templates to expand:", templateListTags.addedFields.templates); // Expand TemplateList-Tags into multiple template items - template["TemplateList-Tags"].addedFields.templates.forEach( + templateListTags.addedFields.templates.forEach( (expandedTemplate, expandedIndex) => { console.log( "Expanding ConditionalAccessTemplate in policies-deployed:", @@ -284,7 +373,7 @@ const PoliciesDeployedPage = () => { expandedTemplate.GUID, "ConditionalAccessTemplate" ); - const packageTagName = template["TemplateList-Tags"].value; + const packageTagName = templateListTags.value; const templateName = expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; @@ -325,6 +414,35 @@ const PoliciesDeployedPage = () => { }); } }); + + // Add any CA templates from comparison data that weren't in template standards + Object.keys(comparisonData).forEach((key) => { + if (key.startsWith("standards.ConditionalAccessTemplate.")) { + const guid = key.replace("standards.ConditionalAccessTemplate.", ""); + // Check if this GUID is already in our list + const alreadyExists = conditionalAccessPolicies.some((p) => p.templateValue === guid); + if (!alreadyExists && comparisonData[key]?.Value === true) { + const standardKey = `ConditionalAccessTemplate.${guid}`; + const driftDisplayName = getDisplayNameFromDrift( + standardKey, + guid, + "ConditionalAccessTemplate" + ); + + conditionalAccessPolicies.push({ + id: conditionalAccessPolicies.length + 1, + name: driftDisplayName || `Conditional Access - ${guid}`, + state: "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(standardKey), + status: getStatus(standardKey, guid, "ConditionalAccessTemplate"), + templateValue: guid, + }); + } + } + }); + // Simple filter for all templates (no type filtering) const templateOptions = standardsApi.data ? standardsApi.data.map((template) => ({ @@ -409,7 +527,7 @@ const PoliciesDeployedPage = () => { > - + {/* Standards Section */} }> From 0bd3db398ccad74e91a59d1a66a883cf1f75acb6 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:04:26 +0100 Subject: [PATCH 099/111] fixes with tags --- src/pages/tenant/manage/policies-deployed.js | 115 +++++++++---------- 1 file changed, 52 insertions(+), 63 deletions(-) diff --git a/src/pages/tenant/manage/policies-deployed.js b/src/pages/tenant/manage/policies-deployed.js index e0790feecdbf..6aad6040402d 100644 --- a/src/pages/tenant/manage/policies-deployed.js +++ b/src/pages/tenant/manage/policies-deployed.js @@ -80,7 +80,7 @@ const PoliciesDeployedPage = () => { if (value === true) { return "Deployed"; } - + // Check if ExpectedValue and CurrentValue match (like drift.js does) if (comparisonItem?.ExpectedValue && comparisonItem?.CurrentValue) { try { @@ -93,7 +93,7 @@ const PoliciesDeployedPage = () => { console.error("Error comparing values:", e); } } - + // If value is explicitly false, it means not deployed (not a deviation) if (value === false) { return "Not Deployed"; @@ -241,7 +241,7 @@ const PoliciesDeployedPage = () => { // Check if this template has TemplateList-Tags (try both property formats) const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags; - + // Check if this template has TemplateList-Tags and expand them if (templateListTags?.value && templateListTags?.addedFields?.templates) { console.log( @@ -251,41 +251,35 @@ const PoliciesDeployedPage = () => { console.log("Templates to expand:", templateListTags.addedFields.templates); // Expand TemplateList-Tags into multiple template items - templateListTags.addedFields.templates.forEach( - (expandedTemplate, expandedIndex) => { - console.log("Expanding IntuneTemplate in policies-deployed:", expandedTemplate); - const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`; - const driftDisplayName = getDisplayNameFromDrift( - standardKey, - expandedTemplate.GUID, - "IntuneTemplate" - ); - const packageTagName = templateListTags.value; - const templateName = - expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; - - intunePolices.push({ - id: intunePolices.length + 1, - name: `${driftDisplayName || templateName} (via ${packageTagName})`, - category: "Intune Template", - platform: "Multi-Platform", - status: getStatus(standardKey, expandedTemplate.GUID, "IntuneTemplate"), - lastModified: getLastRefresh(standardKey), - assignedGroups: template.AssignTo || "N/A", - templateValue: expandedTemplate.GUID, - }); - } - ); + templateListTags.addedFields.templates.forEach((expandedTemplate, expandedIndex) => { + console.log("Expanding IntuneTemplate in policies-deployed:", expandedTemplate); + const standardKey = `IntuneTemplate.${expandedTemplate.GUID}`; + const driftDisplayName = getDisplayNameFromDrift( + standardKey, + expandedTemplate.GUID, + "IntuneTemplate" + ); + const packageTagName = templateListTags.value; + const templateName = + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; + + intunePolices.push({ + id: intunePolices.length + 1, + name: `${driftDisplayName || templateName} (via ${packageTagName})`, + category: "Intune Template", + platform: "Multi-Platform", + status: getStatus(standardKey, expandedTemplate.GUID, "IntuneTemplate"), + lastModified: getLastRefresh(standardKey), + assignedGroups: template.AssignTo || "N/A", + templateValue: expandedTemplate.GUID, + }); + }); } else { // Regular TemplateList processing const templateGuid = template.TemplateList?.value; const standardKey = `IntuneTemplate.${templateGuid}`; - const driftDisplayName = getDisplayNameFromDrift( - standardKey, - templateGuid, - "IntuneTemplate" - ); - + const driftDisplayName = getDisplayNameFromDrift(standardKey, templateGuid, "IntuneTemplate"); + // Try multiple fallbacks for the name let templateName = driftDisplayName; if (!templateName) { @@ -351,7 +345,7 @@ const PoliciesDeployedPage = () => { // Check if this template has TemplateList-Tags (try both property formats) const templateListTags = template["TemplateList-Tags"] || template.TemplateListTags; - + // Check if this template has TemplateList-Tags and expand them if (templateListTags?.value && templateListTags?.addedFields?.templates) { console.log( @@ -361,34 +355,29 @@ const PoliciesDeployedPage = () => { console.log("Templates to expand:", templateListTags.addedFields.templates); // Expand TemplateList-Tags into multiple template items - templateListTags.addedFields.templates.forEach( - (expandedTemplate, expandedIndex) => { - console.log( - "Expanding ConditionalAccessTemplate in policies-deployed:", - expandedTemplate - ); - const standardKey = `ConditionalAccessTemplate.${expandedTemplate.GUID}`; - const driftDisplayName = getDisplayNameFromDrift( - standardKey, - expandedTemplate.GUID, - "ConditionalAccessTemplate" - ); - const packageTagName = templateListTags.value; - const templateName = - expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; - - conditionalAccessPolicies.push({ - id: conditionalAccessPolicies.length + 1, - name: `${driftDisplayName || templateName} (via ${packageTagName})`, - state: template.state || "Unknown", - conditions: "Conditional Access Policy", - controls: "Access Control", - lastModified: getLastRefresh(standardKey), - status: getStatus(standardKey, expandedTemplate.GUID, "ConditionalAccessTemplate"), - templateValue: expandedTemplate.GUID, - }); - } - ); + templateListTags.addedFields.templates.forEach((expandedTemplate, expandedIndex) => { + console.log("Expanding ConditionalAccessTemplate in policies-deployed:", expandedTemplate); + const standardKey = `ConditionalAccessTemplate.${expandedTemplate.GUID}`; + const driftDisplayName = getDisplayNameFromDrift( + standardKey, + expandedTemplate.GUID, + "ConditionalAccessTemplate" + ); + const packageTagName = templateListTags.value; + const templateName = + expandedTemplate.displayName || expandedTemplate.name || "Unknown Template"; + + conditionalAccessPolicies.push({ + id: conditionalAccessPolicies.length + 1, + name: `${driftDisplayName || templateName} (via ${packageTagName})`, + state: template.state || "Unknown", + conditions: "Conditional Access Policy", + controls: "Access Control", + lastModified: getLastRefresh(standardKey), + status: getStatus(standardKey, expandedTemplate.GUID, "ConditionalAccessTemplate"), + templateValue: expandedTemplate.GUID, + }); + }); } else { // Regular TemplateList processing const standardKey = `ConditionalAccessTemplate.${template.TemplateList?.value}`; From 4b3f007d53039f5837be813154ae8b79f96edab9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:58:56 +0100 Subject: [PATCH 100/111] Design changes --- src/pages/tenant/manage/drift.js | 117 +++++++++++++------------------ 1 file changed, 47 insertions(+), 70 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index b4579260f9a2..dddde1a810ba 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -1144,76 +1144,7 @@ const ManageDriftPage = () => { ? driftTemplateOptions.find((option) => option.value === templateId) || null : null; const title = "Manage Drift"; - const subtitle = [ - { - icon: , - text: ( - { - const query = { ...router.query }; - if (selectedTemplate && selectedTemplate.value) { - query.templateId = selectedTemplate.value; - } else { - delete query.templateId; - } - router.replace( - { - pathname: router.pathname, - query: query, - }, - undefined, - { shallow: true } - ); - }} - sx={{ minWidth: 300 }} - placeholder="Select a drift template..." - /> - ), - }, - // Add compliance badges when data is available - ...(totalPolicies > 0 - ? [ - { - component: ( - - - - - } - label={`${compliancePercentage}% Compliant`} - variant="outlined" - size="small" - color={ - compliancePercentage === 100 - ? "success" - : compliancePercentage >= 50 - ? "warning" - : "error" - } - /> - = 80 ? "success" : combinedScore >= 60 ? "warning" : "error" - } - /> - - ), - }, - ] - : []), - ]; + const subtitle = []; return ( { variant="outlined" /> + + + Total Score + + = 80 + ? "warning" + : combinedScore >= 30 + ? "warning" + : "error" + } + variant="outlined" + /> + {/* Filters Card */} + { + const query = { ...router.query }; + if (selectedTemplate && selectedTemplate.value) { + query.templateId = selectedTemplate.value; + } else { + delete query.templateId; + } + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + }} + placeholder="Select a drift template..." + /> + Date: Thu, 15 Jan 2026 23:04:15 +0100 Subject: [PATCH 101/111] UX updates --- src/pages/tenant/manage/applied-standards.js | 84 +++++++++----------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 573140da67f9..5b6539767fce 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -963,51 +963,7 @@ const Page = () => { // Prepare title and subtitle for HeaderedTabbedLayout const title = selectedTemplate?.templateName || selectedTemplate?.displayName || "Tenant Report"; - const subtitle = [ - { - icon: , - text: ( - - { - const query = { ...router.query }; - if (selectedTemplate && selectedTemplate.value) { - query.templateId = selectedTemplate.value; - } else { - delete query.templateId; - } - router.replace( - { - pathname: router.pathname, - query: query, - }, - undefined, - { shallow: true } - ); - }} - sx={{ minWidth: 300 }} - placeholder="Select a template..." - /> - {templateId && ( - - )} - - ), - }, - ]; + const subtitle = []; // Actions for the header const actions = [ @@ -1124,7 +1080,34 @@ const Page = () => { mt: 2, }} > - + + { + const query = { ...router.query }; + if (selectedTemplate && selectedTemplate.value) { + query.templateId = selectedTemplate.value; + } else { + delete query.templateId; + } + router.replace( + { + pathname: router.pathname, + query: query, + }, + undefined, + { shallow: true } + ); + }} + sx={{ width: 300 }} + placeholder="Select template..." + /> { }, }} /> + {templateId && ( + + )} + setByUser(e.target.checked)} color="primary" /> + } + label="Group by User" + labelPlacement="start" + /> + , + ]; + + return ( + <> + {currentTenant ? ( + + This report displays cached data from the CIPP reporting database. Cache timestamps + are shown in the table. Click the Sync button to update the cache for the current + tenant. + + } + /> + ) : ( + Please select a tenant to view mailbox permissions. + )} + ({ + TenantFilter: currentTenant, + Name: `Manual Mailbox Cache Sync - ${currentTenant}`, + Command: { + value: "Set-CIPPDBCacheMailboxes", + label: "Set-CIPPDBCacheMailboxes", + }, + Parameters: { + TenantFilter: currentTenant, + }, + ScheduledTime: "0", + PostExecution: { + Webhook: false, + Email: false, + PSA: false, + }, + DisallowDuplicateName: true, + }), + }} + /> + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index c222f6418bfb..f47559d34499 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -185,7 +185,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr "reviewedDate", // App Consent Requests ]; - const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration)/; + const matchDateTime = /([dD]ate[tT]ime|[Ee]xpiration|[Tt]imestamp)/; if (timeAgoArray.includes(cellName) || matchDateTime.test(cellName)) { return isText && canReceive === false ? ( new Date(data).toLocaleString() // This runs if canReceive is false and isText is true From 202cf3490de50b8c0518146810a3fcb22fe45e43 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 00:16:14 -0500 Subject: [PATCH 105/111] Add tenant support to mailbox permissions report Updated the mailbox permissions report to handle 'AllTenants' selection. The table now conditionally includes a Tenant column, disables sync for all tenants, and improves tenant validation. Removed unnecessary Parameters from the sync dialog configuration. --- src/pages/email/reports/mailbox-permissions/index.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/email/reports/mailbox-permissions/index.js b/src/pages/email/reports/mailbox-permissions/index.js index 5fc23dd4b6de..cc35b4e824e3 100644 --- a/src/pages/email/reports/mailbox-permissions/index.js +++ b/src/pages/email/reports/mailbox-permissions/index.js @@ -13,8 +13,11 @@ const Page = () => { const currentTenant = useSettings().currentTenant; const syncDialog = useDialog(); + const isAllTenants = currentTenant === "AllTenants"; + const columns = byUser ? [ + ...(isAllTenants ? ["Tenant"] : []), "User", "UserMailboxType", "MailboxCount", @@ -23,6 +26,7 @@ const Page = () => { "PermissionCacheTimestamp", ] : [ + ...(isAllTenants ? ["Tenant"] : []), "MailboxUPN", "MailboxDisplayName", "MailboxType", @@ -48,6 +52,7 @@ const Page = () => { } size="xs" onClick={syncDialog.handleOpen} + disabled={isAllTenants} > Sync @@ -64,7 +69,7 @@ const Page = () => { return ( <> - {currentTenant ? ( + {currentTenant && currentTenant !== "" ? ( { value: "Set-CIPPDBCacheMailboxes", label: "Set-CIPPDBCacheMailboxes", }, - Parameters: { - TenantFilter: currentTenant, - }, ScheduledTime: "0", PostExecution: { Webhook: false, From cc4eb7ebcaf396704dd1d37f96c4d119a9f598ef Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:50:47 +0100 Subject: [PATCH 106/111] group by default --- src/pages/email/reports/mailbox-permissions/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/email/reports/mailbox-permissions/index.js b/src/pages/email/reports/mailbox-permissions/index.js index cc35b4e824e3..5719de756e8e 100644 --- a/src/pages/email/reports/mailbox-permissions/index.js +++ b/src/pages/email/reports/mailbox-permissions/index.js @@ -9,7 +9,7 @@ import { useDialog } from "../../../../hooks/use-dialog"; import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; const Page = () => { - const [byUser, setByUser] = useState(false); + const [byUser, setByUser] = useState(true); const currentTenant = useSettings().currentTenant; const syncDialog = useDialog(); From d7ce7b083aa77a527f2654b99acbebe2067ccde4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 10:31:46 -0500 Subject: [PATCH 107/111] Update mailbox permissions sync API and table columns Switched the mailbox permissions cache sync API from a POST to a GET request and updated its parameters for immediate execution. Removed 'MailboxCount' and 'PermissionCount' columns from the table to streamline displayed data. --- .../reports/mailbox-permissions/index.js | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/src/pages/email/reports/mailbox-permissions/index.js b/src/pages/email/reports/mailbox-permissions/index.js index 5719de756e8e..226f4c1efbd9 100644 --- a/src/pages/email/reports/mailbox-permissions/index.js +++ b/src/pages/email/reports/mailbox-permissions/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Button, FormControlLabel, Switch, Alert, SvgIcon } from "@mui/material"; import { useSettings } from "../../../../hooks/use-settings"; import { Stack } from "@mui/system"; @@ -20,7 +20,6 @@ const Page = () => { ...(isAllTenants ? ["Tenant"] : []), "User", "UserMailboxType", - "MailboxCount", "Permissions", "MailboxCacheTimestamp", "PermissionCacheTimestamp", @@ -30,7 +29,6 @@ const Page = () => { "MailboxUPN", "MailboxDisplayName", "MailboxType", - "PermissionCount", "Permissions", "MailboxCacheTimestamp", "PermissionCacheTimestamp", @@ -95,25 +93,13 @@ const Page = () => { title="Sync Mailbox Permissions Cache" fields={[]} api={{ - type: "POST", - url: "/api/AddScheduledItem", - confirmText: `Run mailbox permissions cache sync for ${currentTenant}? This will update mailbox and permission data. Scheduled tasks start within 15 minutes.`, + type: "GET", + url: "/api/ExecCIPPDBCache", + confirmText: `Run mailbox permissions cache sync for ${currentTenant}? This will update mailbox and permission data immediately.`, relatedQueryKeys: ["mailbox-permissions"], - dataFunction: () => ({ - TenantFilter: currentTenant, - Name: `Manual Mailbox Cache Sync - ${currentTenant}`, - Command: { - value: "Set-CIPPDBCacheMailboxes", - label: "Set-CIPPDBCacheMailboxes", - }, - ScheduledTime: "0", - PostExecution: { - Webhook: false, - Email: false, - PSA: false, - }, - DisallowDuplicateName: true, - }), + data: { + Name: "Mailboxes", + }, }} /> From 93bf99b2c4a8cc9c3c4011764ed4886460ecc259 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 11:45:05 -0500 Subject: [PATCH 108/111] Add sync functionality to MFA Report page Introduces a Sync button and dialog to allow users to update the cached MFA data for the current tenant. Also updates table columns to include 'Tenant' and 'CacheTimestamp' when viewing all tenants, and displays an informational alert about cached data. --- .../identity/reports/mfa-report/index.js | 112 ++++++++++++++---- 1 file changed, 90 insertions(+), 22 deletions(-) diff --git a/src/pages/identity/reports/mfa-report/index.js b/src/pages/identity/reports/mfa-report/index.js index 815e2f5a0d3c..f68a19e230c9 100644 --- a/src/pages/identity/reports/mfa-report/index.js +++ b/src/pages/identity/reports/mfa-report/index.js @@ -1,22 +1,51 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { LockPerson } from "@mui/icons-material"; +import { LockPerson, Sync } from "@mui/icons-material"; +import { Button, Alert, SvgIcon } from "@mui/material"; +import { useSettings } from "../../../../hooks/use-settings"; +import { Stack } from "@mui/system"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; const Page = () => { const pageTitle = "MFA Report"; const apiUrl = "/api/ListMFAUsers"; - const simpleColumns = [ - "UPN", - "AccountEnabled", - "isLicensed", - "MFARegistration", - "PerUser", - "CoveredBySD", - "CoveredByCA", - "MFAMethods", - "CAPolicies", - "IsAdmin", - ]; + const currentTenant = useSettings().currentTenant; + const syncDialog = useDialog(); + + const isAllTenants = currentTenant === "AllTenants"; + + const apiData = { + UseReportDB: true, + }; + const simpleColumns = isAllTenants + ? [ + "Tenant", + "UPN", + "AccountEnabled", + "isLicensed", + "MFARegistration", + "PerUser", + "CoveredBySD", + "CoveredByCA", + "MFAMethods", + "CAPolicies", + "IsAdmin", + "CacheTimestamp", + ] + : [ + "UPN", + "AccountEnabled", + "isLicensed", + "MFARegistration", + "PerUser", + "CoveredBySD", + "CoveredByCA", + "MFAMethods", + "CAPolicies", + "IsAdmin", + "CacheTimestamp", + ]; const filters = [ { filterName: "Enabled, licensed users", @@ -48,8 +77,8 @@ const Page = () => { { filterName: "Admin Users", value: [{ id: "IsAdmin", value: "Yes" }], - type: "column" - } + type: "column", + }, ]; const actions = [ @@ -78,14 +107,53 @@ const Page = () => { }, ]; + const pageActions = [ + , + ]; + return ( - + <> + + This report displays cached data from the CIPP reporting database. Click the Sync button + to update the cache for the current tenant. + + } + /> + + ); }; From 5c7fa36c10f4cf934f5bc9338218b5f37daac127 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 12:04:25 -0500 Subject: [PATCH 109/111] Add Relentless Solutions as a sponsor Added Relentless Solutions sponsor images for light and dark themes and updated the side navigation to include the new sponsor with appropriate image selection based on the current theme. --- public/sponsors/relentless-dark.png | Bin 0 -> 369397 bytes public/sponsors/relentless-light.png | Bin 0 -> 47340 bytes src/layouts/side-nav.js | 6 ++++++ 3 files changed, 6 insertions(+) create mode 100644 public/sponsors/relentless-dark.png create mode 100644 public/sponsors/relentless-light.png diff --git a/public/sponsors/relentless-dark.png b/public/sponsors/relentless-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b234ac7c79032cf0cebe5163a93bb1473d080a24 GIT binary patch literal 369397 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 z z3GcLxlnJW0Y_EgIpZ<)y55A0_Tt_l`CSN6A+U_jECP&1rIT2X8KXra`{7m`Q65YZS zpOo_$AkMy^ZCT;EAoeBn7-Ebe?zBh!F%jqJZ#9Nr<&Tm6yeoaK8I6O(gw_;x&oO@2 zIF24U4paSY5XAgW@Y*>`G=A;HLaXuDUA?Zmudqnu*i0!UV*9!3GhEeJ}Q|-)TY7_8heCzvzt;_10uCyT#%GrtR2e>x2sx(sgwW4Yb$-j7dJW4$I5< z;H=h$rSUWZQpN#y-M+gzA4aL{|M|UU%bH}nHp$NIk)mk+^k!RzRZ{T%SPJ}s)eT&R z=NsO-W9g^qng{~ajvvj}&jWJrx#m!pX@4IVcKq0zHWpC7VbvRq9g*o-K6tg}(iV zSCopm@=izm2n|f!xJC8u!$k>k@INh3I?2PGW)6=?!ohDtPLASmIqiAQe9MvsylieZ zK*D+NncncuR&eQ=`RxKKxFMNHCUp~?WUfR+VB)>#+KfW~dH#Gea&uhb!>88;u5n*y zDhX6#({o5vxhjZu`Y3RF*+cmKBAM+#h7`!?zNhDVkyT;;W%OjacVl0D^@>P^->rr6 zr2jY{cwS+vfDTQ#^ULXeW!FiwQzXgm4j2mJmavj@DZ9FMX@TUd9|$Gq`@_PW_ssmq zqHe<|d#&CIaIgL~+2}*)^u$r)$(J75CDbW+pFrc8 zsH?Y^6*{ANe$8Gd1l=~HHoDoJ{pb{QcZ?%udv2=lz38o_>B~{Mi0^b;-IZNnw4i;_ z^Gr(cpc_GUx-h<(7V)0WSXC3Fln}3Y#XI00eQ370p2D%GIikxyJua>@DM-OB8heqg zD{ULRT#4=a{1_@2s^0+JeD>S;Z8fi0 zZJ+TC;U@L`dpo;ni;|#0m3zW?3K`rpqmhuYGbJ}Z-;8p+N1dZX1|@egIOEWxx|$EM z4@_Eyg~&jH?~S6MH!?rm==r}jHQ2t_I3K<6-Ei8{Pa$q!Hql2+9^@kTP7)xt($C@ zH29KMFW69^r)DQ%D9F@dPQK#;Zr0Q+Np+KPI2`U(6$L!X!RP(&Xl;~))pQY_M1SUzIO&0E_{Dn+R)+{+EWmZixPglR3%|v&-7=82 z7K0SVk$_$Vg=TP|6C$zdVMsighy@e-#Vj@VcUzzDl=fP8zpLC~e}BFhZ*)5+LHWUb}ost;I>+dhh@G zVErxbnRnES2sHbDOFTk>f1IzaWERByQ)0DV@#ap_L^gg*TEeIL^vpRPJvp#MsBvA~ zY@zxMa;>FAd?2#o_Q;hF8H#SLuT923wRaSHw%9(2y{yb3fhC!fZ6BAL9I=9qi6t+% zFE0zWuBWYn)U8FWxbe7;KP-s(jOC22kRSq$BZKLN1y1hcnrLRn%UJQ0DFuw&=9eA} zJC-eJKH1{MiNSgvXmX9~x$4AWk(0BXIjOoF`6ujux1<;_@Oqbm*V!nC1h1OT#kGJG z{exadqW0XIhIr5Sv)+%nPncBT1DP1LkS`AX4Xe%DXMcFPU8H& z68euAcO(F?9B^#1@2h(QJKG#20B_#L9YLq-h8V3c|63@(wBVniDPkI!b0+(Z{~i zo^fnXVb!jUn-`6T0@%F0FKMrF*C;X&saY9*IA^FH2fMm!L#x$!dhC#tp04vne(QAN zeTpUWVE4x|!mpKmZ(Z;oR;zh=qIC*7`p~Tl4zm~ogz~f;2tk+n7APe1WE*qR-)D+h zaIpDTn$-4_uaFUUW8DBZ8=a?0KDn5PMK}8GHYSJ$w87JcS(rH2xy>{>*Yl#$&F6k=;w5gL&39_Cc6p2~=>0 z1c&120-5UAs&ICnuH?vs!xtsP$%Z36u;wREc)cEXJv9A06rJJ zor=U)X<&?~hm!synbqWX`Dj5wi3jnTj55V#zPsBNk6tt1be|~Tb|!;8h49|WL5$w( z+yXj@u)o#hBW5{z9m3nC(Ztbefew|G4_{Kq&j~ zZ8L^OvXh;$BwM!ZiIK8p3t1wjvS!Va5Y14i6h)S-SqcdWp~Z-hY{|Z-L@CM|lK0%B z=Xt)r_um@Kocnyvxz2T6=QE2hcA@C6c}jnt!5#Sl3Cu1bH0Q-dNnlT|s5{@>aAu{n zy*Jm3Y2LRTQ=}53`zOHg@Q>0JOWt5KWUwmxa)!EG{CC5zmW&yS=^NGW;N+&ohktdiUsFBay~Kvpo3uBdJM8}zUx8)fks#Snvwn< zvr=W=s**>d?N8TV9s?J8$!~0cQb+N9ITPijCl#r`172r*VvR2^xwVv<_I>%Aj#SZW zC%aO$gL;FJ#}Y4Am!85PowvfhLGsEsdr`Aen)hDoo@Gxbg#{?U_HQb;M3AGc=DCu0 zmt_YG>(HmX=XEwQorw9^)7-7B7V`X6)2y_5KsT@{=|XbLO~bRIDYX`~5p4RsWF5yp zL$>bqJH5Ah9tnvpyWH)uknMRJ%&^P!z<%bOY~+wA;GpAiSB$m+UH2r_>layeOtAZ zCd{Gzg*M)%;ndESLg4a?tS4&R?T)+VUirUxIHPc}L4$wk-hmZ@Qvvhss;k75=>);p z;l_;TO(JEV(yeV^1iFwlDfwSgqp*3fek93MZ2v+ek-cnUu0gAbPtgCOBB2`E-<^8) zlbnJu+~Ot_New#~t@7_aw~#?|>Axf25UkToWZi~ua*fT*kj$l|eZ0|Xz7tE>Td$zw zyJMm6nH*IPdEkjrh|eYsmyFloBnkC;%1(e?YxE7BH-zHVP`yn@_{N@HpsLuYuS3enXM7E5KZb9AxSOiQV|I2lH#4(;K~m%b zcR{46(1qLG(*Jk?s8DoJIUd;bCuBDTI5Rs594(6oFrRqOOe8BHEf8x}X0&HE z0!r+cdr~Jt&>q{@YQ!Hf{&YaT?pu9}o$^Sh8R&zi#m{&p%Mu6GgF49o8e8l4cxX*W zgk)Q%HgoMKwU>UbuYRioIvLhfZUJ zZSvR|(6k8Cms_fcH*!Lu8Cij8a+>>;9WLRw0wn8`$>KNSXW+s(za8*pd297%e47T< zx?y_rCpOI;urtCF-HTNze{K?KYAbm5W}NY_~Xj3;7MlIGGgG*^oeJ84_*DN}vnu~5uRnDIoU(fL2XEO+EH#~j5Q3cmB;kD8b`$59`+$xPVN)9U%P z3o{~&b3U2dzgRV}N@o*q{ZO(hQ;B9Dfl1fF?}KI(Tl~Vs)1#xl_ii2h-y$%eOPKol zF0g8c)UT2E0Gn-L8(mmiJd3<=?aDg7;k*$x89@kKndDD_n&YXg;rCpg1 z-BS**uBGcfb8g7>*sm(_)=RZ&(oA=DT;nVCg{htjt;{ko&Cj23dJ+;@eR(DrlP^s?D`E+=)uqF0^FPQ@1!-C|#+lI4keXwEZJBiJVvBm)~K&Rua^2nCx5g zELeHQb&|!25gmoJwo~DbuN^8i?h@D3V_3R~1e##|E1@=ceCy&>wJe&`BG><&DPF76km|j!ELx8eGZ@aY$`S`uz#G$aGrj zxr>1fqqqs4?U}eg@o2CR0i5z5jX#^X=wGwP3TMB(RPN4@-}ziw&YDy7MCA3H04o3w zVP6%?r~KR8ckLFk-G3LbFT;U#N%U@~QLb-Y+Hpi0j8w}Bu{<9-r1a>gRpphpi2XZ3 zK;^>4%aV@gztgshj76nWPI zV?j3LsKce@O1JM{m0Z34!(Z3AdWT&tO*4+U8d6UQ8Tszxo4e&qIq#t_azEbQH6lTe zqO3!LMPi4ecDJ+Rebyb`WF|>3eaYL3fAw;ld23_!=yGZj1uO* ze%=X(%M@+?IfyHCMS@Bb=+!)73Y8OKX*kiqT%Ab1GT&b;uRkBeKa+QlGV;4d*0(62}^}5h+ zcrJXa*W2wyZ}p>QeCr$f{%XotF6z2cS@@#SDQZq{N~WQaPNbb2J1QYX+Lh-{SnYVz z@`^xns?bu*t(2w{TMj~1v5ow!%3EP&TYq1)8q!qCB8Y6!rXKYb9Cx(Ej~aRHjg4(Q@rf0-UE^*m!5zRXH#@#q)bI3+BeK!;=| zILitx{{T?*6M;Cs8Ie%w^%dd77M#19Sb4{LitueJ;&!`Ku87w|Pk~@R4C)CTH1(O4 zHl@Wt$jM&Gw&HnbG+V&X_16Ni#hORmXNOhE z7|ReqqHA%ZL7xFg-IObOI#iIke!yh&hLxnWU&?P^oDT%_9t;SRGz`E0 z45e{;(%Dc{rG${{#!y9rmO>q^PY~Soi2==AeVLdCUy1YE0&}e<@BA9VSrUVha(1HiL#E6WHdr9_!2xSO zMQaa}^Y5HRKP`9Nsm)7g?mB{RyBHU_P%Emf4nHA>KGbxfRh6_U^Qt1XLN%i844qYc z#X^k$bbe!m;B6QgINosuUa5<42yGdPUnBBha8l+q@3T@*iVO@q?0_SMC>5OVao+pH ziPteVjH5ut+lzu&SC+jn(k(gd0}mn@NxLp2ISVm#S0qAqhH2WMrn&a@;s&Gs4J`bL zcbJmukP1&gaCQP*OmvpScaWcs)S#5^*A0q=(hL#R#xU|<81}D+BhOj}M?Uk?N=-`= zfhKLvMk{XcK-88S{xU5O4TEF#KID<*(tTUJ$x!3+)g+kC33rU56t(0KXo|ylH=S_) z@Cy$h)R)VqSX%M0RLU08in$IW42+@L!23shz?C>L`;~(L#38Oz`*`QagUyGH&snfB z?;rn|eevwRczFM~1pL$Bt6rrKr|-1!vC$qeoK}ZABfp)Zq9q}kNSnt| zQq&+)Kp&01MqD@PuUbyHyvEQQwAiDJxhCw}t##6{V6dfX*8R+p!s{DDuoh4ohnu7cwTz?O33SP8ntBn3zoi!%>nkIDRCog3jTf<@dZU6K3_Y2Fw)=!Heo#` zBv0TD?c9*+t9$(;dcGsfXlA(#vgW*-PR_Xl&cZm-b)JFq(-!ykpy*Y(&aO9*egjfX zW@dKgYZse6`ZWoW+OR3QcRzwNJ}HOX|r-=@#tq{Yl1x|lC) zZX27|iKfv1a)9o3ES$bGb1ldBAQDvPajVXu&uFNhj!6B)NX=NIE<#r5>mbu>jRlX; z4_mCX5HfI@_Z?SHYG7(gVo%l*GQ*;NZs`0bJZCN&VMiw+{r%m~n&BZ8mn9$c?io+@ zm9K<1nR+XPnM91cEjTZ0Z+%TOS$jY$UCLMvQ}kcrh4KmXDd_3`U-Z=YNgyFq5vNBz zJpM@0=o`};HiQ<)!BT`D4z{3H12URZ@jv0`4T@ejO8H94$Qi#l+QaTRKu>E5H3jv2 zs{j#VMc~-~v{wvx&<0af5s6bUd~fZ~T)P{LP_Tqk(x9y*X+)V;zkKqemXRtoj!sM5XChuks=rPXOlUR zT&ZX`5VqUeYlAwGbQ&FjQaJ%9v0M}|sGehLzM(!Z2t2ymoWQ7LDtID@#P!TKeM*TJ z))ivDAD|@lEYSCZZIi&R zlYTf@G&}sgQq(tt>WDe!e?!V;IC1Tv-dhS>nAFz_rAak28$aEx&&4Va1>q_2+VbDNhq_xMv6T8vK!@J-9a-JP8TT z5AUiVF=E$6$d|)>J+tTKGIcbsxL@aWG{Mt!6j-O-8&IC!@i961t~ESwXjdtkzGg2i zmKS)C=g(o@6pE3;w?Q>kqf&UQcC`KT|1d!aScXPUCA4w0>pSp2$to1p$PBJIfz zooIG_9V`fDAme0>505aL?_;O_-=4)RcY|EiJu}hTd*Bco^=<@H^(d%JtLUz};S*_J z`QC@x%|3&i<0rY6}N6XLUBI-!W`oE$BDAoMnXX$31fP%|>&|KW!cn zU})*VuPusr%5{XwHE+Vte#9DG&=WhUVvKnU(wM#TAA~yh%}a2 zDf2d^96Iq^7&)6^7InV%#*UFWzFhML>M8Z<{1|5Oc!ZY?b0!H-_A&dWP{yad(Sb(q zmRaC&F-UkwCf|u9j;~)nWH^Lv8Ixi)@}NF)h?yBV?M=>|V~b5)$#Ww6|0tn%=XrSx z2aw_0{}d(`BM>FCJm!4Zhqc^Rk%AEMos>8IPemZGQWTr_e-)e0OF3;{=rqoyuiFpZ z*Nd|jGyb~c4Ly<)&*_Z#RaHMlxF2v9RV6*#MpmV^$FCW!bJs54gsgW47OS*pR*j)} zTq9xiLevH_>C0UO-rFf7;K}(`Jv=&@;_q=Hltt(kS~hVF=S8D}NmLkCmV%A?>OAAV zB8{t5zVehe=wyhexm5^xTHxA#J2D?pf#cdOwMEBdk!Ee^6$Fi~N0w6)Ay~J0*rVk@ zuoCfTw0TnqMoQo8%OP0~U{eVG_jvMcnB{=MMrdy%GqzwNF?3t(oyfc>@z@3KW<4n} zE5YB&QZzrMBqE}QZ2O1RVZX~%9@ZzbAchikjZ6FJExVq_J3@5bLnzTzWSw~O3X8p{ ztHhPNlTG3^d=N?@L(mpc0z-Rr3PSr-#fa`-y{2JAz#odOJ@u-UB;T0wlr~-yB5O}) zWNgg97rsbnI1UBZn2Oq3@yB4A)_xzuvWT?)iwdv01a#oX906l_HWlpcKgO>D@ZLxm z!C2=IWX9G4$jtNkRDEy)dUx>}_Ka*oCiN#8=qo1K-~o>*^#D5;Rf$ZT>p(HGi<}Gk$CC+ws^!jJ=^ zeI9fnR(~XI;OzB^7na=<4U8;ED?7WP5YASXJuW7m^Jg&fozL>38VMtV>94JpJ$8uK zmER%TOoH{l;KFAY`f=~!Q>Ikb;)$dkO%437jE5u~ui!Cm8i+4PKA%YxCPFIAf~fBd zc{o$b<$GcR86)E{vF4g}-3VBwFE^JLxq9Csv{d0opruq=b*4xf%}NOQLL|~*#GZ>d zbH)xyh`?agPrlAn$96JkswGDVMQ<~#`~ig*fX8NuIV^yg#2t||{s9${3d~PEFb`a& z*5GetE{Zf%x4pF+6ToJ6{||qK)Cb>_1!){#wDH0ydH;i-gIK2wQXy9ufMr_jSu~_( zyj_+;q)^*7#X9ATsP~9Ye(JVUZFh?x#%Y_E61B)ll+>g2Naut3kN9rz$@@uRmyY1Q zH|UGPP#<)deJLtq)pRNGg$hP`=7GeuF4RA+Gh*Z8uLfHI2EKQWQ7lXU4g^4xNLj~q zu=AG@dIO(fEs0kTl6LWRlV_gY@w>Gtu!`W@cooXz&jfm6t>=WvARS@iHZ8z_c254X ziT5hSc8!e!Y@v!1H!1K+XyEyd3nNTuX7NNaGW~ZR81fx$%vgXd<=FPzT&8$#UeUfh z6ne><5d~ZtZt}6 z_2+~LQro_!0m(xPUKrH!i`$8ni7S3(Ri`gEwj5?czZ#w(#7!!K>;w{<526yIbA5 zXDp<r&?T29u*g4oP6Z$n9%fgTPL720tp<3sqOSwBN_c?BJ<6P= zVM}=0{0IR7ZV1jf9-LO3_Re5SQQNsf@=7BTU{Mh{7;0IwWS7tGw&P>Ia|-D4*#|*wByT1^j-5IpxRAU2~&hij8D`wo8f=Nwfk(Rr761 z52JD;!e5ye#;UoqWN5C!FAsPGk*cz+m+NxEoD8Ay^gh! zTdyKb7|*6 zLOp}EBd?dO*6*!Tmh+h~x|w81F?bHDwtc|pG!dNu;I-T&JIv20iY*O?;vGx+Xy})w8Q~+X?^d!}hW*tH8N~*jO(7oW>cQGa%(JQU36y-*Vu_^S5F|crZ_>3Kt(_T35;K%^G9|_T_C*^q~G_OF*?8 zQdSfVkqRMojxbaBf6L3n>IYaf0FFHpv*O8#k(K@|>j96L%`@L7SG+#E-+beY{llZJ z^dwP4q^)=w7QDq)hPzpEr02lL7S-eJN;g3WuR4&K>mGI1Px&8l1RvZ&jZ$pBC z01e)FLZ8xi=0qIrNH2NjXRDLIChsaj)R?iusK9tf-m|$vkfHh^$>)3c0wkT`u}rPm zt%UdIW>tzr_>xuopID08XtSUv zGldZwt9`3_Yqww7ec?ly9UO;RM_kKthJIo~rRN&X!+UVA{>wmy-!F z;NjiiakyKfFRpg!7)ILH>^(a-Jo>5RV1Whn0Cx zDvRr}I_xEAXP%v;IF_u6%{EIePwUxnA97wd**rwnMIRa#t;cw=iv)MvKt=+_*6I7) z-Y^dj9Le@7IsJW|zbWMW7n$oLhVolv;sN@=c;J+~Alfj|4i+5?hp+}ypg`S;%vT>A zaW$SGtnhNeFD);7axs5HIJ~^ddw?$~x;4d( zLy`7V73AM}1XxYwM@{{{K|09eYe6lm`P(F5^p41NH5%!ZHzn30aN?=ZuUV0C;D>SF zErlj8)oFLqvx*5JFDFC6j_+~R;+7#{MbdDf0ZqwGKI*fVXCdL8{ zfTSpC(#S?sf;O<2TeSFs4DgbyKthAwXJq-^9@?+lHfxPGzKi32iq+o?k6mVe8X*0s zg5odo@;6XB5 z^8}1Bj2`JZ@TC4tRNQ`a9i5F_0_aEEZkOgf0fuZ+60Pp|qs0BQJ^EbmT09E6QwmRG zoR{HUv;ZnGA9006R9)Y&vGa{#uh|;hzhcK5l+`}=xW0zJ!d=;vA{7ijDJW+)) z#`*%w@d>eW(O-WPj4o6j)4(nnZy)%LJ}NXOshEf(& zz~17*Urcx}wIe6VK&?)`7(zq4BZL@f* z{Ugsa`P&GsLl8xDzw-l$(vx_CUCkjzERO_Sp$ADQWCjj~s__1G*Q*M_oaP7F8&08x znTP-#qaP;b=n!BRdq`u)ROi75*Z%PWBtLQ;gNTpp!8v!l=E-=*<9)}T0YxnYG0G0#`4z7`mdg3hD~{xF1?#V_ulyfvX{4)H zI`lZ@9yF&T>3bgW!`J&#FQJ87QFIrhFQdx&5B?NxneYs}@KzC<&XL=<9IK3U{NcgX@lsIGq% zteS{m4)EP&5-*=im!z)WnV!6`Q^EsVtdxt0XGamdx^&03=yC<%0PpS8?Rs=Om>(D# zgdT%P(obQNC1iZG$vkLKDX15QT+J%)qg%u=eqs1Cz& ztq`Oing59br)I~&(zET{D@PoQ!~fSZ1QG3K{zrdtVWD_fZVnl@t_glb4nD=k$$0U> zm*;EJkXjhZN>4>v;-Xd)lD*>KvidozGm4`3u3jIS@r(2`8a@?4+eHqh;HpzJBDyh7 zu(6}BLwI#S`Td)M^nSTAI2IFCvq3UW@YjZpH%uvR?#@EwTmn5wO^ZwN+Yg<}f=F&{ z4huy)c0*0*0(RJ#&?H}c=k71JI=KKbxY(ZHb(ZGLovdjzK%ZuOx4?JaypA1=A2Tpk zrQCdMKVO>^eg&pi(~y(LG~v*xZqEYZCORYBXc=UG$7!qX?VGF+}3s!)5;sU}l{10E%CdWyv+Cd6j-y(F!7;9NJw zw0j;|$)>`;8{u$2iG>P5Dx`gY{u$A&xO)y>QMVa;PX#>C;m#AfMNDy3*fF)IH9-MT z84p&UyA!knT0YTJ@+h!kVWi4zqG2KGK9u9SGYriE`M0#NEu!0M%>Ba>$D=TbA950x z6Rgw*`V;RF^wopVtkl;K zJ7hK$x{+hq9QMCp02c!*Xa|aF(Yr7Hf)%vU^Gh}ru?A}M{LPI5VcCdrrCl*1Q-qxc zEyI#>_b!j>#R|7tF1QgMJ6LZ>CaO zg}ID4FT4WwOH2pu`Ub_qY+H@_Or$9`r4LOq;{(5kb5FtWnApp=7Lz9_ z4;_g9<=j_{vzJF(TW6We?hto&ixy@0#q?Q8+@6o619~v~hHEkVZFmTtP+B#jzmOj7 z>`6v86EIR;d+E)gkb|8baT8`gIt|}K!rXEN8MHR~{f_NYxdpw8{(gROR=TX7*&RzQ z*d@G%f5Bt9r_$oH#taWS15sn+dmNV?ccXVNbd~YBe?_(bQ>g)UV_$ju-3%7ETZ$^=H`*u8TlNf-6tb^Pwq>j?mxAW z7t^q+{{CElu5Cs>cl&=4KT9`2E+;pO8192ENJchx;cWyY&pfx8P`zabC}fjjXK9EP z*pF0~5ci4K(c@S99M-NpIRQ`8=|XLtz;#n(1Sm@;E~XWC`phxjxmtSql#lUkf8=kz zW{y9--mUJNm$~D0{HGFRU;`4VF#nuH-ILrSt;mm2fb6i;AwK|xz}dE{GCqkkaz1@z z(!7sFF89eHZzooCAFP`J?5ePeaNj1SZ((_+-0La^-t_d6dGFP$YnKATq!qQig{e}VEf zEVzG>2{!Z1Z>!MfqYO4UgnhkLE%=ezo5hSYdAMRZaYJ5K&aHszOq<^5S?WX>^8TB@ z3bBa8`2If*?zKlE?QJ!Btd#m>4?XsE3pRM_& zh&=C5I$za-Gh|HVV2C~c?TV)^Z`2TQ>`5p7#P?Km0M8(+NFbJ74}ZhsaHX{tm()$M zpeSN0rm<~SRov%3xrGD9ZB>-S8MWo-l6+#^;)w_k`_fw4(qM!SUH*4psmerxA7iwK zQfuE>-nu0#LKm|?8GP5e5n>k1UHI(+5m@9`Hdd(3E-G{rfwcxdNW7Ug5# z(7E{XQHKUZ&<-ie$&~ER#{-JeM_cS*WL#Ddn+|K6g|g|^Pd>w>Sw7%X7IDvRKE?F3 zSw|(;5Ih%O6J((#hA;M*H~>jL(tNO%#2$I^=@YxYZ&zDDUxrZsjsg10l$yJGhy+m- z#SA#T7&#T6ePT6I-nlj7vP3_UwecqS^e>TuWqLIkP^cA3cT zkBQ*05w70#M1m4l?yr~ggeKItM}z7Tr&cD?>A&`DF1mBpN9pw4Hy$kuu87!59xms< z4#E$gr~AoQK9zBDR`oSlG)03Ex);}af0s9nbW7B!C=#9e_Y7C2D4m0HN4_T$??(1M zY&SccHs8ZEbMet`qy9}o>}KC)(Tz@|b^KEi@Zuz?5`let8Mb$&8I6WE6x^fjXuK;j z+?Fml>$)WyK8D94M6thIKcnBF#MO_4(tzR?PE@q1zki^wbJfGVV_L2n3I$;{s+~co zLGBtd(C+~iLAfVRTP>@804f10pEQ4QlYmeMuB^1u1cnnc`>&jack_m>MWL+yyIgb4 z1kKs^N-iF6l$!U#UwuZ0-5-YiUc%tVL>j~7(Qth^3HQ+u?fz1*`#zYTfT2JcCDDW} z&U#(q_n6br_9&TXSA1B43lj^-2{;({%jqo`OgduGg002ro};AqK(BcFmGFTBlYw57 z9>q(cWK5nh6e-K?jbm;>cmoEtP0QV(g`;#fxI;U@cm0I^lf!N{v)Mu6q30cB%!~Is zXTkCG*rM|idfs+~szv?(<}R`+Bj)|D3nuD=M${stZ62kE>8P*Eb|dl6PW#6L(_ljT z>3P(4>MF@8RJd!x505{a3+f~&#|>&gM!PHd_BXMZu2#n^A*Wi*4MDs(sYx0p?RZFT)l&DEt<&LBc}W@{dQILEJdQ90k|t$4cE&0E3C*MdWc z#5dJ0pX{U0Ar)fuKX;5#I_#C|q`j&To4P2F8w&CKOMy`<=sG3BU>1KE2D8b0D7#O% zMj-jK6@N@N%pXQnn_?nOe+f@hyk&Rf<#LGC3CN{(-Key%Z#IF~6H>&zI-izf?j55GRQzP3mLeYqDU@lh4_WeR!r&+nlW z3m3y!du=>syI91;i(DZR1Gk4?LdJTF?=i}LY9IxVsoAg}ouel`$LbIJyt?b^(E35M z3gHuvftqA+Cf2i6Sc{;oJi?0M={lv_A+XD?)^x$03RQW3U(V?4%z=srES8YkwH%E^ z`3h9TUi$xuH8beXaIu2P%Fb=eOzevM5FB}YGOcc$^*~p~k!X{?SKbxxD#^xvdc z-$rJows3dEFS=4krMCJKY4t#4Ij2|*_no zn#yAzG}lkA-Oax|O;p;*g%Sd-_=e!9LZenPA}w)Z()Eh>iGF z)3+cl`(N=OilZMI=0+k4PBQ(rf^$-ADlhT_lCuYHT7zjOLL$200o@f6ag^dhPUwkE zpQX%f*=ZCb@=Q~lDX3sM{r%&Lsj6Xb~QomCykSj5P z792TL3>At$QY#NkgN2BE`0zjZOb2vz@9p+Cw|-shySu;D(uTjpYDd-p@zL?GqKJhU zhkLsuRB#_(uO~#{Km}$WZToe#p~A{NuFb_1f9*@8I`QtoeRD`(Cu@Sm!}n{jvtXEC zaRLRex%Nox>V_#!2DF}VP=JpX-2uNIDIMv93SeT}KhhMAHXquosT4#94$JBrGN%!T znD)B0Cj{mUD0N?LO<6G86zG?6@-Fc>?z$1OR|0wJ|0K23A+XgQE4BN3PmaBSKT=KuJYbRESs{V(#$g28iHgcC z_ucbTX*lQ59E$+jaHSsuk#W)vPm0(Bw{0|SQ}W5kNq9!v=hO5}Mt)T8VGX`ap2GClXm@c*Jb!-qIT{s27n9n2&TQ-?i-jM|*tXlTt@P9m zw$MOHh~4J$y`C&da~Yh#l&xp_J1i>E#3xA2NIjtT@bu2i#BCi?B^6Zpys7CB?0_LY zkxqKtYfw^WA%g{Vb(RMs9dpKs-_jHui3+!mpL%nR%XJ}L(nxD$zyW>xbE@$A+jSGoR-W5Amd>YURuwfwZ2#~a{%VxBWjmt&leWUvADjEgan z>Y;PNo<;LMHo1vD;T^0XSJiGWC4Domhi`~PW@;Gc_V=WMYQG2EGA=o-^{WoI6Af%b zC~O+t*}5AJQ&VBs>cjZ2@GSuJBfGZC#jfO9LTk}g_=b_x7|mk;><9-}(hxOwaT@r5 z?taq#Z?7ClVmu#Our_4FEOgtsEPtoj5(!(eBCPJeI#r>{uZR%-sg!$eeAIH!jWxs| z|F6iCa^aL|j{wl{{Ik>Y$D3^s#%z~TmpCldDDZ0BLYB%&0^71r)8y~%>{VBybiMwQ zYUKmF;kuj80%7}okY{a%=|K$uo38%cpr&8U#csS#G=`-9>*^#DuO%IP_af?jbT~WN z)l&@jy4YY@CuMY~`0zqW8ZjlZoG-C|A450jy_X?=JI?$TI~*@@O~_d@0u{8A<;OAL zC7~O_?KZ8M$J=S(6Vv{cD@dC9<^rg;j;UUWJ$~1Tc`vSd_wwamAunJ=OFfv&$S;UA z!Y{MVFf}b??4CcAO^@un?|!#q$#unNRxNdGtwhNc8R2RLV+-Z!H=3U*QaK==8Kh7l zkp-d;@z+#_leF{Z8S|_=1_!_7Jld9k0+$?-!2^$z_}61|UtSpS0YWHr9i;|5 z@!4)$2ztGXp;e^2i3(y202tAN44RMFBlceWj(={qx%WOLy+W=QPDS=f>fyPIlJ-2s z6GKvZa?XA4t|rjJawG;-NtbNgcaE_iTkZ0u$pg)$`tG@eD8`(nnz`%#>ODp&#u`ko zrQ9z$jI?NW>?HU-dbEA%9}=Z?aBOIEBf-Rb_F_bRcI2~7K@7*`Mk?&DyFZZyeDT7{ z{WW)9b3d2KT=+x5QK2j1P7N9_sdkHmOn=DCnFd{=h|?4l~ITzc6Cum9lyM z=hyUyBk;8oYa?DKBZ)NUR*vpCF7dgO68tFM4B7l16AGKLvt&<>)?07W5FMHaW}V?m zH*YQMdOs$v4~Fi4W)b94Rf-A6sx0Dw6F^@C`bfaQn~%HTraBCtu=L zwmGu4OC+;`&CZ9*Tc*&Ob1SiYtj)gh$g;T$_S3cqub|cLyBCXxpQ7wMXck!giNIT< z&psD&ZqEb0{3LxVFEg_t-kjHJQT1N$;m5f66e2xRsBOwSs~?`Lh=r_gy8EqOeIJ?EK$T0%B`+XsQj1ea7`P|_jPY>LjJJ8iI&!s^Z@x6 z+ldHH90$Qs4nf2s*@c+N#vGFSi0PeYP9v0$J>@=|3mXr-H!u0giN{s%(ESCG76(zI zEVz80#EPhpxUL-_$F&f4M}5k>3TqX3>I}snT~%SOC7Ugux1L%`vSFUaX{N=&q4Q=l z6I%vTlT&Wx6W&L;djcfri;=Zj?xo#&?9&w=^i5T9 z!nKh*ME3VPoW=Vq2|VQ8u>Mwa~~g6nMK_e0WfZ$oG&F&0)h z?ny#0vRwV}bDPItAG@OBW>oy03Cgq@ zpgK?W1LlEI#KcN5mQ7oi&WASBmk;IBR5I3Q?r^=UMT1B+e``sXHlfT?;8(_jtj(#d zAK#uZgcFDmJ^`LzSy^jCb>5pFwf=P4_4{N3H;82FJ(BD52Y;q^u#Nltpr(f0T<9i` z8*R;GZG9NcQvO9kK@fx8CgyMc5&bokJXbn)X*}8W-+!L)&^l|m^}%}U+m=plJ=3l( zDxMY{>bOgRTW{50vWWnpL4}rKHH-c-kj>}8R^ID!F0AM0>j@f=$}kwe$Nt%E9jxS~ z`oW?H6A{=LI^EZ#6WT?2;=psrhnej-@V{Z57$f7Xk@{sAXP~@1lje9eChC&bks+wF3n&7Z6*gKMs01i7+Nit&|*-Ld@Fr(Dy+6=3;o%`K7_&{Le ziQDhg=?)ifGZQ&Xgf@i!$HJJ~>C>mpjK7;azjRuRDZy`!#I4EWYe%j1@Q<|Rne?li zD~FlB{cb%iAT~8Zz5k-(L=~;rs)>x^mRjtespKt^eCYY6Z)I2Z@z~^`+I+74UWg9` z1Y(XYFn+RpHi2MqG*ecX@t%`)=5^kZ{0dYn)Psw7eEb)kU)qJg=+Qus@5s%Ibsi~F zGo&ZY!A`km!8zSk^94raQ-ZIK``OR18x;M%^)8xrC@N3xlx?`N^*~Ho-y-sg|r^; zl<#|9oHa>9fkxp+S;FkjyAqbtn;+4ZVE z=JY*MyM@XTNgL97&Blje&Z^UWnb&O(q^mRxzN@kttJq^n+Id22!{M%~>*u3g5iht} z*(|9TobpKrL!0f`ng$04wT(WFD&Io1dksb3iAojOl`p2DrWvmfk9n~Lf6ZF&JVEWa z(Y<;7M#AE)omSc5;XlXf_LbG|>1Z-4(cbHp?ZZ%3Q>FA8)LcmM9$4lT{<@KRdi}nO z{~~7*!+OfQeD4TRD%2UQugyqG5l)18CXJ|YJwZY2D=%8`GA#a`{_IgaMQ0&~IPU|% zi*o)f z9b3g``H0FyOemfjW!P&mnP@Eo0xCYL?9&*t^M|(FKNwd27jF)0V>uVp5#4gEq zZ(|XSg&d7F22!J!|l`M@LkA`kiq8{&M3(d=Q8p^cNdE3q1l z2eOop(K-yA!vPXL;lMzPu`<6eQ4{ycc@9^M;0RPG7XLCZMRrP2``*5SMwZ-O>zCvC z&v{_=LBfYw9R0;G|01(o0tEu@-M>e-WS6k-n7H{yX40V*9x4AbkzKbru%8U^-42cR z_2>3uxs1qDl=97;Hpl^`Wz`>UdaG_0^{8So+hBkFE$jTRboX0f8?XYe1~=bUxo7=G zM2UZ}AGC>1-p0dY#*l;=A&`VyqaWik_HDU&RVYHTZ(Q#TU-z^Uo65V6qEw6nbG5G8 zG~#YsYuA4!%kE$HQdyh2WP}KQGe@aI#bkyFGaG-%S659hbG>=yE^7M1V&2wcBD&Pb zVRwxC@4;!|Sh%j@_Jg^?4QZI>b>d{C87#ZjH|je6Y#h3krcoV~&T!8@(&SqYRrL7s z#brH7IdttJ_80MLN)Z^ci^fg zxdEC8*B+XaNFgfR<$R}-HxO#P_EBRt#NoZ!83vA+W4`|P-2M$Z#D4})UTk`zyl-z< z?9X?^P@d|w-UFP&)EpIm>SjwiWeJk=RfS05alMC{Pm4Rb8?F(Rx$ya?&qB}J;K(L?nV+z(yZN>Ev#oN`(#Yi;RiqO$J5md!Rn@jDfUO4#t~$Et(j)6g z$un$CE_W5lF0%o~w@r0$LFl_f;UD~Y1Kq@fF2G(WElYfNQm=MUWEmtTyDl=eD`RxI)WUk$(z=i(e~(JJjI&D+N;3{t-}BjVV6C=CST^DXk;K}0f}836gmYSD=-&u z?=Mu!f^#@vrSazqaX&`1(s`x$fQa4BA-LM`e1^;R$YV9k@;BpW-g*2~#@uEz>%_8b z>ik|zy)wU=)D{YxW*Xq3*<&q?+>r85X(tkIU^ILPujfXETo&e1-+)&&*ox=Zd{&6t zJ&u&@_w%9Qz7~mBj;XnyCQ{tS12<3J+iBGG-SzkI`1iAO!|hAS{j&1Tdnc&tCA;ux zv@f^18_RjGxsGQ&IxRMznW_QiZP$F#>ZO%GgLNYthW#%ubS}ysNr4j~MmZ7g6KfB$ z9Bp{Avp44*Hz(`%8ICC)8l3c0p5h6}v8Fzo@bre`<6Eu>XslvZ6)elA;{RWj> z&32)E(_7KQHXgI zJ_sfI3Hwl;Qm)P?OlpxTysGHioc2^VBZ6Hz?EBowOBKY&&!06d^c)N$NjgI+6qzo(4+Eyx!C!g|8EoM@x-c?)yG048 zw2DffZmS(HEb*~Rl6$}@g@=o(cNq4jM@)#<_G*Vz!HQwz+4;4~H4kQYrkG(XpHH0V zQEW`GuD{t!JB~#Vr6yESbRVnek&1J&-|vx{h<0b24j-q&>u5Y6L*zch{kwO*frG$L z$6TB(vfJS@Xwzqlg-(SRw&Kbo#Mv>daK~qo{BC0qc_i;r&GHCZ%A)8ezo{u4Adq5Q z*+bHPZFoSphzv(q&X>fXQq^fZ}HZvIRFg~28_ zHZ~H{2S=c?8=aAy&n!f8e729cb!m#7EW?}SLlt^^O{NX`i1$>`wDW06g|_tcB?^$PRAXaY-4Q~1KK zvjOsOCn->*t*6vt=RJ?(*7}U`ZTs)Sg$DWBA;76X{dv@%x2~By?{gWea6~>OI*-__ z2ULSAQehsue2jIH5XZz)yvx}txrs+yj6`u_HYkrtvDWJvQ%1zf&UBLh0j!e|G4RGw z6znE+2zdA-ytc0V*z<~G3xXI$R@JF29(d$MF)t6@9;{0&|5abzoud2ZHrzA#&jVM+D!JmI zy))@$-RhOP80Qt}Xu>Gt0O_3gKHpHQUYz$D$@B7?0EQSB+JtJ#;~a-+weD)5(&Bf8h0Dh7tc=2 zZ}4nnjc1AH{OlaxsEMw59tyuxEwJP$^(C#v)kT9WHKy>jhx93LpE=8zhFOfOEBSuY z+nPQ70FIHvMt@UjE_<5nui{6v6`y#&e|?WvomJVXzGApOt3(s=uJwSWS;V94Kbv>`=^}L>U)L+B7 zjdH~hlo9UVojsJuhW1q)ze^OM>B+Kau*uhBKlHr(ntzKeYvX(Q`In}V&M6W@b9kJ{ z^V!Ow@CK*IP`ya?X^Q6BC4*t$8Y*Xv*z^(jq4EG2Zi zt^iBhk=V^~KQhkUgZu-TMC;94^lX;jvudU3Y>n<_;EOD$Jq>I>)2(iiP4N;Z=Q&Zg z3G-GAe0!`&4CqdygQz&_VcEuquyPBhrN9*F42CnZmQht|c3uFwDcR(ojk{PA)~d9J zql*S7W!EjLv540S`)Is_O{hmOX5PFreoJGf5Q& zgjR+cIdI)Wd}dPb-Q^w|c138RloO8Q@HcCG^45V-a^nrCAx)gC&UyId0*=5RolYZ> zV)O-I{tE36Zm7F>((6W><#&i&d5fT3}52E3zA>SSeY$L{1?u@!GRl&wb!v1<*2(bl-@pkQ_` z@bj`?>HOV?@ghDdI4Jwgvs<4DTd!U>GN=7+S+Q;8f6%& zd#T~zldqmi!@6JBbzV7KZY#9ickb2ayl1OTu}D zdx8X9v0#jD+2gK0N00m*39+feu%9SRUVuMnL8<{CtZlndJGHr?8zhWBQ+dLEjpj2|><BVn>HiLatA~VJgiTdH5KMn65YQnR42+>H=|&4=z11ah$LnVY*w(^Y{z$ zy-Rgq&yRm>w;zsk;Xn|*IxLdZvuz;%nOwIl~H#c&a7HNhA zc#X!45+-rR-UbRvT<8EZG++t{)=I*|jqompRuz6u?5V){akh(i1v>ckW7VK9v#emW z<@AT>`uvdVd!q}?Z%nM|ye$>;zM3jzoDo-Y(oT1^fYQ zz2*h4@1S_P%E!Y^o2P=!a z#^%KzJJSTsmKM4mOkPa5AcNek^94MDQTH`>0is8;lQcInh)SJ1OBMe3nrxA&(;5OQ z$$Di@KfLD}b&IHW+%%1l?vmYyY<%n^gwf-ABDx7b%p(x>@%O1mpP@;h`~r;GvNFNK zG}0j+3=CNPE{RHRm^6%bU7Hc>NNY!cc+tyZSx(dzlMv#h4}UEr?R@ROXV<{qm7n^p0Zc0-S`l( zxM~eW73Z8X&+4C7EF1~*>R(z^)b=dWElILqG`?%KJ0lm;F^L}q=$e-d;a-|VIAdjr zZ?aL)3Tx;uuq%Xwq{Y~7_>+f*?SdmhK|jTYir{OCd zRt6~gk#4=;nfPUsS;(A>&@Pd#{~<1omp21MB@6zMa*nv9Ej6p7>}f;h_;ka9iA&2j z3UgN<7SwtZkC&AIAmXGJlq6~ek(6fEQ)?C{2A-(b_F>-Ml7&g4>5xe-2>11d!^xg< z81cg1adEie+i-e|jghD(iD}!7pEOTRQt+FB?!_|rZ`%#9SZWe+3LN5xawLwIK09Yh+#8?-z9S4 za2%>9;y!vyu2~fX@_74FD3ryusK`qiTi2$!I|Q1}#|SG);(+sp+?|2h6tEmuKv_McW0OGoH7(HJ^jE2}a$o78zXchE|g_-H3K(h@Ly=gmr1cqE;3 z49T4&g8-8V%4+KD%BCX>$x=7>dfuS-@%ga)y5~7NaXOi+yLILESD%j#gVHI;`!c~w zP_0h0*h=vCX8N($I#fF)qWYivO41t(@(f!-kw0KLmEV?@GEAD0ArXFe`&mh^Dvj-t z@eBzKuNE%d@@JDkMKD1ChF8pu@=GEF*46TDBfcQ)JMXk-6X8@5Du}W~)YN>tnX@%d z*qf#SgPW0pO^!wZ-FR*eVxSj|#6)b9k@ustv68AIFUibfDu?Ly&(!)57W9b_zyA@E z-&6j9-R0L_OhRHcbx(F}>4ckekAOw#{=`esD6yBXvpdL*^iAC*$0t}pZ&Nq0Pr%9h zqqbtu9i=T@W!|=g{QLsLA5{#V>>9LtDNxj!=6ECbvHzQ!RWIH))?EIRy8loq<)pvG zc?$DE>`{Z*8)&pJIQ%G3cn(96JK51ryx(vPWLKTJ ze1bq{0a%+Pn!Mw?64oSw8B3=%W8%P`PNl`13EE?n|27zW=57lbdYEl_gi}VKO}Xt5 z(0s&Aa9UEDk%HP7^+bV=+3c;^ZF2!8v}ZShvt1?w1!ZeDc`6pvW8g(gt%mN<4Z)cd z0B+^rjk+}HP8NaNoy6e6;G9MFA_Yo5^c>EGKk2$r<~VxRix6SG2JrTSo8P<8f6|Cq zM)hY+M!5Fy0)rStoTPROpJ8u=RhyFM7%(WLO}8Do9CSX65>HS%OCT~P&kfy2$8>H z%m|yL>0LHdULAQshO4vFGY6Pc#awp4w%Q!J`I*wX35alJp*Im!P?g-(*~jCI`t1c? zJ!s2meGW`+F#OTQ5S1bsC{P5R9M_r&mHE`&^`9e$j97bU7l#K^dW?f3@qbzweQcV1 z9*!d|Uar^()$aAYKJ<<}kWtPb;Rou|}OgOcOtn%b{f00~gGC zCf4icR0$=wP7c=+4nXt@KzvCOjmYn@*P~w`+nZt zD4f*dEGaMxq#9#d*ZUGy9u&h9s+v;nmony`=z3 z4_Oj%ZfeebI#Ph*ERNpD$hD zp`{rKEFM}@1Up|3gUuL#W8vAF2lh1$%eQ?}P$7XwXS!{5>?;6o0=0-n{m|)9T$6u~ zi6jMQ?DNwL1|gA%vIok#RK7BR7ctqfKO2pi-{5)zRM`Jm6O$;{ltlDH#%7p1H*#l|DXhd__s3!^U3V+^wggi+z}_@hCoTfa2$UNF=`=Nd1Yt z9Od-Uy1c$%=;LwE8pCJPi>}khPY4(ZWfoDChaOP`C^Kmw2`60GE~ zY}nz%b0fU{g-<_gjU+9etV99Zm_~{}(s|xn8b5KtSo+U7>-E1b_lA%N5QyGB-Xzrl z=lpIFJR5{@CYGRqTJ*a9QG&lD$(Ep!CKfIKwYLPl!uC~h$cHyAIzaLN+xfao z`ul^X=3S>E$h7=(f1|y`$%x7RZJVr}Mwrg=DSPB3r3sp67z35C8Oy0Oo}+e|c%1WN z11Dg%<|J^TyCW{~kkdv01m+~dBj{*15F1+d2&)uv7Sf&7SItPQzrR>{*j35HR zrYLw$ejUS$xS0jEFYf-RCw3Ih<)8;JyHe?+HoV*6(8yZX-#a4zIL>Y{^*5DF)xg+d6R8 zg?RJdZw=pe(LA^P5K{Il22kG$!~tJAJw_Et?a#)rn6L0|hI{AVh_~I6Y*QU^Q*DI3 z^+fR7gSQ0b`=n3m&w%USzo;R5L^$~6}FV* z8cq`6_*;S#3v~d0^9+!R{u!YxyRQ*i{7|LDWqw;=?w%lr!qi}`-RNiKN+!+vA*&{17EPl;;msi=LAM(w7s70)kBM2fwEYA#``8ZGqW7b_Cu}h? zE~}Fiulk_sdxUoKFP)*Tq#*O0T!#DO+amK~4R}qzR^Qy5KdLWY6kilO<#FyaYMCV< zO_+ap#4gF;W+k4k80BD9e~g z^Fo42A3D@Yv};U%0T1i|=)e9gGyumP9+GJui*mUC`T3WJHx&3eq^oRrD3+M~`t3*_zdAPd zGZ!_FO}{6r7Ao>b;VOsJeo7TIBt#{!!>lg0igqLevmK@0U`~X>sQR0^%R0ZaR1gbb zI&B%7MgCwBhAlt@|H>M@n*QQ~1~M*m%N=4_f6o@=C?H4kI*QRw^3=;3n@OvDDEzN2=7>EAjYaq~%R)N#y z(%kw9V(n8KLFW!u%IAlAmL5Mg+gI#&YtcDI4;`7U5pvI+4Qv(xgU%g(M(JXk`R=qH z6>*X4eI{R6Utv}xqk86pOkZ+sJ16yfSqXvnfZ7-XBS{k1a94e+!#UGy_n(tDMf*nz zemtMzL}#+MhuN3pl@J|Yh*v@Vn)KQ1%k{vsMDqGe{KmF;pTkM6&k0O{yko#o#83u| zntkMx@PAVkIjjI;iBOukA9#Pr=6jgOjqB#DIrdjxnU&6Xfqp%8>0m*MJpJ@`A$hZF zVf<`aA-rX)&o-{=>oY4LaO(9(5AwwnwQUyV2EF3i(}{B9R2<;^{oo}6e4VqgUhE43 z@O75iAS&SwBy;!-NEo6jl%%6jOpy zNqPE#@IvMeo-Vx_k$j6-_(#SJ25iN=l;VZTHM~CV`1>6`8Mwl;4-VYLZh#$`|I2G) zu!u6+b@#GQ^Q8#)R>w|(gbpWkunuo@evh(kAcL&fMSNGMmlG8VeZJu6yV|3FeIaQ;sh z*u~&Tfg;4Gjt4%EhEwHi7(21V5qdHX{dcu49L3`HLS z>s4V9;-a(qPY*ON!gIzOUINz7;UR72^b9mFB-+538f2!kKTR+{`gzXc%gX4QOVz{-=yq! zlvtu#*BS~Z&S%2a95|bV_AoV)8E}nZL)LYvKmSIfyZwIGj%#a2Z9i|jWiSQ?M*)nDBV0tR5qww- zYF#_#QsR`uASv_`34g=D{lOmfs*Evk-4!4rnB_Xc%IRVmP?ti-%qTfzAeA#K=A!T( z5JJpC0Pg<$K76y-CJID$r;)g06VpUbbOGkJ0KA1%`?4&RO&L>~#xvfYrxA+%L)Wk= z)yV<)|6$4yEK?#MgL{%WBqPU5^t2iF{O_{I-=3JblpP(0Qn^fGu%}s;+$h?>h&js< zBrD%07hcGrx+d8T_KJi|n%@W0+>KPZwXK-p$boZ0zE7%eZd0^$@x`37B-7ShXs@Z5 zEmbQ)y|24&^1S=%{w5C7mihBuQ>o?Q4uU)4Cr_Ef<>1C0+696~EdE^8LQUd(=pD%n zr;I9YC#l6VRk=Crdh%mLC0odF26pk5ee2v|aHTBj8c=Pq%+Y9Y^{mGr6v({Q?V`K79|EM2(>+aWLs;Jf5F>(Yp(6_)g(NV=PK4+L9Y5Pdz^)~YUr3LmqW@8F8DGt>UHFhx4vxura+CW zkv}`9LeXQ6)d42_kM#t6T$O0B_vfY<7P`fx6TwLK+I`0Ro?c?%Mjt-UsWZYBxMWQo%bJ zAfjs|f1h|hMhbze)8@`FsfnfNfPc-V&Qti^jcO80p&j=C>aD zE$3h+t2RPkXs7J%0dePNKTMqKtKhXqKO{U}a43k!o8fuXP*z~UmL(YHy?=fX@`Q#4 z^rc>Bmh)zK={hf@P`zoG(Ce4V4fy-(UwB=>*@q8)kp5D~IjLKYg#AF26p=>UrMn$n zg6fof-vny-LRN57@Qlk04YK!TuZJ3B-}hSuKx>i?l9aijrK zv21IPg#t6A*HplEsDt3-_h=if6&oW$Z|Ee!p5$go%BBSDED`V?^fER)*uZf5^rU)9 zi)k2-%SGgqgixq#V-s38b@A~M<~#5L7P=@h+a8Y;CX;j;CL8)Z+IZ)psL#e7Mu6x` zzAhKJQlc-+Q2qP+GkF?9L}vW@dQgKsnF2Z6=NQx9RcA=;$ zS98mJ;jnAzu&na3X=L1E%}~BH48<#|uNP;+rEyWc>^O1^2k0kW3}SvV2o?BfmRoQ7svw7;;+^N%OT z^rNEqu(YdPsh5U3?w_x)=IlX|5tv1Zpj0N~8~JY8vzhL`cKaXW0IEs=5JNH7$@T1C zF$TF@oMCVAD-VG&IU?Kp`*G08d+gP^(afz8lEaFF#(J?G?sUmjxDE^R!u`3VSktCK zH=ogad@hTsOgLT{StORV=M2-XudTAVb=xDRXK>9A8Y6v01_rm2#CNu=8j`0#rV0j` zY99CY$%w!3#4t-T1V3ZO=QIoxmt9wDIMRMl1b(JUdw&KIwS=Y% z*tmaR=HWLW`hi=2zJG06>j*%b<&jd^(B9``VgAy<4H;WOY{fJrVOznt5(pF(9GN66 z*DWhJ(;-w}K!zyIF>V=&FvD{z2VWq7W&IJDv?)SCFMd6Ci>!zk2l4bwiuBI!j{KY_ z!j?&$W6-Px#)ztIe{*C-3tq$(UaPX*d^q+k93b=Y1(r=(fEQcjcN2in@ldq&W&3xx zhnK)hY$!+R#1pKk(5~}`VZN!>oX|5A@TUhmRc&-UAKaJBE+5%dqQlNWUB6f8F$N+q z&UGqWU;1sYXgX=b8GyV^a>Z^(i6&}xZ9e@lqH?3rQaq9e& z*ZciMep}|}`%Q(Hhq?)T_D=|$Q`3O}_zWm=GAr1pp(KPpnx}eAExxrsuwtcO$W58_ z+R7(|(Ixu}?P-Ih|94l@1<|3Tjix@@mHn%H9AddsVBz z`!iy&Ohpb^x!nGI(q@5^UuC=;*~m`X#451Ndn3(ttkb&u;LSsBl7u;BAF)2CdRZcn z-+KrQd{^G=c(>G8bPI`vq-{z->0L+i_s<_~#{eYus*zy@d-lI>tI-_8)g3Nkh4(N% zk|pW|sx~)XW&T-TMMa=T3yB{*w7+LZQ9xlWIfHZDimNjgP#RpVmxljI1s$UD$UU-E zM=1H4iOWT93nCo883v(2ni;2ig6=R)IR?My)$m^e>^!mrf!L^S8Hl8AzqHA2$M zk4^@Ha2-OmLCP_+&?Tb`2=R}IFF|#|1wY~2m%*rZ@z6*Q4I^7F`M&pq6+B4l7 zmGj|Ir4r)S^&6zB;|bRPK{RNiaLkMfvyy%MRf(NugB35M`N29L%68y;>25SvIaOch z8eK9)iP2C+wWx%rDPE<6a+c4&}v z9hZ)q5ZCaq4eZkOo}wyi$V=W>_BGk+yP3IV9@1~hv;4K2V8Z)wzDqFn;+Ec!{o5)( zrB{`5XobXc$`MU!3Sgn`q)5k-v8pnM;5Nnu@K4B(;`Rn&@eboLi!9qI3B#uMAw z-~*s7($+Lg5s7jTwy}7vXmDGf5q0*0Zu|M4)vR2KQ5YI~mWsgW9cLW@#a@l-@=EwU zTdlEOU6WZ-&&tI%`Vo^&?RL~a}lp*zm|jWma!qW?w4kG$@ZitVCpr7 zRA0}2bE>UsFm*{z{IK7vTZ!{6=tUea)&I9&!pMVy>jnz0kJvg0VhE&sB#@MH`Oe_( z`2cyt5v|oG<@Q5a6~5SYDX-0CN6 zi<{+n|6HCWU-egI1~r6JzT{|b7sxCe36yOF-GZ}(NR|W|MbGpPrh&$_$2M-w%H|?S z1T~~uxGS_ z*R@oxAyW0$dW2w6Pf5yFt+EIP=I%f&zZ_fLnxZz+Tur|^09rC_G0Jw`)F15GppD~@JduRYZaa$&Zt}cSf7q9$@)v26ie{N z5$bcY$*);>@|e!sj&a5wK!CU;sF8SguKR_mI16tS|LVecM5eH5WYB;nA^@DoGLz7B zVJCB)%wD_`3)~EeWIHfY(R9B&A$$EP8|v1UG~t(Dok?fa1kht~qSTrCI_^=Dv**bo zowr^ose=4$1r>uqnmEC4z4V}F`2OjpdMC&8Ug5*D!;%Nz_sEnA72;^V0nbwqD*USY zDp4?f{_eBepgRSNWGACr&ctMtEwN{n*MwKl$3*i6B2hMDc_@(Tp?yyd9REIPkHQYK z>o>KT>$S-kKu+m{Ob+pK&tnv%c!-6lSxrT|k@dH-B$PcVQy z2ZxOn%{NY8w0B(Vi`F=Wz~=3XlJ5quVY)5k*4J^4y^W@yg?Kb5;=U7Kn<4R?WEo*q zF@h+_gMG~Thh=NGsJWoc4Cv9YymJpLYLxH{-iCjS5z^q-(!Fw>Pm*u{;_FpKvZ$>v zl(BA1T62Xn3(D$4W~=c{{+v4yv*dXBph>tY(IDYo4lvK!i8{6P} zOrV@2+0&LLxx%bc+`ua#)GVLuJvH(%DETYxHmV~V*FWy~$9HyTQfX|A-(5>(jM@%j zhZWO4$icRKcJ^2y_~6`&?I|19)^}uH`UPG5^b36Sy-uAAuHV82TaUc(z?z)`De%U4 zwEdy7*;0@UW&x}OZs;;jTcYV?w9v)K9h*kME5HXL5PvTdw; z5i5A6D=Sd>sCO0Us4UM6`D)*4&49WLBxgDT8Y@$E4+xkyD->YwV!{XCFonxyK`qA6 zjRSZ>uN8#;%f5>OqOz2YkD72?{~?%e0rSvs0?LQ>ksfLgxF{|h4N=r$%k)Lm*xw#4 zO)lIAPZj>V1Lt6)7D?IHhH^F)CFkQ>6mPf zbUgNy;5%4|Av+P?p4_+SxqMyH_KhX~c7)-FN&cJ@P>2KHrC;4P^apq0{e1+u3p3y@ z95nJwo@PAWa1!!$WCDDdF&U*7E#Fa>$=*?6mDEG;ZB^etzs+g=Zom&&{IC%UI$6Ds z@G*1Ouras(Q=%Y&zmw-Vq_&z_2|Y%Y-+#H4!MH8xn!*GfNfw~tgV3)Rx&rEgo=RQp zD|qS^sJVqgscbb3`(LZTV1Qjwuw+yLn#4Dg5=d`+;7`*ZJS$9JEZ)je4)13{DTog# z#?n3l_{0SorJIg_@e6kfENwQfE% zwAIqNCM%InzBFJmnLHEwux}$dehv_Le?Q6!p-A@~xnJ89uiLmsUVkN!bX}Bl`njSn zJw!-=&vlBuYq(C@P^EW7f0%4~dhB_7F^7#~@}FWN{@rmnI`i--@B(iCtFTHNU_8g zs^YC66BS0Ha^E?LiC8ys$!R#X%VgmwT;%Mk9hvM zN)}l6+}VEcq(DabfDv^|jtpnjr=Uj^Fr3H&yY47( z6S4bAlb{$irY(38mW%);f=?z(NzVE10MM`u0iB7gAr;D4I+1#&knn=shjiv0oyp{w zm*BQ}ld7ABbA=F{H;``GyczHOjMrP=w=GXX#rtwt?EmTnD1%YwaoR4AIc{V}iq!c` zZ;il{3l1CS+ajJT-pe3kJ6c;iE@p-QUb)oRwqrhWGWA(_DTj^S^1&JasGovK*_p{A z_aq;d?PBxS^Cj{-eKoIT3mU#*QZLR$OSm*~e<3I`tex-zyij6*{N%Az%{i=xH{N0? zaixaj@8yuv_w9t|b(iY3t%3>7*e!D!(A3qxdcuhpIaE)s;-B5w0#wP3z4t=btxR(- z&fFAA&~>Ek`y6#fvoAUrw%#Xr;l*e+AW8(2Cr928a;VeD3YVT`L<;<2CW1%#s_6g^ z8i;=AlwMGrb#R?e{uQG)DUyYLX$uc?b`k++evO<82va8nK&D{z2YTT zfqU9lQ{sh-9UF>kBH%_(*U+pzphTM92TmBE?3Uv?J&VHw@Y)|r%}N-sT<<;1$#wzX z)fPS|F8j%^nB@kihrp#6YIy06=@xtG{xz!qbO;Kem>H3!%!Q*!G>F)AlB0ba- z6V~He-_zlx;Gtwj-N@ACvH?H~38^u-`nhDD=J>eI{nY+Fy=O))eXxx8s}x7batU{4 ze>6Mirj9=FHCX{{D63&y)ITkN20&hh414X7C&o1^n+q{DJ-^Q2YG~wD@JowHD5Ub> zg?^_wz6Mu^6t&eAL=&SoritgGyUJdEeHi@YZ*{0VV~7Uv!OP!Ex;KUzO-D^%3^`sg zrP6y$H+DG`lWoxspyZV|eROafe?o;(Wc|sq^j4FRMfPF{66q9KR?0_uI+Y1-{62iB z4V`n^L*4CVNLP+`MoRPM2{=fCsaG@vnA*?Ze3{4qZUY>;4KW2MX{jIm?+^$I35MuL zjJmkk6)4|XAKaXuJFvHBoJ2Q125@P?-WLK(hE*?1j5aqXMjAqzdQWq*1Hre*glo03 zfebm_qG*K7AzAh!TBi?re~s#F=kd?XKKL1C<{oMOuv#{KvQezmP1kHj4#g%9y`9s^ z)5|D7`*eh)knqR3+rTQ<)*8%qx4Tn4n-bWRR6IK`#kIQV*(E?j#iRjCmuOI)DN8-P zKP{Gq*HzYNRkJ-&iE0obr4!v#AphUblhRTiC#EH4=pXwYzdhCE7rNTZ{bN{*QCm{a z$k#7UFquyC%2^6334|C6p6ZD-KuIIsuhOZHKc;^rWUVuERgeNE0U(l>PVKOweE-LZ zBU?%AS05?ehT$Tf!CA3QE}07`BoA~Ati4EiE*AmQJTw{~N59p`f5?AVhX2ShN((#( zg~oUyd8WXlLmA-t?@h6^7j>LnYv9z#{uG0#0DlM7QaHYTaqr?$rZDr6x*!pNVrHM- zxZ@E4ts=BY)v87R-iHaKp*@b`jISc4lgE!*j21j-HG$C~N*1^XWlk6u6tW&LrR^T? zkZW{jskQgoaDW#9{^2AP9ZuV*Kd)pn=x2Q-9eVVt2$o={{U>K*MU;VkpqqrtyS6*b zuH#Ft2VuULOu ze*h%+Evy;due}SeK?^zu=VfRf~=R2*&EUI)nkx4kTdd-0Rb;1?4ng5 zQhKlN(byyHX{>Ve(vo3~!>y+!hQB;Gf-LLmGq*=tCBJ$7USkl<-sNhemsDx?vtQ!S zRpZAITiM7G_I=06$RV98bX4h~d?&-GVXt%6JD>91d z0R0YT`Y<9CbY&dmQ5*G<;Y%E15rLl+Nu%U2db0DGN=W2iY6z8^&GFZ~ru{t8hS-~z z5?FA-vI|o4T#&Ibb!r;^KM{y9fy=59ln)^PZ|z~;j>pq!9eyTRCbW|=1&syCF=OBt zC6TKW4rv{r9{eZzWn8{tP%b#`eqYSAX1I zBjd1hkkTeAgNE0k+&3q3*@both@Iw1`*(A!k@CTb zR7Zmi5~XQ#z4AO&b@nQ5FlNW&LMyo8XDzTDA`dQWLtA@1*hX}KZ~mZy4|xd-L}u9` z=?~7|SY07H4#|vVMX6@rV!f{vPyHYWJh^^`EgQurvsyu#xCz#;+oK9lSA6?wD5R%L zBf3-aX#qb9ypcD>z+87FXpkP3bEfCi!RFsls1smB-7%|L=rV)F$S_91u%Wz7YC~do z9cV&%BLJvuO|lKDE{&^qY#Uvkm%TUkAvuNyp!YH49;c&p({+WA(cr8ro&M#H8rQ&* z^go^lRF0XS$iiXea(d4Zt4JyD{q9F0?Sx|$zX030AN=DRka(nSSC(!53_&Y_a9$Dd zTnuxIo~~|*sEJ6)KIbv^TBdJeqYTYS6cAl;g(mE0jM->E0+Etn{7R z_W=_CXqn3$d=biD?aDb-fC{AjeEAmdH9VZl5OP=0LVaYg!h@@vc}%~94WvXKm@A>{ zW?sB=c{g#<#8$wW&)AJ}_yH*|-UPnDtIk1}Ve=rt+O1 zIP!skR>l?V3%)yGN%Il6fPxrhXob}Zk=(QYWeyNAC7@6et%$hy-$qT|!Fnwh1&yCV zBg7TC`ZjUyarc7%70HY34ZyjI! z5-ihYsUL4f$)c|I#&BsU1!20Bt#xY1S9_%ODmMAcM`C*ocerspS9@2HqmXw;@jXBAr13yEIID!`s_Q`8edt z(#H!TpYQ=NNwj%HMu`~bx$XOCRaFtvd2 zC{J2AEL%=5HewY7Dauec_@r}P#(k%jh~ewA7p6^3CG0+|MbvfVMv7s}1K{IhPXAzG zlUoe_6mHUfW2c1^iSo(So)@mn2E_?(5<4Xw^in53!kmv!@^kM9B8!KWKY;7}gh~y2$RG++5EtlVCc%cgRE+M`?dtM+83=tD=p}!nFUo7rqQnQ<3 zgJ^+}Wa43H4G1D{j=;oliE`E#)I!z2weTt37c%J#Fb&QwNG~Y&-!*1BijlWvRp&sE zZ#}L1K18_z-gZ)l67x3_ptW)+zC5E;X0K!o{4Fh&jkqo+R~!zy6J!_KXo7d4{oHQ=$j(XU{$S1;a^~fW zop-8_3Ua?urQY5ErZSYuUk>K!o^V805J6^K} zY0&c4xhB=ZPU%PZ;01*Qh?_k}EdpM%!T=dQ7`+z!czQ!X3VcUGK*3Dh^Olwg2AKfh z8*)O5OKo~CHd?Dz6ieCh!pni4PV;lsM1;O&l4dn`2aX~j0zSAkkpY}Ar!fv6b?bM} zdw}HSjcm-U6=C6aHyQC3e5U(V{5hDr{O^=;4iO^VkL8xiE)@nfq#mugFYTb80Mu;f zIW_peD*(jrjHzjz?GYpr(~Knzup0jPX-$X2z&6WBz;1|Lv>9l|O6{Lij7>kO5f5{# zqo3X$Y2p?GDzW=Eu|HEh=WNN>JtSI|O8q5V{%OQeH-Og9<#_RcotRL1wOm>oX}M=a zFCcvHQr%^}dF^tr1PNtjRBEGAM#a)BCtdnMzn$_SG>EA7Oegj*?(F6ZL`GF(siWCs zQzET%?f|`#KUtje6?#1A2Jm7q6iuqEbe88Hzez;laI_C5Ca^59JOn6qgb~7WuoURI z3ZVO_A)o2u{1C*i%8t@N)J=Pc4y?!=GnU*jdZnElPpOwVJv;QR?G#9s@d3{$q@8$_ z(Bj5yb*#RygY-AvC{!2+DMA*JCugoX;UBbWc)kJye;g=s_3Qts!-fE2yt~9aVV}fxbe*b5 z`wH(<@74GI>h=2II*93co__N@HSspdY&%)C#C_%E8L(06yi@73yOJ*KNqC@qa$NOQ z&pfnFnW`+;JM;J*7y}ggCg7d>^WiZLFd=+hax<>)`Bnx%<`Q5iWM5)*LiS~g&g>i( z5Z$&r;K?;wOzJfm$QSl(mKt)X4xYX>ItQ=4LhsuDrSB>vvj$zzvn1&J{ngzO1*GbkCmXA3#TYCom^&b1Eqgw- z*n?g+wzT2786Mk&&mj0Tyiu4?v4nR*mv>U@m6R+p^7Hur4-*4Zdw|mV4s7L?emrK_ zl@f6_aR;Ozb}Im0H?}+VjOPYvTLM=^r1y5|n(X0@a5fubaDPzS!f^6j4E@0;_u@Z~b9k~Vyok*Ss$HDMNZ zCd9YOPf8z2XhoVFW&Ai51EX|GIFozz*dBWQ{on|;5;99Jb?Tad6b#Cysas`0hCHu& z!ZO#hWpJ&gN;Wwi_XeB8$GVz5Ao(l(8X0!S}_0<-@#xqZ-e1A|>#>v`|! zBKmlPx|9<@N2j1<-dGzs=K1Z zGJ?LV{s;jc$JJPVx!+AmzKH|V1EQo-yr6Xk7RispZ8}3oY6UOWcNvMnMeAfzcdSdf z;jQvYKIT^0_gDxk|Am~-fdb6mq29ZBe?;6n*?0YG@eNPH{cjtG_1>;R$nxRaz4UsY z%RfO<7*QYr%M3nHme^U&7Z`d@9Se4o*H1Xf39=1cnx=KW#^(SL5_9nEH|F@r!m_e$ zwMqBk+(^BZnFt2YH(2fZD<=CRqIzNt0I~&K+DhdmKv z^RxVE_b%Tgy#+9;E_oDFb-R;+jj-~ECow}LKrB#QtA6%kz$2d^x7@FfRc06=BUmT) z*5srbBp&S@bWUPX8~yE%$1#g*BN`loZ~9F>FT7p1MLY#CP2NH%=Y+{A>`@;*4?xl$ zOB$e8Y&3!iZKndERGZOV4d?&lTpN&cdn01my<3(BsEUJXBsv>@AN9TG3PCiNJDqk^ z5g>kcP<(DHIv4@tLD~AjpSZ{6*IiffTEYgXir5tc9(5Yh^sH5yU%Zt0YIgq#4(Z{t z+_EuG`tjdk+tv*m@2-|->*q)hZEr3A;7LDixj^pR4n#GTE6|iJj`HY^C+B$@uEajA zOGcMW7MmFVo+$LbswPWK7j+oQ2HUM1%R~+kaDDmyRTJIlaSXX2le&w|$Np8tuk@^s z-;kQWf#DQFpbg!kd%X%0Zh$%Z;E`O*-bHDO@DJCH-vnPSrm&Vi*t!aYb|t{De4cgb zt&lsH-7}0N5S?d4a^(@i-=R?3$B8ry=1sBuj7S2UOLtjK^qp)zBr*`t7UAIJgDeqc zrIRd|@M&A`LvpP9-TrHU9r1+Om@+w*ZWkVBkMU&J@%^Rb{}whaaKeb@{T;-->C%?n z@z=M87n_6kzCGGhDw>ny8tkzhVhB)?* zloK{M1FKZHI-F+55px|>oo&D$bFWP*#j3x@Ze_j|8mlGp81gdETA0o-`+BCTuZ)206_ zZ(zO$#LvMaxjZH$PWzofJU(w9-zD=av6BEA|EYShH3JzI!*~VBoxzW3k@Ppm;$A*A zigHQyT@?$a$z^#M>9(b{Zh66NZ{N$u&D5mHa0#xo`P!;wxs1FlsP5rTgV(A5d%T{Z z&xEcURP27HBn)iEO;u&2W$Yms+tmk#-UA6~ZW-x2$8%X~4{(2v;mxjJvHgw30<~gt zanJQVhJTsDlMr!ZuzdvK9Y`p!>IIz5pIrg$c!>gQY2c*^>6Y^)^~lvtlpL0T^JMgMEX<643=lwRZb} z1y=wHNNF>Z)tKE&N>R$_{_jqkhpuTCo_K3L>B#Q16PkR!Xg{sM;>l(A9GC-oraOo4 zOrXKJlT$_&WYh_4azQ*q9AYG@1YN<$nnLlPg-9U%1p~3i0h_ZI=1pgw+8xyZ`SU6O zY^VZEgI$}%nRIK%Blw#sfvC$!gKM^Aa7M$H6Z?|{q%fPYwAs1^ytJ0xFp;K547XdZ zCCz|4AxIez6n$PbnfoI`wBPChXiJ;DaX(tepBXMRed_Rnq|zO2Sxrv@jY`#nUsz!T}#@=@VvO zkJ=D!SI8F0zqJy@Vpi)OF2{PS^mCZtC0O*|+GjT41WC0Bark=n@AL7H>FZ5b6?Vlj z(RH}J{`$@f45FvmP_HL%5Ar`!#(Q&b>goS6(sn zX7(L3x6={X>~z%G-t(>9Opm`yf2Wo{S@k>m{j0^|jmK>B3&!!B8#lksAAENvkUq49 zul(SSJ>)U>m7ab@@4Zi+6*)v&u4dRvKldc5;c%xXS%~GzLRC0W_G{AKj32^f*_$kx zQjDRnwN>a3dKPI^d`IBs;$L2+*B>*enrD5IA!PJRI!PCe5`(<}1hAuI*Kum9n#Pl- zFq;rrFYIn6Ln^)nx{(`ZB6Q=O#sqg881G`xmcW00b5SCGDLnA82tnwX%(L@8b#d^9 zG7vsr0cw+7(%inj+B|ajPe9g5>Tct06yCbP7)6J}x7ic#W|T(UzEAWZA{`Uw-nFy4 zYQX5D3wE;JfwAPN&hWx&0#K=0_FP#Ld@m+&cDM>5nEyxEcL!40|L=1;IQFqcaqNUh zWn>+)GfVc0?3HBik-ajqM>0$HOhyvfGc#m_kiF;ceXBm-=fB^-kEi3f@AvyPuGe*4 zuce0A238jCEb9MDi@q)(Xm3Di5x!%Bzhpf9eD(OSbnfiyM4jKY*;78c{?N8fjI)(z5GH&vp%gaH8yW5%Nv6gPZ&6Qb@$g;T}Bjf#t0>NLD9B-(s=*2FRV z*RI7>EhPq^)Y1?=Yd1g5Uf5iMJeLC>hR3}lZOh01GG38mnlpwx2>1CBLEhlz6b zW4sX)#LxTJ3>xBNw%H@gm1Vl-kQavuFAj zuHIRqxrB?kmSsj!zQ{+Dts7n{xYqJ_`|Nb;qwKKozW~{p1UyQXF!Z%vDN9*?V5-qm z>VFLYC27sf)OU7=?>{LxZ?h=|Hc`T2T5joNJVaR8+`L17n*}9*Qf}gD34|@?p=p-l zXkfGpH87bG_f<(i-Ea@~ew*AutUNI5wL6PG%bi^Q5wq%sdnOSmd*{~}2u)=O;aJSs zEh4}`Jdr?$@{}#(^J2=rMj|E916L{>Z**^q2gl;s1;z;~uAJtHzH{Dc?v~j`@$g`> z_hJm+X|9Fg;#W0@nU6R>IRgW-X2DxBP;8V9fs|yt>$KXl4s-cY<4kd+5}|)&41Zn# zngH}Q=0RY#HITrS9pxEYxlWZXroApxF+J=6GNHTAS5GI#{gQ-FUZ5yEL&46f(o%%> zVMc9NTOvq&iP&I-dF?;GgivW6MDm=1V{b~*m%yGGr8ii*0_wt}HH7Crs0n|3{Uq}W z`M;ll!f@B?%dw5{`bgtZagKl=M@=9mW&$q!o8kVWIwlnLW4|x2{}zY$an(S@F+1A+ zz+u1|v==JuZnsF+*X3U?yn8~RL(_=^agd?T+3nmSFnJk`2AjAq9(|5G1e&4AU@?5} zZ#53I6g()DhUkk693Sx&`|Ypc0T%&4IJq~}`v50*4EZ$?qaBZz597-{G!tz3v?$JX zY1kW-WXq2>~mb~-+oB*ueG57z(=O580hXf*6z{Z0vPKangtzO zWBR)F(t@UM@_(W_8cp&vRU~+fjl-1KV9cX&)1Mowij{7udXh=z>}{lZ13|yXYGT7x znY9lIl2~l%xLK^aAZ;?aT@l}*w)cvmw%+qixbZVDNkEE;M@+1dO!fG=G3TMPGC;895onSjQhQqRBtZpGVS{nxVN z(0zUoy)PAVFE3~h;ecu_*eAP^yk+Xnv%4gkR9mXD&E1bCisRSlorW zA(N?=#9Q~+vdnM7;cSOQNDT71Tt?al_VxX{`yXe|oa1}A~EdGpep`DZc4 z*b(c!!Jl~MZ)5?38nBE>vL^*lPGSiyIR0Hu@Sn+{iB)EUwYcYMO1LLf5Ka{vHg+%m z0X%x@QV^4Q+|P*}wv)sFh3bLYYp+OeR)O+iLbnt%lHLvE=|O1_acs$tiLHPgxya~`ip32_xnjZQjqewy zbWhX*&4p*rM{i!Dhr&hD!uNo(f7m!rf)5Qpbz?}L$;U5&PaRHQCb!8L_Um1Vak8_s zC3Z+Z@OZ)Tc;ZIJ^t<~te zE)xjpjBEQoJ}3Y(Ttk34L6tc5l84%5$i762gH6j9!z>#&i7Xm8b6bdf)o}RT#qbbp zUqNc>T-xG4e4DT7wN{r)o-O^43qa|=O#+y2B}M(1;aCP>fS7C6C_@m4&J$&^^5R#)sVW6HN1&9{c^$^3Vq!EfHKt*bq{fs2|1aN*{DhK8w_nfTNjAI{>tN;_uBC$K5NiTpYxw4 zkD6Hyc&;AaT6~49Qq~`(Yb~P|b3L#pr8pTUK2ogNQ2piXsQQVHi}7uR;vh=p^Y044 zz1!#i{Dbpfrm<$pY$c9q%|)z_F1J_I#rssayLUQ-c9XwfSSZJa2iWbsxAaN8j&4Hy z2_S|6b*jLH51>lTt)AiAyjyQLLm%OOt?GK;?Zvii_9>bdXH&g zaj%{;axj4{H%c+K{Fm72Dm|?n{?Yx$vx}l|Fo7jEN+cGje$Q785{@XA7lp+(L>qGa z@KRFzSf0QyC(~S!t5@6*^9F~Q=4tY$U;mnUO=*b#D-}^YHe{THGYE_m0z#0sDNzNA z@oD2d`$*XopX4wwJ2X)9{@{8Q@jAXjFLaB56=iyf$=K4rKKd;TLc(8^XG7 zF;$S```WMgUQBBpEdZPWx%-QxukJ7F*9erO&EVyMbvzK;GKOsLLd_D0wx?> z4)qzgdSQR(`+pxf;NbijXbp*ask$e27$1T9komgHqOzYj*^NK^rrh=l1j?U_9cznpF5mXh{I-6N#=e;Qmw4?ov!NHsD7hU z{b9$vN&3ts(a(*?-p?>+_u50jxhI-lR1%#)&G^t0bQqQnM04y^Y3)Qb?&87ke4{Jk zzk5L7&+)$^iV3r&!@ATo%hFn;Q2Y_}vMpp4%rb0;6{8i<#CfhyUv^eYw9;F)N3W zqEHz=ZoSNwTsgaXG%LTh(nd2W)d7uc<8*=c3Eo>g-!4caQHxuVkAaJSTVAs%Q8mon zPagWN<}!w$M3POGDa{@0>F@wvcmMkfu%R+=IDxG&N^!3GmqxLdtKsW2@fT+~sov+l z#r(*R%E}vb!=`0Z&TUf*zvh&prww~PwC4y##bhpywlU(={k2m+p?1Ac~_ZOJ6g z+nb6vYY6_`J4(`M$|p9jaV>b^L~--jCzSs z0d_oCD>%OjhHpM@HppHAt@RV!ukr+KF5Fsn{d3C$!)I|#pm!_40@DUyT?Y}GnBCcJ z{CyfAbgI}U|1!J`v_o#UXxGzklSWL^1x*-O^R}bPpK*0eN}}C7WI-TFN$$)5T@>U3 zxP$SezZdvpJXw_^2$XWZst8WA0yByoTQcI3Vg;t-1)uuwlLmNATLGgjePfM?!cU&l zHx4p14@j|=`XVpct>Pj3_#dL_>vGJhA#;RRaXhG%s`;LD8G^$H!bV?%Aa#UyQD+CM4%5F}MHz-bUFaAjD3p|FEq{?Bd%09np;F+QVj{3@c*ZFVHaN@p!A zPnvmOhp6j2<#*@BNkz$>ZqoiX{a>Ri2mNfYkak#^?O<(bc`#!eXK46_H_nXrw!pE* z4PEiurYo!0UMU09gDXb+?IDw&hjpvyPmXa2kD{L(MFY%lkEBjndX=uo)Zw&0MGV{q zW^_Uo;3jqI`bG`=xQZHgCVFDroYKry)BU57g%+jArMy&Ys@cWH!hjDWIBTZp`cnqJ zoxEM@HhhZ6R%{A&@7$F|XH*%hrfwKuFn~+ zwk~D+9e~ya7%?m2hf#==X{il3E+h>ignJ+8z8|Swg6Y2s4%3LLm zn3H5jO}X`wX)3K3VznLgE?^K3$=z;>Z*5NPqie?rd9GD#JJ!7--lAx=X=_!_RjS5Q z$m8H1mA!Y!?m_R?I?V$=QF9p^V~5D1Pno$x_%lsI3%cZxeG14P(Mms{`y6NgcHEk6 zQon*2rV2G?Z{v>Z9ty=h^&n@xg6}JifMP|wMECl9)Pn{#!C91kb8QtVV1H%8e_q5F zkoJ(k?)K)in~>&xTr1b@$XG`5x^|7L+lANwY!!v~;W8ftf6teC6BET6ETrb%WAg0)uT!=Mp)t*6c z$5D8@%I{B`g*Og?s@O`^Nuof@7j z(TOwU0a@UWzj)9{pyt!0ok2W~lyksl1%o9Brvt=0;??WAC}zx?DKQ`3_X2%z zw15CW>qo&V+9iLt9ve$a?~9HX;2-gR8cV!lAuA#(ZX=IRMp`=-yjfHcF;8AB9J6rw z_^|)sCmDvesVW0^bCd~TnsZOtNzCw)IMmHkR;CZ^>7{6(iZqD5WA*WWnnB(_r|CvY zOp~z|1`oi|mJKZoUjcSC{sCAmhF@!3&7pjn?2~v0|L97;m;8UUP9hM-0r?uhj!p4b zjT>E7_k(&xr}`8}4U9Z5Kn@JAE!Y7Mpa164y`YoAFDEwF&g-Vz?;z!r)R`|gENpmM z2sciow<|euarGwPBUVz_3!^4yD<@7W4}SuaE4J%H%JjD{E9g=aBZv7lH0dY4Sk>Y8 z>X|PwE_2t?K{i*f=+u;tc@Zbpb&qAw-SSa0tEO~u|3aIOBq$(C8Lu$(3t0@J;KO_c z$6JdjbaLf5_T)W;Qs@wduZ;jE$WwxP9f(Q;F z%d~ntyF6D6b|p zu;8|g4(54EuFp}_2jwLRb2xlMKa0Ug8+S4;nu?~BRd#zZ_t%jwp&nhU!$EZ`_Me6NteY|Yz7()nEin|maL7v0KgW*)FiYxKl;oB zAq&j0FZWyZyodX+jE3j!Zrf+f4>Qeo3YsFUkPv#GY-uG=cARlaIF9ZjSzjJPYoI9NsVPv=cqL9gRdQ(>@?r zU1cz}$l_ANsQtA~x%{vG0s5|NYdo{U>L3 zg?_{zD0Q-hIY88yGM3Zq6FuW;(c~U)kT&9paW<8QIw2ku=FRnyUk=ziaHanc|NFvx z=2oRwy?9F950bjq)2nTXvstnGD)op{k3ysJ8!l%Zjk0?QiMZ@H1S*EWo?EN{suDvo z;5Ij@&#VGOH_@efuyNQYuT5UjZIK=uA84PzN9F{)VN3mXH9A?$DLn#kyqpLk;{8 zA280;C(Gy<__4>>Za$%s`H}Y(uJ%zsM}@3`g7xBP!JjQJgp7Ki<-GBa@wonKteK;)kb6bbyAQ8RpZdxC{KPDEptpP>e2Lx{yQ;B&)0RVEY2; z*VO=UpvT*6{l|V!PaRW{!cB%2MfaR#Xu(}#tBC5xt{$tiJO5aa65U9+duX$({$Y0G z8GmzAzwpFS@IAXOcd(`t3bz9+C1o?EOWpZ|esHb4PSN)R<2GlvH-%1aaYenpGKeU& z(XBS23*PXzAbM->X(PY$nh#}VjrSHC^Y@>zqwijkG!_V$w9KXBS>`bEEBP)C;)zN)JwONu*JnZ)3qm+(kdd~T`4Yo)c=oJp=yi7Oi zM4dfwu>|a*w8jk)`Sguz@V|gF@Ao3$jexKf-KazjMw4;UvlZNWRCTUlu$tZbn)Gi}0#ya+e`3n)3e`J46L&|#uQ%yPpOr!SQgvK zd~IQ8eBTxU{VAAD=fnXx9c|ztILQqvKI49`{qIG+arqOx}Ip z7A5ixZ9UCd>bZI+v3|#BTK2J`dJ`l0u(Egy4FaHP#BwL&tx-=w` zGW0mZTh zgibb~0{&l7jsc)^z>^{ZD2W@r-VUAV!p&pIlz@-H*m87a6z0kc})N)16lAY-7Kh4^u37H;;lxPO#vJ zhVcrGLP%tQnmkn#AlFEmvSby z{({;4y3POXa;^f*Rg!@HZnCL1(|(&hh(h(8YO6ZO>taTB&Ff>BZ$~u z1RegBuJ%$xORxbA229VW27xC5-3*oDg4561E!&p8ZsbIplKR9 z9e!#!yGBC89P8sY{dRbW<3;lHN>bBzrB}qV%lj_e%$@uegcs5DqO1faU2REHC!!w? zM7p{j^LcFQIO>pMCie9}nBZF7_8JR(Z3VCINU zFKhy|cQ|%sj|1g6MSE zZ}Q>yY4%H6H2+kmj*ylxM8o7iPFK}}u?O~Zn)^yn<#*Y%JgMY3&oac-f)!5sm&g`U zhld6J)@B3(R0@{evWc}Ve0hL*{dhYr!qJk{EO=K3Sihw~5r;wkO?iSUKjJG3P>a=8 z<(MF40cYDxI!;h9>y?~K%ttGE(oO<*Z{#3DZBMcJ5+s7EYhRBg(mJRO`JitLMDwMZ3cPvMwN>7Z<;Ul@lUWQFiNx zn_JfE(%tAz@PO~{Qhsj}J)KwtfMKX-ZgMj7%=cSnDTuWd?wY8Gqw;=8_Vtc`*%y}) zT%1jaR`uH77r8f$bnLiG0u=f67dhXvPl!@cLYZq|U3~ z@ixhsQZ>7%vp?5a#h6mh1geq3X2G7|s+dA_s?`7rK`o9Btw~tM564hqVK5W`{HUUQ zt9yXP^WC1TsNwx=Um+&Ts;2JLbwDN57`{{HtfO~B})C~s&Fl(Q5;-Bgo4@V}0E>%Ze~O?rFUdr0$5#uEzv1I4Vx zvVt~`ylkuj66C9V=ADzc0IY)AL{+S@;rPPS);9Y8yAh%EK zm`X~@0_WV`Xk+H_>L3c=|0b+xO>p_9*lbU{;@_z$j@B)U;h{Yrj-1hY{30yKc78lpn`0z?2z$3L6LQ%G#m|Mg zty^+3mPv+g)@T_}>o;$$p&r6&CR*->EzvHh#wk0XpS#Qj5|D-o@~46yY& zH8wdIkoJ&U!5@&_Ueg@L%t`$;<4QZ8#4!zy1j+(t`MtKvnK9C(|N7i$HrK1wU5&l( zdH1OQaRDAl1&i=Mj*RELSp@ZhP$0gzG^;gtRGyVdv~MWBTzBzOPbh%z0(Te4 zlmIFA0VUeCW3o5sJhkiElXsB1&Xm*}(iS;F(h_wenD~}w5(RJ~;A6$)Dd@)k;&#PX zLfzKoCWB^c^Ii3csMW*2izWK#eJNBeM>@Yu=DkHto>{hNX_0AehDaz5QPa$%71oDt zDKZ&uOG}VaCN8{hA4+gXT`mGt<_m-vM0_2QL?@7R{E_+0B@2}MG*dn!_skz7apabC zY2mBL1KF>xKpKVWi-q3Jy276q>I(-K1%;WfOA4bzs;t}M=+^P@?WF`@Q3|*OvyOWZ z=iezp218z&!C+%X1foRX_HE1*<3en@JYZi?kztR5$1s=dU3o|3p=?K5ZAK_67sldk zu7;J)ac8YHVZ)E8n z;nKZrKze4vp|@acC@ww_t-`fS387+LW)0b!&>k^3Uv7LgOF%uVeaD!8UAANr$X0&| zmn~BLIUwlUFstTA?ZE@(A&kj4Z{O91M_zw9fH>mm zpyYK6{}Qu=<7T!QHAdSnQe1!2^(!IXZh*lYF=n=^>$U3hPF0s;k0#hLJH{m1x3S)g z8g>t6_Lp-d9syyz85zBmDs_!LyiyP_p@UNZ1p_QD-bESWVD9!*J;-Mv8yPZL8@o%@ zy;{EVbG795duthVI@&b*6z7|h!T}Mm;ev?h{+n~FJ#bk~WI@z~i`ZA8sve~;+(=p9 zCizCq!fw{v(@aFSuxMFT1L8IZC>w9M-I6Pr1%#ykOC#V!%&m&U8|x6F9$i1SBj`QY z`?Rt%KG-lnpcdTHa0I!T6p=ck-MDQ)7hk^xF-lO{A+ndh}2;fm)M8na_e;{ zuL!=*RDTa&SX^{w45rsd(yJep9UYH&m$a1E9+zuTi5e?s?2)t(a|aW1h-h7PKX_X1 z_J(dW8KbGf$s5$Jp7*0);HD_#&q*U$In}k0;p2DXXKr0&yB#@0uHdTVgM+UivllYv znMk6a_&C0%M14$sc~7;lAD%5r+`R9p?;C(6UH8-=zFK!fd^bVK;D-QEQZC7(6*4-; zpJ)heLx8WZ5Rle_y-qIy^0S7ZP5_}qHMZ9M=NWp_?g4ajQt z$uz@^c&zIb&5K##L+O5Mvy!!KcS>CjEDKo_6uhDybsT5AvXM{W2$24ZPhh$QQDmf| zrwPQpR)?mPK2tS5V!PZV&5JRqb>%_<2MA%o^ZRTu#QY*P7oJ+|5~oAkNk2}?7s0F0 z63-0Fe4eqpQLJ0T%WQJ=_?{4?50E_ArI=~stGC(y)ac!}wC!qgv^_j}MDKn8#yt)1 z^AKX~B+X+#A+R(TcW**>IwdTF(Iev-Dr_eLsa2sI5G6I!bkZ$ zK2>C{VN2VmGUJN?+YIk%jh4L#VpIYZPblct`{-92r~O{MXo+TuVvX2+6u$@oGa2uGGkrAk#k^r;QE@snb7>e30{eQ=%Vj|v=_`*J3#0tU+^krh;8 zDBX2wiB;SdUgmg#&M~ZhT4&jqe0(1a43`uP?_AFLd8WT(dY5b#Txdficn<^;Q4XJx zB|pBE1}~>c57F8dQ#}I3P?Hs3svf1?!v}+ z@LOtQCQB@#H|=i!6<}_F0hVMMtIqe+Wxg5xHB432#*+KEN4pBB;SsANtVg@)Kv`5SNvRTU+y2<9*T6(yo^U>rt| ziV`~Wxr@slSNT7_eXPB@V9U2-zF?u&#th>)m3hs2Gej<~HZ5u?S5BX{z6c>&Smap3 z!2V(VDK@+r0+i5YekGKP0#FE?z|CfNiS9p|?zj?GJLoi#x7oe?eUg+!XoQLN4q;vv z0l-Z!*0)5$4f1e z=NUk>-R2AYoB%_m3r+s1Ex^BoN)tn0FpG!08)G7)U<{0t5Uf zyRjT^Y7 z*l3Hb&Q4=GQ2a_)Ixd4`c*B$>c^>V4y^}|!UmIT*Hl?Tdy)uFTrdsJ)V_?T(+#}k9~qW{@iC-Zt31MkE)AL+j%I5p$a)D5cyI$g>bgg-o~oS zcKtAwFLH}i`|sjY5>5Q%_shI{swshUmb4=w2@s|6!+{*>uFAf|$CerzXa#^2+VqE+ zLMh*UMnS4`hPL6v!em%*|-G@qrZK3MTt{vdeyIYSdE`#0~(zONtjJc!}4B zmF~za)H`xV-JIK}XzbA!eiXYaTWlEkEuRJ61iOX;>r@(&U(m%h0-A zKGC}B@tE9qOV^d(=)^5#(>Q8v@4|X3siAANVRh&s8%^-$3QKF0$cKqHyc&v9Y8mrb z?JV5j{ErpJRMd!$tY6inNmu=rkBb$5@jU_Ct}r;m;zP(w!;!8@V;wB(QDg<^D1gL5 zlC`AjGE|v-f!iA2m?FbaU5A-)~foy7W8n&hk|yrE&i+r9ivVeh560+^i0p{VXZQ z>iv98_A|4p*Ydb%L9;Kv6ISrL8CwJKvH5BgoyU_TW>`BjiEOOZ+N!H$;vD(lqIL+Y z+1JJGCytan3>+6O4eK|eiRncSbUAdl<ZdE|#FgZhdFiRktStv$7T$1K1=M@aOl{7@yMEJiUDc(}`iCox|N zVZ9tIwd!sZ)0D}%D0TCd9?%Ao%z?dx^g_?8Kd%()eLEkMJ*Vi9?iK94!PHtDL@nmD zWE}*^Sz1L?`8XE1%MX>xiQ{EBkBND79PL|6zy9v&7 zUM&E{Hsl($?fH@tT#PZYl4lAX;|hh5)iL=^&9#CK>zb3nVc_>%`m_D#6NXY#0mwho z;EDuMr|dypi+k8FAw%4Lu2I)E4^=7hW~!&>Z|6S*jH3}^Ci{_p8hAe zyUJg2@1Iw~tlVU8O42rcBa)cRGjSD+O!*a~Gme~;3ZGuUSwic!j7=F07-C(YZ;64W zKt6C-J1bH)b{&x9UI%y?lcdV1T^>F?^zjDL!PoORXK~S#z5}}th+dzpNgQspzv^wQ z-{&Sgs+Uvs90pQ|W5iEq%8nq#J!{C@U6B;QoB|wL8i0OK-f2$8`lH{__n1kEU{132;jd$AmJZgE z5B8$9?!%*Iu`55926uKn2V?H$IMJhM-W3l%xMlx$ntcHtqEVM%Mn?g`R(#hjOtEXe zP}E*;HIM6)7G4<&-VNS1p@9Q8XW3t!a_WX^UNsq?8pQwezPk z?q)MOhydo2t+k==A|z*^A;uk3BMN)z^XQbs3Vzd#DB(Yjjyx&QgteLtfpxT&9sC7E``kVy*mlG(P1j~d>^Vcu}TqOmxiO9LZsj3A8 zB3mlcsOXx>$@&>z?>X4BR~Z@o>gu&Te&0%0$#1QWY3Vjj^}2R`7g}IQv99eqGQ+=`}4GX{Jt#kp^A1??Tm4KX?ys&#($!t;2|8r7Upgb@4qdDAIfm?RpFN`u2Jo{73I z6J&KYOD0t+3M12QWhvk7jm6H_fd;RdUS_2pqfgov^434D;JXD5G}tH8-eF_3R2IeQY+Bu3> z_Ekhsi2HAF$_mIWop6(Dr_5&!*)f2Y4e)DW+nq$)omu>!v7@Z*2;@@wNk8isvjxTy&@PseE_(~07mB9Fym>0I<7}|3r%O@#ajD3p(uG(;r2lH*Es#JY!Ch%1 zLCX&*)(C*N;tNofyFN2kpp>?QEvIEV+ViLm8|!k*TzNLVM$s9voW?14=WffR!h^Cl zuX#=3h%hut7(}w>^4YKR2+h-;H;$ZbLecYAA~?dpV1$6AioTfx*_%|@S=7~C80K!) z5MQWa|6jL??SF0+@S|PiQXA1br-RRC^meGDX`P*EHR+3zSXeC8MKpC_?l#y&B&LW7 z&Ne^0#M*cK;9GX2x25yl9^m_~fK^0w-6_H?L%3Xtmu5z~JH8 zLm4r0Z?W63zGqv~;x3PBEIVQ6a^xg-@rV`C|A3~-dW(+qeMTouo zQCl+S82M*t>5jNIgB(k-u7??!Y#wiSEUW^L{<*}2rc;3Ub4VDbH7&Oic9LdFrrvw& zMY|c~7$vvvos`o50arlsT#=qAX()sD+x8EU8d!!s#TLf<4 zB)Eaw4L3FH5O_+VDXEest%R;t*2T<3*G9l^2s%chgRv&ZY4;Cw;#O zHjaZglw_FKM+6TRDFh7H=TC_m0>OI*J(Dibh75NdhE{6DxY<1O!93I5O&c$~(=f~Cf-)Q5$Vu?DSSK63L-+)<_j& zCl=II`@)Q`LY;*xO+tf&jX!|(ESmlgXS1&@4>-bDGopT;-kDQN`S_|TSpwai1H9Fv zcZG+8w?OJiH*R;~lfz?dnWO?skqVn2>qUJq`dyL=o*ra1^lS!`|GeMN;>@Tq-%Tfd z(2vyYJ-hO1|M&IFL=Ht~AA`#O)55F*_mVm}q@kT3hMx8LKR=D8CnR3WbfW#u7jxgQ z8R;mo!in-J;HaUrA~*6cpg3Ycm{S0eTx2Q*_pagI(vEc831^*b#28uyF(PI`U8#K{NY+elRFH`D~q58k$Z@9CckTh`_R^WkOU&pSl zX!N=N=!N~yT>IaeG1E+%G__kP544_}2gdwvdA1Bw8sJlygg*_f(q*pG`K*_EFN~4B zR{tu04+B~5W-W=M;5m_$R$N!YHm`%}f~zQ1Ap2HolB?);YSLoTt?lDNOF# zfwS(-%=N~+!SNMY^wZU)!O%PS9M@`zQ!VQ~W`F*^sdUH4W_#?G8JW~kC&xgq7vE|7 zkTRt)`%rNjn?{d4vFFb-7VsnXf!@=yW7^{L(`T2Zw%?bhJ`KpI77bnzkA25~!@}8- zvn=JSQ_J1yK3f?0nwkdanZs{5G$PD(UN~B(@1y;!=sg}iF&kW)je1FL76czzbDd|}JjzKh zXvvd6Gi|-`!{lDP5x$1w?4boqjYMgL)lta0!rZTOZTU=F*z2x!kEjRL>1OV@0Sw?X z!cX%l;BPsRj;|*giZ4~BBU2PgawUM|D!H4mrTf`1e*Os#w}1ZGwoM z;aDw~$yN5e&JX(i(_W6)$*$=8Oz-VeRy|z|QNF>^A(onTk}9_BvHz8x3y<&`+wDw+ z{nEK!_HoXV%#Tfeo4EXMr1;h?Z_`YenVIT@F5q)sdqbQm;;=VcZE#m_3b@eZk{|b* zMdpDYaa?;xENs8Dl-1|+OyW|<^G2S=A$zjHJN$XN56S%$OZn(Pk>{6F3%=#j15{j) z|1(+{1#Sfs{&eSds*}zD@vwY8ZlC^sV3 zs`>2UAYK|IdZbI~RasloNQzRaK=W+hCGMb_e8ic7;fughZ~f4XHLa{K)Dw|A>SyIn zt3Pp{QTU2EFDC2`3Btr{@eH^t38cuOcrYJh`TkbvW>8V}U3D*YY>0K_-l-hXAKw02 z)W$qk`8ITIT;5vUiOcMc$I$M__s2sr$69adi%EqBq>;gzO^y-4&R&GcbU#mn);{Hn z_f~|M;MbV#B(FadTCmY@&pr+<{za>?Xt1ECArzAF@72H&*4Lw%<;J8|-i!5=1WbGW zBz{G4wEn)&`gj5h#1TDLc5k+3?1Wh3qS4mU>x!H8hwZ)p<|!lhT{p;owqV!mnv}o20eS z(dwVo?9I?Jym#%(l`St>UGW89VnIq!Qe(cL9~Loi6w^r6Pwb%ku}YZ+w*->)>TyHVuBjgB^wx{~;It(dXcJs;Q$oW;rH36DY0CeWU#p7X`06fPY_P zO^pegsItK~@nS1PJF}uQ34B!=rh9s@Ob)czFkzGUUHlW>AJ?TqEL_L-%f~$BCc}cx zJK|TI2Y>9tFZa>hyNGGvU8!WDW2~oNF?0E1+GAtKRZe@) zB^^`UWB*uuwI1ij>ghs6LG#HM%7w*a)oD+I57kVZnW-f{*K4Ba+|@T1g7F0s9@QLw zDaGXU|YDMmnq0pJ*uznGb{yA6v zS3yrkzH3hM>Q5f9u5=ZQXPE2Apsc|w;u96!eY&de8hvdTh8(-3wjHsTUy|!_6titK z+NS&dT@?D76ny~2P+srY0~man4JqH>L$@GO@3Bv@)9;r+j zv%j^o=``FBzx{cqc}QlFR_F4UZRhDzNeA;Gu&D6QMe_X)k9m?^n6*1hYbM)lrhlyV z_N({0)Y&~=TEUIuFuY~&icHa1BL!*OhRW@OPt5Nn`aLF8lnk3=gQ9ZtT3O+glG;tR z;U#`l85sg;6PlBLmh3#vrnVzI_}_FA3@{J*8Ugtr)mY-d#=j6N~ku!va;9t!~Jobn3urt2>Z>-R8Hy`R=pWjc#Mq9`AD17~(MUb@_3`_D+$}$&iWOTVB6>*W z%Zqi9bTYg2pG?9L0XIr`S?Ow)G;Ypy&@FIFQ8fV--PSVY*WJf60j70Y44ql~kGwng z(=3*P?*3*%2B5EP3t?ILEk)9ZXF9=}@hq!5t$BR4@oR=-jvcdpthH+w`)7b`ZZ?v! zkb=)OV*hOAUfKt}UY%mQgR8zO0Hi^fCZy^9rpX__i6LHXk< zqoZ1E!7i~IK;~igbukH6=nxfvt2e7kc$sc< zoMUydMZVbQs z(cAm|{nrO&x}Wo$bDitD&bht4p3XmCYK~}3nDy?S?+tBdez`g{r7_~a`f^+xc7r*S zCun+Q>(JVkY=`HdjuLI!GiM~ae|fh+#ZV&}Q~VnD5|-^4R%WH+e3i>i#Rie$Z8hI=&HBcOWvz)nj;odvY+IDmTxKQ4;@*(em z=$mU{?_GrDm6S^Cyl)mCe`LGb$L=FhZxtVORUvq)ek$RL(uXvw_D*m1>gB7xCB06Yso_ zSI{d-aTpaXhCXMkRo_ruRPHLBX=n&XBMr4Nkr4;?pzkvi{;=09|;(>Cadmo8i0e<2&0@` zrsWyG>>>_xc~kH4XVu=0Wx%B&uYM5p>tOg2Xm&|HbYfcy6@)sbEGnHm5s5?5g z=wZ}id#t4ke-X^%TxxPd163p6of69Rr`+C6DhX|>jtlB*wOWtxEY1k$HQ$UHt!k`l zF9LL8LWYJ2cO&tv2-9D%4&ylUPd5zyodtDp5=-`*>APT$ST9Q39Sg4z(|kfIp1Mgy zB(>JyYP#-z1j|(J$aACcE;!ut?IgeLqmA+0p0cF?ts_pTbv@&UcOIi0i{&QnOHVk(HY{SxAoMg|Ob z&RT4R3# zxA%$c@_fyGwBTC@sq*h99v5>o>(u&5m3SJlH8p#C7NI`1Y&s7#VvNxILyAe^i^~10 zxnz$yDM8!ziNza-0N_Wyo4&hg5Y;prG8q~fKN-}VTic9?KklL22cGqfruWi?h3P6^ z75g{e4?kUQM>NjwQ!OcqdlI~&2+-wWrZA7}lTkUB(h5GnVI0Vx)TKFdRF}(FE-=_F z;qab0)`qLszJ?8*~ zd_6B8S0RkLV@;%1u6V#KSgw0A+ii_}UP+i!wK!?j5|!6b-uJw{JD;*xu$pZtJ~xo2 zGT(hVz^WfDCMWt|zQtJ!5Jd;8=NHF2n?9drtx_w@fQ@v4YqeW3cj>B>QuZ% z=eUM)RNB3v{DrYsd zOmpv`6{O=I$ZC36o(6Rz5duMnn$`=4t6tI(t;udtVXEtco{OS9Q({Q7FJ-)fD;{-UvJZ&sdqwM32}hK4R33z`)$~7)en5Rq5Cw#_=L*g*}MlIGT2s~tyEwI1qBu1 zOrH*k;a7ro_u354N9_tGV*3ho*aXbEL{U~TweoA5D`v~TR>Nm#X1-c|j2-9YB$c0d z);$m!weRjcf3+bnnvdA>s@?=@R~_U~a}2zn`5O2m!iuwB)VCFB7T(5orxuJ8B=kZ`CvZ2A6|Z-*HXTNBnj+DAMLU9j)=P zwBSwW?S;mSCaZyKBXpzx%*x8E9ap=0XWYLvS_(BjKRMiSqvHl&Vyjpp-$k|&0{ORm zWWSl7dJ1x)GfIwG40SH|5_+RQc(df8pndub1MBK&w&&xuh4M}YCr#*sJrhqhxkhNB z`}};9qBfE7J4(0jT(TKx2!3m>sU*o)WwYJsP$I2cxRfv+$q~VaLU1KpZOu1Eag6a= zz+iu5{0ldHUW)`gf5}2FNjFDPFZ~u8Unv{O@cJ|d@*IRy+ykuzu6QDQPT@?n&U-&= z7e(dP&aCo;%UBGr(N5bCl1JMX*qAot-`oaId93{OypPQaD^fNW{P+stFpdJ%La+o> zZieba%?iQFk0#F1uKW=A7u`eu2`6a)JrKw+G)BISupLezzr1JeWHr2x&GsOK@cno{ zmQ?yOpa8(Ximyv?2xl-<$Qq0#u19RB#+L)!QjZM;C2S1k8`PZgCDB(Pq7*Y#s3Y? zjtjJ8KERP-lWySx4BT89wUy_ebXT*nlFnEg(1-KHYr>NBye1E`s1-60cy^?q?^t4& zey<&Tj~r5dSv%J@_v;PIfLRbf5$Q_nHZxG+ui)<%QO?nH>kJ|ytawaf^kVARs@EF~ zU`%ihr(Q-<&EhPFE6HaWZ?D95v{GSUq@e3XW~i1d9rq97bB&JX$d4vyrK#N^g`JukO1{xG)Y6Xqzi`&b%9x1nxW&!iUvaDCVSu+(0r~Je!Hc$kc#mTI z1D1Pv&is=AgV(d74)#^k{zBz@miAWE-%NI;%dd6=k9LNxzTm#KSV!7jYCMuc*UOAZ z|Gl&Ed3rfgLl2)`HXTZm!v?qL&Ny^BNfvlLK5a*xYje^|#_iGgQjA6Als)Sa(?Xb`Jt{+SH$YdJ-jA7aDt%%ssIf(bNV>2UiW7eInayO0MN^V}h~a+jj1i z&YkT7LJaQr_Wvj%KhKntOfpj1=7n{vd4!99X?fB7yM5N1`E?*rfp#qAJ<5HotN zu<~cSpvy6j?%vOY8y^AYOb)$8U?mbNacZP>0?j?#0}^PCgC|1Z&kpA#J}5x|bH zi=uAE;J#8gi^3HLG`uF6kz5 z6Ba&9FAupr;B6A~UOid6<^Y(y_KvpVFy=6{B>oc?`xiJ;?D~c#Vv}b1pLGuxM<*RL zV3aYU(3b8A0L$%zcUh6$)C#)0vsB#dc8+Y>gR@#O)l^(+RcAbL57qpTfk^i?f{VwM zMR`}|;_U6MIJ)#oHqjS)$#D9{lkHiLt+pr(q0IWq_ScRO=^^%HY+!UZPwl#z?gd75 zoUa;D7U5v!bfjihJ3Fs;_%eNktmZPnb2u-=UVbnxi5@%#6JkM1A^j*0e8es0(#h}T zKF$3U7MHkL^;M0P!Qb~0=Kc45rx-_8Orxj5xZxwm6`c>lQsVoW1xu(wRU~pw@b%rY zVBdp!yt99MXcQ)fN%Twgn}KMQnzq9fmyiV>+y8W)7_szQNIDB-4=-K^HK#AT@`CT-& zYvxwqyvAM(SW6LE9$`BTNG!fBll^IS)bVP)4o6nA00XkSOxewvOAXRSid6ju~ZuWIta_9_yy%Q5a z*yjDy4_Y5OisFZt;&Vd}uO5pty-J)jEm0s?+4*;J^txdQF8yF%I)vnE1TwEWPR<|J#46Sc+i%y=-Zv zc642a?Q$V0V1h->cm6rWtNf@wMX{cxZ+05 zn)hzo6u~FchMz{cH>iEi>*yTXd}G?gVWty;+ucLCu~Z5J3836vfO5A=mM_5Ik-Upl zUP1kI<`PpugeFD6cjMdo2&JQy zU8T7iVV#*L|FQ;1RC+Y-pE=2l^s1lt4uvj4SWI#($(4S0K%KHN6KuhujItWT)lj!P z%v`K8uGj<)o3`V_uh?-WNFfL`EBC3aty(|=y~W=3Gc(mCmX(zW`0?bIWG>!it6rXQ z{AJW67VNV#l*y+4T;2Wb7%q^=KwDU5Tu^cQ;1#4`k%O!vGl@?Zvq40wff^#c7W%ocJQVpP>e z_4A+DK9hWJuZ_qgt(XYRZ(n%QeOU0BlSdY!Es6i3qaSw7;&xWF+Q~#HQuh*r)gF&W z97iIh4n`Fep)g%F0Os{uIQWgeiw(%x7wjxWjbGV43LSh4kDEp6oQ5?`m!)UH(tll- zcl3y~CI_+XbA8n2q;``pDPDFAi#Z|t&%~>n|8)|EmoR6=5G-Mbq+`~DMj>9tDSWj^ zY1jJR9xvm)EEBsWV@V;nBLw&>cb^#2;S?F@W(uvM1PG?=(^K4jq|D~!*EL=Mo8_Mx zmF&3YP<;MYXZ-oF{bA^1F-_PVO--7Wfn$_bhjE87VVxLJ?Xi2grg64PE<5?IlI1S- zrwhxsfP}@+$6K|XX6RF9z7k_`-ps>kgXdip`8tNpq4z8OLv7iw@a~RYm;3{TY=!x@ zfr>QyAtxBT9EEr5-jwQEHWutZtbVIw7Af=ieOUw=som1i`+Voly*9|>sQ-|kAv6KI=zCAk< zBdYdW*1^?pJRH3pArc|`xH3p5!#8-Rk!p9MYWb6RZ1{ms+^SujJ3HUz17c?kQRHGH zHA9>2!5pc5S#&FXh~y^m(=CVpF>^J6{R_&(qixq-uzOF}0UfkzHSfz(+$G*USoWcd zSgOsqHAs#S405`i`h7V_?qza6*3Bm9!^FO12U5tDceL;Hl-x-bQy?}GRgsXiC7HnD z{BY^=0RryHq$+=)lL(O?4(Cz6^~0^BS%g1Xzt!zP+xUPjJ3Iq3Ml_i!jp=mHCK``R z*zwtN;gvhf-ib}Uh)NDU7>yq~hjID?$!ylIWQpLf0+I9T=y8xELCTi3@^Rn2dOlmj z@K?oXNQo}KZncRCJIs_%Ir-$kikPHPMqRVntNyS!2$)gZ>;j_&P0@>lj`!$* z;QujBfZ-553*R5cD<3n3rHvZzSgU+~vo&OHuG}zRLN4sNNFP9XApQg6w}O|XOCF*J znO5^6+V678ge4j;U;yR)I^dPi7uK~-rgkfgCu zYaF|E)S9BP6vkh<*?oG*QOiGv*A_It_BHETYxuf%T`tyN6U|*)E{BMKOKPF_DOn#N zTn)Lx_-;>)G{$ZD+p7s3ibEA2R}K}(0K_ln28u@EvdL<6=h~&O)H8Jv-=k0V;jDf4 zGQynP)__&#&j1N;-zE-6VYjM~Wdn`qIQE2#;W-w&DXXq9cqj__bK8+0eafc?2`l16 z8R07|Zr)`&CF8caBSl28TQs@3nt0xIbOscL&9h7B1BzIAaJsPJl-C6xa56BKFm9)E zHrf-MCTu~IWNlva)t5|>(%@~|^3+UXp!`ok!C_qgW%Mp2HqOgP#V_97-T2gC5V^){ zT&QU@Tt%)_;8sUnT*T#2{mHQl^F1F>)y`RZ>Gjijx46SQ$|O9_=oVY?#|=DRM-Bf* zwzan@GRoLfXg2!<+bT6Yf0>C}UCt!?9qq@;0}b-;KehD<+?I1vQkAj}W7?W$pj0h? zyuoH$`rvjdHfD6h;q)KIu?3|Jq|>QD&y<3%DlL@9Rx{riAEmV@hnk23Q4qrOUMfa| z7p8i*sh|&b;D6wRAZ|k@ikRJ{4i!=O72ONVYDOj;#2~oCC3{`sU433VcSn|FgBWGw$QkQL^qkNz zA*!$cgfzs%j#`9y*^_U_qXKrRmveOIs<2?K-;+4kdG#Rqvk1B#5xV3H)L&m3^twTH zi`ACQ&5;pbmi()lBq7G;66G8A?HkPrUN(N(8Z}-f(6Fq^3(t#Bjn*Lj zD!O0QAAxpr8@sq>6|dQ?Pn9`CA=K|bImyn-@0V0@tFLUKiH7>B`v6tOh#DId*zkoNF6I8skDWbHAY@D`?j1T;K4_zPBPB>li`R#tLc3i>5GfsrYeyKN z|Cv`l&EwN{!%WBwfp!S`l)ni-$pa}0o~Oe*4spM}BIQxObc<|-+<&Fk3)eKA5ub+O z-oO0R)dG-mLVqIq5y>tA4xmIABX|%6A&zp{Rfa!@T2aNRp4neX7E_%coyBq1b|7h- zEMAJuMM$c>;50&cH+7d^M!#zp@o`9#hhP$n*VLr2_vH94Qn#4tVA zmSjiY%I6e#Tl)E|P;EUDF636dw9~P9v*Oo=>cG--t9LZ6K;(D(ckF-9`0TsRBM1$; z3a>bBmC=m?iX8l&%$Vv}Uz7AyzU||DtnyrtD*^n9gT;Ts+h&~~$m|%9q?6I0MfV~0 z=2!G;TeV>qA#8Sv*ZT<2vkFP#fAA&WGzV zDE(ANMW3BD*#`Rxq^!pWzNT?M;4#V_V!TkaN2O(dZnivSO-C%a)ggA5t7M(z7J4A) z*eWMcr9(^SP=tCurA}7}ZWDpJQxQz|rBPx_Bu`K%^~inm44SThl)!oBk(3Hb>5rz- z`l0szp!M`z)*k%9_z?B6)0oIFg5jrDwqxaaEIOoMw3L9LX-Ay2OsHrWc6I`dDzu9aCHp3kO z=8u}=QF1BECP(N&p`fq2DGIwC7*o(Ez+ zqlMYiN7n7@nT3D(y}tH7q&D=-lu}Wpo+)1IRTJ~9*xw_@bX9?_W59dEO|J`Abr)pR zV4!K`9{;_?dV4N>r6^e0hSeu=eP`!!NG zP43afM|U*Otm(sOEGG^>o(E1GnG}ZcumdXva-E4P0ZYSj34;15TfF=esXl^)N#7x7 z@RqPB^^85x(c@*5(WcNs_m|H@nwF|ymm9-V#&T-XabI9d0(e3SU&biy;Kz`gcj3u| zg%+YwkU=^D?35jh|JZEzH4oDWuuZ5Bd3NSwF$uE{{N zYtj{3Sx$0jpyuieAY}cUJd*7Jl-nocO`)wGP}_i@+_jXQaju!Ez`@O^sw4|8ZPZPq zmJ)S!t&>vrQr%x7%X2AU*fJ+MJS|%N6}Lil`pe}nz7JKR&!6Mgcv~C@b3{zuoIJgC z)QzGs4(H#7KwZ3#FqZNRg>i>GX`#q^R^jQJbB98f0^M%Lf6Km(OV3({df-F~k?8hR+M(or%4Sw-P$zJ0<-t z%x=XG*i$~PiMRK$V`9=EU85ZPSw*jx%HKP&bLEpnXKMHj_U4Ppm&1+x)w7k!JsBpa zrUEBu37f0t2B6yFR{!OKYH|j(VM3r8LAvKSa1u)q=D{C&6(6N%53josvYw7!Jh(1? zZIlol-OYf6p2~ej%jj zmo;a=Qz+vpwZx!xva7o1?i<)vDD%Bh#8attkjrZiUaw8oPH*eic3u6o8#CEk-2UctrdtPXpivMQ_``1`j zanymLI39lZk)~R;vi*`Dh1a^v5$#J!u#z!e|IDq#&0XcHp6fn8K(5)Nh+o65ElCg& zpLKPkANpSlTI;%~s3}t%Nk^1q$y8T9Q6=Boy7bvGVOR1SqiLj{(dM-6Fp^6I!XAPFT+bF&l`WQC)x_43IW&#b2+ZD;7>e%!bS|PakKZJaL_UxcFObgUP zq^*(zoF3NClu3%{J?s`WUtW^i%CUv3Q0X@raJPtEy9k5Bc-<^S76mf>G?)y9Asy)= zwe2C(eu%?gpX!aPV39ny;yF*-%Y1vG-NJe1pC>^NM+2;?bPizGW0THBSfrD-!bGkU ztbf!}?Y>)j5?IQJJ4RK{{U3>O03le7dwH?n(wc{lp zV`1x-d>3--tb(WHK$GxY`O8d9j_wed4iC6Fc`&kMDu39NOf0_&#p=;Nie(Ypf!FQYQMqP;6Dw-`oM>IUfKI z{Y8+W8U06t)e@-_x^?uS3@tMZKIemktLEREy-<>JqPUKSAA)Mx!y8-~B^T(KuA!B8 zJ}ES&POwN#R|bB+5iZ}7c^$hY-eBkx~LO? zLI-21<~O#kbIb|Md8+r-uN7t4brJ2mTc2}9>F55b_wkY{w;C;l1;0D0La$s89ut7~ zt{!wT`W5J4_U5pT*P|o{-?7{;a7jr_n0Usro<&aC9r%jj3Y~2MFd0Ck#iUb58AMt< zzIO+ccNoTiY*Xa%E;T!9o1|>EOvxn%gVWf0c;GP5r97Jv7;RGXr zwPmLR)GF!oRn zJIa0rWr?XfO}zu*yA*n0!d_sQNbQso7Pw?x2N7_gsV;F`PjIPU2e4|#hkw0quIdkC zcUa_2vpUZ`FIQt9+(sSSc-kbp5S?if+%``Dur8E&=V~)fQ;5N@dTMmgO7?|!K_sKm zy`8ZpD1z8orxc$j+2?y&nT<5DQ-g0@#eaKpVZU-@C6ojF4T(%Q1{#!8`VX> z1a{%b2BR@MTTUcO;?f>PM77{w7pcVNG~a%}$oo;O>uzmJ{B3`mmb{U?z+`N)y*68t zs@xGA#_$iXk+O!L7sfk-rlpspSsX=241^nLi&)$}v_%+Uu%O)kSzb-o5{UirLOXd9 z6vCcB#-Z(cxIPQuHSh!2B5T+vGBaom(b0HJJmZX7K~Vqs-Ic@t@1SRdV?@&ss2-(| zFMwt)6wMrimn7J*0DJsN z_EV_Ss5&F+lK2%(_WPXeD({Dg`=jRq+!Q8ZI|@a#;GNOk>X)}b_;Lh-@ZIAxk{k0Q z&~GF*zJ6h@4Ab!MSA{+l(!#p>E9uqsIe67&jL+gnMG-z#v> zMHgjV6bQ2YkW~MhBodQ+AL8M@=!mhaU;j?Oko;Z(GJF%$L1(9ZEa-vt_T|3w6Q9pt zLff85ti>bpab=#}ENQK(q-(C(Y*Rgjs8bccQr@Hl&1F5p{oE6n_^0?wEYb4)AGd{? z_i=38uXV`d?x(k4ob#^kJyx^X+|(?G1hWEv921`sOuCt#Nq{_YD)1vXOs+jv7#~HL zwcuN;I2?Ej%JmCL5AO8 z$vc}Gt9gl?qs~4b zFHE=&gS~Jd9Ev$g15;P)pYdgl+9!CTr?0b>Irr`Yr3rG%W@4>}TQ*kIQ zbU8pqpS|F&`Q@^v2ZJ+nAi?e^N;S(2hQ|P74UlsK_)F9;77@R8Ubl@IWkCj-5KW`k zKo3j;LB7H?<$Q^0zaj)Q3<(ByALVjYefTCzN6YsmQ4L95pex*+8`l0`-q-{@$H&<3 z>y~CoZgwR0iUe8@h3zx>s%(|g9mci^OtF*3l`)>aRD z*VaHNCJFLX^cW#1w5mknK365O_XT$4F00ZDM3;a04Of*H^|ZXA<&`hp1~i^$>sTAO z?kX{u78_hqYPlp>!;+f*l#y*Yj@N_(diM+sY9II6Ji02WFbU0SXv=5xHaLT~Qi17u zQ}Ucvv_3zlh;O=>!_=?v4`rW2DYka1wf0iLB>~tv)T%qlFR&pZK*Mmjp{^EOYwJJ7cJ<$0<+1I$~R2K4_kvm&2C=46nm#i2~S3aL^?S@^1X_QaS~qFVKqRjjS-G}>WU z4~zg@DH2{S)^n=|K4S+bZTDZWhoZ4Oness%g>TsldpQOSt#97EnI<3Ve~;a%hzyBR z&!r5lh7^=PE$crXUNQXr>D@r&MpGAwabd7#M78E6&s8^)NBE)r-}XF0N0cNGCO;a^ zr^H1q)1E*;-1MBbS<{+@ z`jv!#q4bCTK_js86hP@)Rt`+`px4ce66dboKQ`pMn*FZqefO?-w;IzY5zwxvQ%wyR zc@OGa6Q{oIBVT%fOQs`AZ_kdb)c=}!yh%v1h$9>uP2>_bu8rsx{@-aET4K(=_dgm+ zDe7fY+Q?c`i-j{F_oX|aOC5i6{&H3t&F6Rcx??izwYa1AO)7kxW_1SFyH%T)dQnQ& zVglC2$Mt;y0KK2{&(XJrYwZy?J3FFWu5OUXv)uNJeVSE8g>_5KDqxu6oXk3ep(&s6 zwVLlw?u-R!R-QPc;78#;jlYiG@SDA>%|hQhDHc~NC=Kx)_r2+I#v#;?dW`eFI|p1u z4wd7d7f7IcxSh$>ZlkTk)N=vHeqpMR)Nwm5;D+v~I^M9NAp{`pNC-{;R=;Q3H#zlE zbrU!WTWlKjnSRK|ShT)!WtPqAUMh)kb}rQOq?q%Z0hOZm&uKIEDJVwZ&I2P+&vm`Q zb1?Wl;h4uwV2q+(grTCOGGRR`mfXnh16|tSgp*RaDyW}!1G8rI7l82R>$MxrPj1}Csp>m1;+5N1-U`T69LXhHoPx&0I} z7}DxvOvL`aexfn5LGBzOL8DyPg751)MoE~v!P^zVC|^$e*!kwEFF6ar z_D5HQck&7;?MWxUsYiY>yN3@IrsAkoZ264LFG?zp>Mp&lLsv?Wawk5gsJ6*Fi9lJL zODNgoRa~YLed`;2(-W?H2q)eXN&G zRK?Q~%|WQ&x;_%C7BK_|bla;_9e6`(FEUjbB)q2owyy#SRp2v?4_) z*V)VlGR)nPgB7uxXhamZooyTL`}HTm4c)oP_ow_rBm-o~J)2J$9&ZrPg7+Q}9&p&* zZW0%?%+Pt_KP+($#CAZ9pWR_Qo87LVN{Yd{cZSh3(i&2KAe}9c=9n6Jo)Pb~1x?r0 z^QbH4ZfwH=31X(rG{dQ%OR9eD7clv)&tEYIWm{fOYo5@Ds|ibK@jeDq+MJQNV=Nw~FSI|*I*qa2SzR%K=7 zL1;U(b0M`}ut?1x9#|x8>$FtB^h$a5L=yPvY16&l5#RA@Pw=k5a`_4ohnOG@6=a5l zRV+1#w^qf*-$s(s8LcG7DQ=FG?nchxp{2^nYto4;J2Ifvf1^q_buC#azwU?frHZ|Bo zg(rCyLo(ws3*zQS&8wDfeX@eizqW;;3{c2|q8?FZ-Hc&eJ(N_ZhfIcGdiV{SV^lOB zZ_Cl2kHz2N^q5tPUA_k<~s`}EVCUA19k_>XIOp+LXMd& zwNV}p6Fuwfyj;5=@CF3Tjnp(*sxfA)b;jQP;hov7u??jxs2c5c?H_?1iR%o09vAv} ztuxqvjr2~H`$I{VHa9@x{XVW|>?9Y^re^5M^_`y?+^HoRFplCG2H;1k0R!kSgb-5U zZ^XsK%*Rf;e@K9E;aj)RtuW5aQRBzE!zLab=@b=_K7!KHh_Ca~`2 zRbAc}C4u7zzjb0#6?N8uEHP0Rb$&gXQNuy0Di;boj657TWWRu6YRr{W6X}^a3-6~i zRC(-G@WDY}@|@8#oX1I3GippoN(k{Ta4Yi}u5_=j9NR!sH*<2kIBswvO}eI4+8?RT+f0%JZ$FonsaejAVSsL6oY zQtXp-8v~l&Vo$(O*BsAdEUbE+{U{TBgQN8r;YjFa@{8fvVo7ESC6plpC@$D_*2c7? z9IK^YK)yYjJz>cr0(=aRAIXtrj9P}RLy-Mei=HPxGkHjI^r3Y;H(nde(JNBIe$oL$^*}n}( z{s0ZtQwh0=6QkfsZ=?h97Ol9%2csc>@IXO~4x$L*Rp>p7B_^E~WU@!ZI{~x!_gK^K zg27E@v2<_wx>f3}sSjpqUecFc_%Rn2q!lY14d?7eck zF>MwM^ZIRmY8{@He^&e$-p8pNFzF=xHW@2~7_8ocTu7nLGd|gFK~@Ewf?F*#gB0F% zwk(2c<<5F*gM_53Ks^gsI}dcN+#BvmL_z$9eTv3xYu$C|g{#W@NdEVO8YA+%LN`wb z+^H_>Qb=T)em~pFR=ZGMo<=(bLa!7q8^Kd3o-hnkEg`I^5*Z(S-!gHOA*Mjn=yhfC z)1EeDvWSF->WWSzH%EF{$k?~n@to%UA3seso(4Gb#UJ@Fck5jrD*Yj_-j>fD9R~Kg zs|Z=%S|=rok2znwUPT9qSFL*a_IqS(EfrY6LqQc3d1>)Mh#S>>1Zo-)Yj4kEVv<6- zLNQYg3o;!P5!hrFbtZgU$F()qg&z)jG1FOHni=WXE%bCHkYSF(9 z=^z?+gsW_mSC`8LUpXm(m)@KqbJ4HPtEx_|^a#i-)(n3rJia;W8o+*M<9QrcSF z3vIdIXl*LE;Ue+jm(+-F8N6>w2=!1321X^1ZdlE8ITG5KIC(QF)%>9_{&nq0+sJ)j z#17;BBWW>SP`$X%Eql+Wq%FIMzB~*qSrE8+$#0g!7Jq7w)l(&#m;TUtVmcDo(-N8W z?4*3L?Ou7n+YuEJt)`zfiVm({x9jO)$W`3w8U#BMKr4|zhIWyF;lrsC!H3d@*VVgT zxfbei2OMl^YSafCO}K%GsG}P-A+^jid^thoy>QwAFE}!@Y@bAC)tt&dF);KQ2r~Zb zpjpheMS$;$0J2Vis|NIpwO}pedcxp(BKQsc*ITN^y2)Dw-(w#H^e}Fqe8F6n2IyD5 z9)G2T8n|qoTnn>7m~Hvf6IEH_!MJ#YhlX?_wQ#6?uGDfW=+^N6`a@LuYiMNxkZW%_ zUkcPTma807N-PUr(ogZ(HD0M%AXrnBT+iu9Orrhy)(F2y9Y(riGz&b=VyhN%ETv{^ z2~9#C@~h72P65ajDS4&i{ahya`P$HuI9H!)uir<)w;bJqP!f4w$)7eJ^&~@*j8--W!pg|k9`@drK7dMtFh4JXV&eif;&Yr zlU*u?);OjO_Sn_I^)ii4fqUBto^8V=TZ9V7Ai}DfE94OR7jRfR5=9TOiVTlnH)3 zv_}rB>Fz(P_0br(e)nwV^~@OpMvF?pie;I|oj%>gL%mJY(Abo=40wAg4s3tfc?coA zL%Kh+uDsN`L>NmrUSslT^K@cM$>`&x)mQ2RV7UZ9bB}9Z$7QKKN3(o#ySu(pZ@3n7 z&RI2v6-<%fbP@Tm4QL{4^SyOqWCGa?{5f4dF*3-_iHCR}Y8LT%c7YVOttR;hJT}h#O=x%5s%gr^OH-u%}!DtZTfMOjPY*Vl|9Ky`cDXS2Vny zx70r=Z*xe@GtGv?R{Wb#;b*#$q#^S^J0xg$MPHek@sm4yndAnRM$mHEGQSsY>F(-G zGx!kJ@2wQaxs;hqh3*Ksg83>^P36@kpI}}Tz69&G{UPV5zQt&6Xve2dpQ0T0M><1h zIndEuWRXt4X}#xjdLz8OdbHlv%Bv&?8VW?0-m0k$l?y(SWm~$Zgk3SL^KAtLi4l5Z zu%bqAlJ!Ji@5Ej4!S;d8$4=nFs=W{~k%}30B5c;}*G(`e9W))EEK2y=37$;zH4Mw- zB|6)bHb+zf#CUfC$D}8zh(`|;(`tz4#@CO$Yg>Nc<>RLTl-<1%HViX8;y3e4J|^_z z?**w(KtsN_zra1vi~3SV1eFiUX3TnaG3dl~iB5B`39jJCr%S7?Y878+<)q|{Bt0M1 z#5eBP!~uRBn57;ouU8ncT}+;W@=e3AO5dquTiB%IySA@J3kc6(_eSjglM#O-Jp|m^ zaW>xor{x8vjprUl3#i;#f&d_EigMLSfZ>D|`R<_+gDl*=7c%c*`L8A7(854^DIp$fU2 zjPvlpbHgjsI}vJNI`H;2#Lq0_ms(EO&rMePC>X^^;vi}&WQ{EySa zZUFXALT6MJeBBLbiLxT4$vKY4sw^XT@nbrAA-H#-YfN8R$a5zmSGb@$8dH^h2)@Nz z3jF?rpQYmgE0$B1RaKy`d8KlTM5D-1{ukb$Y9zzXdTi-G3Ea&&?N--urNld1ZgGJe zxfAh?apCp$DgWWbIA9`MV7dJ~l=T2<_so#bV9OKH zV1|V$b06=VXB6MYw)YK}n#60IGXQ1|y#$VvOMgof<+*3gX_;WP!o-ojS^ zQ8UZlAz&|!xa4L}LQq=b)Sqr$XtY6_2{oV%m^>cb4%F-j0sL~CZ~iNA__uoPX6JM} zd1%y(wtem&q|xaNg&RRCQ#CmL>9`I)rWMgHFG%%GG_QJzn}${Eb=n0L-#i3k*4}Nl zNB`G75)U(r{=3AB+fXEZ3>wNh4fsFSck)%<$ZIA!Y$JZ(pSE6=>_JnDM>KxxJ@k^o zx&|P`R)+Qjvol`dk%mc!)!rfXSkqWfmIV+`r{)pB@{J?8$%l!ETV!m-HnfE(`$Lys z@=W;dY$+kNlmFsJqfE`oc|S1B-*}AWi&+WgF4(uivTPv07MNLfDE-<(r(nxMQ0Bt( zKY@ORD6MV}6dEPXQE>gmjCWv6jE2WmE^;Tkq?9%XD?&D2pvOj?JxPZx@izLnz$TAQUBCFFZ(*Ps!5K$sSD$q zztlLdJ@a!&8@gF@Z=OhJ_SKM1vLMLrGkHl)z=Y<`WbWho!twa%Xbq{Qda5%LMJ1_b zcRuXnKx%;$gIarYvPU=hBa~vi1`~>()^;KCDPN&%XG~EzX*}za-B)(ZxOMwpb$4dW zf5`^?!GcNjtZ-)XVH?Em&)!{c_p0v;L21*$_JlFWAN5E>G?{t6hPE8=0(XK5wL5@JzdW2qetj|Gk~B#*jN{qh-uPi+oaLPfypPfWZj0#;SC>-?+Qs*Ze;xC6 zCG%U2<&$yq?}cBXc}lkz-#f6??i*fJd;~}jc)p$QTF9HlP_|Cj!I9ktH(0o;$Og(@ z7|=){wOb2%xBF#G&-N5us}@cpqH9k?*FwGbD|d@ssvBuyh!mq1*9$OG^2I;ko#0L9 z3bvYH0C;Cu@&kDXf~*8NHL^X0Q`)b6onC4j{^}tmE8-g~M1I3S*h`@L-!Ns0XEWXh-xdpEoY|V5r z!9?IJIZ8zQ+{XhJa1H0xFgFDa%Rh4qe=zmAo56=%V*c(8O~IvgqhR9U4W5`17y5mm zOWNI%+x7n1uI=c;BYCtGBNZCId!~=$59kGio9qc$r(O9 z`T;a?l->5wsNSHVY-im@ndlhMzu>wH={$fjhuZ%BcTIyAU^YxZ=Wz^n05YCC^7?qB zl&*Jy+ZVU&PA49okuO899>*3?zl9lm{@Pje5AGc3o<0?p6ctIr;`Tu@a*Ip80Ng=5 zCZSu7Y&DVyD8{hh;H95ak2K?RZp`P5UMSdOi2edguwdt`H(2hclsV=Zy!qu$YW+e` z&UEnoKPIZrf;{+}R3Nc|A$+0mU8>BsRsVXz;W&I@`KQpY5SH9($V)>bPhsSbgB55d zT4gqfU|K>=rSb~?1xIwjkiUmS3f)z2?AE|U!a)Y0_D}wm*bWq!RC+OqOH}O5eLcJ? zeUhvFWZBo_q1VF2pvfs*GjOtA28=D(F%f@Yf_~836RKUKXA+BKq`G*1?{uGpf@#3q z8d?j(aC#NnZ=q3h10l#YJrY<${r~$CjS<=A;8v#)Ed%$8kJOHfd%5jDU>$-x2s);U zkKzB<-gkyIxh`uf9fE>N6$I?`s-SdG5kbX92pvI0KtYQ1VuDxzX^N@(Fp0dd5##gs`|$3mOgQ z?D0tX=f5KhQUaQtvd}n+q=yEl%GiS&>>q^()+}SmQ^W-$PX=XS3+07`lM5;ySsC>B z8@b_uz{SB*zKD^j*|v$=s)J_mSi7ptdE9pn02K*uBl5%6SB8v(^(93eyXasXEsO%c z%0-F2SX{P2QJYA?&BA$@6ptHuaH`2j72ROMWLdze&3a=YQH6XIP+4RL#v+w^7L!=;0Z6~wlxW9VAReV)v?YxY1}2Um$H z8}kWM9bf7=FlMjt!E>OJxDs7FsbG_8sP+L8q5Q>p&540FTOcQ^S{lykQvd@1i}}7o^$XnA}A!sbK47u%q%J1f;@u0X49kFX<%m`p4qp$^*km_ zJ&u0o!>*DUIdjGXCxv;iO2yxfdYh9(pr1l5~w7>*h^~S?aPex zm#lp!YuB8!G|OBR@_2{7Xa6Cs&i2%H$43CukJS#J8;5R29g!8GBDU-DtQDh@jtQapr`is$h5d5K~=! zg(DYI0*AA8ziBG?;&N0JVB(A2+>MtRxRn>`OFj|+E{f}+&@UIJmh3TKUkZk?oOfj! z@gScwlc%xZ(%onj)QCg8bC&V!IxwC%tU3khR9UgH@wkzoRsVFI?;c&d4_$8eOg z$JZok|6KKg9EXm}XJxqLtU_5gMyp{xoP;GyH*Ng@?jU3N?vv4A6Lf~)OGBeMLTgo? zU@V@mjUF_YIKJB7{v~72z04FGZ8{}gQy*ZPAwZ0yt;$`5hNDU) zTLovA;}uuFG@^q`jT6*SU{(dxd2x|J3pN_e)2nS@kh+woUH*?H)jt69-~B;-l}?ov zYP}Z6*#!RwKiW)_#{$(<7Dq1ALmTn7-%bo?pZTVl3rZ@m6VKs&A1mGIPr?($)xhS} zF_9ygyFTv#GerH>PE=Y+slXUfauZx8HrC-)?+g@3 z->GD@Tv#5BQwZCI7|jVUXcJi$^b-6uR-o{zRJc`;q(~?x z6@ghEccN;^rlfgf!<#U%t15Rin|AEjp%ldb-bfy)$P6y1XEz=L^nrj}1&U=3S@ow? z^bgnkL&g++XXY;Op7@@Akw=4*gstmcy``uYYq@$p(*`i;sb{S$e$HV17bp5_$DZea zw*G8Xa=YPwjvN~={&5ftn6BMviS20AWZ|;WyK@*cjW5nl&_mR- z|3b)Sno_&jU(?m8h(o>d%yK|;n7kEE1LF#UT?FH$@77Zguz%@z|J2^dWiYY-7>lx` zf%)05)Fzt+eezC!u`V-*+}O@;lBhZXHrhNpAbsmyAJGLw7$Z|U8^6->#}}<-ft(wX zIm@g*H$=b^d3Ai;E>6U`(-tMh>vOGxfAwd5|MWFY*CnK#|RA1#bUGJsKO1*>U=iz{z^8Jg+9 zIA~(Swd)CS&nB3}zp0^dV8Mn<{UUaHMW@7j9mtE8K(i+8nsj#?dXKTGm9oX*h= z*XZ!Yt=FtN+ZG)Ft7#wOTpkAgc==Md_)jab|NOoG&SU@OYd9ppfCmk1W{Ws^)g`Zu zhF}Kj+0*32;vOX0DLCc~$;fzey5;ik-VE>vwp`wRy0uo89&>{2yhH-X^J^+;770Lz zv6RJc<)wmR&`4YE(;{Dkl_JvU+Px=t&x66GThPX`k(-9c7`M*37F9@c8FZ`ACU1mp z@4Rl0*Btn~pcsa{0r=&A>UaObpUxTl&LloGu(g35Y*J;HT+NIF#pjyH%O!_e&ms48 zVxXZ4O)<$#6`CDDqRf1~b2w77Vu@47&*{3Qf$6=XoER*c4s7u}2cHzP_|sn_6&?rH zalEfLKMMu&Emkbsh4a%7#w%uBYS{xUcw0k%nJnJYBD6^erk_&g4@?+w`QgR%-^3kd zTz2X_#B{^M^$_F+JgzCyD!>k>9}Hd#%O=uxygPD(vlwHZ-F$2c-jVctFzwlV(#?Q>&=Xz zt0PTSCxFn-JYU*y7mLGT;iEHMfCwW?87V1T=w6$ph90 zL%nqIVJWl))JT`%ifOyL8y4^@Z2(aZOJ?qG1#wW?*pi)C=*Fxo{B9#{w5{IznKsPs zJ~7k14HWDkJr6prIiPav@3&k`S3T!n?N$`rP5@kbF~>2q`nDTB1M|h^8D5t?x(*g5 z+WF$}>1-D+)BA!aK_YV@&;Ioa=dXim%Qn!&2CWq~vBh6sFTg0$Nx%fHm3J}{l&32O z3xgNS(S^`nW>$(N;fa{A(A-83yO{9i_Uz&p?OP1#Id{~3>3(0FdTC%lSeCq%3BOsK zFpy0=-sh;5a5SFlJeXRyRs~-$VA&Fc?Gh-VyEQ^y%A|pLv57ym7vDF}+tNvHHT}K3 z_n+ksApuC`f@_WoF;$ScQsSHzIFxPtO;f-Zx6tKi6tvqyC{w-{4AOH$86%1Nahqxq zGnmHyxfi*JvaIdlb^%ptkd1U>jWQZ=UgOgV%#XHOzlbtK4b%3bEW4e+b?-j|^b* zxg|1p6Is|r9Ma>nHDxL7I=h=ee~fm5sH$CeMv6uJ+5!inJ|9x&S&w+L|6i}m|JuzN zz=dm~EAkbfhYPT%K98ZbGUcFbYOdVZiL2vvMUKFhv-_iN&g`=(^|Bnu69Bt4j6C-{v0&<(01-i+-X=OH*c-Bk?% z`2$k6EGvX9-^(wYwkhXMg|GB1Fzrh*-q=m7BLX7&;nF;mL)y#BJN5qB2mdZ!e;k+( z`85mh%S|pB3Z2rr!|BQmpL*7&8uiuWidk2nI4EtjwyvyMvTD!<IxK^vqmNu_Vq4^oj& zx(oZ!kwNSKe!YL?8QduM^HdIKCV^sI$;K~>$7Pd7+l>TJ#a8jOx>4A|GB9!#TSE(T zQzn)Ndve<4@}F3@n*80LJaSEDy}oZ9$|~IYg+0#nv|m~`vI;tPtJv`A)2GMpdupdm z(9LiFX9XE^XdF;MxKOR$bbQXo~?X zaPU>Tcc1(U%jPFYE5uc3GR*UX=4#gZ1kfvYH^Or_DRKqo)_i6KH9MqQ{sN177{CM3 zlc6sA(pvtmum1ASzrJs)tI$XO_=rW#1AE5jXFsxuaI3m7XD+z7-uNdmZ<1!dSxSi& z+pg2WlHCqVpEp~M(Uym*rVB6Jy-fecXr3PnvU&>_c}y$KVik=_I1Of~s_&NnSP5>2 zG2Aboi|}QW{3@V>K2KW5_pMF_>q{&;6x`X?oXxgUIIjWCw+N#xda$V4z^{Q5EduGC z%^k|*c_L)VP(y^VDb}*zU%LWw!J;ZJHMma9K z+(HeeE}Y1AT3I1p;1j86a6V|Ee6avZDq|*ey*V(Mlw&{jl4skv#`LV#h+se0owNK^ z&d%iM&p zTfX4SNI0te)~+n~&+d3rz$fIzxtS(_sk*v)oP+$F%;MQ-Zgb(pBL&v+m*&ii^Ix;^ z`;Jea4Jn~(%UwcxFG%IqO1-`%yx_nWxV;&((JYZ+Zqeoq8m$8s~&JGs@S-3a( zGPixMDyV+QDTCWe^65(d?3;x^wUic3nwD+9hB0LrxqL{;O}5qLrZ)u5BK%sT)5Fv} z$NSg7*v`ZGVra|et!{*Smq@tK;|;l-ms`cJeLR^exBL+dUH)~@|676c_luDlpwD!u z6_j;rLuaBn>J8M*1$q1gKLP371_rKuV4cNEia&Gp^A4au5%0P3_I~iy1iLMYm*)Iv z=)Ab0~CY($Ht; zS4jFeNLbWcaJLsDg~MdxyPw-5aqTovKCn0$vHYF;txc`+#^Q)q7ruzS$Dc@8odM+b zN8<#GO};p1!Nuh^t(W5QWTyPxR*f6n$a+XU!#IncE7q4ZJ_B|}IGoh0r(n%6dJDf~ zHmFFoViofko!X&$ON8K{M8EA$E29zK|CUH48jLWQ4-X(Feh;MobHMzK@B)3Eo^Mtl zr_WHfUNQ?_EH;zvaT_Dy)7S@H8G5enpF6u30tW&ahiM1rkiR5ieUcL$Io1WiO~VP1 zT0{4452Qq9a2>?`m6ATQp#H_>LYk!om%}7%M5cgI5GdW&PK#|x-&DB3bU5Xr;1LB( z`vdTuZ~9SLuYuwhWoqqT&-nrNk<$@?Je_Sv?msbFC3c}M-nW&JuuRxud-^gxvCp>; z`nAY~x;Rpls`T>QbV|@QJy2{eu7hE=cEzz$2i$#Ap)(ltYtxC!=z!%(k zXQ3_>b!SBuBu;{P#NR-q01)=+3Nb0ZmxWJZ?fLcuF0ss7C(iSFOjStfe~?B@O|2TI zwpBH*zB^*hU=)MQN-~kJ{CHA}@zJeq^R~>dE+$?Q&9<>l!$n2cY3WRVxEM5JrxHS@|c`o(IZK zOjTo_T~B{ks0u28+5_G;AMgzXY48BY*mYY8+dzV9 z<~G#FtGHa3win_doR4P)7g~FSG0+4HC8KSkA&o|T{7@E3?MQt=y{Dh=D`E5VMXX;A z(y|GXRy3cNvj}u_>SN3z2?l{nvg~23A+bJ+J;|MW^J=e}?!o}^LtWU9*`*xIB%arv zXk2pBfFTcRI;6BiY*lZxz7&_W<2-nV7g1IJjQvOfUiMmeGwaH`x$}o>0>GxuQN(g9 zp4|SWq}SPQV?qg~2hb<@PNyE;KR+$<)}5=i--{u~0h9T>wFG71?+A801nB2$OOBe< zVH;@gHJKbydG`f^tI<_qUYw%#0n}<2j83C#m#^>TP6n8&%QeS& zFPVJvs2^48g|hYYD|dw-TG1;Bc5E|~I!R1@;lq31fPo<_;k0$$g3GbI&@S`26)>=n zzFnA8=aDU+xUh-1c%ihHVhycy{EhZRjSGxhv>5KbI26js%FmnfI(xjwg;!SFiy@>?>)1{#>zn*s_~aV5L_P0uu0l(c#) zNbrv{@1dWc7EN&%s2%WP*h-u~X>}|w@D&$(U%yL&ea~Qn5)cS_tZw_RIU*+uCAf++ znc!L%5Lq&qk)oCProMiWvGn5>uIke@Cr(MZ5|lQAPIFr#({3NAW z#t}{?RK-VDu9b?4T`hdN4Xe44z$E4jIlhj*k6&k@%H78^Or=f`Q&Hg8HVkL^mmHq! z8l7PC9F%s!7A884lRLk%5R~hc)}cr~I5#c! z#{EF;k0DlNS6uMroeZ#i`CIVEP30eAd-IM&$H+;0jXty6sS{d$*SW+l!`tufCgc#y zW1ZxMk0G3eGc$%KR)dzUg*NjZS&Ms_eJF%;e6p9X)->a|t*{G`Rg1YX9%+8oA91GC zq9hqMI6c(Mf#IWY=o1R1(YPr&59eur~XO=BW`0)Nly51!Pcsa}PC#)-O1fxT8hVgof78;1k=q~^D19&}3 zMs+Bf8t!01EhsH7e}1)LPSuBNSzX~)uJveRbT9lUkwlRH$Rzz0`2>!w<%1MGI_DzKXg z##tiYH7OE&5_ugf*Nx=}cX|UTS}uXUHY-x-TESkXp-6ee)X8!;1CGY>g7Ds@BMHXV ziDOG!&TlVgaZ`Q2_mY-iovatW-{eUp7B~Iqfq0*vgK%3!2rOCe)NO1HO_`?C9@_wV z?t@Zyr9$H2UJ28U-d3D1AFT9YzhTdPv?p23p5hbdIePTMDP7M3y!7$#Y*rJ`$$`%5 zD@S!Y4MfZdg-g9dL<&ce&7j(N_Av{U>A&vMkRDXOng?OdpFz>xH#zcE399yCOlg3M z#>Y#$^NS6hOVLhOW4{-zUArzEol=6r-*p*~d`ri)&mi%T{-|~*xZ`1FRy|M5SBSrJ zcc`6cVx3dFC4IUPQIb(S7N79!dGLGq6v6Z1&Q~I|lV^Mpr`N1V8RpN6k#p9{t-|I^riX#jaNXwu18iZ!$H$D>S6O#BF4)D; zfgJoLG^}gCKSRphcv!UN=}SYmQw$XF<&R_$Q^D@N>HCKJzJ0UNua%-LPC$0Qjks3# zvgZLWn3L*Hz5HCKOC@+4O8hFWZPI`He%myh^P0lgPH)oeD20s%VgLyL+sWbIcucui zHQ$suzxZOL6m7*f>@mh7o&%TD674VASm#*}7wI{H>QRYyZ$Rk-;Q;}@O8Qep@a1tj zrj;=Dx0mdsxPmTkXMn9atyt4;DlIl&%V1A9)hK(je``yCfzm-c6g$`K%1+EeP4Xl@-kB@WXz((bXDapiqY{#8eJA0QNRyym} z`zCz$;nfCZ;Mv%qTO!2v`gWD{d=@Wr;Ej8MH{$yh5z@X+`3s+iN9b7dgo_;Yr!mAc zW()Caq!Qs;EBbq0Br!vAS7P;D*2Y-S!MEeVLl;W^LqkvS`jjrPY&k%2pJ7GszcgcB zBbKChmKpQG8D3R`4KOuN{o<5nF@}k;WAsL8Z}ydCAzRadTsYQkeb{PgSful&VwiO^#7<}QIl-~8V5Ot>a_BECjg11@)S zM00;e0KJQ0aj+L4nkbsCv6b{F3rU9ndVe1ugIjrJ_#(YiMXdY8Xie9r@aOqABEE^k zQr_gn#{hNYh(9fk7w~lJsg!c?18T+lm^ff%4?cmqHnxzb&L6gg^SF~JhG-ah^mF*H z%RhKNboo!?%JX*E%V1lFWh`E)TLSLLk(;TzluF$9J!4po!fiad{{8H9#4n)GN4Ciy zXBmE^uRd8A+omI1pEo8Jbz}>dt3&toltcx?!P&1g;P-OU@{fZJP7%KiTNFy(Q3X2# z^Blq)EMxuh@Ttlwo2Y(1t#BlVP2&37fXlI8Sr(K#O=Aj79yFS@6P7}@UpYz+^bcCd zfnH_O#f;M3x^M}k(9`=>79|Ox$|k0#M)D^0p*RQvy^H%vV?seOS9A#~zK8T8iYtNS z2P%*7G}hQ7u0CrWiyR#Hpt8|=?EQ!>^A){Bgm7rNH5%8GP;C6Lp%}?BJ21O`vUZQP zO%`AR+QAiKszp%hO5T^A4}3D;G#UKlNT)d7keQlz4BwlBzm=!|K>ok=16b)}>Ku@U z-^3$(jqpqJ5i?6aqUx07Y;qb|m3_u%VNN_^!^<)E-s#{U=_^7;w3C~v#@!UJ%unDG z=rYU{i*4JjE>uLbz6B)#8a&y=YcwS&~zS#!M5xVrda$IHoq_#$%% zUJA6v@;6wGRS@jslMms>J_P*Zp(^$FvNz|j-+4Ir!fN&p+6}vG7ec!s*9FAdJ4Ulm zV5OwaQl|NZiFsZT>!E|RdTwgxZ_HbiC^_Qy?k5DTSPSv-e)1ZGs{4#=542EhOd+A9zaK8jbGwFr%>w+Rp$)m2P&rVg58pzr^hM+J97YO1xVWWOsJG4Xy?}>ujt?)x z^*emtR}RHR&l=e?#IF%7bae|7_$qH*S*;vdV!m<)_+xFh9ok~K-*qCep-G0v&i3r7 z0$HD0S?Qe*Sy@k{^NSs6bWeHrN|g`rUL8lx>#*H}VcbTF@bKb%IeVgR9mP3j2Bqb$+{z&UEA@oQJShiw)>1g5N0ngCls64Y1}pNX8mJVJs|u*Ut3pSJfWE3Jsrh zV+}wfHG?Mk?S+%6U%>!mslVB^iemRto!Pu9buqb{xQbr8R0i19*F!JH=!g9DuS(H& zRA7@~QRu?~^ack9ycp;5{(a;9qroZwNsxDyjZ6~g2hLVV#=85}I%e~>FJR`+i+yDg z=*ud2eMQx9|7>lQXNwqZz)8K2uL_l-tFjLgQNxb_=fMS7f+G#%C8(A48Utg)Cj?B? ze3kl`?5|j#4!#i0#@y%drLzff&R#YXX^z#{%)a$~c4Y_+P}(D^cGc8TI?^+VA7H23 zRU;tgfy>^~U>97w3T}wJ+I|cIJ~O&4SGRI0`>eOxVRY&+p)|g{+E*F0^i-Ofw@(7r z7u}v+l>ju*w;(UPZSEb5`@PoAs3lW>z!e&*||ab zz6}shm$W9X-Q9e}VwzQXdZMT0>K3j-r?oeS%jP;nU%U6#l7`4EL31@Z;0q+g#Osk2 zPbhde?HG0Tj|x_4IJT{GlCFRqNKgLJT_;)*jdZ<2W(S_jz^A^oX4qjWYcZkyz?5zb zT4z;zVr3^IOl&$cg5rU|Pt-5=SKf-u{?_ncnKmm8<}MSf$2X3$Lgc_c)Y5g67ha=gv#d5DJ z7GtfXcMk=`nHGpzEJKe>r6*Tn%`xPFf0g69UdA}h5cm>L?GNg2&y8N6LzKj17H`YR z7wRF2jYK0CuO!;kJ!%1ug;;?LrxR){z3_x?$J(D&{fuS+BkVK83p}3;iGN7ps-B(g zXH|c_*`mZ|>qL6%%kFM_VPLJfD`Z|^??#oFW>AsWcK)dp?Z`KX7uX^(5Ag!q!h+-U zfVhMSolaomm6r@K$+Kpx<~%ArA-lOIW%v_hy-GLL_Pg=&@bU!hJPMb-c_q=y#W1f= z3=KeLgRArwYy-cB_{9O|%-N%q4u~TlgVkz1x8`T-5$$Q!SUh1C0QQ7*4g!N zr`TcxTWt%R3?%g!R(q z2!kX_-&*KeMcL^d%?pTMQ=6~R!35P{LZR4bK<0&;?{DzJEvenzW5SgHf7ZA!5tH&S zx|OO2ohR%ucb7bA=o&@9U97If8;#?hEHpkZpV0f>(`M{?TO^qnXj>@f8dX8uTqrXr zEz_x7eKzC88&2--K~D$Wy@=Ybq&I)kU!h24^~Df$_ePJBYsMu;vo3a#r+Nk@g|n{l z^VMW#pw$HpF#{*^eDW&?-WLTb}+GDOaH|Ts4&9JJeRJ2 zd?M?jmda|*&=`jt=sRkZibs?HR^WU0pw6quW+E8YZ`gF%DZlBLI^chhG!+iZU0}fX zP}*dMTui8GuPt0@d|mvlADX-EMIfwMTXf|Wyp)KMZ@oc|`zgI;LuPu0MXayHXv;}q ztyw?3uM?ndpSbxh6|gR$3xfjx{7x|s`~LL&tr^1UdhWKE*&4zUp=-$_*=(cVHvy*~ zTr=^6PWFa(aiTl>+ST{U=5I<+=*LpIQof}#yC@j z^&0dwOKB5QZ!9g-HW(G>xcj|)aY@xQd8W3>Q(lzz*uk?cZ^pg%wRJp&Ju?7W_h)|A z{I%(~&=y{HUTdHYc#Gep!XctyyT47s0yzz|fJ_58;uE|WS;#d9uUNRf-q{Lr!(%B% z+k8eZF3KkE+v>7_BSwrNO3aH^3})vg4-vf}5+B3sfgZDu$`0YYj&X1DK^H67UCPR9 z(+6bZNF~$@RFfdZn_oOXea>OeGj5fH=bMmlrpGMFwb`MZg-gqP-=DAb?1Z1I(_?u5 z7}5s9I&V4Vd%^0rWMx*Q-KmiFp*y7xht{;t@qVi;96cMcD@k_AHL5~M58p8ite}>c(>NJn)7InR zI|HqNAap?0e>TkxjqCqWmc1Hdoq}tM*lErx8-cwrd&5(QQ z$le^pgxSkI8oZWm9zA0)X*jAnNbxQj-E+Vi#HH>MTU;ipaUXtB-EB{UM>8qv{g`hP(vQ79*lp~PExIplZ>a4oW3Yz)E*Q#l0c4;_G&pj%+s+6y~g4Nkir@5jN zSDO{YSO%L+g(rRD9AEw{C`p9ObXT-RUu`*_v*JyrcJDLArmI(QUy=q=Mb@|0vBui! zR^6&|0iuj1&<&udu>@saIO|}YOg~s;{uVOOJnOjUNAI$q<-c;K2k=OX!@P?%;|CkS zktOl|A2RZCWhW_p#}-0=RA)~!u0WK;F?!j@;oD9=6qP0BLy%kL^%%x-mqvwfy51N~ zJM^-?+grF6#L}x4S8jTo*pCA}U5gRj$CDs1Vf%m42V%V9i{;bNFUOJx{PfkOXl2oj zfJpK2DIyv7g+3u=7kg(Wq1YZbXTE;bTlcMIp0&_?CFOl&2}+l{av-=bb_IOt7iB@G zJ_blSpS+(}fgerg2PQ+}oL|HJBIMO~#~b z!2BJ}DekB7&$m7S&e;0JSZwDdL)!;=lLkKEy7`UF7DIm_v&Cb9;KZuXiJy}@PS2l- zw>>$!F<=nsS3J8!=}ZUptjYXAG6Re-Iw^B$Dt|jkE4tl#_4<`@$AqQeB?)tXA)`@y zZ@hwNjj$|H0F3bftAbXcouM@_pB3>Hu@PSZQu%B3taGu(zgYw^o=X*4Bl*t0cyU^> zI&71_8jplc8cw?}Y1A&LVl_n58vqhCi$S|ET8`W%cw5LI!D>H3A)>0ENb)pB5<;9ZTd6j1;Iof7Zx@;$XM2L(9Vn`8y65tb$j1d)Hw zqwFVJXo_@^jJ6LO;H<TjD?|(0$N}bS_!ZDI%x{5uhT4QPwFftZekf2P>shHU>#he(8b zYi+*a#uynlACwG^UJ^=(qwxepN6a%YM%LXwh}-)zz_4K16U>C~lsYHO(b@yd56zL~ zitQr0>x@{dGr%7q_m(6Dmil@U{K_Ocno=$jCHAA_NuoZp$CM;TH7!P~sKdd7J&S=^lA{UhYS>Pc=fMm8uB zq)sMRrUIoSoIhPqQ&~w2m_JxuDrjkW98TgLoCn6Ks=&sdEGPwk@LL=W<8Hm6MX-tL z01nwLN?ROPz$GCM0j{fuk<#&mE)ozy6AGc6-JXfLt@yfF zGWDN5r|;zB7Bmo-{lVb`Lp(44T3qB5{%*P3fr-kY+3iB%{74_wKDanXDId5OD5?~- z-waaYgC8%B-Q(4__n@w|#Z>!);jdY!)gJFs(poRQzNgvHU zOz{SrI?f$rf+;Z8k)Yz=rzxjfg^32Yxo=m=Z9_9bGQE%vfEcd>;-nJq5ii`-mOz}9 zOR@L18Avj!*&J64ZtG@(VvErUE934A7oUjkA`*jsI_1HFgcR@;5B!ABr7&6N%%ow_Mv%0Dj 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 literal 0 HcmV?d00001 diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index 7ecb9dc3c440..c9ef006bea2d 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -142,6 +142,12 @@ export const SideNav = (props) => { imagesrc: theme === "light" ? "/sponsors/RoB-light.png" : "/sponsors/RoB.png", priority: 1, }, + { + link: "https://www.relentlesssolutions.com/", + imagesrc: + theme === "light" ? "/sponsors/relentless-light.png" : "/sponsors/relentless-dark.png", + priority: 1, + }, ]; const randomSponsorImage = () => { From cede7f08109b348063dc92be86ae34c53cc9a453 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 16 Jan 2026 12:16:21 -0500 Subject: [PATCH 110/111] Replace info alerts with tooltip icons in reports Replaces the informational Alert components in mailbox permissions and MFA report pages with Tooltip-wrapped Info icon buttons. This streamlines the UI and moves info text to tooltips next to the Sync button, improving visual clarity. --- .../reports/mailbox-permissions/index.js | 26 +++++++----- .../identity/reports/mfa-report/index.js | 40 +++++++++---------- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/pages/email/reports/mailbox-permissions/index.js b/src/pages/email/reports/mailbox-permissions/index.js index 226f4c1efbd9..54ec41158bce 100644 --- a/src/pages/email/reports/mailbox-permissions/index.js +++ b/src/pages/email/reports/mailbox-permissions/index.js @@ -1,10 +1,18 @@ import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; import { useState } from "react"; -import { Button, FormControlLabel, Switch, Alert, SvgIcon } from "@mui/material"; +import { + Button, + FormControlLabel, + Switch, + Alert, + SvgIcon, + IconButton, + Tooltip, +} from "@mui/material"; import { useSettings } from "../../../../hooks/use-settings"; import { Stack } from "@mui/system"; -import { Sync } from "@mui/icons-material"; +import { Sync, Info } from "@mui/icons-material"; import { useDialog } from "../../../../hooks/use-dialog"; import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; @@ -41,7 +49,12 @@ const Page = () => { }; const pageActions = [ - + + + + + + , + + + + + + + + , ]; return ( @@ -132,12 +138,6 @@ const Page = () => { filters={filters} actions={actions} cardButton={pageActions} - tableFilter={ - - This report displays cached data from the CIPP reporting database. Click the Sync button - to update the cache for the current tenant. - - } /> Date: Fri, 16 Jan 2026 12:16:28 -0500 Subject: [PATCH 111/111] Bump version to 10.0.0 Updated version in package.json and public/version.json to 10.0.0 for new release. --- package.json | 4 ++-- public/version.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 50baa212456d..75f5b6ef3148 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "8.8.2", + "version": "10.0.0", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { @@ -118,4 +118,4 @@ "eslint": "9.35.0", "eslint-config-next": "15.5.2" } -} +} \ No newline at end of file diff --git a/public/version.json b/public/version.json index 7a3adc4ddaba..d8ac32de0bb3 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "8.8.2" + "version": "10.0.0" } \ No newline at end of file