From 7dffc4c64a9de48e0d6f3f59abdcc57f1634c019 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:27:59 +0100 Subject: [PATCH 01/86] feat: add fuzzy match distance setting to Intune template based on Levenshtein distance --- src/data/standards.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index fe08f2fcbb45..cddee8945751 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4779,7 +4779,7 @@ } ] }, - { + { "type": "switch", "name": "standards.TeamsGlobalMeetingPolicy.AllowPSTNUsersToBypassLobby", "label": "Allow dial-in users to bypass lobby" @@ -5106,10 +5106,7 @@ "condition": { "field": "standards.TeamsFederationConfiguration.DomainControl.value", "compareType": "isOneOf", - "compareValue": [ - "AllowSpecificExternal", - "BlockSpecificExternal" - ] + "compareValue": ["AllowSpecificExternal", "BlockSpecificExternal"] } } ], @@ -5530,6 +5527,13 @@ "value": "exclude" } ] + }, + { + "type": "number", + "required": false, + "name": "levenshteinDistance", + "label": "Fuzzy Match Distance (0 = exact name match only, higher values allow replacing policies with similar names based on Levenshtein distance)", + "defaultValue": 0 } ] }, From 4fc1b57b65a49f832db2ded4e15563a21ac1ebfa Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:41:42 +0100 Subject: [PATCH 02/86] feat: add warningMessage support standards --- .../CippStandards/CippStandardAccordion.jsx | 49 ++++++++++++++++--- src/data/standards.json | 7 ++- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 1a2811d462c3..31a49987ceaa 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -2,6 +2,7 @@ import React, { useEffect, useState, useMemo } from "react"; import { Card, Stack, + Alert, Avatar, Box, Typography, @@ -53,8 +54,9 @@ const getAvailableActions = (disabledFeatures) => { return allActions.filter((action) => !disabledFeatures?.[action.value.toLowerCase()]); }; -const CippAddedComponent = React.memo(({ standardName, component, formControl }) => { +const CippAddedComponent = React.memo(({ standardName, component, formControl, currentValue }) => { const updatedComponent = { ...component }; + const fieldName = `${standardName}.${updatedComponent.name}`; if (component.type === "AdminRolesMultiSelect") { updatedComponent.type = "autoComplete"; @@ -73,15 +75,30 @@ const CippAddedComponent = React.memo(({ standardName, component, formControl }) updatedComponent.type = component.type; } + const warningThreshold = Number(updatedComponent.warningThreshold); + const numericValue = Number(currentValue); + const showThresholdWarning = + Number.isFinite(warningThreshold) && + !Number.isNaN(numericValue) && + `${currentValue}`.trim() !== "" && + numericValue > warningThreshold; + + const warningMessage = + updatedComponent.warningMessage || + `Values above ${warningThreshold} can match unrelated policies. Use with caution.`; + return ( - + + + {showThresholdWarning && {warningMessage}} + ); }); @@ -937,6 +954,10 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} + currentValue={_.get( + watchedValues, + `${standardName}.${component.name}`, + )} /> ) : ( @@ -945,6 +966,10 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} + currentValue={_.get( + watchedValues, + `${standardName}.${component.name}`, + )} /> ), )} @@ -995,6 +1020,10 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} + currentValue={_.get( + watchedValues, + `${standardName}.${component.name}`, + )} /> ) : ( @@ -1003,6 +1032,10 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} + currentValue={_.get( + watchedValues, + `${standardName}.${component.name}`, + )} /> ), )} diff --git a/src/data/standards.json b/src/data/standards.json index cddee8945751..86a6cf5964c5 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -5533,7 +5533,12 @@ "required": false, "name": "levenshteinDistance", "label": "Fuzzy Match Distance (0 = exact name match only, higher values allow replacing policies with similar names based on Levenshtein distance)", - "defaultValue": 0 + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" } + }, + "warningThreshold": 5, + "warningMessage": "Warning: values above 5 can match unrelated policies. Use with caution." } ] }, From 88bc10a527efe58a986bf96c551c07c74c3f6ab5 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Thu, 2 Apr 2026 19:03:48 +0100 Subject: [PATCH 03/86] Added UI Elements for adding conditional access policies to package tags Added UI Elements for adding conditional access policies to package tags --- src/data/standards.json | 17 +++++++++ .../tenant/conditional/list-template/index.js | 35 +++++++++++++++++-- 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index f643617f475a..133db7df11bc 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -5669,6 +5669,23 @@ } } }, + { + "type": "autoComplete", + "multiple": false, + "required": false, + "creatable": false, + "name": "TemplateList-Tags", + "label": "Or select a package of CA Templates", + "api": { + "queryKey": "ListCATemplates-tag-autocomplete", + "url": "/api/ListCATemplates?mode=Tag", + "labelField": "label", + "valueField": "value", + "addedField": { + "templates": "templates" + } + } + }, { "name": "state", "label": "What state should we deploy this template in?", diff --git a/src/pages/tenant/conditional/list-template/index.js b/src/pages/tenant/conditional/list-template/index.js index 41221d021582..5f7daf3b2737 100644 --- a/src/pages/tenant/conditional/list-template/index.js +++ b/src/pages/tenant/conditional/list-template/index.js @@ -2,7 +2,7 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import { Button, Box } from "@mui/material"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; -import { Delete, GitHub, Edit, RocketLaunch } from "@mui/icons-material"; +import { Delete, GitHub, Edit, RocketLaunch, LocalOffer, LocalOfferOutlined } from "@mui/icons-material"; import { ApiGetCall } from "../../../../api/ApiCall"; import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; import { CippCADeployDrawer } from "../../../../components/CippComponents/CippCADeployDrawer.jsx"; @@ -42,6 +42,37 @@ const Page = () => { icon: , color: "info", }, + { + label: "Add to package", + type: "POST", + url: "/api/ExecSetPackageTag", + data: { GUID: "GUID" }, + fields: [ + { + type: "textField", + name: "Package", + label: "Package Name", + required: true, + validators: { + required: { value: true, message: "Package name is required" }, + }, + }, + ], + confirmText: "Enter the package name to assign to the selected template(s).", + multiPost: true, + icon: , + color: "info", + }, + { + label: "Remove from package", + type: "POST", + url: "/api/ExecSetPackageTag", + data: { GUID: "GUID", Remove: true }, + confirmText: "Are you sure you want to remove the selected template(s) from their package?", + multiPost: true, + icon: , + color: "warning", + }, { label: "Save to GitHub", type: "POST", @@ -110,7 +141,7 @@ const Page = () => { queryKey="ListCATemplates-table" actions={actions} offCanvas={offCanvas} - simpleColumns={["displayName", "GUID"]} + simpleColumns={["displayName", "package", "GUID"]} cardButton={ - - } - apiUrl="/api/ListAssignmentFilters" - queryKey={`assignment-filters-${currentTenant}`} - actions={actions} - offCanvas={offCanvas} - simpleColumns={[ - "displayName", - "description", - "platform", - "assignmentFilterManagementType", - "rule", - ]} - /> + + )} + + + : } + label={useReportDB ? "Cached" : "Live"} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ]; + + return ( + <> + + + {pageActions} + + } + apiUrl={`/api/ListAssignmentFilters${useReportDB ? "?UseReportDB=true" : ""}`} + queryKey={`assignment-filters-${currentTenant}-${useReportDB}`} + actions={actions} + offCanvas={offCanvas} + simpleColumns={simpleColumns} + /> + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId); + } + }, + }} + /> + ); }; diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index e8e34e9338d5..5f1d42f3254e 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -4,9 +4,14 @@ import { CippApiDialog } from "../../../../components/CippComponents/CippApiDial import { useSettings } from "../../../../hooks/use-settings"; import { useDialog } from "../../../../hooks/use-dialog.js"; import { EyeIcon } from "@heroicons/react/24/outline"; -import { Box, Button } from "@mui/material"; +import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Stack } from "@mui/system"; +import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; +import { useEffect, useState } from "react"; import { Sync, + CloudDone, + Bolt, RestartAlt, LocationOn, Password, @@ -25,7 +30,15 @@ import { const Page = () => { const pageTitle = "Devices"; const tenantFilter = useSettings().currentTenant; + const isAllTenants = tenantFilter === "AllTenants"; const depSyncDialog = useDialog(); + const syncDialog = useDialog(); + const [syncQueueId, setSyncQueueId] = useState(null); + const [useReportDB, setUseReportDB] = useState(isAllTenants); + + useEffect(() => { + setUseReportDB(tenantFilter === "AllTenants"); + }, [tenantFilter]); const actions = [ { @@ -385,6 +398,8 @@ const Page = () => { }; const simpleColumns = [ + ...(useReportDB ? ["CacheTimestamp"] : []), + ...(useReportDB && isAllTenants ? ["Tenant"] : []), "deviceName", "userPrincipalName", "complianceState", @@ -398,6 +413,53 @@ const Page = () => { "joinType", ]; + const pageActions = [ + + {useReportDB && ( + <> + + + + )} + + + : } + label={useReportDB ? "Cached" : "Live"} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ]; + return ( <> { apiUrl="/api/ListGraphRequest" apiData={{ Endpoint: "deviceManagement/managedDevices", + ...(useReportDB ? { UseReportDB: true } : {}), }} apiDataKey="Results" actions={actions} - queryKey={`MEMDevices-${tenantFilter}`} + queryKey={`MEMDevices-${tenantFilter}-${useReportDB}`} offCanvas={offCanvas} simpleColumns={simpleColumns} cardButton={ - + - + {pageActions} + } /> { confirmText: `Are you sure you want to sync Apple Device Enrollment Program (DEP) tokens? This will sync all DEP tokens for ${tenantFilter}. This may take several minutes to complete in the background, and can only be done every 15 minutes.`, }} /> + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId); + } + }, + }} + /> ); }; diff --git a/src/pages/endpoint/MEM/list-appprotection-policies/index.js b/src/pages/endpoint/MEM/list-appprotection-policies/index.js index bcf12e6e4efd..f20bee7ffcc2 100644 --- a/src/pages/endpoint/MEM/list-appprotection-policies/index.js +++ b/src/pages/endpoint/MEM/list-appprotection-policies/index.js @@ -1,14 +1,29 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog.jsx' import { PermissionButton } from '../../../../utils/permissions.js' import { CippPolicyDeployDrawer } from '../../../../components/CippComponents/CippPolicyDeployDrawer.jsx' import { useSettings } from '../../../../hooks/use-settings.js' import { useCippIntunePolicyActions } from '../../../../components/CippComponents/CippIntunePolicyActions.jsx' +import { Sync, CloudDone, Bolt } from '@mui/icons-material' +import { Button, Chip, SvgIcon, Tooltip } from '@mui/material' +import { Stack } from '@mui/system' +import { useDialog } from '../../../../hooks/use-dialog' +import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker' +import { useEffect, useState } from 'react' const Page = () => { const pageTitle = 'App Protection & Configuration Policies' const cardButtonPermissions = ['Endpoint.MEM.ReadWrite'] const tenant = useSettings().currentTenant + const isAllTenants = tenant === 'AllTenants' + const syncDialog = useDialog() + const [syncQueueId, setSyncQueueId] = useState(null) + const [useReportDB, setUseReportDB] = useState(isAllTenants) + + useEffect(() => { + setUseReportDB(tenant === 'AllTenants') + }, [tenant]) const actions = useCippIntunePolicyActions(tenant, 'URLName', { templateData: { @@ -31,6 +46,8 @@ const Page = () => { } const simpleColumns = [ + ...(useReportDB ? ['CacheTimestamp'] : []), + ...(useReportDB && isAllTenants ? ['Tenant'] : []), 'displayName', 'PolicyTypeName', 'PolicyAssignment', @@ -38,21 +55,93 @@ const Page = () => { 'lastModifiedDateTime', ] + const pageActions = [ + + {useReportDB && ( + <> + + + + )} + + + : } + label={useReportDB ? 'Cached' : 'Live'} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ] + return ( - - } - /> + <> + + + {pageActions} + + } + /> + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId) + } + }, + }} + /> + ) } diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js index b3394023c492..0b8e395c3db0 100644 --- a/src/pages/endpoint/MEM/list-compliance-policies/index.js +++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js @@ -1,14 +1,29 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { PermissionButton } from "../../../../utils/permissions.js"; import { CippPolicyDeployDrawer } from "../../../../components/CippComponents/CippPolicyDeployDrawer.jsx"; import { useSettings } from "../../../../hooks/use-settings.js"; import { useCippIntunePolicyActions } from "../../../../components/CippComponents/CippIntunePolicyActions.jsx"; +import { Sync, CloudDone, Bolt } from "@mui/icons-material"; +import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Stack } from "@mui/system"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; +import { useEffect, useState } from "react"; const Page = () => { const pageTitle = "Intune Compliance Policies"; const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const tenant = useSettings().currentTenant; + const isAllTenants = tenant === "AllTenants"; + const syncDialog = useDialog(); + const [syncQueueId, setSyncQueueId] = useState(null); + const [useReportDB, setUseReportDB] = useState(isAllTenants); + + useEffect(() => { + setUseReportDB(tenant === "AllTenants"); + }, [tenant]); const actions = useCippIntunePolicyActions(tenant, "deviceCompliancePolicies", { templateData: { @@ -29,6 +44,8 @@ const Page = () => { }; const simpleColumns = [ + ...(useReportDB ? ["CacheTimestamp"] : []), + ...(useReportDB && isAllTenants ? ["Tenant"] : []), "displayName", "PolicyTypeName", "PolicyAssignment", @@ -37,21 +54,93 @@ const Page = () => { "lastModifiedDateTime", ]; + const pageActions = [ + + {useReportDB && ( + <> + + + + )} + + + : } + label={useReportDB ? "Cached" : "Live"} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ]; + return ( - - } - /> + <> + + + {pageActions} + + } + /> + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId); + } + }, + }} + /> + ); }; diff --git a/src/pages/endpoint/MEM/list-policies/index.js b/src/pages/endpoint/MEM/list-policies/index.js index 1a78f45906cb..8c5d4782513c 100644 --- a/src/pages/endpoint/MEM/list-policies/index.js +++ b/src/pages/endpoint/MEM/list-policies/index.js @@ -4,8 +4,8 @@ import { PermissionButton } from '../../../../utils/permissions.js' import { CippPolicyDeployDrawer } from '../../../../components/CippComponents/CippPolicyDeployDrawer.jsx' import { useSettings } from '../../../../hooks/use-settings.js' import { useCippIntunePolicyActions } from '../../../../components/CippComponents/CippIntunePolicyActions.jsx' -import { Sync, Info, CloudDone, Bolt } from '@mui/icons-material' -import { Button, SvgIcon, IconButton, Tooltip, Chip } from '@mui/material' +import { Sync, CloudDone, Bolt } from '@mui/icons-material' +import { Button, SvgIcon, Tooltip, Chip } from '@mui/material' import { Stack } from '@mui/system' import { useDialog } from '../../../../hooks/use-dialog' import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog' @@ -45,7 +45,8 @@ const Page = () => { } const simpleColumns = [ - ...(useReportDB ? ['Tenant', 'CacheTimestamp'] : []), + ...(useReportDB ? ['CacheTimestamp'] : []), + ...(useReportDB && isAllTenants ? ['Tenant'] : []), 'displayName', 'PolicyTypeName', 'PolicyAssignment', @@ -81,8 +82,8 @@ const Page = () => { isAllTenants ? 'AllTenants always uses cached data' : useReportDB - ? 'Showing cached data from the Reporting Database — click to switch to live' - : 'Showing live data — click to switch to cache' + ? 'Showing cached data from the Reporting Database - click to switch to live' + : 'Showing live data - click to switch to cache' } > diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx index 91eb0bf675a6..11b480fae484 100644 --- a/src/pages/endpoint/MEM/list-scripts/index.jsx +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -1,5 +1,6 @@ import { Layout as DashboardLayout } from "../../../../layouts/index"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; import { TrashIcon, PencilIcon, @@ -16,14 +17,19 @@ import { IconButton, CircularProgress, DialogActions, + Chip, + SvgIcon, + Tooltip, } from "@mui/material"; import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; import { useState, useEffect, useMemo } from "react"; import { useDispatch } from "react-redux"; -import { Close, Save, LaptopChromebook } from "@mui/icons-material"; +import { Close, Save, LaptopChromebook, Sync, CloudDone, Bolt } from "@mui/icons-material"; import { useSettings } from "../../../../hooks/use-settings"; +import { useDialog } from "../../../../hooks/use-dialog"; import { Stack } from "@mui/system"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; const assignmentModeOptions = [ { label: "Replace existing assignments", value: "replace" }, @@ -39,6 +45,17 @@ const Page = () => { const [codeContentChanged, setCodeContentChanged] = useState(false); const [warnOpen, setWarnOpen] = useState(false); const [currentScript, setCurrentScript] = useState(null); + const [scriptTenant, setScriptTenant] = useState(null); + + const tenantFilter = useSettings().currentTenant; + const isAllTenants = tenantFilter === "AllTenants"; + const syncDialog = useDialog(); + const [syncQueueId, setSyncQueueId] = useState(null); + const [useReportDB, setUseReportDB] = useState(isAllTenants); + + useEffect(() => { + setUseReportDB(tenantFilter === "AllTenants"); + }, [tenantFilter]); const dispatch = useDispatch(); @@ -48,17 +65,16 @@ const Page = () => { : "powershell"; }, [currentScript?.scriptType]); - const tenantFilter = useSettings().currentTenant; const { isLoading: scriptIsLoading, isRefetching: scriptIsFetching, refetch: scriptRefetch, data, } = useQuery({ - queryKey: ["script", { scriptId }], + queryKey: ["script", { scriptId, scriptTenant }], queryFn: async () => { const response = await fetch( - `/api/EditIntuneScript?TenantFilter=${tenantFilter}&ScriptId=${scriptId}` + `/api/EditIntuneScript?TenantFilter=${scriptTenant || tenantFilter}&ScriptId=${scriptId}` ); return response.json(); }, @@ -79,6 +95,7 @@ const Page = () => { const handleScriptEdit = async (row, action) => { setScriptId(row.id); + setScriptTenant(row?.Tenant || tenantFilter); setCodeOpen(!codeOpen); }; @@ -94,6 +111,7 @@ const Page = () => { setCodeOpen(!codeOpen); setCodeContentChanged(false); setScriptId(null); + setScriptTenant(null); setCodeContent(""); } }; @@ -114,7 +132,7 @@ const Page = () => { scriptType, } = currentScript; const patchData = { - TenantFilter: tenantFilter, + TenantFilter: scriptTenant || tenantFilter, ScriptId: id, ScriptType: scriptType, IntuneScript: JSON.stringify({ @@ -197,7 +215,7 @@ const Page = () => { ], confirmText: 'Are you sure you want to assign "[displayName]" to all users?', customDataformatter: (row, action, formData) => ({ - tenantFilter: tenantFilter, + tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter, ID: row?.id, Type: getScriptEndpoint(row?.scriptType), AssignTo: "allLicensedUsers", @@ -223,7 +241,7 @@ const Page = () => { ], confirmText: 'Are you sure you want to assign "[displayName]" to all devices?', customDataformatter: (row, action, formData) => ({ - tenantFilter: tenantFilter, + tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter, ID: row?.id, Type: getScriptEndpoint(row?.scriptType), AssignTo: "AllDevices", @@ -249,7 +267,7 @@ const Page = () => { ], confirmText: 'Are you sure you want to assign "[displayName]" to all users and devices?', customDataformatter: (row, action, formData) => ({ - tenantFilter: tenantFilter, + tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter, ID: row?.id, Type: getScriptEndpoint(row?.scriptType), AssignTo: "AllDevicesAndUsers", @@ -305,7 +323,7 @@ const Page = () => { customDataformatter: (row, action, formData) => { const selectedGroups = Array.isArray(formData?.groupTargets) ? formData.groupTargets : []; return { - tenantFilter: tenantFilter, + tenantFilter: tenantFilter === "AllTenants" && row?.Tenant ? row.Tenant : tenantFilter, ID: row?.id, Type: getScriptEndpoint(row?.scriptType), GroupIds: selectedGroups.map((group) => group.value).filter(Boolean), @@ -354,6 +372,8 @@ const Page = () => { }; const simpleColumns = [ + ...(useReportDB ? ["CacheTimestamp"] : []), + ...(useReportDB && isAllTenants ? ["Tenant"] : []), "scriptType", "displayName", "ScriptAssignment", @@ -363,14 +383,63 @@ const Page = () => { "lastModifiedDateTime", ]; + const pageActions = [ + + {useReportDB && ( + <> + + + + )} + + + : } + label={useReportDB ? "Cached" : "Live"} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ]; + return ( <> @@ -427,6 +496,7 @@ const Page = () => { setWarnOpen(false); setCodeContent(""); setScriptId(null); + setScriptTenant(null); setCodeContentChanged(false); }} > @@ -434,9 +504,28 @@ const Page = () => { + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId); + } + }, + }} + /> ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/endpoint/MEM/reusable-settings/index.js b/src/pages/endpoint/MEM/reusable-settings/index.js index c301082ea1f0..0c3837e51a0c 100644 --- a/src/pages/endpoint/MEM/reusable-settings/index.js +++ b/src/pages/endpoint/MEM/reusable-settings/index.js @@ -1,18 +1,34 @@ -import { Book, DeleteForever } from "@mui/icons-material"; +import { Book, DeleteForever, Sync, CloudDone, Bolt } from "@mui/icons-material"; import { CippReusableSettingsDeployDrawer } from "../../../../components/CippComponents/CippReusableSettingsDeployDrawer.jsx"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { useSettings } from "../../../../hooks/use-settings"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; +import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Stack } from "@mui/system"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; +import { useEffect, useState } from "react"; const Page = () => { const { currentTenant } = useSettings(); const pageTitle = "Reusable Settings"; + const isAllTenants = currentTenant === "AllTenants"; + const syncDialog = useDialog(); + const [syncQueueId, setSyncQueueId] = useState(null); + const [useReportDB, setUseReportDB] = useState(isAllTenants); + + useEffect(() => { + setUseReportDB(currentTenant === "AllTenants"); + }, [currentTenant]); const actions = [ { label: "Edit Reusable Setting", - link: `/endpoint/MEM/reusable-settings/edit?id=[id]&tenant=${currentTenant}&tenantFilter=${currentTenant}`, + link: isAllTenants + ? "/endpoint/MEM/reusable-settings/edit?id=[id]&tenant=[Tenant]&tenantFilter=[Tenant]" + : `/endpoint/MEM/reusable-settings/edit?id=[id]&tenant=${currentTenant}&tenantFilter=${currentTenant}`, }, { label: "Delete Reusable Setting", @@ -47,18 +63,98 @@ const Page = () => { size: "lg", }; + const simpleColumns = [ + ...(useReportDB ? ["CacheTimestamp"] : []), + ...(useReportDB && isAllTenants ? ["Tenant"] : []), + "displayName", + "description", + "id", + "version", + ]; + + const pageActions = [ + + {useReportDB && ( + <> + + + + )} + + + : } + label={useReportDB ? "Cached" : "Live"} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ]; + return ( - - } - apiUrl="/api/ListIntuneReusableSettings" - queryKey={`ListIntuneReusableSettings-${currentTenant}`} - actions={actions} - offCanvas={offCanvas} - simpleColumns={["displayName", "description", "id", "version"]} - /> + <> + + + {pageActions} + + } + apiUrl={`/api/ListIntuneReusableSettings${useReportDB ? "?UseReportDB=true" : ""}`} + queryKey={`ListIntuneReusableSettings-${currentTenant}-${useReportDB}`} + actions={actions} + offCanvas={offCanvas} + simpleColumns={simpleColumns} + /> + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId); + } + }, + }} + /> + ); }; diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 41671638cfce..4b8b60239a47 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -2,11 +2,14 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline"; -import { LaptopMac, Sync, BookmarkAdd } from "@mui/icons-material"; +import { LaptopMac, Sync, BookmarkAdd, CloudDone, Bolt } from "@mui/icons-material"; import { CippApplicationDeployDrawer } from "../../../../components/CippComponents/CippApplicationDeployDrawer"; -import { Button, Box } from "@mui/material"; +import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Stack } from "@mui/system"; import { useSettings } from "../../../../hooks/use-settings.js"; import { useDialog } from "../../../../hooks/use-dialog.js"; +import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; +import { useEffect, useState } from "react"; const assignmentIntentOptions = [ { label: "Required", value: "Required" }, @@ -44,8 +47,16 @@ const mapOdataToAppType = (odataType) => { const Page = () => { const pageTitle = "Applications"; - const syncDialog = useDialog(); + const vppSyncDialog = useDialog(); + const cacheSyncDialog = useDialog(); const tenant = useSettings().currentTenant; + const isAllTenants = tenant === "AllTenants"; + const [syncQueueId, setSyncQueueId] = useState(null); + const [useReportDB, setUseReportDB] = useState(isAllTenants); + + useEffect(() => { + setUseReportDB(tenant === "AllTenants"); + }, [tenant]); const getAssignmentFilterFields = () => [ { @@ -291,6 +302,8 @@ const Page = () => { }; const simpleColumns = [ + ...(useReportDB ? ["CacheTimestamp"] : []), + ...(useReportDB && isAllTenants ? ["Tenant"] : []), "displayName", "AppAssignment", "AppExclude", @@ -299,26 +312,75 @@ const Page = () => { "createdDateTime", ]; + const pageActions = [ + + {useReportDB && ( + <> + + + + )} + + + : } + label={useReportDB ? "Cached" : "Live"} + color="primary" + size="small" + onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} + clickable={!isAllTenants} + disabled={isAllTenants} + variant="outlined" + /> + + + , + ]; + return ( <> + - - + {pageActions} + } /> { confirmText: `Are you sure you want to sync Apple Volume Purchase Program (VPP) tokens? This will sync all VPP tokens for ${tenant}.`, }} /> + { + if (result?.Metadata?.QueueId) { + setSyncQueueId(result?.Metadata?.QueueId); + } + }, + }} + /> ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; From 1707506b0a39fbf7c0154b9608b9d90a69c19331 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sat, 25 Apr 2026 21:22:00 +0200 Subject: [PATCH 12/86] feat: integrate useCippReportDB for data handling - Replaced local state management for report database usage with useCippReportDB hook across multiple components. - Simplified API calls and state management for caching and syncing data. - Removed redundant code related to tenant checks and sync dialogs. --- .../CippComponents/CippReportDBControls.jsx | 30 ++++-- .../endpoint/MEM/assignment-filters/index.js | 102 +++--------------- src/pages/endpoint/MEM/devices/index.js | 86 +-------------- .../MEM/list-appprotection-policies/index.js | 98 +++-------------- .../MEM/list-compliance-policies/index.js | 98 +++-------------- src/pages/endpoint/MEM/list-scripts/index.jsx | 101 +++-------------- .../endpoint/MEM/reusable-settings/index.js | 100 +++-------------- src/pages/endpoint/applications/list/index.js | 98 +++-------------- 8 files changed, 112 insertions(+), 601 deletions(-) diff --git a/src/components/CippComponents/CippReportDBControls.jsx b/src/components/CippComponents/CippReportDBControls.jsx index 063fd386bb5b..889185e5bfad 100644 --- a/src/components/CippComponents/CippReportDBControls.jsx +++ b/src/components/CippComponents/CippReportDBControls.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; +import { useState, useMemo, useCallback } from "react"; import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; import { Stack } from "@mui/system"; import { Sync, CloudDone, Bolt } from "@mui/icons-material"; @@ -53,16 +53,24 @@ export function useCippReportDB(config) { const isAllTenants = currentTenant === "AllTenants"; const dialog = useDialog(); const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(defaultCached); + const [cacheOverride, setCacheOverride] = useState({ tenant: null, value: null }); + const useReportDB = isAllTenants + ? true + : cacheOverride.tenant === currentTenant + ? cacheOverride.value + : defaultCached; + const setUseReportDB = useCallback( + (valueOrUpdater) => { + setCacheOverride((prev) => { + const previousValue = prev.tenant === currentTenant ? prev.value : defaultCached; + const nextValue = + typeof valueOrUpdater === "function" ? valueOrUpdater(previousValue) : valueOrUpdater; - // Reset to default whenever tenant changes; AllTenants always forces cached - useEffect(() => { - if (isAllTenants) { - setUseReportDB(true); - } else { - setUseReportDB(defaultCached); - } - }, [currentTenant, isAllTenants, defaultCached]); + return { tenant: currentTenant, value: nextValue }; + }); + }, + [currentTenant, defaultCached], + ); // Whether the toggle is actually clickable const canToggle = allowToggle && !isAllTenants; @@ -173,7 +181,7 @@ export function useCippReportDB(config) { relatedQueryKeys: [`${queryKey}-${currentTenant}-true`], data: { Name: cacheName, - Types: "None", + ...(cacheName === "Mailboxes" ? { Types: "None" } : {}), ...(syncData || {}), }, onSuccess: handleSyncSuccess, diff --git a/src/pages/endpoint/MEM/assignment-filters/index.js b/src/pages/endpoint/MEM/assignment-filters/index.js index 32fd698663a2..bedf0ef1ada4 100644 --- a/src/pages/endpoint/MEM/assignment-filters/index.js +++ b/src/pages/endpoint/MEM/assignment-filters/index.js @@ -1,27 +1,23 @@ -import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Button } from "@mui/material"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import Link from "next/link"; import { TrashIcon } from "@heroicons/react/24/outline"; -import { Edit, Add, Book, Sync, CloudDone, Bolt } from "@mui/icons-material"; +import { Edit, Add, Book } from "@mui/icons-material"; import { Stack } from "@mui/system"; -import { useSettings } from "../../../../hooks/use-settings"; -import { useDialog } from "../../../../hooks/use-dialog.js"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; -import { useEffect, useState } from "react"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const Page = () => { const pageTitle = "Assignment Filters"; - const { currentTenant } = useSettings(); - const isAllTenants = currentTenant === "AllTenants"; - const syncDialog = useDialog(); - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(isAllTenants); - useEffect(() => { - setUseReportDB(currentTenant === "AllTenants"); - }, [currentTenant]); + const reportDB = useCippReportDB({ + apiUrl: "/api/ListAssignmentFilters", + queryKey: "assignment-filters", + cacheName: "IntuneAssignmentFilters", + syncTitle: "Sync Assignment Filters Report", + allowToggle: true, + defaultCached: false, + }); const actions = [ { @@ -75,8 +71,7 @@ const Page = () => { }; const simpleColumns = [ - ...(useReportDB ? ["CacheTimestamp"] : []), - ...(useReportDB && isAllTenants ? ["Tenant"] : []), + ...reportDB.cacheColumns, "displayName", "description", "platform", @@ -84,53 +79,6 @@ const Page = () => { "rule", ]; - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? "Cached" : "Live"} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ]; - return ( <> { - {pageActions} + {reportDB.controls} } - apiUrl={`/api/ListAssignmentFilters${useReportDB ? "?UseReportDB=true" : ""}`} - queryKey={`assignment-filters-${currentTenant}-${useReportDB}`} + apiUrl={reportDB.resolvedApiUrl} + queryKey={reportDB.resolvedQueryKey} actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} /> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId); - } - }, - }} - /> + {reportDB.syncDialog} ); }; diff --git a/src/pages/endpoint/MEM/devices/index.js b/src/pages/endpoint/MEM/devices/index.js index 5f1d42f3254e..087052560120 100644 --- a/src/pages/endpoint/MEM/devices/index.js +++ b/src/pages/endpoint/MEM/devices/index.js @@ -4,14 +4,10 @@ import { CippApiDialog } from "../../../../components/CippComponents/CippApiDial import { useSettings } from "../../../../hooks/use-settings"; import { useDialog } from "../../../../hooks/use-dialog.js"; import { EyeIcon } from "@heroicons/react/24/outline"; -import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Button } from "@mui/material"; import { Stack } from "@mui/system"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; -import { useEffect, useState } from "react"; import { Sync, - CloudDone, - Bolt, RestartAlt, LocationOn, Password, @@ -30,15 +26,7 @@ import { const Page = () => { const pageTitle = "Devices"; const tenantFilter = useSettings().currentTenant; - const isAllTenants = tenantFilter === "AllTenants"; const depSyncDialog = useDialog(); - const syncDialog = useDialog(); - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(isAllTenants); - - useEffect(() => { - setUseReportDB(tenantFilter === "AllTenants"); - }, [tenantFilter]); const actions = [ { @@ -398,8 +386,6 @@ const Page = () => { }; const simpleColumns = [ - ...(useReportDB ? ["CacheTimestamp"] : []), - ...(useReportDB && isAllTenants ? ["Tenant"] : []), "deviceName", "userPrincipalName", "complianceState", @@ -413,53 +399,6 @@ const Page = () => { "joinType", ]; - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? "Cached" : "Live"} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ]; - return ( <> { apiUrl="/api/ListGraphRequest" apiData={{ Endpoint: "deviceManagement/managedDevices", - ...(useReportDB ? { UseReportDB: true } : {}), }} apiDataKey="Results" actions={actions} - queryKey={`MEMDevices-${tenantFilter}-${useReportDB}`} + queryKey={`MEMDevices-${tenantFilter}`} offCanvas={offCanvas} simpleColumns={simpleColumns} cardButton={ @@ -479,7 +417,6 @@ const Page = () => { - {pageActions} } /> @@ -493,25 +430,6 @@ const Page = () => { confirmText: `Are you sure you want to sync Apple Device Enrollment Program (DEP) tokens? This will sync all DEP tokens for ${tenantFilter}. This may take several minutes to complete in the background, and can only be done every 15 minutes.`, }} /> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId); - } - }, - }} - /> ); }; diff --git a/src/pages/endpoint/MEM/list-appprotection-policies/index.js b/src/pages/endpoint/MEM/list-appprotection-policies/index.js index f20bee7ffcc2..b864c1a0a447 100644 --- a/src/pages/endpoint/MEM/list-appprotection-policies/index.js +++ b/src/pages/endpoint/MEM/list-appprotection-policies/index.js @@ -1,29 +1,25 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' -import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog.jsx' import { PermissionButton } from '../../../../utils/permissions.js' import { CippPolicyDeployDrawer } from '../../../../components/CippComponents/CippPolicyDeployDrawer.jsx' import { useSettings } from '../../../../hooks/use-settings.js' import { useCippIntunePolicyActions } from '../../../../components/CippComponents/CippIntunePolicyActions.jsx' -import { Sync, CloudDone, Bolt } from '@mui/icons-material' -import { Button, Chip, SvgIcon, Tooltip } from '@mui/material' +import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls' import { Stack } from '@mui/system' -import { useDialog } from '../../../../hooks/use-dialog' -import { CippQueueTracker } from '../../../../components/CippTable/CippQueueTracker' -import { useEffect, useState } from 'react' const Page = () => { const pageTitle = 'App Protection & Configuration Policies' const cardButtonPermissions = ['Endpoint.MEM.ReadWrite'] const tenant = useSettings().currentTenant - const isAllTenants = tenant === 'AllTenants' - const syncDialog = useDialog() - const [syncQueueId, setSyncQueueId] = useState(null) - const [useReportDB, setUseReportDB] = useState(isAllTenants) - useEffect(() => { - setUseReportDB(tenant === 'AllTenants') - }, [tenant]) + const reportDB = useCippReportDB({ + apiUrl: '/api/ListAppProtectionPolicies', + queryKey: 'ListAppProtectionPolicies', + cacheName: 'IntuneAppProtectionPolicies', + syncTitle: 'Sync App Protection Policies Report', + allowToggle: true, + defaultCached: false, + }) const actions = useCippIntunePolicyActions(tenant, 'URLName', { templateData: { @@ -46,8 +42,7 @@ const Page = () => { } const simpleColumns = [ - ...(useReportDB ? ['CacheTimestamp'] : []), - ...(useReportDB && isAllTenants ? ['Tenant'] : []), + ...reportDB.cacheColumns, 'displayName', 'PolicyTypeName', 'PolicyAssignment', @@ -55,59 +50,12 @@ const Page = () => { 'lastModifiedDateTime', ] - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? 'Cached' : 'Live'} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ] - return ( <> { requiredPermissions={cardButtonPermissions} PermissionButton={PermissionButton} /> - {pageActions} + {reportDB.controls} } /> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId) - } - }, - }} - /> + {reportDB.syncDialog} ) } diff --git a/src/pages/endpoint/MEM/list-compliance-policies/index.js b/src/pages/endpoint/MEM/list-compliance-policies/index.js index 0b8e395c3db0..32574567a0ff 100644 --- a/src/pages/endpoint/MEM/list-compliance-policies/index.js +++ b/src/pages/endpoint/MEM/list-compliance-policies/index.js @@ -1,29 +1,25 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { PermissionButton } from "../../../../utils/permissions.js"; import { CippPolicyDeployDrawer } from "../../../../components/CippComponents/CippPolicyDeployDrawer.jsx"; import { useSettings } from "../../../../hooks/use-settings.js"; import { useCippIntunePolicyActions } from "../../../../components/CippComponents/CippIntunePolicyActions.jsx"; -import { Sync, CloudDone, Bolt } from "@mui/icons-material"; -import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; import { Stack } from "@mui/system"; -import { useDialog } from "../../../../hooks/use-dialog"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; -import { useEffect, useState } from "react"; const Page = () => { const pageTitle = "Intune Compliance Policies"; const cardButtonPermissions = ["Endpoint.MEM.ReadWrite"]; const tenant = useSettings().currentTenant; - const isAllTenants = tenant === "AllTenants"; - const syncDialog = useDialog(); - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(isAllTenants); - useEffect(() => { - setUseReportDB(tenant === "AllTenants"); - }, [tenant]); + const reportDB = useCippReportDB({ + apiUrl: "/api/ListCompliancePolicies", + queryKey: "ListCompliancePolicies", + cacheName: "IntuneCompliancePolicies", + syncTitle: "Sync Compliance Policies Report", + allowToggle: true, + defaultCached: false, + }); const actions = useCippIntunePolicyActions(tenant, "deviceCompliancePolicies", { templateData: { @@ -44,8 +40,7 @@ const Page = () => { }; const simpleColumns = [ - ...(useReportDB ? ["CacheTimestamp"] : []), - ...(useReportDB && isAllTenants ? ["Tenant"] : []), + ...reportDB.cacheColumns, "displayName", "PolicyTypeName", "PolicyAssignment", @@ -54,59 +49,12 @@ const Page = () => { "lastModifiedDateTime", ]; - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? "Cached" : "Live"} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ]; - return ( <> { requiredPermissions={cardButtonPermissions} PermissionButton={PermissionButton} /> - {pageActions} + {reportDB.controls} } /> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId); - } - }, - }} - /> + {reportDB.syncDialog} ); }; diff --git a/src/pages/endpoint/MEM/list-scripts/index.jsx b/src/pages/endpoint/MEM/list-scripts/index.jsx index 11b480fae484..07abf51f5fc1 100644 --- a/src/pages/endpoint/MEM/list-scripts/index.jsx +++ b/src/pages/endpoint/MEM/list-scripts/index.jsx @@ -1,6 +1,5 @@ import { Layout as DashboardLayout } from "../../../../layouts/index"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage"; -import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; import { TrashIcon, PencilIcon, @@ -17,19 +16,15 @@ import { IconButton, CircularProgress, DialogActions, - Chip, - SvgIcon, - Tooltip, } from "@mui/material"; import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; import { useState, useEffect, useMemo } from "react"; import { useDispatch } from "react-redux"; -import { Close, Save, LaptopChromebook, Sync, CloudDone, Bolt } from "@mui/icons-material"; +import { Close, Save, LaptopChromebook } from "@mui/icons-material"; import { useSettings } from "../../../../hooks/use-settings"; -import { useDialog } from "../../../../hooks/use-dialog"; import { Stack } from "@mui/system"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const assignmentModeOptions = [ { label: "Replace existing assignments", value: "replace" }, @@ -48,14 +43,14 @@ const Page = () => { const [scriptTenant, setScriptTenant] = useState(null); const tenantFilter = useSettings().currentTenant; - const isAllTenants = tenantFilter === "AllTenants"; - const syncDialog = useDialog(); - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(isAllTenants); - - useEffect(() => { - setUseReportDB(tenantFilter === "AllTenants"); - }, [tenantFilter]); + const reportDB = useCippReportDB({ + apiUrl: "/api/ListIntuneScript", + queryKey: "ListIntuneScript", + cacheName: "IntuneScripts", + syncTitle: "Sync Intune Scripts Report", + allowToggle: true, + defaultCached: false, + }); const dispatch = useDispatch(); @@ -372,8 +367,7 @@ const Page = () => { }; const simpleColumns = [ - ...(useReportDB ? ["CacheTimestamp"] : []), - ...(useReportDB && isAllTenants ? ["Tenant"] : []), + ...reportDB.cacheColumns, "scriptType", "displayName", "ScriptAssignment", @@ -383,63 +377,16 @@ const Page = () => { "lastModifiedDateTime", ]; - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? "Cached" : "Live"} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ]; - return ( <> @@ -504,25 +451,7 @@ const Page = () => { - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId); - } - }, - }} - /> + {reportDB.syncDialog} ); }; diff --git a/src/pages/endpoint/MEM/reusable-settings/index.js b/src/pages/endpoint/MEM/reusable-settings/index.js index 0c3837e51a0c..75219f0d4136 100644 --- a/src/pages/endpoint/MEM/reusable-settings/index.js +++ b/src/pages/endpoint/MEM/reusable-settings/index.js @@ -1,27 +1,25 @@ -import { Book, DeleteForever, Sync, CloudDone, Bolt } from "@mui/icons-material"; +import { Book, DeleteForever } from "@mui/icons-material"; import { CippReusableSettingsDeployDrawer } from "../../../../components/CippComponents/CippReusableSettingsDeployDrawer.jsx"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { useSettings } from "../../../../hooks/use-settings"; import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; -import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; import { Stack } from "@mui/system"; -import { useDialog } from "../../../../hooks/use-dialog"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; -import { useEffect, useState } from "react"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const Page = () => { const { currentTenant } = useSettings(); const pageTitle = "Reusable Settings"; - const isAllTenants = currentTenant === "AllTenants"; - const syncDialog = useDialog(); - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(isAllTenants); + const reportDB = useCippReportDB({ + apiUrl: "/api/ListIntuneReusableSettings", + queryKey: "ListIntuneReusableSettings", + cacheName: "IntuneReusableSettings", + syncTitle: "Sync Reusable Settings Report", + allowToggle: true, + defaultCached: false, + }); + const isAllTenants = reportDB.isAllTenants; - useEffect(() => { - setUseReportDB(currentTenant === "AllTenants"); - }, [currentTenant]); const actions = [ { @@ -64,61 +62,13 @@ const Page = () => { }; const simpleColumns = [ - ...(useReportDB ? ["CacheTimestamp"] : []), - ...(useReportDB && isAllTenants ? ["Tenant"] : []), + ...reportDB.cacheColumns, "displayName", "description", "id", "version", ]; - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? "Cached" : "Live"} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ]; - return ( <> { cardButton={ - {pageActions} + {reportDB.controls} } - apiUrl={`/api/ListIntuneReusableSettings${useReportDB ? "?UseReportDB=true" : ""}`} - queryKey={`ListIntuneReusableSettings-${currentTenant}-${useReportDB}`} + apiUrl={reportDB.resolvedApiUrl} + queryKey={reportDB.resolvedQueryKey} actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} /> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId); - } - }, - }} - /> + {reportDB.syncDialog} ); }; diff --git a/src/pages/endpoint/applications/list/index.js b/src/pages/endpoint/applications/list/index.js index 4b8b60239a47..fec79a4cf7f9 100644 --- a/src/pages/endpoint/applications/list/index.js +++ b/src/pages/endpoint/applications/list/index.js @@ -2,14 +2,13 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog.jsx"; import { GlobeAltIcon, TrashIcon, UserIcon, UserGroupIcon } from "@heroicons/react/24/outline"; -import { LaptopMac, Sync, BookmarkAdd, CloudDone, Bolt } from "@mui/icons-material"; +import { LaptopMac, Sync, BookmarkAdd } from "@mui/icons-material"; import { CippApplicationDeployDrawer } from "../../../../components/CippComponents/CippApplicationDeployDrawer"; -import { Button, Chip, SvgIcon, Tooltip } from "@mui/material"; +import { Button } from "@mui/material"; import { Stack } from "@mui/system"; import { useSettings } from "../../../../hooks/use-settings.js"; import { useDialog } from "../../../../hooks/use-dialog.js"; -import { CippQueueTracker } from "../../../../components/CippTable/CippQueueTracker"; -import { useEffect, useState } from "react"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const assignmentIntentOptions = [ { label: "Required", value: "Required" }, @@ -48,15 +47,16 @@ const mapOdataToAppType = (odataType) => { const Page = () => { const pageTitle = "Applications"; const vppSyncDialog = useDialog(); - const cacheSyncDialog = useDialog(); const tenant = useSettings().currentTenant; - const isAllTenants = tenant === "AllTenants"; - const [syncQueueId, setSyncQueueId] = useState(null); - const [useReportDB, setUseReportDB] = useState(isAllTenants); - useEffect(() => { - setUseReportDB(tenant === "AllTenants"); - }, [tenant]); + const reportDB = useCippReportDB({ + apiUrl: "/api/ListApps", + queryKey: "ListApps", + cacheName: "IntuneApplications", + syncTitle: "Sync Intune Applications Report", + allowToggle: true, + defaultCached: false, + }); const getAssignmentFilterFields = () => [ { @@ -302,8 +302,7 @@ const Page = () => { }; const simpleColumns = [ - ...(useReportDB ? ["CacheTimestamp"] : []), - ...(useReportDB && isAllTenants ? ["Tenant"] : []), + ...reportDB.cacheColumns, "displayName", "AppAssignment", "AppExclude", @@ -312,69 +311,22 @@ const Page = () => { "createdDateTime", ]; - const pageActions = [ - - {useReportDB && ( - <> - - - - )} - - - : } - label={useReportDB ? "Cached" : "Live"} - color="primary" - size="small" - onClick={isAllTenants ? undefined : () => setUseReportDB((prev) => !prev)} - clickable={!isAllTenants} - disabled={isAllTenants} - variant="outlined" - /> - - - , - ]; - return ( <> - {pageActions} + {reportDB.controls} } /> @@ -388,25 +340,7 @@ const Page = () => { confirmText: `Are you sure you want to sync Apple Volume Purchase Program (VPP) tokens? This will sync all VPP tokens for ${tenant}.`, }} /> - { - if (result?.Metadata?.QueueId) { - setSyncQueueId(result?.Metadata?.QueueId); - } - }, - }} - /> + {reportDB.syncDialog} ); }; From d7d36a31eb86f6b6f011d483862024515f63757d Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Sun, 26 Apr 2026 19:13:54 +0200 Subject: [PATCH 13/86] feat: Add allTenants support for all the Teams SharePoint pages --- src/pages/teams-share/onedrive/index.js | 51 ++++++++---- src/pages/teams-share/sharepoint/index.js | 82 +++++++++++-------- .../teams-share/teams/business-voice/index.js | 68 +++++++++------ .../teams-share/teams/list-team/index.js | 51 ++++++++---- .../teams-share/teams/teams-activity/index.js | 34 ++++++-- 5 files changed, 191 insertions(+), 95 deletions(-) diff --git a/src/pages/teams-share/onedrive/index.js b/src/pages/teams-share/onedrive/index.js index 8d279cffaf73..82c89d2c2072 100644 --- a/src/pages/teams-share/onedrive/index.js +++ b/src/pages/teams-share/onedrive/index.js @@ -1,10 +1,21 @@ import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; +import { useCippReportDB } from "../../../components/CippComponents/CippReportDBControls"; import { PersonAdd, PersonRemove } from "@mui/icons-material"; const Page = () => { const pageTitle = "OneDrive"; + const reportDB = useCippReportDB({ + apiUrl: "/api/ListSites?type=OneDriveUsageAccount", + queryKey: "ListSites-OneDriveUsageAccount", + cacheName: "Sites", + syncTitle: "Sync OneDrive Report", + syncData: { Types: "OneDriveUsageAccount" }, + allowToggle: true, + defaultCached: false, + }); + const actions = [ { label: "Add permissions to OneDrive", @@ -77,25 +88,31 @@ const Page = () => { ]; return ( - + <> + + {reportDB.syncDialog} + ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index 21cefc406ca4..0e751f10aa49 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -1,6 +1,7 @@ import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Stack } from "@mui/system"; import { Add, AddToPhotos, @@ -13,11 +14,22 @@ import { import Link from "next/link"; import { CippDataTable } from "../../../components/CippTable/CippDataTable"; import { useSettings } from "../../../hooks/use-settings"; +import { useCippReportDB } from "../../../components/CippComponents/CippReportDBControls"; const Page = () => { const pageTitle = "SharePoint Sites"; const tenantFilter = useSettings().currentTenant; + const reportDB = useCippReportDB({ + apiUrl: "/api/ListSites?type=SharePointSiteUsage", + queryKey: "ListSites-SharePointSiteUsage", + cacheName: "Sites", + syncTitle: "Sync SharePoint Sites Report", + syncData: { Types: "SharePointSiteUsage" }, + allowToggle: true, + defaultCached: true, + }); + const actions = [ { label: "Add Member", @@ -213,40 +225,46 @@ const Page = () => { }; return ( - - - - - } - /> + <> + + + + {reportDB.controls} + + } + /> + {reportDB.syncDialog} + ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/teams-share/teams/business-voice/index.js b/src/pages/teams-share/teams/business-voice/index.js index a3aa56764153..3c9354706147 100644 --- a/src/pages/teams-share/teams/business-voice/index.js +++ b/src/pages/teams-share/teams/business-voice/index.js @@ -1,10 +1,20 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; import { PersonAdd, PersonRemove, LocationOn } from "@mui/icons-material"; const Page = () => { const pageTitle = "Teams Business Voice"; + const reportDB = useCippReportDB({ + apiUrl: "/api/ListTeamsVoice", + queryKey: "ListTeamsVoice", + cacheName: "TeamsVoice", + syncTitle: "Sync Teams Business Voice Report", + allowToggle: true, + defaultCached: false, + }); + const actions = [ // the modal dropdowns that were added below may not exist yet, and will need to be tested. { @@ -81,34 +91,40 @@ const Page = () => { }; return ( - + <> + + {reportDB.syncDialog} + ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/teams-share/teams/list-team/index.js b/src/pages/teams-share/teams/list-team/index.js index bf48cccb08a3..99b51994bafe 100644 --- a/src/pages/teams-share/teams/list-team/index.js +++ b/src/pages/teams-share/teams/list-team/index.js @@ -1,13 +1,24 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; import { Button } from "@mui/material"; +import { Stack } from "@mui/system"; import { Delete, GroupAdd } from "@mui/icons-material"; import Link from "next/link"; import { Edit } from "@mui/icons-material"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const Page = () => { const pageTitle = "Teams"; + const reportDB = useCippReportDB({ + apiUrl: "/api/ListTeams?type=list", + queryKey: "ListTeams-list", + cacheName: "Teams", + syncTitle: "Sync Teams Report", + allowToggle: true, + defaultCached: false, + }); + const actions = [ { label: "Edit Group", @@ -32,22 +43,34 @@ const Page = () => { ]; return ( - - - - } - /> + <> + + + {reportDB.controls} + + } + /> + {reportDB.syncDialog} + ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; diff --git a/src/pages/teams-share/teams/teams-activity/index.js b/src/pages/teams-share/teams/teams-activity/index.js index 2f2797a57cbb..f5fb2bb53754 100644 --- a/src/pages/teams-share/teams/teams-activity/index.js +++ b/src/pages/teams-share/teams/teams-activity/index.js @@ -1,18 +1,40 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; const Page = () => { const pageTitle = "Teams Activity List"; + const reportDB = useCippReportDB({ + apiUrl: "/api/ListTeamsActivity?type=TeamsUserActivityUser", + queryKey: "ListTeamsActivity-TeamsUserActivityUser", + cacheName: "TeamsActivity", + syncTitle: "Sync Teams Activity Report", + allowToggle: true, + defaultCached: false, + }); + return ( - + <> + + {reportDB.syncDialog} + ); }; -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page}; export default Page; From 4d09517912e6008e2ef0a4ad3e0a6ab260cc1a2d Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:13:10 +0200 Subject: [PATCH 14/86] feat(mem): use tooltip-enabled policy details in compare view --- .../endpoint/MEM/compare-policies/index.js | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/pages/endpoint/MEM/compare-policies/index.js b/src/pages/endpoint/MEM/compare-policies/index.js index da74c739462b..b634a6335839 100644 --- a/src/pages/endpoint/MEM/compare-policies/index.js +++ b/src/pages/endpoint/MEM/compare-policies/index.js @@ -4,7 +4,7 @@ import { ApiPostCall } from "../../../../api/ApiCall"; import { CippFormComponent } from "../../../../components/CippComponents/CippFormComponent"; import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; -import { CippCodeBlock } from "../../../../components/CippComponents/CippCodeBlock"; +import CippJsonView from "../../../../components/CippFormPages/CippJSONView"; import { Box, Button, @@ -19,16 +19,12 @@ import { TableHead, TableRow, Paper, - Accordion, - AccordionSummary, - AccordionDetails, Alert, Stack, Chip, Skeleton, } from "@mui/material"; import { - ExpandMore as ExpandMoreIcon, CompareArrows as CompareArrowsIcon, CheckCircle as CheckCircleIcon, Error as ErrorIcon, @@ -359,15 +355,6 @@ const Page = () => { return errData?.Results || compareApi.error?.message || "An error occurred"; }, [compareApi.isError, compareApi.error]); - const sourceAJson = useMemo( - () => (results?.sourceAData ? JSON.stringify(results.sourceAData, null, 2) : ""), - [results?.sourceAData], - ); - const sourceBJson = useMemo( - () => (results?.sourceBData ? JSON.stringify(results.sourceBData, null, 2) : ""), - [results?.sourceBData], - ); - return ( @@ -457,23 +444,17 @@ const Page = () => { )} - - }> - Source A Raw JSON — {results.sourceALabel} - - - - - - - - }> - Source B Raw JSON — {results.sourceBLabel} - - - - - + + + )} From 17077aa800c59b90ce4ac6e6b16ecf502c9b0e15 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 30 Apr 2026 19:51:30 +0200 Subject: [PATCH 15/86] feat(compare): add null safety --- src/pages/endpoint/MEM/compare-policies/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/endpoint/MEM/compare-policies/index.js b/src/pages/endpoint/MEM/compare-policies/index.js index b634a6335839..80b660e666a1 100644 --- a/src/pages/endpoint/MEM/compare-policies/index.js +++ b/src/pages/endpoint/MEM/compare-policies/index.js @@ -355,6 +355,11 @@ const Page = () => { return errData?.Results || compareApi.error?.message || "An error occurred"; }, [compareApi.isError, compareApi.error]); + const comparisonRows = useMemo(() => { + if (!Array.isArray(results?.Results)) return []; + return results.Results.filter(Boolean); + }, [results?.Results]); + return ( @@ -405,14 +410,14 @@ const Page = () => { > {results.identical ? "Policies are identical - no differences found." - : `${results.Results?.length || 0} difference${results.Results?.length === 1 ? "" : "s"} found between policies.`} + : `${comparisonRows.length} difference${comparisonRows.length === 1 ? "" : "s"} found between policies.`} A: {results.sourceALabel} — B:{" "} {results.sourceBLabel} - {!results.identical && results.Results?.length > 0 && ( + {!results.identical && comparisonRows.length > 0 && ( @@ -424,7 +429,7 @@ const Page = () => { - {results.Results.map((row, index) => ( + {comparisonRows.map((row, index) => ( ({ From 1bcf7bd7a9446066f0960e76d89beb832ea16afb Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 1 May 2026 00:53:23 +0200 Subject: [PATCH 16/86] feat(intune): show administrative template policy details Add tenant-aware definition lookup and presentation rendering so administrative template settings can be read in detail views. --- .../CippIntunePolicyDetails.jsx | 22 +- src/components/CippFormPages/CippJSONView.jsx | 287 +++++++++++++++++- src/hooks/use-admin-template-definitions.js | 82 +++++ src/utils/intune-bind-helpers.js | 14 + 4 files changed, 386 insertions(+), 19 deletions(-) create mode 100644 src/hooks/use-admin-template-definitions.js create mode 100644 src/utils/intune-bind-helpers.js diff --git a/src/components/CippComponents/CippIntunePolicyDetails.jsx b/src/components/CippComponents/CippIntunePolicyDetails.jsx index ca822e61739a..064230da3e18 100644 --- a/src/components/CippComponents/CippIntunePolicyDetails.jsx +++ b/src/components/CippComponents/CippIntunePolicyDetails.jsx @@ -4,37 +4,41 @@ import CippJsonView from '../CippFormPages/CippJSONView' export const CippIntunePolicyDetails = ({ row, tenant }) => { const isConfigurationPolicy = row?.URLName?.toLowerCase() === 'configurationpolicies' + const isAdministrativeTemplate = row?.URLName?.toLowerCase() === 'grouppolicyconfigurations' + const isSupportedPolicyType = isConfigurationPolicy || isAdministrativeTemplate + const urlName = isAdministrativeTemplate ? 'groupPolicyConfigurations' : 'configurationPolicies' + const policyTypeLabel = isAdministrativeTemplate ? 'Administrative Template' : 'Settings Catalog' const tenantFilter = tenant === 'AllTenants' && row?.Tenant ? row.Tenant : tenant const policyDetails = ApiGetCall({ url: '/api/ListIntunePolicy', - queryKey: `ListIntunePolicyDetails-${tenantFilter}-${row?.id}`, + queryKey: `ListIntunePolicyDetails-${urlName}-${tenantFilter}-${row?.id}`, data: { TenantFilter: tenantFilter, ID: row?.id, - URLName: 'configurationPolicies', + URLName: urlName, IncludeSettingDefinitions: true, }, - waiting: Boolean(isConfigurationPolicy && tenantFilter && row?.id), + waiting: Boolean(isSupportedPolicyType && tenantFilter && row?.id), retry: 1, refetchOnWindowFocus: false, toast: false, }) - if (!isConfigurationPolicy) { + if (!isSupportedPolicyType) { return null } const details = Array.isArray(policyDetails.data) ? policyDetails.data[0] : policyDetails.data - const fallbackDetails = row?.settings ? row : null - const settingsObject = details?.settings ? details : fallbackDetails + const fallbackDetails = row?.settings || row?.definitionValues ? row : null + const settingsObject = details?.settings || details?.definitionValues ? details : fallbackDetails if (policyDetails.isLoading || policyDetails.isFetching) { return ( - Loading policy settings and Microsoft descriptions... + Loading policy details and Microsoft descriptions... ) @@ -43,7 +47,7 @@ export const CippIntunePolicyDetails = ({ row, tenant }) => { if (policyDetails.isError && !settingsObject) { return ( - Could not load live Settings Catalog details for this policy. + Could not load live {policyTypeLabel} details for this policy. ) } @@ -51,7 +55,7 @@ export const CippIntunePolicyDetails = ({ row, tenant }) => { if (!settingsObject) { return ( - This Settings Catalog policy did not return any settings. + This {policyTypeLabel} policy did not return any settings. ) } diff --git a/src/components/CippFormPages/CippJSONView.jsx b/src/components/CippFormPages/CippJSONView.jsx index cc7fe72e749e..f5044792b149 100644 --- a/src/components/CippFormPages/CippJSONView.jsx +++ b/src/components/CippFormPages/CippJSONView.jsx @@ -24,6 +24,12 @@ import { getCippFormatting } from '../../utils/get-cipp-formatting' import { CippCodeBlock } from '../CippComponents/CippCodeBlock' import intuneCollection from '../../data/intuneCollection.json' import { useGuidResolver } from '../../hooks/use-guid-resolver' +import { useAdminTemplateDefinitions } from '../../hooks/use-admin-template-definitions' +import { + definitionBindPattern, + presentationBindPattern, + extractBindGuid, +} from '../../utils/intune-bind-helpers' const intuneCollectionMap = new Map( (intuneCollection || []).filter((item) => item?.id).map((item) => [item.id, item]) @@ -250,7 +256,21 @@ function CippJsonView({ // Use the GUID resolver hook const { guidMapping, isLoadingGuids, resolveGuids, isGuid } = useGuidResolver() const resolvedType = - type || (object?.omaSettings || object?.settings || object?.added ? 'intune' : undefined) + type || + (object?.omaSettings || object?.settings || object?.definitionValues || object?.added + ? 'intune' + : undefined) + const adminTemplateTenant = + object?.Tenant || object?.tenant || object?.TenantFilter || object?.tenantFilter || null + const { + definitionsMap: addedDefinitionsMap, + isLoadingDefinitions, + isDefinitionsError, + } = useAdminTemplateDefinitions({ + added: object?.added, + manualTenant: adminTemplateTenant, + waiting: resolvedType === 'intune', + }) const renderIntuneItems = (data) => { const items = [] @@ -292,7 +312,7 @@ function CippJsonView({ } const renderDefinitionTooltip = (definition, optionDefinition) => { - const description = definition?.helpText || definition?.description + const description = definition?.helpText || definition?.description || definition?.explainText const optionDescription = optionDefinition?.helpText || optionDefinition?.description const infoUrls = Array.isArray(definition?.infoUrls) ? definition.infoUrls : [] @@ -395,6 +415,185 @@ function CippJsonView({ } } + const getDisplayValue = (value) => { + if (value === null || value === undefined || value === '') { + return '' + } + + if (typeof value === 'boolean') { + return value ? 'Yes' : 'No' + } + + if (typeof value === 'string' || typeof value === 'number') { + return value + } + + try { + return JSON.stringify(value) + } catch { + return String(value) + } + } + + const getAdministrativeTemplatePresentationValue = (presentationValue) => { + if (!presentationValue || typeof presentationValue !== 'object') { + return 'Not configured' + } + + if (Object.prototype.hasOwnProperty.call(presentationValue, 'value')) { + const displayValue = getDisplayValue(presentationValue.value) + if (displayValue !== '') { + return displayValue + } + } + + if (Array.isArray(presentationValue.values)) { + const values = presentationValue.values + .map((entry) => { + if (entry && typeof entry === 'object') { + const entryLabel = + entry.name || + entry.key || + entry.displayName || + entry.id || + entry.PresentationDefinitionLabel || + '' + const entryValue = getDisplayValue( + entry.value ?? entry.text ?? entry.Value ?? entry.StringValue ?? entry + ) + + if (entryLabel && entryValue !== '') { + return `${entryLabel}: ${entryValue}` + } + + return entryValue + } + + return getDisplayValue(entry) + }) + .filter((entry) => entry !== null && entry !== undefined && entry !== '') + + if (values.length > 0) { + return values.join(', ') + } + } + + return 'Not configured' + } + + const getStatusText = (enabled) => + enabled === true ? 'Enabled' : enabled === false ? 'Disabled' : 'Configured' + + const addAdministrativeTemplateValue = ( + value, + index, + { definition, definitionId, label, keyPrefix, presentationKeyInfix, resolvePresentationLabel } + ) => { + if (!value || typeof value !== 'object') { + return + } + + const categoryPath = definition?.categoryPath + const presentationValues = Array.isArray(value.presentationValues) + ? value.presentationValues + : [] + const itemKey = value.id || definitionId || index + + items.push( + + + {getStatusText(value.enabled)} + + {categoryPath && ( + + {categoryPath} + + )} + {!definition && definitionId && ( + + Definition ID: {definitionId} + + )} + {presentationValues.map((presentationValue, presentationIndex) => { + const presentationLabel = resolvePresentationLabel( + definition, + presentationValue, + presentationIndex + ) + const presentationDisplayValue = getAdministrativeTemplatePresentationValue( + presentationValue + ) + + return ( + + + {presentationLabel}: + {' '} + {renderSettingValue(presentationDisplayValue)} + + ) + })} + + } + /> + ) + } + + const getPresentationTypeLabel = (odataType) => { + switch (odataType) { + case '#microsoft.graph.groupPolicyPresentationValueBoolean': + return 'Boolean value' + case '#microsoft.graph.groupPolicyPresentationValueDecimal': + case '#microsoft.graph.groupPolicyPresentationValueLongDecimal': + return 'Numeric value' + case '#microsoft.graph.groupPolicyPresentationValueMultiText': + return 'Text list' + case '#microsoft.graph.groupPolicyPresentationValueList': + return 'List value' + case '#microsoft.graph.groupPolicyPresentationValueText': + return 'Text value' + default: + return 'Value' + } + } + + const getAddedPresentationLabel = (definition, presentationValue) => { + const presentationId = extractBindGuid( + presentationValue?.['presentation@odata.bind'], + presentationBindPattern + ) + + if (presentationId && Array.isArray(definition?.presentations)) { + const resolvedPresentation = definition.presentations.find( + (presentation) => String(presentation?.id || '').toLowerCase() === presentationId + ) + + if (resolvedPresentation) { + return ( + resolvedPresentation.label || + resolvedPresentation.displayName || + resolvedPresentation.id || + getPresentationTypeLabel(presentationValue?.['@odata.type']) + ) + } + } + + return getPresentationTypeLabel(presentationValue?.['@odata.type']) + } + + const resolveLivePresentationLabel = (_definition, presentationValue, presentationIndex) => + presentationValue?.presentation?.label || + presentationValue?.presentation?.displayName || + presentationValue?.presentation?.id || + `Value ${presentationIndex + 1}` + const addSettingInstance = (settingInstance, setting, keyPrefix) => { if (!settingInstance) { return @@ -498,14 +697,82 @@ function CippJsonView({ data.settings.forEach((setting, index) => { addSettingInstance(setting.settingInstance, setting, `setting-${index}`) }) - } else if (data.added) { - items.push( - - ) + } else if (Array.isArray(data.definitionValues)) { + if (data.definitionValues.length === 0) { + items.push( + + ) + } + + data.definitionValues.forEach((definitionValue, index) => { + const definition = definitionValue?.definition + addAdministrativeTemplateValue(definitionValue, index, { + definition, + definitionId: null, + label: definition?.displayName || definition?.id || definitionValue?.id || 'Setting', + keyPrefix: 'definitionValue', + presentationKeyInfix: 'presentation', + resolvePresentationLabel: resolveLivePresentationLabel, + }) + }) + } else if (Array.isArray(data.added)) { + const hasResolvedDefinitions = Object.keys(addedDefinitionsMap).length > 0 + + if (isLoadingDefinitions && !hasResolvedDefinitions) { + items.push( + + + Resolving administrative template settings... + + } + /> + ) + return items + } + + if (data.added.length === 0) { + items.push( + + ) + } + + if (isDefinitionsError && !hasResolvedDefinitions) { + items.push( + + ) + } + + data.added.forEach((addedValue, index) => { + const definitionId = extractBindGuid( + addedValue?.['definition@odata.bind'], + definitionBindPattern + ) + const definition = definitionId ? addedDefinitionsMap[definitionId] : null + addAdministrativeTemplateValue(addedValue, index, { + definition, + definitionId, + label: definition?.displayName || definitionId || `Setting ${index + 1}`, + keyPrefix: 'addedDefinition', + presentationKeyInfix: 'added-presentation', + resolvePresentationLabel: getAddedPresentationLabel, + }) + }) } else { Object.entries(data).forEach(([key, value]) => { // Check if value is a GUID that we've resolved diff --git a/src/hooks/use-admin-template-definitions.js b/src/hooks/use-admin-template-definitions.js new file mode 100644 index 000000000000..8b129daeae27 --- /dev/null +++ b/src/hooks/use-admin-template-definitions.js @@ -0,0 +1,82 @@ +// Resolves groupPolicyDefinitions metadata per-tenant. Can't be a static JSON: +// tenants can import custom ADMX files, so the available definitions are +// tenant-specific. +import { useMemo } from 'react' +import { ApiGetCall } from '../api/ApiCall' +import { useSettings } from './use-settings' +import { definitionBindPattern, extractBindGuid } from '../utils/intune-bind-helpers' + +export const useAdminTemplateDefinitions = ({ added = [], manualTenant = null, waiting = true } = {}) => { + const tenantFilter = useSettings().currentTenant + const activeTenant = manualTenant || tenantFilter + + const definitionIds = useMemo(() => { + if (!Array.isArray(added)) { + return [] + } + + const ids = new Set() + added.forEach((item) => { + const definitionId = extractBindGuid(item?.['definition@odata.bind'], definitionBindPattern) + if (definitionId) { + ids.add(definitionId) + } + }) + + return Array.from(ids).sort() + }, [added]) + + const canResolveDefinitions = + waiting && Boolean(activeTenant) && activeTenant !== 'AllTenants' && definitionIds.length > 0 + + const definitionsRequest = ApiGetCall({ + url: '/api/ListIntunePolicy', + queryKey: `AdminTemplateDefinitions-${activeTenant}-${definitionIds.join(',') || 'none'}`, + data: { + TenantFilter: activeTenant, + URLName: 'GroupPolicyDefinitions', + DefinitionIds: definitionIds.join(','), + }, + waiting: canResolveDefinitions, + retry: 1, + refetchOnWindowFocus: false, + refetchOnMount: false, + toast: false, + staleTime: 15 * 60 * 1000, + }) + + const definitions = useMemo(() => { + if (Array.isArray(definitionsRequest.data)) { + return definitionsRequest.data + } + + if (Array.isArray(definitionsRequest.data?.Results)) { + return definitionsRequest.data.Results + } + + if (Array.isArray(definitionsRequest.data?.value)) { + return definitionsRequest.data.value + } + + return [] + }, [definitionsRequest.data]) + + const definitionsMap = useMemo(() => { + const mapping = {} + + definitions.forEach((definition) => { + if (definition?.id) { + mapping[String(definition.id).toLowerCase()] = definition + } + }) + + return mapping + }, [definitions]) + + return { + definitionsMap, + isLoadingDefinitions: + canResolveDefinitions && (definitionsRequest.isLoading || definitionsRequest.isFetching), + isDefinitionsError: canResolveDefinitions && definitionsRequest.isError, + } +} diff --git a/src/utils/intune-bind-helpers.js b/src/utils/intune-bind-helpers.js new file mode 100644 index 000000000000..6ff61ff0d5bf --- /dev/null +++ b/src/utils/intune-bind-helpers.js @@ -0,0 +1,14 @@ +// Parsers for Intune Admin Template @odata.bind refs (e.g. `groupPolicyDefinitions('GUID')`). +// Shared so the hook and CippJSONView renderer can't drift. + +export const definitionBindPattern = /groupPolicyDefinitions\('([0-9a-f-]{36})'\)/i +export const presentationBindPattern = /presentations\('([0-9a-f-]{36})'\)/i + +export const extractBindGuid = (value, pattern) => { + if (typeof value !== 'string') { + return null + } + + const match = value.match(pattern) + return match?.[1]?.toLowerCase() || null +} From 5828c0067c995be575c43acb781216019aa493b1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 5 May 2026 14:54:41 +0200 Subject: [PATCH 17/86] update standards.json for SMB1001 --- src/data/standards.json | 101 ++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 34 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index ece980ca1c94..124ac1b3b3a9 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -339,7 +339,8 @@ "NIST CSF 2.0 (PR.AA-05)", "EIDSCAAP07", "EIDSCAST08", - "EIDSCAST09" + "EIDSCAST09", + "SMB1001 (2.8)" ], "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)", @@ -413,7 +414,7 @@ { "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.6)", "EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", "EIDSCAAG02", "EIDSCAAG03"], + "tag": ["CIS M365 6.0.1 (5.2.3.6)", "EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", "EIDSCAAG02", "EIDSCAAG03", "SMB1001 (2.8)"], "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.", @@ -553,7 +554,7 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.5)", "ZTNA21953", "ZTNA21955", "ZTNA24560"], + "tag": ["CIS M365 6.0.1 (5.1.4.5)", "ZTNA21953", "ZTNA21955", "ZTNA24560", "SMB1001 (2.2)"], "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.", @@ -655,7 +656,10 @@ "EIDSCAAF03", "EIDSCAAF04", "EIDSCAAF05", - "EIDSCAAF06" + "EIDSCAAF06", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" ], "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.", @@ -671,7 +675,7 @@ { "name": "standards.EnableHardwareOAuth", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.", @@ -767,7 +771,8 @@ "EIDSCAPR02", "EIDSCAPR03", "EIDSCAPR05", - "EIDSCAPR06" + "EIDSCAPR06", + "SMB1001 (2.1)" ], "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)", @@ -817,7 +822,7 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "ZTNA21772", "ZTNA21787"], + "tag": ["CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "ZTNA21772", "ZTNA21787", "SMB1001 (2.8)"], "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.", @@ -868,7 +873,7 @@ { "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21889"], + "tag": ["ZTNA21889", "SMB1001 (2.5)"], "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.", @@ -905,7 +910,7 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.21.1v1)", "ZTNA21868"], + "tag": ["CISA (MS.AAD.21.1v1)", "ZTNA21868", "SMB1001 (2.8)"], "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.", @@ -934,7 +939,8 @@ "EIDSCA.AP10", "Essential 8 (1175)", "NIST CSF 2.0 (PR.AA-05)", - "EIDSCAAP10" + "EIDSCAAP10", + "SMB1001 (2.8)" ], "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.", @@ -977,7 +983,7 @@ { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.3.2)", "CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA21868"], + "tag": ["CIS M365 6.0.1 (5.1.3.2)", "CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA21868", "SMB1001 (2.8)"], "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": [], @@ -1005,7 +1011,7 @@ { "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (1.3.4)"], + "tag": ["CIS M365 6.0.1 (1.3.4)", "SMB1001 (2.8)"], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", "addedComponent": [ @@ -1031,7 +1037,7 @@ { "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21858"], + "tag": ["ZTNA21858", "SMB1001 (2.8)"], "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": [ @@ -1105,7 +1111,7 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "EIDSCAAP04"], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "EIDSCAAP04", "SMB1001 (2.8)"], "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": [ @@ -1177,7 +1183,7 @@ { "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.11.1v1)", "ZTNA21843"], + "tag": ["CISA (MS.AAD.11.1v1)", "ZTNA21843", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], "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.", @@ -1192,7 +1198,7 @@ { "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAS04"], + "tag": ["CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAS04", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], "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.", @@ -1207,7 +1213,7 @@ { "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAV01"], + "tag": ["CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAV01", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], "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.", @@ -1222,7 +1228,7 @@ { "name": "standards.DisableEmail", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.7)", "NIST CSF 2.0 (PR.AA-03)"], + "tag": ["CIS M365 6.0.1 (5.2.3.7)", "NIST CSF 2.0 (PR.AA-03)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], @@ -1275,7 +1281,10 @@ "NIST CSF 2.0 (PR.AA-03)", "ZTNA21780", "ZTNA21782", - "ZTNA21796" + "ZTNA21796", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" ], "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.", @@ -1590,7 +1599,7 @@ { "name": "standards.EnableOnlineArchiving", "cat": "Exchange Standards", - "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)"], + "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)", "SMB1001 (3.1)"], "helpText": "Enables the In-Place Online Archive for all UserMailboxes with a valid license.", "executiveText": "Automatically enables online email archiving for all licensed employees, providing additional storage for older emails while maintaining easy access. This helps manage mailbox sizes, improves email performance, and supports compliance with data retention requirements.", "addedComponent": [], @@ -1611,7 +1620,7 @@ { "name": "standards.EnableLitigationHold", "cat": "Exchange Standards", - "tag": [], + "tag": ["SMB1001 (3.1)"], "helpText": "Enables litigation hold for all UserMailboxes with a valid license.", "executiveText": "Preserves all email content for legal and compliance purposes by preventing permanent deletion of emails, even when users attempt to delete them. This is essential for organizations subject to legal discovery requirements or regulatory compliance mandates.", "addedComponent": [ @@ -1758,7 +1767,7 @@ { "name": "standards.RotateDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.9)"], + "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", "addedComponent": [], @@ -1811,7 +1820,7 @@ { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.9)", "ORCA108", "CISAMSEXO31"], + "tag": ["CIS M365 6.0.1 (2.1.9)", "ORCA108", "CISAMSEXO31", "SMB1001 (2.12)"], "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": [], @@ -1832,7 +1841,7 @@ { "name": "standards.AddDMARCToMOERA", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (2.1.10)", "Security", "PhishingProtection"], + "tag": ["CIS M365 6.0.1 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], "helpText": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "docsDescription": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.", @@ -2491,7 +2500,7 @@ { "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)"], + "tag": ["CIS M365 6.0.1 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)"], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -2506,7 +2515,7 @@ { "name": "standards.DisableResourceMailbox", "cat": "Exchange Standards", - "tag": ["NIST CSF 2.0 (PR.AA-01)"], + "tag": ["NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)"], "helpText": "Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", "docsDescription": "Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", "executiveText": "Prevents direct login to resource mailbox accounts (like conference rooms or equipment), ensuring they can only be managed through proper administrative channels. This security measure eliminates potential unauthorized access to resource scheduling systems while maintaining proper booking functionality.", @@ -2556,7 +2565,7 @@ { "name": "standards.RetentionPolicyTag", "cat": "Exchange Standards", - "tag": [], + "tag": ["SMB1001 (3.1)"], "helpText": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "docsDescription": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "executiveText": "Automatically and permanently removes deleted emails after a specified number of days, helping manage storage costs and ensuring compliance with data retention policies. This prevents accumulation of unnecessary deleted items while maintaining a reasonable recovery window for accidentally deleted emails.", @@ -3057,7 +3066,7 @@ { "name": "standards.PhishingSimulations", "cat": "Defender Standards", - "tag": [], + "tag": ["SMB1001 (1.11)", "SMB1001 (5.1)"], "helpText": "This creates a phishing simulation policy that enables phishing simulations for the entire tenant.", "addedComponent": [ { @@ -4090,7 +4099,7 @@ { "name": "standards.intuneDeviceRegLocalAdmins", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.3)", "CIS M365 6.0.1 (5.1.4.4)"], + "tag": ["CIS M365 6.0.1 (5.1.4.3)", "CIS M365 6.0.1 (5.1.4.4)", "SMB1001 (2.2)"], "helpText": "Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins.", "docsDescription": "Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins.", "executiveText": "Controls whether employees who enroll devices automatically receive local administrator access. Disabling registering-user admin rights follows least-privilege principles and reduces security risk from over-privileged endpoints.", @@ -4118,7 +4127,7 @@ { "name": "standards.intuneRestrictUserDeviceRegistration", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.1)"], + "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], "helpText": "Controls whether users can register devices with Entra.", "docsDescription": "Configures whether users can register devices with Entra. When disabled, users are unable to register devices with Entra.", "executiveText": "Controls whether employees can register their devices for corporate access. Disabling user device registration prevents unauthorized or unmanaged devices from connecting to company resources, enhancing overall security posture.", @@ -4153,7 +4162,7 @@ { "name": "standards.DeletedUserRentention", "cat": "SharePoint Standards", - "tag": [], + "tag": ["SMB1001 (3.1)"], "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.", "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.", "executiveText": "Preserves departed employees' OneDrive files for a specified period, allowing time to recover important business documents before permanent deletion. This helps prevent data loss while managing storage costs and maintaining compliance with data retention policies.", @@ -4641,7 +4650,7 @@ { "name": "standards.DisableUserSiteCreate", "cat": "SharePoint Standards", - "tag": [], + "tag": ["SMB1001 (2.8)"], "helpText": "Disables users from creating new SharePoint sites", "docsDescription": "Disables standard users from creating SharePoint sites, also disables the ability to fully create teams", "executiveText": "Restricts the creation of new SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces and ensuring proper governance. This maintains organized information architecture while requiring approval for new collaborative environments.", @@ -5348,7 +5357,7 @@ { "name": "standards.AutopilotProfile", "cat": "Device Management Standards", - "tag": [], + "tag": ["SMB1001 (2.2)"], "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "helpText": "Assign the appropriate Autopilot profile to streamline device deployment.", "docsDescription": "This standard allows the deployment of Autopilot profiles to devices, including settings such as unique name templates, language options, and local admin privileges.", @@ -5448,6 +5457,17 @@ "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "High Impact", "addedDate": "2023-12-30", + "tag": [ + "SMB1001 (1.2)", + "SMB1001 (1.3)", + "SMB1001 (1.4)", + "SMB1001 (1.8)", + "SMB1001 (1.9)", + "SMB1001 (1.10)", + "SMB1001 (1.12)", + "SMB1001 (2.2)", + "SMB1001 (4.7)" + ], "helpText": "Deploy and manage Intune templates across devices.", "executiveText": "Deploys standardized device management configurations across all corporate devices, ensuring consistent security policies, application settings, and compliance requirements. This template-based approach streamlines device management while maintaining uniform security standards across the organization.", "addedComponent": [ @@ -5536,6 +5556,15 @@ "impact": "High Impact", "impactColour": "info", "addedDate": "2026-01-02", + "tag": [ + "SMB1001 (1.2)", + "SMB1001 (1.3)", + "SMB1001 (1.4)", + "SMB1001 (1.8)", + "SMB1001 (1.9)", + "SMB1001 (1.10)", + "SMB1001 (1.12)" + ], "helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.", "executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.", "addedComponent": [ @@ -5615,7 +5644,11 @@ "CIS M365 6.0.1 (5.2.2.9)", "CIS M365 6.0.1 (5.2.2.10)", "CIS M365 6.0.1 (5.2.2.11)", - "CIS M365 6.0.1 (5.2.2.12)" + "CIS M365 6.0.1 (5.2.2.12)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.8)", + "SMB1001 (2.9)" ], "helpText": "Manage conditional access policies for better security.", "executiveText": "Deploys standardized conditional access policies that automatically enforce security requirements based on user location, device compliance, and risk factors. These templates ensure consistent security controls across the organization while enabling secure access to business resources.", From 8469fae40c2c72ea5448041b6cbedc20c3cea477 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 6 May 2026 02:05:36 +0800 Subject: [PATCH 18/86] deviations count --- src/pages/tenant/standards/alignment/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/standards/alignment/index.js b/src/pages/tenant/standards/alignment/index.js index 45238be2960a..e81ce99d92e7 100644 --- a/src/pages/tenant/standards/alignment/index.js +++ b/src/pages/tenant/standards/alignment/index.js @@ -482,7 +482,8 @@ const Page = () => { 'alignmentScore', 'LicenseMissingPercentage', 'combinedAlignmentScore', - 'currentDeviationsCount', + 'pendingDeviationsCount', + 'deniedDeviationsCount', ] } queryKey={granular ? 'listTenantAlignment-granular' : 'listTenantAlignment'} From 35eff109de4965790d6ef77ac63603f42fea4913 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 5 May 2026 22:21:50 -0400 Subject: [PATCH 19/86] fix: Support IPv6 in GeoIP lookup Allow the GeoIP lookup input to accept IPv6 by adding a new "ipAny" validator that checks for either IPv4 or IPv6 (and returns a specific error message). Switch the IP input to use "ipAny" and update its placeholder to "Enter IP Address (IPv4 or IPv6)". --- src/pages/tenant/tools/geoiplookup/index.js | 4 ++-- src/utils/get-cipp-validator.js | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/tools/geoiplookup/index.js b/src/pages/tenant/tools/geoiplookup/index.js index 162e93a3b5a1..8f6daa37ad3e 100644 --- a/src/pages/tenant/tools/geoiplookup/index.js +++ b/src/pages/tenant/tools/geoiplookup/index.js @@ -102,9 +102,9 @@ const Page = () => { name="ipAddress" type="textField" validators={{ - validate: (value) => getCippValidator(value, "ip"), + validate: (value) => getCippValidator(value, "ipAny"), }} - placeholder="Enter IP Address" + placeholder="Enter IP Address (IPv4 or IPv6)" required /> diff --git a/src/utils/get-cipp-validator.js b/src/utils/get-cipp-validator.js index f5541e0dc25c..b6cff111b8a3 100644 --- a/src/utils/get-cipp-validator.js +++ b/src/utils/get-cipp-validator.js @@ -19,6 +19,12 @@ export const getCippValidator = (value, type) => { return typeof value === "string" || "This is not a valid string"; case "ip": return /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) || "This is not a valid IP address"; + case "ipAny": + return ( + /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(value) || + /^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$/.test(value) || + "This is not a valid IPv4 or IPv6 address" + ); case "ipv4cidr": return /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}\/([0-9]|[12][0-9]|3[0-2])$/.test(value) || "This is not a valid IPv4 CIDR"; case "ipv6": From 8818d4baebbba4be6e6b4f6bf6aaa04b1bf01524 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 6 May 2026 13:43:19 +0800 Subject: [PATCH 20/86] pass utc to api for nice response message --- src/components/CippComponents/CippUserActions.jsx | 9 +++++++++ .../CippFormPages/CippExchangeSettingsForm.jsx | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index ba233fda18fd..a434d7925fb3 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -179,6 +179,15 @@ const ManageLicensesForm = ({ formControl, tenant }) => { // Separate component for Out of Office form to avoid hook issues const OutOfOfficeForm = ({ formControl }) => { + // Send the browser's IANA timezone so the API can display local times in the response + useEffect(() => { + try { + formControl.setValue('timezone', Intl.DateTimeFormat().resolvedOptions().timeZone) + } catch { + // Fallback: leave timezone unset; API will display UTC + } + }, []) + // Watch the Auto Reply State value const autoReplyState = useWatch({ control: formControl.control, diff --git a/src/components/CippFormPages/CippExchangeSettingsForm.jsx b/src/components/CippFormPages/CippExchangeSettingsForm.jsx index ee44517b8c51..0427d3d27d1f 100644 --- a/src/components/CippFormPages/CippExchangeSettingsForm.jsx +++ b/src/components/CippFormPages/CippExchangeSettingsForm.jsx @@ -124,6 +124,15 @@ const CippExchangeSettingsForm = (props) => { ...values[type], }; + // Include browser timezone for OOO so the API can display local times in the response + if (type === "ooo") { + try { + data.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch { + // Fallback: leave timezone unset; API will display UTC + } + } + // Format data for recipient limits if (type === "recipientLimits") { data.Identity = currentSettings.Mailbox[0].Identity; From efa060aab40597ecb76ce4b01c668ff6b7620a1b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 6 May 2026 16:41:39 +0800 Subject: [PATCH 21/86] Correct support for all tenant mode in the tenant backup page --- .../tenant/manage/configuration-backup.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pages/tenant/manage/configuration-backup.js b/src/pages/tenant/manage/configuration-backup.js index 15154dc5e258..2d839fa6466d 100644 --- a/src/pages/tenant/manage/configuration-backup.js +++ b/src/pages/tenant/manage/configuration-backup.js @@ -89,8 +89,8 @@ const Page = () => { queryKey: `BackupTasks-${currentTenant}`, }); - // Use the actual backup files as the backup data - const filteredBackupData = Array.isArray(backupList.data) ? backupList.data : []; + // Use the actual backup files as the backup data — filter out any null entries + const filteredBackupData = Array.isArray(backupList.data) ? backupList.data.filter(Boolean) : []; // Generate backup tags from actual API response items - use raw items directly const generateBackupTags = (backup) => { // Use the Items array directly from the API response without any translation @@ -173,11 +173,12 @@ const Page = () => { }; // Filter backup data by selected tenant if in AllTenants view + const selectedTenantValue = backupTenantFilter?.value ?? backupTenantFilter; const tenantFilteredBackupData = settings.currentTenant === "AllTenants" && - backupTenantFilter && - backupTenantFilter !== "AllTenants" - ? filteredBackupData.filter((backup) => backup.TenantFilter === backupTenantFilter) + selectedTenantValue && + selectedTenantValue !== "AllTenants" + ? filteredBackupData.filter((backup) => backup.TenantFilter === selectedTenantValue) : filteredBackupData; const backupDisplayItems = tenantFilteredBackupData.map((backup, index) => ({ @@ -188,7 +189,7 @@ const Page = () => { tags: generateBackupTags(backup), })); - // Process existing backup configuration, find tenantFilter. by comparing settings.currentTenant with Tenant.value + // Process existing backup configuration const currentConfig = Array.isArray(existingBackupConfig.data) ? existingBackupConfig.data.find( (tenant) => @@ -383,8 +384,9 @@ const Page = () => { No Backup Configuration No backup schedule is currently configured for{" "} - {settings.currentTenant === "AllTenants" ? "any tenant" : settings.currentTenant}. - Click "Add Backup Schedule" to create an automated backup configuration. + {settings.currentTenant === "AllTenants" ? "AllTenants" : settings.currentTenant}. + Click "Add Backup Schedule" to create an automated backup configuration that will apply to all tenants. + A tenant specific backup can exist alongside a global backup, and will run according to its own schedule. )} From f0b54687609d040e808a19d7546cd8e38312cf1a Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 6 May 2026 16:54:09 +0800 Subject: [PATCH 22/86] Fix removing row in bulk add user removing the wrong row --- src/components/CippComponents/CippBulkUserDrawer.jsx | 4 +++- src/components/CippWizard/CippWizardCSVImport.jsx | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippBulkUserDrawer.jsx b/src/components/CippComponents/CippBulkUserDrawer.jsx index 53d4317c07be..8ab4c63681eb 100644 --- a/src/components/CippComponents/CippBulkUserDrawer.jsx +++ b/src/components/CippComponents/CippBulkUserDrawer.jsx @@ -94,7 +94,9 @@ export const CippBulkUserDrawer = ({ const handleRemoveItem = (row) => { if (row === undefined) return false; const currentData = formControl.getValues("bulkUser") || []; - const index = currentData.findIndex((item) => item === row); + const rowKey = JSON.stringify(row); + const index = currentData.findIndex((item) => JSON.stringify(item) === rowKey); + if (index === -1) return false; const newData = [...currentData]; newData.splice(index, 1); formControl.setValue("bulkUser", newData, { shouldValidate: true }); diff --git a/src/components/CippWizard/CippWizardCSVImport.jsx b/src/components/CippWizard/CippWizardCSVImport.jsx index 80983ad4f549..6d1e11942eb7 100644 --- a/src/components/CippWizard/CippWizardCSVImport.jsx +++ b/src/components/CippWizard/CippWizardCSVImport.jsx @@ -31,7 +31,9 @@ export const CippWizardCSVImport = (props) => { const handleRemoveItem = (row) => { if (row === undefined) return false; - const index = tableData?.findIndex((item) => item === row); + const rowKey = JSON.stringify(row); + const index = tableData?.findIndex((item) => JSON.stringify(item) === rowKey); + if (index === -1) return false; const newTableData = [...tableData]; newTableData.splice(index, 1); setTableData(newTableData); From 68331c42efd471e1f8501ee6043e580997e11ac4 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 6 May 2026 17:07:03 +0800 Subject: [PATCH 23/86] Update CippWizardCSVImport.jsx --- src/components/CippWizard/CippWizardCSVImport.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippWizard/CippWizardCSVImport.jsx b/src/components/CippWizard/CippWizardCSVImport.jsx index 6d1e11942eb7..9d12eb088874 100644 --- a/src/components/CippWizard/CippWizardCSVImport.jsx +++ b/src/components/CippWizard/CippWizardCSVImport.jsx @@ -160,4 +160,4 @@ export const CippWizardCSVImport = (props) => { /> ); -}; \ No newline at end of file +}; From 0ac68abe5211c160f79dc0cbd0ec5c471083d7e7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 6 May 2026 12:43:58 +0200 Subject: [PATCH 24/86] public group standard --- src/data/standards.json | 119 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 108 insertions(+), 11 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 124ac1b3b3a9..cb8334bbe4ae 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -414,7 +414,15 @@ { "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.6)", "EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", "EIDSCAAG02", "EIDSCAAG03", "SMB1001 (2.8)"], + "tag": [ + "CIS M365 6.0.1 (5.2.3.6)", + "EIDSCA.AG01", + "EIDSCA.AG02", + "EIDSCA.AG03", + "EIDSCAAG02", + "EIDSCAAG03", + "SMB1001 (2.8)" + ], "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.", @@ -822,7 +830,13 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "ZTNA21772", "ZTNA21787", "SMB1001 (2.8)"], + "tag": [ + "CIS M365 6.0.1 (5.1.2.3)", + "CISA (MS.AAD.6.1v1)", + "ZTNA21772", + "ZTNA21787", + "SMB1001 (2.8)" + ], "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.", @@ -983,7 +997,13 @@ { "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.3.2)", "CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA21868", "SMB1001 (2.8)"], + "tag": [ + "CIS M365 6.0.1 (5.1.3.2)", + "CISA (MS.AAD.20.1v1)", + "NIST CSF 2.0 (PR.AA-05)", + "ZTNA21868", + "SMB1001 (2.8)" + ], "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": [], @@ -1111,7 +1131,14 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "EIDSCAAP04", "SMB1001 (2.8)"], + "tag": [ + "CISA (MS.AAD.18.1v1)", + "EIDSCA.AP04", + "EIDSCA.AP07", + "EIDSCAAP04", + "SMB1001 (2.8)", + "ICS M365 6.0.1 (5.1.6.3)" + ], "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": [ @@ -1198,7 +1225,15 @@ { "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAS04", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "tag": [ + "CIS M365 6.0.1 (5.2.3.5)", + "EIDSCA.AS04", + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAS04", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], "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.", @@ -1213,7 +1248,15 @@ { "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "EIDSCAAV01", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "tag": [ + "CIS M365 6.0.1 (5.2.3.5)", + "EIDSCA.AV01", + "NIST CSF 2.0 (PR.AA-03)", + "EIDSCAAV01", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], "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.", @@ -1228,7 +1271,13 @@ { "name": "standards.DisableEmail", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.7)", "NIST CSF 2.0 (PR.AA-03)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "tag": [ + "CIS M365 6.0.1 (5.2.3.7)", + "NIST CSF 2.0 (PR.AA-03)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], @@ -2500,7 +2549,12 @@ { "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)"], + "tag": [ + "CIS M365 6.0.1 (1.2.2)", + "CISA (MS.AAD.10.1v1)", + "NIST CSF 2.0 (PR.AA-01)", + "SMB1001 (2.3)" + ], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -4295,7 +4349,12 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)", "ZTNA21817"], + "tag": [ + "CIS M365 6.0.1 (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": [], @@ -4723,7 +4782,12 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.3.2)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)", "ZTNA24824"], + "tag": [ + "CIS M365 6.0.1 (7.3.2)", + "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.", @@ -6543,5 +6607,38 @@ "EXCHANGE_S_ENTERPRISE_GOV", "EXCHANGE_LITE" ] + }, + { + "name": "standards.EnforcePrivateGroups", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 6.0.1 (1.2.1)"], + "helpText": "Sets all public Microsoft 365 groups to private automatically. Groups can be excluded by display name keyword.", + "docsDescription": "Ensures only organisation-managed or approved public groups exist by automatically switching public Microsoft 365 (Unified) groups to private visibility. Groups whose display name matches any of the configured exclusion keywords are left unchanged. This aligns with CIS M365 6.0.1 benchmark control 1.2.1.", + "executiveText": "Enforces private visibility on all Microsoft 365 groups to prevent unauthorised external access to group resources such as Teams, SharePoint sites, and Planner boards. Approved public groups can be excluded by name, ensuring governance while retaining flexibility for intentionally public collaboration spaces.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": true, + "required": false, + "label": "Exclude groups by display name keyword", + "name": "standards.EnforcePrivateGroups.ExcludedGroupNames" + } + ], + "label": "Enforce Private M365 Groups", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-06", + "powershellEquivalent": "Update-MgGroup -GroupId -Visibility Private", + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "SHAREPOINTWAC", + "SHAREPOINTSTANDARD", + "SHAREPOINTENTERPRISE", + "SHAREPOINTENTERPRISE_EDU", + "SHAREPOINTENTERPRISE_GOV", + "ONEDRIVE_BASIC", + "ONEDRIVE_ENTERPRISE" + ] } -] \ No newline at end of file +] From 27af9a267445323a4567b8d2cf88f573720ec933 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 6 May 2026 13:13:00 +0200 Subject: [PATCH 25/86] Empty AllowList Standard for CIS --- src/data/standards.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index cb8334bbe4ae..645f0aef0fef 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6640,5 +6640,27 @@ "ONEDRIVE_BASIC", "ONEDRIVE_ENTERPRISE" ] + }, + { + "name": "standards.EmptyFilterIPAllowList", + "cat": "Defender Standards", + "tag": ["CIS M365 6.0.1 (2.1.12)"], + "helpText": "Ensures the connection filter IP allow list is not used. IPs on this list bypass spam, spoof, and authentication checks.", + "docsDescription": "IPs on the connection filter allow list bypass spam, spoof, and authentication checks. CIS recommends keeping this list empty to ensure all inbound email is properly scanned. This standard checks that the IPAllowList on the Default hosted connection filter policy is empty and can remediate by clearing it.", + "executiveText": "Ensures the Exchange Online connection filter IP allow list is empty, preventing any IP addresses from bypassing spam filtering, spoofing checks, and sender authentication. Keeping this list empty ensures all inbound email undergoes full security scanning, reducing the risk of phishing and malware delivery through trusted-but-compromised sources.", + "addedComponent": [], + "label": "Ensure connection filter IP allow list is empty", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-06", + "powershellEquivalent": "Set-HostedConnectionFilterPolicy -Identity Default -IPAllowList @()", + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] } ] From 4b2c9094df00dea443f4eb71bac39efab48f69f9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 6 May 2026 13:23:19 +0200 Subject: [PATCH 26/86] add teasm ZAP standard --- src/data/standards.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 645f0aef0fef..c358c4382db3 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6662,5 +6662,27 @@ "EXCHANGE_S_ENTERPRISE_GOV", "EXCHANGE_LITE" ] + }, + { + "name": "standards.TeamsZAP", + "cat": "Defender Standards", + "tag": ["CIS M365 6.0.1 (2.4.4)"], + "helpText": "Ensures Zero-hour auto purge (ZAP) is enabled for Microsoft Teams, automatically removing malicious messages after delivery.", + "docsDescription": "Zero-hour auto purge (ZAP) for Microsoft Teams retroactively detects and neutralises malicious messages that have already been delivered in Teams chats. Enabling ZAP ensures that phishing, malware, and high confidence phishing messages are automatically purged even after initial delivery, aligning with CIS M365 6.0.1 benchmark control 2.4.4.", + "executiveText": "Enables Zero-hour auto purge for Microsoft Teams to automatically detect and remove malicious messages after delivery. This provides an additional layer of protection against phishing and malware that may bypass initial scanning, ensuring threats are neutralised even after they reach users.", + "addedComponent": [], + "label": "Ensure Zero-hour auto purge for Microsoft Teams is on", + "impact": "Low Impact", + "impactColour": "info", + "addedDate": "2026-05-06", + "powershellEquivalent": "Set-TeamsProtectionPolicy -Identity 'Teams Protection Policy' -ZapEnabled $true", + "recommendedBy": ["CIS"], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] } ] From cdcde9be387d828e86ffa86425945fe3732effe8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 6 May 2026 14:35:35 +0200 Subject: [PATCH 27/86] standards improvements --- src/data/standards.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/standards.json b/src/data/standards.json index c358c4382db3..c78518b01448 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -5786,6 +5786,7 @@ "label": "Group Template", "multi": true, "cat": "Templates", + "tag": ["CIS M365 6.0.1 (5.1.3.1)"], "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2023-12-30", From 4832593df8e0bf85f407ce3e5e5fa5f075144a59 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 6 May 2026 14:40:14 +0200 Subject: [PATCH 28/86] Ensure that collaboration invitations are sent to allowed domains only --- src/data/standards.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index c78518b01448..7eb8d38c164e 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6685,5 +6685,28 @@ "EXCHANGE_S_ENTERPRISE_GOV", "EXCHANGE_LITE" ] + }, + { + "name": "standards.CollaborationDomainRestriction", + "cat": "Entra (AAD) Standards", + "tag": ["CIS M365 6.0.1 (5.1.6.1)"], + "helpText": "Restricts B2B collaboration invitations to a specified list of allowed domains. If no domains are provided, the standard will alert and report on whether any domain restrictions are currently configured.", + "docsDescription": "By default, Microsoft Entra ID allows collaboration invitations to be sent to any external domain. CIS recommends restricting B2B collaboration invitations to only approved domains to reduce the risk of data exfiltration and unauthorized access. This standard checks the B2B management policy for an allow list of domains and can remediate by setting the allowed domains list.", + "executiveText": "Restricts external collaboration invitations to approved domains only, preventing users from sharing data with unapproved external organizations. This reduces the risk of data exfiltration and ensures that collaboration occurs only with trusted business partners.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.CollaborationDomainRestriction.allowedDomains", + "label": "Allowed domains (comma separated)", + "required": false, + "placeholder": "contoso.com, fabrikam.com" + } + ], + "label": "Restrict collaboration invitations to allowed domains only", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-06", + "powershellEquivalent": "Graph API PATCH https://graph.microsoft.com/beta/policies/b2bManagementPolicies/default", + "recommendedBy": ["CIS"] } ] From 344acd1a2370f207e245de7869fd0c81f6ddda69 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 7 May 2026 11:37:21 +0800 Subject: [PATCH 29/86] Enable reporting for standard AutoAddProxy --- src/data/standards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/standards.json b/src/data/standards.json index 7eb8d38c164e..6edccf01a6eb 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2230,7 +2230,7 @@ "addedDate": "2025-02-07", "powershellEquivalent": "Set-Mailbox -EmailAddresses @{add=$EmailAddress}", "recommendedBy": [], - "disabledFeatures": { "report": true, "warn": true, "remediate": false } + "disabledFeatures": { "report": false, "warn": true, "remediate": false } }, { "name": "standards.DisableAdditionalStorageProviders", From 43bec6731527eb7396c0a3cb69dd4459e0b32f06 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 7 May 2026 12:00:18 +0200 Subject: [PATCH 30/86] tag and standard updates --- .../CippStandards/CippStandardAccordion.jsx | 5 +- .../CippStandards/CippStandardDialog.jsx | 6 +- .../CippTestDetailOffCanvas.jsx | 54 +- src/data/standards.json | 505 +++++++++++++++--- .../tenant/standards/templates/template.jsx | 6 +- 5 files changed, 472 insertions(+), 104 deletions(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 1a2811d462c3..ee2d86b3f78c 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -432,7 +432,10 @@ const CippStandardAccordion = ({ (standard.cat && standard.cat.toLowerCase().includes(searchLower)) || (standard.tag && Array.isArray(standard.tag) && - standard.tag.some((tag) => tag.toLowerCase().includes(searchLower))); + standard.tag.some((tag) => tag.toLowerCase().includes(searchLower))) || + (standard.appliesToTest && + Array.isArray(standard.appliesToTest) && + standard.appliesToTest.some((testId) => testId.toLowerCase().includes(searchLower))); const isConfigured = _.get(configuredState, standardName); const matchesFilter = diff --git a/src/components/CippStandards/CippStandardDialog.jsx b/src/components/CippStandards/CippStandardDialog.jsx index 6ebe6362930c..4761ffcab94f 100644 --- a/src/components/CippStandards/CippStandardDialog.jsx +++ b/src/components/CippStandards/CippStandardDialog.jsx @@ -788,7 +788,11 @@ const CippStandardDialog = ({ standard.label.toLowerCase().includes(localSearchQuery.toLowerCase()) || standard.helpText.toLowerCase().includes(localSearchQuery.toLowerCase()) || (standard.tag && - standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase()))); + standard.tag.some((tag) => tag.toLowerCase().includes(localSearchQuery.toLowerCase()))) || + (standard.appliesToTest && + standard.appliesToTest.some((testId) => + testId.toLowerCase().includes(localSearchQuery.toLowerCase()) + )); // Category filter const matchesCategory = diff --git a/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx b/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx index 64abc9b224e4..990ed7472a08 100644 --- a/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx +++ b/src/components/CippTestDetail/CippTestDetailOffCanvas.jsx @@ -49,21 +49,15 @@ const getImpactColor = (impact) => { } }; -const checkCIPPStandardAvailable = (testName) => { - if (!testName) return "No"; - console.log(testName); - // Check if any standard's tag array contains a reference to this test - const hasStandard = standardsData.some((standard) => { - if (!standard.tag || !Array.isArray(standard.tag)) return false; - // Check if any tag matches the test name or contains it - return standard.tag.some((tag) => { - const tagLower = tag.toLowerCase(); - const testLower = testName.toLowerCase(); - return tagLower.includes(testLower) || testLower.includes(tagLower); - }); - }); - - return hasStandard ? "Yes" : "No"; +// Find every CIPP standard whose appliesToTest array includes this test's RowKey. +// appliesToTest stores TestIds (e.g. "CIS_1_1_1", "ZTNA21772", "SMB1001_2_5"); the +// row's RowKey is the same TestId, so this is an exact lookup. +const getMatchingStandards = (testName) => { + if (!testName) return []; + return standardsData.filter( + (standard) => + Array.isArray(standard.appliesToTest) && standard.appliesToTest.includes(testName) + ); }; // Shared markdown styling for consistent rendering @@ -141,6 +135,8 @@ export const CippTestDetailOffCanvas = ({ row }) => { const shouldRenderCustomJson = hasRawCustomData && row.ReturnType === "JSON" && !row.ResultMarkdown; const shouldRenderCustomMarkdown = hasRawCustomData && !shouldRenderCustomJson && !row.ResultMarkdown; + const matchingStandards = getMatchingStandards(row.RowKey); + return ( @@ -235,8 +231,8 @@ export const CippTestDetailOffCanvas = ({ row }) => { 0 ? `Yes (${matchingStandards.length})` : "No"} + color={matchingStandards.length > 0 ? "success" : "default"} size="small" /> @@ -246,6 +242,30 @@ export const CippTestDetailOffCanvas = ({ row }) => { + {matchingStandards.length > 0 && ( + + + + CIPP Standards that satisfy this test + + + The following CIPP standards can be deployed to remediate or enforce this test. + + + {matchingStandards.map((standard) => ( + + ))} + + + + )} + {(row.ResultMarkdown || shouldRenderCustomJson || shouldRenderCustomMarkdown) && ( diff --git a/src/data/standards.json b/src/data/standards.json index 6edccf01a6eb..39116e6cdc2b 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -129,9 +129,12 @@ "tag": [ "CIS M365 6.0.1 (3.1.1)", "mip_search_auditlog", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ "CISAMSEXO171", - "CISAMSEXO173" + "CISAMSEXO173", + "CIS_3_1_1" ], "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.", @@ -154,6 +157,7 @@ "name": "standards.RestrictThirdPartyStorageServices", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (1.3.7)"], + "appliesToTest": ["CIS_1_3_7"], "helpText": "Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers.", "docsDescription": "Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so.", "executiveText": "Prevents employees from using external cloud storage services like Dropbox, Google Drive, and Box within Microsoft 365, reducing data security risks and ensuring all company data remains within controlled corporate systems. This helps maintain data governance and prevents potential data leaks to unauthorized platforms.", @@ -272,6 +276,7 @@ "name": "standards.EnableCustomerLockbox", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (1.3.6)", "CustomerLockBoxEnabled"], + "appliesToTest": ["CIS_1_3_6"], "helpText": "**Requires Entra ID P2.** Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data", "docsDescription": "**Requires Entra ID P2.** Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.", "executiveText": "Requires explicit organizational approval before Microsoft support staff can access company data for service operations. This provides an additional layer of data protection and ensures the organization maintains control over who can access sensitive business information, even during technical support scenarios.", @@ -337,10 +342,15 @@ "EIDSCA.ST08", "EIDSCA.ST09", "NIST CSF 2.0 (PR.AA-05)", + "SMB1001 (2.8)" + ], + "appliesToTest": [ + "CIS_5_1_6_2", "EIDSCAAP07", + "EIDSCAAP14", "EIDSCAST08", "EIDSCAST09", - "SMB1001 (2.8)" + "SMB1001_2_8" ], "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)", @@ -356,7 +366,12 @@ { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)", "ZTNA21799", "CISAMSEXO51"], + "tag": ["CIS M365 6.0.1 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], + "appliesToTest": [ + "CISAMSEXO51", + "CIS_6_5_4", + "ZTNA21799" + ], "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.", @@ -381,7 +396,10 @@ "tag": [ "CIS M365 6.0.1 (1.3.2)", "spo_idle_session_timeout", - "NIST CSF 2.0 (PR.AA-03)", + "NIST CSF 2.0 (PR.AA-03)" + ], + "appliesToTest": [ + "CIS_1_3_2", "ZTNA21813", "ZTNA21814", "ZTNA21815" @@ -419,9 +437,14 @@ "EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", + "SMB1001 (2.8)" + ], + "appliesToTest": [ + "CIS_5_2_3_6", + "EIDSCAAG01", "EIDSCAAG02", "EIDSCAAG03", - "SMB1001 (2.8)" + "SMB1001_2_8" ], "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.", @@ -464,7 +487,11 @@ { "name": "standards.AdminSSPR", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AP01", "EIDSCAAP01", "ZTNA21842"], + "tag": ["EIDSCA.AP01"], + "appliesToTest": [ + "EIDSCAAP01", + "ZTNA21842" + ], "helpText": "Controls whether administrators are allowed to use Self-Service Password Reset through the Microsoft Entra authorization policy.", "docsDescription": "Configures the allowedToUseSSPR property on the Microsoft Entra authorization policy. Microsoft documents this property as controlling whether administrators of the tenant can use Self-Service Password Reset. Use this standard to explicitly enable or disable administrator SSPR based on your security policy.", "executiveText": "Controls whether tenant administrators can reset their own passwords through Self-Service Password Reset. Disabling this capability forces privileged accounts through more controlled recovery processes and reduces the risk of self-service recovery being misused on administrative identities.", @@ -491,7 +518,8 @@ { "name": "standards.AuthMethodsPolicyMigration", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCAAG01"], + "tag": [], + "appliesToTest": ["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.", @@ -562,7 +590,14 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.5)", "ZTNA21953", "ZTNA21955", "ZTNA24560", "SMB1001 (2.2)"], + "tag": ["CIS M365 6.0.1 (5.1.4.5)", "SMB1001 (2.2)"], + "appliesToTest": [ + "CIS_5_1_4_5", + "SMB1001_2_2", + "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.", @@ -585,7 +620,10 @@ "EIDSCA.AM07", "EIDSCA.AM09", "EIDSCA.AM10", - "NIST CSF 2.0 (PR.AA-03)", + "NIST CSF 2.0 (PR.AA-03)" + ], + "appliesToTest": [ + "CIS_5_2_3_1", "EIDSCAAM01", "EIDSCAAM03", "EIDSCAAM04", @@ -608,7 +646,8 @@ { "name": "standards.allowOTPTokens", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AM02", "EIDSCAAM02"], + "tag": ["EIDSCA.AM02"], + "appliesToTest": ["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.", @@ -624,6 +663,7 @@ "name": "standards.PWcompanionAppAllowedState", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AM01"], + "appliesToTest": ["EIDSCAAM01"], "helpText": "Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication.", "docsDescription": "Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app.", "executiveText": "Enables a simplified authentication experience by allowing users to authenticate directly through Outlook without requiring a separate authenticator app. This improves user convenience while maintaining security standards for passwordless authentication.", @@ -659,15 +699,20 @@ "EIDSCA.AF05", "EIDSCA.AF06", "NIST CSF 2.0 (PR.AA-03)", + "SMB1001 (2.5)", + "SMB1001 (2.6)", + "SMB1001 (2.9)" + ], + "appliesToTest": [ "EIDSCAAF01", "EIDSCAAF02", "EIDSCAAF03", "EIDSCAAF04", "EIDSCAAF05", "EIDSCAAF06", - "SMB1001 (2.5)", - "SMB1001 (2.6)", - "SMB1001 (2.9)" + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9" ], "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.", @@ -684,6 +729,11 @@ "name": "standards.EnableHardwareOAuth", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "appliesToTest": [ + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9" + ], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.", @@ -699,6 +749,10 @@ "name": "standards.allowOAuthTokens", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AT01", "EIDSCA.AT02"], + "appliesToTest": [ + "EIDSCAAT01", + "EIDSCAAT02" + ], "helpText": "Allows you to use any software OAuth token generator", "docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.", "executiveText": "Allows employees to use third-party authentication apps and password managers to generate secure login codes, providing flexibility in authentication methods while maintaining security standards. This accommodates diverse user preferences and existing security tools.", @@ -714,6 +768,7 @@ "name": "standards.FormsPhishingProtection", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (1.3.5)", "Security", "PhishingProtection"], + "appliesToTest": ["CIS_1_3_5"], "helpText": "Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns.", "docsDescription": "Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization.", "executiveText": "Automatically scans Microsoft Forms created by employees for malicious content and phishing attempts, preventing the creation and distribution of harmful forms within the organization. This protects against both internal threats and compromised accounts that might be used to distribute malicious content.", @@ -728,7 +783,13 @@ { "name": "standards.TAP", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21845", "ZTNA21846", "EIDSCAAT01", "EIDSCAAT02"], + "tag": [], + "appliesToTest": [ + "EIDSCAAT01", + "EIDSCAAT02", + "ZTNA21845", + "ZTNA21846" + ], "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.", @@ -756,6 +817,7 @@ "name": "standards.PasswordExpireDisabled", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (1.3.1)", "PWAgePolicyNew"], + "appliesToTest": ["CIS_1_3_1"], "helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.", "docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.", "executiveText": "Eliminates mandatory password expiration requirements, allowing employees to keep strong passwords indefinitely rather than forcing frequent changes that often lead to weaker passwords. This modern security approach reduces help desk calls and improves overall password security when combined with multi-factor authentication.", @@ -772,15 +834,19 @@ "cat": "Entra (AAD) Standards", "tag": [ "CIS M365 6.0.1 (5.2.3.2)", - "ZTNA21848", - "ZTNA21849", - "ZTNA21850", + "SMB1001 (2.1)" + ], + "appliesToTest": [ + "CIS_5_2_3_2", "EIDSCAPR01", "EIDSCAPR02", "EIDSCAPR03", "EIDSCAPR05", "EIDSCAPR06", - "SMB1001 (2.1)" + "SMB1001_2_1", + "ZTNA21848", + "ZTNA21849", + "ZTNA21850" ], "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)", @@ -804,7 +870,11 @@ { "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21803", "ZTNA21804"], + "tag": [], + "appliesToTest": [ + "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": [ @@ -833,10 +903,14 @@ "tag": [ "CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", - "ZTNA21772", - "ZTNA21787", "SMB1001 (2.8)" ], + "appliesToTest": [ + "CIS_5_1_2_3", + "SMB1001_2_8", + "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.", @@ -860,12 +934,16 @@ "EIDSCA.CR03", "EIDSCA.CR04", "Essential 8 (1507)", - "NIST CSF 2.0 (PR.AA-05)", - "ZTNA21869", + "NIST CSF 2.0 (PR.AA-05)" + ], + "appliesToTest": [ + "CIS_5_1_5_2", + "EIDSCACP04", "EIDSCACR01", "EIDSCACR02", "EIDSCACR03", - "EIDSCACR04" + "EIDSCACR04", + "ZTNA21869" ], "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", @@ -887,7 +965,11 @@ { "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21889", "SMB1001 (2.5)"], + "tag": ["SMB1001 (2.5)"], + "appliesToTest": [ + "SMB1001_2_5", + "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.", @@ -924,7 +1006,11 @@ { "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.21.1v1)", "ZTNA21868", "SMB1001 (2.8)"], + "tag": ["CISA (MS.AAD.21.1v1)", "SMB1001 (2.8)"], + "appliesToTest": [ + "SMB1001_2_8", + "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.", @@ -953,9 +1039,13 @@ "EIDSCA.AP10", "Essential 8 (1175)", "NIST CSF 2.0 (PR.AA-05)", - "EIDSCAAP10", "SMB1001 (2.8)" ], + "appliesToTest": [ + "CIS_5_1_2_2", + "EIDSCAAP10", + "SMB1001_2_8" + ], "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.", "executiveText": "Prevents regular employees from creating application registrations that could be used to maintain unauthorized access to company systems. This security measure ensures that only authorized IT personnel can create applications, reducing the risk of persistent security breaches through malicious applications.", @@ -970,7 +1060,11 @@ { "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.6)", "ZTNA21954"], + "tag": ["CIS M365 6.0.1 (5.1.4.6)"], + "appliesToTest": [ + "CIS_5_1_4_6", + "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.", @@ -1001,9 +1095,13 @@ "CIS M365 6.0.1 (5.1.3.2)", "CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", - "ZTNA21868", "SMB1001 (2.8)" ], + "appliesToTest": [ + "CIS_5_1_3_2", + "SMB1001_2_8", + "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": [], @@ -1032,6 +1130,10 @@ "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (1.3.4)", "SMB1001 (2.8)"], + "appliesToTest": [ + "CIS_1_3_4", + "SMB1001_2_8" + ], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", "addedComponent": [ @@ -1057,7 +1159,11 @@ { "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", - "tag": ["ZTNA21858", "SMB1001 (2.8)"], + "tag": ["SMB1001 (2.8)"], + "appliesToTest": [ + "SMB1001_2_8", + "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": [ @@ -1086,15 +1192,18 @@ "EIDSCA.AP08", "EIDSCA.AP09", "Essential 8 (1175)", - "NIST CSF 2.0 (PR.AA-05)", - "ZTNA21772", - "ZTNA21774", - "ZTNA21807", + "NIST CSF 2.0 (PR.AA-05)" + ], + "appliesToTest": [ + "CIS_5_1_5_1", "EIDSCAAP08", "EIDSCAAP09", "EIDSCACP01", "EIDSCACP03", - "EIDSCACP04" + "EIDSCACP04", + "ZTNA21772", + "ZTNA21774", + "ZTNA21807" ], "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.", @@ -1135,9 +1244,13 @@ "CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", + "SMB1001 (2.8)" + ], + "appliesToTest": [ + "CIS_5_1_6_3", "EIDSCAAP04", - "SMB1001 (2.8)", - "ICS M365 6.0.1 (5.1.6.3)" + "EIDSCAAP07", + "SMB1001_2_8" ], "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.", @@ -1210,7 +1323,13 @@ { "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", - "tag": ["CISA (MS.AAD.11.1v1)", "ZTNA21843", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "tag": ["CISA (MS.AAD.11.1v1)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], + "appliesToTest": [ + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9", + "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.", @@ -1229,11 +1348,17 @@ "CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", - "EIDSCAAS04", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)" ], + "appliesToTest": [ + "CIS_5_2_3_5", + "EIDSCAAS04", + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9" + ], "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.", @@ -1252,11 +1377,17 @@ "CIS M365 6.0.1 (5.2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", - "EIDSCAAV01", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)" ], + "appliesToTest": [ + "CIS_5_2_3_5", + "EIDSCAAV01", + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9" + ], "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.", @@ -1278,6 +1409,12 @@ "SMB1001 (2.6)", "SMB1001 (2.9)" ], + "appliesToTest": [ + "CIS_5_2_3_7", + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9" + ], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], @@ -1328,13 +1465,18 @@ "Essential 8 (1173)", "Essential 8 (1401)", "NIST CSF 2.0 (PR.AA-03)", - "ZTNA21780", - "ZTNA21782", - "ZTNA21796", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)" ], + "appliesToTest": [ + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_9", + "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.", "addedComponent": [], @@ -1424,6 +1566,7 @@ "name": "standards.OutBoundSpamAlert", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.6)"], + "appliesToTest": ["CIS_2_1_6"], "helpText": "Set the Outbound Spam Alert e-mail address", "docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.", "addedComponent": [ @@ -1649,6 +1792,7 @@ "name": "standards.EnableOnlineArchiving", "cat": "Exchange Standards", "tag": ["Essential 8 (1511)", "NIST CSF 2.0 (PR.DS-11)", "SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Enables the In-Place Online Archive for all UserMailboxes with a valid license.", "executiveText": "Automatically enables online email archiving for all licensed employees, providing additional storage for older emails while maintaining easy access. This helps manage mailbox sizes, improves email performance, and supports compliance with data retention requirements.", "addedComponent": [], @@ -1670,6 +1814,7 @@ "name": "standards.EnableLitigationHold", "cat": "Exchange Standards", "tag": ["SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Enables litigation hold for all UserMailboxes with a valid license.", "executiveText": "Preserves all email content for legal and compliance purposes by preventing permanent deletion of emails, even when users attempt to delete them. This is essential for organizations subject to legal discovery requirements or regulatory compliance mandates.", "addedComponent": [ @@ -1698,7 +1843,13 @@ { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (6.2.3)", "ORCA111", "ORCA240", "CISAMSEXO71"], + "tag": ["CIS M365 6.0.1 (6.2.3)"], + "appliesToTest": [ + "CISAMSEXO71", + "CIS_6_2_3", + "ORCA111", + "ORCA240" + ], "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.", @@ -1740,6 +1891,7 @@ "name": "standards.EnableMailTips", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (6.5.2)", "exo_mailtipsenabled"], + "appliesToTest": ["CIS_6_5_2"], "helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements", "executiveText": "Enables helpful notifications in Outlook that warn users about potential email issues, such as sending to large groups, external recipients, or invalid addresses. This reduces email mistakes and improves communication efficiency by providing real-time guidance to employees.", "addedComponent": [ @@ -1817,6 +1969,10 @@ "name": "standards.RotateDKIM", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], + "appliesToTest": [ + "CIS_2_1_9", + "SMB1001_2_12" + ], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", "addedComponent": [], @@ -1869,7 +2025,13 @@ { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.9)", "ORCA108", "CISAMSEXO31", "SMB1001 (2.12)"], + "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], + "appliesToTest": [ + "CISAMSEXO31", + "CIS_2_1_9", + "ORCA108", + "SMB1001_2_12" + ], "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": [], @@ -1891,6 +2053,10 @@ "name": "standards.AddDMARCToMOERA", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], + "appliesToTest": [ + "CIS_2_1_10", + "SMB1001_2_12" + ], "helpText": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "docsDescription": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.", @@ -1926,8 +2092,13 @@ "exo_mailboxaudit", "Essential 8 (1509)", "Essential 8 (1683)", - "NIST CSF 2.0 (DE.CM-09)", - "CISAMSEXO131" + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO131", + "CIS_6_1_1", + "CIS_6_1_2", + "CIS_6_1_3" ], "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.", @@ -2130,6 +2301,7 @@ "name": "standards.EXOOutboundSpamLimits", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.15)"], + "appliesToTest": ["CIS_2_1_15"], "helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ", "docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.", "executiveText": "Sets limits on how many emails employees can send per hour and per day to prevent spam and protect the organization's email reputation. When limits are exceeded, the system can alert administrators or temporarily block the user, helping detect compromised accounts or prevent abuse.", @@ -2197,7 +2369,12 @@ { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (1.3.3)", "exo_individualsharing", "ZTNA21803", "CISAMSEXO62"], + "tag": ["CIS M365 6.0.1 (1.3.3)", "exo_individualsharing"], + "appliesToTest": [ + "CISAMSEXO62", + "CIS_1_3_3", + "ZTNA21803" + ], "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.", @@ -2235,7 +2412,11 @@ { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (6.5.3)", "exo_storageproviderrestricted", "ZTNA21817"], + "tag": ["CIS M365 6.0.1 (6.5.3)", "exo_storageproviderrestricted"], + "appliesToTest": [ + "CIS_6_5_3", + "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.", @@ -2258,6 +2439,7 @@ "name": "standards.AntiSpamSafeList", "cat": "Defender Standards", "tag": ["CIS M365 6.0.1 (2.1.13)"], + "appliesToTest": ["CIS_2_1_13"], "helpText": "Sets the anti-spam connection filter policy option 'safe list' in Defender.", "docsDescription": "Sets [Microsoft's built-in 'safe list'](https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps#-enablesafelist) in the anti-spam connection filter policy, rather than setting a custom safe/block list of IPs.", "executiveText": "Enables Microsoft's pre-approved list of trusted email servers to improve email delivery from legitimate sources while maintaining spam protection. This reduces false positives where legitimate emails might be blocked while still protecting against spam and malicious emails.", @@ -2339,6 +2521,7 @@ "name": "standards.Bookings", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (1.3.9)"], + "appliesToTest": ["CIS_1_3_9"], "helpText": "Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external.", "docsDescription": "", "executiveText": "Controls whether employees can use Microsoft Bookings to create online appointment scheduling pages for internal and external clients. This feature can improve customer service and streamline appointment management, but may need to be controlled for security or business process reasons.", @@ -2372,6 +2555,7 @@ "name": "standards.EXODirectSend", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (6.5.5)"], + "appliesToTest": ["CIS_6_5_5"], "helpText": "Sets the state of Direct Send in Exchange Online. Direct Send allows applications to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication.", "docsDescription": "Controls whether applications can use Direct Send to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication. A detailed explanation from Microsoft can be found [here.](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)", "executiveText": "Controls whether business applications and devices (like printers or scanners) can send emails through the company's email system without authentication. While this enables convenient features like scan-to-email, it may pose security risks and should be carefully managed.", @@ -2402,7 +2586,10 @@ "CIS M365 6.0.1 (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)" + ], + "appliesToTest": [ + "CIS_6_3_1", "ZTNA21817" ], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", @@ -2511,6 +2698,7 @@ "name": "standards.UserSubmissions", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (8.6.1)"], + "appliesToTest": ["CIS_8_6_1"], "helpText": "Set the state of the spam submission button in Outlook", "docsDescription": "Set the state of the built-in Report button in Outlook. This gives the users the ability to report emails as spam or phish.", "executiveText": "Enables employees to easily report suspicious emails directly from Outlook, helping improve the organization's spam and phishing detection systems. This crowdsourced approach to security allows users to contribute to threat detection while providing valuable feedback to enhance email security filters.", @@ -2555,6 +2743,10 @@ "NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)" ], + "appliesToTest": [ + "CIS_1_2_2", + "SMB1001_2_3" + ], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -2570,6 +2762,7 @@ "name": "standards.DisableResourceMailbox", "cat": "Exchange Standards", "tag": ["NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)"], + "appliesToTest": ["SMB1001_2_3"], "helpText": "Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", "docsDescription": "Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.", "executiveText": "Prevents direct login to resource mailbox accounts (like conference rooms or equipment), ensuring they can only be managed through proper administrative channels. This security measure eliminates potential unauthorized access to resource scheduling systems while maintaining proper booking functionality.", @@ -2598,6 +2791,7 @@ "CISA (MS.EXO.4.1v1)", "NIST CSF 2.0 (PR.DS-02)" ], + "appliesToTest": ["CIS_6_2_1"], "helpText": "Disables the ability for users to automatically forward e-mails to external recipients.", "docsDescription": "Disables the ability for users to automatically forward e-mails to external recipients. This is to prevent data exfiltration. Please check if there are any legitimate use cases for this feature before implementing, like forwarding invoices and such.", "executiveText": "Prevents employees from automatically forwarding company emails to external addresses, protecting against data leaks and unauthorized information sharing. This security measure helps maintain control over sensitive business communications while preventing both accidental and intentional data exfiltration.", @@ -2620,6 +2814,7 @@ "name": "standards.RetentionPolicyTag", "cat": "Exchange Standards", "tag": ["SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "docsDescription": "Creates a CIPP - Deleted Items retention policy tag that permanently deletes items in the Deleted Items folder after X days.", "executiveText": "Automatically and permanently removes deleted emails after a specified number of days, helping manage storage costs and ensuring compliance with data retention policies. This prevents accumulation of unnecessary deleted items while maintaining a reasonable recovery window for accidentally deleted emails.", @@ -2750,7 +2945,13 @@ "CIS M365 6.0.1 (2.1.1)", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO151", + "CISAMSEXO152", + "CISAMSEXO153", + "CIS_2_1_1", "ORCA105", "ORCA106", "ORCA107", @@ -2764,10 +2965,7 @@ "ORCA226", "ORCA236", "ORCA237", - "ORCA238", - "CISAMSEXO151", - "CISAMSEXO152", - "CISAMSEXO153" + "ORCA238" ], "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": [ @@ -2828,7 +3026,13 @@ "mdo_antiphishingpolicies", "mdo_phishthresholdlevel", "CIS M365 6.0.1 (2.1.7)", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO111", + "CISAMSEXO112", + "CISAMSEXO113", + "CIS_2_1_7", "ORCA104", "ORCA115", "ORCA180", @@ -2848,10 +3052,7 @@ "ORCA244", "ZTNA21784", "ZTNA21817", - "ZTNA21819", - "CISAMSEXO111", - "CISAMSEXO112", - "CISAMSEXO113" + "ZTNA21819" ], "helpText": "This creates a Anti-Phishing policy that automatically enables Mailbox Intelligence and spoofing, optional switches for Mail tips.", "addedComponent": [ @@ -3022,7 +3223,10 @@ "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CIS_2_1_4", "ORCA158", "ORCA227" ], @@ -3092,6 +3296,7 @@ "name": "standards.AtpPolicyForO365", "cat": "Defender Standards", "tag": ["CIS M365 6.0.1 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CIS_2_1_5"], "helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.", "addedComponent": [ { @@ -3121,6 +3326,10 @@ "name": "standards.PhishingSimulations", "cat": "Defender Standards", "tag": ["SMB1001 (1.11)", "SMB1001 (5.1)"], + "appliesToTest": [ + "SMB1001_1_11", + "SMB1001_5_1" + ], "helpText": "This creates a phishing simulation policy that enables phishing simulations for the entire tenant.", "addedComponent": [ { @@ -3179,16 +3388,21 @@ "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CISAMSEXO101", + "CISAMSEXO102", + "CISAMSEXO103", + "CISAMSEXO95", + "CIS_2_1_11", + "CIS_2_1_2", + "CIS_2_1_3", "ORCA121", "ORCA124", "ORCA232", "ZTNA21817", - "ZTNA21819", - "CISAMSEXO95", - "CISAMSEXO101", - "CISAMSEXO102", - "CISAMSEXO103" + "ZTNA21819" ], "helpText": "This creates a Malware filter policy that enables the default File filter and Zero-hour auto purge for malware.", "addedComponent": [ @@ -3318,7 +3532,11 @@ { "name": "standards.SpamFilterPolicy", "cat": "Defender Standards", - "tag": [ + "tag": [], + "appliesToTest": [ + "CISAMSEXO141", + "CISAMSEXO142", + "CISAMSEXO143", "ORCA100", "ORCA101", "ORCA102", @@ -3332,10 +3550,7 @@ "ORCA143", "ORCA224", "ORCA231", - "ORCA241", - "CISAMSEXO141", - "CISAMSEXO142", - "CISAMSEXO143" + "ORCA241" ], "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", @@ -3842,6 +4057,7 @@ "name": "standards.IntuneComplianceSettings", "cat": "Intune Standards", "tag": ["CIS M365 6.0.1 (4.1)"], + "appliesToTest": ["CIS_4_1"], "helpText": "Sets the mark devices with no compliance policy assigned as compliance/non compliant and Compliance status validity period.", "executiveText": "Configures how the system treats devices that don't have specific compliance policies and sets how often devices must check in to maintain their compliance status. This ensures proper security oversight of all corporate devices and maintains current compliance information.", "addedComponent": [ @@ -3913,6 +4129,7 @@ "name": "standards.DefaultPlatformRestrictions", "cat": "Intune Standards", "tag": ["CIS M365 6.0.1 (4.2)", "CISA (MS.AAD.19.1v1)"], + "appliesToTest": ["CIS_4_2"], "helpText": "Sets the default platform restrictions for enrolling devices into Intune. Note: Do not block personally owned if platform is blocked.", "executiveText": "Controls which types of devices (iOS, Android, Windows, macOS) and ownership models (corporate vs. personal) can be enrolled in the company's device management system. This helps maintain security standards while supporting necessary business device types and usage scenarios.", "addedComponent": [ @@ -4131,7 +4348,12 @@ { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.2)", "CISA (MS.AAD.17.1v1)", "ZTNA21801", "ZTNA21802"], + "tag": ["CIS M365 6.0.1 (5.1.4.2)", "CISA (MS.AAD.17.1v1)"], + "appliesToTest": [ + "CIS_5_1_4_2", + "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": [ @@ -4154,6 +4376,11 @@ "name": "standards.intuneDeviceRegLocalAdmins", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.3)", "CIS M365 6.0.1 (5.1.4.4)", "SMB1001 (2.2)"], + "appliesToTest": [ + "CIS_5_1_4_3", + "CIS_5_1_4_4", + "SMB1001_2_2" + ], "helpText": "Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins.", "docsDescription": "Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins.", "executiveText": "Controls whether employees who enroll devices automatically receive local administrator access. Disabling registering-user admin rights follows least-privilege principles and reduces security risk from over-privileged endpoints.", @@ -4182,6 +4409,10 @@ "name": "standards.intuneRestrictUserDeviceRegistration", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], + "appliesToTest": [ + "CIS_5_1_4_1", + "SMB1001_2_8" + ], "helpText": "Controls whether users can register devices with Entra.", "docsDescription": "Configures whether users can register devices with Entra. When disabled, users are unable to register devices with Entra.", "executiveText": "Controls whether employees can register their devices for corporate access. Disabling user device registration prevents unauthorized or unmanaged devices from connecting to company resources, enhancing overall security posture.", @@ -4203,7 +4434,12 @@ { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", - "tag": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], + "tag": [], + "appliesToTest": [ + "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", @@ -4217,6 +4453,7 @@ "name": "standards.DeletedUserRentention", "cat": "SharePoint Standards", "tag": ["SMB1001 (3.1)"], + "appliesToTest": ["SMB1001_3_1"], "helpText": "Sets the retention period for deleted users OneDrive to the specified period of time. The default is 30 days.", "docsDescription": "When a OneDrive user gets deleted, the personal SharePoint site is saved for selected amount of time that data can be retrieved from it.", "executiveText": "Preserves departed employees' OneDrive files for a specified period, allowing time to recover important business documents before permanent deletion. This helps prevent data loss while managing storage costs and maintaining compliance with data retention policies.", @@ -4328,6 +4565,7 @@ "name": "standards.SPAzureB2B", "cat": "SharePoint Standards", "tag": ["CIS M365 6.0.1 (7.2.2)"], + "appliesToTest": ["CIS_7_2_2"], "helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled", "executiveText": "Enables secure collaboration with external partners through SharePoint and OneDrive by integrating with Azure B2B guest access. This allows controlled sharing with external organizations while maintaining security oversight and proper access management.", "addedComponent": [], @@ -4352,7 +4590,10 @@ "tag": [ "CIS M365 6.0.1 (7.3.1)", "CISA (MS.SPO.3.1v1)", - "NIST CSF 2.0 (DE.CM-09)", + "NIST CSF 2.0 (DE.CM-09)" + ], + "appliesToTest": [ + "CIS_7_3_1", "ZTNA21817" ], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", @@ -4420,7 +4661,13 @@ { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.9)", "CISA (MS.SPO.1.5v1)", "ZTNA21803", "ZTNA21804", "ZTNA21858"], + "tag": ["CIS M365 6.0.1 (7.2.9)", "CISA (MS.SPO.1.5v1)"], + "appliesToTest": [ + "CIS_7_2_9", + "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": [ @@ -4453,7 +4700,12 @@ { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.10)", "CISA (MS.SPO.1.6v1)", "ZTNA21803", "ZTNA21804"], + "tag": ["CIS M365 6.0.1 (7.2.10)", "CISA (MS.SPO.1.6v1)"], + "appliesToTest": [ + "CIS_7_2_10", + "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": [ @@ -4489,7 +4741,11 @@ "tag": [ "CIS M365 6.0.1 (7.2.7)", "CIS M365 6.0.1 (7.2.11)", - "CISA (MS.SPO.1.4v1)", + "CISA (MS.SPO.1.4v1)" + ], + "appliesToTest": [ + "CIS_7_2_11", + "CIS_7_2_7", "ZTNA21803", "ZTNA21804" ], @@ -4600,7 +4856,10 @@ "CIS M365 6.0.1 (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)" + ], + "appliesToTest": [ + "CIS_7_2_1", "ZTNA21776", "ZTNA21797" ], @@ -4630,7 +4889,11 @@ "CIS M365 6.0.1 (7.2.3)", "CIS M365 6.0.1 (7.2.4)", "CISA (MS.AAD.14.1v1)", - "CISA (MS.SPO.1.1v1)", + "CISA (MS.SPO.1.1v1)" + ], + "appliesToTest": [ + "CIS_7_2_3", + "CIS_7_2_4", "ZTNA21803", "ZTNA21804" ], @@ -4683,7 +4946,10 @@ "tag": [ "CIS M365 6.0.1 (7.2.5)", "CISA (MS.AAD.14.2v1)", - "CISA (MS.SPO.1.2v1)", + "CISA (MS.SPO.1.2v1)" + ], + "appliesToTest": [ + "CIS_7_2_5", "ZTNA21803", "ZTNA21804" ], @@ -4710,6 +4976,7 @@ "name": "standards.DisableUserSiteCreate", "cat": "SharePoint Standards", "tag": ["SMB1001 (2.8)"], + "appliesToTest": ["SMB1001_2_8"], "helpText": "Disables users from creating new SharePoint sites", "docsDescription": "Disables standard users from creating SharePoint sites, also disables the ability to fully create teams", "executiveText": "Restricts the creation of new SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces and ensuring proper governance. This maintains organized information architecture while requiring approval for new collaborative environments.", @@ -4785,7 +5052,10 @@ "tag": [ "CIS M365 6.0.1 (7.3.2)", "CISA (MS.SPO.2.1v1)", - "NIST CSF 2.0 (PR.AA-05)", + "NIST CSF 2.0 (PR.AA-05)" + ], + "appliesToTest": [ + "CIS_7_3_2", "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.", @@ -4819,7 +5089,10 @@ "tag": [ "CIS M365 6.0.1 (7.2.6)", "CISA (MS.AAD.14.3v1)", - "CISA (MS.SPO.1.3v1)", + "CISA (MS.SPO.1.3v1)" + ], + "appliesToTest": [ + "CIS_7_2_6", "ZTNA21803", "ZTNA21804" ], @@ -4873,6 +5146,17 @@ "CIS M365 6.0.1 (8.5.8)", "CIS M365 6.0.1 (8.5.9)" ], + "appliesToTest": [ + "CIS_8_5_1", + "CIS_8_5_2", + "CIS_8_5_3", + "CIS_8_5_4", + "CIS_8_5_5", + "CIS_8_5_6", + "CIS_8_5_7", + "CIS_8_5_8", + "CIS_8_5_9" + ], "helpText": "Defines the CIS recommended global meeting policy for Teams. This includes AllowAnonymousUsersToJoinMeeting, AllowAnonymousUsersToStartMeeting, AutoAdmittedUsers, AllowPSTNUsersToBypassLobby, MeetingChatEnabledType, DesignatedPresenterRoleMode, AllowExternalParticipantGiveRequestControl, AllowParticipantGiveRequestControl", "executiveText": "Establishes security-focused default settings for Teams meetings, controlling who can join meetings, present content, and participate in chats. These policies balance collaboration needs with security requirements, ensuring meetings remain productive while protecting against unauthorized access and disruption.", "addedComponent": [ @@ -4995,6 +5279,7 @@ "name": "standards.TeamsExternalChatWithAnyone", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.2.3)"], + "appliesToTest": ["CIS_8_2_3"], "helpText": "Controls whether users can start Teams chats with any email address, inviting external recipients as guests via email.", "docsDescription": "Manages the Teams messaging policy setting UseB2BInvitesToAddExternalUsers. When enabled, users can start chats with any email address and recipients receive an invitation to join the chat as guests. Disabling the setting prevents these external email chats from being created, keeping conversations limited to internal users and approved guests.", "executiveText": "Allows organizations to decide if employees can launch Microsoft Teams chats with anyone on the internet using just an email address. Disabling the feature keeps conversations inside trusted boundaries and helps prevent accidental data exposure through unexpected external invitations.", @@ -5038,6 +5323,7 @@ "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false", "recommendedBy": ["CIS"], "tag": ["CIS M365 6.0.1 (8.1.2)"], + "appliesToTest": ["CIS_8_1_2"], "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, { @@ -5097,6 +5383,7 @@ "name": "standards.TeamsExternalFileSharing", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.1.1)"], + "appliesToTest": ["CIS_8_1_1"], "helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.", "executiveText": "Controls which external cloud storage services (like Google Drive, Dropbox, Box) employees can access through Teams, ensuring file sharing occurs only through approved and secure platforms. This helps maintain data governance while supporting necessary business integrations.", "addedComponent": [ @@ -5167,6 +5454,10 @@ "name": "standards.TeamsExternalAccessPolicy", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.2.1)", "CIS M365 6.0.1 (8.2.2)"], + "appliesToTest": [ + "CIS_8_2_1", + "CIS_8_2_2" + ], "helpText": "Sets the properties of the Global external access policy.", "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", "executiveText": "Defines the organization's policy for communicating with external users through Teams, including other organizations, Skype users, and unmanaged accounts. This fundamental setting determines the scope of external collaboration while maintaining security boundaries for business communications.", @@ -5194,6 +5485,7 @@ "name": "standards.TeamsFederationConfiguration", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.2.1)"], + "appliesToTest": ["CIS_8_2_1"], "helpText": "Sets the properties of the Global federation configuration.", "docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.", "executiveText": "Configures how the organization federates with external organizations for Teams communication, controlling whether employees can communicate with specific external domains or all external organizations. This setting enables secure inter-organizational collaboration while maintaining control over external communications.", @@ -5269,6 +5561,7 @@ "name": "standards.TeamsMessagingPolicy", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.6.1)"], + "appliesToTest": ["CIS_8_6_1"], "helpText": "Sets the properties of the Global messaging policy.", "docsDescription": "Sets the properties of the Global messaging policy. Messaging policies control which chat and channel messaging features are available to users in Teams.", "executiveText": "Defines what messaging capabilities employees have in Teams, including the ability to edit or delete messages, create custom emojis, and report inappropriate content. These policies help maintain professional communication standards while enabling necessary collaboration features.", @@ -5422,6 +5715,7 @@ "name": "standards.AutopilotProfile", "cat": "Device Management Standards", "tag": ["SMB1001 (2.2)"], + "appliesToTest": ["SMB1001_2_2"], "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "helpText": "Assign the appropriate Autopilot profile to streamline device deployment.", "docsDescription": "This standard allows the deployment of Autopilot profiles to devices, including settings such as unique name templates, language options, and local admin privileges.", @@ -5532,6 +5826,17 @@ "SMB1001 (2.2)", "SMB1001 (4.7)" ], + "appliesToTest": [ + "SMB1001_1_10", + "SMB1001_1_12", + "SMB1001_1_2", + "SMB1001_1_3", + "SMB1001_1_4", + "SMB1001_1_8", + "SMB1001_1_9", + "SMB1001_2_2", + "SMB1001_4_7" + ], "helpText": "Deploy and manage Intune templates across devices.", "executiveText": "Deploys standardized device management configurations across all corporate devices, ensuring consistent security policies, application settings, and compliance requirements. This template-based approach streamlines device management while maintaining uniform security standards across the organization.", "addedComponent": [ @@ -5629,6 +5934,15 @@ "SMB1001 (1.10)", "SMB1001 (1.12)" ], + "appliesToTest": [ + "SMB1001_1_10", + "SMB1001_1_12", + "SMB1001_1_2", + "SMB1001_1_3", + "SMB1001_1_4", + "SMB1001_1_8", + "SMB1001_1_9" + ], "helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.", "executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.", "addedComponent": [ @@ -5714,6 +6028,24 @@ "SMB1001 (2.8)", "SMB1001 (2.9)" ], + "appliesToTest": [ + "CIS_5_2_2_1", + "CIS_5_2_2_10", + "CIS_5_2_2_11", + "CIS_5_2_2_12", + "CIS_5_2_2_2", + "CIS_5_2_2_3", + "CIS_5_2_2_4", + "CIS_5_2_2_5", + "CIS_5_2_2_6", + "CIS_5_2_2_7", + "CIS_5_2_2_8", + "CIS_5_2_2_9", + "SMB1001_2_5", + "SMB1001_2_6", + "SMB1001_2_8", + "SMB1001_2_9" + ], "helpText": "Manage conditional access policies for better security.", "executiveText": "Deploys standardized conditional access policies that automatically enforce security requirements based on user location, device compliance, and risk factors. These templates ensure consistent security controls across the organization while enabling secure access to business resources.", "addedComponent": [ @@ -5787,6 +6119,7 @@ "multi": true, "cat": "Templates", "tag": ["CIS M365 6.0.1 (5.1.3.1)"], + "appliesToTest": ["CIS_5_1_3_1"], "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2023-12-30", @@ -6613,6 +6946,7 @@ "name": "standards.EnforcePrivateGroups", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (1.2.1)"], + "appliesToTest": ["CIS_1_2_1"], "helpText": "Sets all public Microsoft 365 groups to private automatically. Groups can be excluded by display name keyword.", "docsDescription": "Ensures only organisation-managed or approved public groups exist by automatically switching public Microsoft 365 (Unified) groups to private visibility. Groups whose display name matches any of the configured exclusion keywords are left unchanged. This aligns with CIS M365 6.0.1 benchmark control 1.2.1.", "executiveText": "Enforces private visibility on all Microsoft 365 groups to prevent unauthorised external access to group resources such as Teams, SharePoint sites, and Planner boards. Approved public groups can be excluded by name, ensuring governance while retaining flexibility for intentionally public collaboration spaces.", @@ -6646,6 +6980,7 @@ "name": "standards.EmptyFilterIPAllowList", "cat": "Defender Standards", "tag": ["CIS M365 6.0.1 (2.1.12)"], + "appliesToTest": ["CIS_2_1_12"], "helpText": "Ensures the connection filter IP allow list is not used. IPs on this list bypass spam, spoof, and authentication checks.", "docsDescription": "IPs on the connection filter allow list bypass spam, spoof, and authentication checks. CIS recommends keeping this list empty to ensure all inbound email is properly scanned. This standard checks that the IPAllowList on the Default hosted connection filter policy is empty and can remediate by clearing it.", "executiveText": "Ensures the Exchange Online connection filter IP allow list is empty, preventing any IP addresses from bypassing spam filtering, spoofing checks, and sender authentication. Keeping this list empty ensures all inbound email undergoes full security scanning, reducing the risk of phishing and malware delivery through trusted-but-compromised sources.", @@ -6668,6 +7003,7 @@ "name": "standards.TeamsZAP", "cat": "Defender Standards", "tag": ["CIS M365 6.0.1 (2.4.4)"], + "appliesToTest": ["CIS_2_4_4"], "helpText": "Ensures Zero-hour auto purge (ZAP) is enabled for Microsoft Teams, automatically removing malicious messages after delivery.", "docsDescription": "Zero-hour auto purge (ZAP) for Microsoft Teams retroactively detects and neutralises malicious messages that have already been delivered in Teams chats. Enabling ZAP ensures that phishing, malware, and high confidence phishing messages are automatically purged even after initial delivery, aligning with CIS M365 6.0.1 benchmark control 2.4.4.", "executiveText": "Enables Zero-hour auto purge for Microsoft Teams to automatically detect and remove malicious messages after delivery. This provides an additional layer of protection against phishing and malware that may bypass initial scanning, ensuring threats are neutralised even after they reach users.", @@ -6690,6 +7026,7 @@ "name": "standards.CollaborationDomainRestriction", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.6.1)"], + "appliesToTest": ["CIS_5_1_6_1"], "helpText": "Restricts B2B collaboration invitations to a specified list of allowed domains. If no domains are provided, the standard will alert and report on whether any domain restrictions are currently configured.", "docsDescription": "By default, Microsoft Entra ID allows collaboration invitations to be sent to any external domain. CIS recommends restricting B2B collaboration invitations to only approved domains to reduce the risk of data exfiltration and unauthorized access. This standard checks the B2B management policy for an allow list of domains and can remediate by setting the allowed domains list.", "executiveText": "Restricts external collaboration invitations to approved domains only, preventing users from sharing data with unapproved external organizations. This reduces the risk of data exfiltration and ensures that collaboration occurs only with trusted business partners.", diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx index 19cf27c788f2..3890212fdbd8 100644 --- a/src/pages/tenant/standards/templates/template.jsx +++ b/src/pages/tenant/standards/templates/template.jsx @@ -207,7 +207,11 @@ const Page = () => { standard.label.toLowerCase().includes(searchQuery.toLowerCase()) || standard.helpText.toLowerCase().includes(searchQuery.toLowerCase()) || (standard.tag && - standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))), + standard.tag.some((tag) => tag.toLowerCase().includes(searchQuery.toLowerCase()))) || + (standard.appliesToTest && + standard.appliesToTest.some((testId) => + testId.toLowerCase().includes(searchQuery.toLowerCase()) + )), ); const handleToggleStandard = (standardName) => { From d8aa24676157801840144ab3822773c7b5187696 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 7 May 2026 18:31:21 +0800 Subject: [PATCH 31/86] TAP audit log prebuilt alert --- .../CippTable/CIPPTableToptoolbar.js | 2 +- src/data/AuditLogTemplates.json | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/components/CippTable/CIPPTableToptoolbar.js b/src/components/CippTable/CIPPTableToptoolbar.js index 58d8a180813b..232c752bb2c9 100644 --- a/src/components/CippTable/CIPPTableToptoolbar.js +++ b/src/components/CippTable/CIPPTableToptoolbar.js @@ -1239,7 +1239,7 @@ export const CIPPTableToptoolbar = React.memo( )} {/* Cold start indicator */} - {getRequestData?.data?.pages?.[0].Metadata?.ColdStart === true && ( + {getRequestData?.data?.pages?.[0]?.Metadata?.ColdStart === true && ( diff --git a/src/data/AuditLogTemplates.json b/src/data/AuditLogTemplates.json index 051f0dc16283..efa21ac67a53 100644 --- a/src/data/AuditLogTemplates.json +++ b/src/data/AuditLogTemplates.json @@ -420,5 +420,34 @@ } ] } + }, + { + "value": "TAPCreated", + "name": "A Temporary Access Pass has been created for a user", + "template": { + "preset": { + "value": "TAPCreated", + "label": "A Temporary Access Pass has been created for a user" + }, + "logbook": { "value": "Audit.AzureActiveDirectory", "label": "Azure AD" }, + "conditions": [ + { + "Property": { "value": "List:Operation", "label": "Operation" }, + "Operator": { "value": "EQ", "label": "Equals to" }, + "Input": { + "value": "Update user.", + "label": "updated user" + } + }, + { + "Property": { "value": "String", "label": "SecuredAccessPassData" }, + "Operator": { "value": "ne", "label": "Not Equals to" }, + "Input": { + "value": "[]", + "label": "[]" + } + } + ] + } } ] From d3c75d83392fd5d766ce7606279ca89af4cf1a8c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 7 May 2026 12:46:41 +0200 Subject: [PATCH 32/86] more test suite tags --- src/data/standards.json | 90 +++++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 39116e6cdc2b..cc708f747177 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -350,7 +350,8 @@ "EIDSCAAP14", "EIDSCAST08", "EIDSCAST09", - "SMB1001_2_8" + "SMB1001_2_8", + "ZTNA21792" ], "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)", @@ -444,7 +445,8 @@ "EIDSCAAG01", "EIDSCAAG02", "EIDSCAAG03", - "SMB1001_2_8" + "SMB1001_2_8", + "ZTNA21841" ], "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.", @@ -712,7 +714,9 @@ "EIDSCAAF06", "SMB1001_2_5", "SMB1001_2_6", - "SMB1001_2_9" + "SMB1001_2_9", + "ZTNA21838", + "ZTNA21839" ], "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.", @@ -817,7 +821,7 @@ "name": "standards.PasswordExpireDisabled", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (1.3.1)", "PWAgePolicyNew"], - "appliesToTest": ["CIS_1_3_1"], + "appliesToTest": ["CIS_1_3_1", "ZTNA21811"], "helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.", "docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.", "executiveText": "Eliminates mandatory password expiration requirements, allowing employees to keep strong passwords indefinitely rather than forcing frequent changes that often lead to weaker passwords. This modern security approach reduces help desk calls and improves overall password security when combined with multi-factor authentication.", @@ -943,6 +947,7 @@ "EIDSCACR02", "EIDSCACR03", "EIDSCACR04", + "ZTNA21809", "ZTNA21869" ], "helpText": "Enables App consent admin requests for the tenant via the GA role. Does not overwrite existing reviewer settings", @@ -1132,6 +1137,7 @@ "tag": ["CIS M365 6.0.1 (1.3.4)", "SMB1001 (2.8)"], "appliesToTest": [ "CIS_1_3_4", + "EIDSCAAP05", "SMB1001_2_8" ], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", @@ -1203,7 +1209,8 @@ "EIDSCACP04", "ZTNA21772", "ZTNA21774", - "ZTNA21807" + "ZTNA21807", + "ZTNA21810" ], "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.", @@ -1250,7 +1257,8 @@ "CIS_5_1_6_3", "EIDSCAAP04", "EIDSCAAP07", - "SMB1001_2_8" + "SMB1001_2_8", + "ZTNA21791" ], "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.", @@ -1356,6 +1364,7 @@ "CIS_5_2_3_5", "EIDSCAAS04", "SMB1001_2_5", + "SMB1001_2_5_L4", "SMB1001_2_6", "SMB1001_2_9" ], @@ -1385,6 +1394,7 @@ "CIS_5_2_3_5", "EIDSCAAV01", "SMB1001_2_5", + "SMB1001_2_5_L4", "SMB1001_2_6", "SMB1001_2_9" ], @@ -1412,6 +1422,7 @@ "appliesToTest": [ "CIS_5_2_3_7", "SMB1001_2_5", + "SMB1001_2_5_L4", "SMB1001_2_6", "SMB1001_2_9" ], @@ -1560,7 +1571,8 @@ "impactColour": "warning", "addedDate": "2026-03-13", "powershellEquivalent": "Graph API", - "recommendedBy": [] + "recommendedBy": [], + "appliesToTest": ["ZTNA21773", "ZTNA21896", "ZTNA21992"] }, { "name": "standards.OutBoundSpamAlert", @@ -2030,6 +2042,7 @@ "CISAMSEXO31", "CIS_2_1_9", "ORCA108", + "ORCA108_1", "SMB1001_2_12" ], "helpText": "Enables DKIM for all domains that currently support it", @@ -2962,6 +2975,7 @@ "ORCA119", "ORCA156", "ORCA179", + "ORCA189_2", "ORCA226", "ORCA236", "ORCA237", @@ -3228,6 +3242,7 @@ "appliesToTest": [ "CIS_2_1_4", "ORCA158", + "ORCA189", "ORCA227" ], "helpText": "This creates a Safe Attachment policy", @@ -3296,7 +3311,7 @@ "name": "standards.AtpPolicyForO365", "cat": "Defender Standards", "tag": ["CIS M365 6.0.1 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], - "appliesToTest": ["CIS_2_1_5"], + "appliesToTest": ["CIS_2_1_5", "ORCA225"], "helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.", "addedComponent": [ { @@ -3398,8 +3413,10 @@ "CIS_2_1_11", "CIS_2_1_2", "CIS_2_1_3", + "ORCA120_malware", "ORCA121", "ORCA124", + "ORCA205", "ORCA232", "ZTNA21817", "ZTNA21819" @@ -3542,6 +3559,12 @@ "ORCA102", "ORCA103", "ORCA104", + "ORCA109", + "ORCA110", + "ORCA118_1", + "ORCA118_3", + "ORCA120_phish", + "ORCA120_spam", "ORCA123", "ORCA139", "ORCA140", @@ -4352,7 +4375,8 @@ "appliesToTest": [ "CIS_5_1_4_2", "ZTNA21801", - "ZTNA21802" + "ZTNA21802", + "ZTNA21837" ], "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.", @@ -5485,7 +5509,7 @@ "name": "standards.TeamsFederationConfiguration", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.2.1)"], - "appliesToTest": ["CIS_8_2_1"], + "appliesToTest": ["CIS_8_2_1", "CIS_8_2_4"], "helpText": "Sets the properties of the Global federation configuration.", "docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.", "executiveText": "Configures how the organization federates with external organizations for Teams communication, controlling whether employees can communicate with specific external domains or all external organizations. This setting enables secure inter-organizational collaboration while maintaining control over external communications.", @@ -5835,7 +5859,29 @@ "SMB1001_1_8", "SMB1001_1_9", "SMB1001_2_2", - "SMB1001_4_7" + "SMB1001_4_7", + "ZTNA24540", + "ZTNA24541", + "ZTNA24542", + "ZTNA24543", + "ZTNA24545", + "ZTNA24547", + "ZTNA24548", + "ZTNA24549", + "ZTNA24550", + "ZTNA24552", + "ZTNA24553", + "ZTNA24564", + "ZTNA24568", + "ZTNA24569", + "ZTNA24572", + "ZTNA24574", + "ZTNA24575", + "ZTNA24576", + "ZTNA24784", + "ZTNA24839", + "ZTNA24840", + "ZTNA24870" ], "helpText": "Deploy and manage Intune templates across devices.", "executiveText": "Deploys standardized device management configurations across all corporate devices, ensuring consistent security policies, application settings, and compliance requirements. This template-based approach streamlines device management while maintaining uniform security standards across the organization.", @@ -5941,7 +5987,13 @@ "SMB1001_1_3", "SMB1001_1_4", "SMB1001_1_8", - "SMB1001_1_9" + "SMB1001_1_9", + "ZTNA24540", + "ZTNA24550", + "ZTNA24552", + "ZTNA24574", + "ZTNA24575", + "ZTNA24784" ], "helpText": "Deploy and maintain Intune reusable settings templates that can be referenced by multiple policies.", "executiveText": "Creates and keeps reusable Intune settings templates consistent so common firewall and configuration blocks can be reused across many policies.", @@ -6044,7 +6096,19 @@ "SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_8", - "SMB1001_2_9" + "SMB1001_2_9", + "ZTNA21783", + "ZTNA21786", + "ZTNA21806", + "ZTNA21808", + "ZTNA21824", + "ZTNA21825", + "ZTNA21828", + "ZTNA21830", + "ZTNA21883", + "ZTNA21892", + "ZTNA21941", + "ZTNA24827" ], "helpText": "Manage conditional access policies for better security.", "executiveText": "Deploys standardized conditional access policies that automatically enforce security requirements based on user location, device compliance, and risk factors. These templates ensure consistent security controls across the organization while enabling secure access to business resources.", From 7c057478c680134722ffbaf43087185c3a999f17 Mon Sep 17 00:00:00 2001 From: Joachim Date: Thu, 7 May 2026 13:28:05 +0200 Subject: [PATCH 33/86] Add Usage Location field to JIT Admin and template forms (#5910) --- .../administration/jit-admin-templates/add.jsx | 14 ++++++++++++++ .../administration/jit-admin-templates/edit.jsx | 14 ++++++++++++++ .../identity/administration/jit-admin/add.jsx | 17 +++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/pages/identity/administration/jit-admin-templates/add.jsx b/src/pages/identity/administration/jit-admin-templates/add.jsx index 986e7ea378d1..0a57a8b33882 100644 --- a/src/pages/identity/administration/jit-admin-templates/add.jsx +++ b/src/pages/identity/administration/jit-admin-templates/add.jsx @@ -9,6 +9,7 @@ import { CippFormDomainSelector } from "../../../../components/CippComponents/Ci import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; import { CippFormGroupSelector } from "../../../../components/CippComponents/CippFormGroupSelector"; import gdaproles from "../../../../data/GDAPRoles.json"; +import countryList from "../../../../data/countryList.json"; import { useSettings } from "../../../../hooks/use-settings"; import { useEffect } from "react"; @@ -352,6 +353,19 @@ const Page = () => { /> )} + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + { /> )} + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + { if (template.defaultExistingUser) { formControl.setValue("existingUser", template.defaultExistingUser, { shouldDirty: true }); } + if (template.defaultUsageLocation) { + formControl.setValue("usageLocation", template.defaultUsageLocation, { shouldDirty: true }); + } // Dates if (template.defaultDuration) { @@ -343,6 +347,19 @@ const Page = () => { validators={{ required: "Domain is required" }} /> + + ({ + label: Name, + value: Code, + }))} + formControl={formControl} + /> + From 397d0c9ff5e9ddf3c5df02b27aedf0b2845c2cb2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 00:19:28 +0200 Subject: [PATCH 34/86] add purview section --- .../CippDeployCompliancePolicyDrawer.jsx | 236 ++++++++++++++++++ .../CippComponents/CippPolicyImportDrawer.jsx | 41 ++- src/layouts/config.js | 46 ++++ .../compliance/dlp-templates/index.js | 107 ++++++++ src/pages/security/compliance/dlp/index.js | 111 ++++++++ .../compliance/labels-templates/index.js | 114 +++++++++ src/pages/security/compliance/labels/index.js | 90 +++++++ .../compliance/sit-templates/index.js | 107 ++++++++ src/pages/security/compliance/sit/index.js | 81 ++++++ 9 files changed, 923 insertions(+), 10 deletions(-) create mode 100644 src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx create mode 100644 src/pages/security/compliance/dlp-templates/index.js create mode 100644 src/pages/security/compliance/dlp/index.js create mode 100644 src/pages/security/compliance/labels-templates/index.js create mode 100644 src/pages/security/compliance/labels/index.js create mode 100644 src/pages/security/compliance/sit-templates/index.js create mode 100644 src/pages/security/compliance/sit/index.js diff --git a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx new file mode 100644 index 000000000000..c881757e087b --- /dev/null +++ b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx @@ -0,0 +1,236 @@ +import React, { useEffect, useState } from "react"; +import { Button, Divider, Stack } from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm, useFormState, useWatch } from "react-hook-form"; +import { RocketLaunch } from "@mui/icons-material"; +import { CippOffCanvas } from "./CippOffCanvas"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormTenantSelector } from "./CippFormTenantSelector"; +import { CippApiResults } from "./CippApiResults"; +import { ApiPostCall } from "../../api/ApiCall"; + +const MODE_CONFIG = { + DlpCompliancePolicy: { + title: "Deploy DLP Compliance Policy", + buttonLabel: "Deploy DLP Policy", + postUrl: "/api/AddDlpCompliancePolicy", + listTemplatesUrl: "/api/ListDlpCompliancePolicyTemplates", + templateQueryKey: "TemplateListDlpCompliancePolicy", + relatedQueryKeys: ["ListDlpCompliancePolicy", "ListDlpCompliancePolicyTemplates"], + placeholder: `{ + "Name": "Block Credit Card data", + "Comment": "Blocks documents containing credit card numbers", + "Mode": "Enable", + "ExchangeLocation": "All", + "SharePointLocation": "All", + "OneDriveLocation": "All", + "RuleParams": { + "Name": "Block Credit Card data Rule", + "ContentContainsSensitiveInformation": [{ "name": "Credit Card Number", "minCount": "1" }], + "BlockAccess": true + } +}`, + }, + SensitivityLabel: { + title: "Deploy Sensitivity Label", + buttonLabel: "Deploy Sensitivity Label", + postUrl: "/api/AddSensitivityLabel", + listTemplatesUrl: "/api/ListSensitivityLabelTemplates", + templateQueryKey: "TemplateListSensitivityLabel", + relatedQueryKeys: ["ListSensitivityLabel", "ListSensitivityLabelTemplates"], + placeholder: `{ + "Name": "Confidential", + "DisplayName": "Confidential", + "Tooltip": "Confidential data, do not share externally", + "Comment": "Internal-only confidential classification", + "ContentType": "File, Email", + "EncryptionEnabled": true, + "EncryptionProtectionType": "Template", + "ContentMarkingHeaderEnabled": true, + "ContentMarkingHeaderText": "Confidential - Internal Use Only", + "PolicyParams": { + "Name": "Confidential Label Policy", + "ExchangeLocation": "All", + "Settings": [ + ["mandatory", "false"], + ["disablemandatoryinoutlook", "true"] + ] + } +}`, + }, + SensitiveInfoType: { + title: "Deploy Sensitive Information Type", + buttonLabel: "Deploy SIT", + postUrl: "/api/AddSensitiveInfoType", + listTemplatesUrl: "/api/ListSensitiveInfoTypeTemplates", + templateQueryKey: "TemplateListSensitiveInfoType", + relatedQueryKeys: ["ListSensitiveInfoType", "ListSensitiveInfoTypeTemplates"], + placeholder: `{ + "Name": "Custom Employee ID", + "Description": "Internal Employee ID format EMP-NNNNN", + "Pattern": "EMP-\\\\d{5}", + "Confidence": "High", + "Recommended": true +} + +// Or with a base64-encoded XML rule pack: +// { +// "Name": "Custom Rule Pack", +// "FileDataBase64": "" +// }`, + }, +}; + +export const CippDeployCompliancePolicyDrawer = ({ + mode, + buttonText, + requiredPermissions = [], + PermissionButton = Button, +}) => { + const config = MODE_CONFIG[mode]; + + const [drawerVisible, setDrawerVisible] = useState(false); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + selectedTenants: [], + TemplateList: null, + PowerShellCommand: "", + }, + }); + + const { isValid } = useFormState({ control: formControl.control }); + + const templateListVal = useWatch({ control: formControl.control, name: "TemplateList" }); + + useEffect(() => { + if (templateListVal?.value) { + formControl.setValue("PowerShellCommand", JSON.stringify(templateListVal.value)); + } + }, [templateListVal, formControl]); + + const deployPolicy = ApiPostCall({ + urlFromData: true, + relatedQueryKeys: config?.relatedQueryKeys ?? [], + }); + + useEffect(() => { + if (deployPolicy.isSuccess) { + formControl.reset({ + selectedTenants: [], + TemplateList: null, + PowerShellCommand: "", + }); + } + }, [deployPolicy.isSuccess, formControl]); + + if (!config) { + return null; + } + + const handleSubmit = () => { + formControl.trigger(); + if (!isValid) return; + deployPolicy.mutate({ + url: config.postUrl, + data: formControl.getValues(), + }); + }; + + const handleCloseDrawer = () => { + setDrawerVisible(false); + formControl.reset({ + selectedTenants: [], + TemplateList: null, + PowerShellCommand: "", + }); + }; + + return ( + <> + setDrawerVisible(true)} + startIcon={} + > + {buttonText ?? config.buttonLabel} + + + + + + } + > + + + + + + + + + option, + url: config.listTemplatesUrl, + }} + placeholder="Select a template or enter PowerShell JSON manually" + /> + + + + + + + + + + + + + ); +}; diff --git a/src/components/CippComponents/CippPolicyImportDrawer.jsx b/src/components/CippComponents/CippPolicyImportDrawer.jsx index bbd1dbe73751..a4149928b19c 100644 --- a/src/components/CippComponents/CippPolicyImportDrawer.jsx +++ b/src/components/CippComponents/CippPolicyImportDrawer.jsx @@ -21,12 +21,40 @@ import { CippApiResults } from './CippApiResults' import { CippFormTenantSelector } from './CippFormTenantSelector' import { CippFolderNavigation } from './CippFolderNavigation' +// Modes that only support browsing community repos (no tenant fallback) +const REPO_ONLY_MODES = [ + 'ReportBuilder', + 'DlpCompliancePolicy', + 'SensitivityLabel', + 'SensitiveInfoType', +] + +const RELATED_QUERY_KEYS_BY_MODE = { + ConditionalAccess: ['ListCATemplates-table'], + Standards: ['listStandardTemplates'], + ReportBuilder: ['ListReportBuilderTemplates'], + DlpCompliancePolicy: ['ListDlpCompliancePolicyTemplates'], + SensitivityLabel: ['ListSensitivityLabelTemplates'], + SensitiveInfoType: ['ListSensitiveInfoTypeTemplates'], +} + +const MODE_LABELS = { + ReportBuilder: 'Report Template', + DlpCompliancePolicy: 'DLP Policy', + SensitivityLabel: 'Sensitivity Label', + SensitiveInfoType: 'Sensitive Info Type', +} + +const DEFAULT_QUERY_KEYS = ['ListIntuneTemplates-table', 'ListIntuneTemplates-autcomplete'] + export const CippPolicyImportDrawer = ({ buttonText = 'Browse Catalog', requiredPermissions = [], PermissionButton = Button, mode = 'Intune', }) => { + const isRepoOnlyMode = REPO_ONLY_MODES.includes(mode) + const [drawerVisible, setDrawerVisible] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [viewDialogOpen, setViewDialogOpen] = useState(false) @@ -73,14 +101,7 @@ export const CippPolicyImportDrawer = ({ const importPolicy = ApiPostCall({ urlFromData: true, - relatedQueryKeys: - mode === 'ConditionalAccess' - ? ['ListCATemplates-table'] - : mode === 'Standards' - ? ['listStandardTemplates'] - : mode === 'ReportBuilder' - ? ['ListReportBuilderTemplates'] - : ['ListIntuneTemplates-table', 'ListIntuneTemplates-autcomplete'], + relatedQueryKeys: RELATED_QUERY_KEYS_BY_MODE[mode] || DEFAULT_QUERY_KEYS, }) const viewPolicyQuery = ApiPostCall({ @@ -298,7 +319,7 @@ export const CippPolicyImportDrawer = ({ {buttonText} option.value) : []), - ...(mode !== 'ReportBuilder' + ...(!isRepoOnlyMode ? [{ label: 'Get template from existing tenant', value: 'tenant' }] : []), ]} diff --git a/src/layouts/config.js b/src/layouts/config.js index ec6a017bf71b..aac86af27397 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -301,6 +301,9 @@ export const nativeMenuItems = [ 'Security.Alert.*', 'Tenant.DeviceCompliance.*', 'Security.SafeLinksPolicy.*', + 'Security.DlpCompliancePolicy.*', + 'Security.SensitivityLabel.*', + 'Security.SensitiveInfoType.*', ], items: [ { @@ -383,6 +386,49 @@ export const nativeMenuItems = [ }, ], }, + { + title: 'Purview Compliance', + permissions: [ + 'Security.DlpCompliancePolicy.*', + 'Security.SensitivityLabel.*', + 'Security.SensitiveInfoType.*', + ], + items: [ + { + title: 'DLP Policies', + path: '/security/compliance/dlp', + permissions: ['Security.DlpCompliancePolicy.*'], + }, + { + title: 'DLP Policy Templates', + path: '/security/compliance/dlp-templates', + permissions: ['Security.DlpCompliancePolicy.*'], + scope: 'global', + }, + { + title: 'Sensitivity Labels', + path: '/security/compliance/labels', + permissions: ['Security.SensitivityLabel.*'], + }, + { + title: 'Sensitivity Label Templates', + path: '/security/compliance/labels-templates', + permissions: ['Security.SensitivityLabel.*'], + scope: 'global', + }, + { + title: 'Sensitive Information Types', + path: '/security/compliance/sit', + permissions: ['Security.SensitiveInfoType.*'], + }, + { + title: 'Sensitive Info Type Templates', + path: '/security/compliance/sit-templates', + permissions: ['Security.SensitiveInfoType.*'], + scope: 'global', + }, + ], + }, ], }, { diff --git a/src/pages/security/compliance/dlp-templates/index.js b/src/pages/security/compliance/dlp-templates/index.js new file mode 100644 index 000000000000..b2b597c51167 --- /dev/null +++ b/src/pages/security/compliance/dlp-templates/index.js @@ -0,0 +1,107 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "DLP Compliance Policy Templates"; + const cardButtonPermissions = ["Security.DlpCompliancePolicy.ReadWrite"]; + + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { WriteAccess: true }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { required: { value: true, message: "This field is required" } }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveDlpCompliancePolicyTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["name", "comments", "Mode", "Workload", "Enabled", "GUID"], + actions: actions, + }; + + const simpleColumns = ["name", "comments", "Mode", "Workload", "Enabled", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/security/compliance/dlp/index.js b/src/pages/security/compliance/dlp/index.js new file mode 100644 index 000000000000..cfafc189d642 --- /dev/null +++ b/src/pages/security/compliance/dlp/index.js @@ -0,0 +1,111 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Book, Block, Check } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "DLP Compliance Policies"; + const apiUrl = "/api/ListDlpCompliancePolicy"; + const cardButtonPermissions = ["Security.DlpCompliancePolicy.ReadWrite"]; + + const actions = [ + { + label: "Create template based on policy", + type: "POST", + icon: , + url: "/api/AddDlpCompliancePolicyTemplate", + dataFunction: (data) => { + return { ...data }; + }, + confirmText: "Are you sure you want to create a template based on this DLP policy?", + }, + { + label: "Enable Policy", + type: "POST", + icon: , + url: "/api/EditDlpCompliancePolicy", + data: { + State: "!enable", + Identity: "Name", + }, + confirmText: "Are you sure you want to enable this DLP policy?", + condition: (row) => row.Enabled === false, + }, + { + label: "Disable Policy", + type: "POST", + icon: , + url: "/api/EditDlpCompliancePolicy", + data: { + State: "!disable", + Identity: "Name", + }, + confirmText: "Are you sure you want to disable this DLP policy?", + condition: (row) => row.Enabled === true, + }, + { + label: "Delete Policy", + type: "POST", + icon: , + url: "/api/RemoveDlpCompliancePolicy", + data: { + Identity: "Name", + }, + confirmText: "Are you sure you want to delete this DLP policy?", + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "Name", + "Comment", + "Mode", + "Enabled", + "Workload", + "ExchangeLocation", + "SharePointLocation", + "OneDriveLocation", + "TeamsLocation", + "EndpointDlpLocation", + "RuleCount", + "CreatedBy", + "WhenCreatedUTC", + "WhenChangedUTC", + ], + actions: actions, + }; + + const simpleColumns = [ + "Name", + "Mode", + "Enabled", + "Workload", + "RuleCount", + "CreatedBy", + "WhenCreatedUTC", + "WhenChangedUTC", + ]; + + return ( + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/security/compliance/labels-templates/index.js b/src/pages/security/compliance/labels-templates/index.js new file mode 100644 index 000000000000..3a4a5a13119d --- /dev/null +++ b/src/pages/security/compliance/labels-templates/index.js @@ -0,0 +1,114 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "Sensitivity Label Templates"; + const cardButtonPermissions = ["Security.SensitivityLabel.ReadWrite"]; + + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { WriteAccess: true }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { required: { value: true, message: "This field is required" } }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveSensitivityLabelTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "name", + "DisplayName", + "comments", + "ContentType", + "EncryptionEnabled", + "GUID", + ], + actions: actions, + }; + + const simpleColumns = ["name", "DisplayName", "comments", "ContentType", "EncryptionEnabled", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/security/compliance/labels/index.js b/src/pages/security/compliance/labels/index.js new file mode 100644 index 000000000000..53f1259877e9 --- /dev/null +++ b/src/pages/security/compliance/labels/index.js @@ -0,0 +1,90 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Book } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "Sensitivity Labels"; + const apiUrl = "/api/ListSensitivityLabel"; + const cardButtonPermissions = ["Security.SensitivityLabel.ReadWrite"]; + + const actions = [ + { + label: "Create template based on label", + type: "POST", + icon: , + url: "/api/AddSensitivityLabelTemplate", + dataFunction: (data) => { + return { ...data }; + }, + confirmText: "Are you sure you want to create a template based on this sensitivity label?", + }, + { + label: "Delete Label", + type: "POST", + icon: , + url: "/api/RemoveSensitivityLabel", + data: { + Identity: "Guid", + }, + confirmText: + "Are you sure you want to delete this sensitivity label? Labels currently published to users will be removed from policies.", + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "DisplayName", + "Name", + "Comment", + "Tooltip", + "ParentId", + "ContentType", + "EncryptionEnabled", + "EncryptionProtectionType", + "ContentMarkingHeaderEnabled", + "ContentMarkingFooterEnabled", + "ContentMarkingWatermarkEnabled", + "SiteAndGroupProtectionEnabled", + "Priority", + "Disabled", + "PublishedInPolicies", + ], + actions: actions, + }; + + const simpleColumns = [ + "DisplayName", + "Name", + "ContentType", + "EncryptionEnabled", + "ContentMarkingHeaderEnabled", + "ContentMarkingWatermarkEnabled", + "SiteAndGroupProtectionEnabled", + "Priority", + "Disabled", + ]; + + return ( + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/security/compliance/sit-templates/index.js b/src/pages/security/compliance/sit-templates/index.js new file mode 100644 index 000000000000..8bf4c54cc9e1 --- /dev/null +++ b/src/pages/security/compliance/sit-templates/index.js @@ -0,0 +1,107 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "Sensitive Information Type Templates"; + const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"]; + + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { WriteAccess: true }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { required: { value: true, message: "This field is required" } }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveSensitiveInfoTypeTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["name", "comments", "Pattern", "Confidence", "Locale", "GUID"], + actions: actions, + }; + + const simpleColumns = ["name", "comments", "Pattern", "Confidence", "Locale", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/security/compliance/sit/index.js b/src/pages/security/compliance/sit/index.js new file mode 100644 index 000000000000..f88f324d0194 --- /dev/null +++ b/src/pages/security/compliance/sit/index.js @@ -0,0 +1,81 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Book } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "Sensitive Information Types"; + const apiUrl = "/api/ListSensitiveInfoType"; + const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"]; + + const actions = [ + { + label: "Create template based on SIT", + type: "POST", + icon: , + url: "/api/AddSensitiveInfoTypeTemplate", + dataFunction: (data) => { + return { ...data }; + }, + confirmText: + "Are you sure you want to create a template based on this Sensitive Information Type?", + }, + { + label: "Delete SIT", + type: "POST", + icon: , + url: "/api/RemoveSensitiveInfoType", + data: { + Identity: "Name", + }, + confirmText: + "Are you sure you want to delete this Sensitive Information Type? Built-in Microsoft types cannot be deleted.", + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "Name", + "Description", + "Publisher", + "Recommended", + "RulePackId", + "RulePackVersion", + "State", + "Type", + ], + actions: actions, + }; + + const simpleColumns = [ + "Name", + "Publisher", + "Description", + "Recommended", + "RulePackVersion", + "State", + ]; + + return ( + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; From 1b5174664ba9c91d9412e974e1d5a2e2a8532972 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 00:46:37 +0200 Subject: [PATCH 35/86] feat: add AutoDiscover data retrieval to CippDomainCards - Implemented API call to fetch AutoDiscover data for the specified domain. - Added a new DomainResultCard to display AutoDiscover results, including validation passes, warns, and fails. Fixes #5972 --- src/components/CippCards/CippDomainCards.jsx | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/CippCards/CippDomainCards.jsx b/src/components/CippCards/CippDomainCards.jsx index 9268fcd08f96..fede72bd1347 100644 --- a/src/components/CippCards/CippDomainCards.jsx +++ b/src/components/CippCards/CippDomainCards.jsx @@ -470,6 +470,13 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) waiting: !!domain, }); + const { data: autoDiscoverData, isFetching: autoDiscoverLoading } = ApiGetCall({ + url: "/api/ListDomainHealth", + queryKey: `autodiscover-${domain}`, + data: { Domain: domain, Action: "ReadAutoDiscover" }, + waiting: !!domain, + }); + const { data: httpsData, isFetching: httpsLoading } = ApiGetCall({ url: "/api/ListDomainHealth", queryKey: `https-${domain}-${subdomains}`, @@ -684,6 +691,26 @@ export const CippDomainCards = ({ domain: propDomain = "", fullwidth = false }) } /> + + +

+ AutoDiscover ({autoDiscoverData?.RecordType || "None"}): +

+ + + + } + /> +
{enableHttps && ( Date: Fri, 8 May 2026 09:12:36 +0800 Subject: [PATCH 36/86] Add Investigate status to custom tests --- src/pages/tools/custom-tests/add.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/tools/custom-tests/add.jsx b/src/pages/tools/custom-tests/add.jsx index b101c5869550..41b01afdd9ea 100644 --- a/src/pages/tools/custom-tests/add.jsx +++ b/src/pages/tools/custom-tests/add.jsx @@ -311,6 +311,7 @@ const Page = () => { { value: 'Auto', label: 'Auto' }, { value: 'AlwaysPass', label: 'Always Pass' }, { value: 'AlwaysInfo', label: 'Always Info' }, + { value: 'AlwaysInvestigate', label: 'Always Investigate' }, ] const scriptNameField = { @@ -739,7 +740,8 @@ All UPNs: {{join(Result[*].UserPrincipalName, ", ")}}`, Return a hashtable with CIPPStatus (Passed/ - Failed/Info), CIPPResults, and optional{' '} + Failed/Info/Investigate),{' '} + CIPPResults, and optional{' '} CIPPResultMarkdown to control status and rendering directly (Auto result mode only) From b0661ac661e1a26992c4560e0d2c9ad766c9ac24 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 8 May 2026 10:10:21 +0800 Subject: [PATCH 37/86] Update AuditLogTemplates.json --- src/data/AuditLogTemplates.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/data/AuditLogTemplates.json b/src/data/AuditLogTemplates.json index efa21ac67a53..1762fb2eb7bb 100644 --- a/src/data/AuditLogTemplates.json +++ b/src/data/AuditLogTemplates.json @@ -446,6 +446,11 @@ "value": "[]", "label": "[]" } + }, + { + "Property": { "value": "String", "label": "SecuredAccessPassData" }, + "Operator": { "value": "like", "label": "Like" }, + "Input": { "value": "*" } } ] } From 182f0c81f22adc6729260df24b361689efea0034 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 8 May 2026 12:33:46 +0200 Subject: [PATCH 38/86] pushing new compliance menus --- .../CippDeployCompliancePolicyDrawer.jsx | 23 ++++ .../CippComponents/CippPolicyImportDrawer.jsx | 41 ++----- src/layouts/config.js | 107 ++++++++++------- .../compliance/retention-templates/index.js | 107 +++++++++++++++++ .../security/compliance/retention/index.js | 112 ++++++++++++++++++ 5 files changed, 313 insertions(+), 77 deletions(-) create mode 100644 src/pages/security/compliance/retention-templates/index.js create mode 100644 src/pages/security/compliance/retention/index.js diff --git a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx index c881757e087b..3cf35e3cb2f8 100644 --- a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx +++ b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx @@ -29,6 +29,29 @@ const MODE_CONFIG = { "ContentContainsSensitiveInformation": [{ "name": "Credit Card Number", "minCount": "1" }], "BlockAccess": true } +}`, + }, + RetentionCompliancePolicy: { + title: "Deploy Retention Compliance Policy", + buttonLabel: "Deploy Retention Policy", + postUrl: "/api/AddRetentionCompliancePolicy", + listTemplatesUrl: "/api/ListRetentionCompliancePolicyTemplates", + templateQueryKey: "TemplateListRetentionCompliancePolicy", + relatedQueryKeys: [ + "ListRetentionCompliancePolicy", + "ListRetentionCompliancePolicyTemplates", + ], + placeholder: `{ + "Name": "7-year Email Retention", + "Comment": "Retain Exchange mail for 7 years", + "ExchangeLocation": "All", + "Enabled": true, + "RuleParams": { + "Name": "7-year Email Retention Rule", + "RetentionDuration": 2555, + "RetentionComplianceAction": "Keep", + "ExpirationDateOption": "ModificationAgeInDays" + } }`, }, SensitivityLabel: { diff --git a/src/components/CippComponents/CippPolicyImportDrawer.jsx b/src/components/CippComponents/CippPolicyImportDrawer.jsx index a4149928b19c..bbd1dbe73751 100644 --- a/src/components/CippComponents/CippPolicyImportDrawer.jsx +++ b/src/components/CippComponents/CippPolicyImportDrawer.jsx @@ -21,40 +21,12 @@ import { CippApiResults } from './CippApiResults' import { CippFormTenantSelector } from './CippFormTenantSelector' import { CippFolderNavigation } from './CippFolderNavigation' -// Modes that only support browsing community repos (no tenant fallback) -const REPO_ONLY_MODES = [ - 'ReportBuilder', - 'DlpCompliancePolicy', - 'SensitivityLabel', - 'SensitiveInfoType', -] - -const RELATED_QUERY_KEYS_BY_MODE = { - ConditionalAccess: ['ListCATemplates-table'], - Standards: ['listStandardTemplates'], - ReportBuilder: ['ListReportBuilderTemplates'], - DlpCompliancePolicy: ['ListDlpCompliancePolicyTemplates'], - SensitivityLabel: ['ListSensitivityLabelTemplates'], - SensitiveInfoType: ['ListSensitiveInfoTypeTemplates'], -} - -const MODE_LABELS = { - ReportBuilder: 'Report Template', - DlpCompliancePolicy: 'DLP Policy', - SensitivityLabel: 'Sensitivity Label', - SensitiveInfoType: 'Sensitive Info Type', -} - -const DEFAULT_QUERY_KEYS = ['ListIntuneTemplates-table', 'ListIntuneTemplates-autcomplete'] - export const CippPolicyImportDrawer = ({ buttonText = 'Browse Catalog', requiredPermissions = [], PermissionButton = Button, mode = 'Intune', }) => { - const isRepoOnlyMode = REPO_ONLY_MODES.includes(mode) - const [drawerVisible, setDrawerVisible] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [viewDialogOpen, setViewDialogOpen] = useState(false) @@ -101,7 +73,14 @@ export const CippPolicyImportDrawer = ({ const importPolicy = ApiPostCall({ urlFromData: true, - relatedQueryKeys: RELATED_QUERY_KEYS_BY_MODE[mode] || DEFAULT_QUERY_KEYS, + relatedQueryKeys: + mode === 'ConditionalAccess' + ? ['ListCATemplates-table'] + : mode === 'Standards' + ? ['listStandardTemplates'] + : mode === 'ReportBuilder' + ? ['ListReportBuilderTemplates'] + : ['ListIntuneTemplates-table', 'ListIntuneTemplates-autcomplete'], }) const viewPolicyQuery = ApiPostCall({ @@ -319,7 +298,7 @@ export const CippPolicyImportDrawer = ({ {buttonText} option.value) : []), - ...(!isRepoOnlyMode + ...(mode !== 'ReportBuilder' ? [{ label: 'Get template from existing tenant', value: 'tenant' }] : []), ]} diff --git a/src/layouts/config.js b/src/layouts/config.js index aac86af27397..c820f5664acc 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -301,9 +301,11 @@ export const nativeMenuItems = [ 'Security.Alert.*', 'Tenant.DeviceCompliance.*', 'Security.SafeLinksPolicy.*', - 'Security.DlpCompliancePolicy.*', - 'Security.SensitivityLabel.*', - 'Security.SensitiveInfoType.*', + // TEMP: Purview Compliance menu hidden for dev build + // 'Security.DlpCompliancePolicy.*', + // 'Security.RetentionCompliancePolicy.*', + // 'Security.SensitivityLabel.*', + // 'Security.SensitiveInfoType.*', ], items: [ { @@ -386,49 +388,62 @@ export const nativeMenuItems = [ }, ], }, - { - title: 'Purview Compliance', - permissions: [ - 'Security.DlpCompliancePolicy.*', - 'Security.SensitivityLabel.*', - 'Security.SensitiveInfoType.*', - ], - items: [ - { - title: 'DLP Policies', - path: '/security/compliance/dlp', - permissions: ['Security.DlpCompliancePolicy.*'], - }, - { - title: 'DLP Policy Templates', - path: '/security/compliance/dlp-templates', - permissions: ['Security.DlpCompliancePolicy.*'], - scope: 'global', - }, - { - title: 'Sensitivity Labels', - path: '/security/compliance/labels', - permissions: ['Security.SensitivityLabel.*'], - }, - { - title: 'Sensitivity Label Templates', - path: '/security/compliance/labels-templates', - permissions: ['Security.SensitivityLabel.*'], - scope: 'global', - }, - { - title: 'Sensitive Information Types', - path: '/security/compliance/sit', - permissions: ['Security.SensitiveInfoType.*'], - }, - { - title: 'Sensitive Info Type Templates', - path: '/security/compliance/sit-templates', - permissions: ['Security.SensitiveInfoType.*'], - scope: 'global', - }, - ], - }, + // TEMP: Purview Compliance menu hidden for dev build + // { + // title: 'Purview Compliance', + // permissions: [ + // 'Security.DlpCompliancePolicy.*', + // 'Security.RetentionCompliancePolicy.*', + // 'Security.SensitivityLabel.*', + // 'Security.SensitiveInfoType.*', + // ], + // items: [ + // { + // title: 'DLP Policies', + // path: '/security/compliance/dlp', + // permissions: ['Security.DlpCompliancePolicy.*'], + // }, + // { + // title: 'DLP Policy Templates', + // path: '/security/compliance/dlp-templates', + // permissions: ['Security.DlpCompliancePolicy.*'], + // scope: 'global', + // }, + // { + // title: 'Retention Policies', + // path: '/security/compliance/retention', + // permissions: ['Security.RetentionCompliancePolicy.*'], + // }, + // { + // title: 'Retention Policy Templates', + // path: '/security/compliance/retention-templates', + // permissions: ['Security.RetentionCompliancePolicy.*'], + // scope: 'global', + // }, + // { + // title: 'Sensitivity Labels', + // path: '/security/compliance/labels', + // permissions: ['Security.SensitivityLabel.*'], + // }, + // { + // title: 'Sensitivity Label Templates', + // path: '/security/compliance/labels-templates', + // permissions: ['Security.SensitivityLabel.*'], + // scope: 'global', + // }, + // { + // title: 'Sensitive Information Types', + // path: '/security/compliance/sit', + // permissions: ['Security.SensitiveInfoType.*'], + // }, + // { + // title: 'Sensitive Info Type Templates', + // path: '/security/compliance/sit-templates', + // permissions: ['Security.SensitiveInfoType.*'], + // scope: 'global', + // }, + // ], + // }, ], }, { diff --git a/src/pages/security/compliance/retention-templates/index.js b/src/pages/security/compliance/retention-templates/index.js new file mode 100644 index 000000000000..3c3faa0e833a --- /dev/null +++ b/src/pages/security/compliance/retention-templates/index.js @@ -0,0 +1,107 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { GitHub } from "@mui/icons-material"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import { CippPolicyImportDrawer } from "../../../../components/CippComponents/CippPolicyImportDrawer.jsx"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "Retention Policy Templates"; + const cardButtonPermissions = ["Security.RetentionCompliancePolicy.ReadWrite"]; + + const integrations = ApiGetCall({ + url: "/api/ListExtensionsConfig", + queryKey: "Integrations", + refetchOnMount: false, + refetchOnReconnect: false, + }); + + const actions = [ + { + label: "Save to GitHub", + type: "POST", + url: "/api/ExecCommunityRepo", + icon: , + data: { + Action: "UploadTemplate", + GUID: "GUID", + }, + fields: [ + { + label: "Repository", + name: "FullName", + type: "select", + api: { + url: "/api/ListCommunityRepos", + data: { WriteAccess: true }, + queryKey: "CommunityRepos-Write", + dataKey: "Results", + valueField: "FullName", + labelField: "FullName", + }, + multiple: false, + creatable: false, + required: true, + validators: { required: { value: true, message: "This field is required" } }, + }, + { + label: "Commit Message", + placeholder: "Enter a commit message for adding this file to GitHub", + name: "Message", + type: "textField", + multiline: true, + required: true, + rows: 4, + }, + ], + confirmText: "Are you sure you want to save this template to the selected repository?", + condition: () => integrations.isSuccess && integrations?.data?.GitHub?.Enabled, + }, + { + label: "Delete Template", + type: "POST", + url: "/api/RemoveRetentionCompliancePolicyTemplate", + data: { ID: "GUID" }, + confirmText: "Do you want to delete the template?", + icon: , + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: ["name", "comments", "Enabled", "RestrictiveRetention", "GUID"], + actions: actions, + }; + + const simpleColumns = ["name", "comments", "Enabled", "RestrictiveRetention", "GUID"]; + + return ( + + + + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; diff --git a/src/pages/security/compliance/retention/index.js b/src/pages/security/compliance/retention/index.js new file mode 100644 index 000000000000..4bd75687fbce --- /dev/null +++ b/src/pages/security/compliance/retention/index.js @@ -0,0 +1,112 @@ +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Book, Block, Check } from "@mui/icons-material"; +import { TrashIcon } from "@heroicons/react/24/outline"; +import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; +import { PermissionButton } from "../../../../utils/permissions.js"; + +const Page = () => { + const pageTitle = "Purview Retention Policies"; + const apiUrl = "/api/ListRetentionCompliancePolicy"; + const cardButtonPermissions = ["Security.RetentionCompliancePolicy.ReadWrite"]; + + const actions = [ + { + label: "Create template based on policy", + type: "POST", + icon: , + url: "/api/AddRetentionCompliancePolicyTemplate", + dataFunction: (data) => { + return { ...data }; + }, + confirmText: "Are you sure you want to create a template based on this retention policy?", + }, + { + label: "Enable Policy", + type: "POST", + icon: , + url: "/api/EditRetentionCompliancePolicy", + data: { + State: "!enable", + Identity: "Name", + }, + confirmText: "Are you sure you want to enable this retention policy?", + condition: (row) => row.Enabled === false, + }, + { + label: "Disable Policy", + type: "POST", + icon: , + url: "/api/EditRetentionCompliancePolicy", + data: { + State: "!disable", + Identity: "Name", + }, + confirmText: "Are you sure you want to disable this retention policy?", + condition: (row) => row.Enabled === true, + }, + { + label: "Delete Policy", + type: "POST", + icon: , + url: "/api/RemoveRetentionCompliancePolicy", + data: { + Identity: "Name", + }, + confirmText: "Are you sure you want to delete this retention policy?", + color: "danger", + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "Name", + "Comment", + "Enabled", + "Workload", + "RestrictiveRetention", + "ExchangeLocation", + "SharePointLocation", + "OneDriveLocation", + "ModernGroupLocation", + "TeamsChannelLocation", + "TeamsChatLocation", + "RuleCount", + "CreatedBy", + "WhenCreatedUTC", + "WhenChangedUTC", + ], + actions: actions, + }; + + const simpleColumns = [ + "Name", + "Enabled", + "Workload", + "RuleCount", + "RestrictiveRetention", + "CreatedBy", + "WhenCreatedUTC", + "WhenChangedUTC", + ]; + + return ( + + } + /> + ); +}; + +Page.getLayout = (page) => {page}; +export default Page; From 2a59c7970f9589c46861a1fbca1a811e8abda525 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 12:37:07 +0200 Subject: [PATCH 39/86] feat: add manager and sponsor properties to user patching - Introduced 'manager' and 'sponsor' properties to PATCHABLE_PROPERTIES. - Implemented user selection for these properties using CippFormUserSelector. - Added validation for tenant domain restrictions when selecting users. - Updated confirmation step to handle new properties correctly. Fixes #5933 --- .../administration/users/patch-wizard.jsx | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx index 300aebfaafa0..1168f12f593e 100644 --- a/src/pages/identity/administration/users/patch-wizard.jsx +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -15,11 +15,13 @@ import { Switch, FormControlLabel, Autocomplete, + Alert, } from '@mui/material' import { CippWizardStepButtons } from '../../../../components/CippWizard/CippWizardStepButtons' import { ApiPostCall, ApiGetCall } from '../../../../api/ApiCall' import { CippApiResults } from '../../../../components/CippComponents/CippApiResults' import { CippDataTable } from '../../../../components/CippTable/CippDataTable' +import { CippFormUserSelector } from '../../../../components/CippComponents/CippFormUserSelector' import { Delete } from '@mui/icons-material' // User properties that can be patched @@ -54,6 +56,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Job Title', type: 'string', }, + { + property: 'manager', + label: 'Manager', + type: 'userSelector', + }, { property: 'officeLocation', label: 'Office Location', @@ -79,6 +86,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Show in Address List', type: 'boolean', }, + { + property: 'sponsor', + label: 'Sponsor', + type: 'userSelector', + }, { property: 'state', label: 'State/Province', @@ -182,6 +194,21 @@ const PropertySelectionStep = (props) => { // Get unique tenant domains from users const tenantDomains = [...new Set(users?.map((user) => user.Tenant || user.tenantFilter).filter(Boolean))] || [] + const firstTenantDomain = tenantDomains[0] + const hasManagerSelected = selectedProperties.includes('manager') + const hasSponsorSelected = selectedProperties.includes('sponsor') + const hasRelationshipSelected = hasManagerSelected || hasSponsorSelected + + useEffect(() => { + if (!hasRelationshipSelected || !firstTenantDomain) { + return + } + + const currentTenantFilter = formControl.getValues('tenantFilter') + if (currentTenantFilter?.value !== firstTenantDomain) { + formControl.setValue('tenantFilter', { value: firstTenantDomain }) + } + }, [firstTenantDomain, formControl, hasRelationshipSelected]) // Fetch custom data mappings for all tenants const customDataMappings = ApiGetCall({ @@ -248,6 +275,21 @@ const PropertySelectionStep = (props) => { ) } + if (property?.type === 'userSelector') { + return ( + + ) + } + // Default to text input for string types with consistent styling return ( { Properties to update + {hasRelationshipSelected && tenantDomains.length > 1 && ( + + The user picker is scoped to {firstTenantDomain}. Cross-tenant manager or sponsor + assignment is not supported, so the selected user must exist in each target tenant. + + )} {selectedProperties.map(renderPropertyInput)} @@ -455,7 +503,14 @@ const ConfirmationStep = (props) => { } selectedProperties.forEach((propName) => { - if (propertyValues[propName] !== undefined && propertyValues[propName] !== '') { + const propertyValue = propertyValues[propName] + + if (propertyValue !== undefined && propertyValue !== '' && propertyValue !== null) { + if (propName === 'manager' || propName === 'sponsor') { + if (propertyValue?.value) userData[propName] = propertyValue.value + return + } + // Handle dot notation properties (e.g., "extension_abc123.customField") if (propName.includes('.')) { const parts = propName.split('.') @@ -470,10 +525,10 @@ const ConfirmationStep = (props) => { } // Set the final property value - current[parts[parts.length - 1]] = propertyValues[propName] + current[parts[parts.length - 1]] = propertyValue } else { // Handle regular properties - userData[propName] = propertyValues[propName] + userData[propName] = propertyValue } } }) @@ -522,8 +577,13 @@ const ConfirmationStep = (props) => { {selectedProperties.map((propName) => { const property = allProperties.find((p) => p.property === propName) const value = propertyValues[propName] - const displayValue = - property?.type === 'boolean' ? (value ? 'Yes' : 'No') : value || 'Not set' + let displayValue = value || 'Not set' + + if (propName === 'manager' || propName === 'sponsor') { + displayValue = value?.label || value?.value || 'Not set' + } else if (property?.type === 'boolean') { + displayValue = value ? 'Yes' : 'No' + } return ( From d25142333ac8e0787040f4b62d330d90c939cf44 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 13:56:58 +0200 Subject: [PATCH 40/86] fix(jit-admin): submit TAP lifetime within policy bounds Fixes #5965 --- .../identity/administration/jit-admin/add.jsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index a1c1fce87c82..d99a1b6518c1 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -31,6 +31,12 @@ const Page = () => { const watcher = useWatch({ control: formControl.control }); const useTAP = useWatch({ control: formControl.control, name: "UseTAP" }); + const startDate = useWatch({ control: formControl.control, name: "startDate" }); + const endDate = useWatch({ control: formControl.control, name: "endDate" }); + const tapLifetimeInMinutes = useWatch({ + control: formControl.control, + name: "tapLifetimeInMinutes", + }); const tapPolicy = ApiGetCall({ url: selectedTenant @@ -47,6 +53,22 @@ const Page = () => { const useRoles = useWatch({ control: formControl.control, name: "useRoles" }); const useGroups = useWatch({ control: formControl.control, name: "useGroups" }); + useEffect(() => { + if (!useTAP || !startDate || !endDate) { + formControl.setValue("tapLifetimeInMinutes", null); + return; + } + + const requestedMinutes = Math.max(1, Math.round((endDate - startDate) / 60)); + const tapPolicyConfig = tapPolicy.data?.Results?.[0]; + const policyMax = tapPolicyConfig?.maximumLifetimeInMinutes ?? 1440; + const policyMin = Math.min(tapPolicyConfig?.minimumLifetimeInMinutes ?? 1, policyMax); + formControl.setValue( + "tapLifetimeInMinutes", + Math.min(Math.max(requestedMinutes, policyMin), policyMax) + ); + }, [useTAP, startDate, endDate, tapPolicy.data, formControl]); + // Clear fields when switches are toggled off useEffect(() => { if (!useRoles) { @@ -501,6 +523,11 @@ const Page = () => { /> + { TAP is not enabled in this tenant. TAP generation will fail. )} + {useTAP && tapLifetimeInMinutes && ( + + TAP will be valid for {tapLifetimeInMinutes} minutes. + + )} Date: Fri, 8 May 2026 23:17:03 +0800 Subject: [PATCH 41/86] Disable all tenant support for message trace --- src/pages/email/tools/message-trace/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/email/tools/message-trace/index.js b/src/pages/email/tools/message-trace/index.js index 56ccf9bcd20a..d5876859b182 100644 --- a/src/pages/email/tools/message-trace/index.js +++ b/src/pages/email/tools/message-trace/index.js @@ -347,6 +347,5 @@ const Page = () => { ); }; -Page.getLayout = (page) => {page}; - +Page.getLayout = (page) => {page}; export default Page; From 8e09f2231bc97fa6937168143f29512ec4d776ef Mon Sep 17 00:00:00 2001 From: Roel van der Wegen Date: Fri, 8 May 2026 19:30:21 +0200 Subject: [PATCH 42/86] add make to portals list --- src/data/portals.json | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/data/portals.json b/src/data/portals.json index 3d9cdb73b85a..58f49e3ae2c3 100644 --- a/src/data/portals.json +++ b/src/data/portals.json @@ -81,14 +81,23 @@ "icon": "ShieldMoon" }, { - "label": "Power Platform", - "name": "Power_Platform_Portal", + "label": "Power Platform (Admin)", + "name": "Power_Platform_Portal_Admin", "url": "https://admin.powerplatform.microsoft.com/account/login/customerId", "variable": "customerId", "target": "_blank", "external": true, "icon": "PrecisionManufacturing" }, + { + "label": "Power Platform (Maker)", + "name": "Power_Platform_Portal_Maker", + "url": "https://make.powerapps.com/home?tenant=customerId", + "variable": "customerId", + "target": "_blank", + "external": true, + "icon": "PrecisionManufacturing" + } { "label": "Power BI", "name": "Power_BI_Portal", From b05e0923d8afc286f7ca5591056bf847c8dbd755 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 8 May 2026 22:36:34 +0200 Subject: [PATCH 43/86] feat(standards): add by-standard alignment summary view --- .../CippComponents/CippTranslations.jsx | 1 + src/components/CippTable/CippDataTable.js | 15 +- .../CippTable/CippDataTableButton.jsx | 11 +- src/pages/tenant/standards/alignment/index.js | 565 ++++++++++++++++-- src/utils/get-cipp-column-size.js | 2 + src/utils/get-cipp-formatting.js | 6 +- 6 files changed, 545 insertions(+), 55 deletions(-) diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index c4b337ded761..8eb5ada15b07 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -9,6 +9,7 @@ export const CippTranslations = { surName: "Surname", city: "City", tenant: "Tenant", + tenants: "Tenants", tenantFilter: "Tenant", showTenantInformation: "Show Tenant Information", refreshTenantList: "Refresh tenant list", diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 0fc4f87432b7..253327713912 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -579,6 +579,9 @@ export const CippDataTable = (props) => { }, [columns.length, usedData, queryKey, settings?.currentTenant, filterTypeMap]) const createDialog = useDialog() + const hasActions = !!actions + const hasOffCanvas = !!offCanvas + const hasOnChange = !!onChange // Compute modeInfo via useMemo so it stays stable but updates when relevant inputs change. const modeInfo = useMemo( @@ -593,7 +596,7 @@ export const CippDataTable = (props) => { maxHeightOffset, settings ), - [simple, !!actions, !!offCanvas, !!onChange, maxHeightOffset, settings?.tablePageSize?.value] + [simple, hasActions, hasOffCanvas, hasOnChange, maxHeightOffset, settings?.tablePageSize?.value] ) // Include updateTrigger in data memo to force re-render when license backfill completes @@ -651,7 +654,15 @@ export const CippDataTable = (props) => { const muiTableBodyRowProps = useMemo(() => { if (offCanvasOnRowClick && offCanvas) { return ({ row }) => ({ - onClick: () => { + onClick: (event) => { + if ( + event.target?.closest?.( + 'button, a, input, textarea, select, [role="button"], [role="menuitem"], [data-no-row-click="true"]' + ) + ) { + return + } + setOffCanvasData(row.original) const filteredRowsArray = table?.getFilteredRowModel?.()?.rows if (filteredRowsArray) { diff --git a/src/components/CippTable/CippDataTableButton.jsx b/src/components/CippTable/CippDataTableButton.jsx index 79eec0f04bc5..86c3c887e1e9 100644 --- a/src/components/CippTable/CippDataTableButton.jsx +++ b/src/components/CippTable/CippDataTableButton.jsx @@ -5,7 +5,9 @@ import { getCippTranslation } from "../../utils/get-cipp-translation"; const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => { const [openDialogs, setOpenDialogs] = useState([]); - const handleOpenDialog = () => { + const handleOpenDialog = (event) => { + event?.stopPropagation(); + let dataArray; if (Array.isArray(data)) { @@ -21,7 +23,8 @@ const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => { setOpenDialogs([...openDialogs, dataArray]); }; - const handleCloseDialog = (index) => { + const handleCloseDialog = (index, event) => { + event?.stopPropagation?.(); setOpenDialogs(openDialogs.filter((_, i) => i !== index)); }; const dataIsNotANullArray = @@ -48,7 +51,9 @@ const CippDataTableButton = ({ data, title, tableTitle = "Data" }) => { handleCloseDialog(index)} + onClose={(event) => handleCloseDialog(index, event)} + onMouseDown={(event) => event.stopPropagation()} + onClick={(event) => event.stopPropagation()} fullWidth maxWidth="lg" > diff --git a/src/pages/tenant/standards/alignment/index.js b/src/pages/tenant/standards/alignment/index.js index e81ce99d92e7..ce413aa54393 100644 --- a/src/pages/tenant/standards/alignment/index.js +++ b/src/pages/tenant/standards/alignment/index.js @@ -1,16 +1,245 @@ import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { ApiGetCallWithPagination } from '../../../../api/ApiCall' +import { useSettings } from '../../../../hooks/use-settings' import { Delete, Edit } from '@mui/icons-material' -import { EyeIcon, ListBulletIcon, ChartBarIcon } from '@heroicons/react/24/outline' +import { EyeIcon, ListBulletIcon, ChartBarIcon, Squares2X2Icon } from '@heroicons/react/24/outline' import tabOptions from '../tabOptions.json' -import { useState } from 'react' -import { Box, Chip, Divider, Stack, Tooltip, Typography } from '@mui/material' +import { useEffect, useMemo, useState } from 'react' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import { + Box, + Chip, + Divider, + Stack, + ToggleButton, + ToggleButtonGroup, + Tooltip, + Typography, +} from '@mui/material' import standardsData from '../../../../data/standards.json' +const complianceColors = { + compliant: 'success', + 'non-compliant': 'error', + 'accepted deviation': 'info', + 'customer specific': 'info', + 'license missing': 'warning', + 'reporting disabled': 'default', +} + +const compliancePriority = { + compliant: 10, + 'reporting disabled': 20, + 'customer specific': 30, + 'accepted deviation': 40, + 'license missing': 50, + 'non-compliant': 60, +} + +const getComplianceStatus = (status) => String(status ?? 'Unknown').trim() || 'Unknown' + +const getComplianceColor = (status) => + complianceColors[getComplianceStatus(status).toLowerCase()] ?? 'default' + +const getCompliancePriority = (status) => + compliancePriority[getComplianceStatus(status).toLowerCase()] ?? 0 + +const isAlignedComplianceStatus = (status) => + ['compliant', 'accepted deviation', 'customer specific'].includes( + getComplianceStatus(status).toLowerCase() + ) + +const getPageRows = (page) => { + if (Array.isArray(page)) return page + if (Array.isArray(page?.Results)) return page.Results + if (Array.isArray(page?.Data)) return page.Data + if (Array.isArray(page?.data)) return page.data + if (Array.isArray(page?.value)) return page.value + return [] +} + +const getStandardInfo = (standardId) => { + const baseName = standardId?.split('.').slice(0, -1).join('.') + return ( + standardsData.find((s) => s.name === standardId) ?? + standardsData.find((s) => s.name === baseName) + ) +} + const Page = () => { const pageTitle = 'Standard & Drift Alignment' - const [granular, setGranular] = useState(false) + const tenant = useSettings().currentTenant + const [viewMode, setViewMode] = useState('summary') + const [byStandardTenantFilter, setByStandardTenantFilter] = useState('all') + const isSummary = viewMode === 'summary' + const isGranular = viewMode === 'granular' + const isByStandard = viewMode === 'byStandard' + + const { + data: byStandardApiData, + fetchNextPage: fetchNextByStandardPage, + hasNextPage: byStandardHasNextPage, + isFetching: byStandardIsFetching, + isSuccess: byStandardIsSuccess, + } = ApiGetCallWithPagination({ + url: '/api/ListTenantAlignment', + data: { tenantFilter: tenant, granular: 'true' }, + queryKey: `listTenantAlignment-byStandard-source-${tenant}`, + waiting: isByStandard, + }) + + useEffect(() => { + if (isByStandard && byStandardIsSuccess && byStandardHasNextPage && !byStandardIsFetching) { + fetchNextByStandardPage() + } + }, [ + byStandardApiData?.pages?.length, + byStandardHasNextPage, + byStandardIsFetching, + byStandardIsSuccess, + fetchNextByStandardPage, + isByStandard, + ]) + + const byStandardSourceData = useMemo( + () => byStandardApiData?.pages?.flatMap((page) => getPageRows(page)) ?? [], + [byStandardApiData] + ) + + const byStandardData = useMemo(() => { + const groupedStandards = new Map() + + byStandardSourceData.forEach((row) => { + const standardKey = row.standardId || row.standardName + if (!standardKey) return + + const standardInfo = getStandardInfo(row.standardId) + const standardName = standardInfo?.label ?? row.standardName ?? standardKey + + if (!groupedStandards.has(standardKey)) { + groupedStandards.set(standardKey, { + standardId: standardKey, + standardName, + category: standardInfo?.cat ?? 'Uncategorized', + standardTypes: new Set(), + tenants: new Map(), + }) + } + + const standard = groupedStandards.get(standardKey) + const standardType = row.standardType ?? row.templateType + if (standardType) standard.standardTypes.add(standardType) + + const tenantKey = row.tenantFilter ?? row.tenantName ?? row.Tenant ?? 'Unknown' + const status = getComplianceStatus(row.complianceStatus) + const tenant = standard.tenants.get(tenantKey) ?? { + tenantFilter: tenantKey, + complianceStatus: status, + rows: [], + } + + tenant.rows.push(row) + if (getCompliancePriority(status) > getCompliancePriority(tenant.complianceStatus)) { + tenant.complianceStatus = status + } + standard.tenants.set(tenantKey, tenant) + }) + + return Array.from(groupedStandards.values()) + .map((standard) => { + const tenants = Array.from(standard.tenants.values()) + .map((tenant) => { + const templateNames = [ + ...new Set(tenant.rows.map((row) => row.templateName).filter(Boolean)), + ] + const latestDataCollection = tenant.rows + .map((row) => row.latestDataCollection) + .filter(Boolean) + .sort((a, b) => new Date(b) - new Date(a))[0] + + return { + tenantFilter: tenant.tenantFilter, + complianceStatus: tenant.complianceStatus, + templateName: templateNames.join(', ') || 'N/A', + latestDataCollection, + rowCount: tenant.rows.length, + rows: tenant.rows, + } + }) + .sort((a, b) => a.tenantFilter.localeCompare(b.tenantFilter)) + + const counts = tenants.reduce( + (acc, tenant) => { + switch (getComplianceStatus(tenant.complianceStatus).toLowerCase()) { + case 'compliant': + acc.compliantCount += 1 + break + case 'non-compliant': + acc.nonCompliantCount += 1 + break + case 'accepted deviation': + acc.acceptedDeviationCount += 1 + break + case 'customer specific': + acc.customerSpecificCount += 1 + break + case 'license missing': + acc.licenseMissingCount += 1 + break + case 'reporting disabled': + acc.reportingDisabledCount += 1 + break + default: + acc.otherCount += 1 + } + return acc + }, + { + compliantCount: 0, + nonCompliantCount: 0, + acceptedDeviationCount: 0, + customerSpecificCount: 0, + licenseMissingCount: 0, + reportingDisabledCount: 0, + otherCount: 0, + } + ) + + const totalTenants = tenants.length + const alignedCount = + counts.compliantCount + counts.acceptedDeviationCount + counts.customerSpecificCount + const compliancePercentage = totalTenants + ? Math.round((alignedCount / totalTenants) * 100) + : 0 + const licenseMissingPercentage = totalTenants + ? Math.round((counts.licenseMissingCount / totalTenants) * 100) + : 0 + + return { + standardId: standard.standardId, + standardName: standard.standardName, + category: standard.category, + standardType: Array.from(standard.standardTypes).sort().join(', ') || 'N/A', + totalTenants, + alignedCount, + compliancePercentage, + alignmentScore: compliancePercentage, + LicenseMissingPercentage: licenseMissingPercentage, + complianceScore: `${compliancePercentage}%`, + summaryStatus: compliancePercentage === 100 ? 'Fully Compliant' : 'Needs Attention', + hasNonCompliant: counts.nonCompliantCount > 0 ? 'Yes' : 'No', + hasLicenseMissing: counts.licenseMissingCount > 0 ? 'Yes' : 'No', + hasAcceptedDeviation: counts.acceptedDeviationCount > 0 ? 'Yes' : 'No', + isFullyCompliant: compliancePercentage === 100 ? 'Yes' : 'No', + tenants, + ...counts, + } + }) + .sort((a, b) => a.standardName.localeCompare(b.standardName)) + }, [byStandardSourceData]) const summaryFilterList = [ { @@ -53,6 +282,29 @@ const Page = () => { }, ] + const byStandardFilterList = [ + { + filterName: 'Fully Compliant', + value: [{ id: 'isFullyCompliant', value: 'Yes' }], + type: 'column', + }, + { + filterName: 'Has Non-Compliant', + value: [{ id: 'hasNonCompliant', value: 'Yes' }], + type: 'column', + }, + { + filterName: 'License Missing', + value: [{ id: 'hasLicenseMissing', value: 'Yes' }], + type: 'column', + }, + { + filterName: 'Accepted Deviation', + value: [{ id: 'hasAcceptedDeviation', value: 'Yes' }], + type: 'column', + }, + ] + const summaryActions = [ { label: 'View Tenant Report', @@ -178,16 +430,7 @@ const Page = () => { standardsData.find((s) => s.name === baseName)?.label ?? row.standardName - const complianceColors = { - compliant: 'success', - 'non-compliant': 'error', - 'accepted deviation': 'info', - 'customer specific': 'info', - 'license missing': 'warning', - 'reporting disabled': 'default', - } - const statusColor = - complianceColors[String(row.complianceStatus ?? '').toLowerCase()] ?? 'default' + const statusColor = getComplianceColor(row.complianceStatus) const properties = [ { label: 'Standard', value: prettyName }, @@ -434,39 +677,241 @@ const Page = () => { }, } + const byStandardOffCanvas = { + size: 'md', + title: 'Standard Tenant Summary', + contentPadding: 0, + children: (row) => { + const standardInfo = getStandardInfo(row.standardId) + const properties = [ + { label: 'Standard', value: row.standardName }, + { label: 'Category', value: row.category }, + { label: 'Type', value: row.standardType }, + { label: 'Tenants', value: row.totalTenants }, + { label: 'Compliance', value: `${row.alignmentScore}%` }, + { label: 'Licenses Missing', value: `${row.LicenseMissingPercentage}%` }, + ] + const tenants = row.tenants ?? [] + const compliantTenants = tenants.filter((tenant) => + isAlignedComplianceStatus(tenant.complianceStatus) + ) + const nonCompliantTenants = tenants.filter( + (tenant) => !isAlignedComplianceStatus(tenant.complianceStatus) + ) + const filteredTenants = + byStandardTenantFilter === 'compliant' + ? compliantTenants + : byStandardTenantFilter === 'nonCompliant' + ? nonCompliantTenants + : tenants + + return ( + + } + sx={{ borderBottom: '1px solid', borderColor: 'divider' }} + > + {properties.map(({ label, value }) => ( + + + {label} + + + {value ?? 'N/A'} + + + ))} + + + {standardInfo?.helpText && ( + + + Description + + + {standardInfo.helpText} + + + )} + + + + + Tenant Compliance + + { + if (newFilter !== null) setByStandardTenantFilter(newFilter) + }} + sx={{ alignSelf: { xs: 'flex-start', sm: 'center' } }} + > + All ({tenants.length}) + Compliant ({compliantTenants.length}) + + Noncompliant ({nonCompliantTenants.length}) + + + + {filteredTenants.length === 0 && ( + + No tenants match this filter. + + )} + {filteredTenants.map((tenant) => ( + + + + + {tenant.tenantFilter} + + + Template: {tenant.templateName} + + + + + + Last Applied:{' '} + {tenant.latestDataCollection + ? new Date(tenant.latestDataCollection).toLocaleString() + : 'N/A'} + {tenant.rowCount > 1 ? ` (${tenant.rowCount} template matches)` : ''} + + + ))} + + + ) + }, + } + const modeToggle = ( - - - ) : ( - - ) - } - label={granular ? 'Per Standard' : 'Summary'} - onClick={() => setGranular((v) => !v)} - color="primary" - variant="filled" - size="small" - clickable - /> - + { + if (newViewMode !== null) setViewMode(newViewMode) + }} + > + + + + + Summary + + + + + + + + Per Standard + + + + + + + + By Standard + + + + ) return ( { 'standardType', 'latestDataCollection', ] - : [ - 'tenantFilter', - 'standardName', - 'standardType', - 'alignmentScore', - 'LicenseMissingPercentage', - 'combinedAlignmentScore', - 'pendingDeviationsCount', - 'deniedDeviationsCount', - ] + : isByStandard + ? [ + 'standardName', + 'category', + 'standardType', + 'totalTenants', + 'tenants', + 'compliancePercentage', + 'LicenseMissingPercentage', + 'alignedCount', + 'compliantCount', + 'nonCompliantCount', + 'licenseMissingCount', + 'acceptedDeviationCount', + ] + : [ + 'tenantFilter', + 'standardName', + 'standardType', + 'alignmentScore', + 'LicenseMissingPercentage', + 'combinedAlignmentScore', + 'pendingDeviationsCount', + 'deniedDeviationsCount', + ] + } + queryKey={ + isGranular + ? 'listTenantAlignment-granular' + : isByStandard + ? 'listTenantAlignment-byStandard' + : 'listTenantAlignment' } - queryKey={granular ? 'listTenantAlignment-granular' : 'listTenantAlignment'} - offCanvas={granular ? granularOffCanvas : undefined} + offCanvas={isGranular ? granularOffCanvas : isByStandard ? byStandardOffCanvas : undefined} + offCanvasOnRowClick={isByStandard} cardButton={modeToggle} /> ) diff --git a/src/utils/get-cipp-column-size.js b/src/utils/get-cipp-column-size.js index a62c579b8d48..d24c5d549b8c 100644 --- a/src/utils/get-cipp-column-size.js +++ b/src/utils/get-cipp-column-size.js @@ -15,6 +15,8 @@ export const getCippColumnSize = (accessorKey, header) => { switch (accessorKey) { case 'alignmentScore': case 'combinedAlignmentScore': + case 'compliancePercentage': + case 'complianceScore': case 'LicenseMissingPercentage': case 'ScorePercentage': return { size: 250, minSize: 250 } diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 62473638e095..9680ecaca029 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -259,7 +259,11 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return isText ? data : data } - if (cellName === 'alignmentScore' || cellName === 'combinedAlignmentScore') { + if ( + cellName === 'alignmentScore' || + cellName === 'combinedAlignmentScore' || + cellName === 'compliancePercentage' + ) { // Handle alignment score, return a percentage with a label return isText ? ( `${data}%` From 3f7ed1f10d44f7af88535fe4c12efb6a743c390b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 17:09:56 -0400 Subject: [PATCH 44/86] feat: add Indirect Reseller Link component and integrate into onboarding wizard implements #5963 --- .../CippWizard/CippAddTenantTypeSelection.jsx | 94 +++++++----- .../CippWizard/CippIndirectResellerLink.jsx | 141 ++++++++++++++++++ .../CippWizard/OnboardingWizardPage.jsx | 7 + 3 files changed, 202 insertions(+), 40 deletions(-) create mode 100644 src/components/CippWizard/CippIndirectResellerLink.jsx diff --git a/src/components/CippWizard/CippAddTenantTypeSelection.jsx b/src/components/CippWizard/CippAddTenantTypeSelection.jsx index f8ba6a384884..57e0b2ebac87 100644 --- a/src/components/CippWizard/CippAddTenantTypeSelection.jsx +++ b/src/components/CippWizard/CippAddTenantTypeSelection.jsx @@ -1,68 +1,82 @@ -import { Avatar, Card, CardContent, Stack, SvgIcon, Typography } from "@mui/material"; -import { useState, useEffect } from "react"; -import { CippWizardStepButtons } from "./CippWizardStepButtons"; -import { BuildingOfficeIcon, CloudIcon } from "@heroicons/react/24/outline"; +import { Avatar, Card, CardContent, Stack, SvgIcon, Typography } from '@mui/material' +import { useState, useEffect } from 'react' +import { CippWizardStepButtons } from './CippWizardStepButtons' +import { BuildingOfficeIcon, CloudIcon, LinkIcon } from '@heroicons/react/24/outline' export const CippAddTenantTypeSelection = (props) => { - const { onNextStep, formControl, currentStep, onPreviousStep } = props; + const { onNextStep, formControl, currentStep, onPreviousStep } = props - const [selectedOption, setSelectedOption] = useState(null); + const [selectedOption, setSelectedOption] = useState(null) // Register the tenantType field in react-hook-form - formControl.register("tenantType", { + formControl.register('tenantType', { required: true, - }); + }) // Restore selection if already set (when navigating back) useEffect(() => { - const currentValue = formControl.getValues("tenantType"); + const currentValue = formControl.getValues('tenantType') if (currentValue) { - setSelectedOption(currentValue); + setSelectedOption(currentValue) } // Restore the form's selectedOption state if navigating back - const selectedOptionValue = formControl.getValues("selectedOption"); + const selectedOptionValue = formControl.getValues('selectedOption') if (selectedOptionValue) { - formControl.setValue("selectedOption", selectedOptionValue); + formControl.setValue('selectedOption', selectedOptionValue) } - }, [formControl]); + }, [formControl]) const handleOptionClick = (value) => { - setSelectedOption(value); - formControl.setValue("tenantType", value); + setSelectedOption(value) + formControl.setValue('tenantType', value) // Clear validation fields from other paths when changing selection // This ensures going back and choosing a different option doesn't keep old validations - if (value === "GDAP") { + if (value === 'GDAP') { // Clear Direct tenant fields - formControl.unregister("DirectTenantAuth"); - } else if (value === "Direct") { + formControl.unregister('DirectTenantAuth') + } else if (value === 'Direct') { // Clear GDAP fields - formControl.unregister("GDAPTemplate"); - formControl.unregister("GDAPInviteAccepted"); - formControl.unregister("GDAPRelationshipId"); - formControl.unregister("GDAPOnboardingComplete"); + formControl.unregister('GDAPTemplate') + formControl.unregister('GDAPInviteAccepted') + formControl.unregister('GDAPRelationshipId') + formControl.unregister('GDAPOnboardingComplete') + } else if (value === 'IndirectReseller') { + // Clear other paths + formControl.unregister('DirectTenantAuth') + formControl.unregister('GDAPTemplate') + formControl.unregister('GDAPInviteAccepted') + formControl.unregister('GDAPRelationshipId') + formControl.unregister('GDAPOnboardingComplete') } // Trigger validation only for the tenantType field - formControl.trigger("tenantType"); - }; + formControl.trigger('tenantType') + } const options = [ { - value: "GDAP", - label: "Add GDAP Tenant", + value: 'GDAP', + label: 'Add GDAP Tenant', description: "Select this option to add a new tenant to your Microsoft Partner center environment. We'll walk you through the steps of setting up GDAP.", icon: , }, { - value: "Direct", - label: "Add Direct Tenant", + value: 'Direct', + label: 'Add Direct Tenant', description: - "Select this option if you are not a Microsoft partner, or want to add a tenant outside of the scope of your partner center.", + 'Select this option if you are not a Microsoft partner, or want to add a tenant outside of the scope of your partner center.', icon: , }, - ]; + { + value: 'IndirectReseller', + label: 'Get Indirect Reseller Invite Link', + description: + 'Generate a reseller relationship invite link to send to a customer. This does not add the tenant to CIPP, but may be used by other vendors to populate their customer list.', + icon: , + }, + ] return ( @@ -74,7 +88,7 @@ export const CippAddTenantTypeSelection = (props) => { {options.map((option) => { - const isSelected = selectedOption === option.value; + const isSelected = selectedOption === option.value return ( { onClick={() => handleOptionClick(option.value)} variant="outlined" sx={{ - cursor: "pointer", + cursor: 'pointer', ...(isSelected && { boxShadow: (theme) => `0px 0px 0px 2px ${theme.palette.primary.main}`, }), - "&:hover": { + '&:hover': { ...(isSelected ? {} : { boxShadow: 8 }), }, }} @@ -96,9 +110,9 @@ export const CippAddTenantTypeSelection = (props) => { @@ -111,7 +125,7 @@ export const CippAddTenantTypeSelection = (props) => { - ); + ) })} { formControl={formControl} /> - ); -}; + ) +} -export default CippAddTenantTypeSelection; +export default CippAddTenantTypeSelection diff --git a/src/components/CippWizard/CippIndirectResellerLink.jsx b/src/components/CippWizard/CippIndirectResellerLink.jsx new file mode 100644 index 000000000000..82a83dcae989 --- /dev/null +++ b/src/components/CippWizard/CippIndirectResellerLink.jsx @@ -0,0 +1,141 @@ +import { useEffect, useMemo, useState } from 'react' +import { Alert, Autocomplete, Box, Skeleton, Stack, TextField, Typography } from '@mui/material' +import { ApiGetCall } from '../../api/ApiCall' +import { CippWizardStepButtons } from './CippWizardStepButtons' +import { CippCopyToClipBoard } from '../CippComponents/CippCopyToClipboard' + +export const CippIndirectResellerLink = (props) => { + const { formControl, currentStep, onPreviousStep, onNextStep } = props + const [selectedProvider, setSelectedProvider] = useState(null) + + const linkData = ApiGetCall({ + url: '/api/ListResellerRelationshipLink', + queryKey: 'ListResellerRelationshipLink', + }) + + const inviteUrl = linkData.data?.inviteUrl ?? null + const indirectProviders = linkData.data?.indirectProviders ?? [] + const inviteUrlError = linkData.data?.inviteUrlError ?? null + + const finalUrl = useMemo(() => { + if (!inviteUrl) return null + if (!selectedProvider) return inviteUrl + // Append the indirect provider ID before the # fragment + const hashIndex = inviteUrl.indexOf('#') + const base = hashIndex !== -1 ? inviteUrl.slice(0, hashIndex) : inviteUrl + const hash = hashIndex !== -1 ? inviteUrl.slice(hashIndex) : '' + return `${base}&indirectCSPId=${selectedProvider.id}${hash}` + }, [inviteUrl, selectedProvider]) + + const providerOptions = useMemo( + () => + indirectProviders.map((p) => ({ + label: p.name, + id: p.id, + mpnId: p.mpnId, + location: p.location, + })), + [indirectProviders] + ) + + return ( + + + + Indirect Reseller Relationship Link + + + Generate an invite link to send to a customer so they can authorize you as their indirect + reseller. This does not add the tenant to CIPP — it only provides the + Microsoft Admin Portal invitation link. + + + + {linkData.isFetching && ( + + {/* Indirect provider dropdown skeleton */} + + {/* Link field skeleton */} + + + + + + + )} + + {linkData.isError && ( + + Failed to load relationship link from the Partner Center API. Ensure your CIPP application + has the required Partner Center permissions. + + )} + + {inviteUrlError && !linkData.isError && {inviteUrlError}} + + {!linkData.isFetching && !linkData.isError && inviteUrl && ( + <> + {indirectProviders.length > 0 && ( + setSelectedProvider(value)} + getOptionLabel={(option) => option.label} + renderOption={(renderProps, option) => ( +
  • + + {option.label} + + MPN ID: {option.mpnId} · {option.location} + + +
  • + )} + renderInput={(params) => ( + + )} + /> + )} + + + + Invite Link + + + + + + + Send this link to your customer. When they follow it, they will be linked to your + reseller account in the Microsoft Admin Portal. + + + + + There is no automatic confirmation when the customer accepts this invite. You can verify + the relationship in Partner Center once the customer has completed the process. + + + )} + + +
    + ) +} diff --git a/src/components/CippWizard/OnboardingWizardPage.jsx b/src/components/CippWizard/OnboardingWizardPage.jsx index c80416dc8622..6b0876ad9a58 100644 --- a/src/components/CippWizard/OnboardingWizardPage.jsx +++ b/src/components/CippWizard/OnboardingWizardPage.jsx @@ -10,6 +10,7 @@ import { CippAlertsStep } from './CippAlertsStep.jsx' import { CippAddTenantTypeSelection } from './CippAddTenantTypeSelection.jsx' import { CippDirectTenantDeploy } from './CippDirectTenantDeploy.jsx' import { CippGDAPTenantSetup } from './CippGDAPTenantSetup.jsx' +import { CippIndirectResellerLink } from './CippIndirectResellerLink.jsx' import { CippGDAPTenantOnboarding } from './CippGDAPTenantOnboarding.jsx' import { BuildingOfficeIcon, CloudIcon, CpuChipIcon } from '@heroicons/react/24/outline' import { useRouter } from 'next/router' @@ -103,6 +104,12 @@ const OnboardingWizardPage = () => { showStepWhen: (values) => values?.selectedOption === 'AddTenant' && values?.tenantType === 'GDAP', }, + { + description: 'Reseller Link', + component: CippIndirectResellerLink, + showStepWhen: (values) => + values?.selectedOption === 'AddTenant' && values?.tenantType === 'IndirectReseller', + }, { description: 'GDAP Onboarding', component: CippGDAPTenantOnboarding, From 3878e5c48dbf2cc5bd3dee5c64af308342961377 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 17:13:57 -0400 Subject: [PATCH 45/86] fix typo --- src/data/portals.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/portals.json b/src/data/portals.json index 58f49e3ae2c3..a4402305faca 100644 --- a/src/data/portals.json +++ b/src/data/portals.json @@ -97,7 +97,7 @@ "target": "_blank", "external": true, "icon": "PrecisionManufacturing" - } + }, { "label": "Power BI", "name": "Power_BI_Portal", From f482fa814178cd09135d5228222c357c51c20009 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 17:18:43 -0400 Subject: [PATCH 46/86] fix: minor tweaks --- .../CippWizard/CippAddTenantTypeSelection.jsx | 2 +- .../CippWizard/CippIndirectResellerLink.jsx | 86 +++++++++---------- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/components/CippWizard/CippAddTenantTypeSelection.jsx b/src/components/CippWizard/CippAddTenantTypeSelection.jsx index 57e0b2ebac87..520d1b978936 100644 --- a/src/components/CippWizard/CippAddTenantTypeSelection.jsx +++ b/src/components/CippWizard/CippAddTenantTypeSelection.jsx @@ -71,7 +71,7 @@ export const CippAddTenantTypeSelection = (props) => { }, { value: 'IndirectReseller', - label: 'Get Indirect Reseller Invite Link', + label: 'Get Reseller Invite Link', description: 'Generate a reseller relationship invite link to send to a customer. This does not add the tenant to CIPP, but may be used by other vendors to populate their customer list.', icon: , diff --git a/src/components/CippWizard/CippIndirectResellerLink.jsx b/src/components/CippWizard/CippIndirectResellerLink.jsx index 82a83dcae989..991e18683f34 100644 --- a/src/components/CippWizard/CippIndirectResellerLink.jsx +++ b/src/components/CippWizard/CippIndirectResellerLink.jsx @@ -1,12 +1,13 @@ -import { useEffect, useMemo, useState } from 'react' -import { Alert, Autocomplete, Box, Skeleton, Stack, TextField, Typography } from '@mui/material' +import { useEffect, useMemo } from 'react' +import { Alert, Box, Skeleton, Stack, TextField, Typography } from '@mui/material' import { ApiGetCall } from '../../api/ApiCall' import { CippWizardStepButtons } from './CippWizardStepButtons' import { CippCopyToClipBoard } from '../CippComponents/CippCopyToClipboard' +import CippFormComponent from '../CippComponents/CippFormComponent' +import { useWatch } from 'react-hook-form' export const CippIndirectResellerLink = (props) => { const { formControl, currentStep, onPreviousStep, onNextStep } = props - const [selectedProvider, setSelectedProvider] = useState(null) const linkData = ApiGetCall({ url: '/api/ListResellerRelationshipLink', @@ -17,27 +18,36 @@ export const CippIndirectResellerLink = (props) => { const indirectProviders = linkData.data?.indirectProviders ?? [] const inviteUrlError = linkData.data?.inviteUrlError ?? null + const noneOption = { label: 'None (no indirect provider)', value: null } + + const providerOptions = useMemo(() => { + const providers = indirectProviders.map((p) => ({ + label: `${p.name} — MPN: ${p.mpnId} (${p.location})`, + value: p.id, + })) + return [noneOption, ...providers] + }, [indirectProviders]) + + useEffect(() => { + if (!linkData.isFetching && providerOptions.length > 0) { + const current = formControl.getValues('indirectProviderId') + if (!current) { + formControl.setValue('indirectProviderId', noneOption) + } + } + }, [linkData.isFetching, providerOptions]) + + const selectedProvider = useWatch({ control: formControl.control, name: 'indirectProviderId' }) + const finalUrl = useMemo(() => { if (!inviteUrl) return null - if (!selectedProvider) return inviteUrl - // Append the indirect provider ID before the # fragment + if (!selectedProvider?.value) return inviteUrl const hashIndex = inviteUrl.indexOf('#') const base = hashIndex !== -1 ? inviteUrl.slice(0, hashIndex) : inviteUrl const hash = hashIndex !== -1 ? inviteUrl.slice(hashIndex) : '' - return `${base}&indirectCSPId=${selectedProvider.id}${hash}` + return `${base}&indirectCSPId=${selectedProvider.value}${hash}` }, [inviteUrl, selectedProvider]) - const providerOptions = useMemo( - () => - indirectProviders.map((p) => ({ - label: p.name, - id: p.id, - mpnId: p.mpnId, - location: p.location, - })), - [indirectProviders] - ) - return ( @@ -53,9 +63,7 @@ export const CippIndirectResellerLink = (props) => { {linkData.isFetching && ( - {/* Indirect provider dropdown skeleton */} - {/* Link field skeleton */} @@ -75,32 +83,17 @@ export const CippIndirectResellerLink = (props) => { {!linkData.isFetching && !linkData.isError && inviteUrl && ( <> - {indirectProviders.length > 0 && ( - setSelectedProvider(value)} - getOptionLabel={(option) => option.label} - renderOption={(renderProps, option) => ( -
  • - - {option.label} - - MPN ID: {option.mpnId} · {option.location} - - -
  • - )} - renderInput={(params) => ( - - )} - /> - )} + @@ -110,9 +103,8 @@ export const CippIndirectResellerLink = (props) => {
    From 64a54390e0a7ee199d930fd89a150e899a97d415 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 8 May 2026 19:17:26 -0400 Subject: [PATCH 47/86] chore: bump version to 10.4.4 --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ee84a95ad7bf..bba74b63d2e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.4.3", + "version": "10.4.4", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 521d35e0af9b..0ac81ba1b0ba 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.4.3" + "version": "10.4.4" } From 45f1d72bf7e1376fbc2e4f58ecc39d9a4c3e363b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 10 May 2026 01:25:58 +0200 Subject: [PATCH 48/86] purview adding --- .../CippDeployCompliancePolicyDrawer.jsx | 19 +-- src/data/standards.json | 104 +++++++++++++++ src/layouts/config.js | 120 +++++++++--------- .../compliance/dlp-templates/index.js | 1 + src/pages/security/compliance/dlp/index.js | 1 + .../compliance/labels-templates/index.js | 1 + src/pages/security/compliance/labels/index.js | 1 + .../compliance/retention-templates/index.js | 1 + .../security/compliance/retention/index.js | 1 + .../compliance/sit-templates/index.js | 1 + src/pages/security/compliance/sit/index.js | 13 +- 11 files changed, 182 insertions(+), 81 deletions(-) diff --git a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx index 3cf35e3cb2f8..c03c976f9f94 100644 --- a/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx +++ b/src/components/CippComponents/CippDeployCompliancePolicyDrawer.jsx @@ -88,18 +88,21 @@ const MODE_CONFIG = { listTemplatesUrl: "/api/ListSensitiveInfoTypeTemplates", templateQueryKey: "TemplateListSensitiveInfoType", relatedQueryKeys: ["ListSensitiveInfoType", "ListSensitiveInfoTypeTemplates"], - placeholder: `{ - "Name": "Custom Employee ID", - "Description": "Internal Employee ID format EMP-NNNNN", + placeholder: `// Simple mode — backend wraps the regex in a rule pack for you +{ + "Name": "Acme Employee ID", + "Description": "Matches Acme employee IDs in the format EMP-NNNNN", "Pattern": "EMP-\\\\d{5}", - "Confidence": "High", - "Recommended": true + "Confidence": 85, + "PatternsProximity": 300, + "PublisherName": "Acme Corp" } -// Or with a base64-encoded XML rule pack: +// Advanced mode — provide your own rule pack XML, base64-encoded // { -// "Name": "Custom Rule Pack", -// "FileDataBase64": "" +// "Name": "Acme Custom Rule Pack", +// "Description": "Multi-pattern rule pack", +// "FileDataBase64": "" // }`, }, }; diff --git a/src/data/standards.json b/src/data/standards.json index cc708f747177..5b70e1eba3b1 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6205,6 +6205,110 @@ ], "requiredCapabilities": ["EXCHANGE_S_STANDARD", "EXCHANGE_S_ENTERPRISE", "EXCHANGE_LITE"] }, + { + "name": "standards.DlpCompliancePolicyTemplate", + "label": "DLP Compliance Policy Template", + "multi": true, + "cat": "Templates", + "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "impact": "Medium Impact", + "addedDate": "2026-05-10", + "helpText": "Deploy Microsoft Purview DLP compliance policies from CIPP templates.", + "executiveText": "Deploys Data Loss Prevention policies from a standardized template library. Ensures consistent DLP coverage across tenants for sensitive data such as financial, identity, and regulated content.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "dlpCompliancePolicyTemplate", + "label": "Select DLP Compliance Policy Templates", + "api": { + "url": "/api/ListDlpCompliancePolicyTemplates", + "labelField": "name", + "valueField": "GUID", + "queryKey": "ListDlpCompliancePolicyTemplates" + } + } + ] + }, + { + "name": "standards.RetentionCompliancePolicyTemplate", + "label": "Retention Compliance Policy Template", + "multi": true, + "cat": "Templates", + "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "impact": "Medium Impact", + "addedDate": "2026-05-10", + "helpText": "Deploy Microsoft Purview retention compliance policies from CIPP templates.", + "executiveText": "Deploys retention policies that govern how long content is preserved in Exchange, SharePoint, OneDrive, and Teams. Enforces consistent compliance retention across tenants for regulatory and legal hold needs.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "retentionCompliancePolicyTemplate", + "label": "Select Retention Compliance Policy Templates", + "api": { + "url": "/api/ListRetentionCompliancePolicyTemplates", + "labelField": "name", + "valueField": "GUID", + "queryKey": "ListRetentionCompliancePolicyTemplates" + } + } + ] + }, + { + "name": "standards.SensitivityLabelTemplate", + "label": "Sensitivity Label Template", + "multi": true, + "cat": "Templates", + "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "impact": "Medium Impact", + "addedDate": "2026-05-10", + "helpText": "Deploy Microsoft Purview sensitivity labels from CIPP templates.", + "executiveText": "Deploys sensitivity labels for classification and protection of files, emails, and Microsoft 365 group content. Ensures consistent classification taxonomy and encryption settings across tenants.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "sensitivityLabelTemplate", + "label": "Select Sensitivity Label Templates", + "api": { + "url": "/api/ListSensitivityLabelTemplates", + "labelField": "name", + "valueField": "GUID", + "queryKey": "ListSensitivityLabelTemplates" + } + } + ] + }, + { + "name": "standards.SensitiveInfoTypeTemplate", + "label": "Sensitive Information Type Template", + "multi": true, + "cat": "Templates", + "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "impact": "Low Impact", + "addedDate": "2026-05-10", + "helpText": "Deploy custom Microsoft Purview Sensitive Information Types from CIPP templates.", + "executiveText": "Deploys custom Sensitive Information Types so DLP policies can detect organization-specific identifiers — employee IDs, project codenames, internal account numbers — across tenants consistently.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "name": "sensitiveInfoTypeTemplate", + "label": "Select Sensitive Information Type Templates", + "api": { + "url": "/api/ListSensitiveInfoTypeTemplates", + "labelField": "name", + "valueField": "GUID", + "queryKey": "ListSensitiveInfoTypeTemplates" + } + } + ] + }, { "name": "standards.AssignmentFilterTemplate", "label": "Assignment Filter Template", diff --git a/src/layouts/config.js b/src/layouts/config.js index c820f5664acc..0cc0c8ec303b 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -301,11 +301,10 @@ export const nativeMenuItems = [ 'Security.Alert.*', 'Tenant.DeviceCompliance.*', 'Security.SafeLinksPolicy.*', - // TEMP: Purview Compliance menu hidden for dev build - // 'Security.DlpCompliancePolicy.*', - // 'Security.RetentionCompliancePolicy.*', - // 'Security.SensitivityLabel.*', - // 'Security.SensitiveInfoType.*', + 'Security.DlpCompliancePolicy.*', + 'Security.RetentionCompliancePolicy.*', + 'Security.SensitivityLabel.*', + 'Security.SensitiveInfoType.*', ], items: [ { @@ -388,62 +387,61 @@ export const nativeMenuItems = [ }, ], }, - // TEMP: Purview Compliance menu hidden for dev build - // { - // title: 'Purview Compliance', - // permissions: [ - // 'Security.DlpCompliancePolicy.*', - // 'Security.RetentionCompliancePolicy.*', - // 'Security.SensitivityLabel.*', - // 'Security.SensitiveInfoType.*', - // ], - // items: [ - // { - // title: 'DLP Policies', - // path: '/security/compliance/dlp', - // permissions: ['Security.DlpCompliancePolicy.*'], - // }, - // { - // title: 'DLP Policy Templates', - // path: '/security/compliance/dlp-templates', - // permissions: ['Security.DlpCompliancePolicy.*'], - // scope: 'global', - // }, - // { - // title: 'Retention Policies', - // path: '/security/compliance/retention', - // permissions: ['Security.RetentionCompliancePolicy.*'], - // }, - // { - // title: 'Retention Policy Templates', - // path: '/security/compliance/retention-templates', - // permissions: ['Security.RetentionCompliancePolicy.*'], - // scope: 'global', - // }, - // { - // title: 'Sensitivity Labels', - // path: '/security/compliance/labels', - // permissions: ['Security.SensitivityLabel.*'], - // }, - // { - // title: 'Sensitivity Label Templates', - // path: '/security/compliance/labels-templates', - // permissions: ['Security.SensitivityLabel.*'], - // scope: 'global', - // }, - // { - // title: 'Sensitive Information Types', - // path: '/security/compliance/sit', - // permissions: ['Security.SensitiveInfoType.*'], - // }, - // { - // title: 'Sensitive Info Type Templates', - // path: '/security/compliance/sit-templates', - // permissions: ['Security.SensitiveInfoType.*'], - // scope: 'global', - // }, - // ], - // }, + { + title: 'Purview Compliance', + permissions: [ + 'Security.DlpCompliancePolicy.*', + 'Security.RetentionCompliancePolicy.*', + 'Security.SensitivityLabel.*', + 'Security.SensitiveInfoType.*', + ], + items: [ + { + title: 'DLP Policies', + path: '/security/compliance/dlp', + permissions: ['Security.DlpCompliancePolicy.*'], + }, + { + title: 'DLP Policy Templates', + path: '/security/compliance/dlp-templates', + permissions: ['Security.DlpCompliancePolicy.*'], + scope: 'global', + }, + { + title: 'Retention Policies', + path: '/security/compliance/retention', + permissions: ['Security.RetentionCompliancePolicy.*'], + }, + { + title: 'Retention Policy Templates', + path: '/security/compliance/retention-templates', + permissions: ['Security.RetentionCompliancePolicy.*'], + scope: 'global', + }, + { + title: 'Sensitivity Labels', + path: '/security/compliance/labels', + permissions: ['Security.SensitivityLabel.*'], + }, + { + title: 'Sensitivity Label Templates', + path: '/security/compliance/labels-templates', + permissions: ['Security.SensitivityLabel.*'], + scope: 'global', + }, + { + title: 'Sensitive Information Types', + path: '/security/compliance/sit', + permissions: ['Security.SensitiveInfoType.*'], + }, + { + title: 'Sensitive Info Type Templates', + path: '/security/compliance/sit-templates', + permissions: ['Security.SensitiveInfoType.*'], + scope: 'global', + }, + ], + }, ], }, { diff --git a/src/pages/security/compliance/dlp-templates/index.js b/src/pages/security/compliance/dlp-templates/index.js index b2b597c51167..99e949f700d1 100644 --- a/src/pages/security/compliance/dlp-templates/index.js +++ b/src/pages/security/compliance/dlp-templates/index.js @@ -81,6 +81,7 @@ const Page = () => { { { { { { { { const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"]; const actions = [ - { - label: "Create template based on SIT", - type: "POST", - icon: , - url: "/api/AddSensitiveInfoTypeTemplate", - dataFunction: (data) => { - return { ...data }; - }, - confirmText: - "Are you sure you want to create a template based on this Sensitive Information Type?", - }, { label: "Delete SIT", type: "POST", @@ -63,6 +51,7 @@ const Page = () => { Date: Sun, 10 May 2026 14:51:51 +0200 Subject: [PATCH 49/86] fix alert mode --- src/data/standards.json | 8 ++++---- src/pages/security/compliance/retention/index.js | 4 +--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 5b70e1eba3b1..c7a825e06120 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6210,7 +6210,7 @@ "label": "DLP Compliance Policy Template", "multi": true, "cat": "Templates", - "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2026-05-10", "helpText": "Deploy Microsoft Purview DLP compliance policies from CIPP templates.", @@ -6236,7 +6236,7 @@ "label": "Retention Compliance Policy Template", "multi": true, "cat": "Templates", - "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2026-05-10", "helpText": "Deploy Microsoft Purview retention compliance policies from CIPP templates.", @@ -6262,7 +6262,7 @@ "label": "Sensitivity Label Template", "multi": true, "cat": "Templates", - "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Medium Impact", "addedDate": "2026-05-10", "helpText": "Deploy Microsoft Purview sensitivity labels from CIPP templates.", @@ -6288,7 +6288,7 @@ "label": "Sensitive Information Type Template", "multi": true, "cat": "Templates", - "disabledFeatures": { "report": false, "warn": true, "remediate": false }, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, "impact": "Low Impact", "addedDate": "2026-05-10", "helpText": "Deploy custom Microsoft Purview Sensitive Information Types from CIPP templates.", diff --git a/src/pages/security/compliance/retention/index.js b/src/pages/security/compliance/retention/index.js index db9fd2d6eae8..962301013f29 100644 --- a/src/pages/security/compliance/retention/index.js +++ b/src/pages/security/compliance/retention/index.js @@ -16,9 +16,7 @@ const Page = () => { type: "POST", icon: , url: "/api/AddRetentionCompliancePolicyTemplate", - dataFunction: (data) => { - return { ...data }; - }, + data: { Identity: "Name" }, confirmText: "Are you sure you want to create a template based on this retention policy?", }, { From 98d5d94a0d122d5fbc7380d5abe5cb02fba366fd Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 11 May 2026 21:49:35 +0800 Subject: [PATCH 50/86] Custom Test - Alert on X statuses --- src/pages/tools/custom-tests/add.jsx | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/pages/tools/custom-tests/add.jsx b/src/pages/tools/custom-tests/add.jsx index 41b01afdd9ea..e4da31a623c5 100644 --- a/src/pages/tools/custom-tests/add.jsx +++ b/src/pages/tools/custom-tests/add.jsx @@ -37,6 +37,7 @@ import { renderCustomScriptMarkdownTemplate } from '../../../utils/customScriptT import { useSettings } from '../../../hooks/use-settings' import CippFormPage from '../../../components/CippFormPages/CippFormPage' import CippFormComponent from '../../../components/CippComponents/CippFormComponent' +import { CippFormCondition } from '../../../components/CippComponents/CippFormCondition' import { CippApiResults } from '../../../components/CippComponents/CippApiResults' import { CippCodeBlock } from '../../../components/CippComponents/CippCodeBlock' import { markdownStyles } from '../../../components/CippTestDetail/CippTestDetailOffCanvas' @@ -117,6 +118,7 @@ const Page = () => { ScriptContent: '', Enabled: false, AlertOnFailure: false, + AlertStatuses: [{ value: 'Failed', label: 'Failed' }], ReturnType: 'JSON', ResultMode: { value: 'Auto', label: 'Auto' }, MarkdownTemplate: '', @@ -146,6 +148,12 @@ const Page = () => { ScriptContent: script.ScriptContent || '', Enabled: script.Enabled || false, AlertOnFailure: script.AlertOnFailure || false, + AlertStatuses: script.AlertStatuses + ? (typeof script.AlertStatuses === 'string' + ? JSON.parse(script.AlertStatuses) + : script.AlertStatuses + ).map((s) => ({ value: s, label: s })) + : [{ value: 'Failed', label: 'Failed' }], ReturnType: script.ReturnType || 'JSON', ResultMode: toSelectOption(script.ResultMode, 'Auto'), MarkdownTemplate: script.MarkdownTemplate || '', @@ -253,6 +261,9 @@ const Page = () => { ScriptContent: data.ScriptContent, Enabled: data.Enabled, AlertOnFailure: data.AlertOnFailure, + AlertStatuses: data.AlertOnFailure + ? (data.AlertStatuses?.map(s => s.value) || ['Failed']) + : [], ReturnType: data.ReturnType, ResultMode: data.ResultMode?.value ?? data.ResultMode, MarkdownTemplate: data.MarkdownTemplate, @@ -401,6 +412,20 @@ const Page = () => { 'When enabled, a failed test triggers an alert routed to your configured notification channels (email, webhook, or PSA).', } + const alertStatusesField = { + name: 'AlertStatuses', + label: 'Alert on Status', + type: 'autoComplete', + multiple: true, + options: [ + { label: 'Failed', value: 'Failed' }, + { label: 'Passed', value: 'Passed' }, + { label: 'Info', value: 'Info' }, + { label: 'Investigate', value: 'Investigate' }, + ], + helperText: 'Choose which test result statuses trigger an alert.', + } + const returnTypeField = { name: 'ReturnType', label: 'Result Display Type', @@ -1293,6 +1318,20 @@ $md = $summaryTable + "\n\n---\n\n" + $policyTable disabled={isScriptLoading} />
    + + + + + From 4b9efd827d27a3498eca51b444934a99e78c634f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 11 May 2026 23:14:49 +0800 Subject: [PATCH 51/86] Update index.js --- src/pages/tenant/reports/list-licenses/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/reports/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js index 35d61ea16158..24a228054f1d 100644 --- a/src/pages/tenant/reports/list-licenses/index.js +++ b/src/pages/tenant/reports/list-licenses/index.js @@ -16,7 +16,7 @@ const Page = () => { "TermInfo", // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby ]; - return ; + return ; }; Page.getLayout = (page) => {page}; From cfe8c705025c918e34646f68bba815af2a0d04b2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 11 May 2026 19:51:43 +0200 Subject: [PATCH 52/86] adds #5939 --- .../tenant/reports/list-licenses/index.js | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/reports/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js index 24a228054f1d..7c2a26c3922f 100644 --- a/src/pages/tenant/reports/list-licenses/index.js +++ b/src/pages/tenant/reports/list-licenses/index.js @@ -1,5 +1,7 @@ import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { AssignmentInd } from "@mui/icons-material"; +import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; const Page = () => { const pageTitle = "Licences Report"; @@ -16,7 +18,73 @@ const Page = () => { "TermInfo", // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby ]; - return ; + const actions = [ + { + label: "Assign License to User", + type: "POST", + url: "/api/ExecBulkLicense", + icon: , + confirmText: "Are you sure you want to assign [License] to the selected user?", + multiPost: false, + children: ({ formHook, row }) => ( + `${option.displayName} (${option.userPrincipalName})`, + valueField: "id", + queryKey: `Users-${row?.Tenant}`, + data: { + Endpoint: "users", + $select: "id,displayName,userPrincipalName", + $count: true, + $orderby: "displayName", + $top: 999, + }, + }} + /> + ), + customDataformatter: (row, action, formData) => ({ + tenantFilter: row.Tenant, + LicenseOperation: "Add", + Licenses: [{ label: row.License, value: row.skuId }], + userIds: [formData.userIds?.value], + }), + }, + ]; + + const offCanvas = { + extendedInfoFields: [ + "Tenant", + "License", + "CountUsed", + "CountAvailable", + "TotalLicenses", + "AssignedUsers", + "AssignedGroups", + "TermInfo", + ], + actions: actions, + }; + + return ( + + ); }; Page.getLayout = (page) => {page}; From 0d42f6798b69367401c33f692b541faa8f483651 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 11 May 2026 19:51:47 +0200 Subject: [PATCH 53/86] #5939 --- .../tenant/reports/list-licenses/index.js | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/src/pages/tenant/reports/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js index 7c2a26c3922f..4d877df75f8f 100644 --- a/src/pages/tenant/reports/list-licenses/index.js +++ b/src/pages/tenant/reports/list-licenses/index.js @@ -1,30 +1,30 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { AssignmentInd } from "@mui/icons-material"; -import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { AssignmentInd } from '@mui/icons-material' +import CippFormComponent from '../../../../components/CippComponents/CippFormComponent' const Page = () => { - const pageTitle = "Licences Report"; - const apiUrl = "/api/ListLicenses"; + const pageTitle = 'Licences Report' + const apiUrl = '/api/ListLicenses' const simpleColumns = [ - "Tenant", - "License", - "CountUsed", - "CountAvailable", - "TotalLicenses", - "AssignedUsers", - "AssignedGroups", - "TermInfo", // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby - ]; + 'Tenant', + 'License', + 'CountUsed', + 'CountAvailable', + 'TotalLicenses', + 'AssignedUsers', + 'AssignedGroups', + 'TermInfo', // TODO TermInfo is not showing as a clickable json object in the table, like CApolicies does in the mfa report. IDK how to fix it. -Bobby + ] const actions = [ { - label: "Assign License to User", - type: "POST", - url: "/api/ExecBulkLicense", + label: 'Assign License to User', + type: 'POST', + url: '/api/ExecBulkLicense', icon: , - confirmText: "Are you sure you want to assign [License] to the selected user?", + confirmText: 'Are you sure you want to assign [License] to the selected user?', multiPost: false, children: ({ formHook, row }) => ( { multiple={false} creatable={false} formControl={formHook} - validators={{ required: "Please select a user" }} + validators={{ required: 'Please select a user' }} api={{ tenantFilter: row?.Tenant, - url: "/api/ListGraphRequest", - dataKey: "Results", + url: '/api/ListGraphRequest', + dataKey: 'Results', labelField: (option) => `${option.displayName} (${option.userPrincipalName})`, - valueField: "id", + valueField: 'id', queryKey: `Users-${row?.Tenant}`, data: { - Endpoint: "users", - $select: "id,displayName,userPrincipalName", + Endpoint: 'users', + $select: 'id,displayName,userPrincipalName', $count: true, - $orderby: "displayName", + $orderby: 'displayName', $top: 999, }, }} @@ -54,26 +54,26 @@ const Page = () => { ), customDataformatter: (row, action, formData) => ({ tenantFilter: row.Tenant, - LicenseOperation: "Add", + LicenseOperation: 'Add', Licenses: [{ label: row.License, value: row.skuId }], userIds: [formData.userIds?.value], }), }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Tenant", - "License", - "CountUsed", - "CountAvailable", - "TotalLicenses", - "AssignedUsers", - "AssignedGroups", - "TermInfo", + 'Tenant', + 'License', + 'CountUsed', + 'CountAvailable', + 'TotalLicenses', + 'AssignedUsers', + 'AssignedGroups', + 'TermInfo', ], actions: actions, - }; + } return ( { actions={actions} offCanvas={offCanvas} /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page From 64e408072e2cea57f098f49acbd84dd3d96187b2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 11 May 2026 19:58:59 +0200 Subject: [PATCH 54/86] implemenets #5948 --- .../CippApplicationDeployDrawer.jsx | 299 ++++++++++-------- 1 file changed, 159 insertions(+), 140 deletions(-) diff --git a/src/components/CippComponents/CippApplicationDeployDrawer.jsx b/src/components/CippComponents/CippApplicationDeployDrawer.jsx index 99c2cd52d249..56c7ecae0655 100644 --- a/src/components/CippComponents/CippApplicationDeployDrawer.jsx +++ b/src/components/CippComponents/CippApplicationDeployDrawer.jsx @@ -1,119 +1,119 @@ -import React, { useEffect, useCallback, useState } from "react"; -import { Divider, Button, Alert, CircularProgress } from "@mui/material"; -import { Grid } from "@mui/system"; -import { useForm, useWatch } from "react-hook-form"; -import { Add } from "@mui/icons-material"; -import { CippOffCanvas } from "./CippOffCanvas"; -import CippFormComponent from "./CippFormComponent"; -import { CippFormTenantSelector } from "./CippFormTenantSelector"; -import { CippFormCondition } from "./CippFormCondition"; -import { CippApiResults } from "./CippApiResults"; -import languageList from "../../data/languageList.json"; -import { ApiPostCall } from "../../api/ApiCall"; +import React, { useEffect, useCallback, useState } from 'react' +import { Divider, Button, Alert, CircularProgress } from '@mui/material' +import { Grid } from '@mui/system' +import { useForm, useWatch } from 'react-hook-form' +import { Add } from '@mui/icons-material' +import { CippOffCanvas } from './CippOffCanvas' +import CippFormComponent from './CippFormComponent' +import { CippFormTenantSelector } from './CippFormTenantSelector' +import { CippFormCondition } from './CippFormCondition' +import { CippApiResults } from './CippApiResults' +import languageList from '../../data/languageList.json' +import { ApiPostCall } from '../../api/ApiCall' export const CippApplicationDeployDrawer = ({ - buttonText = "Add Application", + buttonText = 'Add Application', requiredPermissions = [], PermissionButton = Button, }) => { - const [drawerVisible, setDrawerVisible] = useState(false); + const [drawerVisible, setDrawerVisible] = useState(false) const formControl = useForm({ - mode: "onChange", - }); + mode: 'onChange', + }) const selectedTenants = useWatch({ control: formControl.control, - name: "selectedTenants", - }); + name: 'selectedTenants', + }) const applicationType = useWatch({ control: formControl.control, - name: "appType", - }); + name: 'appType', + }) const searchQuerySelection = useWatch({ control: formControl.control, - name: "packageSearch", - }); + name: 'packageSearch', + }) const updateSearchSelection = useCallback( (searchQuerySelection) => { if (searchQuerySelection) { - formControl.setValue("packagename", searchQuerySelection.value.packagename); - formControl.setValue("applicationName", searchQuerySelection.value.applicationName); - formControl.setValue("description", searchQuerySelection.value.description); + formControl.setValue('packagename', searchQuerySelection.value.packagename) + formControl.setValue('applicationName', searchQuerySelection.value.applicationName) + formControl.setValue('description', searchQuerySelection.value.description) searchQuerySelection.value.customRepo - ? formControl.setValue("customRepo", searchQuerySelection.value.customRepo) - : null; + ? formControl.setValue('customRepo', searchQuerySelection.value.customRepo) + : null } }, - [formControl.setValue], - ); + [formControl.setValue] + ) useEffect(() => { - updateSearchSelection(searchQuerySelection); - }, [updateSearchSelection, searchQuerySelection]); + updateSearchSelection(searchQuerySelection) + }, [updateSearchSelection, searchQuerySelection]) const postUrl = { - mspApp: "/api/AddMSPApp", - StoreApp: "/api/AddStoreApp", - winGetApp: "/api/AddwinGetApp", - chocolateyApp: "/api/AddChocoApp", - officeApp: "/api/AddOfficeApp", - win32ScriptApp: "/api/AddWin32ScriptApp", - }; + mspApp: '/api/AddMSPApp', + StoreApp: '/api/AddStoreApp', + winGetApp: '/api/AddwinGetApp', + chocolateyApp: '/api/AddChocoApp', + officeApp: '/api/AddOfficeApp', + win32ScriptApp: '/api/AddWin32ScriptApp', + } const ChocosearchResults = ApiPostCall({ urlFromData: true, - }); + }) const winGetSearchResults = ApiPostCall({ urlFromData: true, - }); + }) const deployApplication = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["Queued Applications"], - }); + relatedQueryKeys: ['Queued Applications'], + }) const searchApp = (searchText, type) => { - if (type === "choco") { + if (type === 'choco') { ChocosearchResults.mutate({ url: `/api/ListAppsRepository`, data: { search: searchText }, queryKey: `SearchApp-${searchText}-${type}`, - }); + }) } - if (type === "StoreApp") { + if (type === 'StoreApp') { winGetSearchResults.mutate({ url: `/api/ListPotentialApps`, - data: { searchString: searchText, type: "WinGet" }, + data: { searchString: searchText, type: 'WinGet' }, queryKey: `SearchApp-${searchText}-${type}`, - }); + }) } - }; + } const handleSubmit = () => { - const formData = formControl.getValues(); - const formattedData = { ...formData }; - formattedData.tenantFilter = "allTenants"; //added to prevent issues with location check. temp fix + const formData = formControl.getValues() + const formattedData = { ...formData } + formattedData.tenantFilter = 'allTenants' //added to prevent issues with location check. temp fix formattedData.selectedTenants = selectedTenants.map((tenant) => ({ defaultDomainName: tenant.value, customerId: tenant.addedFields.customerId, - })); + })) deployApplication.mutate({ url: postUrl[applicationType?.value], data: formattedData, - relatedQueryKeys: ["Queued Applications"], - }); - }; + relatedQueryKeys: ['Queued Applications'], + }) + } const handleCloseDrawer = () => { - setDrawerVisible(false); - formControl.reset(); - }; + setDrawerVisible(false) + formControl.reset() + } return ( <> @@ -130,7 +130,7 @@ export const CippApplicationDeployDrawer = ({ onClose={handleCloseDrawer} size="xl" footer={ -
    +
    @@ -175,7 +174,7 @@ const Page = () => { )}
    - ); + ) return ( { actions={actions} tableFilter={tableFilter} simpleColumns={[ - "templateName", - "type", - "tenantFilter", - "excludedTenants", - "updatedAt", - "updatedBy", - "runManually", - "standards", + 'templateName', + 'type', + 'tenantFilter', + 'excludedTenants', + 'updatedAt', + 'updatedBy', + 'runManually', + 'standards', ]} queryKey="listStandardTemplates" /> - ); -}; + ) +} Page.getLayout = (page) => ( {page} -); +) -export default Page; +export default Page diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx index 3890212fdbd8..630fdee6f2ce 100644 --- a/src/pages/tenant/standards/templates/template.jsx +++ b/src/pages/tenant/standards/templates/template.jsx @@ -1,205 +1,205 @@ -import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from "@mui/material"; -import { Grid } from "@mui/system"; -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { useForm, useWatch } from "react-hook-form"; -import { useRouter } from "next/router"; -import { Add, SaveRounded } from "@mui/icons-material"; -import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from "react"; -import standards from "../../../../data/standards"; -import CippStandardAccordion from "../../../../components/CippStandards/CippStandardAccordion"; +import { Box, Button, Container, Stack, Typography, SvgIcon, Skeleton } from '@mui/material' +import { Grid } from '@mui/system' +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { useForm, useWatch } from 'react-hook-form' +import { useRouter } from 'next/router' +import { Add, SaveRounded } from '@mui/icons-material' +import { useEffect, useState, useCallback, useMemo, useRef, lazy, Suspense } from 'react' +import standards from '../../../../data/standards' +import CippStandardAccordion from '../../../../components/CippStandards/CippStandardAccordion' // Lazy load the dialog to improve initial page load performance const CippStandardDialog = lazy( - () => import("../../../../components/CippStandards/CippStandardDialog"), -); -import CippStandardsSideBar from "../../../../components/CippStandards/CippStandardsSideBar"; -import { ArrowLeftIcon } from "@mui/x-date-pickers"; -import { useDialog } from "../../../../hooks/use-dialog"; -import { ApiGetCall } from "../../../../api/ApiCall"; -import _ from "lodash"; -import { createDriftManagementActions } from "../../manage/driftManagementActions"; -import { ActionsMenu } from "../../../../components/actions-menu"; -import { useSettings } from "../../../../hooks/use-settings"; -import { CippHead } from "../../../../components/CippComponents/CippHead"; + () => import('../../../../components/CippStandards/CippStandardDialog') +) +import CippStandardsSideBar from '../../../../components/CippStandards/CippStandardsSideBar' +import { ArrowLeftIcon } from '@mui/x-date-pickers' +import { useDialog } from '../../../../hooks/use-dialog' +import { ApiGetCall } from '../../../../api/ApiCall' +import _ from 'lodash' +import { createDriftManagementActions } from '../../manage/driftManagementActions' +import { ActionsMenu } from '../../../../components/actions-menu' +import { useSettings } from '../../../../hooks/use-settings' +import { CippHead } from '../../../../components/CippComponents/CippHead' const Page = () => { - const router = useRouter(); - const [editMode, setEditMode] = useState(false); - const formControl = useForm({ mode: "onBlur" }); - const { formState } = formControl; - const [dialogOpen, setDialogOpen] = useState(false); - const [expanded, setExpanded] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedStandards, setSelectedStandards] = useState({}); - const [updatedAt, setUpdatedAt] = useState(false); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [currentStep, setCurrentStep] = useState(0); - const [hasDriftConflict, setHasDriftConflict] = useState(false); - const initialStandardsRef = useRef({}); - - const currentTenant = useSettings().currentTenant; + const router = useRouter() + const [editMode, setEditMode] = useState(false) + const formControl = useForm({ mode: 'onBlur' }) + const { formState } = formControl + const [dialogOpen, setDialogOpen] = useState(false) + const [expanded, setExpanded] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + const [selectedStandards, setSelectedStandards] = useState({}) + const [updatedAt, setUpdatedAt] = useState(false) + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) + const [currentStep, setCurrentStep] = useState(0) + const [hasDriftConflict, setHasDriftConflict] = useState(false) + const initialStandardsRef = useRef({}) + + const currentTenant = useSettings().currentTenant // Check if this is drift mode - const isDriftMode = router.query.type === "drift"; + const isDriftMode = router.query.type === 'drift' // Set drift mode flag in form when in drift mode useEffect(() => { if (isDriftMode) { - formControl.setValue("isDriftTemplate", true); + formControl.setValue('isDriftTemplate', true) } - }, [isDriftMode, formControl]); + }, [isDriftMode, formControl]) // Watch form values to check valid configuration - const watchForm = useWatch({ control: formControl.control }); + const watchForm = useWatch({ control: formControl.control }) const existingTemplate = ApiGetCall({ url: `/api/listStandardTemplates`, data: { id: router.query.id }, queryKey: `listStandardTemplates-${router.query.id}`, waiting: editMode, - }); + }) // Check if the template configuration is valid and update currentStep useEffect(() => { const stepsStatus = { - step1: !!_.get(watchForm, "templateName"), - step2: _.get(watchForm, "tenantFilter", []).length > 0, + step1: !!_.get(watchForm, 'templateName'), + step2: _.get(watchForm, 'tenantFilter', []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - _.get(watchForm, "standards") && + _.get(watchForm, 'standards') && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { - const standardValues = _.get(watchForm, standardName, {}); + const standardValues = _.get(watchForm, standardName, {}) // Always require an action value which should be an array with at least one element - const actionValue = _.get(standardValues, "action"); - return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); + const actionValue = _.get(standardValues, 'action') + return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0) }), - }; + } - const completedSteps = Object.values(stepsStatus).filter(Boolean).length; - setCurrentStep(completedSteps); - }, [selectedStandards, watchForm, isDriftMode]); + const completedSteps = Object.values(stepsStatus).filter(Boolean).length + setCurrentStep(completedSteps) + }, [selectedStandards, watchForm, isDriftMode]) // Handle route change events const handleRouteChange = useCallback( (url) => { if (hasUnsavedChanges) { const confirmLeave = window.confirm( - "You have unsaved changes. Are you sure you want to leave this page?", - ); + 'You have unsaved changes. Are you sure you want to leave this page?' + ) if (!confirmLeave) { - router.events.emit("routeChangeError"); - throw "Route change was aborted"; + router.events.emit('routeChangeError') + throw 'Route change was aborted' } } }, - [hasUnsavedChanges, router], - ); + [hasUnsavedChanges, router] + ) // Handle browser back/forward navigation or tab close useEffect(() => { const handleBeforeUnload = (e) => { if (hasUnsavedChanges) { - e.preventDefault(); - e.returnValue = "You have unsaved changes. Are you sure you want to leave this page?"; - return e.returnValue; + e.preventDefault() + e.returnValue = 'You have unsaved changes. Are you sure you want to leave this page?' + return e.returnValue } - }; + } // Add event listeners - window.addEventListener("beforeunload", handleBeforeUnload); - router.events.on("routeChangeStart", handleRouteChange); + window.addEventListener('beforeunload', handleBeforeUnload) + router.events.on('routeChangeStart', handleRouteChange) // Remove event listeners on cleanup return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - router.events.off("routeChangeStart", handleRouteChange); - }; - }, [hasUnsavedChanges, handleRouteChange, router.events]); + window.removeEventListener('beforeunload', handleBeforeUnload) + router.events.off('routeChangeStart', handleRouteChange) + } + }, [hasUnsavedChanges, handleRouteChange, router.events]) // Track form changes useEffect(() => { // Compare the current form values with the initial values to check for real changes - const currentValues = formControl.getValues(); - const initialValues = initialStandardsRef.current; + const currentValues = formControl.getValues() + const initialValues = initialStandardsRef.current if ( formState.isDirty || JSON.stringify(selectedStandards) !== JSON.stringify(initialStandardsRef.current) ) { - setHasUnsavedChanges(true); + setHasUnsavedChanges(true) } else { - setHasUnsavedChanges(false); + setHasUnsavedChanges(false) } - }, [formState.isDirty, selectedStandards, formControl]); + }, [formState.isDirty, selectedStandards, formControl]) useEffect(() => { if (router.query.id) { - setEditMode(true); + setEditMode(true) } if (existingTemplate.isSuccess) { //formControl.reset(existingTemplate.data?.[0]); - const apiData = existingTemplate.data?.[0]; + const apiData = existingTemplate.data?.[0] Object.keys(apiData.standards).forEach((key) => { if (Array.isArray(apiData.standards[key])) { apiData.standards[key] = apiData.standards[key].filter( - (value) => value !== null && value !== undefined, - ); + (value) => value !== null && value !== undefined + ) } - }); + }) - formControl.reset(apiData); + formControl.reset(apiData) if (router.query.clone) { - formControl.setValue("templateName", `${apiData.templateName} (Clone)`); - formControl.setValue("GUID", ""); + formControl.setValue('templateName', `${apiData.templateName} (Clone)`) + formControl.setValue('GUID', '') } //set the updated at date and user setUpdatedAt({ date: apiData?.updatedAt, user: apiData?.updatedBy, - }); + }) // Transform standards from the API to match the format for selectedStandards - const standardsFromApi = apiData?.standards; - const transformedStandards = {}; + const standardsFromApi = apiData?.standards + const transformedStandards = {} Object.keys(standardsFromApi).forEach((key) => { if (Array.isArray(standardsFromApi[key])) { standardsFromApi[key].forEach((_, index) => { - transformedStandards[`standards.${key}[${index}]`] = true; - }); + transformedStandards[`standards.${key}[${index}]`] = true + }) } else { - transformedStandards[`standards.${key}`] = true; + transformedStandards[`standards.${key}`] = true } - }); + }) - setSelectedStandards(transformedStandards); + setSelectedStandards(transformedStandards) // Store initial state for change detection - initialStandardsRef.current = { ...transformedStandards }; - setHasUnsavedChanges(false); + initialStandardsRef.current = { ...transformedStandards } + setHasUnsavedChanges(false) } - }, [existingTemplate.isSuccess, router]); + }, [existingTemplate.isSuccess, router]) // Memoize categories to avoid unnecessary recalculations const categories = useMemo(() => { return standards.reduce((acc, standard) => { - const { cat } = standard; + const { cat } = standard if (!acc[cat]) { - acc[cat] = []; + acc[cat] = [] } - acc[cat].push(standard); - return acc; - }, {}); - }, []); + acc[cat].push(standard) + return acc + }, {}) + }, []) const handleOpenDialog = useCallback(() => { - setDialogOpen(true); - }, []); + setDialogOpen(true) + }, []) const handleCloseDialog = useCallback(() => { - setDialogOpen(false); - setSearchQuery(""); - }, []); + setDialogOpen(false) + setSearchQuery('') + }, []) const filterStandards = (standardsList) => standardsList.filter( @@ -211,149 +211,157 @@ const Page = () => { (standard.appliesToTest && standard.appliesToTest.some((testId) => testId.toLowerCase().includes(searchQuery.toLowerCase()) - )), - ); + )) + ) const handleToggleStandard = (standardName) => { setSelectedStandards((prev) => ({ ...prev, [standardName]: !prev[standardName], - })); - }; + })) + } const handleAddMultipleStandard = (standardName) => { //if the standardname contains an array qualifier,e.g standardName[0], strip that away. - const arrayPattern = /(.*)\[(\d+)\]$/; - const match = standardName.match(arrayPattern); + const arrayPattern = /(.*)\[(\d+)\]$/ + const match = standardName.match(arrayPattern) if (match) { - standardName = match[1]; + standardName = match[1] } setSelectedStandards((prev) => { - const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName)); - const newIndex = existingInstances.length; + const existingInstances = Object.keys(prev).filter((name) => name.startsWith(standardName)) + const newIndex = existingInstances.length return { ...prev, [`${standardName}[${newIndex}]`]: true, - }; - }); - }; + } + }) + } const handleRemoveStandard = (standardName) => { - const arrayPattern = /(.*)\[(\d+)\]$/; - const match = standardName.match(arrayPattern); + const arrayPattern = /(.*)\[(\d+)\]$/ + const match = standardName.match(arrayPattern) if (match) { - const baseName = match[1]; - const removedIndex = parseInt(match[2]); + const baseName = match[1] + const removedIndex = parseInt(match[2]) // Remove the item from the form array - const currentArray = formControl.getValues(baseName) || []; - const updatedArray = currentArray.filter((_, i) => i !== removedIndex); - formControl.setValue(baseName, updatedArray); + const currentArray = formControl.getValues(baseName) || [] + const updatedArray = currentArray.filter((_, i) => i !== removedIndex) + formControl.setValue(baseName, updatedArray) // Re-index selectedStandards to keep indices contiguous - const escapedBaseName = baseName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - const reindexPattern = new RegExp(`^${escapedBaseName}\\[(\\d+)\\]$`); + const escapedBaseName = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const reindexPattern = new RegExp(`^${escapedBaseName}\\[(\\d+)\\]$`) setSelectedStandards((prev) => { - const newSelected = {}; + const newSelected = {} Object.keys(prev).forEach((key) => { - const keyMatch = key.match(reindexPattern); + const keyMatch = key.match(reindexPattern) if (keyMatch) { - const idx = parseInt(keyMatch[1]); + const idx = parseInt(keyMatch[1]) if (idx < removedIndex) { - newSelected[key] = prev[key]; + newSelected[key] = prev[key] } else if (idx > removedIndex) { // Shift higher indices down by 1 - newSelected[`${baseName}[${idx - 1}]`] = prev[key]; + newSelected[`${baseName}[${idx - 1}]`] = prev[key] } // Skip the removed index } else { - newSelected[key] = prev[key]; + newSelected[key] = prev[key] } - }); - return newSelected; - }); + }) + return newSelected + }) } else { setSelectedStandards((prev) => { - const newSelected = { ...prev }; - delete newSelected[standardName]; - return newSelected; - }); - formControl.unregister(standardName); + const newSelected = { ...prev } + delete newSelected[standardName] + return newSelected + }) + formControl.unregister(standardName) } - }; + } const handleAccordionToggle = (standardName) => { - setExpanded((prev) => (prev === standardName ? null : standardName)); - }; + setExpanded((prev) => (prev === standardName ? null : standardName)) + } - const createDialog = useDialog(); + const createDialog = useDialog() // Save action that will open the create dialog const handleSave = () => { - createDialog.handleOpen(); + createDialog.handleOpen() // Will be set to false after successful save in the dialog component - }; + } // Determine if save button should be disabled based on configuration const isSaveDisabled = isDriftMode - ? !_.get(watchForm, "tenantFilter") || - !_.get(watchForm, "tenantFilter").length || + ? !_.get(watchForm, 'tenantFilter') || + !_.get(watchForm, 'tenantFilter').length || currentStep < 4 || hasDriftConflict // For drift mode, require all steps and no drift conflicts - : !_.get(watchForm, "tenantFilter") || - !_.get(watchForm, "tenantFilter").length || - currentStep < 4; + : !_.get(watchForm, 'tenantFilter') || + !_.get(watchForm, 'tenantFilter').length || + currentStep < 4 // Create drift management actions (excluding refresh) const driftActions = useMemo(() => { - if (!editMode || !router.query.id) return []; + if (!editMode || !router.query.id) return [] const allActions = createDriftManagementActions({ templateId: router.query.id, - onRefresh: () => {}, // Empty function since we're filtering out refresh + onRefresh: () => {}, currentTenant: currentTenant, - }); + templateTenants: Array.isArray(watchForm?.tenantFilter) ? watchForm.tenantFilter : [], + excludedTenants: Array.isArray(watchForm?.excludedTenants) ? watchForm.excludedTenants : [], + }) // Filter out the refresh action - return allActions.filter((action) => action.label !== "Refresh Data"); - }, [editMode, router.query.id, currentTenant]); + return allActions.filter((action) => action.label !== 'Refresh Data') + }, [ + editMode, + router.query.id, + currentTenant, + watchForm?.tenantFilter, + watchForm?.excludedTenants, + ]) - const actions = []; + const actions = [] const steps = [ - "Set a name for the Template", - "Assigned Template to Tenants", - "Added Standards to Template", - "Configured all Standards", - ]; + 'Set a name for the Template', + 'Assigned Template to Tenants', + 'Added Standards to Template', + 'Configured all Standards', + ] const handleSafeNavigation = (url) => { if (hasUnsavedChanges) { const confirmLeave = window.confirm( - "You have unsaved changes. Are you sure you want to leave this page?", - ); + 'You have unsaved changes. Are you sure you want to leave this page?' + ) if (confirmLeave) { - router.push(url); + router.push(url) } } else { - router.push(url); + router.push(url) } - }; + } return ( - + @@ -368,11 +376,11 @@ const Page = () => { {editMode ? isDriftMode - ? "Edit Drift Template" - : "Edit Standards Template" + ? 'Edit Drift Template' + : 'Edit Standards Template' : isDriftMode - ? "Add Drift Template" - : "Add Standards Template"} + ? 'Add Drift Template' + : 'Add Standards Template'}
    + + + Template Name + Included In + + + + {row.usedInTemplates.map((u, i) => ( + + + + {u.templateName ?? u.templateId} + + + + {u.matchType === 'package' ? ( + + } + /> + + ) : ( + + + + )} + + + ))} + +
    + + )} + + + ), + size: 'lg', + } - const simpleColumns = ["displayName", "isSynced", "package", "description", "Type"]; + const simpleColumns = [ + 'displayName', + 'isSynced', + 'package', + 'description', + 'Type', + 'usedInTemplates', + ] + + const filterList = [ + { + filterName: 'Synced Templates', + value: [{ id: 'isSynced', value: 'Yes' }], + type: 'column', + }, + { + filterName: 'Custom Templates', + value: [{ id: 'isSynced', value: 'No' }], + type: 'column', + }, + ] return ( <> @@ -166,6 +260,7 @@ const Page = () => { actions={actions} offCanvas={offCanvas} simpleColumns={simpleColumns} + filters={filterList} queryKey="ListIntuneTemplates-table" cardButton={ { } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 9680ecaca029..5580ab4b5de0 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -11,6 +11,7 @@ import { BarChart, } from '@mui/icons-material' import { Chip, Link, SvgIcon, Tooltip } from '@mui/material' +import NextLink from 'next/link' import { alpha } from '@mui/material/styles' import { Box } from '@mui/system' import { CippCopyToClipBoard } from '../components/CippComponents/CippCopyToClipboard' @@ -273,7 +274,8 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr } if (cellName === 'currentDeviationsCount') { - if (data === undefined || data === null) return isText ? 'N/A' : + if (data === undefined || data === null) + return isText ? 'N/A' : const count = Number(data) const color = count > 0 ? 'warning' : 'success' const label = count > 0 ? `${count} Deviation${count !== 1 ? 's' : ''}` : 'None' @@ -998,8 +1000,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr // into human-readable form (e.g. "1 hour 23 minutes 30 seconds") across all CIPP tables. // The try/catch below handles same-suffixed fields that are not actually ISO 8601. // Add explicit entries below for fields that don't follow the *Duration naming convention. - const durationArray = [ - ] + const durationArray = [] if (durationArray.includes(cellName) || cellName.endsWith('Duration')) { isoDuration.setLocales( { @@ -1020,6 +1021,17 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr } } + // Internal CIPP navigation links + if ((cellName === 'cippLink') && typeof data === 'string') { + return isText ? ( + data + ) : ( + + View + + ) + } + //if string starts with http, return a link if (typeof data === 'string' && data.toLowerCase().startsWith('http')) { return isText ? ( From 0a70010e2185488a67f91f8e508f67f14f0708df Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 12 May 2026 00:07:31 -0400 Subject: [PATCH 71/86] fix: update translation keys and adjust template usage references --- .../CippComponents/CippTranslations.jsx | 130 +++++++++--------- .../endpoint/MEM/list-templates/index.js | 18 +-- 2 files changed, 72 insertions(+), 76 deletions(-) diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx index d00d3f6d7f38..5975edd0b13a 100644 --- a/src/components/CippComponents/CippTranslations.jsx +++ b/src/components/CippComponents/CippTranslations.jsx @@ -1,66 +1,66 @@ export const CippTranslations = { - userPrincipalName: "User Principal Name", - displayName: "Display Name", - mail: "Mail", - mobilePhone: "Mobile Phone", - officePhone: "Office Phone", - jobTitle: "Job Title", - department: "Department", - surName: "Surname", - city: "City", - tenant: "Tenant", - tenants: "Tenants", - tenantFilter: "Tenant", - showTenantInformation: "Show Tenant Information", - refreshTenantList: "Refresh tenant list", - tenantId: "Tenant ID", - id: "ID", - customerId: "Customer ID", - street: "Street", - technicalNotificationMails: "Technical Notification Mails", - onPremisesSyncEnabled: "On Premises Sync Enabled", - onPremisesLastSyncDateTime: "On Premises Last Sync Date Time", - onPremisesLastPasswordSyncDateTime: "On Premises Last Password Sync Date Time", - postalCode: "Postal Code", - deleteTenant: "Delete Tenant", - ScorePercentage: "Score", - TenantID: "Tenant ID", - ApplicationID: "Application ID", - ApplicationSecret: "Application Secret", - GUID: "GUID", - cippLink: "CIPP Link", - isDrift: "Drift Template", - usedInTemplates: "Used In Templates", - portal_m365: "M365", - portal_exchange: "Exchange", - portal_entra: "Entra", - portal_teams: "Teams", - portal_azure: "Azure", - portal_intune: "Intune", - portal_security: "Security", - portal_compliance: "Compliance", - portal_sharepoint: "SharePoint", - portal_platform: "Power Platform", - portal_bi: "Power BI", - "@odata.type": "Type", - roleDefinitionId: "GDAP Role", - FromIP: "From IP", - ToIP: "To IP", - "info.logoUrl": "Logo", - "commitmentTerm.renewalConfiguration.renewalDate": "Renewal Date", - storageUsedInBytes: "Storage Used", - prohibitSendReceiveQuotaInBytes: "Quota", - ClientId: "Client ID", - html_url: "URL", - sendtoIntegration: "Send Notifications to Integration", - includeTenantId: "Include Tenant ID in Notifications", - logsToInclude: "Logs to Include in notifications", - assignmentFilterManagementType: "Filter Type", - microsoftSupport: "Microsoft Support", - syndicatePartner: "Syndicate Partner", - breadthPartner: "Breadth Partner", - breadthPartnerDelegatedAdmin: "Breadth Partner (Delegated)", - resellerPartnerDelegatedAdmin: "Direct Reseller", - valueAddedResellerPartnerDelegatedAdmin: "Indirect Reseller", - unknownFutureValue: "Unknown", -}; + userPrincipalName: 'User Principal Name', + displayName: 'Display Name', + mail: 'Mail', + mobilePhone: 'Mobile Phone', + officePhone: 'Office Phone', + jobTitle: 'Job Title', + department: 'Department', + surName: 'Surname', + city: 'City', + tenant: 'Tenant', + tenants: 'Tenants', + tenantFilter: 'Tenant', + showTenantInformation: 'Show Tenant Information', + refreshTenantList: 'Refresh tenant list', + tenantId: 'Tenant ID', + id: 'ID', + customerId: 'Customer ID', + street: 'Street', + technicalNotificationMails: 'Technical Notification Mails', + onPremisesSyncEnabled: 'On Premises Sync Enabled', + onPremisesLastSyncDateTime: 'On Premises Last Sync Date Time', + onPremisesLastPasswordSyncDateTime: 'On Premises Last Password Sync Date Time', + postalCode: 'Postal Code', + deleteTenant: 'Delete Tenant', + ScorePercentage: 'Score', + TenantID: 'Tenant ID', + ApplicationID: 'Application ID', + ApplicationSecret: 'Application Secret', + GUID: 'GUID', + cippLink: 'CIPP Link', + isDrift: 'Drift Template', + usage: 'Usage', + portal_m365: 'M365', + portal_exchange: 'Exchange', + portal_entra: 'Entra', + portal_teams: 'Teams', + portal_azure: 'Azure', + portal_intune: 'Intune', + portal_security: 'Security', + portal_compliance: 'Compliance', + portal_sharepoint: 'SharePoint', + portal_platform: 'Power Platform', + portal_bi: 'Power BI', + '@odata.type': 'Type', + roleDefinitionId: 'GDAP Role', + FromIP: 'From IP', + ToIP: 'To IP', + 'info.logoUrl': 'Logo', + 'commitmentTerm.renewalConfiguration.renewalDate': 'Renewal Date', + storageUsedInBytes: 'Storage Used', + prohibitSendReceiveQuotaInBytes: 'Quota', + ClientId: 'Client ID', + html_url: 'URL', + sendtoIntegration: 'Send Notifications to Integration', + includeTenantId: 'Include Tenant ID in Notifications', + logsToInclude: 'Logs to Include in notifications', + assignmentFilterManagementType: 'Filter Type', + microsoftSupport: 'Microsoft Support', + syndicatePartner: 'Syndicate Partner', + breadthPartner: 'Breadth Partner', + breadthPartnerDelegatedAdmin: 'Breadth Partner (Delegated)', + resellerPartnerDelegatedAdmin: 'Direct Reseller', + valueAddedResellerPartnerDelegatedAdmin: 'Indirect Reseller', + unknownFutureValue: 'Unknown', +} diff --git a/src/pages/endpoint/MEM/list-templates/index.js b/src/pages/endpoint/MEM/list-templates/index.js index 567315975d23..c1c81670d8b7 100644 --- a/src/pages/endpoint/MEM/list-templates/index.js +++ b/src/pages/endpoint/MEM/list-templates/index.js @@ -167,7 +167,7 @@ const Page = () => { const offCanvas = { children: (row) => ( - {Array.isArray(row.usedInTemplates) && row.usedInTemplates.length > 0 && ( + {Array.isArray(row.usage) && row.usage.length > 0 && ( { - {row.usedInTemplates.map((u, i) => ( + {row.usage.map((u, i) => ( { /> ) : ( - + )} @@ -229,14 +232,7 @@ const Page = () => { size: 'lg', } - const simpleColumns = [ - 'displayName', - 'isSynced', - 'package', - 'description', - 'Type', - 'usedInTemplates', - ] + const simpleColumns = ['displayName', 'isSynced', 'package', 'description', 'Type', 'usage'] const filterList = [ { From 63c85df03c93f0172b357ccd3be0536f075210da Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 12 May 2026 16:03:50 +0200 Subject: [PATCH 72/86] OneDrive Sharing disable --- .../CippCards/CippRemediationCard.jsx | 1 + .../CippOffboardingDefaultSettings.jsx | 10 +++++++ .../CippComponents/CippUserActions.jsx | 27 +++++++++++++++++++ .../CippWizard/CippWizardOffboarding.jsx | 8 +++++- 4 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/components/CippCards/CippRemediationCard.jsx b/src/components/CippCards/CippRemediationCard.jsx index d864719f80e1..5641ff7e4588 100644 --- a/src/components/CippCards/CippRemediationCard.jsx +++ b/src/components/CippCards/CippRemediationCard.jsx @@ -60,6 +60,7 @@ export default function CippRemediationCard(props) { Disconnect all current sessions Remove all MFA methods for the user Disable all inbox rules for the user + Disable OneDrive sharing { /> ), }, + { + label: "Disable OneDrive Sharing Links", + value: ( + + ), + }, ]} /> diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index a434d7925fb3..d071dc6791ed 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -20,6 +20,7 @@ import { Shortcut, EditAttributes, CloudSync, + Share, } from '@mui/icons-material' import { getCippLicenseTranslation } from '../../utils/get-cipp-license-translation' import { useSettings } from '../../hooks/use-settings.js' @@ -654,6 +655,32 @@ export const useCippUserActions = () => { multiPost: false, condition: () => canWriteUser, }, + { + label: 'Set OneDrive External Sharing', + type: 'POST', + icon: , + url: '/api/ExecSetOneDriveSharing', + data: { UPN: 'userPrincipalName' }, + fields: [ + { + type: 'autoComplete', + name: 'SharingCapability', + label: 'Sharing Level', + multiple: false, + creatable: false, + validators: { required: 'Please select a sharing level' }, + options: [ + { label: 'Disabled - No external sharing allowed', value: 'Disabled' }, + { label: 'External User Sharing Only - Guests must sign in', value: 'ExternalUserSharingOnly' }, + { label: 'External User and Guest Sharing - Anyone links allowed', value: 'ExternalUserAndGuestSharing' }, + { label: 'Existing External User Sharing Only - Existing guests only', value: 'ExistingExternalUserSharingOnly' }, + ], + }, + ], + confirmText: 'Select the sharing level for [userPrincipalName]\'s OneDrive:', + multiPost: false, + condition: () => canWriteUser, + }, { label: 'Add OneDrive Shortcut', type: 'POST', diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index d1a651e5c3a5..18ba59b564df 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -205,6 +205,13 @@ export const CippWizardOffboarding = (props) => { formControl={formControl} disabled={!!deleteUser} /> + { }, }} /> - Email Forwarding From 6c55bc422c0e8d89c66dbd4de14b595babfd188f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 12 May 2026 16:03:54 +0200 Subject: [PATCH 73/86] OneDrive Sharing disable --- .../CippCards/CippRemediationCard.jsx | 28 +- .../CippOffboardingDefaultSettings.jsx | 420 +++++++++--------- .../CippComponents/CippUserActions.jsx | 17 +- .../CippWizard/CippWizardOffboarding.jsx | 3 +- 4 files changed, 239 insertions(+), 229 deletions(-) diff --git a/src/components/CippCards/CippRemediationCard.jsx b/src/components/CippCards/CippRemediationCard.jsx index 5641ff7e4588..4546311a60fa 100644 --- a/src/components/CippCards/CippRemediationCard.jsx +++ b/src/components/CippCards/CippRemediationCard.jsx @@ -1,13 +1,13 @@ -import { Button, Typography, List, ListItem, SvgIcon } from "@mui/material"; -import CippButtonCard from "./CippButtonCard"; // Adjust the import path as needed -import { CippApiDialog } from "../CippComponents/CippApiDialog"; -import { useDialog } from "../../hooks/use-dialog"; -import { Sync } from "@mui/icons-material"; -import { ShieldCheckIcon } from "@heroicons/react/24/outline"; +import { Button, Typography, List, ListItem, SvgIcon } from '@mui/material' +import CippButtonCard from './CippButtonCard' // Adjust the import path as needed +import { CippApiDialog } from '../CippComponents/CippApiDialog' +import { useDialog } from '../../hooks/use-dialog' +import { Sync } from '@mui/icons-material' +import { ShieldCheckIcon } from '@heroicons/react/24/outline' export default function CippRemediationCard(props) { - const { userPrincipalName, isFetching, userId, tenantFilter, restartProcess } = props; - const createDialog = useDialog(); + const { userPrincipalName, isFetching, userId, tenantFilter, restartProcess } = props + const createDialog = useDialog() return ( - ); + ) } diff --git a/src/components/CippComponents/CippOffboardingDefaultSettings.jsx b/src/components/CippComponents/CippOffboardingDefaultSettings.jsx index 2e406a2d9763..5b6189ad2a7c 100644 --- a/src/components/CippComponents/CippOffboardingDefaultSettings.jsx +++ b/src/components/CippComponents/CippOffboardingDefaultSettings.jsx @@ -1,41 +1,41 @@ -import { CippPropertyListCard } from "../../components/CippCards/CippPropertyListCard"; -import CippFormComponent from "../../components/CippComponents/CippFormComponent"; -import { Typography, Box } from "@mui/material"; +import { CippPropertyListCard } from '../../components/CippCards/CippPropertyListCard' +import CippFormComponent from '../../components/CippComponents/CippFormComponent' +import { Typography, Box } from '@mui/material' export const CippOffboardingDefaultSettings = (props) => { - const { formControl, defaultsSource = null, title = "Offboarding Default Settings" } = props; - + const { formControl, defaultsSource = null, title = 'Offboarding Default Settings' } = props + const getSourceIndicator = () => { // Only show the indicator if defaultsSource is explicitly provided (for wizard, not tenant config) - if (!defaultsSource || defaultsSource === null) return null; - - let sourceText = ""; - let color = "text.secondary"; - + if (!defaultsSource || defaultsSource === null) return null + + let sourceText = '' + let color = 'text.secondary' + switch (defaultsSource) { - case "tenant": - sourceText = "Using Tenant Defaults"; - color = "primary.main"; - break; - case "user": - sourceText = "Using User Defaults"; - color = "info.main"; - break; - case "none": + case 'tenant': + sourceText = 'Using Tenant Defaults' + color = 'primary.main' + break + case 'user': + sourceText = 'Using User Defaults' + color = 'info.main' + break + case 'none': default: - sourceText = "Using Default Settings"; - color = "text.secondary"; - break; + sourceText = 'Using Default Settings' + color = 'text.secondary' + break } - + return ( - + {sourceText} - ); - }; + ) + } return ( <> @@ -45,188 +45,188 @@ export const CippOffboardingDefaultSettings = (props) => { showDivider={false} title={title} propertyItems={[ - { - label: "Convert to Shared Mailbox", - value: ( - - ), - }, - { - label: "Remove from all groups", - value: ( - - ), - }, - { - label: "Hide from Global Address List", - value: ( - - ), - }, - { - label: "Remove Licenses", - value: ( - - ), - }, - { - label: "Cancel all calendar invites", - value: ( - - ), - }, - { - label: "Revoke all sessions", - value: ( - - ), - }, - { - label: "Remove users mailbox permissions", - value: ( - - ), - }, - { - label: "Remove users calendar permissions", - value: ( - - ), - }, - { - label: "Remove all Rules", - value: ( - - ), - }, - { - label: "Reset Password", - value: ( - - ), - }, - { - label: "Keep copy of forwarded mail in source mailbox", - value: ( - - ), - }, - { - label: "Delete user", - value: ( - - ), - }, - { - label: "Remove all Mobile Devices", - value: ( - - ), - }, - { - label: "Disable Sign in", - value: ( - - ), - }, - { - label: "Remove all MFA Devices", - value: ( - - ), - }, - { - label: "Remove Teams Phone DID", - value: ( - - ), - }, - { - label: "Clear Immutable ID", - value: ( - - ), - }, - { - label: "Disable OneDrive Sharing Links", - value: ( - - ), - }, - ]} - /> + { + label: 'Convert to Shared Mailbox', + value: ( + + ), + }, + { + label: 'Remove from all groups', + value: ( + + ), + }, + { + label: 'Hide from Global Address List', + value: ( + + ), + }, + { + label: 'Remove Licenses', + value: ( + + ), + }, + { + label: 'Cancel all calendar invites', + value: ( + + ), + }, + { + label: 'Revoke all sessions', + value: ( + + ), + }, + { + label: 'Remove users mailbox permissions', + value: ( + + ), + }, + { + label: 'Remove users calendar permissions', + value: ( + + ), + }, + { + label: 'Remove all Rules', + value: ( + + ), + }, + { + label: 'Reset Password', + value: ( + + ), + }, + { + label: 'Keep copy of forwarded mail in source mailbox', + value: ( + + ), + }, + { + label: 'Delete user', + value: ( + + ), + }, + { + label: 'Remove all Mobile Devices', + value: ( + + ), + }, + { + label: 'Disable Sign in', + value: ( + + ), + }, + { + label: 'Remove all MFA Devices', + value: ( + + ), + }, + { + label: 'Remove Teams Phone DID', + value: ( + + ), + }, + { + label: 'Clear Immutable ID', + value: ( + + ), + }, + { + label: 'Disable OneDrive Sharing Links', + value: ( + + ), + }, + ]} + /> - ); -}; + ) +} diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index d071dc6791ed..c1af17ab13e6 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -671,13 +671,22 @@ export const useCippUserActions = () => { validators: { required: 'Please select a sharing level' }, options: [ { label: 'Disabled - No external sharing allowed', value: 'Disabled' }, - { label: 'External User Sharing Only - Guests must sign in', value: 'ExternalUserSharingOnly' }, - { label: 'External User and Guest Sharing - Anyone links allowed', value: 'ExternalUserAndGuestSharing' }, - { label: 'Existing External User Sharing Only - Existing guests only', value: 'ExistingExternalUserSharingOnly' }, + { + label: 'External User Sharing Only - Guests must sign in', + value: 'ExternalUserSharingOnly', + }, + { + label: 'External User and Guest Sharing - Anyone links allowed', + value: 'ExternalUserAndGuestSharing', + }, + { + label: 'Existing External User Sharing Only - Existing guests only', + value: 'ExistingExternalUserSharingOnly', + }, ], }, ], - confirmText: 'Select the sharing level for [userPrincipalName]\'s OneDrive:', + confirmText: "Select the sharing level for [userPrincipalName]'s OneDrive:", multiPost: false, condition: () => canWriteUser, }, diff --git a/src/components/CippWizard/CippWizardOffboarding.jsx b/src/components/CippWizard/CippWizardOffboarding.jsx index 18ba59b564df..990cb9d35b11 100644 --- a/src/components/CippWizard/CippWizardOffboarding.jsx +++ b/src/components/CippWizard/CippWizardOffboarding.jsx @@ -290,7 +290,8 @@ export const CippWizardOffboarding = (props) => { /> {deleteUser && ( - When a user is deleted, their OneDrive is retained for 30 days by default unless otherwise configured. + When a user is deleted, their OneDrive is retained for 30 days by default unless + otherwise configured. )} Date: Tue, 12 May 2026 16:32:20 +0200 Subject: [PATCH 74/86] Add AlertUserReportPhising --- src/data/alerts.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/data/alerts.json b/src/data/alerts.json index 041719bf5ca9..9bce965852ab 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -602,5 +602,20 @@ "label": "Alert on new Check phishing extension detections", "recommendedRunInterval": "30m", "description": "Monitors for new phishing site detections reported by the Check browser extension. Alerts when a user visits a page that the extension flags as a potential credential phishing or AiTM attack. Requires the Check browser extension to be deployed to users." + }, + { + "name": "UserReportedPhishing", + "label": "Alert on emails reported by users via Outlook Report Phishing", + "recommendedRunInterval": "4h", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "number", + "inputLabel": "Hours to look back (default: 24)", + "inputName": "HoursBack" + } + ], + "description": "Monitors for emails reported by users through Outlook's built-in Report Phishing feature. Alerts when new user-reported email threat submissions are found in Microsoft Defender. Requires ThreatSubmission.ReadWrite.All permission." } ] From d00ebb77ca928c0fe06f5f728a78a459b76dd911 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 12 May 2026 11:32:16 -0400 Subject: [PATCH 75/86] chore: bump version to 10.4.5 --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 76b7d2a5f018..673c2c56b5cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.4.4", + "version": "10.4.5", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 0ac81ba1b0ba..a09d0fcf2ccd 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.4.4" + "version": "10.4.5" } From 19c48eabf7ff5779f683b6edf33ca24d9c76dae0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 13 May 2026 04:19:28 +0800 Subject: [PATCH 76/86] auth options --- .../ForcedSsoMigrationDialog.jsx | 129 ++++++++++ .../CippComponents/SsoMigrationDialog.jsx | 137 +++++++++++ .../CippApiClientManagement.jsx | 61 ++++- .../CippSettings/CippContainerManagement.jsx | 223 ++++++++++++++++++ .../CippSettings/CippSSOSettings.jsx | 202 ++++++++++++++++ .../CippSettings/CippUserManagement.jsx | 221 +++++++++++++++++ src/layouts/TabbedLayout.jsx | 25 +- src/layouts/index.js | 4 + .../cipp/advanced/super-admin/cipp-users.js | 33 +++ .../cipp/advanced/super-admin/container.js | 26 ++ src/pages/cipp/advanced/super-admin/sso.js | 26 ++ .../cipp/advanced/super-admin/tabOptions.json | 12 + 12 files changed, 1088 insertions(+), 11 deletions(-) create mode 100644 src/components/CippComponents/ForcedSsoMigrationDialog.jsx create mode 100644 src/components/CippComponents/SsoMigrationDialog.jsx create mode 100644 src/components/CippSettings/CippContainerManagement.jsx create mode 100644 src/components/CippSettings/CippSSOSettings.jsx create mode 100644 src/components/CippSettings/CippUserManagement.jsx create mode 100644 src/pages/cipp/advanced/super-admin/cipp-users.js create mode 100644 src/pages/cipp/advanced/super-admin/container.js create mode 100644 src/pages/cipp/advanced/super-admin/sso.js diff --git a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx new file mode 100644 index 000000000000..abc303fe1797 --- /dev/null +++ b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx @@ -0,0 +1,129 @@ +import { useCallback, useState } from 'react' +import { + Alert, + Box, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Switch, + Typography, + Button, +} from '@mui/material' +import { ApiGetCall, ApiPostCall } from '../../api/ApiCall' + +export const ForcedSsoMigrationDialog = () => { + const [multiTenant, setMultiTenant] = useState(false) + const [submitted, setSubmitted] = useState(false) + + const currentRole = ApiGetCall({ + url: '/api/me', + queryKey: 'authmecipp', + }) + + const ssoSetup = ApiPostCall({ + relatedQueryKeys: 'authmecipp', + }) + + const permissions = currentRole.data?.permissions || [] + const forceSsoMigration = currentRole.data?.forceSsoMigration + const hasPermission = permissions.includes('CIPP.AppSettings.ReadWrite') + + const open = !!(currentRole.isSuccess && hasPermission && forceSsoMigration?.status === 'pending') + + const result = ssoSetup.data?.data?.Results ?? ssoSetup.data?.Results + const isSuccess = result?.severity === 'success' + const isError = ssoSetup.isError || result?.severity === 'failed' + + const handleMigrate = useCallback(() => { + setSubmitted(true) + ssoSetup.mutate({ + url: '/api/ExecSSOSetup', + data: { + Action: 'Migrate', + multiTenant, + }, + }) + }, [multiTenant, ssoSetup]) + + return ( + e.stopPropagation() } }} + > + Complete Authentication Setup + + {!submitted ? ( + <> + + Your CIPP instance requires a dedicated CIPP-SSO app registration in + your tenant for authentication. This gives you full control over Conditional Access + policies, MFA requirements, and session management for your CIPP users. + + + The app will only require minimal permissions (OpenID, Profile, Email). + + + This step is required before you can use CIPP. + + + setMultiTenant(e.target.checked)} /> + } + label="Multi-tenant mode (allow users from multiple Entra ID tenants to log in)" + sx={{ mb: 1 }} + /> + + ) : isSuccess ? ( + + SSO migration complete. The application will restart to apply the new authentication + configuration. This may take a couple of minutes — you will be prompted to log in again + once the restart is finished. + + ) : ssoSetup.isPending ? ( + + + Creating CIPP-SSO app and configuring authentication... + + ) : isError ? ( + <> + + {result?.message || + ssoSetup.error?.message || + 'SSO migration failed. Please try again.'} + + + If this error persists, contact your CIPP administrator. + + + ) : null} + + + {!submitted ? ( + + ) : isSuccess ? ( + + ) : isError ? ( + + ) : null} + + + ) +} diff --git a/src/components/CippComponents/SsoMigrationDialog.jsx b/src/components/CippComponents/SsoMigrationDialog.jsx new file mode 100644 index 000000000000..fe184c40cd72 --- /dev/null +++ b/src/components/CippComponents/SsoMigrationDialog.jsx @@ -0,0 +1,137 @@ +import { useCallback, useEffect, useState } from 'react' +import { + Alert, + Button, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Switch, + Typography, +} from '@mui/material' +import { ApiGetCall, ApiPostCall } from '../../api/ApiCall' + +const DISMISS_KEY = 'cipp_sso_migration_dismissed' + +export const SsoMigrationDialog = () => { + const [open, setOpen] = useState(false) + const [multiTenant, setMultiTenant] = useState(false) + const [submitted, setSubmitted] = useState(false) + + const currentRole = ApiGetCall({ + url: '/api/me', + queryKey: 'authmecipp', + }) + + const ssoSetup = ApiPostCall({ + relatedQueryKeys: 'authmecipp', + }) + + const permissions = currentRole.data?.permissions || [] + const ssoMigration = currentRole.data?.ssoMigration + const hasPermission = permissions.includes('CIPP.AppSettings.ReadWrite') + + useEffect(() => { + if (!currentRole.isSuccess || !hasPermission || !ssoMigration) return + if (ssoMigration.status !== 'none') return + + const dismissed = localStorage.getItem(DISMISS_KEY) + if (dismissed === 'true') return + + setOpen(true) + }, [currentRole.isSuccess, hasPermission, ssoMigration]) + + const handleApprove = useCallback(() => { + setSubmitted(true) + ssoSetup.mutate({ + url: '/api/ExecSSOSetup', + data: { + Action: 'Create', + multiTenant, + }, + }) + }, [multiTenant, ssoSetup]) + + const handleDismiss = useCallback(() => { + localStorage.setItem(DISMISS_KEY, 'true') + setOpen(false) + }, []) + + const handleClose = useCallback(() => { + setOpen(false) + }, []) + + const result = ssoSetup.data?.data?.Results ?? ssoSetup.data?.Results + const isSuccess = result?.severity === 'success' + const isError = ssoSetup.isError || result?.severity === 'failed' + + return ( + + Prepare for CIPP Single Sign-On + + {!submitted ? ( + <> + + CIPP will soon be moving to a dedicated Single Sign-On model, giving you full control + over Conditional Access policies, MFA requirements, and session management for your + CIPP users. + + + To get ready, CIPP needs to create an app registration in your tenant called{' '} + CIPP-SSO with minimal permissions (OpenID, Profile, Email only). + This won't change how you log in today — it just prepares your tenant for when + the update rolls out. + + + Review the options below and click "Create App Registration" to get set up + ahead of time. + + + setMultiTenant(e.target.checked)} /> + } + label="Multi-tenant mode (allow users from multiple Entra ID tenants to log in)" + sx={{ mb: 1 }} + /> + + ) : ssoSetup.isPending ? ( + <> + + Creating CIPP-SSO app registration... + + ) : isSuccess ? ( + + {result.message} + + ) : isError ? ( + + {result?.message || ssoSetup.error?.message || 'SSO setup failed. It will be retried automatically.'} + + ) : null} + + + {!submitted ? ( + <> + + + + ) : ( + + )} + + + ) +} diff --git a/src/components/CippIntegrations/CippApiClientManagement.jsx b/src/components/CippIntegrations/CippApiClientManagement.jsx index 3dab6bf2bf1b..a9a2d2960ef1 100644 --- a/src/components/CippIntegrations/CippApiClientManagement.jsx +++ b/src/components/CippIntegrations/CippApiClientManagement.jsx @@ -1,6 +1,7 @@ import { Button, Stack, SvgIcon, Menu, MenuItem, ListItemText, Alert } from "@mui/material"; -import { useState } from "react"; +import { useState, useEffect, useMemo } from "react"; import isEqual from "lodash/isEqual"; +import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { ApiGetCall, ApiGetCallWithPagination, ApiPostCall } from "../../api/ApiCall"; import { CippDataTable } from "../CippTable/CippDataTable"; @@ -19,6 +20,7 @@ import { CippCopyToClipBoard } from "../CippComponents/CippCopyToClipboard"; import { Box } from "@mui/system"; const CippApiClientManagement = () => { + const router = useRouter(); const [openAddClientDialog, setOpenAddClientDialog] = useState(false); const [openAddExistingAppDialog, setOpenAddExistingAppDialog] = useState(false); const [addClientRetryPayload, setAddClientRetryPayload] = useState(null); @@ -45,6 +47,46 @@ const CippApiClientManagement = () => { queryKey: "ApiClients", }); + const hasUnsavedChanges = useMemo(() => { + if (!azureConfig.isSuccess || !apiClients.isSuccess) return false; + return !isEqual( + (apiClients.data?.pages?.[0]?.Results || []) + .filter((c) => c.Enabled) + .map((c) => c.ClientId) + .sort(), + (azureConfig.data?.Results?.ClientIDs || []).sort() + ); + }, [azureConfig.isSuccess, azureConfig.data, apiClients.isSuccess, apiClients.data]); + + useEffect(() => { + const handleBeforeUnload = (e) => { + if (hasUnsavedChanges) { + e.preventDefault(); + e.returnValue = ""; + } + }; + + const handleRouteChange = (url) => { + if ( + hasUnsavedChanges && + !window.confirm( + "You have unsaved API client changes. Are you sure you want to leave this page?" + ) + ) { + router.events.emit("routeChangeError"); + throw "Route change aborted due to unsaved changes."; + } + }; + + window.addEventListener("beforeunload", handleBeforeUnload); + router.events.on("routeChangeStart", handleRouteChange); + + return () => { + window.removeEventListener("beforeunload", handleBeforeUnload); + router.events.off("routeChangeStart", handleRouteChange); + }; + }, [hasUnsavedChanges, router.events]); + const handleMenuOpen = (event) => { setMenuAnchorEl(event.currentTarget); }; @@ -54,11 +96,18 @@ const CippApiClientManagement = () => { }; const handleSaveToAzure = () => { + handleMenuClose(); + if ( + !window.confirm( + "Saving to Azure will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?" + ) + ) { + return; + } postCall.mutate({ url: `/api/ExecApiClient?action=SaveToAzure`, data: {}, }); - handleMenuClose(); }; const getRetryPayload = (result) => { @@ -284,13 +333,7 @@ const CippApiClientManagement = () => { /> {azureConfig.isSuccess && apiClients.isSuccess && ( <> - {!isEqual( - (apiClients.data?.pages?.[0]?.Results || []) - .filter((c) => c.Enabled) - .map((c) => c.ClientId) - .sort(), - (azureConfig.data?.Results?.ClientIDs || []).sort() - ) && ( + {hasUnsavedChanges && ( You have unsaved changes. Click Actions > Save Azure Configuration to update diff --git a/src/components/CippSettings/CippContainerManagement.jsx b/src/components/CippSettings/CippContainerManagement.jsx new file mode 100644 index 000000000000..526051233c70 --- /dev/null +++ b/src/components/CippSettings/CippContainerManagement.jsx @@ -0,0 +1,223 @@ +import { useEffect } from "react"; +import { + Alert, + Button, + CardActions, + CardContent, + Chip, + Divider, + Skeleton, + Stack, + Typography, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; + +const channelLabels = { + latest: { label: "Latest (Stable)", color: "success" }, + dev: { label: "Dev", color: "warning" }, + nightly: { label: "Nightly", color: "info" }, + unknown: { label: "Unknown", color: "default" }, +}; + +export const CippContainerManagement = () => { + const formControl = useForm({ + mode: "onChange", + defaultValues: { Channel: null }, + }); + + const containerStatus = ApiGetCall({ + url: "/api/ExecContainerManagement", + data: { Action: "Status" }, + queryKey: "containerStatus", + }); + + const containerAction = ApiPostCall({ + relatedQueryKeys: ["containerStatus"], + }); + + const data = containerStatus.data?.Results; + const channelInfo = channelLabels[data?.CurrentChannel] ?? channelLabels.unknown; + + const channelOptions = (data?.ValidChannels ?? ["latest", "dev", "nightly"]).map((c) => ({ + label: channelLabels[c]?.label ?? c, + value: c, + })); + + useEffect(() => { + if (containerStatus.isSuccess && data?.CurrentChannel) { + const current = channelOptions.find((o) => o.value === data.CurrentChannel); + if (current) { + formControl.reset({ Channel: current }); + } + } + }, [containerStatus.isSuccess, data?.CurrentChannel]); + + const handleUpdateChannel = () => { + const selected = formControl.getValues("Channel"); + const channel = selected?.value ?? selected; + containerAction.mutate({ + url: "/api/ExecContainerManagement", + data: { Action: "UpdateChannel", Channel: channel }, + }); + }; + + const handleRestart = () => { + containerAction.mutate({ + url: "/api/ExecContainerManagement", + data: { Action: "Restart" }, + }); + }; + + return ( + + + + {containerStatus.isLoading ? ( + + + + + ) : ( + + {data?.ConfiguredChannel && data.ConfiguredChannel !== data.CurrentChannel && ( + + A channel change is pending. Running: {data.CurrentChannel}, + configured: {data.ConfiguredChannel}. Restart the container to + apply. + + )} + + + + Running Channel + + + + + + + + + Image Tag + + + + + {data?.ImageTag ?? "unknown"} + + + + + + App Version + + + + + {data?.CurrentVersion ?? "unknown"} + + + + + + Commit SHA + + + + + {data?.CommitSha ?? "unknown"} + + + + {data?.CurrentImage && data.CurrentImage !== "unknown" && ( + <> + + + Container Image + + + + + {data.CurrentImage} + + + + )} + + {data?.SiteName && ( + <> + + + App Service + + + + {data.SiteName} + + + )} + + + )} + + + + + + + + Changing the release channel updates the container image tag. The new image will be + pulled on the next container restart. Switching to "Dev" or + "Nightly" may include unstable or untested changes. + + + + + + + + + + + + + + Restart the application container. This will cause a brief downtime while the container + restarts. If you changed the release channel, this will pull the new image. + + + + + + + + ); +}; + +export default CippContainerManagement; diff --git a/src/components/CippSettings/CippSSOSettings.jsx b/src/components/CippSettings/CippSSOSettings.jsx new file mode 100644 index 000000000000..e22cd1ab6edf --- /dev/null +++ b/src/components/CippSettings/CippSSOSettings.jsx @@ -0,0 +1,202 @@ +import { useEffect, useState } from "react"; +import { + Alert, + Button, + CardActions, + CardContent, + CardHeader, + Chip, + Divider, + Skeleton, + Stack, + Typography, +} from "@mui/material"; +import { useForm } from "react-hook-form"; +import { Grid } from "@mui/system"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import CippButtonCard from "../CippCards/CippButtonCard"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; +import { CippApiResults } from "../CippComponents/CippApiResults"; + +const statusLabels = { + none: { label: "Not Configured", color: "default" }, + app_created: { label: "App Created", color: "info" }, + appid_stored: { label: "App ID Stored", color: "info" }, + secrets_stored: { label: "Secrets Stored", color: "success" }, + complete: { label: "Complete", color: "success" }, + error: { label: "Error", color: "error" }, +}; + +export const CippSSOSettings = () => { + const [showCreate, setShowCreate] = useState(false); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { multiTenant: false }, + }); + + const ssoStatus = ApiGetCall({ + url: "/api/ExecSSOSetup", + data: { Action: "Status" }, + queryKey: "SSOStatus", + }); + + const ssoAction = ApiPostCall({ + relatedQueryKeys: ["SSOStatus", "authmecipp"], + }); + + useEffect(() => { + if (ssoStatus.isSuccess && ssoStatus.data?.Results) { + const data = ssoStatus.data.Results; + formControl.reset({ multiTenant: data.multiTenant ?? false }); + setShowCreate(!data.configured); + } + }, [ssoStatus.isSuccess, ssoStatus.data]); + + const handleUpdate = () => { + if ( + !window.confirm( + "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?" + ) + ) { + return; + } + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { + Action: "Update", + multiTenant: formControl.getValues("multiTenant"), + }, + }); + }; + + const handleCreate = () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { + Action: "Create", + multiTenant: formControl.getValues("multiTenant"), + }, + }); + }; + + const handleRotateSecret = () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { Action: "RotateSecret" }, + }); + }; + + const data = ssoStatus.data?.Results; + const statusInfo = statusLabels[data?.status] ?? statusLabels.none; + + return ( + + + {ssoStatus.isLoading ? ( + + + + + ) : ( + + + + + Status + + + + + + + {data?.appId && ( + <> + + + App ID + + + + + {data.appId} + + + + )} + + {data?.createdAt && ( + <> + + + Created + + + + + {new Date(data.createdAt).toLocaleString()} + + + + )} + + {data?.lastError && ( + <> + + + {data.lastError} + + + + )} + + + + + + + + + )} + + {!ssoStatus.isLoading && ( + + + {showCreate ? ( + + ) : ( + <> + + + + )} + + + )} + + ); +}; diff --git a/src/components/CippSettings/CippUserManagement.jsx b/src/components/CippSettings/CippUserManagement.jsx new file mode 100644 index 000000000000..ab4be74c1b2b --- /dev/null +++ b/src/components/CippSettings/CippUserManagement.jsx @@ -0,0 +1,221 @@ +import React, { useState } from "react"; +import { + Alert, + Box, + Button, + CardActions, + CardContent, + Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + Stack, + SvgIcon, + Typography, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import { useForm } from "react-hook-form"; +import { TrashIcon, PlusIcon, PencilIcon } from "@heroicons/react/24/outline"; +import { CippDataTable } from "../CippTable/CippDataTable"; +import CippFormComponent from "../CippComponents/CippFormComponent"; +import { CippApiResults } from "../CippComponents/CippApiResults"; +import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; + +export const CippUserManagement = () => { + const [dialogOpen, setDialogOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { UPN: "", Roles: [] }, + }); + + const rolesQuery = ApiGetCall({ + url: "/api/ListCustomRole", + queryKey: "customRoleList", + }); + + const userAction = ApiPostCall({ + relatedQueryKeys: ["cippUsersList"], + }); + + const allRoles = Array.isArray(rolesQuery.data) ? rolesQuery.data : []; + const roleOptions = allRoles.map((r) => ({ + label: `${r.RoleName} (${r.Type})`, + value: r.RoleName, + })); + + const openAddDialog = () => { + setEditingUser(null); + formControl.reset({ UPN: "", Roles: [] }); + setDialogOpen(true); + }; + + const openEditDialog = (row) => { + setEditingUser(row); + const currentRoles = (row.Roles ?? []).map((r) => { + const match = roleOptions.find((opt) => opt.value === r); + return match ?? { label: r, value: r }; + }); + formControl.reset({ UPN: row.UPN, Roles: currentRoles }); + setDialogOpen(true); + }; + + const handleSaveUser = (data) => { + const roles = Array.isArray(data.Roles) ? data.Roles.map((r) => r.value ?? r) : [data.Roles]; + userAction.mutate( + { + url: "/api/ExecCIPPUsers", + data: { + Action: "AddUpdate", + UPN: data.UPN, + Roles: roles, + }, + }, + { + onSuccess: () => { + formControl.reset({ UPN: "", Roles: [] }); + setEditingUser(null); + setDialogOpen(false); + }, + } + ); + }; + + const actions = [ + { + label: "Edit Roles", + icon: ( + + + + ), + noConfirm: true, + customFunction: (row) => openEditDialog(row), + }, + { + label: "Delete User", + icon: ( + + + + ), + confirmText: "Are you sure you want to remove this user's access to CIPP?", + url: "/api/ExecCIPPUsers", + type: "POST", + data: { + Action: "Delete", + UPN: "UPN", + }, + relatedQueryKeys: ["cippUsersList"], + }, + ]; + + const offCanvas = { + children: (row) => ( + + + + Email / UPN + + {row.UPN} + + + + + Assigned Roles + + + {(row.Roles ?? []).map((role, idx) => ( + + ))} + + + + ), + }; + + return ( + + + + + } + onClick={openAddDialog} + > + Add User + + } + api={{ + url: "/api/ListCIPPUsers", + dataKey: "Users", + }} + queryKey="cippUsersList" + simpleColumns={["UPN", "Roles"]} + offCanvas={offCanvas} + /> + + + + setDialogOpen(false)} + maxWidth="sm" + fullWidth + > + {editingUser ? `Edit Roles — ${editingUser.UPN}` : "Add CIPP User"} + + + + {editingUser + ? "Update the roles assigned to this user." + : "Add a user by their email address (UPN) and assign one or more roles. If the user already exists, their roles will be updated."} + + {!editingUser && ( + + )} + + + + + + + + + + ); +}; + +export default CippUserManagement; diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index a085b528b45c..031f363c4dac 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -1,6 +1,8 @@ +import { useMemo } from 'react' import { usePathname, useRouter } from 'next/navigation' import { Box, Divider, Stack, Tab, Tabs } from '@mui/material' import { useSearchParams } from 'next/navigation' +import { ApiGetCall } from '../api/ApiCall' export const TabbedLayout = (props) => { const { tabOptions, children } = props @@ -8,6 +10,25 @@ export const TabbedLayout = (props) => { const pathname = usePathname() const searchParams = useSearchParams() + const featureFlags = ApiGetCall({ + url: '/api/ListFeatureFlags', + queryKey: 'featureFlags', + staleTime: 600000, + }) + + const visibleTabs = useMemo(() => { + if (!featureFlags.isSuccess || !Array.isArray(featureFlags.data)) return tabOptions + + const disabledPages = featureFlags.data + .filter((flag) => flag.Enabled === false || flag.enabled === false) + .flatMap((flag) => flag.Pages || flag.pages || []) + .filter((page) => typeof page === 'string') + + if (disabledPages.length === 0) return tabOptions + + return tabOptions.filter((option) => !disabledPages.includes(option.path)) + }, [tabOptions, featureFlags.isSuccess, featureFlags.data]) + const handleTabsChange = (event, value) => { // Preserve existing query parameters when changing tabs const currentParams = new URLSearchParams(searchParams.toString()) @@ -16,7 +37,7 @@ export const TabbedLayout = (props) => { router.push(newPath) } - const currentTab = tabOptions.find((option) => option.path === pathname) + const currentTab = visibleTabs.find((option) => option.path === pathname) return ( { }, }} > - {tabOptions.map((option) => ( + {visibleTabs.map((option) => ( ))} diff --git a/src/layouts/index.js b/src/layouts/index.js index b741d5bd0ea4..d00dc338a82d 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -27,6 +27,8 @@ import { CippImageCard } from '../components/CippCards/CippImageCard' import { useDialog } from '../hooks/use-dialog' import { nativeMenuItems } from './config' import { CippBreadcrumbNav } from '../components/CippComponents/CippBreadcrumbNav' +import { SsoMigrationDialog } from '../components/CippComponents/SsoMigrationDialog' +import { ForcedSsoMigrationDialog } from '../components/CippComponents/ForcedSsoMigrationDialog' const OnboardingWizardPage = dynamic( () => import('../components/CippWizard/OnboardingWizardPage.jsx'), @@ -335,6 +337,8 @@ export const Layout = (props) => { + + {!setupCompleted && ( diff --git a/src/pages/cipp/advanced/super-admin/cipp-users.js b/src/pages/cipp/advanced/super-admin/cipp-users.js new file mode 100644 index 000000000000..8fe35569ef16 --- /dev/null +++ b/src/pages/cipp/advanced/super-admin/cipp-users.js @@ -0,0 +1,33 @@ +import { TabbedLayout } from "../../../../layouts/TabbedLayout"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import tabOptions from "./tabOptions"; +import CippPageCard from "../../../../components/CippCards/CippPageCard"; +import { CippUserManagement } from "../../../../components/CippSettings/CippUserManagement"; +import { CardContent, Stack, Alert } from "@mui/material"; + +const Page = () => { + return ( + + + + + Manage users who can access CIPP. Add users by their email address (UPN) and assign + them built-in or custom roles. Users not in this list will still be able to log in if + "Allow All Tenant Users" is enabled, but they will only receive default + (authenticated) permissions. Role resolution also considers Entra group mappings + configured on the CIPP Roles page. + + + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/advanced/super-admin/container.js b/src/pages/cipp/advanced/super-admin/container.js new file mode 100644 index 000000000000..d56595ac546c --- /dev/null +++ b/src/pages/cipp/advanced/super-admin/container.js @@ -0,0 +1,26 @@ +import { Container } from "@mui/material"; +import { Grid } from "@mui/system"; +import { TabbedLayout } from "../../../../layouts/TabbedLayout"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import tabOptions from "./tabOptions"; +import { CippContainerManagement } from "../../../../components/CippSettings/CippContainerManagement"; + +const Page = () => { + return ( + + + + + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/advanced/super-admin/sso.js b/src/pages/cipp/advanced/super-admin/sso.js new file mode 100644 index 000000000000..fc5b112f3f1c --- /dev/null +++ b/src/pages/cipp/advanced/super-admin/sso.js @@ -0,0 +1,26 @@ +import { Container } from "@mui/material"; +import { Grid } from "@mui/system"; +import { TabbedLayout } from "../../../../layouts/TabbedLayout"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import tabOptions from "./tabOptions"; +import { CippSSOSettings } from "../../../../components/CippSettings/CippSSOSettings"; + +const Page = () => { + return ( + + + + + + + + ); +}; + +Page.getLayout = (page) => ( + + {page} + +); + +export default Page; diff --git a/src/pages/cipp/advanced/super-admin/tabOptions.json b/src/pages/cipp/advanced/super-admin/tabOptions.json index 672df76996c6..fbccb6b73c55 100644 --- a/src/pages/cipp/advanced/super-admin/tabOptions.json +++ b/src/pages/cipp/advanced/super-admin/tabOptions.json @@ -22,5 +22,17 @@ { "label": "SAM App Permissions", "path": "/cipp/advanced/super-admin/sam-app-permissions" + }, + { + "label": "CIPP Users", + "path": "/cipp/advanced/super-admin/cipp-users" + }, + { + "label": "SSO", + "path": "/cipp/advanced/super-admin/sso" + }, + { + "label": "Container Management", + "path": "/cipp/advanced/super-admin/container" } ] From 36071e5403afe1bf5bc8426e49db987ce23a0bb8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 13 May 2026 14:20:34 +0800 Subject: [PATCH 77/86] Module updates and import changes --- next.config.js | 1 + package.json | 20 +- .../CippCards/CippStandardsDialog.jsx | 10 +- .../CippAppPermissionBuilder.jsx | 6 +- .../CippSettings/CippContainerManagement.jsx | 247 ++++++- .../CippStandards/CippStandardAccordion.jsx | 70 +- .../CippStandards/CippStandardsSideBar.jsx | 22 +- src/components/CippTable/CippDataTable.js | 3 +- .../cipp/advanced/super-admin/container.js | 7 +- src/pages/cipp/settings/features.js | 1 + src/pages/tenant/standards/bpa-report/view.js | 6 +- .../tenant/standards/templates/template.jsx | 20 +- yarn.lock | 626 +----------------- 13 files changed, 343 insertions(+), 696 deletions(-) diff --git a/next.config.js b/next.config.js index 97685f34f91e..f2bc28bcd2bb 100644 --- a/next.config.js +++ b/next.config.js @@ -16,6 +16,7 @@ const config = { 'mui-tiptap', 'recharts', '@react-pdf/renderer', + 'lodash', ], webpackMemoryOptimizations: true, preloadEntriesOnStart: false, diff --git a/package.json b/package.json index 9a98ddfc4b3a..a4402ea681b5 100644 --- a/package.json +++ b/package.json @@ -48,28 +48,26 @@ "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^3.4.1", "@tiptap/extension-heading": "^3.4.1", - "@tiptap/extension-image": "^3.20.5", "@tiptap/extension-table": "^3.19.0", "@tiptap/pm": "^3.22.3", "@tiptap/react": "^3.20.5", "@tiptap/starter-kit": "^3.20.5", - "@uiw/react-json-view": "^2.0.0-alpha.42", "@vvo/tzdb": "^6.198.0", "apexcharts": "5.10.4", "axios": "1.15.0", "date-fns": "4.1.0", "diff": "^8.0.3", + "dompurify": "^3.4.2", "eml-parse-js": "^1.2.0-beta.0", "export-to-csv": "^1.3.0", "formik": "2.4.9", "gray-matter": "4.0.3", - "i18next": "25.8.18", "javascript-time-ago": "^2.6.2", "jspdf": "^4.2.0", "jspdf-autotable": "^5.0.7", "leaflet": "^1.9.4", - "leaflet-defaulticon-compatibility": "^0.1.2", "leaflet.markercluster": "^1.5.3", + "lodash": "^4.18.1", "lodash.isequal": "4.5.0", "material-react-table": "^3.0.1", "monaco-editor": "^0.55.1", @@ -82,15 +80,12 @@ "react": "19.2.5", "react-apexcharts": "2.1.0", "react-beautiful-dnd": "13.1.1", - "react-copy-to-clipboard": "^5.1.0", "react-dom": "19.2.5", "react-dropzone": "15.0.0", "react-error-boundary": "^6.1.1", - "react-grid-layout": "^2.2.3", "react-hook-form": "^7.72.0", "react-hot-toast": "2.6.0", "react-html-parser": "^2.0.2", - "react-i18next": "16.6.5", "react-leaflet": "5.0.0", "react-leaflet-markercluster": "^5.0.0-rc.0", "react-markdown": "10.1.0", @@ -101,28 +96,23 @@ "react-syntax-highlighter": "^16.1.0", "react-time-ago": "^7.3.3", "react-virtuoso": "^4.18.5", - "react-window": "^2.2.7", "recharts": "^3.8.1", "redux": "5.0.1", - "redux-devtools-extension": "2.13.9", "redux-persist": "^6.0.0", - "redux-thunk": "3.1.0", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.0", + "remark-parse": "^11.0.0", "simplebar": "6.3.3", "simplebar-react": "3.3.2", "stylis-plugin-rtl": "2.1.1", - "typescript": "5.9.3", + "unified": "^11.0.5", "yup": "1.7.1" }, "devDependencies": { "@svgr/webpack": "8.1.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", "eslint": "^9.39.4", "eslint-config-next": "^16.2.3", "eslint-config-prettier": "^10.1.8", - "prettier": "^3.8.1", - "prettier-eslint": "^16.4.2" + "prettier": "^3.8.1" } } diff --git a/src/components/CippCards/CippStandardsDialog.jsx b/src/components/CippCards/CippStandardsDialog.jsx index 86de00f07d92..0e006ef43615 100644 --- a/src/components/CippCards/CippStandardsDialog.jsx +++ b/src/components/CippCards/CippStandardsDialog.jsx @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import _ from 'lodash' +import { get } from 'lodash' import { Dialog, DialogTitle, @@ -311,7 +311,7 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan {info.addedComponent.map((component, componentIndex) => { - const value = _.get(templateItem, component.name) + const value = get(templateItem, component.name) let displayValue = 'N/A' if (value) { @@ -427,7 +427,7 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan let extractedValue = null // Try direct access first - componentValue = _.get(config, component.name) + componentValue = get(config, component.name) // If direct access fails and component name contains dots (nested structure) if ( @@ -441,7 +441,7 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan if (pathParts[0] === 'standards' && config.standards) { // Remove 'standards.' prefix and try to find the value in config.standards const nestedPath = pathParts.slice(1).join('.') - extractedValue = _.get(config.standards, nestedPath) + extractedValue = get(config.standards, nestedPath) // If still not found, try alternative nested structures // Some standards have double nesting like: config.standards.StandardName.fieldName @@ -452,7 +452,7 @@ export const CippStandardsDialog = ({ open, onClose, standardsData, currentTenan ) { const standardName = pathParts[1] const fieldPath = pathParts.slice(2).join('.') - extractedValue = _.get( + extractedValue = get( config.standards, `${standardName}.${fieldPath}` ) diff --git a/src/components/CippComponents/CippAppPermissionBuilder.jsx b/src/components/CippComponents/CippAppPermissionBuilder.jsx index da386b770f91..07d21613fb84 100644 --- a/src/components/CippComponents/CippAppPermissionBuilder.jsx +++ b/src/components/CippComponents/CippAppPermissionBuilder.jsx @@ -37,7 +37,7 @@ import { import { useWatch } from "react-hook-form"; import { CippCardTabPanel } from "./CippCardTabPanel"; import { CippApiResults } from "./CippApiResults"; -import _ from "lodash"; +import { isEqual } from "lodash"; import { CippCodeBlock } from "./CippCodeBlock"; import { CippOffCanvas } from "./CippOffCanvas"; import { FileDropzone } from "../file-dropzone"; @@ -388,7 +388,7 @@ const CippAppPermissionBuilder = ({ }); setExpanded("00000003-0000-0000-c000-000000000000"); // Automatically expand Microsoft Graph } - } else if (!_.isEqual(currentPermissions, initialPermissions)) { + } else if (!isEqual(currentPermissions, initialPermissions)) { setSelectedApp([]); // Avoid redundant updates setNewPermissions(currentPermissions); setInitialPermissions(currentPermissions); @@ -398,7 +398,7 @@ const CippAppPermissionBuilder = ({ initialAppIds.includes(sp.appId), )?.sort((a, b) => a.displayName.localeCompare(b.displayName)); - if (!_.isEqual(selectedApp, newApps)) { + if (!isEqual(selectedApp, newApps)) { setSelectedApp(newApps); // Prevent unnecessary updates } diff --git a/src/components/CippSettings/CippContainerManagement.jsx b/src/components/CippSettings/CippContainerManagement.jsx index 526051233c70..37daf6aa3e45 100644 --- a/src/components/CippSettings/CippContainerManagement.jsx +++ b/src/components/CippSettings/CippContainerManagement.jsx @@ -24,24 +24,55 @@ const channelLabels = { unknown: { label: "Unknown", color: "default" }, }; +const intervalOptions = [ + { label: "Disabled", value: "0" }, + { label: "Every hour", value: "1h" }, + { label: "Every 4 hours", value: "4h" }, + { label: "Every 12 hours", value: "12h" }, + { label: "Every day", value: "1d" }, +]; + +const hourOptions = Array.from({ length: 24 }, (_, i) => ({ + label: `${i.toString().padStart(2, "0")}:00`, + value: String(i), +})); + export const CippContainerManagement = () => { - const formControl = useForm({ + const channelForm = useForm({ mode: "onChange", defaultValues: { Channel: null }, }); + const updateSettingsForm = useForm({ + mode: "onChange", + defaultValues: { CheckInterval: null, AutoUpdate: false, CheckTime: null }, + }); + const containerStatus = ApiGetCall({ url: "/api/ExecContainerManagement", data: { Action: "Status" }, queryKey: "containerStatus", }); - const containerAction = ApiPostCall({ + const channelAction = ApiPostCall({ + relatedQueryKeys: ["containerStatus"], + }); + + const restartAction = ApiPostCall({ + relatedQueryKeys: ["containerStatus"], + }); + + const updateCheckAction = ApiPostCall({ + relatedQueryKeys: ["containerStatus"], + }); + + const updateSettingsAction = ApiPostCall({ relatedQueryKeys: ["containerStatus"], }); const data = containerStatus.data?.Results; const channelInfo = channelLabels[data?.CurrentChannel] ?? channelLabels.unknown; + const updateSettings = data?.UpdateSettings; const channelOptions = (data?.ValidChannels ?? ["latest", "dev", "nightly"]).map((c) => ({ label: channelLabels[c]?.label ?? c, @@ -52,29 +83,75 @@ export const CippContainerManagement = () => { if (containerStatus.isSuccess && data?.CurrentChannel) { const current = channelOptions.find((o) => o.value === data.CurrentChannel); if (current) { - formControl.reset({ Channel: current }); + channelForm.reset({ Channel: current }); } } }, [containerStatus.isSuccess, data?.CurrentChannel]); + useEffect(() => { + if (containerStatus.isSuccess && updateSettings) { + const interval = intervalOptions.find((o) => o.value === (updateSettings.CheckInterval ?? "0")); + const hour = updateSettings.CheckTime != null + ? hourOptions.find((o) => o.value === String(updateSettings.CheckTime)) + : null; + updateSettingsForm.reset({ + CheckInterval: interval ?? intervalOptions[0], + AutoUpdate: updateSettings.AutoUpdate ?? false, + CheckTime: hour ?? null, + }); + } + }, [containerStatus.isSuccess, updateSettings?.CheckInterval, updateSettings?.AutoUpdate, updateSettings?.CheckTime]); + const handleUpdateChannel = () => { - const selected = formControl.getValues("Channel"); + const selected = channelForm.getValues("Channel"); const channel = selected?.value ?? selected; - containerAction.mutate({ + channelAction.mutate({ url: "/api/ExecContainerManagement", data: { Action: "UpdateChannel", Channel: channel }, }); }; const handleRestart = () => { - containerAction.mutate({ + restartAction.mutate({ url: "/api/ExecContainerManagement", data: { Action: "Restart" }, }); }; + const handleCheckUpdate = () => { + updateCheckAction.mutate({ + url: "/api/ExecContainerManagement", + data: { Action: "CheckUpdate" }, + }); + }; + + const handleSaveUpdateSettings = () => { + const interval = updateSettingsForm.getValues("CheckInterval"); + const autoUpdate = updateSettingsForm.getValues("AutoUpdate"); + const checkTime = updateSettingsForm.getValues("CheckTime"); + updateSettingsAction.mutate({ + url: "/api/ExecContainerManagement", + data: { + Action: "SaveUpdateSettings", + CheckInterval: interval?.value ?? interval ?? "0", + AutoUpdate: autoUpdate ?? false, + CheckTime: checkTime?.value ?? checkTime ?? null, + }, + }); + }; + + const truncateDigest = (digest) => { + if (!digest) return "—"; + // Show algo prefix + first 12 hex chars + if (digest.startsWith("sha256:")) { + return `sha256:${digest.slice(7, 19)}…`; + } + return digest.length > 20 ? `${digest.slice(0, 20)}…` : digest; + }; + return ( - + + {containerStatus.isLoading ? ( @@ -91,6 +168,11 @@ export const CippContainerManagement = () => { apply. )} + {updateSettings?.UpdateAvailable && ( + + A container update is available. Restart the container to pull the latest image. + + )} @@ -134,6 +216,25 @@ export const CippContainerManagement = () => { + {updateSettings?.RunningDigest && ( + <> + + + Container Digest + + + + + {truncateDigest(updateSettings.RunningDigest)} + + + + )} + {data?.CurrentImage && data.CurrentImage !== "unknown" && ( <> @@ -142,7 +243,10 @@ export const CippContainerManagement = () => { - + {data.CurrentImage} @@ -166,7 +270,116 @@ export const CippContainerManagement = () => { )} + + + + + + + Configure automatic update checking. CIPP will query the container registry for a new + image digest and optionally restart the container to apply the update. + NOTE: If the container restarts for any reason the latest image version for your update channel will be pulled regardless + + + + + + + + + + + + + {updateSettings?.LastCheck && ( + + Last checked: {new Date(updateSettings.LastCheck * 1000).toLocaleString()} + {updateSettings.UpdateAvailable ? ( + + ) : ( + + )} + + )} + + {updateSettings?.RunningDigest && updateSettings?.RemoteDigest && ( + + + + Running Digest + + + + + {truncateDigest(updateSettings.RunningDigest)} + + + + + Remote Digest + + + + + {truncateDigest(updateSettings.RemoteDigest)} + + + + )} + + + + + + + + + + + + @@ -180,43 +393,49 @@ export const CippContainerManagement = () => { name="Channel" label="Release Channel" options={channelOptions} - formControl={formControl} + formControl={channelForm} creatable={false} multiple={false} /> - + + + + Restart the application container. This will cause a brief downtime while the container restarts. If you changed the release channel, this will pull the new image. + + - + + ); }; diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index b42c73d6c5ba..5aed7f6950a8 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -32,7 +32,7 @@ import { import { Grid } from "@mui/system"; import CippFormComponent from "../CippComponents/CippFormComponent"; import { useWatch, useFormState } from "react-hook-form"; -import _ from "lodash"; +import { get, isEqual, cloneDeep } from "lodash"; import Microsoft from "../../icons/iconly/bulk/microsoft"; import Azure from "../../icons/iconly/bulk/azure"; import Exchange from "../../icons/iconly/bulk/exchange"; @@ -168,7 +168,7 @@ const CippStandardAccordion = ({ // ALWAYS require an action for any standard to be considered configured // The action field should be an array with at least one element - const actionValue = _.get(values, "action"); + const actionValue = get(values, "action"); if (!actionValue || (Array.isArray(actionValue) && actionValue.length === 0)) return false; // Additional checks for required components @@ -188,7 +188,7 @@ const CippStandardAccordion = ({ // Handle conditional fields if (component.condition) { const conditionField = component.condition.field; - const conditionValue = _.get(values, conditionField); + const conditionValue = get(values, conditionField); const compareType = component.condition.compareType || "is"; const compareValue = component.condition.compareValue; const propertyName = component.condition.propertyName || "value"; @@ -197,10 +197,10 @@ const CippStandardAccordion = ({ if (propertyName === "value") { switch (compareType) { case "is": - conditionMet = _.isEqual(conditionValue, compareValue); + conditionMet = isEqual(conditionValue, compareValue); break; case "isNot": - conditionMet = !_.isEqual(conditionValue, compareValue); + conditionMet = !isEqual(conditionValue, compareValue); break; default: conditionMet = false; @@ -224,7 +224,7 @@ const CippStandardAccordion = ({ if (!isRequired) return true; // Get field value using lodash's get to properly handle nested properties - const fieldValue = _.get(values, component.name); + const fieldValue = get(values, component.name); // Check if field has a value based on its type and multiple property if (component.type === "autoComplete" || component.type === "select") { @@ -263,10 +263,10 @@ const CippStandardAccordion = ({ // For each standard, get its current values and determine if it's configured Object.keys(selectedStandards).forEach((standardName) => { - const currentValues = _.get(watchedValues, standardName); + const currentValues = get(watchedValues, standardName); if (!currentValues) return; - initial[standardName] = _.cloneDeep(currentValues); + initial[standardName] = cloneDeep(currentValues); const baseStandardName = standardName.split("[")[0]; const standard = providedStandards.find((s) => s.name === baseStandardName); @@ -305,9 +305,9 @@ const CippStandardAccordion = ({ const updated = { ...prev }; removedKeys.forEach((k) => delete updated[k]); addedKeys.forEach((k) => { - const currentValues = _.get(watchedValues, k); + const currentValues = get(watchedValues, k); if (currentValues) { - updated[k] = _.cloneDeep(currentValues); + updated[k] = cloneDeep(currentValues); } }); return updated; @@ -319,7 +319,7 @@ const CippStandardAccordion = ({ addedKeys.forEach((k) => { const baseStandardName = k.split("[")[0]; const standard = providedStandards.find((s) => s.name === baseStandardName); - const currentValues = _.get(watchedValues, k); + const currentValues = get(watchedValues, k); if (standard && currentValues) { updated[k] = isStandardConfigured(k, standard, currentValues); } @@ -333,7 +333,7 @@ const CippStandardAccordion = ({ // Save changes for a standard const handleSave = (standardName, standard, current) => { // Clone the current values to avoid reference issues - const newValues = _.cloneDeep(current); + const newValues = cloneDeep(current); // Update saved values setSavedValues((prev) => ({ @@ -369,11 +369,11 @@ const CippStandardAccordion = ({ // Cancel changes for a standard const handleCancel = (standardName) => { // Get the last saved values - const savedValue = _.get(savedValues, standardName); + const savedValue = get(savedValues, standardName); if (!savedValue) return; // Set the entire standard's value at once to ensure proper handling of nested objects and arrays - formControl.setValue(standardName, _.cloneDeep(savedValue)); + formControl.setValue(standardName, cloneDeep(savedValue)); // Find the original standard definition to get the base standard const baseStandardName = standardName.split("[")[0]; @@ -454,7 +454,7 @@ const CippStandardAccordion = ({ Array.isArray(standard.appliesToTest) && standard.appliesToTest.some((testId) => testId.toLowerCase().includes(searchLower))); - const isConfigured = _.get(configuredState, standardName); + const isConfigured = get(configuredState, standardName); const matchesFilter = filter === "all" || (filter === "configured" && isConfigured) || @@ -616,10 +616,10 @@ const CippStandardAccordion = ({ const isExpanded = expanded === standardName; const hasAddedComponents = standard.addedComponent && standard.addedComponent.length > 0; - const isConfigured = _.get(configuredState, standardName); + const isConfigured = get(configuredState, standardName); const disabledFeatures = standard.disabledFeatures || {}; - let selectedActions = _.get(watchedValues, `${standardName}.action`); + let selectedActions = get(watchedValues, `${standardName}.action`); if (selectedActions && !Array.isArray(selectedActions)) { selectedActions = [selectedActions]; } @@ -628,13 +628,13 @@ const CippStandardAccordion = ({ let templateDisplayName = ""; if (standardName.startsWith("standards.IntuneTemplate")) { // Check for TemplateList selection - const templateList = _.get(watchedValues, `${standardName}.TemplateList`); + const templateList = get(watchedValues, `${standardName}.TemplateList`); if (templateList && templateList.label) { templateDisplayName = templateList.label; } // Check for TemplateList-Tags selection (takes priority) - const templateListTags = _.get(watchedValues, `${standardName}.TemplateList-Tags`); + const templateListTags = get(watchedValues, `${standardName}.TemplateList-Tags`); if (templateListTags && templateListTags.label) { templateDisplayName = templateListTags.label; } @@ -642,21 +642,21 @@ const CippStandardAccordion = ({ // For multiple standards, check the first added component const selectedTemplateName = standard.multiple - ? _.get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) + ? get(watchedValues, `${standardName}.${standard.addedComponent?.[0]?.name}`) : ""; // Build accordion title with template name if available const accordionTitle = templateDisplayName ? `${standard.label} - ${templateDisplayName}` - : selectedTemplateName && _.get(selectedTemplateName, "label") - ? `${standard.label} - ${_.get(selectedTemplateName, "label")}` + : selectedTemplateName && get(selectedTemplateName, "label") + ? `${standard.label} - ${get(selectedTemplateName, "label")}` : standard.label; // Get current values and check if they differ from saved values - const current = _.get(watchedValues, standardName); - const saved = _.get(savedValues, standardName) || {}; + const current = get(watchedValues, standardName); + const saved = get(savedValues, standardName) || {}; - const hasUnsaved = !_.isEqual(current, saved); + const hasUnsaved = !isEqual(current, saved); // Check if all required fields are filled const requiredFieldsFilled = current @@ -671,7 +671,7 @@ const CippStandardAccordion = ({ // Handle conditional fields if (component.condition) { const conditionField = component.condition.field; - const conditionValue = _.get(current, conditionField); + const conditionValue = get(current, conditionField); const compareType = component.condition.compareType || "is"; const compareValue = component.condition.compareValue; const propertyName = component.condition.propertyName || "value"; @@ -680,10 +680,10 @@ const CippStandardAccordion = ({ if (propertyName === "value") { switch (compareType) { case "is": - conditionMet = _.isEqual(conditionValue, compareValue); + conditionMet = isEqual(conditionValue, compareValue); break; case "isNot": - conditionMet = !_.isEqual(conditionValue, compareValue); + conditionMet = !isEqual(conditionValue, compareValue); break; default: conditionMet = false; @@ -705,7 +705,7 @@ const CippStandardAccordion = ({ } // Get field value for validation using lodash's get to properly handle nested properties - const fieldValue = _.get(current, component.name); + const fieldValue = get(current, component.name); // Check if required field has a value based on its type and multiple property if (component.type === "autoComplete" || component.type === "select") { @@ -734,12 +734,12 @@ const CippStandardAccordion = ({ ); // Action is always required and must be an array with at least one element - const actionValue = _.get(current, "action"); + const actionValue = get(current, "action"); const hasAction = actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); // Check if this standard has any validation errors - const standardErrors = _.get(formErrors, standardName); + const standardErrors = get(formErrors, standardName); const hasValidationErrors = standardErrors && Object.keys(standardErrors).length > 0; // Allow saving if: @@ -957,7 +957,7 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} - currentValue={_.get( + currentValue={get( watchedValues, `${standardName}.${component.name}`, )} @@ -969,7 +969,7 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} - currentValue={_.get( + currentValue={get( watchedValues, `${standardName}.${component.name}`, )} @@ -1023,7 +1023,7 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} - currentValue={_.get( + currentValue={get( watchedValues, `${standardName}.${component.name}`, )} @@ -1035,7 +1035,7 @@ const CippStandardAccordion = ({ standardName={standardName} component={component} formControl={formControl} - currentValue={_.get( + currentValue={get( watchedValues, `${standardName}.${component.name}`, )} diff --git a/src/components/CippStandards/CippStandardsSideBar.jsx b/src/components/CippStandards/CippStandardsSideBar.jsx index f633f9b14ed9..2cab69c42f7e 100644 --- a/src/components/CippStandards/CippStandardsSideBar.jsx +++ b/src/components/CippStandards/CippStandardsSideBar.jsx @@ -29,7 +29,7 @@ import CheckIcon from "@heroicons/react/24/outline/CheckIcon"; import CloseIcon from "@mui/icons-material/Close"; import { useWatch } from "react-hook-form"; import { useEffect, useState } from "react"; -import _ from "lodash"; +import { get } from "lodash"; import CippFormComponent from "../CippComponents/CippFormComponent"; import { CippFormTenantSelector } from "../CippComponents/CippFormTenantSelector"; import { CippApiDialog } from "../CippComponents/CippApiDialog"; @@ -241,14 +241,14 @@ const CippStandardsSideBar = ({ useEffect(() => { const stepsStatus = { - step1: !!_.get(watchForm, "templateName"), - step2: _.get(watchForm, "tenantFilter", []).length > 0, + step1: !!get(watchForm, "templateName"), + step2: get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - _.get(watchForm, "standards") && + get(watchForm, "standards") && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { - const standardValues = _.get(watchForm, `${standardName}`, {}); + const standardValues = get(watchForm, `${standardName}`, {}); const standard = selectedStandards[standardName]; // Check if this standard requires an action const hasRequiredComponents = @@ -258,7 +258,7 @@ const CippStandardsSideBar = ({ ); const actionRequired = standard?.disabledFeatures !== undefined || hasRequiredComponents; // Always require an action value which should be an array with at least one element - const actionValue = _.get(standardValues, "action"); + const actionValue = get(standardValues, "action"); return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; @@ -269,17 +269,17 @@ const CippStandardsSideBar = ({ // Create a local reference to the stepsStatus from the latest effect run const stepsStatus = { - step1: !!_.get(watchForm, "templateName"), - step2: _.get(watchForm, "tenantFilter", []).length > 0, + step1: !!get(watchForm, "templateName"), + step2: get(watchForm, "tenantFilter", []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - _.get(watchForm, "standards") && + get(watchForm, "standards") && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { - const standardValues = _.get(watchForm, `${standardName}`, {}); + const standardValues = get(watchForm, `${standardName}`, {}); const standard = selectedStandards[standardName]; // Always require an action for all standards (must be an array with at least one element) - const actionValue = _.get(standardValues, "action"); + const actionValue = get(standardValues, "action"); return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0); }), }; diff --git a/src/components/CippTable/CippDataTable.js b/src/components/CippTable/CippDataTable.js index 253327713912..22c9bfe66ff2 100644 --- a/src/components/CippTable/CippDataTable.js +++ b/src/components/CippTable/CippDataTable.js @@ -341,6 +341,7 @@ export const CippDataTable = (props) => { }, exportEnabled = true, simpleColumns = [], + dataFilter, actions, title = 'Report', simple = false, @@ -476,7 +477,7 @@ export const CippDataTable = (props) => { const nestedData = getNestedValue(page, api.dataKey) return nestedData !== undefined ? nestedData : [] }) - setUsedData(combinedResults) + setUsedData(dataFilter ? combinedResults.filter(dataFilter) : combinedResults) } }, [ getRequestData.isSuccess, diff --git a/src/pages/cipp/advanced/super-admin/container.js b/src/pages/cipp/advanced/super-admin/container.js index d56595ac546c..9fb8a701174b 100644 --- a/src/pages/cipp/advanced/super-admin/container.js +++ b/src/pages/cipp/advanced/super-admin/container.js @@ -1,5 +1,4 @@ import { Container } from "@mui/material"; -import { Grid } from "@mui/system"; import { TabbedLayout } from "../../../../layouts/TabbedLayout"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; import tabOptions from "./tabOptions"; @@ -8,11 +7,7 @@ import { CippContainerManagement } from "../../../../components/CippSettings/Cip const Page = () => { return ( - - - - - + ); }; diff --git a/src/pages/cipp/settings/features.js b/src/pages/cipp/settings/features.js index 15b6fd3a111e..d630e93eb2ba 100644 --- a/src/pages/cipp/settings/features.js +++ b/src/pages/cipp/settings/features.js @@ -59,6 +59,7 @@ const Page = () => { offCanvas={offCanvas} simpleColumns={simpleColumns} tenantInTitle={false} + dataFilter={(row) => !row.Hidden} /> ); }; diff --git a/src/pages/tenant/standards/bpa-report/view.js b/src/pages/tenant/standards/bpa-report/view.js index f85fb633a3a3..6abd0203b330 100644 --- a/src/pages/tenant/standards/bpa-report/view.js +++ b/src/pages/tenant/standards/bpa-report/view.js @@ -10,7 +10,7 @@ import { useEffect, useState } from "react"; import CippButtonCard from "../../../../components/CippCards/CippButtonCard"; import { CippDataTable } from "../../../../components/CippTable/CippDataTable"; import { CippImageCard } from "../../../../components/CippCards/CippImageCard"; -import _ from "lodash"; +import { get } from "lodash"; const Page = () => { const router = useRouter(); const { id } = router.query; @@ -52,8 +52,8 @@ const Page = () => { const tenantData = bpaData?.data?.Data?.find((data) => data.GUID === tenantId); const cards = frontendFields.map((field) => { //instead of this, use lodash to get the data for blockData - const blockData = _.get(tenantData, field.value) - ? _.get(tenantData, field.value) + const blockData = get(tenantData, field.value) + ? get(tenantData, field.value) : undefined; return { name: field.name, diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx index 630fdee6f2ce..7e442863b3f1 100644 --- a/src/pages/tenant/standards/templates/template.jsx +++ b/src/pages/tenant/standards/templates/template.jsx @@ -15,7 +15,7 @@ import CippStandardsSideBar from '../../../../components/CippStandards/CippStand import { ArrowLeftIcon } from '@mui/x-date-pickers' import { useDialog } from '../../../../hooks/use-dialog' import { ApiGetCall } from '../../../../api/ApiCall' -import _ from 'lodash' +import { get } from 'lodash' import { createDriftManagementActions } from '../../manage/driftManagementActions' import { ActionsMenu } from '../../../../components/actions-menu' import { useSettings } from '../../../../hooks/use-settings' @@ -61,16 +61,16 @@ const Page = () => { // Check if the template configuration is valid and update currentStep useEffect(() => { const stepsStatus = { - step1: !!_.get(watchForm, 'templateName'), - step2: _.get(watchForm, 'tenantFilter', []).length > 0, + step1: !!get(watchForm, 'templateName'), + step2: get(watchForm, 'tenantFilter', []).length > 0, step3: Object.keys(selectedStandards).length > 0, step4: - _.get(watchForm, 'standards') && + get(watchForm, 'standards') && Object.keys(selectedStandards).length > 0 && Object.keys(selectedStandards).every((standardName) => { - const standardValues = _.get(watchForm, standardName, {}) + const standardValues = get(watchForm, standardName, {}) // Always require an action value which should be an array with at least one element - const actionValue = _.get(standardValues, 'action') + const actionValue = get(standardValues, 'action') return actionValue && (!Array.isArray(actionValue) || actionValue.length > 0) }), } @@ -299,12 +299,12 @@ const Page = () => { // Determine if save button should be disabled based on configuration const isSaveDisabled = isDriftMode - ? !_.get(watchForm, 'tenantFilter') || - !_.get(watchForm, 'tenantFilter').length || + ? !get(watchForm, 'tenantFilter') || + !get(watchForm, 'tenantFilter').length || currentStep < 4 || hasDriftConflict // For drift mode, require all steps and no drift conflicts - : !_.get(watchForm, 'tenantFilter') || - !_.get(watchForm, 'tenantFilter').length || + : !get(watchForm, 'tenantFilter') || + !get(watchForm, 'tenantFilter').length || currentStep < 4 // Create drift management actions (excluding refresh) diff --git a/yarn.lock b/yarn.lock index 847ef311f58e..acfda103a76e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1016,14 +1016,14 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6" integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg== -"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": +"@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1": version "4.9.1" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.6.1": +"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2": version "4.12.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== @@ -1051,21 +1051,6 @@ dependencies: "@types/json-schema" "^7.0.15" -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" - ignore "^5.2.0" - import-fresh "^3.2.1" - js-yaml "^4.1.0" - minimatch "^3.1.2" - strip-json-comments "^3.1.1" - "@eslint/eslintrc@^3.3.5": version "3.3.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz#c131793cfc1a7b96f24a83e0a8bbd4b881558c60" @@ -1081,11 +1066,6 @@ minimatch "^3.1.5" strip-json-comments "^3.1.1" -"@eslint/js@8.57.1": - version "8.57.1" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2" - integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q== - "@eslint/js@9.39.4": version "9.39.4" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.4.tgz#a3f83bfc6fd9bf33a853dfacd0b49b398eb596c1" @@ -1142,25 +1122,11 @@ "@humanfs/core" "^0.19.1" "@humanwhocodes/retry" "^0.4.0" -"@humanwhocodes/config-array@^0.13.0": - version "0.13.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" - integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== - dependencies: - "@humanwhocodes/object-schema" "^2.0.3" - debug "^4.3.1" - minimatch "^3.0.5" - "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.3": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== - "@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2": version "0.4.3" resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" @@ -1313,13 +1279,6 @@ resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz#a81ffb00e69267cd0a1d626eaedb8a8430b2b2f8" integrity sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw== -"@jest/schemas@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" - integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== - dependencies: - "@sinclair/typebox" "^0.27.8" - "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz#6342a19f44347518c93e43b1ac69deb3c4656a1f" @@ -1722,7 +1681,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": +"@nodelib/fs.walk@^1.2.3": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -1950,11 +1909,6 @@ resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== -"@sinclair/typebox@^0.27.8": - version "0.27.10" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.10.tgz#beefe675f1853f73676aecc915b2bd2ac98c4fc6" - integrity sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA== - "@sinonjs/text-encoding@^0.7.2": version "0.7.3" resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" @@ -2263,11 +2217,6 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.5.tgz#c21b2c7405f4aad7b507e36cc3394aba51ea2253" integrity sha512-4UtpUHg8cRzxWjJUGtni5VnXYbhsO7ygf1H1pr4Rv63XMBg9lfYDeSwByIuVy9biEFP7eGEFnezzb5Zlh1btmQ== -"@tiptap/extension-image@^3.20.5": - version "3.20.5" - resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-3.20.5.tgz#90a80ce694dcda452a296d38f457bfbe72bf940d" - integrity sha512-qxKupWKhX75Xc9GJ9Uel+KIFL9x6tb8W3RvQM1UolyJX/H7wyBO7sXp9XmKRkHZsDXRgLVbnkYBe+X83o16AIA== - "@tiptap/extension-italic@^3.20.5": version "3.20.5" resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-3.20.5.tgz#c53436f05968b16eda6b8e0efbaebaf3f4587e3b" @@ -2597,11 +2546,6 @@ resolved "https://registry.yarnpkg.com/@types/raf/-/raf-3.4.3.tgz#85f1d1d17569b28b8db45e16e996407a56b0ab04" integrity sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw== -"@types/react-dom@^19.2.3": - version "19.2.3" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c" - integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ== - "@types/react-redux@^7.1.20": version "7.1.34" resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.34.tgz#83613e1957c481521e6776beeac4fd506d11bd0e" @@ -2617,7 +2561,7 @@ resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044" integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w== -"@types/react@*", "@types/react@^19.2.14": +"@types/react@*": version "19.2.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.14.tgz#39604929b5e3957e3a6fa0001dafb17c7af70bad" integrity sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w== @@ -2669,17 +2613,6 @@ "@typescript-eslint/visitor-keys" "8.57.1" debug "^4.4.3" -"@typescript-eslint/parser@^6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== - dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - "@typescript-eslint/project-service@8.57.1": version "8.57.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.57.1.tgz#16af9fe16eedbd7085e4fdc29baa73715c0c55c5" @@ -2689,14 +2622,6 @@ "@typescript-eslint/types" "^8.57.1" debug "^4.4.3" -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - "@typescript-eslint/scope-manager@8.57.1": version "8.57.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz#4524d7e7b420cb501807499684d435ae129aaf35" @@ -2721,30 +2646,11 @@ debug "^4.4.3" ts-api-utils "^2.4.0" -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - "@typescript-eslint/types@8.57.1", "@typescript-eslint/types@^8.57.1": version "8.57.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.57.1.tgz#54b27a8a25a7b45b4f978c3f8e00c4c78f11142c" integrity sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ== -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - "@typescript-eslint/typescript-estree@8.57.1": version "8.57.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz#a9fd28d4a0ec896aa9a9a7e0cead62ea24f99e76" @@ -2770,14 +2676,6 @@ "@typescript-eslint/types" "8.57.1" "@typescript-eslint/typescript-estree" "8.57.1" -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - "@typescript-eslint/visitor-keys@8.57.1": version "8.57.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz#3af4f88118924d3be983d4b8ae84803f11fe4563" @@ -2786,12 +2684,7 @@ "@typescript-eslint/types" "8.57.1" eslint-visitor-keys "^5.0.0" -"@uiw/react-json-view@^2.0.0-alpha.42": - version "2.0.0-alpha.42" - resolved "https://registry.yarnpkg.com/@uiw/react-json-view/-/react-json-view-2.0.0-alpha.42.tgz#0830cfa6767debb621c10ff71201c2302605c096" - integrity sha512-PY7IF+zL3gYaW/FG3th0w6JG2SpkYqh/UZOgKm2XuY/UpCZ5inWlopR+pfRadRz/k/uTaOhsQa9jZnlp8QBJDA== - -"@ungap/structured-clone@^1.0.0", "@ungap/structured-clone@^1.2.0": +"@ungap/structured-clone@^1.0.0": version "1.3.0" resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== @@ -2908,12 +2801,12 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.15.0, acorn@^8.9.0: +acorn@^8.15.0: version "8.16.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== -ajv@^6.12.4, ajv@^6.14.0: +ajv@^6.14.0: version "6.14.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" integrity sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw== @@ -2923,21 +2816,6 @@ ajv@^6.12.4, ajv@^6.14.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== - ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -2945,11 +2823,6 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - apexcharts@5.10.4: version "5.10.4" resolved "https://registry.yarnpkg.com/apexcharts/-/apexcharts-5.10.4.tgz#79c9a05ab40b069f33873a1859de6cb0882ccf0e" @@ -2994,11 +2867,6 @@ array-includes@^3.1.6, array-includes@^3.1.8, array-includes@^3.1.9: is-string "^1.1.1" math-intrinsics "^1.1.0" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" @@ -3202,13 +3070,6 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" -brace-expansion@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" - integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== - dependencies: - balanced-match "^1.0.0" - brace-expansion@^5.0.2: version "5.0.4" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.4.tgz#614daaecd0a688f660bbbc909a8748c3d80d4336" @@ -3313,17 +3174,6 @@ ccount@^2.0.0: resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== -chalk@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - chalk@^4.0.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" @@ -3404,11 +3254,6 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -common-tags@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" - integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -3424,13 +3269,6 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== -copy-to-clipboard@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz#55ac43a1db8ae639a4bd99511c148cdd1b83a1b0" - integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA== - dependencies: - toggle-selection "^1.0.6" - core-js-compat@^3.48.0: version "3.49.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.49.0.tgz#06145447d92f4aaf258a0c44f24b47afaeaffef6" @@ -3474,7 +3312,7 @@ crelt@^1.0.0: resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.6.tgz#7cc898ea74e190fb6ef9dae57f8f81cf7302df72" integrity sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g== -cross-spawn@^7.0.2, cross-spawn@^7.0.6: +cross-spawn@^7.0.6: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -3720,7 +3558,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.4.0, debug@^4.4.3: +debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -3816,18 +3654,6 @@ diff@^8.0.3: resolved "https://registry.yarnpkg.com/diff/-/diff-8.0.4.tgz#4f5baf3188b9b2431117b962eb20ba330fadf696" integrity sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -3835,13 +3661,6 @@ doctrine@^2.1.0: dependencies: esutils "^2.0.2" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dom-helpers@^5.0.1: version "5.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" @@ -3905,6 +3724,13 @@ dompurify@^3.3.1: optionalDependencies: "@types/trusted-types" "^2.0.7" +dompurify@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218" + integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA== + optionalDependencies: + "@types/trusted-types" "^2.0.7" + domutils@^1.5.1: version "1.7.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" @@ -4137,11 +3963,6 @@ escalade@^3.2.0: resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== -escape-string-regexp@^1.0.2: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" @@ -4282,14 +4103,6 @@ eslint-plugin-react@^7.37.0: string.prototype.matchall "^4.0.12" string.prototype.repeat "^1.0.0" -eslint-scope@^7.1.1, eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - eslint-scope@^8.4.0: version "8.4.0" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82" @@ -4298,7 +4111,7 @@ eslint-scope@^8.4.0: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -4313,50 +4126,6 @@ eslint-visitor-keys@^5.0.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== -eslint@^8.57.1: - version "8.57.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9" - integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.1" - "@humanwhocodes/config-array" "^0.13.0" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - eslint@^9.39.4: version "9.39.4" resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.4.tgz#855da1b2e2ad66dc5991195f35e262bcec8117b5" @@ -4406,21 +4175,12 @@ espree@^10.0.1, espree@^10.4.0: acorn-jsx "^5.3.2" eslint-visitor-keys "^4.2.1" -espree@^9.3.1, espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0, esquery@^1.4.2, esquery@^1.5.0: +esquery@^1.5.0: version "1.7.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d" integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== @@ -4491,11 +4251,6 @@ fast-diff@1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.1.2.tgz#4b62c42b8e03de3f848460b639079920695d0154" integrity sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig== -fast-equals@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-4.0.3.tgz#72884cc805ec3c6679b99875f6b7654f39f0e8c7" - integrity sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg== - fast-equals@^5.3.3: version "5.4.0" resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-5.4.0.tgz#b60073b8764f27029598447f05773c7534ba7f1e" @@ -4512,17 +4267,6 @@ fast-glob@3.3.1: merge2 "^1.3.0" micromatch "^4.0.4" -fast-glob@^3.2.9: - version "3.3.3" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" - integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.8" - fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -4566,13 +4310,6 @@ fflate@^0.8.1: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" @@ -4607,15 +4344,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" - flat-cache@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" @@ -4686,11 +4414,6 @@ formik@2.4.9: tiny-warning "^1.0.2" tslib "^2.0.0" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - function-bind@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" @@ -4777,30 +4500,11 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - globals@16.4.0: version "16.4.0" resolved "https://registry.yarnpkg.com/globals/-/globals-16.4.0.tgz#574bc7e72993d40cf27cf6c241f324ee77808e51" integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw== -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - globals@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" @@ -4814,18 +4518,6 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - goober@^2.1.16: version "2.1.18" resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.18.tgz#b72d669bd24d552d441638eee26dfd5716ea6442" @@ -4836,11 +4528,6 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - gray-matter@4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" @@ -4851,13 +4538,6 @@ gray-matter@4.0.3: section-matter "^1.0.0" strip-bom-string "^1.0.0" -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== - dependencies: - ansi-regex "^2.0.0" - has-bigints@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" @@ -5039,13 +4719,6 @@ hsl-to-rgb-for-reals@^1.1.0: resolved "https://registry.yarnpkg.com/hsl-to-rgb-for-reals/-/hsl-to-rgb-for-reals-1.1.1.tgz#e1eb23f6b78016e3722431df68197e6dcdc016d9" integrity sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg== -html-parse-stringify@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" - integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== - dependencies: - void-elements "3.1.0" - html-tokenize@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/html-tokenize/-/html-tokenize-2.0.1.tgz#c3b2ea6e2837d4f8c06693393e9d2a12c960be5f" @@ -5092,13 +4765,6 @@ hyphen@^1.6.4: resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.14.1.tgz#c9fbd5e1af750f00d5034aa37f6ec41f95ffed93" integrity sha512-kvL8xYl5QMTh+LwohVN72ciOxC0OEV79IPdJSTwEXok9y9QHebXGdFgrED4sWfiax/ODx++CAMk3hMy4XPJPOw== -i18next@25.8.18: - version "25.8.18" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-25.8.18.tgz#51863b65bc42e3525271f2680ebbf7d150ff53cc" - integrity sha512-lzY5X83BiL5AP77+9DydbrqkQHFN9hUzWGjqjLpPcp5ZOzuu1aSoKaU3xbBLSjWx9dAzW431y+d+aogxOZaKRA== - dependencies: - "@babel/runtime" "^7.28.6" - ignore@^5.2.0: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -5132,20 +4798,7 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5349,11 +5002,6 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - is-plain-obj@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" @@ -5567,7 +5215,7 @@ jspdf@^4.2.0: object.assign "^4.1.4" object.values "^1.1.6" -keyv@^4.5.3, keyv@^4.5.4: +keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -5591,11 +5239,6 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" -leaflet-defaulticon-compatibility@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/leaflet-defaulticon-compatibility/-/leaflet-defaulticon-compatibility-0.1.2.tgz#f5e1a5841aeab9d1682d17887348855a741b3c2a" - integrity sha512-IrKagWxkTwzxUkFIumy/Zmo3ksjuAu3zEadtOuJcKzuXaD76Gwvg2Z1mLyx7y52ykOzM8rAH5ChBs4DnfdGa6Q== - leaflet.markercluster@^1.5.3: version "1.5.3" resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz#9cdb52a4eab92671832e1ef9899669e80efc4056" @@ -5671,18 +5314,10 @@ lodash@^4.17.21, lodash@^4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.23.tgz#f113b0378386103be4f6893388c73d0bde7f2c5a" integrity sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w== -loglevel-colored-level-prefix@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/loglevel-colored-level-prefix/-/loglevel-colored-level-prefix-1.0.0.tgz#6a40218fdc7ae15fc76c3d0f3e676c465388603e" - integrity sha512-u45Wcxxc+SdAlh4yeF/uKlC1SPUPCy0gullSNKXod5I4bmifzk+Q4lSLExNEVn19tGaJipbZ4V4jbFn79/6mVA== - dependencies: - chalk "^1.1.3" - loglevel "^1.4.1" - -loglevel@^1.4.1: - version "1.9.2" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.9.2.tgz#c2e028d6c757720107df4e64508530db6621ba08" - integrity sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg== +lodash@^4.18.1: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== longest-streak@^3.0.0: version "3.1.0" @@ -5965,7 +5600,7 @@ memoize-one@^6.0.0: resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -6243,7 +5878,7 @@ micromark@^4.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromatch@^4.0.4, micromatch@^4.0.8: +micromatch@^4.0.4: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -6263,13 +5898,6 @@ mime-types@^2.1.12: dependencies: mime-db "1.52.0" -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - minimatch@^10.2.2: version "10.2.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde" @@ -6277,7 +5905,7 @@ minimatch@^10.2.2: dependencies: brace-expansion "^5.0.2" -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2, minimatch@^3.1.5: +minimatch@^3.1.2, minimatch@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e" integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== @@ -6483,13 +6111,6 @@ object.values@^1.1.6, object.values@^1.2.1: define-properties "^1.2.1" es-object-atoms "^1.0.0" -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -6602,11 +6223,6 @@ path-exists@^4.0.0: resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" @@ -6666,38 +6282,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-eslint@^16.4.2: - version "16.4.2" - resolved "https://registry.yarnpkg.com/prettier-eslint/-/prettier-eslint-16.4.2.tgz#d84bff76e0ce4a6ffccacacb2474f7635ca8ac35" - integrity sha512-vtJAQEkaN8fW5QKl08t7A5KCjlZuDUNeIlr9hgolMS5s3+uzbfRHDwaRnzrdqnY2YpHDmeDS/8zY0MKQHXJtaA== - dependencies: - "@typescript-eslint/parser" "^6.21.0" - common-tags "^1.8.2" - dlv "^1.1.3" - eslint "^8.57.1" - indent-string "^4.0.0" - lodash.merge "^4.6.2" - loglevel-colored-level-prefix "^1.0.0" - prettier "^3.5.3" - pretty-format "^29.7.0" - require-relative "^0.8.7" - tslib "^2.8.1" - vue-eslint-parser "^9.4.3" - -prettier@^3.5.3, prettier@^3.8.1: +prettier@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.1.tgz#edf48977cf991558f4fcbd8a3ba6015ba2a3a173" integrity sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg== -pretty-format@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" - integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== - dependencies: - "@jest/schemas" "^29.6.3" - ansi-styles "^5.0.0" - react-is "^18.0.0" - prismjs@^1.30.0: version "1.30.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.30.0.tgz#d9709969d9d4e16403f6f348c63553b19f0975a9" @@ -6708,7 +6297,7 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -prop-types@15.8.1, prop-types@15.x, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6966,14 +6555,6 @@ react-colorful@^5.6.1: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== -react-copy-to-clipboard@^5.1.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.1.tgz#76adb8be03616e99692fcf3f762365ed3fb5ff16" - integrity sha512-s+HrzLyJBxrpGTYXF15dTgMjAJpEPZT/Yp6NytAtZMRngejxt6Pt5WrfFxLAcsqUDU6sY1Jz6tyHwIicE1U2Xg== - dependencies: - copy-to-clipboard "^3.3.3" - prop-types "^15.8.1" - react-dom@19.2.5: version "19.2.5" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.5.tgz#b8768b10837d0b8e9ca5b9e2d58dff3d880ea25e" @@ -6981,14 +6562,6 @@ react-dom@19.2.5: dependencies: scheduler "^0.27.0" -react-draggable@^4.4.6, react-draggable@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.5.0.tgz#0b274ccb6965fcf97ed38fcf7e3cc223bc48cdf5" - integrity sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw== - dependencies: - clsx "^2.1.1" - prop-types "^15.8.1" - react-dropzone@15.0.0: version "15.0.0" resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-15.0.0.tgz#bd03c7c2b14fe4ea9db1a9c74502b85339f2e505" @@ -7008,18 +6581,6 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-grid-layout@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-2.2.3.tgz#6daf24b8c48448af617238520dd233a9375e2f16" - integrity sha512-OAEJHBxmfuxQfVtZwRzmsokijGlBgzYIJ7MUlLk/VSa43SaGzu15w5D0P2RDrfX5EvP9POMbL6bFrai/huDzbQ== - dependencies: - clsx "^2.1.1" - fast-equals "^4.0.3" - prop-types "^15.8.1" - react-draggable "^4.4.6" - react-resizable "^3.1.3" - resize-observer-polyfill "^1.5.1" - react-hook-form@^7.72.0: version "7.72.0" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.72.0.tgz#995a655b894249fd8798f36383e43f55ed66ae25" @@ -7040,15 +6601,6 @@ react-html-parser@^2.0.2: dependencies: htmlparser2 "^3.9.0" -react-i18next@16.6.5: - version "16.6.5" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-16.6.5.tgz#6fd2b0b82ed6988b87e51487d53c28954994d361" - integrity sha512-bfdJhmyjQCXtU9CLcGMn3a1V5/jTeUX/x29cOhlS1Lolm/epRtm24gnYsltxArsc29ow3klSJEijjfYXc5kxjg== - dependencies: - "@babel/runtime" "^7.29.2" - html-parse-stringify "^3.0.1" - use-sync-external-store "^1.6.0" - react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -7059,11 +6611,6 @@ react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" - integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== - react-is@^19.2.3: version "19.2.4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.4.tgz#a080758243c572ccd4a63386537654298c99d135" @@ -7150,14 +6697,6 @@ react-redux@^7.2.0: prop-types "^15.7.2" react-is "^17.0.2" -react-resizable@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.1.3.tgz#b8c3f8aeffb7b0b2c2306bfc7a742462e58125fb" - integrity sha512-liJBNayhX7qA4tBJiBD321FDhJxgGTJ07uzH5zSORXoE8h7PyEZ8mLqmosST7ppf6C4zUsbd2gzDMmBCfFp9Lw== - dependencies: - prop-types "15.x" - react-draggable "^4.5.0" - react-syntax-highlighter@^16.1.0: version "16.1.1" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-16.1.1.tgz#928459855d375f5cfc8e646071e20d541cebcb52" @@ -7199,11 +6738,6 @@ react-virtuoso@^4.18.5: resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.5.tgz#450108e585c7a1124b995c7ea3cf367ed4857631" integrity sha512-QDyNjyNEuurZG67SOmzYyxEkQYSyGmAMixOI6M15L/Q4CF39EgG+88y6DgZRo0q7rmy0HPx3Fj90I8/tPdnRCQ== -react-window@^2.2.7: - version "2.2.7" - resolved "https://registry.yarnpkg.com/react-window/-/react-window-2.2.7.tgz#7f3d31695d4323701b7e80dfc9bbbe1d4a0c160f" - integrity sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w== - react@19.2.5: version "19.2.5" resolved "https://registry.yarnpkg.com/react/-/react-19.2.5.tgz#c888ab8b8ef33e2597fae8bdb2d77edbdb42858b" @@ -7258,17 +6792,12 @@ recharts@^3.8.1: use-sync-external-store "^1.2.2" victory-vendor "^37.0.2" -redux-devtools-extension@2.13.9: - version "2.13.9" - resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz#6b764e8028b507adcb75a1cae790f71e6be08ae7" - integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A== - redux-persist@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== -redux-thunk@3.1.0, redux-thunk@^3.1.0: +redux-thunk@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-3.1.0.tgz#94aa6e04977c30e14e892eae84978c1af6058ff3" integrity sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw== @@ -7428,21 +6957,11 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-relative@^0.8.7: - version "0.8.7" - resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de" - integrity sha512-AKGr4qvHiryxRb19m3PsLRGuKVAbJLUD7E6eOaHkfKhwc+vSgVOCY5xNvm9EkolBKTOf0GrQAZKLimOCz81Khg== - reselect@5.1.1, reselect@^5.1.0, reselect@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/reselect/-/reselect-5.1.1.tgz#c766b1eb5d558291e5e550298adb0becc24bb72e" integrity sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w== -resize-observer-polyfill@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" - integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -7489,13 +7008,6 @@ rgbcolor@^1.0.1: resolved "https://registry.yarnpkg.com/rgbcolor/-/rgbcolor-1.0.1.tgz#d6505ecdb304a6595da26fa4b43307306775945d" integrity sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw== -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - rope-sequence@^1.3.0: version "1.3.4" resolved "https://registry.yarnpkg.com/rope-sequence/-/rope-sequence-1.3.4.tgz#df85711aaecd32f1e756f76e43a415171235d425" @@ -7574,7 +7086,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.6, semver@^7.5.4, semver@^7.7.1, semver@^7.7.3: +semver@^7.7.1, semver@^7.7.3: version "7.7.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== @@ -7725,11 +7237,6 @@ simplebar@6.3.3: dependencies: simplebar-core "^1.3.2" -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -7876,20 +7383,6 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-bom-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" @@ -7938,11 +7431,6 @@ stylis@4.2.0: resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" integrity sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw== -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== - supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -7990,11 +7478,6 @@ text-segmentation@^1.0.3: dependencies: utrie "^1.0.2" -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - through2@~0.4.1: version "0.4.2" resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b" @@ -8043,11 +7526,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -toggle-selection@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" - integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== - toposort@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" @@ -8063,11 +7541,6 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== -ts-api-utils@^1.0.1: - version "1.4.3" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064" - integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw== - ts-api-utils@^2.4.0: version "2.5.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" @@ -8083,7 +7556,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0, tslib@^2.8.1: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.7.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -8095,11 +7568,6 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" @@ -8160,11 +7628,6 @@ typescript-eslint@^8.46.0: "@typescript-eslint/typescript-estree" "8.57.1" "@typescript-eslint/utils" "8.57.1" -typescript@5.9.3: - version "5.9.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" - integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== - uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" @@ -8224,7 +7687,7 @@ unicode-trie@^2.0.0: pako "^0.2.5" tiny-inflate "^1.0.0" -unified@^11.0.0: +unified@^11.0.0, unified@^11.0.5: version "11.0.5" resolved "https://registry.yarnpkg.com/unified/-/unified-11.0.5.tgz#f66677610a5c0a9ee90cab2b8d4d66037026d9e1" integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== @@ -8397,24 +7860,6 @@ vite-compatible-readable-stream@^3.6.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -void-elements@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" - integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== - -vue-eslint-parser@^9.4.3: - version "9.4.3" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz#9b04b22c71401f1e8bca9be7c3e3416a4bde76a8" - integrity sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg== - dependencies: - debug "^4.3.4" - eslint-scope "^7.1.1" - eslint-visitor-keys "^3.3.0" - espree "^9.3.1" - esquery "^1.4.0" - lodash "^4.17.21" - semver "^7.3.6" - w3c-keyname@^2.2.0: version "2.2.8" resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.8.tgz#7b17c8c6883d4e8b86ac8aba79d39e880f8869c5" @@ -8490,11 +7935,6 @@ word-wrap@^1.2.5: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - xtend@~2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b" From c0946109c91b31ee9699ba7dbf377a8f299b1a55 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 13 May 2026 16:47:42 +0800 Subject: [PATCH 78/86] Fix bulk mailbox rule changes --- .../administration/users/user/exchange.jsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/pages/identity/administration/users/user/exchange.jsx b/src/pages/identity/administration/users/user/exchange.jsx index 1d01f699c5a7..1336a740612d 100644 --- a/src/pages/identity/administration/users/user/exchange.jsx +++ b/src/pages/identity/administration/users/user/exchange.jsx @@ -947,13 +947,15 @@ const Page = () => { icon: , url: "/api/ExecSetMailboxRule", customDataformatter: (row, action, formData) => { - return { - ruleId: row?.Identity, + const rows = Array.isArray(row) ? row : [row]; + const result = rows.map((r) => ({ + ruleId: r?.Identity, userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, - ruleName: row?.Name, + ruleName: r?.Name, Enable: true, tenantFilter: userSettingsDefaults.currentTenant, - }; + })); + return Array.isArray(row) ? result : result[0]; }, condition: (row) => row && !row.Enabled, confirmText: "Are you sure you want to enable this mailbox rule?", @@ -965,13 +967,15 @@ const Page = () => { icon: , url: "/api/ExecSetMailboxRule", customDataformatter: (row, action, formData) => { - return { - ruleId: row?.Identity, + const rows = Array.isArray(row) ? row : [row]; + const result = rows.map((r) => ({ + ruleId: r?.Identity, userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, - ruleName: row?.Name, + ruleName: r?.Name, Disable: true, tenantFilter: userSettingsDefaults.currentTenant, - }; + })); + return Array.isArray(row) ? result : result[0]; }, condition: (row) => row && row.Enabled, confirmText: "Are you sure you want to disable this mailbox rule?", @@ -983,12 +987,14 @@ const Page = () => { icon: , url: "/api/ExecRemoveMailboxRule", customDataformatter: (row, action, formData) => { - return { - ruleId: row?.Identity, - ruleName: row?.Name, + const rows = Array.isArray(row) ? row : [row]; + const result = rows.map((r) => ({ + ruleId: r?.Identity, + ruleName: r?.Name, userPrincipalName: graphUserRequest.data?.[0]?.userPrincipalName, tenantFilter: userSettingsDefaults.currentTenant, - }; + })); + return Array.isArray(row) ? result : result[0]; }, confirmText: "Are you sure you want to remove this mailbox rule?", multiPost: false, From 72d8658d5ded1c0f47fddeacb3a384c5cbf0e38d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 13 May 2026 17:06:41 +0800 Subject: [PATCH 79/86] Add Apps and SP to universal search --- .../CippCards/CippUniversalSearchV2.jsx | 29 +++++++++++++++++++ src/components/bulk-actions-menu.js | 4 ++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 28a53f35ef82..070396f0a56e 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -348,6 +348,16 @@ export const CippUniversalSearchV2 = React.forwardRef( router.push( `/identity/administration/groups/group?groupId=${itemData.id}&tenantFilter=${tenantDomain}`, ); + } else if (searchType === "Applications") { + if (match.Type === "Apps") { + router.push( + `/tenant/administration/applications/app-registration?appId=${itemData.appId || itemData.id}&tenantFilter=${tenantDomain}`, + ); + } else { + router.push( + `/tenant/administration/applications/enterprise-app?spId=${itemData.id}&tenantFilter=${tenantDomain}`, + ); + } } else if (searchType === "Pages") { router.push(match.path, undefined, { shallow: true }); } @@ -389,6 +399,11 @@ export const CippUniversalSearchV2 = React.forwardRef( icon: "Group", onClick: () => handleTypeChange("Groups"), }, + { + label: "Applications", + icon: "Apps", + onClick: () => handleTypeChange("Applications"), + }, { label: "BitLocker", icon: "FilePresent", @@ -730,6 +745,20 @@ const Results = ({ )} )} + {searchType === "Applications" && ( + <> + {itemData.appId && ( + + {highlightMatch(itemData.appId || "")} + + )} + {itemData.publisherName && ( + + {highlightMatch(itemData.publisherName || "")} + + )} + + )} ; case "Group": return ; + case "Apps": + return ; default: return null; } From ba196dde059e16c53ee182a7d49eff8292073e27 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 13 May 2026 12:07:00 +0200 Subject: [PATCH 80/86] expand side nav slightly for ux --- .claude/worktrees/blissful-golick-d405ab | 1 + src/layouts/side-nav.js | 174 +++++++++++------------ 2 files changed, 88 insertions(+), 87 deletions(-) create mode 160000 .claude/worktrees/blissful-golick-d405ab diff --git a/.claude/worktrees/blissful-golick-d405ab b/.claude/worktrees/blissful-golick-d405ab new file mode 160000 index 000000000000..0710355e2ada --- /dev/null +++ b/.claude/worktrees/blissful-golick-d405ab @@ -0,0 +1 @@ +Subproject commit 0710355e2adac37fffe4c7eef48d6f2c3a04993d diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index ec43e9fb857f..5b01ee107331 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -1,64 +1,64 @@ -import { useState, useRef, useEffect } from "react"; -import { usePathname } from "next/navigation"; -import PropTypes from "prop-types"; -import { Box, Divider, Drawer, Stack } from "@mui/material"; -import { SideNavItem } from "./side-nav-item"; -import { SideNavBookmarks } from "./side-nav-bookmarks"; -import { ApiGetCall } from "../api/ApiCall.jsx"; -import { CippSponsor } from "../components/CippComponents/CippSponsor"; -import { useSettings } from "../hooks/use-settings"; - -const SIDE_NAV_WIDTH = 270; -const SIDE_NAV_COLLAPSED_WIDTH = 73; // icon size + padding + border right -const TOP_NAV_HEIGHT = 64; +import { useState, useRef, useEffect } from 'react' +import { usePathname } from 'next/navigation' +import PropTypes from 'prop-types' +import { Box, Divider, Drawer, Stack } from '@mui/material' +import { SideNavItem } from './side-nav-item' +import { SideNavBookmarks } from './side-nav-bookmarks' +import { ApiGetCall } from '../api/ApiCall.jsx' +import { CippSponsor } from '../components/CippComponents/CippSponsor' +import { useSettings } from '../hooks/use-settings' + +const SIDE_NAV_WIDTH = 290 +const SIDE_NAV_COLLAPSED_WIDTH = 73 // icon size + padding + border right +const TOP_NAV_HEIGHT = 64 const isPathPrefix = (pathname, itemPath) => { - if (!pathname || !itemPath) return false; - if (pathname === itemPath) return true; + if (!pathname || !itemPath) return false + if (pathname === itemPath) return true // Root "/" maps to /dashboardv2 under the hood - if (itemPath === "/") return pathname.startsWith("/dashboardv2"); - return pathname.startsWith(itemPath + "/") || pathname.startsWith(itemPath + "?"); -}; + if (itemPath === '/') return pathname.startsWith('/dashboardv2') + return pathname.startsWith(itemPath + '/') || pathname.startsWith(itemPath + '?') +} const markOpenItems = (items, pathname) => { return items.map((item) => { - const checkPath = !!(item.path && pathname); - const exactMatch = checkPath ? pathname === item.path : false; - const partialMatch = checkPath ? isPathPrefix(pathname, item.path) : false; + const checkPath = !!(item.path && pathname) + const exactMatch = checkPath ? pathname === item.path : false + const partialMatch = checkPath ? isPathPrefix(pathname, item.path) : false - let openImmediately = exactMatch; - let newItems = item.items || []; + let openImmediately = exactMatch + let newItems = item.items || [] if (newItems.length > 0) { - newItems = markOpenItems(newItems, pathname); - const childOpen = newItems.some((child) => child.openImmediately); - openImmediately = openImmediately || childOpen || exactMatch; // Ensure parent opens if child is open + newItems = markOpenItems(newItems, pathname) + const childOpen = newItems.some((child) => child.openImmediately) + openImmediately = openImmediately || childOpen || exactMatch // Ensure parent opens if child is open } else { - openImmediately = openImmediately || partialMatch; // Leaf items open on partial match + openImmediately = openImmediately || partialMatch // Leaf items open on partial match } return { ...item, items: newItems, openImmediately, - }; - }); -}; + } + }) +} -const renderItems = ({ collapse = false, depth = 0, items, pathname, category = "" }) => +const renderItems = ({ collapse = false, depth = 0, items, pathname, category = '' }) => items.reduce( (acc, item) => reduceChildRoutes({ acc, collapse, depth, item, pathname, category }), [] - ); + ) const reduceChildRoutes = ({ acc, collapse, depth, item, pathname, category }) => { - const checkPath = !!(item.path && pathname); - const exactMatch = checkPath && pathname === item.path; - const partialMatch = checkPath ? isPathPrefix(pathname, item.path) : false; + const checkPath = !!(item.path && pathname) + const exactMatch = checkPath && pathname === item.path + const partialMatch = checkPath ? isPathPrefix(pathname, item.path) : false - const hasChildren = item.items && item.items.length > 0; - const isActive = exactMatch || (partialMatch && !hasChildren); - const currentCategory = depth === 0 && item.type === "header" ? item.title : category; + const hasChildren = item.items && item.items.length > 0 + const isActive = exactMatch || (partialMatch && !hasChildren) + const currentCategory = depth === 0 && item.type === 'header' ? item.title : category if (hasChildren) { acc.push( @@ -80,7 +80,7 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname, category }) = component="ul" spacing={0.5} sx={{ - listStyle: "none", + listStyle: 'none', m: 0, p: 0, }} @@ -94,7 +94,7 @@ const reduceChildRoutes = ({ acc, collapse, depth, item, pathname, category }) = })} - ); + ) } else { acc.push( - ); + ) } - return acc; -}; + return acc +} export const SideNav = (props) => { - const { items, onPin, pinned = false } = props; - const pathname = usePathname(); - const [hovered, setHovered] = useState(false); - const collapse = !(pinned || hovered); - const { data: profile } = ApiGetCall({ url: "/api/me", queryKey: "authmecipp" }); - const settings = useSettings(); - const showSidebarBookmarks = settings.bookmarkSidebar !== false; - const paperRef = useRef(null); + const { items, onPin, pinned = false } = props + const pathname = usePathname() + const [hovered, setHovered] = useState(false) + const collapse = !(pinned || hovered) + const { data: profile } = ApiGetCall({ url: '/api/me', queryKey: 'authmecipp' }) + const settings = useSettings() + const showSidebarBookmarks = settings.bookmarkSidebar !== false + const paperRef = useRef(null) // Intercept wheel events on the side nav to fully isolate scroll. // preventDefault stops wheel events from reaching the main content, // and manual scrollTop has no momentum so it stops instantly when the cursor leaves. // Uses RAF-based easing to smooth out discrete mouse wheel jumps. useEffect(() => { - const el = paperRef.current; - if (!el) return; + const el = paperRef.current + if (!el) return - let targetScrollTop = el.scrollTop; - let animating = false; + let targetScrollTop = el.scrollTop + let animating = false const animate = () => { - const diff = targetScrollTop - el.scrollTop; + const diff = targetScrollTop - el.scrollTop if (Math.abs(diff) < 0.5) { - el.scrollTop = targetScrollTop; - animating = false; - return; + el.scrollTop = targetScrollTop + animating = false + return } - el.scrollTop += diff * 0.25; - requestAnimationFrame(animate); - }; + el.scrollTop += diff * 0.25 + requestAnimationFrame(animate) + } const handleWheel = (e) => { - e.preventDefault(); - const maxScroll = el.scrollHeight - el.clientHeight; - targetScrollTop = Math.max(0, Math.min(maxScroll, targetScrollTop + e.deltaY)); + e.preventDefault() + const maxScroll = el.scrollHeight - el.clientHeight + targetScrollTop = Math.max(0, Math.min(maxScroll, targetScrollTop + e.deltaY)) if (!animating) { - animating = true; - requestAnimationFrame(animate); + animating = true + requestAnimationFrame(animate) } - }; + } - el.addEventListener("wheel", handleWheel, { passive: false }); - return () => el.removeEventListener("wheel", handleWheel); - }, []); + el.addEventListener('wheel', handleWheel, { passive: false }) + return () => el.removeEventListener('wheel', handleWheel) + }, []) // Preprocess items to mark which should be open - const processedItems = markOpenItems(items, pathname); + const processedItems = markOpenItems(items, pathname) return ( <> {profile?.clientPrincipal && profile?.clientPrincipal?.userRoles?.length > 2 && ( @@ -174,13 +174,13 @@ export const SideNav = (props) => { onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), sx: { - backgroundColor: "background.default", + backgroundColor: 'background.default', height: `calc(100% - ${TOP_NAV_HEIGHT}px)`, - overflowX: "hidden", - overflowY: "auto", - scrollbarGutter: "stable", + overflowX: 'hidden', + overflowY: 'auto', + scrollbarGutter: 'stable', top: TOP_NAV_HEIGHT, - transition: "width 250ms ease-in-out", + transition: 'width 250ms ease-in-out', width: collapse ? SIDE_NAV_COLLAPSED_WIDTH : SIDE_NAV_WIDTH, zIndex: (theme) => theme.zIndex.appBar - 100, }, @@ -189,9 +189,9 @@ export const SideNav = (props) => { @@ -199,7 +199,7 @@ export const SideNav = (props) => { component="ul" sx={{ flexGrow: 1, - listStyle: "none", + listStyle: 'none', m: 0, p: 0, }} @@ -218,24 +218,24 @@ export const SideNav = (props) => { items: processedItems, pathname, })} - {" "} + {' '} {/* Add this closing tag */} {profile?.clientPrincipal && ( )} - {" "} + {' '} {/* Closing tag for the parent Box */} )} - ); -}; + ) +} SideNav.propTypes = { onPin: PropTypes.func, pinned: PropTypes.bool, -}; +} From 52a4763907144faafc70dfeb439c705679e24dc0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 13 May 2026 18:52:11 +0800 Subject: [PATCH 81/86] Nice CA policy editor and template creator/editor --- .../CippComponents/CippCAPolicyBuilder.jsx | 1121 +++++++++++++++++ .../CippTemplateFieldRenderer.jsx | 46 +- src/data/conditionalAccessSchema.json | 664 ++++++++++ .../tenant/conditional/list-policies/edit.jsx | 75 ++ .../tenant/conditional/list-policies/index.js | 7 + .../conditional/list-template/create.jsx | 39 + .../tenant/conditional/list-template/edit.jsx | 42 +- .../tenant/conditional/list-template/index.js | 11 +- 8 files changed, 1998 insertions(+), 7 deletions(-) create mode 100644 src/components/CippComponents/CippCAPolicyBuilder.jsx create mode 100644 src/data/conditionalAccessSchema.json create mode 100644 src/pages/tenant/conditional/list-policies/edit.jsx create mode 100644 src/pages/tenant/conditional/list-template/create.jsx diff --git a/src/components/CippComponents/CippCAPolicyBuilder.jsx b/src/components/CippComponents/CippCAPolicyBuilder.jsx new file mode 100644 index 000000000000..c7999edff6c6 --- /dev/null +++ b/src/components/CippComponents/CippCAPolicyBuilder.jsx @@ -0,0 +1,1121 @@ +import React, { useMemo, useCallback, useEffect } from "react"; +import { + Typography, + Divider, + Alert, + Chip, + Accordion, + AccordionSummary, + AccordionDetails, + Stack, + Tooltip, + IconButton, + Paper, +} from "@mui/material"; +import { Grid } from "@mui/system"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import WarningAmberIcon from "@mui/icons-material/WarningAmber"; +import { useWatch } from "react-hook-form"; +import CippFormComponent from "./CippFormComponent"; +import { CippFormCondition } from "./CippFormCondition"; +import caSchema from "../../data/conditionalAccessSchema.json"; +import gdapRoles from "../../data/GDAPRoles.json"; + +/** + * CippCAPolicyBuilder — A schema-driven Conditional Access policy builder. + * + * Renders structured form sections for every CA policy property, with: + * - Enum validation via the Microsoft Graph v1.0 schema + * - Friendly labels sourced from the schema's enumLabels + * - Licence requirement indicators (P2 for risk fields) + * - Grant control constraint validation (block vs other controls) + * - Accordion sections matching the Entra admin centre layout + * + * Props: + * formControl — react-hook-form's return from useForm() + * existingPolicy — optional JSON to pre-populate fields (edit mode) + * disabled — optional boolean to make the form read-only + */ + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** Resolve a $ref path like "#/$defs/conditionalAccessUsers" in the schema */ +function resolveRef(ref) { + if (!ref) return null; + const path = ref.replace("#/", "").split("/"); + let node = caSchema; + for (const segment of path) { + node = node?.[segment]; + } + return node ?? null; +} + +/** Convert schema enum + enumLabels into {label, value} options */ +function enumToOptions(schemaProp) { + if (!schemaProp) return []; + const enumVals = schemaProp.items?.enum ?? schemaProp.enum ?? []; + const labels = schemaProp.items?.enumLabels ?? schemaProp.enumLabels ?? {}; + return enumVals.map((v) => ({ + label: labels[v] ?? v, + value: v, + })); +} + +/** Build options from wellKnownValues or wellKnownDirectoryRoles */ +function wellKnownToOptions(values) { + if (!values) return []; + return Object.entries(values).map(([id, label]) => ({ label: `${label}`, value: id })); +} + +/** Build special-value options from schema metadata */ +function specialValueOptions(schemaProp) { + const vals = schemaProp?.specialValues ?? []; + const labels = schemaProp?.specialValueLabels ?? {}; + return vals.map((v) => ({ label: labels[v] ?? v, value: v })); +} + +// --------------------------------------------------------------------------- +// Sub-section renderers +// --------------------------------------------------------------------------- + +function SectionHeader({ title, description, requiresLicense, icon }) { + return ( + + {icon} + {title} + {requiresLicense && ( + + + + )} + {description && ( + + + + + + )} + + ); +} + +// --------------------------------------------------------------------------- +// Users & Groups section +// --------------------------------------------------------------------------- +function UsersSection({ formControl, disabled, prefix = "conditions.users" }) { + const schemaDef = resolveRef("#/$defs/conditionalAccessUsers"); + const guestSchema = resolveRef("#/$defs/conditionalAccessGuestsOrExternalUsers"); + const roleOptions = useMemo( + () => gdapRoles.map((r) => ({ label: r.Name, value: r.ObjectId })), + [] + ); + const specialUserOpts = useMemo( + () => specialValueOptions(schemaDef?.properties?.includeUsers), + [schemaDef] + ); + + const guestTypeOpts = useMemo(() => { + const prop = guestSchema?.properties?.guestOrExternalUserTypes; + const flags = prop?.flagEnum ?? []; + const labels = prop?.flagEnumLabels ?? {}; + return flags + .filter((f) => f !== "none") + .map((f) => ({ label: labels[f] ?? f, value: f })); + }, [guestSchema]); + + return ( + + {/* Include users */} + + + + {/* Exclude users */} + + + + {/* Include groups */} + + + + {/* Exclude groups */} + + + + {/* Include roles */} + + + + {/* Exclude roles */} + + + + + {/* Guest / External User Exclusions */} + + + + Exclude Guests or External Users + + + + + + + Select one or more external user types to exclude from this policy. + + + + + + + Choose whether the exclusion applies to all external tenants or specific ones. Only + relevant for external user types (not internal guests). + + + + + + + Enter the tenant IDs to scope this exclusion to (e.g. your partner tenant ID for + service provider exclusion). + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Applications section +// --------------------------------------------------------------------------- +function ApplicationsSection({ formControl, disabled, prefix = "conditions.applications" }) { + const schemaDef = resolveRef("#/$defs/conditionalAccessApplications"); + const includeAppOpts = useMemo( + () => specialValueOptions(schemaDef?.properties?.includeApplications), + [schemaDef] + ); + const userActionOpts = useMemo( + () => enumToOptions(schemaDef?.properties?.includeUserActions), + [schemaDef] + ); + + return ( + + + + + + + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Conditions (client apps, platforms, locations, risk, etc.) +// --------------------------------------------------------------------------- +function ConditionsSection({ formControl, disabled }) { + const condSchema = resolveRef("#/$defs/conditionalAccessConditionSet"); + const platformSchema = resolveRef("#/$defs/conditionalAccessPlatforms"); + const authFlowSchema = resolveRef("#/$defs/conditionalAccessAuthenticationFlows"); + + const clientAppOpts = useMemo( + () => enumToOptions(condSchema?.properties?.clientAppTypes), + [condSchema] + ); + const includePlatOpts = useMemo( + () => enumToOptions(platformSchema?.properties?.includePlatforms), + [platformSchema] + ); + const excludePlatOpts = useMemo( + () => enumToOptions(platformSchema?.properties?.excludePlatforms), + [platformSchema] + ); + const signInRiskOpts = useMemo( + () => enumToOptions(condSchema?.properties?.signInRiskLevels), + [condSchema] + ); + const userRiskOpts = useMemo( + () => enumToOptions(condSchema?.properties?.userRiskLevels), + [condSchema] + ); + const spRiskOpts = useMemo( + () => enumToOptions(condSchema?.properties?.servicePrincipalRiskLevels), + [condSchema] + ); + const insiderRiskOpts = useMemo( + () => enumToOptions(condSchema?.properties?.insiderRiskLevels), + [condSchema] + ); + const authFlowOpts = useMemo( + () => enumToOptions(authFlowSchema?.properties?.transferMethods), + [authFlowSchema] + ); + + const locationSchema = resolveRef("#/$defs/conditionalAccessLocations"); + const includeLocOpts = useMemo( + () => specialValueOptions(locationSchema?.properties?.includeLocations), + [locationSchema] + ); + const excludeLocOpts = useMemo( + () => specialValueOptions(locationSchema?.properties?.excludeLocations), + [locationSchema] + ); + + return ( + + {/* Client app types */} + + + + + {/* Platforms */} + + + + + + + + {/* Locations */} + + + + + + + + {/* Device filter */} + + + + Device Filter + + + + + + + + + + + {/* Risk levels */} + + + + + Risk Levels + + + + + + + + + + + + + + + + {/* Insider risk */} + + + + + {/* Auth flows */} + + + + + ); +} + +// --------------------------------------------------------------------------- +// Grant Controls section +// --------------------------------------------------------------------------- +function GrantControlsSection({ formControl, disabled }) { + const grantSchema = resolveRef("#/$defs/conditionalAccessGrantControls"); + const operatorOpts = useMemo( + () => enumToOptions(grantSchema?.properties?.operator), + [grantSchema] + ); + const builtInOpts = useMemo( + () => enumToOptions(grantSchema?.properties?.builtInControls), + [grantSchema] + ); + + const authStrengthSchema = resolveRef("#/$defs/authenticationStrengthPolicy"); + const authStrengthOpts = useMemo( + () => wellKnownToOptions(authStrengthSchema?.properties?.id?.wellKnownValues), + [authStrengthSchema] + ); + + const selectedControls = useWatch({ + control: formControl.control, + name: "grantControls.builtInControls", + }); + + const hasBlock = useMemo(() => { + if (!selectedControls) return false; + return (Array.isArray(selectedControls) ? selectedControls : [selectedControls]).some( + (c) => (c?.value ?? c) === "block" + ); + }, [selectedControls]); + + return ( + + + + + + + {hasBlock && ( + + "Block access" cannot be combined with other grant controls. All other + selections will be ignored by Entra ID. + + )} + + + + + + + + + ); +} + +// --------------------------------------------------------------------------- +// Session Controls section +// --------------------------------------------------------------------------- +function SessionControlsSection({ formControl, disabled }) { + const casSchema = resolveRef("#/$defs/cloudAppSecuritySessionControl"); + const casTypeOpts = useMemo( + () => enumToOptions(casSchema?.properties?.cloudAppSecurityType), + [casSchema] + ); + + const signinSchema = resolveRef("#/$defs/signInFrequencySessionControl"); + const freqTypeOpts = useMemo( + () => enumToOptions(signinSchema?.properties?.type), + [signinSchema] + ); + const freqIntervalOpts = useMemo( + () => enumToOptions(signinSchema?.properties?.frequencyInterval), + [signinSchema] + ); + const freqAuthTypeOpts = useMemo( + () => enumToOptions(signinSchema?.properties?.authenticationType), + [signinSchema] + ); + + const persistSchema = resolveRef("#/$defs/persistentBrowserSessionControl"); + const persistModeOpts = useMemo( + () => enumToOptions(persistSchema?.properties?.mode), + [persistSchema] + ); + + return ( + + {/* App enforced restrictions */} + + + Application Enforced Restrictions + + + Only Exchange Online and SharePoint Online support this control. + + + + + + + {/* Cloud App Security */} + + + Conditional Access App Control + + + + + + + + + {/* Sign-in frequency */} + + + Sign-in Frequency + + + + + + + + + + + + + + + + + + {/* Persistent browser */} + + + Persistent Browser Session + + + + + + + + + {/* Resilience defaults */} + + + + + + + When enabled, Entra ID will not extend existing sessions during outages. + + + + ); +} + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- +const CippCAPolicyBuilder = ({ formControl, existingPolicy, disabled = false }) => { + const policySchema = caSchema; + + // Pre-populate form from existing policy when editing + useEffect(() => { + if (existingPolicy && formControl) { + const populate = (obj, prefix = "") => { + if (!obj || typeof obj !== "object") return; + Object.entries(obj).forEach(([key, value]) => { + // Skip read-only / OData / internal / Graph metadata properties + if ( + (key === "id" && !prefix) || // Only skip top-level policy id + key === "createdDateTime" || + key === "modifiedDateTime" || + key === "deletedDateTime" || + key === "templateId" || + key === "partialEnablementStrategy" || + key.includes("@odata") || // Catch both @odata.type and includePlatforms@odata.type + key.startsWith("#") || // Catch #microsoft.graph.restore etc. + key === "GUID" || + key === "source" || + key === "isSynced" || + key === "package" + ) { + return; + } + // Skip null, empty arrays, and empty strings — treat as "not set" + if (value === null || value === undefined) return; + if (Array.isArray(value) && value.length === 0) return; + if (typeof value === "string" && value.trim() === "") return; + + const path = prefix ? `${prefix}.${key}` : key; + + // Special handling for authenticationStrength — only extract the policy ID, + // not the full expanded object (displayName, description, allowedCombinations, etc.) + if (key === "authenticationStrength" && typeof value === "object" && !Array.isArray(value)) { + if (value.id) { + formControl.setValue(`${path}.id`, value.id); + } + return; + } + + // Special handling for guestOrExternalUserTypes — Graph stores as comma-separated + // string but our form uses a multi-select array + if (key === "guestOrExternalUserTypes" && typeof value === "string") { + const types = value.split(",").filter((t) => t.trim() !== "" && t !== "none"); + if (types.length > 0) { + formControl.setValue(path, types); + } + return; + } + + // Special handling for externalTenants — extract members and set _scope + if (key === "externalTenants" && typeof value === "object" && !Array.isArray(value)) { + if (value.members && Array.isArray(value.members) && value.members.length > 0) { + formControl.setValue(`${path}.members`, value.members); + formControl.setValue(`${path}._scope`, { label: "Specific tenants", value: "enumerated" }); + } else { + formControl.setValue(`${path}._scope`, { label: "All external tenants", value: "all" }); + } + return; + } + + if (typeof value === "object" && !Array.isArray(value)) { + populate(value, path); + } else { + formControl.setValue(path, value); + } + }); + }; + populate(existingPolicy); + } + }, [existingPolicy, formControl]); + + // Schema-level validation: extract options for top-level policy state + const stateOpts = useMemo( + () => enumToOptions(policySchema.properties.state), + [policySchema] + ); + + return ( + + {/* Policy basics */} + + + + + + + + + + + + + {/* Users & Groups */} + + }> + + Users and Groups + + + + + + + + {/* Cloud Apps or Actions */} + + }> + + Cloud Apps or Actions + + + + + + + + {/* Conditions */} + + }> + + Conditions + + + + + + + + {/* Grant Controls */} + + }> + + Grant Controls + + + + + + + + {/* Session Controls */} + + }> + + Session Controls + + + + + + + + ); +}; + +export default CippCAPolicyBuilder; + +/** + * Utility: extract a clean CA policy JSON from react-hook-form values. + * + * Call this in your form's submit handler to strip out { label, value } + * wrapper objects from autoComplete fields, remove empty/null branches, + * and ensure the JSON is ready to send to AddCAPolicy / AddCATemplate. + */ +export function extractCAPolicyJSON(formValues) { + const clean = (obj) => { + if (obj === null || obj === undefined) return undefined; + + // Unwrap {label,value} from autoComplete + if (typeof obj === "object" && "value" in obj && "label" in obj) { + return obj.value; + } + + if (Array.isArray(obj)) { + const arr = obj.map(clean).filter((v) => v !== undefined && v !== null && v !== ""); + return arr.length > 0 ? arr : undefined; + } + + if (typeof obj === "object") { + const result = {}; + let hasContent = false; + for (const [key, value] of Object.entries(obj)) { + // Strip internal builder fields (e.g. _scope) + if (key.startsWith("_")) continue; + // Strip OData annotations EXCEPT @odata.type (required by Graph for polymorphic types) + if (key === "@odata.type") { + result[key] = value; + hasContent = true; + continue; + } + if (key.includes("@odata") || key.startsWith("#")) continue; + + const cleaned = clean(value); + if (cleaned !== undefined) { + result[key] = cleaned; + hasContent = true; + } + } + return hasContent ? result : undefined; + } + + // Booleans, numbers, non-empty strings pass through + if (typeof obj === "string" && obj.trim() === "") return undefined; + return obj; + }; + + const cleaned = clean(formValues) ?? {}; + + // Post-process: fix guestsOrExternalUsers structures for Graph API + const fixGuestExternalUsers = (guestObj) => { + if (!guestObj) return guestObj; + // Graph expects guestOrExternalUserTypes as a comma-separated string + if (Array.isArray(guestObj.guestOrExternalUserTypes)) { + guestObj.guestOrExternalUserTypes = guestObj.guestOrExternalUserTypes.join(","); + } + // Determine scope from the internal _scope field or from members presence + const scope = guestObj.externalTenants?._scope; + const hasMembers = + guestObj.externalTenants?.members && guestObj.externalTenants.members.length > 0; + + if (guestObj.externalTenants) { + // Remove internal _scope field + delete guestObj.externalTenants._scope; + + if (scope === "enumerated" || hasMembers) { + guestObj.externalTenants["@odata.type"] = + "#microsoft.graph.conditionalAccessEnumeratedExternalTenants"; + guestObj.externalTenants.membershipKind = "enumerated"; + } else { + guestObj.externalTenants = { + "@odata.type": "#microsoft.graph.conditionalAccessAllExternalTenants", + membershipKind: "all", + }; + } + } else if (guestObj.guestOrExternalUserTypes) { + // No tenants specified — default to all external tenants + guestObj.externalTenants = { + "@odata.type": "#microsoft.graph.conditionalAccessAllExternalTenants", + membershipKind: "all", + }; + } + return guestObj; + }; + + if (cleaned.conditions?.users?.excludeGuestsOrExternalUsers) { + cleaned.conditions.users.excludeGuestsOrExternalUsers = fixGuestExternalUsers( + cleaned.conditions.users.excludeGuestsOrExternalUsers + ); + } + if (cleaned.conditions?.users?.includeGuestsOrExternalUsers) { + cleaned.conditions.users.includeGuestsOrExternalUsers = fixGuestExternalUsers( + cleaned.conditions.users.includeGuestsOrExternalUsers + ); + } + + return cleaned; +} diff --git a/src/components/CippComponents/CippTemplateFieldRenderer.jsx b/src/components/CippComponents/CippTemplateFieldRenderer.jsx index 1757e400acda..8ecef26973eb 100644 --- a/src/components/CippComponents/CippTemplateFieldRenderer.jsx +++ b/src/components/CippComponents/CippTemplateFieldRenderer.jsx @@ -244,11 +244,43 @@ const CippTemplateFieldRenderer = ({ React.useEffect(() => { if (templateData && formControl) { const processedData = parseIntuneRawJson(templateData); - const formValues = {}; + // Recursively strip null values, empty arrays, empty strings, + // and @odata / Graph metadata keys so they don't create blank + // form fields or phantom sections in the builder. + const stripEmpty = (obj) => { + if (obj === null || obj === undefined) return undefined; + if (typeof obj === "string" && obj.trim() === "") return undefined; + if (Array.isArray(obj)) { + const filtered = obj + .map(stripEmpty) + .filter((v) => v !== undefined && v !== null); + return filtered.length > 0 ? filtered : undefined; + } + if (typeof obj === "object") { + const result = {}; + let hasContent = false; + for (const [k, v] of Object.entries(obj)) { + // Drop @odata annotations and Graph metadata + if (k.includes("@odata") || k.startsWith("#")) continue; + const cleaned = stripEmpty(v); + if (cleaned !== undefined) { + result[k] = cleaned; + hasContent = true; + } + } + return hasContent ? result : undefined; + } + return obj; + }; + + const formValues = {}; Object.keys(processedData).forEach((key) => { if (!isFieldBlacklisted(key)) { - formValues[key] = processedData[key]; + const cleaned = stripEmpty(processedData[key]); + if (cleaned !== undefined) { + formValues[key] = cleaned; + } } }); formControl.reset(formValues); @@ -258,6 +290,10 @@ const CippTemplateFieldRenderer = ({ const renderFormField = (key, value, path = "") => { const fieldPath = path ? `${path}.${key}` : key; + // Skip null/undefined values and @odata / metadata keys + if (value === null || value === undefined) return null; + if (key.includes("@odata") || key.startsWith("#")) return null; + if (isFieldBlacklisted(key)) { return null; } @@ -776,12 +812,16 @@ const CippTemplateFieldRenderer = ({ {priorityFields.map( (fieldName) => processedData[fieldName] !== undefined && + processedData[fieldName] !== null && renderFormField(fieldName, processedData[fieldName]) )} {/* Render all other fields except priority fields */} {Object.entries(processedData) - .filter(([key]) => !priorityFields.includes(key)) + .filter( + ([key, value]) => + !priorityFields.includes(key) && value !== null && value !== undefined + ) .map(([key, value]) => renderFormField(key, value))} ); diff --git a/src/data/conditionalAccessSchema.json b/src/data/conditionalAccessSchema.json new file mode 100644 index 000000000000..e6161261854e --- /dev/null +++ b/src/data/conditionalAccessSchema.json @@ -0,0 +1,664 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "microsoft.graph.conditionalAccessPolicy", + "title": "Conditional Access Policy", + "description": "Schema derived from the Microsoft Graph v1.0 conditionalAccessPolicy resource type. Reference: https://learn.microsoft.com/en-us/graph/api/resources/conditionalaccesspolicy?view=graph-rest-1.0", + "type": "object", + "required": ["displayName", "state", "conditions"], + "properties": { + "displayName": { + "type": "string", + "title": "Display Name", + "description": "A display name for the policy.", + "minLength": 1, + "maxLength": 256 + }, + "state": { + "type": "string", + "title": "Policy State", + "description": "The state of the policy.", + "enum": ["enabled", "disabled", "enabledForReportingButNotEnforced"], + "enumLabels": { + "enabled": "Enabled", + "disabled": "Disabled", + "enabledForReportingButNotEnforced": "Report-only" + } + }, + "conditions": { + "$ref": "#/$defs/conditionalAccessConditionSet" + }, + "grantControls": { + "$ref": "#/$defs/conditionalAccessGrantControls" + }, + "sessionControls": { + "$ref": "#/$defs/conditionalAccessSessionControls" + } + }, + "$defs": { + "conditionalAccessConditionSet": { + "type": "object", + "title": "Conditions", + "description": "Rules that must be met for the policy to apply.", + "required": ["users", "applications", "clientAppTypes"], + "properties": { + "users": { + "$ref": "#/$defs/conditionalAccessUsers" + }, + "applications": { + "$ref": "#/$defs/conditionalAccessApplications" + }, + "clientAppTypes": { + "type": "array", + "title": "Client App Types", + "description": "Client application types included in the policy.", + "items": { + "type": "string", + "enum": ["all", "browser", "mobileAppsAndDesktopClients", "exchangeActiveSync", "easSupported", "other"] + }, + "enumLabels": { + "all": "All", + "browser": "Browser", + "mobileAppsAndDesktopClients": "Mobile apps and desktop clients", + "exchangeActiveSync": "Exchange ActiveSync", + "easSupported": "EAS supported", + "other": "Other clients" + } + }, + "platforms": { + "$ref": "#/$defs/conditionalAccessPlatforms" + }, + "locations": { + "$ref": "#/$defs/conditionalAccessLocations" + }, + "devices": { + "$ref": "#/$defs/conditionalAccessDevices" + }, + "clientApplications": { + "$ref": "#/$defs/conditionalAccessClientApplications" + }, + "signInRiskLevels": { + "type": "array", + "title": "Sign-in Risk Levels", + "description": "Sign-in risk levels included in the policy. Requires Entra ID P2.", + "items": { + "type": "string", + "enum": ["low", "medium", "high", "hidden", "none"] + }, + "enumLabels": { + "low": "Low", + "medium": "Medium", + "high": "High", + "hidden": "Hidden", + "none": "No risk" + }, + "requiresLicense": "AAD_PREMIUM_P2" + }, + "userRiskLevels": { + "type": "array", + "title": "User Risk Levels", + "description": "User risk levels included in the policy. Requires Entra ID P2.", + "items": { + "type": "string", + "enum": ["low", "medium", "high", "hidden", "none"] + }, + "enumLabels": { + "low": "Low", + "medium": "Medium", + "high": "High", + "hidden": "Hidden", + "none": "No risk" + }, + "requiresLicense": "AAD_PREMIUM_P2" + }, + "servicePrincipalRiskLevels": { + "type": "array", + "title": "Service Principal Risk Levels", + "description": "Service principal risk levels included in the policy.", + "items": { + "type": "string", + "enum": ["low", "medium", "high", "none"] + }, + "enumLabels": { + "low": "Low", + "medium": "Medium", + "high": "High", + "none": "No risk" + } + }, + "insiderRiskLevels": { + "type": "string", + "title": "Insider Risk Levels", + "description": "Insider risk levels included in the policy.", + "enum": ["minor", "moderate", "elevated"], + "enumLabels": { + "minor": "Minor", + "moderate": "Moderate", + "elevated": "Elevated" + } + }, + "authenticationFlows": { + "$ref": "#/$defs/conditionalAccessAuthenticationFlows" + } + } + }, + "conditionalAccessUsers": { + "type": "object", + "title": "Users and Groups", + "description": "Users, groups, and roles included in and excluded from the policy.", + "properties": { + "includeUsers": { + "type": "array", + "title": "Include Users", + "description": "User IDs in scope, or 'All', 'None', 'GuestsOrExternalUsers'.", + "items": { "type": "string" }, + "specialValues": ["All", "None", "GuestsOrExternalUsers"], + "graphLookup": "users" + }, + "excludeUsers": { + "type": "array", + "title": "Exclude Users", + "description": "User IDs excluded from scope.", + "items": { "type": "string" }, + "specialValues": ["GuestsOrExternalUsers"], + "graphLookup": "users" + }, + "includeGroups": { + "type": "array", + "title": "Include Groups", + "description": "Group IDs in scope of the policy.", + "items": { "type": "string" }, + "graphLookup": "groups" + }, + "excludeGroups": { + "type": "array", + "title": "Exclude Groups", + "description": "Group IDs excluded from the policy.", + "items": { "type": "string" }, + "graphLookup": "groups" + }, + "includeRoles": { + "type": "array", + "title": "Include Roles", + "description": "Directory role IDs in scope of the policy.", + "items": { "type": "string" }, + "graphLookup": "directoryRoles" + }, + "excludeRoles": { + "type": "array", + "title": "Exclude Roles", + "description": "Directory role IDs excluded from the policy.", + "items": { "type": "string" }, + "graphLookup": "directoryRoles" + }, + "includeGuestsOrExternalUsers": { + "$ref": "#/$defs/conditionalAccessGuestsOrExternalUsers" + }, + "excludeGuestsOrExternalUsers": { + "$ref": "#/$defs/conditionalAccessGuestsOrExternalUsers" + } + } + }, + "conditionalAccessGuestsOrExternalUsers": { + "type": "object", + "title": "Guests or External Users", + "description": "Internal guests or external user types.", + "properties": { + "guestOrExternalUserTypes": { + "type": "string", + "title": "Guest or External User Types", + "description": "Multi-valued flags. Combine with commas.", + "flagEnum": ["none", "internalGuest", "b2bCollaborationGuest", "b2bCollaborationMember", "b2bDirectConnectUser", "otherExternalUser", "serviceProvider"], + "flagEnumLabels": { + "none": "None", + "internalGuest": "Internal guest", + "b2bCollaborationGuest": "B2B collaboration guest", + "b2bCollaborationMember": "B2B collaboration member", + "b2bDirectConnectUser": "B2B direct connect user", + "otherExternalUser": "Other external user", + "serviceProvider": "Service provider" + } + }, + "externalTenants": { + "$ref": "#/$defs/conditionalAccessExternalTenants" + } + } + }, + "conditionalAccessExternalTenants": { + "type": "object", + "title": "External Tenants", + "description": "External tenant scope.", + "properties": { + "@odata.type": { + "type": "string", + "title": "Membership Kind", + "description": "Whether to enumerate or specify all tenants.", + "enum": [ + "#microsoft.graph.conditionalAccessAllExternalTenants", + "#microsoft.graph.conditionalAccessEnumeratedExternalTenants" + ], + "enumLabels": { + "#microsoft.graph.conditionalAccessAllExternalTenants": "All external tenants", + "#microsoft.graph.conditionalAccessEnumeratedExternalTenants": "Specific tenants" + } + }, + "members": { + "type": "array", + "title": "Tenant IDs", + "description": "List of tenant IDs when using enumerated membership.", + "items": { "type": "string" }, + "visibleWhen": { + "field": "@odata.type", + "value": "#microsoft.graph.conditionalAccessEnumeratedExternalTenants" + } + } + } + }, + "conditionalAccessApplications": { + "type": "object", + "title": "Cloud Apps or Actions", + "description": "Applications and user actions included in and excluded from the policy.", + "properties": { + "includeApplications": { + "type": "array", + "title": "Include Applications", + "description": "Application client IDs the policy applies to, or 'All', 'Office365', 'MicrosoftAdminPortals'.", + "items": { "type": "string" }, + "specialValues": ["All", "Office365", "MicrosoftAdminPortals"], + "specialValueLabels": { + "All": "All cloud apps", + "Office365": "Office 365", + "MicrosoftAdminPortals": "Microsoft Admin Portals" + }, + "graphLookup": "servicePrincipals" + }, + "excludeApplications": { + "type": "array", + "title": "Exclude Applications", + "description": "Application client IDs explicitly excluded.", + "items": { "type": "string" }, + "graphLookup": "servicePrincipals" + }, + "includeUserActions": { + "type": "array", + "title": "User Actions", + "description": "User actions to include instead of cloud apps.", + "items": { + "type": "string", + "enum": ["urn:user:registersecurityinfo", "urn:user:registerdevice"] + }, + "enumLabels": { + "urn:user:registersecurityinfo": "Register security information", + "urn:user:registerdevice": "Register or join devices" + } + }, + "includeAuthenticationContextClassReferences": { + "type": "array", + "title": "Authentication Context", + "description": "Authentication context class references included.", + "items": { "type": "string" } + }, + "applicationFilter": { + "$ref": "#/$defs/conditionalAccessFilter", + "title": "Application Filter", + "description": "Dynamic filter rule for applications." + } + } + }, + "conditionalAccessPlatforms": { + "type": "object", + "title": "Device Platforms", + "description": "Device platforms included in and excluded from the policy.", + "properties": { + "includePlatforms": { + "type": "array", + "title": "Include Platforms", + "description": "Platforms the policy applies to.", + "items": { + "type": "string", + "enum": ["android", "iOS", "windows", "windowsPhone", "macOS", "linux", "all"] + }, + "enumLabels": { + "android": "Android", + "iOS": "iOS", + "windows": "Windows", + "windowsPhone": "Windows Phone", + "macOS": "macOS", + "linux": "Linux", + "all": "All platforms" + } + }, + "excludePlatforms": { + "type": "array", + "title": "Exclude Platforms", + "description": "Platforms excluded from the policy.", + "items": { + "type": "string", + "enum": ["android", "iOS", "windows", "windowsPhone", "macOS", "linux"] + }, + "enumLabels": { + "android": "Android", + "iOS": "iOS", + "windows": "Windows", + "windowsPhone": "Windows Phone", + "macOS": "macOS", + "linux": "Linux" + } + } + } + }, + "conditionalAccessLocations": { + "type": "object", + "title": "Locations", + "description": "Locations included in and excluded from the policy.", + "properties": { + "includeLocations": { + "type": "array", + "title": "Include Locations", + "description": "Named location IDs or 'All', 'AllTrusted'.", + "items": { "type": "string" }, + "specialValues": ["All", "AllTrusted"], + "specialValueLabels": { + "All": "Any location", + "AllTrusted": "All trusted locations" + }, + "graphLookup": "namedLocations" + }, + "excludeLocations": { + "type": "array", + "title": "Exclude Locations", + "description": "Named location IDs excluded.", + "items": { "type": "string" }, + "specialValues": ["AllTrusted"], + "specialValueLabels": { + "AllTrusted": "All trusted locations" + }, + "graphLookup": "namedLocations" + } + } + }, + "conditionalAccessDevices": { + "type": "object", + "title": "Devices", + "description": "Device filter for the policy.", + "properties": { + "deviceFilter": { + "$ref": "#/$defs/conditionalAccessFilter", + "title": "Device Filter", + "description": "Dynamic filter rule for devices using device properties." + } + } + }, + "conditionalAccessFilter": { + "type": "object", + "title": "Filter", + "description": "Dynamic filter with rule syntax.", + "properties": { + "mode": { + "type": "string", + "title": "Filter Mode", + "description": "Whether to include or exclude matching items.", + "enum": ["include", "exclude"], + "enumLabels": { + "include": "Include filtered items", + "exclude": "Exclude filtered items" + } + }, + "rule": { + "type": "string", + "title": "Filter Rule", + "description": "Dynamic membership rule expression. Syntax matches Entra ID dynamic group rules." + } + }, + "required": ["mode", "rule"] + }, + "conditionalAccessClientApplications": { + "type": "object", + "title": "Workload Identities", + "description": "Service principals and workload identities included in and excluded from the policy.", + "properties": { + "includeServicePrincipals": { + "type": "array", + "title": "Include Service Principals", + "description": "Service principal IDs, or 'ServicePrincipalsInMyTenant'.", + "items": { "type": "string" }, + "specialValues": ["ServicePrincipalsInMyTenant"], + "specialValueLabels": { + "ServicePrincipalsInMyTenant": "All service principals" + } + }, + "excludeServicePrincipals": { + "type": "array", + "title": "Exclude Service Principals", + "description": "Service principal IDs excluded.", + "items": { "type": "string" } + }, + "servicePrincipalFilter": { + "$ref": "#/$defs/conditionalAccessFilter", + "title": "Service Principal Filter", + "description": "Dynamic filter rule for service principals." + } + } + }, + "conditionalAccessAuthenticationFlows": { + "type": "object", + "title": "Authentication Flows", + "description": "Authentication flow types in scope.", + "properties": { + "transferMethods": { + "type": "string", + "title": "Transfer Methods", + "description": "Transfer methods in scope for the policy.", + "enum": ["none", "deviceCodeFlow", "authenticationTransfer"], + "enumLabels": { + "none": "None", + "deviceCodeFlow": "Device code flow", + "authenticationTransfer": "Authentication transfer" + } + } + } + }, + "conditionalAccessGrantControls": { + "type": "object", + "title": "Grant Controls", + "description": "Grant controls that must be fulfilled to pass the policy.", + "properties": { + "operator": { + "type": "string", + "title": "Control Operator", + "description": "How multiple controls relate to each other.", + "enum": ["AND", "OR"], + "enumLabels": { + "AND": "Require all selected controls", + "OR": "Require one of the selected controls" + } + }, + "builtInControls": { + "type": "array", + "title": "Built-in Controls", + "description": "Built-in grant controls required by the policy.", + "items": { + "type": "string", + "enum": ["block", "mfa", "compliantDevice", "domainJoinedDevice", "approvedApplication", "compliantApplication", "passwordChange", "riskRemediation"] + }, + "enumLabels": { + "block": "Block access", + "mfa": "Require multifactor authentication", + "compliantDevice": "Require device to be marked as compliant", + "domainJoinedDevice": "Require Microsoft Entra hybrid joined device", + "approvedApplication": "Require approved client app", + "compliantApplication": "Require app protection policy", + "passwordChange": "Require password change", + "riskRemediation": "Require risk remediation" + }, + "constraints": { + "mutuallyExclusive": [["block"], ["mfa", "compliantDevice", "domainJoinedDevice", "approvedApplication", "compliantApplication", "passwordChange", "riskRemediation"]], + "passwordChangeRequires": ["mfa"], + "riskRemediationExcludes": ["passwordChange"] + } + }, + "customAuthenticationFactors": { + "type": "array", + "title": "Custom Controls", + "description": "Custom control IDs required by the policy.", + "items": { "type": "string" } + }, + "termsOfUse": { + "type": "array", + "title": "Terms of Use", + "description": "Terms of use IDs required by the policy.", + "items": { "type": "string" } + }, + "authenticationStrength": { + "$ref": "#/$defs/authenticationStrengthPolicy" + } + } + }, + "authenticationStrengthPolicy": { + "type": "object", + "title": "Authentication Strength", + "description": "Authentication strength policy required. Use instead of or alongside builtInControls.", + "properties": { + "id": { + "type": "string", + "title": "Authentication Strength Policy", + "description": "ID of the authentication strength policy.", + "graphLookup": "authenticationStrengthPolicies", + "wellKnownValues": { + "00000000-0000-0000-0000-000000000002": "Multifactor authentication", + "00000000-0000-0000-0000-000000000003": "Passwordless MFA", + "00000000-0000-0000-0000-000000000004": "Phishing-resistant MFA" + } + } + } + }, + "conditionalAccessSessionControls": { + "type": "object", + "title": "Session Controls", + "description": "Session controls enforced after sign-in.", + "properties": { + "applicationEnforcedRestrictions": { + "$ref": "#/$defs/applicationEnforcedRestrictionsSessionControl" + }, + "cloudAppSecurity": { + "$ref": "#/$defs/cloudAppSecuritySessionControl" + }, + "signInFrequency": { + "$ref": "#/$defs/signInFrequencySessionControl" + }, + "persistentBrowser": { + "$ref": "#/$defs/persistentBrowserSessionControl" + }, + "disableResilienceDefaults": { + "type": "boolean", + "title": "Disable Resilience Defaults", + "description": "When true, Entra ID will not extend existing sessions based on information collected prior to an outage." + } + } + }, + "applicationEnforcedRestrictionsSessionControl": { + "type": "object", + "title": "App Enforced Restrictions", + "description": "Enforce application restrictions. Only Exchange Online and SharePoint Online support this.", + "properties": { + "isEnabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether application enforced restrictions are enabled." + } + } + }, + "cloudAppSecuritySessionControl": { + "type": "object", + "title": "Conditional Access App Control", + "description": "Apply Defender for Cloud Apps controls.", + "properties": { + "isEnabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether cloud app security control is enabled." + }, + "cloudAppSecurityType": { + "type": "string", + "title": "Control Type", + "description": "Type of cloud app security enforcement.", + "enum": ["mcasConfigured", "monitorOnly", "blockDownloads"], + "enumLabels": { + "mcasConfigured": "Use custom policy", + "monitorOnly": "Monitor only", + "blockDownloads": "Block downloads" + } + } + } + }, + "signInFrequencySessionControl": { + "type": "object", + "title": "Sign-in Frequency", + "description": "Enforce periodic reauthentication.", + "properties": { + "isEnabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether sign-in frequency control is enabled." + }, + "value": { + "type": "integer", + "title": "Frequency Value", + "description": "Number of hours or days.", + "minimum": 1 + }, + "type": { + "type": "string", + "title": "Frequency Unit", + "description": "Unit of the sign-in frequency.", + "enum": ["hours", "days"], + "enumLabels": { + "hours": "Hours", + "days": "Days" + } + }, + "frequencyInterval": { + "type": "string", + "title": "Frequency Interval", + "description": "Whether frequency is time-based or every sign-in.", + "enum": ["timeBased", "everyTime"], + "enumLabels": { + "timeBased": "Time-based (use value/type above)", + "everyTime": "Every time" + } + }, + "authenticationType": { + "type": "string", + "title": "Authentication Type", + "description": "Which authentication types this applies to.", + "enum": ["primaryAndSecondaryAuthentication", "secondaryAuthentication"], + "enumLabels": { + "primaryAndSecondaryAuthentication": "Primary and secondary authentication", + "secondaryAuthentication": "Secondary authentication only" + } + } + } + }, + "persistentBrowserSessionControl": { + "type": "object", + "title": "Persistent Browser Session", + "description": "Whether to persist cookies after browser close.", + "properties": { + "isEnabled": { + "type": "boolean", + "title": "Enabled", + "description": "Whether persistent browser session control is enabled." + }, + "mode": { + "type": "string", + "title": "Mode", + "description": "Whether browser sessions should always or never persist.", + "enum": ["always", "never"], + "enumLabels": { + "always": "Always persistent", + "never": "Never persistent" + } + } + } + } + } +} diff --git a/src/pages/tenant/conditional/list-policies/edit.jsx b/src/pages/tenant/conditional/list-policies/edit.jsx new file mode 100644 index 000000000000..156cd39f12b2 --- /dev/null +++ b/src/pages/tenant/conditional/list-policies/edit.jsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from "react"; +import { Alert, Box } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { useRouter } from "next/router"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import CippFormSkeleton from "../../../../components/CippFormPages/CippFormSkeleton"; +import { ApiGetCall } from "../../../../api/ApiCall"; +import CippCAPolicyBuilder, { + extractCAPolicyJSON, +} from "../../../../components/CippComponents/CippCAPolicyBuilder"; +import { useSettings } from "../../../../hooks/use-settings.js"; + +const EditCAPolicy = () => { + const router = useRouter(); + const { id: policyId } = router.query; + const tenantFilter = useSettings()?.currentTenant; + const [policyData, setPolicyData] = useState(null); + + const formControl = useForm({ mode: "onChange" }); + + // Fetch the current policies for this tenant + const policiesQuery = ApiGetCall({ + url: `/api/ListConditionalAccessPolicies?tenantFilter=${tenantFilter}`, + queryKey: `CAPolicies-${tenantFilter}`, + enabled: !!policyId && !!tenantFilter, + }); + + useEffect(() => { + if (policiesQuery.isSuccess && policiesQuery.data?.Results) { + const match = policiesQuery.data.Results.find((p) => p.id === policyId); + if (match?.rawjson) { + const parsed = JSON.parse(match.rawjson); + setPolicyData(parsed); + } + } + }, [policiesQuery.isSuccess, policiesQuery.data, policyId]); + + const dataFormatter = (values) => { + const cleaned = extractCAPolicyJSON(values); + return { + tenantFilter, + PolicyId: policyId, + PolicyBody: cleaned, + }; + }; + + return ( + + + {policiesQuery.isLoading ? ( + + ) : policiesQuery.isError ? ( + Error loading policies. + ) : !policyData ? ( + Policy not found for ID: {policyId} + ) : ( + + )} + + + ); +}; + +EditCAPolicy.getLayout = (page) => {page}; + +export default EditCAPolicy; diff --git a/src/pages/tenant/conditional/list-policies/index.js b/src/pages/tenant/conditional/list-policies/index.js index d7f48eac6694..1e85c99b3ebc 100644 --- a/src/pages/tenant/conditional/list-policies/index.js +++ b/src/pages/tenant/conditional/list-policies/index.js @@ -25,6 +25,13 @@ const Page = () => { // Actions configuration const actions = [ + { + label: "Edit Policy", + link: "/tenant/conditional/list-policies/edit?id=[id]", + icon: , + color: "info", + hideBulk: true, + }, { label: "Create template based on policy", type: "POST", diff --git a/src/pages/tenant/conditional/list-template/create.jsx b/src/pages/tenant/conditional/list-template/create.jsx new file mode 100644 index 000000000000..1843c369c323 --- /dev/null +++ b/src/pages/tenant/conditional/list-template/create.jsx @@ -0,0 +1,39 @@ +import React from "react"; +import { Box } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { Layout as DashboardLayout } from "../../../../layouts/index.js"; +import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; +import CippCAPolicyBuilder, { extractCAPolicyJSON } from "../../../../components/CippComponents/CippCAPolicyBuilder"; + +const CreateCATemplate = () => { + const formControl = useForm({ + mode: "onChange", + defaultValues: { + state: { label: "Report-only", value: "enabledForReportingButNotEnforced" }, + }, + }); + + const customDataFormatter = (values) => { + return extractCAPolicyJSON(values); + }; + + return ( + + + + + + ); +}; + +CreateCATemplate.getLayout = (page) => {page}; + +export default CreateCATemplate; diff --git a/src/pages/tenant/conditional/list-template/edit.jsx b/src/pages/tenant/conditional/list-template/edit.jsx index 9521ddae99ed..6d82be777062 100644 --- a/src/pages/tenant/conditional/list-template/edit.jsx +++ b/src/pages/tenant/conditional/list-template/edit.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Alert, Box, Typography } from "@mui/material"; +import { Alert, Box, Typography, ToggleButtonGroup, ToggleButton } from "@mui/material"; import { useForm } from "react-hook-form"; import { useRouter } from "next/router"; import { Layout as DashboardLayout } from "../../../../layouts/index.js"; @@ -7,15 +7,29 @@ import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; import CippFormSkeleton from "../../../../components/CippFormPages/CippFormSkeleton"; import { ApiGetCall } from "../../../../api/ApiCall"; import CippTemplateFieldRenderer from "../../../../components/CippComponents/CippTemplateFieldRenderer"; +import CippCAPolicyBuilder, { + extractCAPolicyJSON, +} from "../../../../components/CippComponents/CippCAPolicyBuilder"; const EditCATemplate = () => { const router = useRouter(); const { GUID } = router.query; const [templateData, setTemplateData] = useState(null); const [originalData, setOriginalData] = useState(null); + const [editorMode, setEditorMode] = useState("builder"); const formControl = useForm({ mode: "onChange" }); + // When switching to builder mode, reset the form to clear any empty [] + // values that CippTemplateFieldRenderer may have injected + const handleEditorModeChange = (e, val) => { + if (!val) return; + if (val === "builder" && editorMode !== "builder") { + formControl.reset({}); + } + setEditorMode(val); + }; + // Fetch the template data const templateQuery = ApiGetCall({ url: `/api/ListCATemplates?GUID=${GUID}`, @@ -110,6 +124,14 @@ const EditCATemplate = () => { }; }; + // Build a data formatter that works for both editor modes + const builderDataFormatter = (values) => { + const cleaned = extractCAPolicyJSON(values); + return { GUID, ...cleaned }; + }; + + const activeFormatter = editorMode === "builder" ? builderDataFormatter : customDataFormatter; + return ( { queryKey={[`CATemplate-${GUID}`, "CATemplates"]} backButtonTitle="Conditional Access Templates" postUrl="/api/ExecEditTemplate?type=CATemplate" - customDataformatter={customDataFormatter} + customDataformatter={activeFormatter} formPageType="Edit" + titleButton={ + + Policy Builder + Field Editor + + } > {templateQuery.isLoading ? ( ) : templateQuery.isError || !templateData ? ( Error loading template or template not found. + ) : editorMode === "builder" ? ( + ) : ( { const pageTitle = "Available Conditional Access Templates"; @@ -144,7 +145,13 @@ const Page = () => { simpleColumns={["displayName", "package", "GUID"]} cardButton={ - Date: Wed, 13 May 2026 18:54:03 +0800 Subject: [PATCH 82/86] Update CippCAPolicyBuilder.jsx --- .../CippComponents/CippCAPolicyBuilder.jsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/components/CippComponents/CippCAPolicyBuilder.jsx b/src/components/CippComponents/CippCAPolicyBuilder.jsx index c7999edff6c6..30a38673bd4e 100644 --- a/src/components/CippComponents/CippCAPolicyBuilder.jsx +++ b/src/components/CippComponents/CippCAPolicyBuilder.jsx @@ -1117,5 +1117,25 @@ export function extractCAPolicyJSON(formValues) { ); } + // Post-process: strip session control sub-objects where isEnabled is false. + // Graph validates fields like `mode` even when disabled — safest to omit entirely. + if (cleaned.sessionControls) { + const sessionKeys = [ + "applicationEnforcedRestrictions", + "cloudAppSecurity", + "signInFrequency", + "persistentBrowser", + ]; + for (const key of sessionKeys) { + if (cleaned.sessionControls[key]?.isEnabled === false) { + delete cleaned.sessionControls[key]; + } + } + // If sessionControls is now empty, remove it too + if (Object.keys(cleaned.sessionControls).length === 0) { + delete cleaned.sessionControls; + } + } + return cleaned; } From 60a50738fc68fd8b17ea615343f785828c713209 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 13 May 2026 21:08:55 +0200 Subject: [PATCH 83/86] fixes tenantfilter property --- .../administration/mailbox-rules/index.js | 113 ++++++++++-------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/src/pages/email/administration/mailbox-rules/index.js b/src/pages/email/administration/mailbox-rules/index.js index 4b9a9ece88cb..98f0d076caff 100644 --- a/src/pages/email/administration/mailbox-rules/index.js +++ b/src/pages/email/administration/mailbox-rules/index.js @@ -1,90 +1,97 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { getCippTranslation } from "../../../../utils/get-cipp-translation"; -import { getCippFormatting } from "../../../../utils/get-cipp-formatting"; -import { CippPropertyListCard } from "../../../../components/CippCards/CippPropertyListCard"; -import { Block, PlayArrow, DeleteForever } from "@mui/icons-material"; -import { useCippReportDB } from "../../../../components/CippComponents/CippReportDBControls"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { getCippTranslation } from '../../../../utils/get-cipp-translation' +import { getCippFormatting } from '../../../../utils/get-cipp-formatting' +import { CippPropertyListCard } from '../../../../components/CippCards/CippPropertyListCard' +import { Block, PlayArrow, DeleteForever } from '@mui/icons-material' +import { useCippReportDB } from '../../../../components/CippComponents/CippReportDBControls' const Page = () => { - const pageTitle = "Mailbox Rules"; + const pageTitle = 'Mailbox Rules' const reportDB = useCippReportDB({ - apiUrl: "/api/ListMailboxRules", - queryKey: "ListMailboxRules", - cacheName: "Mailboxes", - syncTitle: "Sync Mailbox Rules", - syncData: { Types: "Rules" }, + apiUrl: '/api/ListMailboxRules', + queryKey: 'ListMailboxRules', + cacheName: 'Mailboxes', + syncTitle: 'Sync Mailbox Rules', + syncData: { Types: 'Rules' }, allowToggle: false, defaultCached: true, - }); + }) const simpleColumns = [ - ...reportDB.cacheColumns.filter((c) => c === "Tenant"), - "UserPrincipalName", - "Name", - "Priority", - "Enabled", - "From", - ...reportDB.cacheColumns.filter((c) => c !== "Tenant"), - ]; + ...reportDB.cacheColumns.filter((c) => c === 'Tenant'), + 'UserPrincipalName', + 'Name', + 'Priority', + 'Enabled', + 'From', + ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'), + ] const actions = [ { - label: "Enable Mailbox Rule", - type: "POST", + label: 'Enable Mailbox Rule', + type: 'POST', icon: , - url: "/api/ExecSetMailboxRule", + url: '/api/ExecSetMailboxRule', data: { - ruleId: "Identity", - userPrincipalName: "OperationGuid", - ruleName: "Name", + ruleId: 'Identity', + userPrincipalName: 'OperationGuid', + ruleName: 'Name', Enable: true, + tenantFilter: 'Tenant', }, condition: (row) => !row.Enabled, - confirmText: "Are you sure you want to enable this mailbox rule?", + confirmText: 'Are you sure you want to enable this mailbox rule?', multiPost: false, }, { - label: "Disable Mailbox Rule", - type: "POST", + label: 'Disable Mailbox Rule', + type: 'POST', icon: , - url: "/api/ExecSetMailboxRule", + url: '/api/ExecSetMailboxRule', data: { - ruleId: "Identity", - userPrincipalName: "OperationGuid", - ruleName: "Name", + ruleId: 'Identity', + userPrincipalName: 'OperationGuid', + ruleName: 'Name', Disable: true, + tenantFilter: 'Tenant', }, condition: (row) => row.Enabled, - confirmText: "Are you sure you want to disable this mailbox rule?", + confirmText: 'Are you sure you want to disable this mailbox rule?', multiPost: false, }, { - label: "Remove Mailbox Rule", - type: "POST", + label: 'Remove Mailbox Rule', + type: 'POST', icon: , - url: "/api/ExecRemoveMailboxRule", - data: { ruleId: "Identity", userPrincipalName: "OperationGuid", ruleName: "Name" }, - confirmText: "Are you sure you want to remove this mailbox rule?", + url: '/api/ExecRemoveMailboxRule', + data: { + ruleId: 'Identity', + userPrincipalName: 'OperationGuid', + ruleName: 'Name', + tenantFilter: 'Tenant', + }, + confirmText: 'Are you sure you want to remove this mailbox rule?', multiPost: false, }, - ]; + ] const offCanvas = { children: (data) => { const keys = Object.keys(data).filter( - (key) => !key.includes("@odata") && !key.includes("@data"), - ); - const properties = []; + (key) => !key.includes('@odata') && !key.includes('@data') + ) + const properties = [] keys.forEach((key) => { if (data[key] && data[key].length > 0) { properties.push({ label: getCippTranslation(key), value: getCippFormatting(data[key], key), - }); + }) } - }); + }) return ( { actionItems={actions} data={data} /> - ); + ) }, - }; + } return ( <> @@ -111,8 +118,8 @@ const Page = () => { /> {reportDB.syncDialog} - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page From fd6a9e36007839cbb51f850ac75b5585726724a0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 14 May 2026 15:38:27 +1000 Subject: [PATCH 84/86] Logs --- src/layouts/config.js | 7 + src/pages/cipp/advanced/container-logs.js | 388 ++++++++++++++++++++++ 2 files changed, 395 insertions(+) create mode 100644 src/pages/cipp/advanced/container-logs.js diff --git a/src/layouts/config.js b/src/layouts/config.js index ad0004307a96..a9df0957241d 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -1109,6 +1109,13 @@ export const nativeMenuItems = [ permissions: ['CIPP.SuperAdmin.*'], scope: 'global', }, + { + title: 'Container Logs', + path: '/cipp/advanced/container-logs', + roles: ['superadmin'], + permissions: ['CIPP.SuperAdmin.*'], + scope: 'global', + }, ], }, ], diff --git a/src/pages/cipp/advanced/container-logs.js b/src/pages/cipp/advanced/container-logs.js new file mode 100644 index 000000000000..c014d71734d8 --- /dev/null +++ b/src/pages/cipp/advanced/container-logs.js @@ -0,0 +1,388 @@ +import { useState, useEffect, useMemo } from "react"; +import { useForm, useWatch } from "react-hook-form"; +import { + Box, + Button, + Stack, + Typography, + Chip, + Accordion, + AccordionSummary, + AccordionDetails, + Alert, +} from "@mui/material"; +import { ExpandMore, Search, Refresh } from "@mui/icons-material"; +import { CippFormComponent } from "../../../components/CippComponents/CippFormComponent"; +import { Grid } from "@mui/system"; +import { Layout as DashboardLayout } from "../../../layouts/index.js"; +import { CippTablePage } from "../../../components/CippComponents/CippTablePage"; +import { ApiGetCall } from "../../../api/ApiCall"; + +const levelOptions = [ + { label: "All Levels", value: "" }, + { label: "Debug", value: "DBG" }, + { label: "Information", value: "INF" }, + { label: "Warning", value: "WRN" }, + { label: "Error", value: "ERR" }, + { label: "Critical", value: "CRT" }, +]; + +const timeRangeOptions = [ + { label: "Last 15 minutes", value: "15" }, + { label: "Last 30 minutes", value: "30" }, + { label: "Last 1 hour", value: "60" }, + { label: "Last 3 hours", value: "180" }, + { label: "Last 6 hours", value: "360" }, + { label: "Last 12 hours", value: "720" }, + { label: "Last 24 hours", value: "1440" }, + { label: "Custom Range", value: "custom" }, + { label: "No Time Filter", value: "" }, +]; + +const getLevelColor = (level) => { + switch (level) { + case "CRT": + return "error"; + case "ERR": + return "error"; + case "WRN": + return "warning"; + case "INF": + return "info"; + case "DBG": + return "default"; + default: + return "default"; + } +}; + +const getLevelLabel = (level) => { + switch (level) { + case "CRT": + return "Critical"; + case "ERR": + return "Error"; + case "WRN": + return "Warning"; + case "INF": + return "Info"; + case "DBG": + return "Debug"; + case "TRC": + return "Trace"; + default: + return level || "Unknown"; + } +}; + +const ContainerLogsFilter = ({ onSubmitFilter }) => { + const [expanded, setExpanded] = useState(true); + + const formControl = useForm({ + mode: "onChange", + defaultValues: { + timeRange: "60", + level: "", + search: "", + file: "", + tail: "500", + searchAll: false, + fromDate: "", + toDate: "", + }, + }); + + const { handleSubmit } = formControl; + const timeRange = useWatch({ control: formControl.control, name: "timeRange" }); + + const fileListQuery = ApiGetCall({ + url: "/api/ListContainerLogs", + data: { Action: "ListFiles" }, + queryKey: "ContainerLogFiles", + }); + + const fileOptions = useMemo(() => { + const opts = [{ label: "Current Log", value: "" }]; + if (fileListQuery.isSuccess && fileListQuery.data?.Results) { + fileListQuery.data.Results.forEach((f) => { + if (!f.IsCurrent) { + opts.push({ + label: `${f.Name} (${f.SizeFormatted})`, + value: f.Name, + }); + } + }); + } + return opts; + }, [fileListQuery.isSuccess, fileListQuery.data]); + + const onSubmit = (values) => { + const params = { + Action: values.searchAll ? "SearchAll" : "ReadLog", + Tail: values.tail || "500", + }; + + // Level filter + const levelVal = Array.isArray(values.level) ? values.level[0]?.value : values.level; + if (levelVal) params.Level = levelVal; + + // Search text + if (values.search) params.Search = values.search; + + // File selection + const fileVal = Array.isArray(values.file) ? values.file[0]?.value : values.file; + if (fileVal && !values.searchAll) params.File = fileVal; + + // Time range + const rangeVal = Array.isArray(values.timeRange) + ? values.timeRange[0]?.value + : values.timeRange; + if (rangeVal === "custom") { + if (values.fromDate) params.From = new Date(values.fromDate).toISOString(); + if (values.toDate) params.To = new Date(values.toDate).toISOString(); + } else if (rangeVal && rangeVal !== "") { + const minutes = parseInt(rangeVal, 10); + if (!isNaN(minutes)) { + params.From = new Date(Date.now() - minutes * 60 * 1000).toISOString(); + } + } + + onSubmitFilter(params); + setExpanded(false); + }; + + const handleClear = () => { + formControl.reset(); + onSubmitFilter(null); + setExpanded(true); + }; + + return ( + setExpanded(!expanded)}> + }> + Log Filters + + + + + Search the local container log files directly. Logs are rotated by size and retained on + disk. Use “Search All Files” to search across rotated log files. + + + + + + + + + + + + + + + + + + + + {(Array.isArray(timeRange) ? timeRange[0]?.value : timeRange) === "custom" && ( + + + + + + + + + )} + + + + + + + + + + + + + + + + + + + + ); +}; + +const Page = () => { + const [apiFilter, setApiFilter] = useState(null); + const queryKey = JSON.stringify(apiFilter); + + return ( + + + + + + } + clearOnError={true} + offCanvas={{ + size: "lg", + children: (row) => { + const levelColor = getLevelColor(row.Level); + return ( + + + + + + + {row.Timestamp} + + + + + + Message + + + + {row.Message} + + + + {row.Raw && row.Raw !== row.Message && ( + + + Raw Log Line + + + + {row.Raw} + + + + )} + + + ); + }, + }} + title="Container Logs" + tenantInTitle={false} + apiDataKey="Results" + apiUrl={apiFilter ? "/api/ListContainerLogs" : "/api/ListEmptyResults"} + apiData={apiFilter} + queryKey={queryKey} + simpleColumns={["Timestamp", "Level", "Message"]} + actions={[]} + /> + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From b5d48bcddaed40af6839e15ba7a9907c4b22ad33 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 14 May 2026 18:09:04 +1000 Subject: [PATCH 85/86] logging --- src/data/ContainerLogPresets.json | 52 +++ src/pages/cipp/advanced/container-logs.js | 392 ++++++++++++++++------ 2 files changed, 345 insertions(+), 99 deletions(-) create mode 100644 src/data/ContainerLogPresets.json diff --git a/src/data/ContainerLogPresets.json b/src/data/ContainerLogPresets.json new file mode 100644 index 000000000000..71f3dc5b8e93 --- /dev/null +++ b/src/data/ContainerLogPresets.json @@ -0,0 +1,52 @@ +[ + { + "name": "Recent Errors (Last 1h)", + "id": "cl-preset-errors-1h", + "query": "where Level in (\"ERR\", \"CRT\")\n| where Timestamp > ago(1h)\n| take 500\n| sort by Timestamp desc" + }, + { + "name": "Warnings & Errors (Last 24h)", + "id": "cl-preset-warn-err-24h", + "query": "where Level in (\"ERR\", \"CRT\", \"WRN\")\n| where Timestamp > ago(24h)\n| take 1000\n| sort by Timestamp desc" + }, + { + "name": "All Logs (Last 15 min)", + "id": "cl-preset-all-15m", + "query": "where Timestamp > ago(15m)\n| take 500\n| sort by Timestamp desc" + }, + { + "name": "Startup Logs", + "id": "cl-preset-startup", + "query": "where Message contains \"Starting\"\n| where Message !contains \"heartbeat\"\n| take 200\n| sort by Timestamp desc" + }, + { + "name": "Graph API Errors", + "id": "cl-preset-graph-errors", + "query": "where Level in (\"ERR\", \"CRT\")\n| where Message matches regex \"graph|Graph|GRAPH\"\n| where Timestamp > ago(24h)\n| take 500\n| sort by Timestamp desc" + }, + { + "name": "Token / Auth Issues", + "id": "cl-preset-auth", + "query": "where Message matches regex \"token|auth|unauthorized|forbidden|401|403\"\n| where Timestamp > ago(24h)\n| take 500\n| sort by Timestamp desc" + }, + { + "name": "Timeout Errors", + "id": "cl-preset-timeouts", + "query": "where Message matches regex \"timeout|timed out|TaskCanceled\"\n| where Timestamp > ago(24h)\n| take 500\n| sort by Timestamp desc" + }, + { + "name": "All Errors (Search All Files)", + "id": "cl-preset-all-errors", + "query": "search all files\n| where Level in (\"ERR\", \"CRT\")\n| take 1000\n| sort by Timestamp desc" + }, + { + "name": "Standards Processing", + "id": "cl-preset-standards", + "query": "where Message contains \"Standard\"\n| where Timestamp > ago(24h)\n| take 500\n| sort by Timestamp desc" + }, + { + "name": "Full Log (Last 1h, no heartbeats)", + "id": "cl-preset-full-clean", + "query": "where Timestamp > ago(1h)\n| where Message !contains \"heartbeat\"\n| take 1000\n| sort by Timestamp desc" + } +] diff --git a/src/pages/cipp/advanced/container-logs.js b/src/pages/cipp/advanced/container-logs.js index c014d71734d8..b77aac6590a9 100644 --- a/src/pages/cipp/advanced/container-logs.js +++ b/src/pages/cipp/advanced/container-logs.js @@ -10,13 +10,17 @@ import { AccordionSummary, AccordionDetails, Alert, + AlertTitle, + Tab, + Tabs, } from "@mui/material"; -import { ExpandMore, Search, Refresh } from "@mui/icons-material"; +import { ExpandMore, Search, Refresh, PlayArrow } from "@mui/icons-material"; import { CippFormComponent } from "../../../components/CippComponents/CippFormComponent"; import { Grid } from "@mui/system"; import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippTablePage } from "../../../components/CippComponents/CippTablePage"; import { ApiGetCall } from "../../../api/ApiCall"; +import defaultPresets from "../../../data/ContainerLogPresets.json"; const levelOptions = [ { label: "All Levels", value: "" }, @@ -77,23 +81,64 @@ const getLevelLabel = (level) => { const ContainerLogsFilter = ({ onSubmitFilter }) => { const [expanded, setExpanded] = useState(true); + const [tabValue, setTabValue] = useState(0); // 0 = Query, 1 = Guided + const [selectedPreset, setSelectedPreset] = useState(null); - const formControl = useForm({ + // Query mode form + const queryForm = useForm({ + mode: "onChange", + defaultValues: { + queryPreset: null, + query: 'where Timestamp > ago(1h)\n| take 500\n| sort by Timestamp desc', + }, + }); + + const queryValue = useWatch({ control: queryForm.control, name: "query" }); + const queryPreset = useWatch({ control: queryForm.control, name: "queryPreset" }); + + // Guided mode form + const guidedForm = useForm({ mode: "onChange", defaultValues: { timeRange: "60", level: "", search: "", + exclude: "", + regex: "", file: "", tail: "500", searchAll: false, + sortDesc: true, fromDate: "", toDate: "", }, }); - const { handleSubmit } = formControl; - const timeRange = useWatch({ control: formControl.control, name: "timeRange" }); + const timeRange = useWatch({ control: guidedForm.control, name: "timeRange" }); + + // Preset options (built-in only — no API save/load for container log presets) + const presetOptions = useMemo( + () => + defaultPresets.map((preset) => ({ + label: preset.name, + value: preset.id, + query: preset.query, + isBuiltin: true, + })), + [] + ); + + // Load preset when selected + useEffect(() => { + if (queryPreset) { + const preset = Array.isArray(queryPreset) ? queryPreset[0] : queryPreset; + if (preset?.query) { + queryForm.setValue("query", preset.query); + setSelectedPreset(preset); + queryForm.setValue("queryPreset", null); + } + } + }, [queryPreset, queryForm]); const fileListQuery = ApiGetCall({ url: "/api/ListContainerLogs", @@ -116,12 +161,26 @@ const ContainerLogsFilter = ({ onSubmitFilter }) => { return opts; }, [fileListQuery.isSuccess, fileListQuery.data]); - const onSubmit = (values) => { + // Submit query mode + const handleQuerySubmit = queryForm.handleSubmit((values) => { + if (values.query && values.query.trim()) { + onSubmitFilter({ + Action: "Query", + Query: values.query.trim(), + }); + setExpanded(false); + } + }); + + // Submit guided mode + const handleGuidedSubmit = guidedForm.handleSubmit((values) => { const params = { Action: values.searchAll ? "SearchAll" : "ReadLog", Tail: values.tail || "500", }; + if (values.sortDesc) params.SortDesc = "true"; + // Level filter const levelVal = Array.isArray(values.level) ? values.level[0]?.value : values.level; if (levelVal) params.Level = levelVal; @@ -129,6 +188,12 @@ const ContainerLogsFilter = ({ onSubmitFilter }) => { // Search text if (values.search) params.Search = values.search; + // Exclude text + if (values.exclude) params.Exclude = values.exclude; + + // Regex pattern + if (values.regex) params.Regex = values.regex; + // File selection const fileVal = Array.isArray(values.file) ? values.file[0]?.value : values.file; if (fileVal && !values.searchAll) params.File = fileVal; @@ -149,10 +214,15 @@ const ContainerLogsFilter = ({ onSubmitFilter }) => { onSubmitFilter(params); setExpanded(false); - }; + }); const handleClear = () => { - formControl.reset(); + if (tabValue === 0) { + queryForm.reset(); + setSelectedPreset(null); + } else { + guidedForm.reset(); + } onSubmitFilter(null); setExpanded(true); }; @@ -160,115 +230,239 @@ const ContainerLogsFilter = ({ onSubmitFilter }) => { return ( setExpanded(!expanded)}> }> - Log Filters + Log Query - - - Search the local container log files directly. Logs are rotated by size and retained on - disk. Use “Search All Files” to search across rotated log files. - - - - - - - - - - - - - - - - + + setTabValue(v)} + sx={{ borderBottom: 1, borderColor: "divider" }} + > + + + + + {/* ── Tab 0: Query Editor ── */} + {tabValue === 0 && ( + + + + Query Syntax + + Use a KQL-inspired pipe syntax to filter container logs. Separate clauses with{" "} + |. Supported operators: + + + where Level == "ERR" — exact level +
    + where Level in ("ERR", "CRT") — multiple + levels +
    + where Level != "DBG" — exclude level +
    + where Message contains "text" — search +
    + where Message !contains "text" — exclude +
    + where Message matches regex "err|fail" — regex +
    + where Timestamp > ago(1h) — relative time (s/m/h/d/w) +
    + where Timestamp between (ago(2h) .. ago(1h)) — range +
    + take 500 — limit results +
    + sort by Timestamp desc — newest first +
    + search all files — include rotated logs +
    +
    + + + + + -
    - {(Array.isArray(timeRange) ? timeRange[0]?.value : timeRange) === "custom" && ( + ago(1h)\n| take 500\n| sort by Timestamp desc`} + sx={{ + "& textarea": { + fontFamily: "monospace", + fontSize: "0.875rem", + }, + }} + /> + + + + + +
    +
    + )} + + {/* ── Tab 1: Guided Filter ── */} + {tabValue === 1 && ( + + + + Search the local container log files directly. Logs are rotated by size and + retained on disk. Use “Search All Files” to search across rotated log + files. + + - + + + + + + + - + - )} - - - - + + + + + + + + + + + - - + + {(Array.isArray(timeRange) ? timeRange[0]?.value : timeRange) === "custom" && ( + + + + + + + + + )} + + + + + + + + - - - - - + + + + + - - +
    + )}
    From 12a07270752ea03f3c94eb33f419fdd7336f5985 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 14 May 2026 14:34:30 +0200 Subject: [PATCH 86/86] comingsoon --- public/assets/integrations/autotask.png | Bin 0 -> 7685 bytes public/assets/integrations/connectwise.png | Bin 0 -> 18445 bytes public/assets/integrations/kaseya.svg | 3 + src/data/Extensions.json | 30 +++++ src/pages/cipp/integrations/index.js | 132 +++++++++++++++------ 5 files changed, 128 insertions(+), 37 deletions(-) create mode 100644 public/assets/integrations/autotask.png create mode 100644 public/assets/integrations/connectwise.png create mode 100644 public/assets/integrations/kaseya.svg diff --git a/public/assets/integrations/autotask.png b/public/assets/integrations/autotask.png new file mode 100644 index 0000000000000000000000000000000000000000..cf29404276136ca8860f3c62fb3bf6a60ebe5784 GIT binary patch literal 7685 zcmc(E1x#FBw>IuAZpDhrAT1P%lu{_}K7$YL?p_9W*J3Sh!{9DcpcF4&Y^D^4;;y~C z_f5XM`EK(4H-G-*-`P1?``OQW*4isO`|Ohwt*$Ef1n(Ih3JS^-1$k*r6cjWN3JR(i z4*DY^^nx(*5eQaS)|QcTQAUvt_z}j#%?`!BD*1^-R+`60&Y%cpuN9?hnwdVU$IY_Md$w7RISH|4cNGRe6k796=@Qz^jic9Uwp>uKc(o zyF4^iwNOyq?{5VT(vMG{UtgF#=u^lLVqs8iPAZ@xrDxGTpvU38j<&+g!>}JMIec>2 zUARm3%Hzi5;rCrXhe!~%6E05M`b`Bdz6MK2EE6%TVS1A;@EhEEO*OSBiH)1)Y{G($ z^UdN%xg+nLGSa7Z(0AcK{3#Gu&8{bJC4~rH6zM{A<@Bmn5=gTA;%(1mNx2%Azh-JF zMZ$XXMPKwAtkUmiGOf;;O}Rqa7beVyD-G1W?%qY+?MkIg*8ZG9v)EohL3!q*AT6ow zv-o@Y+Jw}a9{Z?xH7X2DprFoJ_)a?^>^(67i;s(ZYxQ#yjJkFXGV=1YJ_^hkzvnt+ zj`o->OGX_~@yp+Yx5_JChim7HdxVRIN%r|e%2wY-<}XekhB7TSeY39D#U=y<2>GcW z>-PVp^2&S(CVuVwAshqR0Q z?%U1m+TbvBRL#YQ?J4Tb>r5)+kYe+T)I(Y`r{FL*%qdk&Wb4-#YJ5e8A^lqL6%HT_ zOG`51`lXX`$jqbBb`Mve#Y=MGy2`NU`u0NN7I&Cj{tdZB)@#1NsL0Ug$J*bGpop(< z6x=e%gm=ft>=p+Q`5JrcwR%Ypn;X|vxc{Tq{CM6#NJdn;TlbW|F*IoLWNfcIMXaG; z1=Cr814z=5J{kl~v*ZIxlo5pWeUYsDD}gK0GypEErfRy>z1#UI8xR_0Cf8VZqKZjv zHNVo*p8;rlGlwlhf6Mj<2@&*36aW=V0<0cmMZWa~I#AlPn@c3LMS-ImkA=NqY(ml! z?H!92QA^pOjoJdRz zI7x*oqb4luDe^Ku=&<>j!v(NBI6#&g;o5-MpQiS zySpi)?7vX$m=>^%w}kC(W~`_%ftU!*@uO`_82`My9oGJ?14aCa-<~B|0qU@Pg^O;! zBE?YZ96tF5*EA;ktGEm?e86S^bvJ!KH>l#=wwsNZ+qD#z&`8$!)^GkEQeJB){+6fR zuuX3N<u{O)U`ERAd(r)vrov~$C_5;I z8Lboobvw_#ax0-jGJFro!l*jP!b@eDuuS>B%l$;%G6(_lz?ARa9v8_PSkMXo zCWR67c^t{#1LlnMQN>)@P`sPEp2?p3c`^SMR9eIsiiV*?;E&e>PUaPDP)2Uw@m2 zvgH%IU-~0v52eezvGWX^#%FO&tqwe*W@MvajQ^@zFIEI?JyWU9`bxFjB4g&6|3nJVO1QWseCnkFA4y zD*#~=Qpx0iS1ep;I4)M8B{WOMXn6AFfOQaL)1#oce4Nl(l^r{{+mA6ary{luMRIn# zjJa7>5hnpslBEmq+J|RLQgaVahs4rxtFb67>-mvABcxm$@$8wkPK6e}I*W6T&c?cv zeopjVqSh);5KLX>B}2lCx({slUJ)%maz<&g`v)!sG-dv3AqpnD#XLKlp|$ z*u&03!}2ytZ|0v6JALF3&6Nv9zz|v7#Y2@z6oNpz3xJQ)$7P|1FhPB{EwqDkV8fcW5H4Ul#c|vL-_EvSnfhmoOv1>O)Z)qV?cGxrNoz z!>3RCLbE~7cbZKHnzOh-6PWc)JitT`(7LDz6BPQ$^|;-! z{eMNhbLDCJgx+|Dh3c=sTaWj2PBtNuiz~7$;1cu=)KZq?w2}LqFlXOi0n~)^2LzJ{JsuW`p_$|M-f``3sjI*` z`ebNoNBIgx<>2#mU{ZM;1_DamSxeI1Wje&c0y^%Z4Mjuh9R9GHC0|G;d*6pM1hb?0 z*7yppXqI^Pu31?~b?Aer1=ltg@DQ^`M$qcDe*(w>lCIP9{0_rAZATiTsyIU0euDJR z^J2ajkEb*EI2ldx-9=~}&aaxJ{RHa`r%J@KG+r_RNiMd9Wd zBGxB?T&e3cM)2LeY@|bZ+7nhjc7^1sU8@U?sM~X|RDrXeKL;_hhkq^(DJARh^h66h z?Hnv~NMpf6nZre5@V&2|lUP;8)Pc|_Mnp?y5$5E6VQGRj%r8d-4>`5VH5s`cp`7ti z6IPfjY?_wN#sVtA(dE3I@A|66Myc~@c6UkQ{xq@2|5ybKeHuvKNZ|=X{%MghBd%V! zpUhu{@cVkexop|Z-kj|ZkZfEG3EV++eia{@`%X$|kFW^pL1SixI^{I9*=hKlGm6c0 z>ViKq_#DoxhxhMp1z*d_S9`5I(HAWcY~(BK9()d(Lu^mD3|e@=b2OsybZak7XZ#k8 z$s#%_tRFyFP4?#D0g#d|wI1Wdw>ZM=K+UN$D0n3$og#B^eS@-@Rkb2;_51HT6}c& zp24?seV2-8Vo?iv*VTeB5JZ+c+fA{nbep|&4`!`Sqgc9Q&J`82yKKME2g51_ z-{#Fw>qJ}4>RxuGyh`{%W^iDwt3ui2X4r}q0rmIx>MFQMmJW`6c33VFNauKzI=xnl zv1n+g8|!hlXDjp5WNlidfwKFhK{0seH|dN?yCc_GQw3JN_0;BWyP>;Qb}m2$E-F0i zmm*zAfVXvCSnPD#HjDqx;e7$BaFZq35(f;Ci zyu=gWg8Nw= zaqx0~DfB4FGE@9);qVrLEI%^yygAxBeKV{OTGA9!>&))5>`@^|aFQv?jzRG{lx#P^ z#$3FHp}hl|bi-SKd3MZ$k2nm-xBkh1RE6*1jOo_%Q&kf0nOUQ zV@9dY25*O$iUj+H8#Yxyu$k29pQl(1r+d=x51!2HU!GL*uhJ4GrY#s-ueXCF*0LZI zgbxn|&JQWM-WNVe$a(KC;Nu`l*Vplpz6+)=$iyni@iVwwiat262#<)ma8O=l0-32` z9Am{3@^j4SXt8MGH=O8Msw;S227$dYrbUb6@s0NMV!{QO=Au~+EWS~!{lRwAeP&k z|8g=B=*E!d2JK2tw|pUA`Zepl`GOnG3t}T>|L3NPvQeNvco9slDiyiHs+wBDu|-z` zug|pEs{L6XZwu28Te*xUecdOhgm3xOvUf^C9a5FK6D}UMBdL4%#>_BN2jIg3eYnNk z|4`@wD0`AY?8ZEL@+#_H7ox1hjvb$!6)Wzh-9dxS2S2^dc%#-au~y&TMZY6xPLYipElNFNB_I^9nyLbOc__Wo`A*3$eokW^&U^bJ_CwM~(&7M@0tKiVJ;~46R3l_Qa zCtSQ>#Q>~C8-(SpPDHA ziuu%CKmroh4(7krG-^$M=M~ zFCU*5KL;gTC*^wNy@#nssC-$Vv{z1nPZ`w#-pzfK+u;ciSX;4+$uYLp4m{0)OnSN; ziO3mFE^|j=jSy=SaAT*f0e9Y-!j$(iITdkX-=(aJuBK_c#fl*^D%=2+Ir{?Irrj&j zvglyPq#UX^oE*sU^q(7Qpy7aSiw$fvZ#V=KR|4Hxc9Mi_hMK6#oO{cNV&{m+-%K%+sA`PNT;Htm*hB-bP)2^(lHq5x0NZMal(BPH z$A}=b%J)og{x*U%Gcco$eY}bX`49Pa3s|`s=JFv!P})p$%xvF}1|Xje9(~3*b2*#7 zw+RY&0(nY$O|DK>ZCuA{t}tx0c=kg@NgxK8(IPk(A;OrVW6)s%o7sE10Pebbg`31q z&=WcgHIv4?R)!Z(T~q&r$J*C{b#ga+ZrRh&_wYQlNfSf0F%!(WiLf)$i{o5bTHKel zyz+3WLH&NF?rwI{1oSH@@XuTUCo*%`!4|Km(L~2jK0>0Wu3Pusq6GxFp3`9{O=yry zI=5lKj3J#XuYKDw)8EB^@)Y^p%SD`%;$<6LW@Dv9vqH$0M9`k$QMW|(S-98$Nw zQ(p6$POs6`GOHcJk46EoY17dWBd<(SZJAvEI_>4IcD4 zP49z6UVzZ8rW!f;VN{E$!P;SaftV?n;&eW+EZo6Dpb%S?l#?Ty!ZHmC!1Q-cBkE<- z_woXzCMm~^uGQ3))fFfETnw;+fi=vBo+)5uUFr=d zrwCF>%zZb0id;0)9h#vq>>qNX;`Y>_=)Qe-l)%3rFtj&xi0J(YIh*lq&LM1mMm5;R zRiwY^)RsA?IU4jD8jMX1O5^=Kozs~eK4}@rBhJNIuv*gK40)!MtcO$mb??vOx znrN=@&iAkM1fVrLI;5g`_N0}1No>h(DA-7pp{SF3Z3c);U81|LQEPh}HUKOt{Ygp$ zc&IpadMCci*%v}IExw3V>tMY7HKVNxkkp6TNRM77nr!rCOpH zA|9@cp6opt@VK& zVTk!JWCwHTy*z?2p&4teIA*!><7Zw1x%%o{{t+&cT3h=`+VmV4VrBA}_9e*#+}e>t zIQckpj+kh3Xq2T4=XDusQyhS?m<7C+?3*JOzKgj}-!mSt`Z_f5J(Y9vm{ITr6V3A3 zEVLixEKEzsC(s!dW=+=})hVjuh^1h0S5P0(_)QYsD;0Xjtb9EtSoMqGuKcyx;&m;>jj%UEK|qt+L!yN^{N^)uf{Uqcgr7By?KH!F0^3HV6@Snik{M z=i9-J<@Xd!u*%z?5(YQ!pCe=fqwDTuZFINi+zXxA#{}iPha@X+E|1mA`>gmZs#j`ksUDsa z|2p$YoTAwM$*M42y>IwV>)4BV=Gf}_;17c_9=mVI?kn|Wwy8C+);+$`k-)z5(20P) zwZ3G!X$kDzF>3kDj)vbhEzg&NcSSoBO*_uRIcuVgOhJ!E#qXfjED6$E5I8h#W6BpL zjH@e@y&$UWlKZY3?1!HmVu=@nf+A-a06b7`U#r1()~WPTT7O$j^=Wl5;Vm;p-emq2O0kdTR{@ z=gt^)W7Wzp$zo8Pc%_m_?m_TvsIG_-+p%va%3_wWPH`1~FZ6w_8#~hL4ocozxs-$k zNpeA5lDF?c^S7M>KF%4uv|=$t+}d<=$)V)w%%`bg0*S9A0bqPvBR>+DdZ~fB7{7;O zPOo)|<~W0>C|=FH%&QObJWpAyMN=T*NP=6mVXKKGl|1VnKk&%Q+)oX5nf|L&9DR@P z4lpYwe)d$=**aKeVCU{fcOixIw!3BTJ$`4R;xtaLRHYT=T!eCVds+Mts=zv8dV_)0 zH(URU#boI4{sW5p5)&qV54C1jV)|JAYPKj}Yp=YA&KuWJAJnW|BN5M9L0&gTSnDz7 z>@GsqxgejcdV9;nXxyF>8^l#x2daIAjvsdFjb^rYhq9ep3rbcVjF$%U{n?>~Phtr^$ z+}RE4-;5lMG;zpDu~!!~;C1 z%wgdpJ`=yr74PY3*x$}=9v}Zy8ZFQ__NhRi>HZ^IzB7KdFXF|IChPh8(UY}k>G#M@ zm4(wTd?_KXgr*?;>q_6%g4(nDd2kX)P@PM?6DVgGf2M>HtTI5or^9zqJ zop-(PkYk5VZi}z2qU6RtP>w41O_xd!jfx?TJ=#(Uit{S$7N?AA$nZtJgR}XVjj_u7 zf@g(~f1~dih-rajRDnG;npKTO4MXdAlkR;yI&*^#RfU~HSJK{2Px4h)(#~~K({<9o zOZ&vU@dH~LpA5p&>Pr7RDfiApwZn(?PY+j!_R>0N&caDIu=DnJ)9O&>J-LD6gK0rb z%eb>i4XADN65{d8+0e0?pHzEP*VI0#BCbY1mnS;`aQ%W(?AcV^_P{JwB4TW= zqngP^p52`FPdj8*fI)Z5bs*UKwr`+eyf3CYpYxk2A@NKP?u01e~$9z0%>DB+JHyO5&-*z~o#%PZ+2`FzO?3sLM|6(>005Da;yY~sfB*^r;6^;W zhaI^k)dXVyS!$~4$|<<1vI;o-Uwrr-SpUyISY&>O_l)?J*yWhLwAFP0?QeonB;U+m zj<77dX(m*)qu!9Pd4q*ZSSO7}zWOV&z_ZRFv^2I3dHSzgrol?Ht z9GwWoWxe*_;p|}1*t(7_(=KS{f{{^ww#K6W|G)p&8juM2@pb?4{mlJ1&%Bo;04I_b zVp9nM|EAP-zsvc0;Hg_S#X_zSE1rj0d z_YtR23=)c##ih{@Ck&)$2~0=$fP+?vEC5v2@O-T&G#WM{F>UGh!Q)7zq;g8~@u{$N zM8SLfkj&v zu7o=XLt7hQL|SOvWm$6g6wwKFyp@O-G8HDuj8Mm^i0z~XQ0p6n77|0jt!pxM8_Gh` z%!tKVgt}|pLomRKI>X~Kn!4mdw#}&f$hH&onxk^(vM>w+s2QgB!p>B2UkMG|yd+1Q zV5ZN7grG!CD7^OWfa+4SJL-$HmN1tMqod7E(Ak!Bm_-0L2RQ=ZM~y@h9p)?}bk!PU z3Wh`BrwwcvWh{GZuUT9yoF4P~T1va7^=@$KtoayGKT~3e8@S;53Z9- zRPtoeoUtS;AGdI2Q>1E7|w^_txX_Y1BJ$7U=G{Z7;nQyupH*6EFya^#$CjK)wG8%`7HXLF61(FV zl&T{fx|eqeree&Lj9VF?IBpdSL{ybDj>pl-j~Wq5c--cI6vtJyiHcm?&~5LGle1Db z0J3rPgwoSg@Va-=pF_U%p4P-8#o&i3=663C-00@iG@o-cZVeyBF2u4gYwB@z+Id!( zhE_iS05l6=GHq`jW^NcYGM>j^w`1`n;v4vz)R_^@JRtz^=H)o<4X!3|odE{`U~fsK z-3IXm3^h@43qovtPZxXd(i?(=%kOSiAO{|$sSNH-ScErkQ_PfxC*gXH_)qn-9?=T* z=u<+zMOfD}8M!nCS}Xsi=?7mI zv>fZr`R38eMJ}Fk(rV0{V4Bo7|YxUv4dmt6vo9GI|6+>Nok80o4w&7tvtU@ExGo5 zG9r-%3wm9)J#Q)PH2~X*Dg{JmxqtGogv2ke$1M{BVx*q;yGdC*l)G94p|Ed4yh=S@s8g%C6 z?aR~tBQPmAm-pGsd`{5FF1({@vP%Z(iaW%3B)U-~+Ckx4^izh;|qU#qKPMQI=o+Ijsn z-^=tF>!!vYo9Pce?P)@p|4bEJ77zkf6hWi}70_HLzK7HP&oDudoYq%#2XK(Z7&1%W zDx#lMl}cg9gB596>D}?y;tqJ=7gTxIAvwRVv^)XCo9@|DtWZUyY*#cubm%vvHM*>@ ziaKf^2e5owr_+$$5fgr}RLw3EWNfXL1dLLKn@oa9JIy@aBCxh~(KuyXt zl;s{>c2~FZgK@;4U3jupwP6XE%xM;Cs;ZsQ`ma7MBbm32HAaM66f}S$^-p;bMXvU) zZn(IBcD8M*OIng??dK7&*83vG10~yw=hF`0Msh6LaM+676Xotg}0W<`fo z!bv0H;E(H$XMG|8DYXsnY>BG_a{IA`4AWb-)Rj|V)~kbO640aW*AbFMgz*Tseb+y` zqLTkm04Q1D$qlWU*FfIlVxe`&M|?e4`z z^ri*NhrrN@1;@ISK4ZwzNF`qAA{iFv`Lup;T|aWF%A7Euf)4-7cB(5l9^bUJ&*4b& ztc)X8T)@c#2M`?IkMsdLJOzU=aztxHF21ZvgUbF5rpm6eZ(97p+wnhMh5In2y5gV(UfMq8m=I z>nZD84^J0;V}Jll%iU}d6SpR?>-?qf7_c*mt$Bu-&s#F5!drB&5jhh0I;ee@v-#@S zx8b!ksjIJg<8N66^uTfU63U3aSr}m_52PDP>4$WLGUtrrlr1c|mAQ^aBTs{;Z|pWC z6=hCE85g}eD;Za6FCfiSGusDkLW1OTPa4mS%QeJut5hXZip|O#OL!#)olj(LFz_FK z=K=oPrIT1r@_*?JjsCV4>^r_Lqjj1p$_==>Y=9~ObJrlL6wH?=bwDbEf&NMZP>{c<7J?@4mtibCy;6<_T)%O5ARVS9-E0rV;(j z=V^v%JVyzrffi(UIaq3u5$^51Lv7lxmDjgv{@wuYCEERb)oprIRR?}?CrMy-F82%^ zeR7GJ900+s{&)&kL(i_-@@^2-10N@@a^@jVz8rn>h>osn2qYaq8gol8`d-`uf>k5x z14qxY8e=|Cj3NiBlw$r;7MF#p$tg~IKUYqqeMtwwE7M4N?^V9_U6J9=PboPbM%tg6 z3e)2Qk5ZdhTn5a6+xfD_lb1d|yflA%Spz$c*ecCUfis&%v(IyPN*HI3tNq^P0PRH~ z^uBL(CH)lJUo}&9CZb>>Bt)nLbQN&IxydRxBS|#sH~QR-v;!J_utM4;ywP-3+r;1m(0#s z?W0Cf;87kYu?B{ItM?`1Snu|rop#}LjJWKTF_2^o9gYfCvL;%1;CtV`0&wZUhpEs< z;BwB(H6#uoWp0PhqbhGwx@eLPzJG>KU6QRb*@Z!q@$}44-CcrC?n$-JxpDiuGo=VG zwUII$9d@O3Tcn(gvUcP?!~?W#2J>`l+^gE-PsQ_uS*?A7Zb&v!(Rv_ht<7D=Zi0-5HxTX^Q3jJi^h4Zo z`Uu;`re=IYTYXo&KlRq(o(sy$=^hqn>e3FvIs?}2qGmV}feq4fK(I>%Q&|%5BSFLD z&EFGm5}X=wnWQBP2@=FNx#ibIo?pI+u9v;>QwT6C;5ip4hz2p?WI#HNPj{sHdmqFp z;ZH9i>~=4qgl+G>_Q@pxi(Xp9p2!KTq&TOI4S*CMRbBAR&9?Dr$V9V@W)8W&wvGe$kbG_VJO-=OFsJhx1H-ZjQdnF3;dza&-{eQY;Y+64bnEJAiBCh;#f)f~Ker;|#i zM#=f!FjXIjAcg{mTeJxEsKjfKLvgV9%l!wokDl~E*uFHT=D@TzFIx9iJ~1j&TzXW& zch{rcI-TWTghLNI|X;P+pHy;8NRlq0j}Kyv{4Mp4kBr{?DvtE_sN2E{yKN z0}^&)&T<1~z9M_iN~?gNvV^eL|EAudG&(1U8@>sCa?WH1%JK;u`?0jHBK$s@WuZK3 zq!#Z)@BVz!A?|`(4ugR-#S!>< zBLN<=oqcj$nF^|!Ia+(TvGGN-hnD5<)C$-n2N;?2W3fE^j(ibs*q7>Z zH87s@0qsoLO|Y69jhHTiIA>)7EvVx`LE}DM{P>dVI&Q3^Wenp|eH&VZy!@3`hIi$G zvz3@j81na|l&{A3271vfNSg94mJ^(EPAP}@M40K+nR?2Q^y75hk;8Tq<%l17i~lh1e)Cc!|i=t)_2X;X(qK31w12t zK$yZ^S_2;!8fQFE6upCPHP^01hw4p2Bra>N1hv+=r#^~jxd}3;qWe1g5t-rA*PoqH zX(L^oCBJQT!#_p0V`7!X{a=V~=J2_#EvJ%R9C?pC2~D1Ry1(Haj@E{gyB|<0tsa*v zr)H@w&TP%4mV%{AE)NUYz5vS@9Mn#{g;zFtc)ZS=NvY1WR_6!l<4LC6%gRro8#2b# zoGYwueJMYJ4#`Zx?Oq?Ow)`!=J1`4uAFvP5`h04wv{Au4F&pS4kAF7#I(1M6llZgR zcwm40Wm71uI|)dCRjP;>;=JDF&T;xq7lP3J{tPeTF9zG&~Gq@Ff+HS>hU7fO_4PJ`*V)Uz(F9&5V2MMjE5uG{$_svG{@XL=TVS z@3lbJsZp5-$9Lw@xq+o@o(FKDK$$Yfo$4#Xid-d$Z0ZOz{np-OxZ_o+0wNCH>vKf@ z+Afz7{grah#l<9tP`$xAt`bL3gpJ7Ley1}@+rdCxw<`6wxK1nek0nKwQTiK<{-PGg zV6htP0W*!Wr=k<^`bV|oVtm5Y(n$f#KAkhz=P~V$tN8I_vh7;o@BUregBsgpaeBI& z*-)w04YKW_58DGYh&N=;_y~RX5Qn-iAWXXT#^3`k60h3ktHJm2i_8E}I0j>i>1jdJd%8x7AqXR@6 zXDq_uVe4_ZIatH5%H za|NcTJWjXX99-kLC{f~J(=7g+DJ)aT%Z&%VLV;vm!GbQtFUcHon?tbg`W46^bSc%H+DEX9#IdYS2^quB~pFiONwiRR7!atNC?0s zlP~Ure|xlWE;|;8kgX$XY`i}dh&-A0c&BS)(^=^_;{V|(R7z;HLqi=9D@?~JjHEq@~F7Ie1W zBy^p@j|6_?^HGaeD@%$6tc0-3SHv%F*sW5OuWOf(>0>umhd!-u%8GA9rein0Ly5kl>uY6^ihSJ4ZrwIb0cVh--@-+7wzjwzd+1bU*rp!*a%;s){?Pt ze>4rGD7_a*L99tCr-Az>qKi5m;k3-X*1vhaT(}9%) zZ}Tv^jRBE8zjwtLXC$Mzl03&0SbjfneK<8alL&)KMz|b-F#;cfuV6xiU`khfIW%1d zN|y%*b=7%HG|f5sN5=fXGAjkN-YB3o)wfdS@2{A7ySiWC^GfLv>SHS81(P|RNY3r$ zG@*^!_hoM93>5-&=NRZ7Y7)6mQjy2)vH_is96m5>cq^6XJifmNHla zPphZ(SRxv-T?BwXC6S$uDTKqBpN5Qsy7o5;&V}T%*N3Tvjnmsfg|I*_9Po3D10}+N zHr9z4eB87c)Ia38*5YBFg0B6!nzG)CpZnyj0;!$a1%e2O!xp&6&QG)<;Xnwwzok!u zW0@QFv=@x=K$kKgR-cw$Xn{v3R>Ln<)A+Rf!n%1;as&=;AHl?6>38hb7wTYb(sfwK zlSXeecG)yG3m^`7N9{xgJD=PIdK+$;UMU^MFEK`Vp~=$#LM+!RwCM@<`!FH`AHxVf z0;7RT&ifnAal~NjWx_+zx_|L9790NCk)Ju15Jv(C7rXnWxH05P!f&k{jOH zmA&4G@B4Nq8$ZKaCAQJ`R?2!lUa3Z`0AaLU)UWp^SIT_QQK|DlCB6(Cm#dp2t(F8Y5JsOFKj(O0Fz$Ui2#mk~hE63Rum zTX@k?&j_}j8>&4VH$JWHa`*rTr(U>3lwN5SS2EV<#r$0~|TkTBJ~Q!B4>+3tGw@dKFY z{fZoW$w!)Ss2FwrqH5i+OYC&f_tn6J%w}>uo586kXsdr>qchdD1ki|z)&OTOadnwhJpvwuleC1Vs@#)I8VFsxn17!|N``p; z;1dp%l$GVI0|c)?Z7vfUJMvqClf6=FgGpmVuXnk0QVES&Wrm2@zaM*G1X4#6S}lC# zE_o7)KV_W8$@sbq$lgy>MCXm9;z6|>I2=RZ)vGd+p)I0^%hB@pL~a!v1wM*gYb&N< zD~Mh**B|~UsqiSuE0cTc=a%8rDSXg7*!^{Dbl$aG42#HlARMc4i3ifvXW}!V)+X=v@!HsjE9qMHMHEa(j)-j~6*sIvmnxLQjF#H<&p%5!Br z6(}M8Hn-C(F{FB){N?bG`=o!Mxr0MOE;|!qgfdzlbtX{v2pnG82^!=z#b)hmPnQwl zrmJP{og8eO@N>f(GNoPp5WTQ!vv-z>sLBqu|7H5sJ9{|#6Fcs`ZHy%@^!?7jBSe*P zu^2i)D@(QhzMQXdf5!0jHYKM6=iWZw1MrI{t@>D3G(GQB<^Gz5D7SiPJgZlFR+Pvn zYW47{p;Bw@@tIM<13`nc%H-L7kEorqM1>U>PMi-S{oyDWxGrY zy<1O-ushhXSY((oIjKf{{XG6g^6}l3Iq;WRMzcvoAi$+RRFGXA%-PB0^yIMrUSNS2 z2E&y)MSFXU@N|&Kk#gkmK6ols_%;MMacTY#T%4AH^(zN6T+R>O$+rx9Ifb;+65?Bi$&`0sqs`pb18(3%Bk4wnLvTis?#Xu*Tlf zvqRAprflAPT1ZwU{h;D~?<}bz5?l_d&%vAy_vYUXF88evx=R^!Qimb9KEWpl-tb;w zoJ3?SisoBY3aZX1NG`+J2Hl&!^p~sL}kY;@p%>G?4z=gtNZR3S@xUbW# za9OBFNiEXmaLpy)fD`2CyQuwUCm(`$1SyF$M}2vBzFvP^Y@u$9>!_MHbGm=G z$R+5>B#bQLqb59myYI^fBdUyct)Z_*hJ5{lHM#=ddpspv*l48}+3pKT!&H5RUmt+M zi1HOev7^#YaIX$g4;u%IHo{(X^54y-e%@tJc_9IH8Yxuo&o4jQ6H)MRr&+|kKw;Lf zt;CR~FA)C%>E!aAv#zAEzrW+z*g$Ejj|x4c1wmoLpMtT6xG5n|G;U0rI(pX{>_?ob zd{)}xzbQn)qnl%vyl=Yj5W&qE6zKG2^d&KPur~E6tSKaw`$3|ZfL5~t`_c`q?oXlY zlL<&zzqq2-+P6)M7FWG#^J|T%A>wHDFI;EpIrwY4Vfp}< zE4?`RQj<{WK=+?kS$-}~7`B$er(GhcMs{F+s)KVlkd-6W8g-I(US_7EH?|FW!S_MU!=U^IN$h)IlP^IR5MK^=YCq(P}Rs+*ZYwsZUOs$WPf18PvAQ{yzlVD z3tQOoec?AO=D?h88{(dY&woFOK2!eMn&jOB*HPA6%f4aoMw62xJPJ**=Skyl+GyOc z7L5{cE3v37W%FX*;x&nG8G}3_i{QsJnt}aLP`Nl%Z$st>62ykF@Yyk~q}U-ds!koJ zhHeH%y~tdOuXjiMjbUzdx1M2M-x_yST{SEt-p!+WG4#Jv( zz}59j8mP?xqiJegtH!Xkc-J77?BT}JG$Qa_wXa!p#qJvlB7FLXyz200mv``%-)gmt z)US5Oj5M}Z?mH3M3kw?OckK?uqQx@PkyPNQKh)gMKoh0Ybg0P^9OUB8mOeC@F6DAI zWx-4Z$_V+e`(r7a*!HD(#ftWK^MxmGAX$r)Z8dDW9`CF2Taz+WHK~NJgXzm~V#AJr zG$u`Gv0?;6Bs?Uax)LqsI2h+ksgpQaE6d2X*;>QkM)1{O$jf1Pqokn-a|hM&-|sG& zpyT@mg3dY)z2$wZZY3OVgJ0KcZTJoGN0TNtbk~*9W-^DpqU0%pSaPNE7cv(NG(WC? zRfhPh;p^qZn*K_eqd+JzI^}q{ovh|RD6d(^#U}F_aou0O)%32}!bZ7>YmKMmSCyPg z{v?dY-rp$JRiTVu@o}PowNElXkcC(t*Ei6-JWXDikGr0r1dkCNNIrs`pEK3DmWPaVY>X_pkc#brfsh)H6 zZA4}@sI;!Ug~}DSIt)1C6-o5(ODtSx=_D`d(VtmwDwtNbH~`g->p?J43NS!Y0=0Jz)}9v#Mn{=J^tkv!`!VFnto3JrS*%)$;hmn(U*#i=a!0jw zpa+l<#wd8JHmB3EwK6O%t*f#d*)%k+t;n6?)18`)8O!F4fGg(xTxK++SwD5TnR>nb z241`K*@q=;GG6`ptLiN+lWim=7!oL%ij^UWzyzB1`O3ya>hPxz6Tdh|DOsOk;3Ty+q2H^Y7ssz+#s$GQA%hY9Rf^GD{t&A$k zE+lqbuCp3o=*?#`mu}<|GZ1=QurUqI8qec5IQG`JmuMEJD-sa8sqaqpd*gV6RdbV^ zWp&Dgf`hHEa>!^J!w4MjghqWje#N_zrLoFFSB?^jhxhN^x=>cA`=y`76& zc)tE~;im|n-6gD^4+CgI&}2E1HY~%tAZXT}0aBUJ2O?WkM!2{%k!t=c^#bX2X`J{< zvrmA>Kg>7nRmvuT5b+F_H+{_nj<@(cnSj7buQRwE^&^gJ=P^W zy8_Mfn#M%H55gB)(X-k2KSuyRYZbpNkCBLg8*L?fTIbhTC_-N#+G4}P2#+@sk{=vw zm}vhRST}m`d+_InZ7IH4Y~xn5 z;v-rpc!!yFtz7X_mg!#t@6)#;HL|z~^5zkUg^7a!}__f4}}hig%Tp_=_0X zXKu?vg~UeRS1msmet}qo`*=vq&91vWMP!EJ2W8AzW!RtY)^mx+*soaI)hvDvfPRq9 z4Qd|U74NQ*DrhU5{c1Y=6^(&CyorE=&nVN-7#h?LI=Er#JL1XlnPn3-WAsgyp#$#& zz1Y31i-YR!e13702p}jjxj*BajB{bOyr&4o9^4{Dt)yNTI184x1Jy@gL>5Z8-IYF< zm24_IZfSvAoQv@g6Z!E^m9(}`i4lSsyB*hRnC4m&Q_qBFe<^PX^+a8(iZZep1YCVs z-`&f!_kPJjY%NN;6l2}y*6hVc?QKZaJ@w#WK#0T5nJ8=_=(;7SZe~6U2e{NL{(GkB z8uL+QD`+3>Y-M>vqr>>e$xd=h)(KBox!ed9~{Q82F`F_$H zoy!R+i!&kqYV0ua$koMJ1S^#92QYGFz&yjh8bg<6gq?uUzD4Oqx5)XwyBNEej+mr2 zX2Nz&#QbJDy_%t#Mm)ZKX=5F)3#;D1_J6Rfw;Ner&5BErIh)|0BzS#2#FE~=#7mq= zn7$?K>HWPUrSd+Y&iW&EcwZA=SJUBM-|?p~E5T2(oKMD9UlmDU8*7{dKUr;()(4of zOG>G|tT!mZbb37j0L)qPW(BlWq}0cLuzp6C{$4f2g7iK6F&9%rHV*4=mFP{-R(pFE zx`XWuSx$Y?s63|l-ZA7UPhzqr%za)G+j%1b3+BsarHI!7DE^=^S}-#hVEX!{1RvYp zk=FNcREC#-p=fsa4ePsuDfFjZNAnsierBYx$c(zBqJy!0p+3g#&w2VTcLv)Cb|y|c zVzu}Nq4ouxZP6CVsdzQ_JiuYQ9@`Ska!|ROXIsdEC~C~FB=Ypq&)c%G>cj!OIp@J0 zGQZCyKF^x)t%96D?EMv1S`rpPXj9~2-jh@#H}L(v@-IT+UFX;?q9!&0s9t3hBsy3+ zgUx+Dv`pH1mv9c-C$%2ihuT1Wqv?=utD{>W%+x#l0qsw(u)u-^XMXlZdo&ejn)B?E z)3L}nLQ-Mt*1L`!zwQBoojy<0G32bWp(6;IqV5-pG*~0Wwo8Su9b0Y@=VhWURzf*c6l^y~BhSd>&*jxK5P|u%>VI;I$B8tL@u)0;TyC}Em|MMtb z$Arp9i87-T+tIcDiy}w1xvvyeZ06;36KMYXMf|Jg2Y_HGjfKEmQ10Rg%j%z~0GpbV zg&%css5GpJ9*Wdmy_y@Kv}{Q^4Qt$#WN2iJ8Dg3>HR_L1vV{d|vBwbBSHl72!GD5PgCLw;As_ zjL=^yGgJ_I(+_*|b`DtVa<%)9l{cM9$HpFE-__)H3rnOht|a!dB-l(_- zlq)u~VrMkhJo~Xw-y;M1iyzf8y}~1zGp4^W-g~7jzdPxi4oh48oP)LP^B!F?vVN{H zsMC@2-(}4^vk$`jIvUlLU$c!&Oc$k0TVS5YUJ!q4H1UP2>mGNY9RMJg?p6LeGW zwn*U_{|n*pv+meehTgvJsM06CJ&5i*ktSP8JbTjr&wv;crI4-O~#p=F^TzQO^uNTuJ&q{FSvXl-?U zNwdtB<<`&p$bqfyYy(9cKskO_`x982M9=w{JpC(cZw4RFa_i_VGgd^>#MnVOq1iU1 zt#S&CY?i|A+L_Fv&0zg0E^h9&f+jNt%eQj9Pv|5r1dh-6+W59!JROu!=}F*v7N-FDYGZTa|?Pv@F6AK zH)ddoaH0Au_L%~es{7yP6&6&TmUi;Y`&0_c@@ z$n8z=04#!0RFkXltjW1vzwP6<7i9 zq1=P-xwW$a+8-xHd&){Isd*=UbBT1b<{&L7jl259jQBgNRvZN{fs+BFJoFqQ3Un!JMC=Jjg*OA`cGd#I$6TvY7 zmj#gnf=#p};Z0Evy=e}$z}6^JN9)sE2m8uM``{$*uHW?;B#J4iYb9{U$+)7N-Mw3_ zljAm(Ols*f1!BMZfR{|OSB(g>otHmSSoBnZ5gx|yP4t}vEj@?$Lw>3 z=xbxAmi#$vOvoBxTEKy1)&5$n=E=A&EiK9A6L#0O%Eo-5*o$Mfg5@uYugbM39S7!j zUY;EZEnowBOxSs&gAsGFQS%1l&NFVQ^1EsA@v{kcmWVJ85+A2_C!FhB4NS`K3Z^aZXV(i_hVN$@Bg#-pHSb=KI!07#vA3Ds8QYb*Jh`aRwjz&- zNjWLtsOk6F_+Es=Ru}r~J=oq5tiM`TU343|>Uh&SxYrkq4X}6c^XED#--nb%`dq6| zAOEDI%WE#J0R9XPGnE|&T{i247acip7i};BzAaFUeFpM*>C3-Q87s|vu`i$V%b&t6 zcLq~K4$cT;{uu?AMM1!lMYBhW832HtB`VLSxG$5EJ_t_!Zd~aIO0|XGS}c(H1>GzR zvnE>Qh90|KWe#C}|6{|pffM5b;kNB&m0Mu1=x2TEocAuNrSLy$Ds(^oF=gP;@U5t^ zh1`H7;0@v1HSR?}m6BT4!%P9?{RH@uri$nxEDwn}0U~NZGa8h~brhhtk@5cXTv0I13)B8`lL({f2)pz} z`~W&&N*~wRV*UIL`P(%|(u?s&S(I8y6R%?Or^eS*`=#S{48XS#gbP9WK$S0|Ow7$+ zEyw&HIi4l&;JnXJ)UrDsv7YoFYaP_bBuN*=V{h)8P@;@!{jC<4?6qgu5^&0X`vt`8 z!&&`9%jrLQqKOvyK-#?lbtp{OTFj));PCCN#=!2I$$R$L(!@K4QpnR-l)Hy1ndhN5 zm0bv#A}nXS4src#erYf6sW`lnbV|k`jNE`D2VDHJEvVY1#PthiL$tgu~{JBS3j{CUY zMQX&a0r9Z}&TPqeWc3)k5m75;f3*EJ4IkRd(Pg`q(te_UrTRXkxnuU^3pu!#ASr;jagPks$-?&?547%&ua6>Wqbh7iV|0sQPMd#V=(2ZX^6E?8X#k zEi=SucJwW;AZX#`UwXhBe$V9ALDM@fd!NsitJ*}mE+Yo{XVz>87yi=vW{0eWNl?>Y zW)bZ7Lqe(lm_kRQ*1-Zrm{t|KooUwbTb`_;?|va)st`QCkPe_9KlB)xuumMDLL`J4 z?Ijv6Jpcf@9ebXmPb05dqlpgUVuz+YkMRd}{A>u1z3CUHy}r2_2j-*bF6&*0PiOg; zNde2@*!Pd7r=6ar%$CFBqZyr%lQaIe-SZ9MU-QsnsqzHpORaE$(-&>(gQ4C3?f2ow z$#mewy$VK^W1*9P1;iqeGRuN0AL1~tltXE+3Ml&Gh{M!Bm@GY*=0=zMpe>U0LIC93Z%NcnPKZ1#pUQuO1 zOm&BRt{0im3@`A#thXON#?;Xp5&#mo?hPWM3l!V2 zmD-RPv6urkD5GxUQg&%h0YV^rbJN-X)!$-FUHY81g8im7&Z}dL#AKr$9@(5qT>12EUm~4qTjflp5;O6LD zj?qWt%-ZdN@y_6(DmnIW@0SI$|2P4XwSjr>zslIww12R+N2+yip5oa0w*&V06cuhw z{ap?4*kS@n)~+KbW~c$+p;+#z#v#a4P0M*^PO-P8AX(o+m~q#V6o<-l&vHzJn2 z-kB_W)m2rNs`UMfyuZv(dQz*}>0SG8;Jy3|E>p;+4RMi^KFnS`!{rkifR{k8!DU?N z?2hT%kwnK19Oh|Vs`ZQ7s>=Ef;n2Do#kf8tY|dU#^`arz?AsV>WPS&|`8lf^=rNn) zr*jwthAt4sbF+Qs^r-gvr-H!(ho=Cs2LDraajFF4sXSLqjh!pYJ@unaaJjBZ~ z@x#YuiA!jTi%1**inGh8iTZ(Us^;UgYh4*oDAUco)xDH=OLiJrD$o9H&PV>w%+H>& z?m3=F5pW_fHIBYX`@`>KInIH9q~|l{e~qa@50(Ic3I60X39*D&dj`|a(GGN}dGU(u z6*cxQjm62rRLsNg9$+L)QMAR;FnNeWzK#Z5Bj0FnFx$q0sEjo%`0%DS44dY^5oR{Dzix2Tnl75n8WMR6#&Bx=jRN6vt+!-(8 zr1si&wi|TF^38>E;FKN^%=xZW@CZvoqs%raYif`z9Ju!k?E9Ao5haj#pSIBnhU9%1qZT}EFidaO;761eUo7aj-UxeHsDE7V63f*q)?KIFU84-M z=aCf86YGl{A@6uEw0ycQDZu_V)STctyj&_5cTyw?lKhVPUN9?u9mV7cla;|a=t}D= zLv(e`4Q=GU{KD#)|%`XyB3${lxmrG3}tyTLSLq*z~XDLEYvp+(x@sA<9WY6`Xrg|D?rK|07-q(X@63 zH#~-mSvnq-W~u-4B5xjT_rnCZRXWCPF5u zF5W7DV06npdE~2;72~IauBqo{e*3#~*)FSy3gmbkJ^fj7!_A)_nAqBve+GW6G+7OI z{~$3;lRi%weAK!2ZtYxktWq2kn?7w^44)gG_`3r)Lb!CM{G*NuO}5XEFnsOXUt1whWuDNfB(gs@ABL_ zI@6PIr@4MUQ5Xlnm5@o5v1`JbBU3iadn`p5Z<&Qva`WM~F;w!^LaOyB=hwqo?y`)V zwGvR^&W#KX_A(N-cxZ-KT4z55UUC2C<-3@O6%ffDzw7Ahf*G1TzRmKEo0uIp?-BeL zE=a$WS}N?Z#>!GMw$X$NOiolOHXYKmlW;LjfSStTk}gLJmuHBn-`Mwq|JhrWk$7Bk zjG#CkO39?);QMT+*)qam{#*-|{BxMA*8Z#sl4R$ddLlww{I6`?e*eYecMpB7rd%&j zf|BV>*-~joqM1Bb#whn>KjWG!HD~Dt-nDf+hunK2?_xZ5G?2D6pS0zWp?ws#zbtLcN?cxHK?53f~;EXJwh0)!wxWM@LUB zN8M9Q{E}#m{{WmqYLj30Vqpy(RHt;7&Rrb;Gx*odEM}1d@Z2m*!fv)eQpBLO^$q@zT$<;yAtWjL+uDwypsK=K9dRPE+VDEi=HTI2iC9K4MvXl*f zmG!^p18_Ljc|8-g<`Y9mWU%n7lf8oBXjVPF!x@q~ON)+kf1)sXT%j~h4Zw?jo->)3 zOx6hTm{OYL@0M$#3jQef_*L<{y;z;Yyfr8H6|G7B|Fx$#f9NqVoPTiJY2~jwzzAFL zYKBrq3d@3ssngbneANn^9n!Md_3TVVhNo!)`*cf=E}E3^BJ8K=adZ3ZQX{4f?aED) zGmfgS`<}k0nMEn?xr2B^602_f%X6#WyqW*}_}*sVJkm8&20xCn7b4d**vO!K`N)mv7-c|HGW#j}=un> zfvH;bH&MCiqP0_MPwrh8b?;2Z87+p@mp+xQe|1gozWt`@FG^N#x0eVmz58S<6L5r6 zB;D=eu5~;2o$c@6+jt{pezsw$+=0@qO;!x9T{B!?wme<-s`lfz4?na^?tc8c-a|Ui zUL@R;K_kdz|K7kLc?;S5ea$b9+dL19{rfHa(8ZpXex3MRlYt3)-qXL|_9oO$s^9wc zV}XTF>FmuyH+a8(`5S(0*VM||M>)C78{S@K{QmK#*z#QrQ(mM9sa9WowS)b@KbF@o z_e3%TEV!tnQl`CPC3C>MX2I1Fchl}L2RypjAh#GH z*f8nTyOwh9_hJld++*8px$O*C95VZUy}fDp9q8C|H)Ctc)_iMJWbjJnQmr=ASj`+T zWAe{$5f8*H82H#87{-*dEHH>ly0N|absxi&)Ss!xJvZF=%2j?>kl{H~fqD9F29dN% z_cIC+IRQ&o3pOk=vD0f&&Ff@-5Wyg)I>VIF!Bel>eA(uNjh?rp7&W}!Zc?@`|H7!t zFo(;bQ(q#@mmwhJQ|ce{+aaF&7;dC}S}Hj+_mxUKaMtRlkhYoEC)NXMlh-SRY{jUEGYlJa8G1s{}T`%dV+t$1y1na ztlYErRZUFVd~V+Ypx&s@Z*T7IzGTI~n%UIEBlgqAcd4KzLs;PM*B!3!O2ivP{aJsp zrZe&FyYWSdpKl+7#)$=zY~6p}J5FJUIQ!$~Hj_?4>GF_B_5~{Q?;NpTeSAY{uPfhy zGBdT?LSmK-YqlHSoVMWv + Kaseya BMS + diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 52df55dd4726..66ff5b1a482d 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -938,5 +938,35 @@ } ], "mappingRequired": false + }, + { + "name": "ConnectWise PSA", + "id": "ConnectWisePSA", + "type": "ConnectWisePSA", + "cat": "Ticketing", + "logo": "/assets/integrations/connectwise.png", + "comingSoon": true, + "description": "Enable the ConnectWise PSA integration to send alerts to your ticketing system.", + "mappingRequired": false + }, + { + "name": "Autotask PSA", + "id": "AutotaskPSA", + "type": "AutotaskPSA", + "cat": "Ticketing", + "logo": "/assets/integrations/autotask.png", + "comingSoon": true, + "description": "Enable the Autotask PSA integration to send alerts to your ticketing system.", + "mappingRequired": false + }, + { + "name": "Kaseya BMS", + "id": "KaseyaBMS", + "type": "KaseyaBMS", + "cat": "Ticketing", + "logo": "/assets/integrations/kaseya.svg", + "comingSoon": true, + "description": "Enable the Kaseya BMS integration to send alerts to your ticketing system.", + "mappingRequired": false } ] diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index 60ee764853b4..6d3f24f86ee4 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -68,45 +68,83 @@ const Page = () => { status = 'Enabled' } - return ( - - - + {extension.comingSoon && ( + + Coming Soon + + )} + + - - {extension?.logo && ( + {extension?.logo && ( + + )} + + + {extension.description} + + +
    + + + {extension.comingSoon ? ( + <> - )} - - {extension.description} - - -
    - - + Coming Soon + + ) : ( + <> {integrations.isSuccess ? ( { {integrations.isSuccess ? status : 'Loading'} - - - - + + )} + + + + ) + + return ( + + {extension.comingSoon ? ( + cardContent + ) : ( + + {cardContent} + + )} ) })}