From 51a82e82684e9d6638eb8f029566d13fafc338da Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 18 Jan 2026 14:59:30 +0100 Subject: [PATCH 01/14] updated text guidiance --- src/components/CippWizard/CippSAMDeploy.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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. From f3e3b47b9e864ff0f9e30858d71b6b60bcf36043 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:52:38 +0100 Subject: [PATCH 02/14] Updated drift management --- src/pages/tenant/manage/drift.js | 409 +++++++++++++++++++++++++++++-- 1 file changed, 387 insertions(+), 22 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 5e3d3756352d..4dea1b493dab 100644 --- a/src/pages/tenant/manage/drift.js +++ b/src/pages/tenant/manage/drift.js @@ -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 @@ -376,6 +378,45 @@ 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"; @@ -466,10 +507,15 @@ const ManageDriftPage = () => { 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 +558,285 @@ 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} From 2c5ab0d167c57631a095b93e79e57e2c5844f158 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 18 Jan 2026 16:52:46 +0100 Subject: [PATCH 03/14] Update compliant drift monitoring --- src/pages/tenant/manage/drift.js | 123 +++++++++++++++++++------------ 1 file changed, 77 insertions(+), 46 deletions(-) diff --git a/src/pages/tenant/manage/drift.js b/src/pages/tenant/manage/drift.js index 4dea1b493dab..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 @@ -238,7 +238,7 @@ const ManageDriftPage = () => { deniedDeviationsList: [], alignedStandards: [], latestDataCollection: null, - } + }, ); // Transform currentDeviations into deviation items for display @@ -380,14 +380,15 @@ const ManageDriftPage = () => { // Helper function to format differences for display const formatDifferences = (differences) => { - if (!differences || typeof differences !== 'object') return null; + 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', + expected: + value.expected !== undefined ? JSON.stringify(value.expected, null, 2) : "Not set", + current: value.current !== undefined ? JSON.stringify(value.current, null, 2) : "Not set", }); }); @@ -400,14 +401,14 @@ const ManageDriftPage = () => { try { const obj = typeof value === "string" ? JSON.parse(value) : value; - - if (typeof obj !== 'object' || obj === null) return null; + + 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', + value: val !== undefined ? JSON.stringify(val, null, 2) : "Not set", }); }); @@ -495,13 +496,13 @@ 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; @@ -510,7 +511,7 @@ const ManageDriftPage = () => { // If we have JSON differences, format them for display let formattedDifferences = null; let formattedCompliantProps = null; - + if (jsonDifferences && !isLicenseSkipped && !isActuallyCompliant) { formattedDifferences = formatDifferences(jsonDifferences); } else if ((isActuallyCompliant || actualStatus === "aligned") && displayExpectedValue) { @@ -583,7 +584,9 @@ const ManageDriftPage = () => { > {diff.property} - + { > {prop.property} - + { @@ -886,11 +901,14 @@ const ManageDriftPage = () => { fontSize: "0.8125rem", whiteSpace: "pre-wrap", wordBreak: "break-word", - color: isActuallyCompliant || actualStatus === "aligned" ? "success.dark" : "text.primary", + color: + isActuallyCompliant || actualStatus === "aligned" + ? "success.dark" + : "text.primary", }} > - {displayExpectedValue === "Compliant with template" - ? displayReceivedValue || "Compliant" + {displayExpectedValue === "Compliant with template" + ? displayReceivedValue || "Compliant" : displayExpectedValue} @@ -915,10 +933,20 @@ const ManageDriftPage = () => { @@ -947,7 +975,10 @@ const ManageDriftPage = () => { fontSize: "0.8125rem", whiteSpace: "pre-wrap", wordBreak: "break-word", - color: isActuallyCompliant || actualStatus === "aligned" ? "success.dark" : "text.primary", + color: + isActuallyCompliant || actualStatus === "aligned" + ? "success.dark" + : "text.primary", }} > {displayReceivedValue} @@ -1011,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"); @@ -1027,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 @@ -1249,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 @@ -1412,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()), ); } @@ -1650,10 +1681,10 @@ const ManageDriftPage = () => { combinedScore === 100 ? "success" : combinedScore >= 80 - ? "warning" - : combinedScore >= 30 - ? "warning" - : "error" + ? "warning" + : combinedScore >= 30 + ? "warning" + : "error" } variant="outlined" /> @@ -1685,7 +1716,7 @@ const ManageDriftPage = () => { query: query, }, undefined, - { shallow: true } + { shallow: true }, ); }} placeholder="Select a drift template..." @@ -1729,8 +1760,8 @@ const ManageDriftPage = () => { sortBy === "name" ? "Name" : sortBy === "status" - ? "Status" - : "Category", + ? "Status" + : "Category", value: sortBy, } : null @@ -1791,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")}> @@ -1913,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} From 438b5a9fc845d860ce1c1893e7fa64cc399cd1b1 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:52:28 +0800 Subject: [PATCH 04/14] Update CippAddTestReportDrawer.jsx --- src/components/CippComponents/CippAddTestReportDrawer.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: [] }; From 26d9e09ea97e0caa555fcc588267c24b99ffa97b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:10:03 +0800 Subject: [PATCH 05/14] securescore alert --- src/data/alerts.json | 34 +++++++++++++++++++ .../alert-configuration/alert.jsx | 20 +++++++++++ 2 files changed, 54 insertions(+) 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/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, + } + : {} + ) + } /> From 8c99a916f1297bd7bd6b18a15bcc57426bb7b4ac Mon Sep 17 00:00:00 2001 From: Luke Steward <29278153+LukeSteward@users.noreply.github.com> Date: Mon, 19 Jan 2026 13:25:14 +0000 Subject: [PATCH 06/14] Fixed failed labeling workflow Workflow was missing token as part of the required usage. https://github.com/JasonEtco/is-sponsor-label-action?tab=readme-ov-file#usage Reusing existing token value inside of the repository as referenced by other workflows Changed the label to match the one on GitHub Signed-off-by: Luke Steward <29278153+LukeSteward@users.noreply.github.com> --- .github/workflows/label_sponsor_requests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 }} From bf8e9126ecca961985b25c41c5daae8f4912419e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Mon, 19 Jan 2026 09:43:21 -0500 Subject: [PATCH 07/14] Expand permissions for Mailbox Permissions menu Updated the permissions for the 'Mailbox Permissions' menu item from 'Exchange.Mailbox.Read' to 'Exchange.Mailbox.*' to allow broader access. This change enables users with any Exchange mailbox permission to access the menu. --- src/layouts/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/layouts/config.js b/src/layouts/config.js index d735b015c94c..8e8fd1f97c39 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -719,7 +719,7 @@ export const nativeMenuItems = [ { title: "Mailbox Permissions", path: "/email/reports/mailbox-permissions", - permissions: ["Exchange.Mailbox.Read"], + permissions: ["Exchange.Mailbox.*"], }, { title: "Anti-Phishing Filters", From 02b4ce54d37dd9b0372d893bdefd36d4a53558ee Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 11:04:54 -0500 Subject: [PATCH 08/14] Pass dateFilter to log entry view and API call Updated the log entry view action to include the dateFilter parameter in the URL. Modified logentry.js to extract dateFilter from the query and pass it to the API call for fetching log details. Also improved code formatting for severity badge rendering. --- src/pages/cipp/logs/index.js | 12 ++++++------ src/pages/cipp/logs/logentry.js | 15 ++++++++------- 2 files changed, 14 insertions(+), 13 deletions(-) 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" /> From c17dceae613ef8fb329b85ddf4aefaff248d65e3 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 12:01:39 -0500 Subject: [PATCH 09/14] Improve error handling and retry logic in API calls Added status code 302 and 500 to the list of HTTP statuses that should not trigger retries in ApiGetCallWithPagination. When a 302 redirect to the AAD login is detected, the 'authmecipp' query is invalidated. Also fixed minor formatting and trailing comma issues in several functions. --- src/api/ApiCall.jsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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; From 8e0c76f20fdf38fc408f6114be85daee751ae10a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 12:17:58 -0500 Subject: [PATCH 10/14] Add calendar permissions report page Introduces a new page for viewing calendar permissions, allowing grouping by user or calendar. Includes cache sync functionality and displays cached data from the reporting database, with support for multi-tenant views. --- src/layouts/config.js | 5 + .../reports/calendar-permissions/index.js | 117 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/pages/email/reports/calendar-permissions/index.js diff --git a/src/layouts/config.js b/src/layouts/config.js index 8e8fd1f97c39..82e68a157e9c 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -721,6 +721,11 @@ export const nativeMenuItems = [ path: "/email/reports/mailbox-permissions", permissions: ["Exchange.Mailbox.*"], }, + { + title: "Calendar Permissions", + path: "/email/reports/calendar-permissions", + permissions: ["Exchange.Mailbox.*"], + }, { title: "Anti-Phishing Filters", path: "/email/reports/antiphishing-filters", 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; From bcd57dc2b88c236a58b39d35620b218b3928ff0d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:40:04 +0100 Subject: [PATCH 11/14] Fixes drift issues --- src/pages/tenant/manage/applied-standards.js | 1007 +++++++++++++----- 1 file changed, 764 insertions(+), 243 deletions(-) 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} + )} )} From a9d0c78d34311d86cd56c037009f26775af31627 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 20 Jan 2026 15:14:07 -0500 Subject: [PATCH 12/14] Add tenant region display to lookup page Displays the tenant region using OpenIdConfig. Also updates the fallback text for tenant brand name from 'N/A' to 'Not Specified'. --- src/pages/tenant/tools/tenantlookup/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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} From 326462e9e67279fd96c6c6bc80f8956bdf030998 Mon Sep 17 00:00:00 2001 From: Luke Steward <29278153+LukeSteward@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:57:31 +0000 Subject: [PATCH 13/14] Update NodeJS workflow for concurrency and Node version Signed-off-by: Luke Steward <29278153+LukeSteward@users.noreply.github.com> --- .github/workflows/Node_Project_Check.yml | 36 ++++++++++++++---------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/Node_Project_Check.yml b/.github/workflows/Node_Project_Check.yml index b135c589ea7b..649b3a6dfcba 100644 --- a/.github/workflows/Node_Project_Check.yml +++ b/.github/workflows/Node_Project_Check.yml @@ -1,31 +1,37 @@ ---- name: NodeJS Project Check + on: pull_request: branches: - main - dev + concurrency: - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} - cancel-in-progress: false + group: NodeJS Project Check ${{ github.event_name == 'pull_request' && format('PR:{0}', github.event.pull_request.number) || format('Branch:{0}', github.ref_name) }} + cancel-in-progress: true + jobs: install-build: if: github.repository_owner == 'KelvinTegelaar' - name: NPM Install and Build + name: Install, Build, and Test runs-on: ubuntu-latest + strategy: matrix: - node-version: [16.x] - os: [ubuntu-latest] + node-version: [22.x] # optionally: [20.x, 22.x] + steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - - name: Install and Build Test - run: | - npm install --legacy-peer-deps - npm run build - env: - CI: true + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build From 6df5996d5959bd4c4ba08376cdf566353291b2d6 Mon Sep 17 00:00:00 2001 From: Luke Steward <29278153+LukeSteward@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:00:01 +0000 Subject: [PATCH 14/14] Fix comment syntax in Node_Project_Check.yml Signed-off-by: Luke Steward <29278153+LukeSteward@users.noreply.github.com> --- .github/workflows/Node_Project_Check.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Node_Project_Check.yml b/.github/workflows/Node_Project_Check.yml index 649b3a6dfcba..c83519a6b16f 100644 --- a/.github/workflows/Node_Project_Check.yml +++ b/.github/workflows/Node_Project_Check.yml @@ -12,13 +12,13 @@ concurrency: jobs: install-build: - if: github.repository_owner == 'KelvinTegelaar' + #if: github.repository_owner == 'KelvinTegelaar' name: Install, Build, and Test runs-on: ubuntu-latest strategy: matrix: - node-version: [22.x] # optionally: [20.x, 22.x] + node-version: [22.x] steps: - name: Checkout repo