diff --git a/.github/workflows/label_sponsor_requests.yml b/.github/workflows/label_sponsor_requests.yml index 479cad06c728..b974cf66776f 100644 --- a/.github/workflows/label_sponsor_requests.yml +++ b/.github/workflows/label_sponsor_requests.yml @@ -1,4 +1,3 @@ ---- name: Label Issues on: issues: @@ -14,4 +13,6 @@ jobs: - name: Sponsor Labels uses: JasonEtco/is-sponsor-label-action@v1.2.0 with: - label: 'Sponsor Request' + label: 'Sponsor Priority' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index ac17d5b76536..bdcffddcdf11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.0.1", + "version": "10.0.3", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 028831b26807..c05615499486 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.0.2" + "version": "10.0.3" } diff --git a/src/api/ApiCall.jsx b/src/api/ApiCall.jsx index e4d9a366908c..a2d7d0396ce1 100644 --- a/src/api/ApiCall.jsx +++ b/src/api/ApiCall.jsx @@ -50,7 +50,7 @@ export function ApiGetCall(props) { title: `${ error.config?.params?.tenantFilter ? error.config?.params?.tenantFilter : "" } Error`, - }) + }), ); } return returnRetry; @@ -211,7 +211,7 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) { if (!query.queryKey || !query.queryKey[0]) return false; const queryKeyStr = String(query.queryKey[0]); const matches = wildcardPatterns.some((pattern) => - queryKeyStr.startsWith(pattern) + queryKeyStr.startsWith(pattern), ); // Debug logging for each query check @@ -220,7 +220,7 @@ export function ApiPostCall({ relatedQueryKeys, onResult }) { queryKey: query.queryKey, queryKeyStr, matchedPattern: wildcardPatterns.find((pattern) => - queryKeyStr.startsWith(pattern) + queryKeyStr.startsWith(pattern), ), }); } @@ -252,8 +252,9 @@ export function ApiGetCallWithPagination({ waiting = true, }) { const dispatch = useDispatch(); + const queryClient = useQueryClient(); const MAX_RETRIES = retry; - const HTTP_STATUS_TO_NOT_RETRY = [401, 403, 404]; + const HTTP_STATUS_TO_NOT_RETRY = [302, 401, 403, 404, 500]; const retryFn = (failureCount, error) => { let returnRetry = true; @@ -261,6 +262,12 @@ export function ApiGetCallWithPagination({ returnRetry = false; } if (isAxiosError(error) && HTTP_STATUS_TO_NOT_RETRY.includes(error.response?.status ?? 0)) { + if ( + error.response?.status === 302 && + error.response?.headers.get("location").includes("/.auth/login/aad") + ) { + queryClient.invalidateQueries({ queryKey: ["authmecipp"] }); + } returnRetry = false; } @@ -270,7 +277,7 @@ export function ApiGetCallWithPagination({ message: getCippError(error), title: "Error", toastError: error, - }) + }), ); } return returnRetry; diff --git a/src/components/CippComponents/CippAddTestReportDrawer.jsx b/src/components/CippComponents/CippAddTestReportDrawer.jsx index d7ee1646656f..6ea7d84ea0bb 100644 --- a/src/components/CippComponents/CippAddTestReportDrawer.jsx +++ b/src/components/CippComponents/CippAddTestReportDrawer.jsx @@ -43,13 +43,13 @@ export const CippAddTestReportDrawer = ({ buttonText = "Create custom report" }) const createReport = ApiPostCall({ urlFromData: true, - relatedQueryKeys: ["ListTestReports"], + relatedQueryKeys: "ListTestReports", }); // Fetch available tests for the form const availableTestsApi = ApiGetCall({ url: "/api/ListAvailableTests", - queryKey: ["ListAvailableTests"], + queryKey: "ListAvailableTests", }); const availableTests = availableTestsApi.data || { IdentityTests: [], DevicesTests: [] }; diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index 9d3e284e3678..9b79b58fd32c 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -333,7 +333,7 @@ export const useCippUserActions = () => { labelField: (option) => option?.calculatedGroupType ? `${option.displayName} (${option.calculatedGroupType})` - : option?.displayName ?? "", + : (option?.displayName ?? ""), valueField: "id", addedField: { groupType: "groupType", @@ -549,6 +549,17 @@ export const useCippUserActions = () => { "Are you sure you want to change the source of authority for [userPrincipalName]? Setting it to On-Premises Managed will take until the next sync cycle to show the change.", multiPost: false, }, + { + label: "Reprocess License Assignments", + type: "POST", + icon: , + url: "/api/ExecReprocessUserLicenses", + data: { ID: "id", userPrincipalName: "userPrincipalName" }, + confirmText: + "Are you sure you want to reprocess license assignments for [userPrincipalName]?", + multiPost: false, + condition: (row) => canWriteUser, + }, { label: "Revoke all user sessions", type: "POST", diff --git a/src/components/CippWizard/CippSAMDeploy.jsx b/src/components/CippWizard/CippSAMDeploy.jsx index 2cb619fef7aa..d38d0f66ddf2 100644 --- a/src/components/CippWizard/CippSAMDeploy.jsx +++ b/src/components/CippWizard/CippSAMDeploy.jsx @@ -92,7 +92,10 @@ export const CippSAMDeploy = (props) => { here -
  • (Temporary) Global Administrator permissions for the CIPP Service Account
  • +
  • + An account with at minimum:
  • Application Administrator
  • +
  • User Administrator
  • +
  • Multi-factor authentication enabled for the CIPP Service Account, with no trusted locations or other exclusions. diff --git a/src/data/alerts.json b/src/data/alerts.json index dd6d8b6cc065..22a8f06ca072 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -331,5 +331,39 @@ "label": "Alert on quarantine release requests", "recommendedRunInterval": "30m", "description": "Monitors for user requests to release quarantined messages and provides a CIPP-native alternative to the external email forwarding method. This helps MSPs maintain secure configurations while getting timely notifications about quarantine activity. Links to the tenant's quarantine page are provided in alerts." + }, + { + "name": "SecureScore", + "label": "Alert on a low Secure Score", + "recommendedRunInterval": "1d", + "requiresInput": true, + "multipleInput": true, + "inputs": [ + { + "inputType": "autoComplete", + "inputLabel": "Threshold type absolute number or percent", + "inputName": "ThresholdType", + "creatable": false, + "multiple": false, + "options": [ + { + "label": "Percent", + "value": "percent" + }, + { + "label": "Absolute", + "value": "absolute" + } + ], + "required": true + }, + { + "inputType": "number", + "inputLabel": "Threshold Value (below this will trigger the alert)", + "inputName": "InputValue", + "required": true + } + ], + "description": "Monitors Secure Score and alerts when it falls below the specified threshold (absolute or percent value). Helps identify security gaps and areas for improvement." } ] diff --git a/src/layouts/config.js b/src/layouts/config.js index d735b015c94c..82e68a157e9c 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -719,7 +719,12 @@ export const nativeMenuItems = [ { title: "Mailbox Permissions", path: "/email/reports/mailbox-permissions", - permissions: ["Exchange.Mailbox.Read"], + permissions: ["Exchange.Mailbox.*"], + }, + { + title: "Calendar Permissions", + path: "/email/reports/calendar-permissions", + permissions: ["Exchange.Mailbox.*"], }, { title: "Anti-Phishing Filters", diff --git a/src/pages/cipp/logs/index.js b/src/pages/cipp/logs/index.js index 18f8ad68174c..41eac2225d46 100644 --- a/src/pages/cipp/logs/index.js +++ b/src/pages/cipp/logs/index.js @@ -37,7 +37,7 @@ const pageTitle = "Logbook Results"; const actions = [ { label: "View Log Entry", - link: "/cipp/logs/logentry?logentry=[RowKey]", + link: "/cipp/logs/logentry?logentry=[RowKey]&dateFilter=[DateFilter]", icon: , color: "primary", }, @@ -100,14 +100,14 @@ const Page = () => { setStartDate( data.startDate ? new Date(data.startDate * 1000).toISOString().split("T")[0].replace(/-/g, "") - : null + : null, ); // Format end date if available setEndDate( data.endDate ? new Date(data.endDate * 1000).toISOString().split("T")[0].replace(/-/g, "") - : null + : null, ); // Set username filter if available @@ -117,7 +117,7 @@ const Page = () => { setSeverity( data.severity && data.severity.length > 0 ? data.severity.map((item) => item.value).join(",") - : null + : null, ); // Close the accordion after applying filters @@ -157,13 +157,13 @@ const Page = () => { <> {startDate ? new Date( - startDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00" + startDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00", ).toLocaleDateString() : new Date().toLocaleDateString()} {startDate && endDate ? " - " : ""} {endDate ? new Date( - endDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00" + endDate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3") + "T00:00:00", ).toLocaleDateString() : ""} diff --git a/src/pages/cipp/logs/logentry.js b/src/pages/cipp/logs/logentry.js index 2dbb4a23a5a7..175dcc915aba 100644 --- a/src/pages/cipp/logs/logentry.js +++ b/src/pages/cipp/logs/logentry.js @@ -11,12 +11,13 @@ import { getCippTranslation } from "../../../utils/get-cipp-translation"; const Page = () => { const router = useRouter(); - const { logentry } = router.query; + const { logentry, dateFilter } = router.query; const logRequest = ApiGetCall({ url: `/api/Listlogs`, data: { logentryid: logentry, + dateFilter: dateFilter, }, queryKey: `GetLogEntry-${logentry}`, waiting: !!logentry, @@ -44,12 +45,12 @@ const Page = () => { logData.Severity === "CRITICAL" ? "error" : logData.Severity === "Error" - ? "error" - : logData.Severity === "Warn" - ? "warning" - : logData.Severity === "Info" - ? "info" - : "default" + ? "error" + : logData.Severity === "Warn" + ? "warning" + : logData.Severity === "Info" + ? "info" + : "default" } variant="filled" /> diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index f59032fbcba5..89abb862d188 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -2,25 +2,25 @@ import tabOptions from "./tabOptions"; import { TabbedLayout } from "/src/layouts/TabbedLayout"; import { Layout as DashboardLayout } from "/src/layouts/index.js"; import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; -import { Button, SvgIcon } from "@mui/material"; +import { Button, SvgIcon, Stack } from "@mui/material"; import { TrashIcon } from "@heroicons/react/24/outline"; -import { Add } from "@mui/icons-material"; +import { Add, RestartAlt } from "@mui/icons-material"; import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../hooks/use-dialog"; const Page = () => { const pageTitle = "Excluded Licenses"; - const apiUrl = "/api/ExecExcludeLicenses"; - const apiData = { List: true }; + const apiUrl = "/api/ListExcludedLicenses"; const createDialog = useDialog(); + const resetDialog = useDialog(); const simpleColumns = ["Product_Display_Name", "GUID"]; const actions = [ { label: "Delete Exclusion", type: "POST", - url: "/api/ExecExcludeLicenses?RemoveExclusion=true", - data: { GUID: "GUID" }, + url: "/api/ExecExcludeLicenses", + data: { Action: "!RemoveExclusion", GUID: "GUID" }, confirmText: "Do you want to delete this exclusion?", color: "error", icon: ( @@ -31,21 +31,32 @@ const Page = () => { }, ]; - const AddExcludedLicense = () => { + const CardButtons = () => { return ( - + + + + ); }; @@ -60,9 +71,9 @@ const Page = () => { title={pageTitle} queryKey="ExcludedLicenses" apiUrl={apiUrl} - cardButton={} - apiData={apiData} + cardButton={} actions={actions} + apiDataKey="Results" offCanvas={offCanvas} simpleColumns={simpleColumns} tenantInTitle={false} @@ -85,15 +96,34 @@ const Page = () => { }, ]} api={{ - url: "/api/ExecExcludeLicenses?AddExclusion=true", + url: "/api/ExecExcludeLicenses", confirmText: "Add a license to the exclusion table, make sure to enter the correct GUID and SKU Name", type: "POST", - data: {}, + data: { Action: "!AddExclusion" }, replacementBehaviour: "removeNulls", relatedQueryKeys: ["ExcludedLicenses"], }} /> + ); }; diff --git a/src/pages/email/reports/calendar-permissions/index.js b/src/pages/email/reports/calendar-permissions/index.js new file mode 100644 index 000000000000..d945e29b8c10 --- /dev/null +++ b/src/pages/email/reports/calendar-permissions/index.js @@ -0,0 +1,117 @@ +import { Layout as DashboardLayout } from "/src/layouts/index.js"; +import { CippTablePage } from "/src/components/CippComponents/CippTablePage.jsx"; +import { useState } from "react"; +import { + Button, + FormControlLabel, + Switch, + Alert, + SvgIcon, + IconButton, + Tooltip, +} from "@mui/material"; +import { useSettings } from "../../../../hooks/use-settings"; +import { Stack } from "@mui/system"; +import { Sync, Info } from "@mui/icons-material"; +import { useDialog } from "../../../../hooks/use-dialog"; +import { CippApiDialog } from "../../../../components/CippComponents/CippApiDialog"; + +const Page = () => { + const [byUser, setByUser] = useState(true); + const currentTenant = useSettings().currentTenant; + const syncDialog = useDialog(); + + const isAllTenants = currentTenant === "AllTenants"; + + const columns = byUser + ? [ + ...(isAllTenants ? ["Tenant"] : []), + "User", + "UserMailboxType", + "Permissions", + "MailboxCacheTimestamp", + "PermissionCacheTimestamp", + ] + : [ + ...(isAllTenants ? ["Tenant"] : []), + "CalendarUPN", + "CalendarDisplayName", + "CalendarType", + "Permissions", + "MailboxCacheTimestamp", + "PermissionCacheTimestamp", + ]; + + // Compute apiData based on byUser directly (no useState needed) + const apiData = { + UseReportDB: true, + ByUser: byUser, + }; + + const pageActions = [ + + + + + + + + setByUser(e.target.checked)} color="primary" /> + } + label="Group by User" + labelPlacement="start" + /> + , + ]; + + return ( + <> + {currentTenant && currentTenant !== "" ? ( + + ) : ( + Please select a tenant to view calendar permissions. + )} + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; diff --git a/src/pages/identity/administration/groups/index.js b/src/pages/identity/administration/groups/index.js index af5182224a12..4289bb9d01e6 100644 --- a/src/pages/identity/administration/groups/index.js +++ b/src/pages/identity/administration/groups/index.js @@ -287,6 +287,11 @@ const Page = () => { "mailEnabled", "securityEnabled", "visibility", + "assignedLicenses", + "licenseProcessingState.state", + "onPremisesSamAccountName", + "membershipRule", + "onPremisesSyncEnabled", ], actions: actions, }; @@ -320,11 +325,11 @@ const Page = () => { "mailNickname", "groupType", "assignedLicenses", + "licenseProcessingState.state", "visibility", "onPremisesSamAccountName", "membershipRule", "onPremisesSyncEnabled", - "userPrincipalName", ]} /> ); diff --git a/src/pages/identity/administration/users/index.js b/src/pages/identity/administration/users/index.js index 27148701979b..8f5d613095d1 100644 --- a/src/pages/identity/administration/users/index.js +++ b/src/pages/identity/administration/users/index.js @@ -50,6 +50,7 @@ const Page = () => { "onPremisesLastSyncDateTime", // OnPrem Last Sync "onPremisesDistinguishedName", // OnPrem DN "otherMails", // Alternate Email Addresses + "licenseAssignmentStates", // License Assignment States ], actions: userActions, }; @@ -85,7 +86,7 @@ const Page = () => { Endpoint: "users", manualPagination: true, $select: - "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", + "id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,officeLocation,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,licenseAssignmentStates,onPremisesSyncEnabled,OnPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesDistinguishedName", $count: true, $orderby: "displayName", $top: 999, @@ -101,6 +102,7 @@ const Page = () => { "businessPhones", "proxyAddresses", "assignedLicenses", + "licenseAssignmentStates", ]} filters={filters} /> diff --git a/src/pages/tenant/administration/alert-configuration/alert.jsx b/src/pages/tenant/administration/alert-configuration/alert.jsx index 91d671113f6b..a156d6086093 100644 --- a/src/pages/tenant/administration/alert-configuration/alert.jsx +++ b/src/pages/tenant/administration/alert-configuration/alert.jsx @@ -958,6 +958,16 @@ const AlertWizard = () => { name={commandValue.value?.inputName} formControl={formControl} label={commandValue.value?.inputLabel} + required={commandValue.value?.required || false} + {...(commandValue.value?.inputType === 'autoComplete' + ? { + options: commandValue.value?.options, + creatable: commandValue.value?.creatable || true, + multiple: commandValue.value?.multiple || true, + } + : {} + ) + } /> )} {commandValue?.value?.multipleInput && @@ -974,6 +984,16 @@ const AlertWizard = () => { name={input.inputName} formControl={formControl} label={input.inputLabel} + required={input.required || false} + {...(input.inputType === 'autoComplete' + ? { + options: input.options, + creatable: input.creatable ?? true, + multiple: input.multiple ?? true, + } + : {} + ) + } /> diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 5b6539767fce..ef11f5e33e34 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -35,6 +35,7 @@ import { NotificationImportant, Construction, Schedule, + Check, } from "@mui/icons-material"; import standards from "/src/data/standards.json"; import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; @@ -91,7 +92,7 @@ const Page = () => { // Selected template object (safe lookup) const selectedTemplate = useMemo( () => templates.find((t) => t.GUID === templateId), - [templates, templateId] + [templates, templateId], ); // Run the report once @@ -123,7 +124,7 @@ const Page = () => { useEffect(() => { if (templateId && templateDetails.isSuccess && templateDetails.data) { const selectedTemplate = templateDetails.data.find( - (template) => template.GUID === templateId + (template) => template.GUID === templateId, ); if (selectedTemplate && comparisonApi.isSuccess && comparisonApi.data) { @@ -157,30 +158,55 @@ const Page = () => { const itemTemplateId = expandedTemplate.GUID; const standardId = `standards.IntuneTemplate.${itemTemplateId}`; const standardInfo = standards.find( - (s) => s.name === `standards.IntuneTemplate` + (s) => s.name === `standards.IntuneTemplate`, ); // Find the tenant's value for this specific template const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId + (s) => s.standardId === standardId, ); // Get the standard object and its value from the tenant object const standardObject = currentTenantObj?.[standardId]; const directStandardValue = standardObject?.Value; - // Determine compliance status + // Determine compliance status - match main logic let isCompliant = false; - // For IntuneTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { - isCompliant = true; - } else if ( - directStandardValue !== undefined && - typeof directStandardValue !== "object" + // FIRST: Check if CurrentValue and ExpectedValue exist and match + if ( + standardObject?.CurrentValue !== undefined && + standardObject?.ExpectedValue !== undefined ) { + const sortedCurrent = + typeof standardObject.CurrentValue === "object" && + standardObject.CurrentValue !== null + ? Object.keys(standardObject.CurrentValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.CurrentValue[key]; + return obj; + }, {}) + : standardObject.CurrentValue; + const sortedExpected = + typeof standardObject.ExpectedValue === "object" && + standardObject.ExpectedValue !== null + ? Object.keys(standardObject.ExpectedValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.ExpectedValue[key]; + return obj; + }, {}) + : standardObject.ExpectedValue; + isCompliant = + JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); + } + // SECOND: Check if Value is explicitly true + else if (directStandardValue === true) { isCompliant = true; - } else if (currentTenantStandard) { + } + // THIRD: Fall back to currentTenantStandard + else if (currentTenantStandard) { isCompliant = currentTenantStandard.value === true; } @@ -220,8 +246,8 @@ const Page = () => { complianceStatus: isOverridden ? "Overridden" : isCompliant - ? "Compliant" - : "Non-Compliant", + ? "Compliant" + : "Non-Compliant", isOverridden, overridingTemplateId: isOverridden ? tenantTemplateId : null, overridingTemplateName, @@ -258,12 +284,12 @@ const Page = () => { if (itemTemplateId) { const standardId = `standards.IntuneTemplate.${itemTemplateId}`; const standardInfo = standards.find( - (s) => s.name === `standards.IntuneTemplate` + (s) => s.name === `standards.IntuneTemplate`, ); // Find the tenant's value for this specific template const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId + (s) => s.standardId === standardId, ); // Get the standard object and its value from the tenant object @@ -320,8 +346,8 @@ const Page = () => { complianceStatus: isOverridden ? "Overridden" : isCompliant - ? "Compliant" - : "Non-Compliant", + ? "Compliant" + : "Non-Compliant", isOverridden, overridingTemplateId: isOverridden ? tenantTemplateId : null, overridingTemplateName, @@ -374,12 +400,12 @@ const Page = () => { const itemTemplateId = expandedTemplate.GUID; const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; const standardInfo = standards.find( - (s) => s.name === `standards.ConditionalAccessTemplate` + (s) => s.name === `standards.ConditionalAccessTemplate`, ); // Find the tenant's value for this specific template const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId + (s) => s.standardId === standardId, ); const standardObject = currentTenantObj?.[standardId]; const directStandardValue = standardObject?.Value; @@ -390,10 +416,40 @@ const Page = () => { : null; let isCompliant = false; - // For ConditionalAccessTemplate, the value is true if compliant, or an object with comparison data if not compliant - if (directStandardValue === true) { + // FIRST: Check if CurrentValue and ExpectedValue exist and match + if ( + standardObject?.CurrentValue !== undefined && + standardObject?.ExpectedValue !== undefined + ) { + const sortedCurrent = + typeof standardObject.CurrentValue === "object" && + standardObject.CurrentValue !== null + ? Object.keys(standardObject.CurrentValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.CurrentValue[key]; + return obj; + }, {}) + : standardObject.CurrentValue; + const sortedExpected = + typeof standardObject.ExpectedValue === "object" && + standardObject.ExpectedValue !== null + ? Object.keys(standardObject.ExpectedValue) + .sort() + .reduce((obj, key) => { + obj[key] = standardObject.ExpectedValue[key]; + return obj; + }, {}) + : standardObject.ExpectedValue; + isCompliant = + JSON.stringify(sortedCurrent) === JSON.stringify(sortedExpected); + } + // SECOND: Check if Value is explicitly true + else if (directStandardValue === true) { isCompliant = true; - } else { + } + // THIRD: Otherwise not compliant + else { isCompliant = false; } @@ -423,8 +479,8 @@ const Page = () => { complianceStatus: isOverridden ? "Overridden" : isCompliant - ? "Compliant" - : "Non-Compliant", + ? "Compliant" + : "Non-Compliant", complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", standardDescription: standardInfo?.helpText || "", @@ -461,12 +517,12 @@ const Page = () => { if (itemTemplateId) { const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}`; const standardInfo = standards.find( - (s) => s.name === `standards.ConditionalAccessTemplate` + (s) => s.name === `standards.ConditionalAccessTemplate`, ); // Find the tenant's value for this specific template const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId + (s) => s.standardId === standardId, ); const standardObject = currentTenantObj?.[standardId]; const directStandardValue = standardObject?.Value; @@ -509,8 +565,8 @@ const Page = () => { complianceStatus: isOverridden ? "Overridden" : isCompliant - ? "Compliant" - : "Non-Compliant", + ? "Compliant" + : "Non-Compliant", complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", standardDescription: standardInfo?.helpText || "", @@ -553,7 +609,7 @@ const Page = () => { // Find the tenant's value for this template const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId + (s) => s.standardId === standardId, ); const standardObject = currentTenantObj?.[standardId]; const directStandardValue = standardObject?.Value; @@ -633,8 +689,8 @@ const Page = () => { complianceStatus: isOverridden ? "Overridden" : isCompliant - ? "Compliant" - : "Non-Compliant", + ? "Compliant" + : "Non-Compliant", complianceDetails: standardInfo?.docsDescription || standardInfo?.helpText || "", standardDescription: standardInfo?.helpText || "", standardImpact: standardInfo?.impact || "Medium Impact", @@ -674,44 +730,119 @@ const Page = () => { actions.filter( (action) => action?.value.toLowerCase() === "report" || - action?.value.toLowerCase() === "remediate" + action?.value.toLowerCase() === "remediate", ).length > 0; // Find the tenant's value for this standard const currentTenantStandard = currentTenantData.find( - (s) => s.standardId === standardId + (s) => s.standardId === standardId, ); // Check if the standard is directly in the tenant object (like "standards.AuditLog": {...}) const standardIdWithoutPrefix = standardId.replace("standards.", ""); const standardObject = currentTenantObj?.[standardId]; + console.log( + "standardId:", + standardId, + "includes IntuneTag:", + standardId.includes("IntuneTag"), + ); + + // Debug logging for Intune tags + if (standardId.includes("IntuneTag") || standardId.includes("intuneTag")) { + console.log(`[${standardId}] standardObject:`, { + standardObject, + hasCurrentValue: standardObject?.CurrentValue !== undefined, + hasExpectedValue: standardObject?.ExpectedValue !== undefined, + Value: standardObject?.Value, + CurrentValue: standardObject?.CurrentValue, + ExpectedValue: standardObject?.ExpectedValue, + }); + } + // Extract the actual value from the standard object (new data structure includes .Value property) const directStandardValue = standardObject?.Value; - // Determine compliance - use backend's logic: Value === true OR CurrentValue === ExpectedValue + // Determine compliance - prioritize Value field, then CurrentValue/ExpectedValue comparison let isCompliant = false; let reportingDisabled = !reportingEnabled; - if (directStandardValue === true) { - // Boolean true means compliant + // Helper function to compare values, handling arrays with order-independent comparison + const compareValues = (val1, val2) => { + // If both are arrays, compare as sets (order-independent) + if (Array.isArray(val1) && Array.isArray(val2)) { + if (val1.length !== val2.length) return false; + // Sort both arrays by their JSON representation for consistent comparison + const sorted1 = [...val1].sort((a, b) => + JSON.stringify(a).localeCompare(JSON.stringify(b)), + ); + const sorted2 = [...val2].sort((a, b) => + JSON.stringify(a).localeCompare(JSON.stringify(b)), + ); + return JSON.stringify(sorted1) === JSON.stringify(sorted2); + } + // For objects, sort keys to ensure consistent comparison + if ( + typeof val1 === "object" && + val1 !== null && + typeof val2 === "object" && + val2 !== null + ) { + const sortedVal1 = Object.keys(val1) + .sort() + .reduce((obj, key) => { + obj[key] = val1[key]; + return obj; + }, {}); + const sortedVal2 = Object.keys(val2) + .sort() + .reduce((obj, key) => { + obj[key] = val2[key]; + return obj; + }, {}); + return JSON.stringify(sortedVal1) === JSON.stringify(sortedVal2); + } + // Otherwise use standard JSON comparison + return JSON.stringify(val1) === JSON.stringify(val2); + }; + + // FIRST: Check if CurrentValue and ExpectedValue exist and match (most reliable) + if ( + standardObject?.CurrentValue !== undefined && + standardObject?.ExpectedValue !== undefined + ) { + isCompliant = compareValues( + standardObject.CurrentValue, + standardObject.ExpectedValue, + ); + // Debug logging for Intune tags + if (standardId.includes("IntuneTag") || standardId.includes("intuneTag")) { + console.log(`[${standardId}] Comparing CurrentValue vs ExpectedValue:`, { + CurrentValue: standardObject.CurrentValue, + ExpectedValue: standardObject.ExpectedValue, + isCompliant, + currentJSON: JSON.stringify(standardObject.CurrentValue), + expectedJSON: JSON.stringify(standardObject.ExpectedValue), + areEqual: + JSON.stringify(standardObject.CurrentValue) === + JSON.stringify(standardObject.ExpectedValue), + }); + } + } + // SECOND: Check if Value is explicitly true (compliant) or false (non-compliant) + else if (directStandardValue === true) { isCompliant = true; - } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { - // Compare CurrentValue and ExpectedValue (backend's comparison logic) - isCompliant = - JSON.stringify(standardObject.CurrentValue) === - JSON.stringify(standardObject.ExpectedValue); - } else if (standardObject?.CurrentValue && standardObject?.ExpectedValue) { - // Compare CurrentValue and ExpectedValue (backend's comparison logic) - isCompliant = - JSON.stringify(standardObject.CurrentValue) === - JSON.stringify(standardObject.ExpectedValue); - } else if (directStandardValue !== undefined) { - // For non-boolean values, use strict equality + } else if (directStandardValue === false) { + isCompliant = false; + } + // THIRD: For other non-boolean, non-null values, use strict equality with template settings + else if (directStandardValue !== undefined && directStandardValue !== null) { isCompliant = JSON.stringify(directStandardValue) === JSON.stringify(standardSettings); - } else if (currentTenantStandard) { - // Fall back to the previous logic if the standard is not directly in the tenant object + } + // FOURTH: Fall back to the previous logic if the standard is not directly in the tenant object + else if (currentTenantStandard) { if (typeof standardSettings === "boolean" && standardSettings === true) { isCompliant = currentTenantStandard.value === true; } else { @@ -725,8 +856,8 @@ const Page = () => { const complianceStatus = reportingDisabled ? "Reporting Disabled" : isCompliant - ? "Compliant" - : "Non-Compliant"; + ? "Compliant" + : "Non-Compliant"; // Check if this standard is overridden by another template const tenantTemplateId = standardObject?.TemplateId; @@ -913,14 +1044,14 @@ const Page = () => { const compliancePercentage = allCount > 0 ? Math.round( - (compliantCount / (allCount - reportingDisabledCount - overriddenCount || 1)) * 100 + (compliantCount / (allCount - reportingDisabledCount - overriddenCount || 1)) * 100, ) : 0; const missingLicensePercentage = allCount > 0 ? Math.round( - (missingLicenseCount / (allCount - reportingDisabledCount - overriddenCount || 1)) * 100 + (missingLicenseCount / (allCount - reportingDisabledCount - overriddenCount || 1)) * 100, ) : 0; @@ -1102,7 +1233,7 @@ const Page = () => { query: query, }, undefined, - { shallow: true } + { shallow: true }, ); }} sx={{ width: 300 }} @@ -1198,8 +1329,8 @@ const Page = () => { compliancePercentage === 100 ? "success" : compliancePercentage >= 50 - ? "warning" - : "error" + ? "warning" + : "error" } /> { missingLicensePercentage === 0 ? "success" : missingLicensePercentage <= 25 - ? "warning" - : "error" + ? "warning" + : "error" } /> { standard.complianceStatus === "Compliant" ? "success.main" : standard.complianceStatus === "Overridden" - ? "warning.main" - : standard.complianceStatus === "Reporting Disabled" - ? "grey.500" - : "error.main", + ? "warning.main" + : standard.complianceStatus === "Reporting Disabled" + ? "grey.500" + : "error.main", }} > {standard.complianceStatus === "Compliant" ? ( @@ -1481,78 +1612,94 @@ const Page = () => { - {!standard.standardValue ? ( - - This data has not yet been collected. Collect the data by pressing - the report button on the top of the page. - - ) : ( + {/* Show Expected Configuration with property-by-property breakdown */} + {standard.currentTenantValue?.ExpectedValue !== undefined ? ( - - - {standard.standardValue && - typeof standard.standardValue === "object" && - Object.keys(standard.standardValue).length > 0 ? ( - Object.entries(standard.standardValue).map(([key, value]) => ( - + + Expected Configuration + + {typeof standard.currentTenantValue.ExpectedValue === "object" && + standard.currentTenantValue.ExpectedValue !== null ? ( + + {Object.entries(standard.currentTenantValue.ExpectedValue).map( + ([key, val]) => ( + - {key}: + {key} - - {typeof value === "object" && value !== null - ? value?.label || JSON.stringify(value) - : value === true - ? "Enabled" - : value === false - ? "Disabled" - : String(value)} - + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + - )) - ) : ( - - {standard.standardValue === true ? ( - - This setting is configured correctly - - ) : standard.standardValue === false ? ( - - This setting is not configured correctly - - ) : standard.standardValue !== undefined ? ( - typeof standard.standardValue === "object" ? ( - "No settings configured" - ) : ( - String(standard.standardValue) - ) - ) : ( - - This setting is not configured, or data has not been - collected. If you are getting this after data - collection, the tenant might not be licensed for this - feature - - )} - + ), )} + + ) : ( + + + {String(standard.currentTenantValue.ExpectedValue)} + - + )} + ) : ( + + This data has not yet been collected. Collect the data by pressing + the report button on the top of the page. + )} @@ -1563,8 +1710,8 @@ const Page = () => { standard.standardImpactColour === "info" ? "info" : standard.standardImpactColour === "warning" - ? "warning" - : "error" + ? "warning" + : "error" } sx={{ mr: 1 }} /> @@ -1629,10 +1776,10 @@ const Page = () => { standard.complianceStatus === "Compliant" ? "success.main" : standard.complianceStatus === "Overridden" - ? "warning.main" - : standard.complianceStatus === "Reporting Disabled" - ? "grey.500" - : "error.main", + ? "warning.main" + : standard.complianceStatus === "Reporting Disabled" + ? "grey.500" + : "error.main", borderRadius: "50%", width: 8, height: 8, @@ -1652,7 +1799,7 @@ const Page = () => { } size="small" label={`${new Date( - standard.currentTenantValue.LastRefresh + standard.currentTenantValue.LastRefresh, ).toLocaleString()}`} variant="outlined" /> @@ -1689,109 +1836,244 @@ const Page = () => { ) : standard.complianceStatus === "Compliant" ? ( <> - - This setting is configured correctly - - - ) : standard.currentTenantValue?.Value === false ? ( - <> - - This setting is not configured correctly - - {/* Show Current/Expected values for non-compliant standards */} - {standard.currentTenantValue?.CurrentValue && - standard.currentTenantValue?.ExpectedValue && ( - - - - Expected - - + {/* Show Current value property-by-property for compliant standards */} + {standard.currentTenantValue?.CurrentValue !== undefined ? ( + typeof standard.currentTenantValue.CurrentValue === + "object" && + standard.currentTenantValue.CurrentValue !== null ? ( + + + Current Configuration + + {Object.entries( + standard.currentTenantValue.CurrentValue, + ).map(([key, val]) => ( + - {JSON.stringify( - standard.currentTenantValue.ExpectedValue, - null, - 2 - )} + {key} + + + + + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + - - + ))} + + ) : ( + + + Current Configuration + + - Current + {String(standard.currentTenantValue.CurrentValue)} - + + + ) + ) : null} + + ) : ( + <> + {standard.currentTenantValue?.Value === false && ( + + This setting is not configured correctly + + )} + {/* Show Current value property-by-property for non-compliant standards */} + {standard.currentTenantValue?.CurrentValue !== undefined && + (typeof standard.currentTenantValue.CurrentValue === + "object" && + standard.currentTenantValue.CurrentValue !== null ? ( + + + Current Configuration + + {Object.entries( + standard.currentTenantValue.CurrentValue, + ).map(([key, val]) => ( + - {JSON.stringify( - standard.currentTenantValue.CurrentValue, - null, - 2 - )} + {key} + + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + + ))} + + ) : ( + + + Current Configuration + + + + {String(standard.currentTenantValue.CurrentValue)} + - )} + ))} - ) : null} + )} {/* Only show values if they're not simple true/false that's already covered by the alerts above */} {!( @@ -1810,7 +2092,7 @@ const Page = () => { key === "Value" && (standard.currentTenantValue?.Value === true || standard.currentTenantValue?.Value === false) - ) + ), ) .map(([key, value]) => { const actualValue = key === "Value" ? value : value; @@ -1858,8 +2140,8 @@ const Page = () => { standard.complianceStatus === "Compliant" ? "success.main" : isDifferent - ? "error.main" - : "inherit", + ? "error.main" + : "inherit", fontWeight: standard.complianceStatus === "Non-Compliant" && isDifferent @@ -1902,10 +2184,10 @@ const Page = () => { standard.complianceStatus === "Compliant" ? "success.main" : standard.complianceStatus === "Overridden" - ? "warning.main" - : standard.complianceStatus === "Reporting Disabled" - ? "text.secondary" - : "error.main", + ? "warning.main" + : standard.complianceStatus === "Reporting Disabled" + ? "text.secondary" + : "error.main", fontWeight: standard.complianceStatus === "Non-Compliant" ? "medium" @@ -1924,26 +2206,265 @@ const Page = () => { standard.overridingTemplateId} ) : standard.complianceStatus === "Compliant" ? ( - - This setting is configured correctly - - ) : standard.currentTenantValue?.Value === false || - standard.currentTenantValue === false ? ( - - This setting is not configured correctly - - ) : standard.currentTenantValue !== undefined ? ( - String( - standard.currentTenantValue?.Value !== undefined - ? standard.currentTenantValue?.Value - : standard.currentTenantValue - ) + <> + {/* Show Current value property-by-property in card view */} + {standard.currentTenantValue?.CurrentValue !== undefined ? ( + typeof standard.currentTenantValue.CurrentValue === + "object" && + standard.currentTenantValue.CurrentValue !== null ? ( + + + Current Configuration + + {Object.entries( + standard.currentTenantValue.CurrentValue, + ).map(([key, val]) => ( + + + {key} + + + + + + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + + + ))} + + ) : ( + + + Current Configuration + + + + {String(standard.currentTenantValue.CurrentValue)} + + + + ) + ) : null} + ) : ( - - This setting is not configured, or data has not been collected. - If you are getting this after data collection, the tenant might - not be licensed for this feature - + <> + {(standard.currentTenantValue?.Value === false || + standard.currentTenantValue === false) && ( + + This setting is not configured correctly + + )} + {/* Show Current value property-by-property for non-compliant standards in card view */} + {standard.currentTenantValue?.CurrentValue !== undefined ? ( + typeof standard.currentTenantValue.CurrentValue === + "object" && + standard.currentTenantValue.CurrentValue !== null ? ( + + + Current Configuration + + {Object.entries( + standard.currentTenantValue.CurrentValue, + ).map(([key, val]) => ( + + + {key} + + + + {val !== undefined + ? JSON.stringify(val, null, 2) + : "Not set"} + + + + ))} + + ) : ( + + + Current Configuration + + + + {String(standard.currentTenantValue.CurrentValue)} + + + + ) + ) : standard.currentTenantValue !== undefined && + standard.currentTenantValue?.Value !== true && + standard.currentTenantValue?.Value !== false ? ( + + {String( + standard.currentTenantValue?.Value !== undefined + ? standard.currentTenantValue?.Value + : standard.currentTenantValue, + )} + + ) : standard.currentTenantValue === undefined || + (standard.currentTenantValue?.Value === null && + standard.currentTenantValue?.CurrentValue === undefined && + standard.currentTenantValue?.ExpectedValue === + undefined) ? ( + + This setting is not configured, or data has not been + collected. If you are getting this after data collection, + the tenant might not be licensed for this feature + + ) : null} + )} )} diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 5e3d3756352d..0b3c226bc693 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -130,7 +130,7 @@ const ManageDriftPage = () => { } if (item.customerSpecificDeviations && Array.isArray(item.customerSpecificDeviations)) { acc.customerSpecificDeviationsList.push( - ...item.customerSpecificDeviations.filter((dev) => dev !== null) + ...item.customerSpecificDeviations.filter((dev) => dev !== null), ); } if (item.deniedDeviations && Array.isArray(item.deniedDeviations)) { @@ -143,7 +143,7 @@ const ManageDriftPage = () => { Array.isArray(item.driftSettings.ComparisonDetails) ) { const compliantStandards = item.driftSettings.ComparisonDetails.filter( - (detail) => detail.Compliant === true + (detail) => detail.Compliant === true, ) .map((detail) => { // Strip "standards." prefix if present @@ -205,8 +205,10 @@ const ManageDriftPage = () => { ComplianceStatus: detail.ComplianceStatus, StandardValue: detail.StandardValue, ReportingDisabled: detail.ReportingDisabled, - expectedValue: "Compliant with template", - receivedValue: detail.StandardValue, + ExpectedValue: detail.ExpectedValue, + CurrentValue: detail.CurrentValue, + expectedValue: detail.ExpectedValue || "Compliant with template", + receivedValue: detail.CurrentValue || detail.StandardValue, }; }) .filter((item) => item !== null); // Filter out null items where templates weren't found @@ -236,7 +238,7 @@ const ManageDriftPage = () => { deniedDeviationsList: [], alignedStandards: [], latestDataCollection: null, - } + }, ); // Transform currentDeviations into deviation items for display @@ -376,6 +378,46 @@ const ManageDriftPage = () => { } }; + // Helper function to format differences for display + const formatDifferences = (differences) => { + if (!differences || typeof differences !== "object") return null; + + const formatted = []; + Object.entries(differences).forEach(([key, value]) => { + formatted.push({ + property: key, + expected: + value.expected !== undefined ? JSON.stringify(value.expected, null, 2) : "Not set", + current: value.current !== undefined ? JSON.stringify(value.current, null, 2) : "Not set", + }); + }); + + return formatted; + }; + + // Helper function to format matching properties for compliant items + const formatCompliantProperties = (value) => { + if (!value) return null; + + try { + const obj = typeof value === "string" ? JSON.parse(value) : value; + + if (typeof obj !== "object" || obj === null) return null; + + const formatted = []; + Object.entries(obj).forEach(([key, val]) => { + formatted.push({ + property: key, + value: val !== undefined ? JSON.stringify(val, null, 2) : "Not set", + }); + }); + + return formatted.length > 0 ? formatted : null; + } catch (e) { + return null; + } + }; + // Helper function to format policy objects for display const formatPolicyValue = (value) => { if (!value) return "N/A"; @@ -454,22 +496,27 @@ const ManageDriftPage = () => { const actualStatus = isActuallyCompliant ? "aligned" : isLicenseSkipped - ? "skipped" - : statusOverride || deviation.Status || deviation.state; + ? "skipped" + : statusOverride || deviation.Status || deviation.state; const actualStatusText = isActuallyCompliant ? "Compliant" : isLicenseSkipped - ? "Skipped - No License Available" - : getDeviationStatusText(actualStatus); + ? "Skipped - No License Available" + : getDeviationStatusText(actualStatus); // For skipped items, show different expected/received values let displayExpectedValue = deviation.ExpectedValue || deviation.expectedValue; let displayReceivedValue = deviation.CurrentValue || deviation.receivedValue; - // If we have JSON differences, show only the differences + // If we have JSON differences, format them for display + let formattedDifferences = null; + let formattedCompliantProps = null; + if (jsonDifferences && !isLicenseSkipped && !isActuallyCompliant) { - displayExpectedValue = JSON.stringify(jsonDifferences, null, 2); - displayReceivedValue = "See differences in Expected column"; + formattedDifferences = formatDifferences(jsonDifferences); + } else if ((isActuallyCompliant || actualStatus === "aligned") && displayExpectedValue) { + // For compliant items, format the properties to show them matching + formattedCompliantProps = formatCompliantProperties(displayExpectedValue); } return { @@ -512,10 +559,289 @@ const ManageDriftPage = () => { )} - {(displayExpectedValue && displayExpectedValue !== "Compliant with template") || - displayReceivedValue ? ( - - {displayExpectedValue && displayExpectedValue !== "Compliant with template" && ( + {formattedDifferences && formattedDifferences.length > 0 ? ( + + + Property Differences + + {formattedDifferences.map((diff, idx) => ( + + + {diff.property} + + + + + Expected + + + + + + + {diff.expected} + + + + + + Current + + + + + + + {diff.current} + + + + + + ))} + + ) : formattedCompliantProps && formattedCompliantProps.length > 0 ? ( + + + Compliant Properties + + {formattedCompliantProps.map((prop, idx) => ( + + + {prop.property} + + + + + Expected + + + + + + + {prop.value} + + + + + + Current + + + + + + + {prop.value} + + + + + + ))} + + ) : displayExpectedValue || displayReceivedValue ? ( + + {displayExpectedValue && ( { color: "text.secondary", textTransform: "uppercase", letterSpacing: 0.5, + display: "block", + mb: 0.5, }} > - {jsonDifferences ? "Differences" : "Expected"} + Expected + {(isActuallyCompliant || actualStatus === "aligned") && ( + + + + )} { fontSize: "0.8125rem", whiteSpace: "pre-wrap", wordBreak: "break-word", + color: + isActuallyCompliant || actualStatus === "aligned" + ? "success.dark" + : "text.primary", }} > - {displayExpectedValue} + {displayExpectedValue === "Compliant with template" + ? displayReceivedValue || "Compliant" + : displayExpectedValue} )} - {displayReceivedValue && !jsonDifferences && ( + {displayReceivedValue && ( { color: "text.secondary", textTransform: "uppercase", letterSpacing: 0.5, + display: "block", + mb: 0.5, }} > Current + {(isActuallyCompliant || actualStatus === "aligned") && ( + + + + )} { fontSize: "0.8125rem", whiteSpace: "pre-wrap", wordBreak: "break-word", + color: + isActuallyCompliant || actualStatus === "aligned" + ? "success.dark" + : "text.primary", }} > {displayReceivedValue} @@ -646,15 +1042,15 @@ const ManageDriftPage = () => { const deviationItems = createDeviationItems(processedDriftData.currentDeviations); const acceptedDeviationItems = createDeviationItems( processedDriftData.acceptedDeviations, - "accepted" + "accepted", ); const customerSpecificDeviationItems = createDeviationItems( processedDriftData.customerSpecificDeviationsList, - "customerspecific" + "customerspecific", ); const deniedDeviationItems = createDeviationItems( processedDriftData.deniedDeviationsList, - "denied" + "denied", ); const alignedStandardItems = createDeviationItems(processedDriftData.alignedStandards, "aligned"); @@ -662,7 +1058,7 @@ const ManageDriftPage = () => { const licenseSkippedItems = deviationItems.filter((item) => item.isLicenseSkipped); const compliantFromDeviations = deviationItems.filter((item) => item.isActuallyCompliant); const actualDeviationItems = deviationItems.filter( - (item) => !item.isLicenseSkipped && !item.isActuallyCompliant + (item) => !item.isLicenseSkipped && !item.isActuallyCompliant, ); // Combine compliant items from both sources @@ -884,7 +1280,7 @@ const ManageDriftPage = () => { // Find current tenant data const currentTenantData = currentTenantInfo.data?.find( - (tenant) => tenant.defaultDomainName === tenantFilter + (tenant) => tenant.defaultDomainName === tenantFilter, ); // Actions for the ActionsMenu @@ -1047,7 +1443,7 @@ const ManageDriftPage = () => { (item) => item.text?.toLowerCase().includes(searchQuery.toLowerCase()) || item.subtext?.toLowerCase().includes(searchQuery.toLowerCase()) || - item.standardName?.toLowerCase().includes(searchQuery.toLowerCase()) + item.standardName?.toLowerCase().includes(searchQuery.toLowerCase()), ); } @@ -1285,10 +1681,10 @@ const ManageDriftPage = () => { combinedScore === 100 ? "success" : combinedScore >= 80 - ? "warning" - : combinedScore >= 30 - ? "warning" - : "error" + ? "warning" + : combinedScore >= 30 + ? "warning" + : "error" } variant="outlined" /> @@ -1320,7 +1716,7 @@ const ManageDriftPage = () => { query: query, }, undefined, - { shallow: true } + { shallow: true }, ); }} placeholder="Select a drift template..." @@ -1364,8 +1760,8 @@ const ManageDriftPage = () => { sortBy === "name" ? "Name" : sortBy === "status" - ? "Status" - : "Category", + ? "Status" + : "Category", value: sortBy, } : null @@ -1426,7 +1822,7 @@ const ManageDriftPage = () => { (deviation.standardName?.includes("ConditionalAccessTemplate") || deviation.standardName?.includes("IntuneTemplate")) && deviation.expectedValue === - "This policy only exists in the tenant, not in the template." + "This policy only exists in the tenant, not in the template.", ) && ( handleBulkAction("deny-all-delete")}> @@ -1548,10 +1944,10 @@ const ManageDriftPage = () => { actionData.action?.type === "single" ? "this deviation" : actionData.action?.type === "bulk" - ? `these ${actionData.action?.count || 0} deviations` - : actionData.action?.type === "reset" - ? "for this tenant" - : "this deviation" + ? `these ${actionData.action?.count || 0} deviations` + : actionData.action?.type === "reset" + ? "for this tenant" + : "this deviation" }?`, }} row={actionData.data} diff --git a/src/pages/tenant/tools/tenantlookup/index.js b/src/pages/tenant/tools/tenantlookup/index.js index 1678f428f818..08453771819d 100644 --- a/src/pages/tenant/tools/tenantlookup/index.js +++ b/src/pages/tenant/tools/tenantlookup/index.js @@ -84,7 +84,11 @@ const Page = () => { Tenant Brand Name :{" "} {getTenant.data?.GraphRequest?.federationBrandName ? getTenant.data?.GraphRequest?.federationBrandName - : "N/A"} + : "Not Specified"} + + + Tenant Region:{" "} + {getTenant.data?.OpenIdConfig?.tenant_region_scope} diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index f47559d34499..68521b0781df 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -249,14 +249,14 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr unit === "d" ? "day" : unit === "h" - ? "hour" - : unit === "w" - ? "week" - : unit === "m" - ? "minutes" - : unit === "y" - ? "year" - : unit; + ? "hour" + : unit === "w" + ? "week" + : unit === "m" + ? "minutes" + : unit === "y" + ? "year" + : unit; return isText ? `Every ${value} ${unitText}` : `Every ${value} ${unitText}`; } } @@ -352,7 +352,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr icon: icon, key: key, }; - }) + }), ); } else { // Handle null/undefined single element @@ -459,7 +459,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr : renderChipList( data .filter((item) => item) - .map((item) => (typeof item === "object" && item?.label ? item.label : item)) + .map((item) => (typeof item === "object" && item?.label ? item.label : item)), ); } } @@ -500,12 +500,12 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr normalized === "enabled" ? "Enabled" : normalized === "disabled" - ? "Disabled" - : normalized === "enabledforreportingbutnotenforced" || - normalized === "report-only" || - normalized === "reportonly" - ? "Report Only" - : data.charAt(0).toUpperCase() + data.slice(1); + ? "Disabled" + : normalized === "enabledforreportingbutnotenforced" || + normalized === "report-only" || + normalized === "reportonly" + ? "Report Only" + : data.charAt(0).toUpperCase() + data.slice(1); if (isText) { return label; @@ -561,8 +561,8 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr const accessRights = Array.isArray(data) ? data.flatMap((item) => (typeof item === "string" ? item.split(", ") : [])) : typeof data === "string" - ? data.split(", ") - : []; + ? data.split(", ") + : []; return isText ? accessRights.join(", ") : renderChipList(accessRights); } @@ -643,8 +643,37 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ? translatedLicenses.join(", ") : translatedLicenses : Array.isArray(translatedLicenses) - ? renderChipList(translatedLicenses) - : translatedLicenses; + ? renderChipList(translatedLicenses) + : translatedLicenses; + } + + // Handle license assignment states + if (cellName === "licenseAssignmentStates") { + if (!Array.isArray(data) || data.length === 0) { + return []; + } + + // Transform the array to replace skuId with translated name and remove disabledPlans + const transformedData = data.map((license) => { + const translatedLicense = getCippLicenseTranslation([license]); + const licenseName = Array.isArray(translatedLicense) + ? translatedLicense[0] + : translatedLicense; + + // Return new object with skuId replaced by License and without disabledPlans + const { skuId, disabledPlans, ...rest } = license; + return { + License: licenseName, + ...rest, + }; + }); + + // Render as a table + return isText ? ( + JSON.stringify(transformedData) + ) : ( + + ); } if (cellName === "unifiedRoles") { @@ -849,7 +878,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr }, { fallbackLocale: "en", - } + }, ); const duration = isoDuration(data); return duration.humanize("en"); @@ -904,7 +933,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr return { label: item.label, }; - }) + }), ); }