From 2ba860f7d20c7388ef0852a780864f4945728b80 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 12 May 2026 02:01:46 +0800 Subject: [PATCH 001/133] Better intune policy support for alltenants list --- .../CippComponents/CippIntunePolicyDetails.jsx | 2 +- src/components/CippFormPages/CippJSONView.jsx | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippIntunePolicyDetails.jsx b/src/components/CippComponents/CippIntunePolicyDetails.jsx index 064230da3e18..b1820e1c47b8 100644 --- a/src/components/CippComponents/CippIntunePolicyDetails.jsx +++ b/src/components/CippComponents/CippIntunePolicyDetails.jsx @@ -60,5 +60,5 @@ export const CippIntunePolicyDetails = ({ row, tenant }) => { ) } - return + return } diff --git a/src/components/CippFormPages/CippJSONView.jsx b/src/components/CippFormPages/CippJSONView.jsx index f5044792b149..fb69ef966b37 100644 --- a/src/components/CippFormPages/CippJSONView.jsx +++ b/src/components/CippFormPages/CippJSONView.jsx @@ -248,27 +248,29 @@ function CippJsonView({ type, defaultOpen = false, title = 'Policy Details', + tenant = null, }) { const [viewJson, setViewJson] = useState(false) const [accordionOpen, setAccordionOpen] = useState(defaultOpen) const [drilldownData, setDrilldownData] = useState([]) // Array of { data, title } + const objectTenant = + tenant || object?.Tenant || object?.tenant || object?.TenantFilter || object?.tenantFilter || null + // Use the GUID resolver hook - const { guidMapping, isLoadingGuids, resolveGuids, isGuid } = useGuidResolver() + const { guidMapping, isLoadingGuids, resolveGuids, isGuid } = useGuidResolver(objectTenant) const resolvedType = type || (object?.omaSettings || object?.settings || object?.definitionValues || object?.added ? 'intune' : undefined) - const adminTemplateTenant = - object?.Tenant || object?.tenant || object?.TenantFilter || object?.tenantFilter || null const { definitionsMap: addedDefinitionsMap, isLoadingDefinitions, isDefinitionsError, } = useAdminTemplateDefinitions({ added: object?.added, - manualTenant: adminTemplateTenant, + manualTenant: objectTenant, waiting: resolvedType === 'intune', }) From 5cf6098a407ea3aa3b6588eaac8b7c0710667ff8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 12 May 2026 02:28:27 +0800 Subject: [PATCH 002/133] Update manifest for PWA chrome install option --- public/manifest.json | 17 ++++++++++++++--- public/sw.js | 8 ++++++++ src/pages/_app.js | 5 +++++ src/pages/_document.js | 6 ++++++ 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 public/sw.js diff --git a/public/manifest.json b/public/manifest.json index 2cc60cd8b5a7..f30bb85dcc29 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,14 +1,25 @@ { - "short_name": "Carpatin", - "name": "Carpatin", + "short_name": "CIPP", + "name": "CIPP - CyberDrian Improved Partner Portal", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" + }, + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" } ], - "start_url": ".", + "start_url": "/", + "scope": "/", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 000000000000..a5b7af04ecf4 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,8 @@ +// Minimal service worker to satisfy Chrome's installability criteria. +// This does NOT cache anything or provide offline support — it simply +// passes all requests through to the network so Chrome treats the site +// as an installable web app. + +self.addEventListener('install', () => self.skipWaiting()) +self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())) +self.addEventListener('fetch', () => {}) diff --git a/src/pages/_app.js b/src/pages/_app.js index f924bd5f626e..aa387f0417fa 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -80,6 +80,11 @@ const App = (props) => { useEffect(() => { if (typeof window === 'undefined') return + // Register minimal service worker for Chrome installability + if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js').catch(() => {}) + } + const language = navigator.language || navigator.userLanguage || 'en-US' const baseLang = language.split('-')[0] diff --git a/src/pages/_document.js b/src/pages/_document.js index c764cde02995..4cceb2676ef2 100644 --- a/src/pages/_document.js +++ b/src/pages/_document.js @@ -8,6 +8,12 @@ class CustomDocument extends Document { return ( + + + + + + Date: Tue, 12 May 2026 02:28:43 +0800 Subject: [PATCH 003/133] Update manifest.json --- public/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/manifest.json b/public/manifest.json index f30bb85dcc29..42f5d73ea6af 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -21,6 +21,6 @@ "start_url": "/", "scope": "/", "display": "standalone", - "theme_color": "#000000", + "theme_color": "#ffffff", "background_color": "#ffffff" -} \ No newline at end of file +} From 0710355e2adac37fffe4c7eef48d6f2c3a04993d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 12 May 2026 11:32:16 -0400 Subject: [PATCH 004/133] chore: bump version to 10.4.5 --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bba74b63d2e2..57cfa34daf68 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.4.4", + "version": "10.4.5", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index 0ac81ba1b0ba..a09d0fcf2ccd 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.4.4" + "version": "10.4.5" } From c958401045bf32a2326446cb24b4550e63b15f75 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:43:59 +0000 Subject: [PATCH 005/133] chore(deps): bump dompurify from 3.4.2 to 3.4.3 Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.3. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.3) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..76e6c08f07d0 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "axios": "1.15.0", "date-fns": "4.1.0", "diff": "^8.0.3", - "dompurify": "^3.4.2", + "dompurify": "^3.4.3", "eml-parse-js": "^1.2.0-beta.0", "export-to-csv": "^1.3.0", "formik": "2.4.9", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..56f92541f6b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3717,17 +3717,10 @@ dompurify@3.2.7: optionalDependencies: "@types/trusted-types" "^2.0.7" -dompurify@^3.3.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.3.3.tgz#680cae8af3e61320ddf3666a3bc843f7b291b2b6" - integrity sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA== - optionalDependencies: - "@types/trusted-types" "^2.0.7" - -dompurify@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.2.tgz#f0ff81be682c485505097ba8195a058d8f575218" - integrity sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA== +dompurify@^3.3.1, dompurify@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.3.tgz#3ef336e7a757c3bf1efbd3781afb149a3ae5cfa4" + integrity sha512-VVwJidIJcp1hpg2OMXML3ZVRPYSZiq4aX7qBh83BSIpOaRDqI+qxhXjjIWnpzkOXhmp0L81lnoME1mnCc9H48A== optionalDependencies: "@types/trusted-types" "^2.0.7" From a783d28ab623dc69767ffd758e5cd140f171f2d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:44:16 +0000 Subject: [PATCH 006/133] chore(deps): bump @tiptap/extension-table from 3.20.4 to 3.20.5 Bumps [@tiptap/extension-table](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-table) from 3.20.4 to 3.20.5. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-table/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.20.5/packages/extension-table) --- updated-dependencies: - dependency-name: "@tiptap/extension-table" dependency-version: 3.20.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..ac434fa694a5 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^3.4.1", "@tiptap/extension-heading": "^3.4.1", - "@tiptap/extension-table": "^3.19.0", + "@tiptap/extension-table": "^3.20.5", "@tiptap/pm": "^3.22.3", "@tiptap/react": "^3.20.5", "@tiptap/starter-kit": "^3.20.5", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..fd4f55d4b40e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2259,10 +2259,10 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-strike/-/extension-strike-3.20.5.tgz#a3689fc17ad89a23c88f11b27c7f53896caa54f3" integrity sha512-uwhvmfS4ciGYJRLUg0AHbWsprMCwyWVWd2RXOLRm0ZQeWkvzonPXZhJvzIhIgsFkPLj/dsN5t0+LdiK4UQMnyA== -"@tiptap/extension-table@^3.19.0": - version "3.20.4" - resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.20.4.tgz#b2067cf1609bb1c39b61e504dc4aa05cba13d9ca" - integrity sha512-vEHXRL9k9G02pp3P+DyUnN4YRaRAHGfTBC6gck0s9TpsCM9NIchL0qI1fb/u46Bu6UaoMMk58DGr7xaJ29g7KQ== +"@tiptap/extension-table@^3.20.5": + version "3.20.5" + resolved "https://registry.yarnpkg.com/@tiptap/extension-table/-/extension-table-3.20.5.tgz#bac3d76e1c5fc8a4672f1495532a934651f50ce8" + integrity sha512-YvTB5OfGqjqHqutkSyywplouFvJwlsDTpZAjtAh5TzKfOan42aiVepmHVpteoQP6LH0mSjw69RndFMIYhIGmSQ== "@tiptap/extension-text@^3.20.5": version "3.20.5" From 2285d39cfdfdbbdffe377fbcc357200763f27e84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:44:34 +0000 Subject: [PATCH 007/133] chore(deps): bump @tiptap/core from 3.20.5 to 3.22.3 Bumps [@tiptap/core](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/core) from 3.20.5 to 3.22.3. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/core/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.22.3/packages/core) --- updated-dependencies: - dependency-name: "@tiptap/core" dependency-version: 3.22.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..cfb4488fb562 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@tanstack/react-query-devtools": "^5.96.2", "@tanstack/react-query-persist-client": "^5.96.2", "@tanstack/react-table": "^8.19.2", - "@tiptap/core": "^3.4.1", + "@tiptap/core": "^3.22.3", "@tiptap/extension-heading": "^3.4.1", "@tiptap/extension-table": "^3.19.0", "@tiptap/pm": "^3.22.3", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..2a1e1d68ef0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2145,10 +2145,10 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== -"@tiptap/core@^3.20.5", "@tiptap/core@^3.4.1": - version "3.20.5" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.20.5.tgz#edf98b45f98463b12ed59357ea9b4bf155e3e194" - integrity sha512-Pkjd41UJ4F6Z8cPV+gEvqnt1VhY2g66xMjbpxREs0ECA5jRezCNKSZcc2pueQRTMtmn1SaSzGM9U/ifhVlVYOA== +"@tiptap/core@^3.20.5", "@tiptap/core@^3.22.3": + version "3.22.3" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-3.22.3.tgz#89cd6d3d374f5f757bcb5e18e70c346a9eb9b2cd" + integrity sha512-Dv9MKK5BDWCF0N2l6/Pxv3JNCce2kwuWf2cKMBc2bEetx0Pn6o7zlFmSxMvYK4UtG1Tw9Yg/ZHi6QOFWK0Zm9Q== "@tiptap/extension-blockquote@^3.20.5": version "3.20.5" From d392d1cfebf3dd1fc82e50e0cc0fda449814d693 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:45:13 +0000 Subject: [PATCH 008/133] chore(deps): bump @tanstack/react-query from 5.96.2 to 5.100.10 Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.96.2 to 5.100.10. - [Release notes](https://github.com/TanStack/query/releases) - [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md) - [Commits](https://github.com/TanStack/query/commits/HEAD/packages/react-query) --- updated-dependencies: - dependency-name: "@tanstack/react-query" dependency-version: 5.100.10 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index a4402ea681b5..3b319eaa4bcc 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@react-pdf/renderer": "^4.3.2", "@reduxjs/toolkit": "^2.11.2", "@tanstack/query-sync-storage-persister": "^5.90.25", - "@tanstack/react-query": "^5.96.2", + "@tanstack/react-query": "^5.100.10", "@tanstack/react-query-devtools": "^5.96.2", "@tanstack/react-query-persist-client": "^5.96.2", "@tanstack/react-table": "^8.19.2", diff --git a/yarn.lock b/yarn.lock index acfda103a76e..856baa2a6b63 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2051,6 +2051,11 @@ dependencies: remove-accents "0.5.0" +"@tanstack/query-core@5.100.10": + version "5.100.10" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.100.10.tgz#aeb34d301fd4ff9762e67dfa018adc33b7a18be4" + integrity sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w== + "@tanstack/query-core@5.91.2": version "5.91.2" resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.91.2.tgz#d83825a928aa49ded38d3910f05284178cce89d3" @@ -2102,12 +2107,12 @@ dependencies: "@tanstack/query-persist-client-core" "5.96.2" -"@tanstack/react-query@^5.96.2": - version "5.96.2" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.96.2.tgz#a164abfb80eb5e7772bbcddfa7240f3fd8d0d7be" - integrity sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA== +"@tanstack/react-query@^5.100.10": + version "5.100.10" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.100.10.tgz#3bf1844efd76f5f68f9f39da2917fc4c6023e726" + integrity sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q== dependencies: - "@tanstack/query-core" "5.96.2" + "@tanstack/query-core" "5.100.10" "@tanstack/react-table@8.20.6": version "8.20.6" From c9bfe909582d9cc9d308903d7e74b44c6d926087 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 10:44:12 +0200 Subject: [PATCH 009/133] feat(endpoint): add MEM enrollment profiles page (Apple ADE, Android, Autopilot) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new "Enrollment Profiles" page. Autopilot profiles page is kept, but could be removed in the future. Refs KelvinTegelaar/CIPP#5941 (related work — does not fully close). --- .../CippCards/CippUniversalSearchV2.jsx | 1 + .../CippComponents/CippBreadcrumbNav.jsx | 1 + src/layouts/config.js | 5 + .../EnrollmentProfileTabs.jsx | 466 ++++++++++++++++++ .../enrollment-profiles/android-enterprise.js | 14 + .../endpoint/MEM/enrollment-profiles/index.js | 14 + .../MEM/enrollment-profiles/tabOptions.json | 14 + .../enrollment-profiles/windows-autopilot.js | 14 + 8 files changed, 529 insertions(+) create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/index.js create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json create mode 100644 src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 070396f0a56e..7ffa2bfdb725 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -40,6 +40,7 @@ async function loadTabOptions() { "/email/administration/exchange-retention", "/cipp/custom-data", "/cipp/advanced/super-admin", + "/endpoint/MEM/enrollment-profiles", "/tenant/standards", "/tenant/manage", "/tenant/administration/applications", diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index 560f84efb631..d890e84a0554 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -16,6 +16,7 @@ async function loadTabOptions() { "/email/administration/exchange-retention", "/cipp/custom-data", "/cipp/advanced/super-admin", + "/endpoint/MEM/enrollment-profiles", "/tenant/standards", "/tenant/manage", "/tenant/administration/applications", diff --git a/src/layouts/config.js b/src/layouts/config.js index a9df0957241d..c34329156da5 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -564,6 +564,11 @@ export const nativeMenuItems = [ path: '/endpoint/MEM/list-scripts', permissions: ['Endpoint.MEM.*'], }, + { + title: 'Enrollment Profiles', + path: '/endpoint/MEM/enrollment-profiles', + permissions: ['Endpoint.MEM.*'], + }, ], }, { diff --git a/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx b/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx new file mode 100644 index 000000000000..73fd35519bd0 --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx @@ -0,0 +1,466 @@ +import { useMemo, useState } from 'react' +import { + Alert, + Button, + Card, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography, +} from '@mui/material' +import { Box, Container, Stack } from '@mui/system' +import { + AccountTree, + Apple, + ContentCopy, + Delete, + EventAvailable, + QrCode2, + Sync, +} from '@mui/icons-material' +import { CippHead } from '../../../../components/CippComponents/CippHead.jsx' +import { CippDataTable } from '../../../../components/CippTable/CippDataTable.js' +import { CippInfoBar } from '../../../../components/CippCards/CippInfoBar.jsx' +import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog.jsx' +import { CippAutopilotProfileDrawer } from '../../../../components/CippComponents/CippAutopilotProfileDrawer.jsx' +import CippJsonView from '../../../../components/CippFormPages/CippJSONView.jsx' +import { ApiGetCall } from '../../../../api/ApiCall.jsx' +import { useDialog } from '../../../../hooks/use-dialog.js' +import { useSettings } from '../../../../hooks/use-settings.js' + +const pageTitle = 'Enrollment Profiles' +const appleADEPageTitle = 'Apple Enrollment Profiles' +const androidEnterprisePageTitle = 'Android Enrollment Profiles' +const windowsAutopilotPageTitle = 'Windows Autopilot Profiles' + +const EnrollmentProfilesPage = ({ children, title = pageTitle }) => { + return ( + <> + + + + + {children} + + + + + ) +} + +const AndroidQrDialog = ({ row, drawerVisible, setDrawerVisible }) => { + const [copied, setCopied] = useState(false) + + const tokenValue = useMemo(() => { + if (row?.tokenValue) { + return row.tokenValue + } + + if (!row?.qrCodeContent) { + return '' + } + + try { + const parsed = JSON.parse(row.qrCodeContent) + const adminExtras = parsed?.['android.app.extra.PROVISIONING_ADMIN_EXTRAS_BUNDLE'] + return adminExtras?.['com.google.android.apps.work.clouddpc.EXTRA_ENROLLMENT_TOKEN'] || '' + } catch { + return '' + } + }, [row?.qrCodeContent, row?.tokenValue]) + + const handleClose = () => { + setDrawerVisible(false) + } + + const handleCopy = async () => { + if (!tokenValue) { + return + } + + try { + await navigator.clipboard.writeText(tokenValue) + setCopied(true) + setTimeout(() => setCopied(false), 1500) + } catch { + setCopied(false) + } + } + + const qrCodeImageValue = row?.qrCodeImage?.value + const qrCodeImageType = row?.qrCodeImage?.type || 'image/png' + + return ( + + Enrollment QR - {row?.displayName} + + {qrCodeImageValue && ( + + Enrollment QR code + + )} + + Token value + + + + {tokenValue || 'No token value available.'} + + + + + + + ) +} + +export const AppleADEEnrollmentProfiles = () => { + const currentTenant = useSettings().currentTenant + const depSyncDialog = useDialog() + + const appleProfiles = ApiGetCall({ + url: '/api/ListAppleEnrollmentProfiles', + data: { tenantFilter: currentTenant }, + queryKey: `AppleEnrollmentProfiles-${currentTenant}`, + waiting: Boolean(currentTenant), + }) + + const appleData = appleProfiles.data?.Results || {} + const tokens = appleData.Tokens || [] + const profiles = appleData.Profiles || [] + const syncErrorCodes = { + 1: { + label: 'Expired', + message: 'The ADE token sync has expired.', + severity: 'error', + }, + 2: { + label: 'Unknown', + message: 'The ADE token sync state is unknown.', + severity: 'error', + }, + 3: { + label: 'Terms & Conditions', + message: 'New Apple Business Manager terms are ready to accept.', + severity: 'warning', + }, + 4: { + label: 'Warning', + message: 'The ADE token sync completed with a warning.', + severity: 'warning', + }, + } + const syncErrorTokens = tokens.filter( + (token) => token.lastSyncErrorCode != null && Number(token.lastSyncErrorCode) !== 0 + ) + const expiringTokens = tokens.filter( + (token) => token.daysUntilExpiration !== null && token.daysUntilExpiration <= 30 + ) + const totalSyncedDevices = tokens.reduce( + (sum, token) => sum + Number(token.syncedDeviceCount || 0), + 0 + ) + const lastSuccessfulSync = tokens + .map((token) => token.lastSuccessfulSyncDateTime) + .filter(Boolean) + .sort() + .pop() + + const infoBarData = [ + { + icon: , + name: 'ADE Tokens', + data: tokens.length, + offcanvas: { + title: 'Apple ADE Tokens', + propertyItems: tokens.flatMap((token) => [ + { + label: `${token.tokenName || token.id} - Apple ID`, + value: token.appleIdentifier || 'N/A', + }, + { + label: `${token.tokenName || token.id} - Expiration`, + value: token.tokenExpirationDateTime || 'N/A', + }, + { + label: `${token.tokenName || token.id} - Synced Devices`, + value: token.syncedDeviceCount ?? 'N/A', + }, + { + label: `${token.tokenName || token.id} - Last Sync`, + value: token.lastSuccessfulSyncDateTime || 'N/A', + }, + ]), + }, + }, + { + icon: , + name: 'Expiring Tokens', + data: expiringTokens.length, + color: expiringTokens.length ? 'error' : 'success', + toolTip: 'Tokens expiring within 30 days', + }, + { + icon: , + name: 'ADE Profiles', + data: profiles.length, + }, + { + icon: , + name: 'Last Successful Sync', + data: lastSuccessfulSync ? new Date(lastSuccessfulSync).toLocaleString() : 'N/A', + toolTip: `${totalSyncedDevices} synced devices across all tokens`, + }, + ] + + const appleActions = [ + { + label: 'Delete Profile', + type: 'POST', + icon: , + url: '/api/ExecRemoveEnrollmentProfile', + relatedQueryKeys: [`AppleEnrollmentProfiles*-${currentTenant}`], + data: { + profileId: 'id', + tokenId: 'tokenId', + profileType: 'profileType', + displayName: 'displayName', + }, + confirmText: 'Are you sure you want to delete enrollment profile [displayName]?', + color: 'danger', + }, + ] + + const appleFilters = useMemo( + () => [ + { filterName: 'All', value: [] }, + { filterName: 'macOS', value: [{ id: 'platform', value: 'macOS' }] }, + { + filterName: 'iOS/iPadOS', + value: [{ id: 'platform', value: 'iOS/iPadOS' }], + }, + ], + [] + ) + + return ( + <> + + {!appleProfiles.isFetching && + syncErrorTokens.map((token, index) => { + const errorCode = Number(token.lastSyncErrorCode) + const syncError = syncErrorCodes[errorCode] || { + label: 'Unknown Error', + message: 'The ADE token sync was not successful.', + severity: 'warning', + } + const tokenName = token.tokenName || token.id || 'Unknown token' + const appleIdentifier = token.appleIdentifier ? ` (${token.appleIdentifier})` : '' + const lastSuccessfulSyncText = token.lastSuccessfulSyncDateTime + ? ` Last successful sync: ${new Date( + token.lastSuccessfulSyncDateTime + ).toLocaleString()}.` + : '' + + return ( + + {`Token "${tokenName}"${appleIdentifier}: ${syncError.message} (Code ${errorCode} - ${syncError.label}).${lastSuccessfulSyncText}`} + + ) + })} + + + + + + + , + size: 'xl', + }} + cardButton={ + + + + } + /> + + + + + ) +} + +export const AndroidEnterpriseEnrollmentProfiles = () => { + const currentTenant = useSettings().currentTenant + const androidActions = [ + { + label: 'Show QR', + icon: , + hideBulk: true, + noConfirm: true, + condition: (row) => Boolean(row?.tokenValue || row?.qrCodeImage?.value || row?.qrCodeContent), + customComponent: (row, { drawerVisible, setDrawerVisible }) => ( + + ), + }, + { + label: 'Delete Profile', + type: 'POST', + icon: , + url: '/api/ExecRemoveEnrollmentProfile', + data: { + profileId: 'id', + profileType: '!android', + displayName: 'displayName', + }, + confirmText: 'Are you sure you want to delete Android enrollment profile [displayName]?', + color: 'danger', + }, + ] + + return ( + + + , + size: 'xl', + }} + /> + + + ) +} + +export const WindowsAutopilotEnrollmentProfiles = () => { + const currentTenant = useSettings().currentTenant + const autopilotActions = [ + { + label: 'Delete Profile', + icon: , + type: 'POST', + url: '/api/RemoveAutopilotConfig', + data: { ID: 'id', displayName: 'displayName', assignments: 'assignments' }, + confirmText: + 'Are you sure you want to delete this Autopilot profile? This action cannot be undone.', + color: 'danger', + }, + ] + + return ( + + + , + size: 'xl', + }} + cardButton={} + /> + + + ) +} diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js new file mode 100644 index 000000000000..826afd75f085 --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js @@ -0,0 +1,14 @@ +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { AndroidEnterpriseEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import tabOptions from './tabOptions.json' + +const Page = () => + +Page.getLayout = (page) => ( + + {page} + +) + +export default Page diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/MEM/enrollment-profiles/index.js new file mode 100644 index 000000000000..e6fc7bfad50d --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/index.js @@ -0,0 +1,14 @@ +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { AppleADEEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import tabOptions from './tabOptions.json' + +const Page = () => + +Page.getLayout = (page) => ( + + {page} + +) + +export default Page diff --git a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json new file mode 100644 index 000000000000..4bf6047e9d31 --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json @@ -0,0 +1,14 @@ +[ + { + "label": "Apple ADE", + "path": "/endpoint/MEM/enrollment-profiles" + }, + { + "label": "Android Enterprise", + "path": "/endpoint/MEM/enrollment-profiles/android-enterprise" + }, + { + "label": "Windows Autopilot", + "path": "/endpoint/MEM/enrollment-profiles/windows-autopilot" + } +] diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js new file mode 100644 index 000000000000..4c072ed7302d --- /dev/null +++ b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js @@ -0,0 +1,14 @@ +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { WindowsAutopilotEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import tabOptions from './tabOptions.json' + +const Page = () => + +Page.getLayout = (page) => ( + + {page} + +) + +export default Page From b19024214e96bd207640c5e63dcefb38d408e161 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 16:51:32 +0200 Subject: [PATCH 010/133] feat: Bit more margin to make tabbed layout of first item less cramped --- src/layouts/HeaderedTabbedLayout.jsx | 11 ++++++++++- src/layouts/TabbedLayout.jsx | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index 1b5585a6812a..d36ca26f0497 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -102,7 +102,16 @@ export const HeaderedTabbedLayout = (props) => { )}
- + {tabOptions.map((option) => ( ))} diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index 031f363c4dac..c69157050da6 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -55,7 +55,7 @@ export const TabbedLayout = (props) => { variant="scrollable" sx={{ '& .MuiTab-root:first-of-type': { - ml: 1, + ml: 2, }, }} > From 6dab9339978f195dc8e6db35633071da8f16eb8a Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 17:50:36 +0200 Subject: [PATCH 011/133] feat(tabs): support icons in tabbed layouts --- src/layouts/HeaderedTabbedLayout.jsx | 19 +++- src/layouts/TabbedLayout.jsx | 17 +++- .../MEM/enrollment-profiles/tabOptions.json | 9 +- src/utils/icon-registry.js | 95 +++++++++++++++++++ 4 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 src/utils/icon-registry.js diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index d36ca26f0497..c1abbe1d6e2b 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import { ActionsMenu } from "../components/actions-menu"; import { useMediaQuery } from "@mui/material"; +import { getIconByName } from "../utils/icon-registry"; export const HeaderedTabbedLayout = (props) => { const { @@ -112,9 +113,19 @@ export const HeaderedTabbedLayout = (props) => { }, }} > - {tabOptions.map((option) => ( - - ))} + {tabOptions.map((option) => { + const icon = getIconByName(option.icon, { fontSize: "small" }); + + return ( + + ); + })}
@@ -142,6 +153,8 @@ HeaderedTabbedLayout.propTypes = { PropTypes.shape({ label: PropTypes.string.isRequired, path: PropTypes.string.isRequired, + icon: PropTypes.string, + iconPosition: PropTypes.oneOf(["bottom", "end", "start", "top"]), }) ).isRequired, title: PropTypes.string.isRequired, diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index c69157050da6..fc3f7e440773 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -3,6 +3,7 @@ import { usePathname, useRouter } from 'next/navigation' import { Box, Divider, Stack, Tab, Tabs } from '@mui/material' import { useSearchParams } from 'next/navigation' import { ApiGetCall } from '../api/ApiCall' +import { getIconByName } from '../utils/icon-registry' export const TabbedLayout = (props) => { const { tabOptions, children } = props @@ -59,9 +60,19 @@ export const TabbedLayout = (props) => { }, }} > - {visibleTabs.map((option) => ( - - ))} + {visibleTabs.map((option) => { + const icon = getIconByName(option.icon, { fontSize: 'small' }) + + return ( + + ) + })} diff --git a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json index 4bf6047e9d31..9ed4566b1448 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json +++ b/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json @@ -1,14 +1,17 @@ [ { "label": "Apple ADE", - "path": "/endpoint/MEM/enrollment-profiles" + "path": "/endpoint/MEM/enrollment-profiles", + "icon": "Apple" }, { "label": "Android Enterprise", - "path": "/endpoint/MEM/enrollment-profiles/android-enterprise" + "path": "/endpoint/MEM/enrollment-profiles/android-enterprise", + "icon": "Android" }, { "label": "Windows Autopilot", - "path": "/endpoint/MEM/enrollment-profiles/windows-autopilot" + "path": "/endpoint/MEM/enrollment-profiles/windows-autopilot", + "icon": "Window" } ] diff --git a/src/utils/icon-registry.js b/src/utils/icon-registry.js new file mode 100644 index 000000000000..a87db806b2f0 --- /dev/null +++ b/src/utils/icon-registry.js @@ -0,0 +1,95 @@ +import { + AdminPanelSettings, + Android, + Apple, + Apps, + Assignment, + BarChart, + Business, + CheckCircle, + Cloud, + Computer, + Dashboard, + Description, + Devices, + Dns, + Domain, + Email, + FactCheck, + FilePresent, + Group, + Groups, + Home, + Key, + Laptop, + List, + Lock, + Mail, + ManageAccounts, + Notifications, + Person, + Policy, + PrecisionManufacturing, + Security, + Settings, + Share, + Shield, + ShieldMoon, + Storage, + Sync, + Timeline, + Window, + Warning, +} from '@mui/icons-material' + +export const iconRegistry = { + AdminPanelSettings, + Android, + Apple, + Apps, + Assignment, + BarChart, + Business, + CheckCircle, + Cloud, + Computer, + Dashboard, + Description, + Devices, + Dns, + Domain, + Email, + FactCheck, + FilePresent, + Group, + Groups, + Home, + Key, + Laptop, + List, + Lock, + Mail, + ManageAccounts, + Notifications, + Person, + Policy, + PrecisionManufacturing, + Security, + Settings, + Share, + Shield, + ShieldMoon, + Storage, + Sync, + Timeline, + Window, + Warning, +} + +export const getIconComponentByName = (iconName) => iconRegistry[iconName] ?? null + +export const getIconByName = (iconName, props = {}) => { + const Icon = getIconComponentByName(iconName) + + return Icon ? : null +} From 9b6164999145da5dab8cb726cc2b4d6a25cba44b Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 14 May 2026 18:46:23 +0200 Subject: [PATCH 012/133] feat: Migrate to use shared icon registry for string to icon conversion --- .../CippCards/CippPropertyListCard.jsx | 9 ++- .../CippCards/CippUniversalSearchV2.jsx | 4 +- .../CippComponents/CippTenantSelector.jsx | 53 ++++--------- src/components/bulk-actions-menu.js | 78 ++++++------------- src/data/portals.json | 6 +- src/layouts/HeaderedTabbedLayout.jsx | 5 +- src/layouts/TabbedLayout.jsx | 5 +- src/utils/icon-registry.js | 2 + 8 files changed, 63 insertions(+), 99 deletions(-) diff --git a/src/components/CippCards/CippPropertyListCard.jsx b/src/components/CippCards/CippPropertyListCard.jsx index 4e7bb2d81f0f..01eb598409da 100644 --- a/src/components/CippCards/CippPropertyListCard.jsx +++ b/src/components/CippCards/CippPropertyListCard.jsx @@ -15,6 +15,7 @@ import { PropertyListItem } from "../../components/property-list-item"; import { useDialog } from "../../hooks/use-dialog"; import { CippApiDialog } from "../CippComponents/CippApiDialog"; import { useState } from "react"; +import { getIconByName } from "../../utils/icon-registry"; export const CippPropertyListCard = (props) => { const { @@ -51,6 +52,12 @@ export const CippPropertyListCard = (props) => { return false; }; + const renderActionIcon = (icon) => { + if (!icon) return null; + if (typeof icon === "string") return getIconByName(icon, { fontSize: "small" }); + return {icon}; + }; + return ( <> @@ -160,7 +167,7 @@ export const CippPropertyListCard = (props) => { actionItems.map((item, index) => ( {item.icon}} + icon={renderActionIcon(item.icon)} label={item.label} onClick={() => { setActionData({ diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 7ffa2bfdb725..83e127352851 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -392,7 +392,7 @@ export const CippUniversalSearchV2 = React.forwardRef( const typeMenuActions = [ { label: "Users", - icon: "UsersIcon", + icon: "Groups", onClick: () => handleTypeChange("Users"), }, { @@ -412,7 +412,7 @@ export const CippUniversalSearchV2 = React.forwardRef( }, { label: "Pages", - icon: "GlobeAltIcon", + icon: "Public", onClick: () => handleTypeChange("Pages"), }, ]; diff --git a/src/components/CippComponents/CippTenantSelector.jsx b/src/components/CippComponents/CippTenantSelector.jsx index 74447184f4ba..0b2c3ce62508 100644 --- a/src/components/CippComponents/CippTenantSelector.jsx +++ b/src/components/CippComponents/CippTenantSelector.jsx @@ -1,30 +1,15 @@ import PropTypes from "prop-types"; import { CippAutoComplete } from "../CippComponents/CippAutocomplete"; import { ApiGetCall } from "../../api/ApiCall"; -import { IconButton, SvgIcon, Tooltip, Box } from "@mui/material"; -import { - FilePresent, - Laptop, - Mail, - Refresh, - Share, - Shield, - ShieldMoon, - PrecisionManufacturing, - BarChart, -} from "@mui/icons-material"; -import { - BuildingOfficeIcon, - GlobeAltIcon, - ServerIcon, - UsersIcon, -} from "@heroicons/react/24/outline"; +import { IconButton, Tooltip, Box } from "@mui/material"; +import { Refresh } from "@mui/icons-material"; import React, { useEffect, useState, useMemo, useCallback, useRef } from "react"; import { useRouter } from "next/router"; import { CippOffCanvas } from "./CippOffCanvas"; import { useSettings } from "../../hooks/use-settings"; import { getCippError } from "../../utils/get-cipp-error"; import { useQueryClient } from "@tanstack/react-query"; +import { getIconByName } from "../../utils/icon-registry"; export const CippTenantSelector = React.forwardRef((props, ref) => { const { width, allTenants = false, multiple = false, refreshButton, tenantButton } = props; @@ -65,68 +50,68 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { key: "M365_Portal", label: "M365 Admin Portal", link: `https://admin.cloud.microsoft/?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, - icon: , + icon: "Public", }, { key: "Exchange_Portal", label: "Exchange Portal", link: `https://admin.cloud.microsoft/exchange?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, - icon: , + icon: "Mail", }, { key: "Entra_Portal", label: "Entra Portal", link: `https://entra.microsoft.com/${currentTenant?.value}`, - icon: , + icon: "Groups", }, { key: "Teams_Portal", label: "Teams Portal", link: `https://admin.teams.microsoft.com/?delegatedOrg=${currentTenant?.addedFields?.initialDomainName}`, - icon: , + icon: "FilePresent", }, { key: "Azure_Portal", label: "Azure Portal", link: `https://portal.azure.com/${currentTenant?.value}`, - icon: , + icon: "Dns", }, { key: "Intune_Portal", label: "Intune Portal", link: `https://intune.microsoft.com/${currentTenant?.value}`, - icon: , + icon: "Laptop", }, { key: "SharePoint_Admin", label: "SharePoint Portal", link: `/api/ListSharePointAdminUrl?tenantFilter=${currentTenant?.value}`, - icon: , + icon: "Share", external: true, }, { key: "Security_Portal", label: "Security Portal", link: `https://security.microsoft.com/?tid=${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "Shield", }, { key: "Compliance_Portal", label: "Compliance Portal", link: `https://purview.microsoft.com/?tid=${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "ShieldMoon", }, { key: "Power_Platform_Portal", label: "Power Platform Portal", link: `https://admin.powerplatform.microsoft.com/account/login/${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "PrecisionManufacturing", }, { key: "Power_BI_Portal", label: "Power BI Portal", link: `https://app.powerbi.com/admin-portal?ctid=${currentTenant?.addedFields?.customerId}`, - icon: , + icon: "BarChart", }, ]; @@ -164,7 +149,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { key: "Manage_Tenant", label: "Manage Tenant", link: `/tenant/manage/edit?tenantFilter=${currentTenant?.value}`, - icon: , + icon: "Business", }); return filteredActions; @@ -343,9 +328,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { disabled={!currentTenant || currentTenant.value === "AllTenants"} > - - - + {getIconByName("Business")} )} @@ -396,9 +379,7 @@ export const CippTenantSelector = React.forwardRef((props, ref) => { }} > - - - + )} diff --git a/src/components/bulk-actions-menu.js b/src/components/bulk-actions-menu.js index ff9e8e1f36dd..18f1ad5af1c0 100644 --- a/src/components/bulk-actions-menu.js +++ b/src/components/bulk-actions-menu.js @@ -1,46 +1,12 @@ -import PropTypes from "prop-types"; -import ChevronDownIcon from "@heroicons/react/24/outline/ChevronDownIcon"; -import { Button, Link, ListItemText, Menu, MenuItem, SvgIcon } from "@mui/material"; -import { usePopover } from "../hooks/use-popover"; -import { FilePresent, Laptop, Mail, Share, Shield, ShieldMoon, PrecisionManufacturing, BarChart, Group, Apps } from "@mui/icons-material"; -import { GlobeAltIcon, UsersIcon, ServerIcon } from "@heroicons/react/24/outline"; - -function getIconByName(iconName) { - switch (iconName) { - case "GlobeAltIcon": - return ; - case "Mail": - return ; - case "UsersIcon": - return ; - case "FilePresent": - return ; - case "ServerIcon": - return ; - case "Laptop": - return ; - case "Share": - return ; - case "Shield": - return ; - case "ShieldMoon": - return ; - case "PrecisionManufacturing": - return ; - case "BarChart": - return ; - case "Group": - return ; - case "Apps": - return ; - default: - return null; - } -} +import PropTypes from 'prop-types' +import ChevronDownIcon from '@heroicons/react/24/outline/ChevronDownIcon' +import { Button, Link, ListItemText, Menu, MenuItem, SvgIcon } from '@mui/material' +import { usePopover } from '../hooks/use-popover' +import { getIconByName } from '../utils/icon-registry' export const BulkActionsMenu = (props) => { - const { buttonName, sx, row, actions = [], ...other } = props; - const popover = usePopover(); + const { buttonName, sx, row, actions = [], ...other } = props + const popover = usePopover() return ( <> @@ -55,7 +21,7 @@ export const BulkActionsMenu = (props) => { variant="outlined" sx={{ flexShrink: 0, - whiteSpace: "nowrap", + whiteSpace: 'nowrap', ...sx, }} {...other} @@ -65,8 +31,8 @@ export const BulkActionsMenu = (props) => { { onClose={popover.handleClose} open={popover.open} transformOrigin={{ - horizontal: "right", - vertical: "top", + horizontal: 'right', + vertical: 'top', }} > {actions.map((action, index) => { + const icon = getIconByName(action.icon, { sx: { mr: 1 } }) + if (action.link) { return ( { target="_blank" rel="noreferrer" > - {getIconByName(action.icon)} + {icon} - ); + ) } else { return ( { if (action.onClick) { - action.onClick(); + action.onClick() } - popover.handleClose(); + popover.handleClose() }} > - {getIconByName(action.icon)} + {icon} - ); + ) } })} - ); -}; + ) +} BulkActionsMenu.propTypes = { onArchive: PropTypes.func, onDelete: PropTypes.func, selectedCount: PropTypes.number, -}; +} diff --git a/src/data/portals.json b/src/data/portals.json index a4402305faca..874810c6d976 100644 --- a/src/data/portals.json +++ b/src/data/portals.json @@ -6,7 +6,7 @@ "variable": "initialDomainName", "target": "_blank", "external": true, - "icon": "GlobeAltIcon" + "icon": "Public" }, { "label": "Exchange", @@ -24,7 +24,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "UsersIcon" + "icon": "Groups" }, { "label": "Teams", @@ -42,7 +42,7 @@ "variable": "defaultDomainName", "target": "_blank", "external": true, - "icon": "ServerIcon" + "icon": "Dns" }, { "label": "Intune", diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx index c1abbe1d6e2b..d217c9c87d5e 100644 --- a/src/layouts/HeaderedTabbedLayout.jsx +++ b/src/layouts/HeaderedTabbedLayout.jsx @@ -115,6 +115,8 @@ export const HeaderedTabbedLayout = (props) => { > {tabOptions.map((option) => { const icon = getIconByName(option.icon, { fontSize: "small" }); + const iconPosition = option.iconPosition ?? "start"; + const compactIcon = icon && ["end", "start"].includes(iconPosition); return ( { label={option.label} value={option.path} icon={icon ?? undefined} - iconPosition={icon ? (option.iconPosition ?? "start") : undefined} + iconPosition={icon ? iconPosition : undefined} + sx={compactIcon ? { minHeight: 48, py: 1.5 } : undefined} /> ); })} diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx index fc3f7e440773..33c861fb85dd 100644 --- a/src/layouts/TabbedLayout.jsx +++ b/src/layouts/TabbedLayout.jsx @@ -62,6 +62,8 @@ export const TabbedLayout = (props) => { > {visibleTabs.map((option) => { const icon = getIconByName(option.icon, { fontSize: 'small' }) + const iconPosition = option.iconPosition ?? 'start' + const compactIcon = icon && ['end', 'start'].includes(iconPosition) return ( { label={option.label} value={option.path} icon={icon ?? undefined} - iconPosition={icon ? (option.iconPosition ?? 'start') : undefined} + iconPosition={icon ? iconPosition : undefined} + sx={compactIcon ? { minHeight: 48, py: 1.5 } : undefined} /> ) })} diff --git a/src/utils/icon-registry.js b/src/utils/icon-registry.js index a87db806b2f0..2863958edbb4 100644 --- a/src/utils/icon-registry.js +++ b/src/utils/icon-registry.js @@ -30,6 +30,7 @@ import { Person, Policy, PrecisionManufacturing, + Public, Security, Settings, Share, @@ -74,6 +75,7 @@ export const iconRegistry = { Person, Policy, PrecisionManufacturing, + Public, Security, Settings, Share, From 983b48a1b1c35c329c9ad8405635c26e6859b0b9 Mon Sep 17 00:00:00 2001 From: Clint Thomon Date: Thu, 14 May 2026 13:59:43 -0500 Subject: [PATCH 013/133] fix: Remove accidentally committed .claude/worktrees directory The .claude/worktrees directory was accidentally committed, causing CI failures due to git treating it as a submodule without a URL in .gitmodules. - Remove .claude/worktrees from git tracking - Add .claude/ to .gitignore to prevent future accidents --- .claude/worktrees/blissful-golick-d405ab | 1 - .gitignore | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 160000 .claude/worktrees/blissful-golick-d405ab diff --git a/.claude/worktrees/blissful-golick-d405ab b/.claude/worktrees/blissful-golick-d405ab deleted file mode 160000 index 0710355e2ada..000000000000 --- a/.claude/worktrees/blissful-golick-d405ab +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0710355e2adac37fffe4c7eef48d6f2c3a04993d diff --git a/.gitignore b/.gitignore index 44dac6dd492c..2758639d59a4 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ AGENTS.md # azurite __* AzuriteConfig +# Claude/Cursor worktrees and local AI tooling +.claude/ From 0c32a84eb21d3df3a719427b54d10821a94b40a4 Mon Sep 17 00:00:00 2001 From: "jwebster@protectedtrust.com" Date: Thu, 14 May 2026 16:26:44 -0400 Subject: [PATCH 014/133] Add additional portal links to Invoke-HuduExtensionSync Added links to Defender, Compliance (purview) and the partner center page that is managed by Microsoft. --- src/data/Extensions.json | 44 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 66ff5b1a482d..7bb91b6165f9 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -614,6 +614,50 @@ "action": "disable" } }, + { + "type": "switch", + "name": "Hudu.HideEmptyRoles", + "label": "Hide Empty Roles in Magic Dash", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "switch", + "name": "Hudu.IncludeParterCenterLink", + "label": "Include link to Partner Center Service management page (partner.microsoft.com)", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "switch", + "name": "Hudu.IncludeDefenderLink", + "label": "Include link to Defender Portal (security.microsoft.com)", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, + { + "type": "switch", + "name": "Hudu.IncludeComplianceLink", + "label": "Include link to Compliance Portal (compliance.microsoft.com)", + "condition": { + "field": "Hudu.Enabled", + "compareType": "is", + "compareValue": true, + "action": "disable" + } + }, { "_comment": "I have added this switch as a logic check for the Hudu integration script to check against when CIPP first connects to the Hudu Instance via Connect-HuduAPI.ps1", "type": "switch", From 186a2c6ea22589ca28f655eba01526ceb74ec2d9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 15 May 2026 02:29:31 -0500 Subject: [PATCH 015/133] audit log template tweak --- src/data/AuditLogTemplates.json | 2 +- src/layouts/config.js | 7 + src/pages/cipp/advanced/worker-health.js | 442 +++++++++++++++++++++++ 3 files changed, 450 insertions(+), 1 deletion(-) create mode 100644 src/pages/cipp/advanced/worker-health.js diff --git a/src/data/AuditLogTemplates.json b/src/data/AuditLogTemplates.json index 1762fb2eb7bb..63df852bd318 100644 --- a/src/data/AuditLogTemplates.json +++ b/src/data/AuditLogTemplates.json @@ -450,7 +450,7 @@ { "Property": { "value": "String", "label": "SecuredAccessPassData" }, "Operator": { "value": "like", "label": "Like" }, - "Input": { "value": "*" } + "Input": { "value": "[*]" } } ] } diff --git a/src/layouts/config.js b/src/layouts/config.js index a9df0957241d..987ab9d8f984 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -1116,6 +1116,13 @@ export const nativeMenuItems = [ permissions: ['CIPP.SuperAdmin.*'], scope: 'global', }, + { + title: 'Worker Health', + path: '/cipp/advanced/worker-health', + roles: ['superadmin'], + permissions: ['CIPP.SuperAdmin.*'], + scope: 'global', + }, ], }, ], diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js new file mode 100644 index 000000000000..fd23de33c031 --- /dev/null +++ b/src/pages/cipp/advanced/worker-health.js @@ -0,0 +1,442 @@ +import { useMemo } from "react"; +import Head from "next/head"; +import { + Box, + Button, + Card, + CardContent, + CardHeader, + Chip, + CircularProgress, + Container, + LinearProgress, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; +import { + Memory, + Speed, + PlayArrow, + HourglassEmpty, + CheckCircle, + Warning, + Cancel, + Delete, + LowPriority, + DeleteSweep, +} from "@mui/icons-material"; +import { Grid } from "@mui/system"; +import { Layout as DashboardLayout } from "../../../layouts/index.js"; +import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; +import { CippPropertyListCard } from "../../../components/CippCards/CippPropertyListCard"; +import { CippDataTable } from "../../../components/CippTable/CippDataTable"; +import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; + +const formatDuration = (ms) => { + if (ms === 0 || ms == null) return "—"; + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${(ms / 60000).toFixed(1)}m`; +}; + +const formatUptime = (seconds) => { + if (!seconds) return "—"; + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +}; + +const WorkerStatusChip = ({ isBusy, currentFunction }) => { + if (isBusy) { + return ( + } + sx={{ maxWidth: 200 }} + /> + ); + } + return } />; +}; + +const UtilizationBar = ({ value }) => ( + + + 80 ? "error" : value > 50 ? "warning" : "primary"} + sx={{ height: 8, borderRadius: 4 }} + /> + + + {value}% + + +); + +const WorkerTable = ({ workers, title }) => { + if (!workers || workers.length === 0) return null; + + return ( + + + + + + + + Worker + Status + Invocations + Utilization + Avg + Min + Max + Last + Faults + + + + {workers.map((w) => ( + + + + W{w.WorkerId} + + + + + + {w.TotalInvocations?.toLocaleString() ?? 0} + + + + {formatDuration(w.AvgDurationMs)} + {formatDuration(w.MinDurationMs)} + {formatDuration(w.MaxDurationMs)} + {formatDuration(w.LastDurationMs)} + + {w.TotalFaults > 0 ? ( + + ) : ( + "0" + )} + + + ))} + +
+
+
+
+ ); +}; + +const Page = () => { + const healthQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "Snapshot" }, + queryKey: "WorkerHealth", + refetchInterval: 5000, + }); + + const jobAction = ApiPostCall({ + relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], + }); + + const snapshot = healthQuery.data?.Results; + + const infoBarData = useMemo(() => { + if (!snapshot) return []; + const http = snapshot.HttpPool || {}; + const bg = snapshot.BgPool || {}; + const jobs = snapshot.Jobs || {}; + const limiter = snapshot.Limiter || {}; + + return [ + { + icon: , + name: "HTTP Workers", + data: `${http.BusyCount ?? 0} / ${http.PoolSize ?? 0} busy`, + color: http.BusyCount >= http.PoolSize ? "error" : "primary", + }, + { + icon: , + name: "BG Workers", + data: `${bg.BusyCount ?? 0} / ${bg.PoolSize ?? 0} busy`, + color: bg.BusyCount >= bg.PoolSize ? "error" : "primary", + }, + { + icon: jobs.Running > 0 ? : , + name: "Job Queue", + data: `${jobs.Running ?? 0} running, ${jobs.Queued ?? 0} queued`, + color: jobs.Queued > 10 ? "warning" : "primary", + }, + { + icon: limiter.IsHttpThrottled ? : , + name: "BG Limiter", + data: limiter.IsHttpThrottled + ? "HTTP Throttled" + : `${limiter.Active ?? 0} / ${limiter.CurrentMax ?? 0} active`, + color: limiter.IsHttpThrottled ? "error" : "primary", + }, + ]; + }, [snapshot]); + + const httpPoolItems = useMemo(() => { + if (!snapshot?.HttpPool) return []; + const p = snapshot.HttpPool; + return [ + { label: "Pool Size", value: p.PoolSize }, + { label: "Available", value: p.Available }, + { label: "Busy", value: p.BusyCount }, + { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, + { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, + { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, + { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, + { label: "Total Faults", value: p.TotalFaults ?? 0 }, + ]; + }, [snapshot]); + + const bgPoolItems = useMemo(() => { + if (!snapshot?.BgPool) return []; + const p = snapshot.BgPool; + return [ + { label: "Pool Size", value: p.PoolSize }, + { label: "Available", value: p.Available }, + { label: "Busy", value: p.BusyCount }, + { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, + { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, + { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, + { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, + { label: "Total Faults", value: p.TotalFaults ?? 0 }, + ]; + }, [snapshot]); + + const limiterItems = useMemo(() => { + if (!snapshot?.Limiter) return []; + const l = snapshot.Limiter; + return [ + { label: "Base Concurrency", value: l.BaseConcurrency }, + { label: "Ceiling Concurrency", value: l.CeilingConcurrency }, + { label: "Current Max", value: l.CurrentMax }, + { label: "Active", value: l.Active }, + { label: "Waiting", value: l.Waiting }, + { + label: "HTTP Throttled", + value: l.IsHttpThrottled ? "Yes" : "No", + }, + ]; + }, [snapshot]); + + const jobItems = useMemo(() => { + if (!snapshot?.Jobs) return []; + const j = snapshot.Jobs; + return [ + { label: "Running", value: j.Running }, + { label: "Queued", value: j.Queued }, + { label: "Completed", value: j.Completed?.toLocaleString() ?? 0 }, + { label: "Failed", value: j.Failed }, + { label: "Total Processed", value: j.TotalProcessed?.toLocaleString() ?? 0 }, + { label: "Max Concurrency", value: j.MaxConcurrency }, + { label: "Active Concurrency", value: j.ActiveConcurrency }, + ]; + }, [snapshot]); + + const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "WaitSeconds", "DurationSeconds"]; + + const jobActions = useMemo( + () => [ + { + label: "Cancel Job", + icon: , + color: "error.main", + noConfirm: true, + customFunction: (row) => { + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "CancelJob", JobId: row.Id }, + }); + }, + condition: (row) => row.Status === "Queued", + }, + { + label: "Change Priority", + icon: , + fields: [ + { + type: "textField", + name: "Priority", + label: "New Priority (0 = highest)", + }, + ], + url: "/api/ListWorkerHealth", + data: { Action: "ChangePriority" }, + dataFunction: (row, formData) => ({ + Action: "ChangePriority", + JobId: row.Id, + Priority: parseInt(formData.Priority, 10), + }), + confirmText: "Change", + condition: (row) => row.Status === "Queued", + relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], + }, + { + label: "Cancel Run", + icon: , + color: "error.main", + noConfirm: true, + customFunction: (row) => { + if (row.RunName) { + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "CancelRun", RunName: row.RunName }, + }); + } + }, + condition: (row) => row.Status === "Queued" && row.RunName, + }, + { + label: "Delete", + icon: , + noConfirm: true, + customFunction: (row) => { + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "DeleteJob", JobId: row.Id }, + }); + }, + condition: (row) => row.Status !== "Queued" && row.Status !== "Running", + }, + ], + [jobAction] + ); + + const jobFilters = useMemo( + () => [ + { + filterName: "Queued", + value: [{ id: "Status", value: "Queued" }], + }, + { + filterName: "Running", + value: [{ id: "Status", value: "Running" }], + }, + { + filterName: "Failed", + value: [{ id: "Status", value: "Failed" }], + }, + ], + [] + ); + + return ( + <> + + Worker Health | CIPP + + + + + + Worker Health + + {healthQuery.isFetching && } + {snapshot && ( + + Uptime: {formatUptime(snapshot.UptimeSeconds)} | Auto-refreshing every 5s + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + } + color="warning" + onClick={() => + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "PurgeCompleted" }, + }) + } + > + Purge Completed + + } + /> + + + + + ); +}; + +Page.getLayout = (page) => {page}; + +export default Page; From 7a85827ef1072955a48cb1d48c3ce2aafe3ab88d Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 16:34:51 +0200 Subject: [PATCH 016/133] feat(users): add bulk update contact and UPN fields Fixes #6015 Fixes #6013 --- .../administration/users/patch-wizard.jsx | 81 ++++++++++++++++++- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/src/pages/identity/administration/users/patch-wizard.jsx b/src/pages/identity/administration/users/patch-wizard.jsx index 1168f12f593e..67e1d08041a9 100644 --- a/src/pages/identity/administration/users/patch-wizard.jsx +++ b/src/pages/identity/administration/users/patch-wizard.jsx @@ -21,11 +21,17 @@ import { CippWizardStepButtons } from '../../../../components/CippWizard/CippWiz import { ApiPostCall, ApiGetCall } from '../../../../api/ApiCall' import { CippApiResults } from '../../../../components/CippComponents/CippApiResults' import { CippDataTable } from '../../../../components/CippTable/CippDataTable' +import { CippFormDomainSelector } from '../../../../components/CippComponents/CippFormDomainSelector' import { CippFormUserSelector } from '../../../../components/CippComponents/CippFormUserSelector' import { Delete } from '@mui/icons-material' // User properties that can be patched const PATCHABLE_PROPERTIES = [ + { + property: 'businessPhones', + label: 'Business Phone', + type: 'string', + }, { property: 'city', label: 'City', @@ -51,6 +57,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Employee Type', type: 'string', }, + { + property: 'faxNumber', + label: 'Fax Number', + type: 'string', + }, { property: 'jobTitle', label: 'Job Title', @@ -61,6 +72,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Manager', type: 'userSelector', }, + { + property: 'mobilePhone', + label: 'Mobile Phone', + type: 'string', + }, { property: 'officeLocation', label: 'Office Location', @@ -106,6 +122,11 @@ const PATCHABLE_PROPERTIES = [ label: 'Usage Location', type: 'string', }, + { + property: 'userPrincipalName', + label: 'UPN Domain Suffix', + type: 'domainPicker', + }, ] // Step 1: Display users to be updated @@ -195,12 +216,15 @@ const PropertySelectionStep = (props) => { const tenantDomains = [...new Set(users?.map((user) => user.Tenant || user.tenantFilter).filter(Boolean))] || [] const firstTenantDomain = tenantDomains[0] + const isSingleTenant = tenantDomains.length <= 1 const hasManagerSelected = selectedProperties.includes('manager') const hasSponsorSelected = selectedProperties.includes('sponsor') const hasRelationshipSelected = hasManagerSelected || hasSponsorSelected + const hasUPNSelected = selectedProperties.includes('userPrincipalName') + const hasTenantScopedSelectorSelected = hasRelationshipSelected || hasUPNSelected useEffect(() => { - if (!hasRelationshipSelected || !firstTenantDomain) { + if (!hasTenantScopedSelectorSelected || !firstTenantDomain) { return } @@ -208,7 +232,7 @@ const PropertySelectionStep = (props) => { if (currentTenantFilter?.value !== firstTenantDomain) { formControl.setValue('tenantFilter', { value: firstTenantDomain }) } - }, [firstTenantDomain, formControl, hasRelationshipSelected]) + }, [firstTenantDomain, formControl, hasTenantScopedSelectorSelected]) // Fetch custom data mappings for all tenants const customDataMappings = ApiGetCall({ @@ -242,8 +266,13 @@ const PropertySelectionStep = (props) => { // Combine standard properties with custom data properties const allProperties = useMemo(() => { - return [...PATCHABLE_PROPERTIES, ...customDataProperties] - }, [customDataProperties]) + return [ + ...PATCHABLE_PROPERTIES.filter( + (property) => isSingleTenant || property.property !== 'userPrincipalName' + ), + ...customDataProperties, + ] + }, [customDataProperties, isSingleTenant]) // Register form fields formControl.register('selectedProperties', { required: true }) @@ -290,6 +319,24 @@ const PropertySelectionStep = (props) => { ) } + if (property?.type === 'domainPicker') { + return ( + + + Changes the domain after @ only. Users will be logged out and must sign in with the new + UPN. + + + + ) + } + // Default to text input for string types with consistent styling return ( { Loading custom data mappings... )} + {!isSingleTenant && ( + + UPN domain suffix changes are only available when all selected users are from the same + tenant. + + )} { return } + if (propName === 'businessPhones') { + userData[propName] = [propertyValue] + return + } + + if (propName === 'userPrincipalName') { + const selectedDomain = propertyValue?.value || propertyValue?.label || propertyValue + const currentUPN = user.userPrincipalName || '' + const upnPrefix = currentUPN.includes('@') ? currentUPN.split('@')[0] : currentUPN + + if (selectedDomain && upnPrefix) { + userData[propName] = `${upnPrefix}@${selectedDomain}` + } + + return + } + // Handle dot notation properties (e.g., "extension_abc123.customField") if (propName.includes('.')) { const parts = propName.split('.') @@ -581,6 +651,9 @@ const ConfirmationStep = (props) => { if (propName === 'manager' || propName === 'sponsor') { displayValue = value?.label || value?.value || 'Not set' + } else if (propName === 'userPrincipalName') { + const selectedDomain = value?.label || value?.value || value + displayValue = selectedDomain ? `Change domain to: ${selectedDomain}` : 'Not set' } else if (property?.type === 'boolean') { displayValue = value ? 'Yes' : 'No' } From 8232e5c11b26c897151d6daaaaa74f8288f52110 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 20:30:01 +0200 Subject: [PATCH 017/133] feat(standards): add intuneRestrictUserDeviceJoin entry Frontend pair for the new backend standard covering azureADJoin. Co-Authored-By: Claude Opus 4.7 --- src/data/standards.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 4c61c04cf250..410739a2ebb5 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4455,6 +4455,29 @@ "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", "recommendedBy": [] }, + { + "name": "standards.intuneRestrictUserDeviceJoin", + "cat": "Entra (AAD) Standards", + "tag": [], + "appliesToTest": [], + "helpText": "Controls whether users can join devices to Entra.", + "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", + "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.intuneRestrictUserDeviceJoin.disableUserDeviceJoin", + "label": "Disable users from joining devices", + "defaultValue": true + } + ], + "label": "Configure user restriction for Entra device join", + "impact": "High Impact", + "impactColour": "warning", + "addedDate": "2026-05-15", + "powershellEquivalent": "Update-MgBetaPolicyDeviceRegistrationPolicy", + "recommendedBy": [] + }, { "name": "standards.intuneRequireMFA", "cat": "Intune Standards", From f768330cd45a2b2e1f6a7cae857b7a5e2ec7d999 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Fri, 15 May 2026 20:30:31 +0200 Subject: [PATCH 018/133] fix(standards): move CIS 5.1.4.1 and SMB1001 (2.8) tags to join standard Both controls describe restricting device join, not registration. Without the move, BPA would report them green whenever registration was disabled, even with join wide open. Co-Authored-By: Claude Opus 4.7 --- src/data/standards.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 410739a2ebb5..6db99775997c 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4432,11 +4432,8 @@ { "name": "standards.intuneRestrictUserDeviceRegistration", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], - "appliesToTest": [ - "CIS_5_1_4_1", - "SMB1001_2_8" - ], + "tag": [], + "appliesToTest": [], "helpText": "Controls whether users can register devices with Entra.", "docsDescription": "Configures whether users can register devices with Entra. When disabled, users are unable to register devices with Entra.", "executiveText": "Controls whether employees can register their devices for corporate access. Disabling user device registration prevents unauthorized or unmanaged devices from connecting to company resources, enhancing overall security posture.", @@ -4458,8 +4455,11 @@ { "name": "standards.intuneRestrictUserDeviceJoin", "cat": "Entra (AAD) Standards", - "tag": [], - "appliesToTest": [], + "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], + "appliesToTest": [ + "CIS_5_1_4_1", + "SMB1001_2_8" + ], "helpText": "Controls whether users can join devices to Entra.", "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", From 9d5ce40275098a4a442d247fe42541958d08ba88 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 07:27:51 -0400 Subject: [PATCH 019/133] Org auto expanding archive property usage --- src/components/CippCards/CippExchangeInfoCard.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/CippCards/CippExchangeInfoCard.jsx b/src/components/CippCards/CippExchangeInfoCard.jsx index 6a00f53c0248..8bb2737f76c3 100644 --- a/src/components/CippCards/CippExchangeInfoCard.jsx +++ b/src/components/CippCards/CippExchangeInfoCard.jsx @@ -247,7 +247,9 @@ export const CippExchangeInfoCard = (props) => { <> - Auto Expanding Archive: + {exchangeData?.AutoExpandingArchiveScope === 'Organization' + ? 'Auto Expanding Archive: (org)' + : 'Auto Expanding Archive:'} {getCippFormatting( From 6db7e7760fc02807df809897fcda40b48a8b365b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 18 May 2026 14:37:01 -0400 Subject: [PATCH 020/133] Delete .claude directory Signed-off-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- .claude/worktrees/blissful-golick-d405ab | 1 - 1 file changed, 1 deletion(-) delete mode 160000 .claude/worktrees/blissful-golick-d405ab diff --git a/.claude/worktrees/blissful-golick-d405ab b/.claude/worktrees/blissful-golick-d405ab deleted file mode 160000 index 0710355e2ada..000000000000 --- a/.claude/worktrees/blissful-golick-d405ab +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0710355e2adac37fffe4c7eef48d6f2c3a04993d From 1e7aef11995feb42e9872ec4aefac39fc7ba67c5 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 08:19:29 -0400 Subject: [PATCH 021/133] Update alerts.json --- src/data/alerts.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index 9bce965852ab..ac8a28fa6cc0 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -125,7 +125,7 @@ "inputType": "textField", "inputLabel": "Enter quota percentage", "inputName": "QuotaUsedQuota", - "recommendedRunInterval": "4h" + "recommendedRunInterval": "1d" }, { "name": "SharePointQuota", @@ -134,7 +134,7 @@ "inputType": "textField", "inputLabel": "Enter quota percentage", "inputName": "SharePointQuota", - "recommendedRunInterval": "4h" + "recommendedRunInterval": "1d" }, { "name": "OneDriveQuota", @@ -143,7 +143,7 @@ "inputType": "textField", "inputLabel": "Enter quota percentage (default: 90)", "inputName": "OneDriveQuota", - "recommendedRunInterval": "4h" + "recommendedRunInterval": "1d" }, { "name": "ExpiringLicenses", From fc246a54ee6c1720f9d442f4cf22568b53add85d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 19 May 2026 10:02:02 -0400 Subject: [PATCH 022/133] update default value for standard --- src/data/standards.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/standards.json b/src/data/standards.json index 4c61c04cf250..16a6406db1fa 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4100,7 +4100,7 @@ "type": "number", "name": "standards.IntuneComplianceSettings.deviceComplianceCheckinThresholdDays", "label": "Compliance status validity period (days)", - "defaultValue": 130, + "defaultValue": 120, "validators": { "min": { "value": 1, "message": "Minimum value is 1" }, "max": { "value": 120, "message": "Maximum value is 120" } From 766a3c52814dcc09d9170c780b4caa5e59d7a343 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 20 May 2026 22:10:11 -0400 Subject: [PATCH 023/133] feat: add in missing options for Windows Hello standard Fixes #6034 --- src/data/standards.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 16a6406db1fa..bc21aa38b6c3 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -4358,6 +4358,28 @@ "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.remotePassportEnabled", "label": "Allow phone sign-in", "default": true + }, + { + "type": "autoComplete", + "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.enhancedSignInSecurity", + "label": "Enable enhanced sign-in security", + "multiple": false, + "options": [ + { "label": "Not configured", "value": "0" }, + { "label": "Enabled on capable hardware", "value": "1" }, + { "label": "Disabled on all systems", "value": "2" } + ] + }, + { + "type": "autoComplete", + "name": "standards.EnrollmentWindowsHelloForBusinessConfiguration.securityKeyForSignIn", + "label": "Use security keys for sign-in", + "multiple": false, + "options": [ + { "label": "Not configured", "value": "notConfigured" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ] } ], "label": "Windows Hello for Business enrollment configuration", From 5b5302ca907b971e86c5ed5fd8c1f56491f796c8 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 20 May 2026 23:18:38 -0400 Subject: [PATCH 024/133] feat(standards): add DLP via DCS OWA standard --- src/data/standards.json | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 16a6406db1fa..eb49a0e5b91d 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -2707,6 +2707,40 @@ "EXCHANGE_LITE" ] }, + { + "name": "standards.DlpViaDcsEnabled", + "cat": "Exchange Standards", + "tag": [], + "helpText": "Sets whether Outlook on the web uses Data Classification Services for DLP evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users).", + "docsDescription": "Configures whether Outlook on the web uses Data Classification Services (DCS)-based Data Loss Prevention (DLP) policy evaluation instead of Exchange-based evaluation. Review DLP policies before enabling this setting, as some legacy Exchange-based predicates are not supported with DCS-based evaluation. See [Microsoft's policy tip reference](https://learn.microsoft.com/en-us/purview/dlp-ol365-win32-policy-tips#sensitive-information-types-that-support-policy-tips-for-outlook-perpetual-users).", + "executiveText": "Improves how Outlook on the web applies Data Loss Prevention policies, giving users clearer guidance when sensitive information may be shared and helping reduce accidental data exposure.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Select value", + "name": "standards.DlpViaDcsEnabled.state", + "options": [ + { "label": "Enabled", "value": "true" }, + { "label": "Disabled", "value": "false" } + ] + } + ], + "label": "Set OWA DLP evaluation via DCS", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-20", + "powershellEquivalent": "Set-OrganizationConfig -DlpViaDcsEnabled", + "recommendedBy": [], + "requiredCapabilities": [ + "EXCHANGE_S_STANDARD", + "EXCHANGE_S_ENTERPRISE", + "EXCHANGE_S_STANDARD_GOV", + "EXCHANGE_S_ENTERPRISE_GOV", + "EXCHANGE_LITE" + ] + }, { "name": "standards.UserSubmissions", "cat": "Exchange Standards", From 131927b943aebebd6844ba986fbcf30fb21ac8bc Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 22 May 2026 12:51:00 -0400 Subject: [PATCH 025/133] Stats --- src/components/PrivateRoute.js | 33 +- src/layouts/index.js | 4 +- src/pages/cipp/advanced/worker-health.js | 785 +++++++++++++++++++---- 3 files changed, 674 insertions(+), 148 deletions(-) diff --git a/src/components/PrivateRoute.js b/src/components/PrivateRoute.js index 5b067cf4e7c3..15b438b2c608 100644 --- a/src/components/PrivateRoute.js +++ b/src/components/PrivateRoute.js @@ -2,8 +2,11 @@ import { ApiGetCall } from "../api/ApiCall.jsx"; import UnauthenticatedPage from "../pages/unauthenticated.js"; import LoadingPage from "../pages/loading.js"; import ApiOfflinePage from "../pages/api-offline.js"; +import { useState, useEffect } from "react"; export const PrivateRoute = ({ children, routeType }) => { + const [unauthLatched, setUnauthLatched] = useState(false); + const session = ApiGetCall({ url: "/.auth/me", queryKey: "authmeswa", @@ -11,13 +14,34 @@ export const PrivateRoute = ({ children, routeType }) => { staleTime: 120000, // 2 minutes }); + // Latch the unauthenticated state so refetches from child components + // don't flip us back to loading. Clear the latch when session succeeds (after login). + useEffect(() => { + if ( + !session.isLoading && + !session.isFetching && + (session.isError || + null === session?.data?.clientPrincipal || + session?.data === undefined) + ) { + setUnauthLatched(true); + } else if (session.isSuccess && session.data?.clientPrincipal) { + setUnauthLatched(false); + } + }, [session.isLoading, session.isFetching, session.isError, session.isSuccess, session.data]); + const apiRoles = ApiGetCall({ url: "/api/me", queryKey: "authmecipp", - retry: 2, // Reduced retry count to show offline message sooner - waiting: !session.isSuccess || session.data?.clientPrincipal === null, + retry: 2, + waiting: session.isSuccess && session.data?.clientPrincipal !== null, }); + // If latched as unauthenticated, always show unauthenticated page + if (unauthLatched) { + return ; + } + // Check if the session is still loading before determining authentication status if ( session.isLoading || @@ -38,11 +62,6 @@ export const PrivateRoute = ({ children, routeType }) => { return ; } - // if not logged into swa - if (null === session?.data?.clientPrincipal || session?.data === undefined) { - return ; - } - let roles = null; if ( diff --git a/src/layouts/index.js b/src/layouts/index.js index d00dc338a82d..f69628a3c706 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -35,7 +35,7 @@ const OnboardingWizardPage = dynamic( { ssr: false } ) -const SIDE_NAV_WIDTH = 270 +const SIDE_NAV_WIDTH = 290 const SIDE_NAV_PINNED_WIDTH = 50 const TOP_NAV_HEIGHT = 50 @@ -111,7 +111,7 @@ export const Layout = (props) => { const currentRole = ApiGetCall({ url: '/api/me', queryKey: 'authmecipp', - waiting: !swaStatus.isSuccess || swaStatus.data?.clientPrincipal === null, + waiting: swaStatus.isSuccess && swaStatus.data?.clientPrincipal !== null, }) const featureFlags = ApiGetCall({ diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index fd23de33c031..ccc19a335570 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import Head from "next/head"; import { Box, @@ -9,6 +9,7 @@ import { Chip, CircularProgress, Container, + IconButton, LinearProgress, Stack, Table, @@ -17,6 +18,9 @@ import { TableContainer, TableHead, TableRow, + ToggleButton, + ToggleButtonGroup, + Tooltip, Typography, } from "@mui/material"; import { @@ -30,11 +34,33 @@ import { Delete, LowPriority, DeleteSweep, + Timeline, + RocketLaunch, + Pause, + FileDownload, + FileUpload, + Refresh, + Close, } from "@mui/icons-material"; import { Grid } from "@mui/system"; +import { useTheme } from "@mui/material/styles"; +import { useQueryClient } from "@tanstack/react-query"; +import { + AreaChart, + Area, + LineChart, + Line, + BarChart, + Bar, + CartesianGrid, + XAxis, + YAxis, + ResponsiveContainer, + Tooltip as RechartsTooltip, + Legend, +} from "recharts"; import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippInfoBar } from "../../../components/CippCards/CippInfoBar"; -import { CippPropertyListCard } from "../../../components/CippCards/CippPropertyListCard"; import { CippDataTable } from "../../../components/CippTable/CippDataTable"; import { ApiGetCall, ApiPostCall } from "../../../api/ApiCall"; @@ -142,19 +168,376 @@ const WorkerTable = ({ workers, title }) => { ); }; +const TIME_RANGES = [ + { label: "1h", minutes: 60 }, + { label: "6h", minutes: 360 }, + { label: "24h", minutes: 1440 }, + { label: "3d", minutes: 4320 }, + { label: "7d", minutes: 10080 }, +]; + +const formatChartTime = (timestamp, rangeMinutes) => { + const d = new Date(timestamp); + if (rangeMinutes <= 1440) { + return d.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" }); + } + return d.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +}; + +const STARTUP_PHASES = [ + { key: "BaseWorkerMs", label: "Base Worker", fkey: "BaseFunctionCount", color: "#7c4dff" }, + { key: "WarmupMs", label: "Warmup", fkey: null, color: "#ffc107" }, + { key: "HttpReadyMs", label: "HTTP Ready", fkey: "HttpFunctionCount", color: "#00c853" }, + { key: "HttpPoolFullMs", label: "HTTP Pool Full", fkey: null, color: "#69f0ae" }, + { key: "BgReadyMs", label: "BG Ready", fkey: "BgFunctionCount", color: "#29b6f6" }, + { key: "FullyReadyMs", label: "Fully Ready", fkey: null, color: "#66bb6a" }, +]; + +const StartupTimingBar = ({ startup }) => { + if (!startup) return null; + + // Build segments as incremental durations between phases + const phases = STARTUP_PHASES.filter((p) => startup[p.key] > 0); + const totalMs = startup.FullyReadyMs || Math.max(...phases.map((p) => startup[p.key]), 1); + + // Compute incremental segments (each phase = cumulative time to that point) + const segments = phases.map((phase, i) => { + const cumMs = startup[phase.key]; + const prevMs = i > 0 ? startup[phases[i - 1].key] : 0; + const deltaMs = Math.max(cumMs - prevMs, 0); + return { + ...phase, + cumMs, + deltaMs, + pct: totalMs > 0 ? (deltaMs / totalMs) * 100 : 0, + functions: phase.fkey ? startup[phase.fkey] : null, + }; + }); + + return ( + + } + subheader={`${startup.ReadinessMode} / ${startup.WarmupMode} — ${startup.CpuCount} CPUs, ${startup.HttpPoolSize}H + ${startup.BgPoolSize}BG — Total: ${formatDuration(totalMs)}`} + subheaderTypographyProps={{ variant: "caption" }} + sx={{ pb: 0 }} + /> + + {/* Single horizontal stacked bar */} + + {segments.map((seg) => ( + + + {seg.label} + + + {formatDuration(seg.deltaMs)} (cumulative: {formatDuration(seg.cumMs)}) + + {seg.functions != null && ( + + {seg.functions} functions loaded + + )} + + } + > + 8 ? 0 : 4, + cursor: "pointer", + transition: "filter 0.15s", + "&:hover": { filter: "brightness(1.2)" }, + }} + > + {seg.pct > 12 && ( + + {formatDuration(seg.deltaMs)} + + )} + + + ))} + + {/* Legend */} + + {segments.map((seg) => ( + + + + {seg.label} + {seg.functions != null && ` (${seg.functions})`} + + + ))} + + Modules: {startup.SharedModuleCount} shared + {startup.HttpOnlyModuleCount > 0 && `, ${startup.HttpOnlyModuleCount} HTTP`} + {startup.BgOnlyModuleCount > 0 && `, ${startup.BgOnlyModuleCount} BG`} + + + + + ); +}; + +const CompactStatsRow = ({ snapshot }) => { + if (!snapshot) return null; + + const http = snapshot.HttpPool || {}; + const bg = snapshot.BgPool || {}; + const jobs = snapshot.Jobs || {}; + const limiter = snapshot.Limiter || {}; + + const sections = [ + { + label: "HTTP Pool", + color: "primary", + stats: [ + { k: "Size", v: http.PoolSize ?? 0 }, + { k: "Busy", v: http.BusyCount ?? 0, w: http.BusyCount >= http.PoolSize }, + { k: "Invocations", v: http.TotalInvocations?.toLocaleString() ?? 0 }, + { k: "Util", v: `${http.AvgUtilizationPct ?? 0}%`, w: http.AvgUtilizationPct > 80 }, + { k: "Avg", v: formatDuration(http.AvgDurationMs) }, + { k: "Faults", v: http.TotalFaults ?? 0, w: http.TotalFaults > 0 }, + ], + }, + { + label: "BG Pool", + color: "warning", + stats: [ + { k: "Size", v: bg.PoolSize ?? 0 }, + { k: "Busy", v: bg.BusyCount ?? 0, w: bg.BusyCount >= bg.PoolSize }, + { k: "Invocations", v: bg.TotalInvocations?.toLocaleString() ?? 0 }, + { k: "Util", v: `${bg.AvgUtilizationPct ?? 0}%`, w: bg.AvgUtilizationPct > 80 }, + { k: "Avg", v: formatDuration(bg.AvgDurationMs) }, + { k: "Faults", v: bg.TotalFaults ?? 0, w: bg.TotalFaults > 0 }, + ], + }, + { + label: "Jobs", + color: "info", + stats: [ + { k: "Running", v: jobs.Running ?? 0 }, + { k: "Queued", v: jobs.Queued ?? 0, w: jobs.Queued > 10 }, + { k: "Done", v: jobs.Completed?.toLocaleString() ?? 0 }, + { k: "Failed", v: jobs.Failed ?? 0, w: jobs.Failed > 0 }, + ], + }, + { + label: "Limiter", + color: "default", + stats: [ + { k: "Active", v: `${limiter.Active ?? 0} / ${limiter.CurrentMax ?? 0}` }, + { k: "Waiting", v: limiter.Waiting ?? 0 }, + ...(limiter.IsHttpThrottled ? [{ k: "Status", v: "Throttled", w: true }] : []), + ], + }, + ]; + + return ( + + + + + + {sections.map((sec) => ( + + + + + {sec.stats.map((s) => ( + + + {s.k} + + + {s.v} + + + ))} + {/* Pad empty cells so columns stay aligned */} + {Array.from({ length: Math.max(0, 6 - sec.stats.length) }).map((_, i) => ( + + ))} + + ))} + +
+
+
+
+ ); +}; + +const HistoryChart = ({ data, rangeMinutes, title, icon, children }) => { + const theme = useTheme(); + + if (!data || data.length === 0) { + return ( + + + + + + No historical data available yet — data collection starts after 60 seconds + + + + + ); + } + + return ( + + + + + + {children(data, theme)} + + + + + ); +}; + const Page = () => { + const theme = useTheme(); + const queryClient = useQueryClient(); + const fileInputRef = useRef(null); + const [historyRange, setHistoryRange] = useState(60); + const [paused, setPaused] = useState(false); + const [importedData, setImportedData] = useState(null); + + const isImported = importedData !== null; + const effectivePaused = paused || isImported; + const healthQuery = ApiGetCall({ url: "/api/ListWorkerHealth", data: { Action: "Snapshot" }, queryKey: "WorkerHealth", - refetchInterval: 5000, + refetchInterval: effectivePaused ? false : 5000, + }); + + const startupQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "Startup" }, + queryKey: "WorkerStartup", + }); + + const historyQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "History", Minutes: String(historyRange), MaxPoints: "500" }, + queryKey: `WorkerHistory-${historyRange}`, + refetchInterval: effectivePaused ? false : 60000, }); const jobAction = ApiPostCall({ relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], }); - const snapshot = healthQuery.data?.Results; + // Resolve data: imported overrides live + const snapshot = isImported ? importedData.snapshot : healthQuery.data?.Results; + const startupInfo = isImported ? importedData.startup : startupQuery.data?.Results; + const importedJobs = useMemo(() => { + if (!isImported || !importedData.jobs) return null; + // Handle both array and { Results: [...] } shapes from query cache + if (Array.isArray(importedData.jobs)) return importedData.jobs; + if (Array.isArray(importedData.jobs?.Results)) return importedData.jobs.Results; + if (Array.isArray(importedData.jobs?.data?.Results)) return importedData.jobs.data.Results; + if (Array.isArray(importedData.jobs?.data)) return importedData.jobs.data; + return []; + }, [isImported, importedData]); + + const historyData = useMemo(() => { + const raw = isImported + ? importedData.history?.Data ?? importedData.history + : historyQuery.data?.Results?.Data; + if (!raw || !Array.isArray(raw)) return []; + return raw.map((p) => ({ + ...p, + time: formatChartTime(p.TimestampUtc, isImported ? importedData.historyRange ?? 60 : historyRange), + })); + }, [historyQuery.data, historyRange, importedData, isImported]); + + // ── Export ── + const handleExport = useCallback(() => { + const payload = { + exportedAt: new Date().toISOString(), + historyRange, + snapshot: healthQuery.data?.Results ?? null, + startup: startupQuery.data?.Results ?? null, + history: historyQuery.data?.Results ?? null, + jobs: null, + }; + // Try to grab current job data from query cache + // CippDataTable may store the key with extra params, so search by prefix + const allQueries = queryClient.getQueriesData({ queryKey: ["WorkerHealthJobs"] }); + for (const [, data] of allQueries) { + if (data) { + const rows = data?.Results ?? data?.data?.Results ?? data; + if (Array.isArray(rows)) { + payload.jobs = rows; + break; + } + } + } + + const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `worker-health-${new Date().toISOString().slice(0, 16).replace(/:/g, "")}.json`; + a.click(); + URL.revokeObjectURL(url); + }, [healthQuery.data, startupQuery.data, historyQuery.data, historyRange, queryClient]); + + // ── Import ── + const handleImport = useCallback((event) => { + const file = event.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (e) => { + try { + const data = JSON.parse(e.target.result); + setImportedData(data); + setPaused(true); + } catch { + // invalid JSON — ignore + } + }; + reader.readAsText(file); + // Reset input so same file can be re-imported + event.target.value = ""; + }, []); + + const handleClearImport = useCallback(() => { + setImportedData(null); + setPaused(false); + }, []); + + const handleRefreshHistory = useCallback(() => { + queryClient.invalidateQueries({ queryKey: [`WorkerHistory-${historyRange}`] }); + }, [queryClient, historyRange]); const infoBarData = useMemo(() => { if (!snapshot) return []; @@ -193,66 +576,6 @@ const Page = () => { ]; }, [snapshot]); - const httpPoolItems = useMemo(() => { - if (!snapshot?.HttpPool) return []; - const p = snapshot.HttpPool; - return [ - { label: "Pool Size", value: p.PoolSize }, - { label: "Available", value: p.Available }, - { label: "Busy", value: p.BusyCount }, - { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, - { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, - { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, - { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, - { label: "Total Faults", value: p.TotalFaults ?? 0 }, - ]; - }, [snapshot]); - - const bgPoolItems = useMemo(() => { - if (!snapshot?.BgPool) return []; - const p = snapshot.BgPool; - return [ - { label: "Pool Size", value: p.PoolSize }, - { label: "Available", value: p.Available }, - { label: "Busy", value: p.BusyCount }, - { label: "Total Invocations", value: p.TotalInvocations?.toLocaleString() ?? 0 }, - { label: "Total Busy Time", value: formatDuration(p.TotalBusyMs) }, - { label: "Avg Utilization", value: `${p.AvgUtilizationPct ?? 0}%` }, - { label: "Avg Duration", value: formatDuration(p.AvgDurationMs) }, - { label: "Total Faults", value: p.TotalFaults ?? 0 }, - ]; - }, [snapshot]); - - const limiterItems = useMemo(() => { - if (!snapshot?.Limiter) return []; - const l = snapshot.Limiter; - return [ - { label: "Base Concurrency", value: l.BaseConcurrency }, - { label: "Ceiling Concurrency", value: l.CeilingConcurrency }, - { label: "Current Max", value: l.CurrentMax }, - { label: "Active", value: l.Active }, - { label: "Waiting", value: l.Waiting }, - { - label: "HTTP Throttled", - value: l.IsHttpThrottled ? "Yes" : "No", - }, - ]; - }, [snapshot]); - - const jobItems = useMemo(() => { - if (!snapshot?.Jobs) return []; - const j = snapshot.Jobs; - return [ - { label: "Running", value: j.Running }, - { label: "Queued", value: j.Queued }, - { label: "Completed", value: j.Completed?.toLocaleString() ?? 0 }, - { label: "Failed", value: j.Failed }, - { label: "Total Processed", value: j.TotalProcessed?.toLocaleString() ?? 0 }, - { label: "Max Concurrency", value: j.MaxConcurrency }, - { label: "Active Concurrency", value: j.ActiveConcurrency }, - ]; - }, [snapshot]); - const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "WaitSeconds", "DurationSeconds"]; const jobActions = useMemo( @@ -324,18 +647,9 @@ const Page = () => { const jobFilters = useMemo( () => [ - { - filterName: "Queued", - value: [{ id: "Status", value: "Queued" }], - }, - { - filterName: "Running", - value: [{ id: "Status", value: "Running" }], - }, - { - filterName: "Failed", - value: [{ id: "Status", value: "Failed" }], - }, + { filterName: "Queued", value: [{ id: "Status", value: "Queued" }] }, + { filterName: "Running", value: [{ id: "Status", value: "Running" }] }, + { filterName: "Failed", value: [{ id: "Status", value: "Failed" }] }, ], [] ); @@ -347,89 +661,282 @@ const Page = () => { - + + {/* ── Header toolbar ── */} Worker Health - {healthQuery.isFetching && } - {snapshot && ( - - Uptime: {formatUptime(snapshot.UptimeSeconds)} | Auto-refreshing every 5s + {isImported && ( + } + /> + )} + {!isImported && healthQuery.isFetching && } + {!isImported && snapshot && ( + + Uptime: {formatUptime(snapshot.UptimeSeconds)} )} + + setPaused((p) => !p)} + color={effectivePaused ? "warning" : "default"} + disabled={isImported} + > + {effectivePaused ? : } + + + + + + + + + fileInputRef.current?.click()}> + + + + + {/* ── KPI bar ── */} + {/* ── Compact pool / jobs / limiter stats ── */} + + + {/* ── Worker tables ── */} + + + + {/* ── Job Queue ── */} + {isImported && importedJobs ? ( + + + + {importedJobs.length === 0 ? ( + + + No job data was captured in this export + + + ) : ( + + + + + {jobSimpleColumns.map((col) => ( + {col} + ))} + + + + {importedJobs.slice(0, 200).map((row, i) => ( + + {jobSimpleColumns.map((col) => ( + + {row[col] != null ? String(row[col]) : "—"} + + ))} + + ))} + +
+
+ )} +
+
+ ) : ( + } + color="warning" + onClick={() => + jobAction.mutate({ + url: "/api/ListWorkerHealth", + data: { Action: "PurgeCompleted" }, + }) + } + > + Purge Completed + + } + /> + )} + + {/* ── Historical Trends header with controls ── */} + + } + action={ + + {!isImported && ( + + + + + + )} + val !== null && setHistoryRange(val)} + size="small" + disabled={isImported} + > + {TIME_RANGES.map((r) => ( + + {r.label} + + ))} + + + } + /> + + - - + + } + > + {(data, t) => ( + + + + + + + + + + )} + - - + + } + > + {(data, t) => ( + + + + + + + + + + )} + - - + + } + > + {(data, t) => ( + + + + + + + + + + )} + - - + + } + > + {(data, t) => ( + + + + + + + + + + + + + )} + - - - - } - color="warning" - onClick={() => - jobAction.mutate({ - url: "/api/ListWorkerHealth", - data: { Action: "PurgeCompleted" }, - }) - } - > - Purge Completed - - } - /> + {/* ── Startup Timing (bottom) ── */} +
From a6ae2610d2bf4977dc4ff0a63979d32f263cf440 Mon Sep 17 00:00:00 2001 From: Luis Mengel Date: Sat, 23 May 2026 15:21:33 +0200 Subject: [PATCH 026/133] Add Group-Based Licensing support --- .../CippFormLicenseSelector.jsx | 1 + .../CippFormPages/CippAddGroupForm.jsx | 16 ++ .../CippAddGroupTemplateForm.jsx | 16 ++ .../CippWizard/CippWizardGroupTemplates.jsx | 4 + .../administration/group-templates/edit.jsx | 1 + .../identity/administration/groups/edit.jsx | 234 +++++++++++------- 6 files changed, 186 insertions(+), 86 deletions(-) diff --git a/src/components/CippComponents/CippFormLicenseSelector.jsx b/src/components/CippComponents/CippFormLicenseSelector.jsx index 28d3db47f745..8ec20ec6478a 100644 --- a/src/components/CippComponents/CippFormLicenseSelector.jsx +++ b/src/components/CippComponents/CippFormLicenseSelector.jsx @@ -25,6 +25,7 @@ export const CippFormLicenseSelector = ({ addedField: addedField, tenantFilter: userSettingsDefaults.currentTenant ?? undefined, url: "/api/ListLicenses", + dataKey: "Results", labelField: (option) => `${getCippLicenseTranslation([option])} (${option?.availableUnits} available)`, valueField: "skuId", diff --git a/src/components/CippFormPages/CippAddGroupForm.jsx b/src/components/CippFormPages/CippAddGroupForm.jsx index 713bb414c638..6ce4b08ce3bc 100644 --- a/src/components/CippFormPages/CippAddGroupForm.jsx +++ b/src/components/CippFormPages/CippAddGroupForm.jsx @@ -4,6 +4,7 @@ import CippFormComponent from "../CippComponents/CippFormComponent"; import { CippFormCondition } from "../CippComponents/CippFormCondition"; import { CippFormDomainSelector } from "../CippComponents/CippFormDomainSelector"; import { CippFormUserSelector } from "../CippComponents/CippFormUserSelector"; +import { CippFormLicenseSelector } from "../CippComponents/CippFormLicenseSelector"; const DynamicMembershipRules = ({ formControl }) => ( @@ -100,6 +101,21 @@ const CippAddGroupForm = (props) => { ]} /> + + + + + { const { formControl } = props; @@ -66,6 +67,21 @@ const CippAddGroupTemplateForm = (props) => { {/* Debug output */}
Current groupType: {formControl.watch("groupType")}
+ + + + + { formControl.setValue("membershipRules", watcher.addedFields.membershipRules, { shouldValidate: true, }); + formControl.setValue("licenses", watcher.addedFields.licenses || [], { + shouldValidate: true, + }); console.log("Set membershipRules to:", watcher.addedFields.membershipRules); }, 100); @@ -71,6 +74,7 @@ export const CippWizardGroupTemplates = (props) => { username: "username", allowExternal: "allowExternal", membershipRules: "membershipRules", + licenses: "licenses", }, showRefresh: true, }} diff --git a/src/pages/identity/administration/group-templates/edit.jsx b/src/pages/identity/administration/group-templates/edit.jsx index 8c83b0461005..5748cc165f72 100644 --- a/src/pages/identity/administration/group-templates/edit.jsx +++ b/src/pages/identity/administration/group-templates/edit.jsx @@ -44,6 +44,7 @@ const Page = () => { groupType: templateData.groupType, membershipRules: templateData.membershipRules, allowExternal: templateData.allowExternal, + licenses: templateData.licenses || [], tenantFilter: userSettingsDefaults.currentTenant, }); formControl.trigger(); diff --git a/src/pages/identity/administration/groups/edit.jsx b/src/pages/identity/administration/groups/edit.jsx index 4409353cb92b..a4ef9e9d5cb6 100644 --- a/src/pages/identity/administration/groups/edit.jsx +++ b/src/pages/identity/administration/groups/edit.jsx @@ -1,41 +1,43 @@ -import { useEffect, useState } from "react"; -import { Box, Button, Divider, Typography, Alert } from "@mui/material"; -import { Grid } from "@mui/system"; -import { useForm } from "react-hook-form"; -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; -import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; -import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; -import { useRouter } from "next/router"; -import { ApiGetCall } from "../../../../api/ApiCall"; -import { useSettings } from "../../../../hooks/use-settings"; -import { CippFormContactSelector } from "../../../../components/CippComponents/CippFormContactSelector"; -import { CippDataTable } from "../../../../components/CippTable/CippDataTable"; +import { useEffect, useState } from 'react' +import { Box, Button, Divider, Typography, Alert } from '@mui/material' +import { Grid } from '@mui/system' +import { useForm } from 'react-hook-form' +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import CippFormPage from '../../../../components/CippFormPages/CippFormPage' +import CippFormComponent from '../../../../components/CippComponents/CippFormComponent' +import { CippFormUserSelector } from '../../../../components/CippComponents/CippFormUserSelector' +import { useRouter } from 'next/router' +import { ApiGetCall } from '../../../../api/ApiCall' +import { useSettings } from '../../../../hooks/use-settings' +import { CippFormContactSelector } from '../../../../components/CippComponents/CippFormContactSelector' +import { CippDataTable } from '../../../../components/CippTable/CippDataTable' +import { CippFormLicenseSelector } from '../../../../components/CippComponents/CippFormLicenseSelector' +import { getCippLicenseTranslation } from '../../../../utils/get-cipp-license-translation' const EditGroup = () => { - const router = useRouter(); - const { groupId, groupType } = router.query; - const [groupIdReady, setGroupIdReady] = useState(false); - const [showMembershipTable, setShowMembershipTable] = useState(false); - const [combinedData, setCombinedData] = useState([]); - const [initialValues, setInitialValues] = useState({}); - const tenantFilter = useSettings().currentTenant; + const router = useRouter() + const { groupId, groupType } = router.query + const [groupIdReady, setGroupIdReady] = useState(false) + const [showMembershipTable, setShowMembershipTable] = useState(false) + const [combinedData, setCombinedData] = useState([]) + const [initialValues, setInitialValues] = useState({}) + const tenantFilter = useSettings().currentTenant const groupInfo = ApiGetCall({ url: `/api/ListGroups?groupID=${groupId}&tenantFilter=${tenantFilter}&members=true&owners=true&groupType=${groupType}`, queryKey: `ListGroups-${groupId}`, waiting: groupIdReady, - }); + }) useEffect(() => { if (groupId) { - setGroupIdReady(true); - groupInfo.refetch(); + setGroupIdReady(true) + groupInfo.refetch() } - }, [router.query, groupId, tenantFilter]); + }, [router.query, groupId, tenantFilter]) const formControl = useForm({ - mode: "onChange", + mode: 'onChange', defaultValues: { tenantFilter: tenantFilter, AddMember: [], @@ -44,51 +46,53 @@ const EditGroup = () => { RemoveOwner: [], AddContact: [], RemoveContact: [], - visibility: "Public", + AddLicenses: [], + RemoveLicenses: [], + visibility: 'Public', }, - }); + }) useEffect(() => { if (groupInfo.isSuccess) { - const group = groupInfo.data?.groupInfo; + const group = groupInfo.data?.groupInfo if (group) { // Create combined data for the table const combinedData = [ ...(groupInfo.data?.owners?.map((o) => ({ - type: "Owner", + type: 'Owner', userPrincipalName: o.userPrincipalName, displayName: o.displayName, })) || []), ...(groupInfo.data?.members?.map((m) => ({ - type: m?.["@odata.type"] === "#microsoft.graph.orgContact" ? "Contact" : "Member", + type: m?.['@odata.type'] === '#microsoft.graph.orgContact' ? 'Contact' : 'Member', userPrincipalName: m.userPrincipalName ?? m.mail, displayName: m.displayName, })) || []), - ]; - setCombinedData(combinedData); + ] + setCombinedData(combinedData) // Create initial values object const formValues = { tenantFilter: tenantFilter, mail: group.mail, - mailNickname: group.mailNickname || "", + mailNickname: group.mailNickname || '', allowExternal: groupInfo?.data?.allowExternal, sendCopies: groupInfo?.data?.sendCopies, hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, - visibility: group?.visibility ?? "Public", + visibility: group?.visibility ?? 'Public', displayName: group.displayName, - description: group.description || "", - membershipRules: group.membershipRule || "", + description: group.description || '', + membershipRules: group.membershipRule || '', groupId: group.id, groupType: (() => { - if (group.groupTypes?.includes("Unified")) { - return "Microsoft 365"; + if (group.groupTypes?.includes('Unified')) { + return 'Microsoft 365' } if (!group.mailEnabled && group.securityEnabled) { - return "Security"; + return 'Security' } if (group.mailEnabled && group.securityEnabled) { - return "Mail-Enabled Security"; + return 'Mail-Enabled Security' } if ( @@ -96,9 +100,9 @@ const EditGroup = () => { group.mailEnabled && !group.securityEnabled ) { - return "Distribution List"; + return 'Distribution List' } - return null; + return null })(), securityEnabled: group.securityEnabled, // Initialize empty arrays for add/remove actions @@ -108,7 +112,9 @@ const EditGroup = () => { RemoveOwner: [], AddContact: [], RemoveContact: [], - }; + AddLicenses: [], + RemoveLicenses: [], + } // Store initial values for comparison setInitialValues({ @@ -116,43 +122,43 @@ const EditGroup = () => { sendCopies: groupInfo?.data?.sendCopies, hideFromOutlookClients: groupInfo?.data?.hideFromOutlookClients, securityEnabled: group.securityEnabled, - visibility: group.visibility ?? "Public", - }); + visibility: group.visibility ?? 'Public', + }) // Reset the form with all values - formControl.reset(formValues); + formControl.reset(formValues) } } - }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]); + }, [groupInfo.isSuccess, router.query, groupInfo.isFetching]) // Custom data formatter to only send changed values const customDataFormatter = (formData) => { - const cleanedData = { ...formData }; + const cleanedData = { ...formData } // Properties that should only be sent if they've changed from initial values const changeDetectionProperties = [ - "allowExternal", - "sendCopies", - "hideFromOutlookClients", - "securityEnabled", - "visibility", - ]; + 'allowExternal', + 'sendCopies', + 'hideFromOutlookClients', + 'securityEnabled', + 'visibility', + ] changeDetectionProperties.forEach((property) => { if (formData[property] === initialValues[property]) { - delete cleanedData[property]; + delete cleanedData[property] } - }); + }) - return cleanedData; - }; + return cleanedData + } return ( <> { onClick={() => setShowMembershipTable(!showMembershipTable)} sx={{ mb: 2 }} > - {showMembershipTable ? "Edit Membership" : "View members"} + {showMembershipTable ? 'Edit Membership' : 'View members'} } @@ -181,7 +187,7 @@ const EditGroup = () => { @@ -225,7 +231,7 @@ const EditGroup = () => { /> - {groupInfo.data?.groupInfo?.groupTypes?.includes("DynamicMembership") && ( + {groupInfo.data?.groupInfo?.groupTypes?.includes('DynamicMembership') && ( { isFetching={groupInfo.isFetching} disabled={groupInfo.isFetching} addedField={{ - id: "id", - displayName: "displayName", - userPrincipalName: "userPrincipalName", + id: 'id', + displayName: 'displayName', + userPrincipalName: 'userPrincipalName', }} dataFilter={(option) => !groupInfo.data?.members?.some((m) => m.id === option.value) @@ -272,9 +278,9 @@ const EditGroup = () => { isFetching={groupInfo.isFetching} disabled={groupInfo.isFetching} addedField={{ - id: "id", - displayName: "displayName", - userPrincipalName: "userPrincipalName", + id: 'id', + displayName: 'displayName', + userPrincipalName: 'userPrincipalName', }} dataFilter={(option) => !groupInfo.data?.owners?.some((o) => o.id === option.value) @@ -289,15 +295,15 @@ const EditGroup = () => { label="Add Contacts" multiple={true} addedField={{ - id: "Guid", - displayName: "displayName", - WindowsEmailAddress: "WindowsEmailAddress", + id: 'Guid', + displayName: 'displayName', + WindowsEmailAddress: 'WindowsEmailAddress', }} isFetching={groupInfo.isFetching} disabled={groupInfo.isFetching} dataFilter={(option) => !groupInfo.data?.members - ?.filter((m) => m?.["@odata.type"] === "#microsoft.graph.orgContact") + ?.filter((m) => m?.['@odata.type'] === '#microsoft.graph.orgContact') ?.some((c) => c.id === option.value) } /> @@ -319,7 +325,7 @@ const EditGroup = () => { disabled={groupInfo.isFetching} options={ groupInfo.data?.members - ?.filter((m) => m?.["@odata.type"] !== "#microsoft.graph.orgContact") + ?.filter((m) => m?.['@odata.type'] !== '#microsoft.graph.orgContact') ?.map((m) => ({ label: `${m.displayName} (${m.userPrincipalName})`, value: m.id, @@ -369,7 +375,7 @@ const EditGroup = () => { disabled={groupInfo.isFetching} options={ groupInfo.data?.members - ?.filter((m) => m?.["@odata.type"] === "#microsoft.graph.orgContact") + ?.filter((m) => m?.['@odata.type'] === '#microsoft.graph.orgContact') ?.map((m) => ({ label: `${m.displayName} (${m.mail})`, value: m.mail, @@ -385,7 +391,7 @@ const EditGroup = () => { Group Settings - {groupType === "Microsoft 365" && ( + {groupType === 'Microsoft 365' && ( { isFetching={groupInfo.isFetching} disabled={groupInfo.isFetching} options={[ - { label: "Public", value: "Public" }, - { label: "Private", value: "Private" }, + { label: 'Public', value: 'Public' }, + { label: 'Private', value: 'Private' }, ]} /> )} - {(groupType === "Microsoft 365" || groupType === "Distribution List") && ( + {(groupType === 'Microsoft 365' || groupType === 'Distribution List') && ( { )} - {groupType === "Microsoft 365" && ( + {groupType === 'Microsoft 365' && ( { )} - {groupType === "Microsoft 365" && ( + {groupType === 'Microsoft 365' && ( { /> )} - {groupType === "Microsoft 365" && ( + {groupType === 'Microsoft 365' && ( { /> )} + + {groupType === 'Security' && !groupInfo.data?.groupInfo?.onPremisesSyncEnabled && ( + <> + + Licenses + + Licenses assigned to this group are automatically applied to all members. + Changes can take 2-5 minutes to propagate. + + + + {groupInfo.data?.groupInfo?.assignedLicenses?.length > 0 && ( + + + Currently assigned licenses: + + {groupInfo.data.groupInfo.assignedLicenses.map((lic) => ( + + - {getCippLicenseTranslation([lic])} + + ))} + + )} + + + + + + + ({ + label: getCippLicenseTranslation([lic]), + value: lic.skuId, + })) || [] + } + sortOptions={true} + /> + + + )} )} - ); -}; + ) +} -EditGroup.getLayout = (page) => {page}; +EditGroup.getLayout = (page) => {page} -export default EditGroup; +export default EditGroup From a32790443fc1297fab6900acc498df9cff107aa0 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 24 May 2026 09:13:35 +1000 Subject: [PATCH 027/133] CIPP Hosted Notices --- .../CippComponents/FailedPaymentDialog.jsx | 49 +++++++++++++++++++ .../SubscriptionEndedDialog.jsx | 25 ++++++++++ src/layouts/index.js | 4 ++ 3 files changed, 78 insertions(+) create mode 100644 src/components/CippComponents/FailedPaymentDialog.jsx create mode 100644 src/components/CippComponents/SubscriptionEndedDialog.jsx diff --git a/src/components/CippComponents/FailedPaymentDialog.jsx b/src/components/CippComponents/FailedPaymentDialog.jsx new file mode 100644 index 000000000000..5668f619c443 --- /dev/null +++ b/src/components/CippComponents/FailedPaymentDialog.jsx @@ -0,0 +1,49 @@ +import { useEffect, useState, useCallback } from 'react' +import { Alert, Button, Dialog, DialogActions, DialogContent, DialogTitle, Typography } from '@mui/material' + +const DISMISS_KEY = 'cipp_hosted_payment_dismissed' +const DISMISS_DURATION_MS = 24 * 60 * 60 * 1000 // 1 day + +export const FailedPaymentDialog = ({ hostedFailedPayments }) => { + const [open, setOpen] = useState(false) + + useEffect(() => { + if (!hostedFailedPayments) return + + const dismissedAt = localStorage.getItem(DISMISS_KEY) + if (dismissedAt && Date.now() - Number(dismissedAt) < DISMISS_DURATION_MS) return + + setOpen(true) + }, [hostedFailedPayments]) + + const handleDismiss = useCallback(() => { + localStorage.setItem(DISMISS_KEY, String(Date.now())) + setOpen(false) + }, []) + + return ( + e.stopPropagation() } }} + > + Payment Issue + + + There is a payment issue with your CIPP subscription. + + + A recent payment has failed. Please contact your account holder to update payment + information and avoid service interruption. + + + + + + + ) +} diff --git a/src/components/CippComponents/SubscriptionEndedDialog.jsx b/src/components/CippComponents/SubscriptionEndedDialog.jsx new file mode 100644 index 000000000000..e715cce54d4e --- /dev/null +++ b/src/components/CippComponents/SubscriptionEndedDialog.jsx @@ -0,0 +1,25 @@ +import { Alert, Dialog, DialogContent, DialogTitle, Typography } from '@mui/material' + +export const SubscriptionEndedDialog = ({ hostedSubscriptionEnded }) => { + const open = !!hostedSubscriptionEnded + + return ( + e.stopPropagation() } }} + > + Subscription Ended + + + Your CIPP subscription has ended. Access to this instance is no longer available. + + + Please contact your account holder to renew the subscription and restore access. + + + + ) +} diff --git a/src/layouts/index.js b/src/layouts/index.js index f69628a3c706..07f2f152a562 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -29,6 +29,8 @@ import { nativeMenuItems } from './config' import { CippBreadcrumbNav } from '../components/CippComponents/CippBreadcrumbNav' import { SsoMigrationDialog } from '../components/CippComponents/SsoMigrationDialog' import { ForcedSsoMigrationDialog } from '../components/CippComponents/ForcedSsoMigrationDialog' +import { SubscriptionEndedDialog } from '../components/CippComponents/SubscriptionEndedDialog' +import { FailedPaymentDialog } from '../components/CippComponents/FailedPaymentDialog' const OnboardingWizardPage = dynamic( () => import('../components/CippWizard/OnboardingWizardPage.jsx'), @@ -337,6 +339,8 @@ export const Layout = (props) => { + + {!setupCompleted && ( From 04c63849f689bbd2686fb473cbaef1462a1d1b99 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 22:41:10 +0200 Subject: [PATCH 028/133] implement standards template deployment for intune apps --- src/data/standards.json | 361 ++++++++++------------------------------ 1 file changed, 84 insertions(+), 277 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 7748d7dee0c7..266bb8af19cd 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -126,16 +126,8 @@ { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": [ - "CIS M365 6.0.1 (3.1.1)", - "mip_search_auditlog", - "NIST CSF 2.0 (DE.CM-09)" - ], - "appliesToTest": [ - "CISAMSEXO171", - "CISAMSEXO173", - "CIS_3_1_1" - ], + "tag": ["CIS M365 6.0.1 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CISAMSEXO171", "CISAMSEXO173", "CIS_3_1_1"], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", "addedComponent": [], @@ -368,11 +360,7 @@ "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], - "appliesToTest": [ - "CISAMSEXO51", - "CIS_6_5_4", - "ZTNA21799" - ], + "appliesToTest": ["CISAMSEXO51", "CIS_6_5_4", "ZTNA21799"], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", "executiveText": "Disables outdated email authentication methods that are vulnerable to security attacks, forcing applications and devices to use modern, more secure authentication protocols. This reduces the risk of email-based security breaches and credential theft.", @@ -394,17 +382,8 @@ { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": [ - "CIS M365 6.0.1 (1.3.2)", - "spo_idle_session_timeout", - "NIST CSF 2.0 (PR.AA-03)" - ], - "appliesToTest": [ - "CIS_1_3_2", - "ZTNA21813", - "ZTNA21814", - "ZTNA21815" - ], + "tag": ["CIS M365 6.0.1 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], + "appliesToTest": ["CIS_1_3_2", "ZTNA21813", "ZTNA21814", "ZTNA21815"], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", "addedComponent": [ @@ -490,10 +469,7 @@ "name": "standards.AdminSSPR", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AP01"], - "appliesToTest": [ - "EIDSCAAP01", - "ZTNA21842" - ], + "appliesToTest": ["EIDSCAAP01", "ZTNA21842"], "helpText": "Controls whether administrators are allowed to use Self-Service Password Reset through the Microsoft Entra authorization policy.", "docsDescription": "Configures the allowedToUseSSPR property on the Microsoft Entra authorization policy. Microsoft documents this property as controlling whether administrators of the tenant can use Self-Service Password Reset. Use this standard to explicitly enable or disable administrator SSPR based on your security policy.", "executiveText": "Controls whether tenant administrators can reset their own passwords through Self-Service Password Reset. Disabling this capability forces privileged accounts through more controlled recovery processes and reduces the risk of self-service recovery being misused on administrative identities.", @@ -593,13 +569,7 @@ "name": "standards.laps", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.5)", "SMB1001 (2.2)"], - "appliesToTest": [ - "CIS_5_1_4_5", - "SMB1001_2_2", - "ZTNA21953", - "ZTNA21955", - "ZTNA24560" - ], + "appliesToTest": ["CIS_5_1_4_5", "SMB1001_2_2", "ZTNA21953", "ZTNA21955", "ZTNA24560"], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", "executiveText": "Enables Local Administrator Password Solution (LAPS) capability, which automatically manages and rotates local administrator passwords on company computers. This significantly improves security by preventing the use of shared or static administrator passwords that could be exploited by attackers.", @@ -733,11 +703,7 @@ "name": "standards.EnableHardwareOAuth", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], - "appliesToTest": [ - "SMB1001_2_5", - "SMB1001_2_6", - "SMB1001_2_9" - ], + "appliesToTest": ["SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_9"], "helpText": "Enables the HardwareOath authenticationMethod for the tenant. This allows you to use hardware tokens for generating 6 digit MFA codes.", "docsDescription": "Enables Hardware OAuth tokens for the tenant. This allows users to use hardware tokens like a Yubikey for authentication.", "executiveText": "Enables physical hardware tokens that generate secure authentication codes, providing an alternative to smartphone-based authentication. This is particularly valuable for employees who cannot use mobile devices or require the highest security standards for accessing sensitive systems.", @@ -753,10 +719,7 @@ "name": "standards.allowOAuthTokens", "cat": "Entra (AAD) Standards", "tag": ["EIDSCA.AT01", "EIDSCA.AT02"], - "appliesToTest": [ - "EIDSCAAT01", - "EIDSCAAT02" - ], + "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02"], "helpText": "Allows you to use any software OAuth token generator", "docsDescription": "Enables OTP Software OAuth tokens for the tenant. This allows users to use OTP codes generated via software, like a password manager to be used as an authentication method.", "executiveText": "Allows employees to use third-party authentication apps and password managers to generate secure login codes, providing flexibility in authentication methods while maintaining security standards. This accommodates diverse user preferences and existing security tools.", @@ -788,12 +751,7 @@ "name": "standards.TAP", "cat": "Entra (AAD) Standards", "tag": [], - "appliesToTest": [ - "EIDSCAAT01", - "EIDSCAAT02", - "ZTNA21845", - "ZTNA21846" - ], + "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02", "ZTNA21845", "ZTNA21846"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", "docsDescription": "Enables Temporary Password generation for the tenant.", "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", @@ -836,10 +794,7 @@ { "name": "standards.CustomBannedPasswordList", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS M365 6.0.1 (5.2.3.2)", - "SMB1001 (2.1)" - ], + "tag": ["CIS M365 6.0.1 (5.2.3.2)", "SMB1001 (2.1)"], "appliesToTest": [ "CIS_5_2_3_2", "EIDSCAPR01", @@ -875,10 +830,7 @@ "name": "standards.ExternalMFATrusted", "cat": "Entra (AAD) Standards", "tag": [], - "appliesToTest": [ - "ZTNA21803", - "ZTNA21804" - ], + "appliesToTest": ["ZTNA21803", "ZTNA21804"], "helpText": "Sets the state of the Cross-tenant access setting to trust external MFA. This allows guest users to use their home tenant MFA to access your tenant.", "executiveText": "Allows external partners and vendors to use their own organization's multi-factor authentication when accessing company resources, streamlining collaboration while maintaining security standards. This reduces friction for external users while ensuring they still meet authentication requirements.", "addedComponent": [ @@ -904,17 +856,8 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": [ - "CIS M365 6.0.1 (5.1.2.3)", - "CISA (MS.AAD.6.1v1)", - "SMB1001 (2.8)" - ], - "appliesToTest": [ - "CIS_5_1_2_3", - "SMB1001_2_8", - "ZTNA21772", - "ZTNA21787" - ], + "tag": ["CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_2_3", "SMB1001_2_8", "ZTNA21772", "ZTNA21787"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", "executiveText": "Prevents regular employees from creating new Microsoft 365 organizations, ensuring all new tenants are properly managed and controlled by IT administrators. This prevents unauthorized shadow IT environments and maintains centralized governance over Microsoft 365 resources.", @@ -971,10 +914,7 @@ "name": "standards.NudgeMFA", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.5)"], - "appliesToTest": [ - "SMB1001_2_5", - "ZTNA21889" - ], + "appliesToTest": ["SMB1001_2_5", "ZTNA21889"], "helpText": "Sets the state of the registration campaign for the tenant", "docsDescription": "Sets the state of the registration campaign for the tenant. If enabled nudges users to set up the Microsoft Authenticator during sign-in.", "executiveText": "Prompts employees to set up multi-factor authentication during login, gradually improving the organization's security posture by encouraging adoption of stronger authentication methods. This helps achieve better security compliance without forcing immediate mandatory changes.", @@ -1012,10 +952,7 @@ "name": "standards.DisableM365GroupUsers", "cat": "Entra (AAD) Standards", "tag": ["CISA (MS.AAD.21.1v1)", "SMB1001 (2.8)"], - "appliesToTest": [ - "SMB1001_2_8", - "ZTNA21868" - ], + "appliesToTest": ["SMB1001_2_8", "ZTNA21868"], "helpText": "Restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "docsDescription": "Users by default are allowed to create M365 groups. This restricts M365 group creation to certain admin roles. This disables the ability to create Teams, SharePoint sites, Planner, etc", "executiveText": "Restricts the creation of Microsoft 365 groups, Teams, and SharePoint sites to authorized administrators, preventing uncontrolled proliferation of collaboration spaces. This ensures proper governance, naming conventions, and resource management while maintaining oversight of all collaborative environments.", @@ -1046,11 +983,7 @@ "NIST CSF 2.0 (PR.AA-05)", "SMB1001 (2.8)" ], - "appliesToTest": [ - "CIS_5_1_2_2", - "EIDSCAAP10", - "SMB1001_2_8" - ], + "appliesToTest": ["CIS_5_1_2_2", "EIDSCAAP10", "SMB1001_2_8"], "helpText": "Disables the ability for users to create App registrations in the tenant.", "docsDescription": "Disables the ability for users to create applications in Entra. Done to prevent breached accounts from creating an app to maintain access to the tenant, even after the breached account has been secured.", "executiveText": "Prevents regular employees from creating application registrations that could be used to maintain unauthorized access to company systems. This security measure ensures that only authorized IT personnel can create applications, reducing the risk of persistent security breaches through malicious applications.", @@ -1066,10 +999,7 @@ "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.6)"], - "appliesToTest": [ - "CIS_5_1_4_6", - "ZTNA21954" - ], + "appliesToTest": ["CIS_5_1_4_6", "ZTNA21954"], "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", "executiveText": "Gives administrators centralized control over BitLocker recovery secrets—restrict access to ensure IT-assisted recovery flows, or allow self-service when rapid device unlocks are a priority.", @@ -1102,11 +1032,7 @@ "NIST CSF 2.0 (PR.AA-05)", "SMB1001 (2.8)" ], - "appliesToTest": [ - "CIS_5_1_3_2", - "SMB1001_2_8", - "ZTNA21868" - ], + "appliesToTest": ["CIS_5_1_3_2", "SMB1001_2_8", "ZTNA21868"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], @@ -1135,11 +1061,7 @@ "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (1.3.4)", "SMB1001 (2.8)"], - "appliesToTest": [ - "CIS_1_3_4", - "EIDSCAAP05", - "SMB1001_2_8" - ], + "appliesToTest": ["CIS_1_3_4", "EIDSCAAP05", "SMB1001_2_8"], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", "addedComponent": [ @@ -1166,10 +1088,7 @@ "name": "standards.DisableGuests", "cat": "Entra (AAD) Standards", "tag": ["SMB1001 (2.8)"], - "appliesToTest": [ - "SMB1001_2_8", - "ZTNA21858" - ], + "appliesToTest": ["SMB1001_2_8", "ZTNA21858"], "helpText": "Blocks login for guest users that have not logged in for a number of days", "executiveText": "Automatically disables external guest accounts that haven't been used for a number of days, reducing security risks from dormant accounts while maintaining access for active external collaborators. This helps maintain a clean user directory and reduces potential attack vectors.", "addedComponent": [ @@ -1247,19 +1166,8 @@ { "name": "standards.GuestInvite", "cat": "Entra (AAD) Standards", - "tag": [ - "CISA (MS.AAD.18.1v1)", - "EIDSCA.AP04", - "EIDSCA.AP07", - "SMB1001 (2.8)" - ], - "appliesToTest": [ - "CIS_5_1_6_3", - "EIDSCAAP04", - "EIDSCAAP07", - "SMB1001_2_8", - "ZTNA21791" - ], + "tag": ["CISA (MS.AAD.18.1v1)", "EIDSCA.AP04", "EIDSCA.AP07", "SMB1001 (2.8)"], + "appliesToTest": ["CIS_5_1_6_3", "EIDSCAAP04", "EIDSCAAP07", "SMB1001_2_8", "ZTNA21791"], "helpText": "This setting controls who can invite guests to your directory to collaborate on resources secured by your company, such as SharePoint sites or Azure resources.", "executiveText": "Controls who within the organization can invite external partners and vendors to access company resources, ensuring proper oversight of external access while enabling necessary business collaboration. This helps maintain security while supporting partnership and vendor relationships.", "addedComponent": [ @@ -1332,12 +1240,7 @@ "name": "standards.SecurityDefaults", "cat": "Entra (AAD) Standards", "tag": ["CISA (MS.AAD.11.1v1)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.9)"], - "appliesToTest": [ - "SMB1001_2_5", - "SMB1001_2_6", - "SMB1001_2_9", - "ZTNA21843" - ], + "appliesToTest": ["SMB1001_2_5", "SMB1001_2_6", "SMB1001_2_9", "ZTNA21843"], "helpText": "Enables security defaults for the tenant, for newer tenants this is enabled by default. Do not enable this feature if you use Conditional Access.", "docsDescription": "Enables SD for the tenant, which disables all forms of basic authentication and enforces users to configure MFA. Users are only prompted for MFA when a logon is considered 'suspect' by Microsoft.", "executiveText": "Activates Microsoft's baseline security configuration that requires multi-factor authentication and blocks legacy authentication methods. This provides essential security protection for organizations without complex conditional access policies, significantly improving security posture with minimal configuration.", @@ -1419,13 +1322,7 @@ "SMB1001 (2.6)", "SMB1001 (2.9)" ], - "appliesToTest": [ - "CIS_5_2_3_7", - "SMB1001_2_5", - "SMB1001_2_5_L4", - "SMB1001_2_6", - "SMB1001_2_9" - ], + "appliesToTest": ["CIS_5_2_3_7", "SMB1001_2_5", "SMB1001_2_5_L4", "SMB1001_2_6", "SMB1001_2_9"], "helpText": "This blocks users from using email as an MFA method. This disables the email OTP option for guest users, and instead prompts them to create a Microsoft account.", "executiveText": "Disables email-based authentication codes due to security concerns with email interception and account compromise. This forces users to adopt more secure authentication methods, particularly affecting guest users who must use stronger verification methods.", "addedComponent": [], @@ -1856,12 +1753,7 @@ "name": "standards.SpoofWarn", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (6.2.3)"], - "appliesToTest": [ - "CISAMSEXO71", - "CIS_6_2_3", - "ORCA111", - "ORCA240" - ], + "appliesToTest": ["CISAMSEXO71", "CIS_6_2_3", "ORCA111", "ORCA240"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", "executiveText": "Displays visual warnings in Outlook when emails come from external senders, helping employees identify potentially suspicious messages and reducing the risk of phishing attacks. This security feature makes it easier for staff to distinguish between internal and external communications.", @@ -1981,10 +1873,7 @@ "name": "standards.RotateDKIM", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], - "appliesToTest": [ - "CIS_2_1_9", - "SMB1001_2_12" - ], + "appliesToTest": ["CIS_2_1_9", "SMB1001_2_12"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", "addedComponent": [], @@ -2038,13 +1927,7 @@ "name": "standards.AddDKIM", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], - "appliesToTest": [ - "CISAMSEXO31", - "CIS_2_1_9", - "ORCA108", - "ORCA108_1", - "SMB1001_2_12" - ], + "appliesToTest": ["CISAMSEXO31", "CIS_2_1_9", "ORCA108", "ORCA108_1", "SMB1001_2_12"], "helpText": "Enables DKIM for all domains that currently support it", "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", "addedComponent": [], @@ -2066,10 +1949,7 @@ "name": "standards.AddDMARCToMOERA", "cat": "Global Standards", "tag": ["CIS M365 6.0.1 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], - "appliesToTest": [ - "CIS_2_1_10", - "SMB1001_2_12" - ], + "appliesToTest": ["CIS_2_1_10", "SMB1001_2_12"], "helpText": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "docsDescription": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "executiveText": "Implements advanced email security for Microsoft's default domain names (onmicrosoft.com) to prevent criminals from impersonating your organization. This blocks fraudulent emails that could damage your company's reputation and protects partners and customers from phishing attacks using your domain names.", @@ -2107,12 +1987,7 @@ "Essential 8 (1683)", "NIST CSF 2.0 (DE.CM-09)" ], - "appliesToTest": [ - "CISAMSEXO131", - "CIS_6_1_1", - "CIS_6_1_2", - "CIS_6_1_3" - ], + "appliesToTest": ["CISAMSEXO131", "CIS_6_1_1", "CIS_6_1_2", "CIS_6_1_3"], "helpText": "Enables Mailbox auditing for all mailboxes and on tenant level. Disables audit bypass on all mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "docsDescription": "Enables mailbox auditing on tenant level and for all mailboxes. Disables audit bypass on all mailboxes. By default Microsoft does not enable mailbox auditing for Resource Mailboxes, Public Folder Mailboxes and DiscoverySearch Mailboxes. Unified Audit Log needs to be enabled for this standard to function.", "executiveText": "Enables comprehensive logging of all email access and modifications across all employee mailboxes, providing detailed audit trails for security investigations and compliance requirements. This helps detect unauthorized access, data breaches, and supports regulatory compliance efforts.", @@ -2383,11 +2258,7 @@ "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (1.3.3)", "exo_individualsharing"], - "appliesToTest": [ - "CISAMSEXO62", - "CIS_1_3_3", - "ZTNA21803" - ], + "appliesToTest": ["CISAMSEXO62", "CIS_1_3_3", "ZTNA21803"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", "executiveText": "Prevents employees from sharing their calendars with external parties, protecting sensitive meeting information and internal schedules from unauthorized access. This security measure helps maintain confidentiality of business activities while still allowing internal collaboration.", @@ -2426,10 +2297,7 @@ "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", "tag": ["CIS M365 6.0.1 (6.5.3)", "exo_storageproviderrestricted"], - "appliesToTest": [ - "CIS_6_5_3", - "ZTNA21817" - ], + "appliesToTest": ["CIS_6_5_3", "ZTNA21817"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", "executiveText": "Prevents employees from accessing personal cloud storage services like Dropbox or Google Drive through Outlook on the web, reducing data security risks and ensuring company information stays within approved corporate systems. This helps maintain data governance and prevents accidental data leaks.", @@ -2601,10 +2469,7 @@ "NIST CSF 2.0 (PR.AA-05)", "NIST CSF 2.0 (PR.PS-05)" ], - "appliesToTest": [ - "CIS_6_3_1", - "ZTNA21817" - ], + "appliesToTest": ["CIS_6_3_1", "ZTNA21817"], "helpText": "Disables the ability for users to install add-ins in Outlook. This is to prevent users from installing malicious add-ins.", "docsDescription": "Disables users from being able to install add-ins in Outlook. Only admins are able to approve add-ins for the users. This is done to reduce the threat surface for data exfiltration.", "executiveText": "Prevents employees from installing third-party add-ins in Outlook without administrative approval, reducing security risks from potentially malicious extensions. This ensures only vetted and approved tools can access company email data while maintaining centralized control over email functionality.", @@ -2756,10 +2621,7 @@ "NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)" ], - "appliesToTest": [ - "CIS_1_2_2", - "SMB1001_2_3" - ], + "appliesToTest": ["CIS_1_2_2", "SMB1001_2_3"], "helpText": "Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.", "docsDescription": "Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.", "executiveText": "Prevents direct login to shared mailbox accounts (like info@company.com), ensuring they can only be accessed through authorized users' accounts. This security measure eliminates the risk of shared passwords and unauthorized access while maintaining proper access control and audit trails.", @@ -3239,12 +3101,7 @@ "mdo_safeattachmentpolicy", "NIST CSF 2.0 (DE.CM-09)" ], - "appliesToTest": [ - "CIS_2_1_4", - "ORCA158", - "ORCA189", - "ORCA227" - ], + "appliesToTest": ["CIS_2_1_4", "ORCA158", "ORCA189", "ORCA227"], "helpText": "This creates a Safe Attachment policy", "addedComponent": [ { @@ -3341,10 +3198,7 @@ "name": "standards.PhishingSimulations", "cat": "Defender Standards", "tag": ["SMB1001 (1.11)", "SMB1001 (5.1)"], - "appliesToTest": [ - "SMB1001_1_11", - "SMB1001_5_1" - ], + "appliesToTest": ["SMB1001_1_11", "SMB1001_5_1"], "helpText": "This creates a phishing simulation policy that enables phishing simulations for the entire tenant.", "addedComponent": [ { @@ -4394,12 +4248,7 @@ "name": "standards.intuneDeviceReg", "cat": "Intune Standards", "tag": ["CIS M365 6.0.1 (5.1.4.2)", "CISA (MS.AAD.17.1v1)"], - "appliesToTest": [ - "CIS_5_1_4_2", - "ZTNA21801", - "ZTNA21802", - "ZTNA21837" - ], + "appliesToTest": ["CIS_5_1_4_2", "ZTNA21801", "ZTNA21802", "ZTNA21837"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", "addedComponent": [ @@ -4422,11 +4271,7 @@ "name": "standards.intuneDeviceRegLocalAdmins", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.3)", "CIS M365 6.0.1 (5.1.4.4)", "SMB1001 (2.2)"], - "appliesToTest": [ - "CIS_5_1_4_3", - "CIS_5_1_4_4", - "SMB1001_2_2" - ], + "appliesToTest": ["CIS_5_1_4_3", "CIS_5_1_4_4", "SMB1001_2_2"], "helpText": "Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins.", "docsDescription": "Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins.", "executiveText": "Controls whether employees who enroll devices automatically receive local administrator access. Disabling registering-user admin rights follows least-privilege principles and reduces security risk from over-privileged endpoints.", @@ -4478,10 +4323,7 @@ "name": "standards.intuneRestrictUserDeviceJoin", "cat": "Entra (AAD) Standards", "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], - "appliesToTest": [ - "CIS_5_1_4_1", - "SMB1001_2_8" - ], + "appliesToTest": ["CIS_5_1_4_1", "SMB1001_2_8"], "helpText": "Controls whether users can join devices to Entra.", "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", "executiveText": "Controls whether employees can join their devices to the corporate Entra directory. Disabling user device join prevents unauthorized or unmanaged devices from becoming corporate-managed identities, enhancing overall security posture.", @@ -4504,11 +4346,7 @@ "name": "standards.intuneRequireMFA", "cat": "Intune Standards", "tag": [], - "appliesToTest": [ - "ZTNA21782", - "ZTNA21796", - "ZTNA21872" - ], + "appliesToTest": ["ZTNA21782", "ZTNA21796", "ZTNA21872"], "helpText": "Requires MFA for all users to register devices with Intune. This is useful when not using Conditional Access.", "executiveText": "Requires employees to use multi-factor authentication when registering devices for corporate access, adding an extra security layer to prevent unauthorized device enrollment. This helps ensure only legitimate users can connect their devices to company systems.", "label": "Require Multi-factor Authentication to register or join devices with Microsoft Entra", @@ -4656,15 +4494,8 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.3.1)", - "CISA (MS.SPO.3.1v1)", - "NIST CSF 2.0 (DE.CM-09)" - ], - "appliesToTest": [ - "CIS_7_3_1", - "ZTNA21817" - ], + "tag": ["CIS M365 6.0.1 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], + "appliesToTest": ["CIS_7_3_1", "ZTNA21817"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", "addedComponent": [], @@ -4731,12 +4562,7 @@ "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", "tag": ["CIS M365 6.0.1 (7.2.9)", "CISA (MS.SPO.1.5v1)"], - "appliesToTest": [ - "CIS_7_2_9", - "ZTNA21803", - "ZTNA21804", - "ZTNA21858" - ], + "appliesToTest": ["CIS_7_2_9", "ZTNA21803", "ZTNA21804", "ZTNA21858"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", "addedComponent": [ @@ -4770,11 +4596,7 @@ "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", "tag": ["CIS M365 6.0.1 (7.2.10)", "CISA (MS.SPO.1.6v1)"], - "appliesToTest": [ - "CIS_7_2_10", - "ZTNA21803", - "ZTNA21804" - ], + "appliesToTest": ["CIS_7_2_10", "ZTNA21803", "ZTNA21804"], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", "addedComponent": [ @@ -4807,17 +4629,8 @@ { "name": "standards.DefaultSharingLink", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.2.7)", - "CIS M365 6.0.1 (7.2.11)", - "CISA (MS.SPO.1.4v1)" - ], - "appliesToTest": [ - "CIS_7_2_11", - "CIS_7_2_7", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 6.0.1 (7.2.7)", "CIS M365 6.0.1 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "appliesToTest": ["CIS_7_2_11", "CIS_7_2_7", "ZTNA21803", "ZTNA21804"], "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", "executiveText": "Configures SharePoint default sharing links to implement the principle of least privilege for document sharing. This security measure reduces the risk of accidental data modification while maintaining collaboration functionality, requiring users to explicitly select Edit permissions when necessary. The sharing type setting controls whether links are restricted to specific recipients or available to the entire organization. This reduces the risk of accidental data exposure through link sharing.", @@ -4927,11 +4740,7 @@ "CISA (MS.AAD.3.1v1)", "NIST CSF 2.0 (PR.IR-01)" ], - "appliesToTest": [ - "CIS_7_2_1", - "ZTNA21776", - "ZTNA21797" - ], + "appliesToTest": ["CIS_7_2_1", "ZTNA21776", "ZTNA21797"], "helpText": "Disables the ability to authenticate with SharePoint using legacy authentication methods. Any applications that use legacy authentication will need to be updated to use modern authentication.", "docsDescription": "Disables the ability for users and applications to access SharePoint via legacy basic authentication. This will likely not have any user impact, but will block systems/applications depending on basic auth or the SharePointOnlineCredentials class.", "executiveText": "Disables outdated authentication methods for SharePoint access, forcing applications and users to use modern, more secure authentication protocols. This significantly improves security by eliminating vulnerable authentication pathways while requiring updates to older applications.", @@ -4960,12 +4769,7 @@ "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)" ], - "appliesToTest": [ - "CIS_7_2_3", - "CIS_7_2_4", - "ZTNA21803", - "ZTNA21804" - ], + "appliesToTest": ["CIS_7_2_3", "CIS_7_2_4", "ZTNA21803", "ZTNA21804"], "helpText": "Sets the default sharing level for OneDrive and SharePoint. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Defines the organization's default policy for sharing files and folders in SharePoint and OneDrive, balancing collaboration needs with security requirements. This fundamental setting determines whether employees can share with external users, anonymous links, or only internal colleagues.", "addedComponent": [ @@ -5012,16 +4816,8 @@ { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.2.5)", - "CISA (MS.AAD.14.2v1)", - "CISA (MS.SPO.1.2v1)" - ], - "appliesToTest": [ - "CIS_7_2_5", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 6.0.1 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], + "appliesToTest": ["CIS_7_2_5", "ZTNA21803", "ZTNA21804"], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", "executiveText": "Prevents external users from sharing company documents with additional people, maintaining control over document distribution and preventing unauthorized access expansion. This security measure ensures that external sharing remains within intended boundaries set by internal employees.", @@ -5118,15 +4914,8 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.3.2)", - "CISA (MS.SPO.2.1v1)", - "NIST CSF 2.0 (PR.AA-05)" - ], - "appliesToTest": [ - "CIS_7_3_2", - "ZTNA24824" - ], + "tag": ["CIS M365 6.0.1 (7.3.2)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "appliesToTest": ["CIS_7_3_2", "ZTNA24824"], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -5155,16 +4944,8 @@ { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": [ - "CIS M365 6.0.1 (7.2.6)", - "CISA (MS.AAD.14.3v1)", - "CISA (MS.SPO.1.3v1)" - ], - "appliesToTest": [ - "CIS_7_2_6", - "ZTNA21803", - "ZTNA21804" - ], + "tag": ["CIS M365 6.0.1 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], + "appliesToTest": ["CIS_7_2_6", "ZTNA21803", "ZTNA21804"], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", "addedComponent": [ @@ -5523,10 +5304,7 @@ "name": "standards.TeamsExternalAccessPolicy", "cat": "Teams Standards", "tag": ["CIS M365 6.0.1 (8.2.1)", "CIS M365 6.0.1 (8.2.2)"], - "appliesToTest": [ - "CIS_8_2_1", - "CIS_8_2_2" - ], + "appliesToTest": ["CIS_8_2_1", "CIS_8_2_2"], "helpText": "Sets the properties of the Global external access policy.", "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", "executiveText": "Defines the organization's policy for communicating with external users through Teams, including other organizations, Skype users, and unmanaged accounts. This fundamental setting determines the scope of external collaboration while maintaining security boundaries for business communications.", @@ -7289,5 +7067,34 @@ "addedDate": "2026-05-06", "powershellEquivalent": "Graph API PATCH https://graph.microsoft.com/beta/policies/b2bManagementPolicies/default", "recommendedBy": ["CIS"] + }, + { + "name": "standards.IntuneAppTemplateDeploy", + "cat": "Intune Standards", + "tag": [], + "helpText": "Deploys selected Intune application templates to the tenant. Supports WinGet/Store apps, Office apps, Chocolatey apps, Win32 script apps, and MSP apps.", + "docsDescription": "Uses CIPP Intune Application Templates to deploy applications across tenants as a standard. Each template can contain multiple applications of different types which will be queued for deployment.", + "executiveText": "Automatically deploys approved Intune applications across all managed tenants, ensuring consistent software availability and reducing manual deployment overhead. Supports WinGet, Office, Chocolatey, Win32, and MSP application types.", + "addedComponent": [ + { + "type": "autoComplete", + "multiple": true, + "creatable": false, + "label": "Select Application Templates", + "name": "standards.IntuneAppTemplateDeploy.templateIds", + "api": { + "url": "/api/ListAppTemplates", + "labelField": "displayName", + "valueField": "GUID", + "queryKey": "StdIntuneAppTemplateList" + } + } + ], + "label": "Deploy Intune Application Template", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-23", + "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", + "recommendedBy": [] } ] From 28cafc931d46c705b3481a1e5bd185b39ca8879d Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 23:06:16 +0200 Subject: [PATCH 029/133] added third party notice --- src/pages/cipp/integrations/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index 6d3f24f86ee4..ceb37a1cd0cb 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -141,7 +141,7 @@ const Page = () => { height: 8, }} /> - Coming Soon + Coming Soon through third-Party ) : ( <> From 30455f273d7cfcba5add1012bc260bb789f7c405 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 23:06:35 +0200 Subject: [PATCH 030/133] third party --- src/pages/cipp/integrations/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index ceb37a1cd0cb..cc3067675bab 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -141,7 +141,7 @@ const Page = () => { height: 8, }} /> - Coming Soon through third-Party + Coming Soon through Third-Party ) : ( <> From d4f458a15b67d7c8cdd8b8095d3a638a040a9dd4 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sun, 24 May 2026 23:07:33 +0200 Subject: [PATCH 031/133] Third party text --- src/pages/cipp/integrations/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/cipp/integrations/index.js b/src/pages/cipp/integrations/index.js index cc3067675bab..a60530323c46 100644 --- a/src/pages/cipp/integrations/index.js +++ b/src/pages/cipp/integrations/index.js @@ -141,7 +141,7 @@ const Page = () => { height: 8, }} /> - Coming Soon through Third-Party + Coming Soon Through Third-Party ) : ( <> From ee0ab2abe3341bd2f0270391f36e781f6f4ef8d2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 00:13:35 +0200 Subject: [PATCH 032/133] add extendedValues --- src/components/CippFormPages/CippAddEditUser.jsx | 9 +++++++++ src/pages/tenant/manage/user-defaults.js | 10 ++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/components/CippFormPages/CippAddEditUser.jsx b/src/components/CippFormPages/CippAddEditUser.jsx index 1e4ac2353fd8..ad578556a517 100644 --- a/src/components/CippFormPages/CippAddEditUser.jsx +++ b/src/components/CippFormPages/CippAddEditUser.jsx @@ -351,6 +351,15 @@ const CippAddEditUser = (props) => { } } } + + // Populate custom user attributes from template + if (template.defaultAttributes) { + Object.entries(template.defaultAttributes).forEach(([key, attr]) => { + if (attr?.Value) { + setFieldIfEmpty(`defaultAttributes.${key}.Value`, attr.Value) + } + }) + } } }, [watchedFields.userTemplate, formType]) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 25fd4b63362d..7f512cae7763 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -194,6 +194,13 @@ const Page = () => { name: 'businessPhones[0]', type: 'textField', }, + ...(userSettings?.userAttributes + ?.filter((attribute) => attribute.value !== 'sponsor') + .map((attribute) => ({ + label: attribute.label, + name: `defaultAttributes.${attribute.label}.Value`, + type: 'textField', + })) || []), ] const actions = [ @@ -241,6 +248,9 @@ const Page = () => { 'department', 'mobilePhone', 'businessPhones', + ...(userSettings?.userAttributes + ?.filter((attribute) => attribute.value !== 'sponsor') + .map((attribute) => `defaultAttributes.${attribute.label}.Value`) || []), ], actions: actions, } From 17bf1f8dbbd6fe16a63fa268c60dfa8fe8a0c053 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 00:14:08 +0200 Subject: [PATCH 033/133] fixes #5995 --- src/pages/tenant/manage/user-defaults.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 7f512cae7763..8eb4f2592f61 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -91,7 +91,8 @@ const Page = () => { labelField: 'id', valueField: 'id', queryKey: `ListGraphRequest-domains-${userSettings.currentTenant}`, - dataFilter: (options) => options.filter((option) => option?.addedFields?.isVerified === true), // Only include verified domains + dataFilter: (options) => + options.filter((option) => option?.addedFields?.isVerified === true), // Only include verified domains }, multiple: false, creatable: false, From 8097e6ede3991b61e65b03bd21496dc55c96bc33 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 01:34:03 +0200 Subject: [PATCH 034/133] FIDO2 profile standards --- src/data/standards.json | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 266bb8af19cd..0322daffd1a3 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7096,5 +7096,64 @@ "addedDate": "2026-05-23", "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", "recommendedBy": [] + }, + { + "name": "standards.FIDO2PasskeyProfiles", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures FIDO2 passkey profiles including AAGUID allowlists, attestation enforcement, and passkey types for the tenant.", + "docsDescription": "Manages FIDO2 passkey profiles on the tenant authentication methods policy. Allows defining passkey profiles that control which authenticators (hardware keys, password managers, Microsoft Authenticator) are permitted via AAGUID allowlists, whether attestation is enforced, and which passkey types (device-bound, synced, or both) are allowed. This enables MSPs to centrally deploy phishing-resistant MFA configurations across tenants.", + "executiveText": "Configures passkey (FIDO2) profiles that control which authenticators users can register for phishing-resistant MFA. Supports allowlisting specific hardware keys (e.g., YubiKey models), password managers (e.g., 1Password), and Microsoft Authenticator by AAGUID, with control over attestation enforcement and passkey types.", + "addedComponent": [ + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.PasskeyTypes", + "label": "Allowed Passkey Types", + "options": [ + { "label": "Device-bound only", "value": "deviceBound" }, + { "label": "Synced only", "value": "synced" }, + { "label": "Both device-bound and synced", "value": "deviceBound,synced" } + ], + "required": true + }, + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.AttestationEnforcement", + "label": "Attestation Enforcement", + "options": [ + { "label": "Disabled (required for synced passkeys)", "value": "disabled" }, + { "label": "Registration only", "value": "registrationOnly" } + ], + "required": true + }, + { + "type": "switch", + "name": "standards.FIDO2PasskeyProfiles.EnforceKeyRestrictions", + "label": "Enforce AAGUID Key Restrictions" + }, + { + "type": "select", + "multiple": false, + "name": "standards.FIDO2PasskeyProfiles.EnforcementType", + "label": "Key Restriction Type", + "options": [ + { "label": "Allow listed AAGUIDs only", "value": "allow" }, + { "label": "Block listed AAGUIDs", "value": "block" } + ] + }, + { + "type": "textField", + "name": "standards.FIDO2PasskeyProfiles.AAGUIDs", + "label": "AAGUIDs (comma-separated list of authenticator AAGUIDs)" + } + ], + "label": "Configure FIDO2 Passkey Profile", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-25", + "powershellEquivalent": "Graph API PATCH /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2", + "recommendedBy": ["CIPP"] } ] From 389babe3e5641be1363afed31b78eddd1a0e3949 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 01:58:14 +0200 Subject: [PATCH 035/133] add global var showing --- .../CippComponents/CippCustomVariables.jsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/CippComponents/CippCustomVariables.jsx b/src/components/CippComponents/CippCustomVariables.jsx index 69b5975d1777..d53b789428ae 100644 --- a/src/components/CippComponents/CippCustomVariables.jsx +++ b/src/components/CippComponents/CippCustomVariables.jsx @@ -60,6 +60,7 @@ const CippCustomVariables = ({ id }) => { confirmText: "Update the custom variable '[RowKey]'?", hideBulk: true, setDefaultValues: true, + condition: (row) => row.Scope !== "Global" || id === "AllTenants", fields: [ { type: "textField", @@ -74,7 +75,6 @@ const CippCustomVariables = ({ id }) => { type: "textField", name: "Value", label: "Value", - disableVariables: true, placeholder: "Enter the value for the custom variable.", required: true, }, @@ -99,6 +99,7 @@ const CippCustomVariables = ({ id }) => { label: "Delete", icon: , confirmText: "Are you sure you want to delete [RowKey]?", + condition: (row) => row.Scope !== "Global" || id === "AllTenants", type: "POST", url: "/api/ExecCippReplacemap", data: { @@ -127,10 +128,17 @@ const CippCustomVariables = ({ id }) => { title={id === "AllTenants" ? "Global Variables" : "Custom Variables"} actions={actions} api={{ - url: `/api/ExecCippReplacemap?Action=List&tenantId=${id}`, + url: + id === "AllTenants" + ? `/api/ExecCippReplacemap?Action=List&tenantId=${id}` + : `/api/ExecCippReplacemap?Action=List&tenantId=${id}&includeGlobal=true`, dataKey: "Results", }} - simpleColumns={["RowKey", "Value", "Description"]} + simpleColumns={ + id === "AllTenants" + ? ["RowKey", "Value", "Description"] + : ["RowKey", "Value", "Description", "Scope"] + } cardButton={ - diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index 444ee7dcd4ac..dd45430feee4 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -65,7 +65,7 @@ export const AccountPopover = (props) => { // delete query cache and persisted data queryClient.clear(); - router.push("/.auth/logout?post_logout_redirect_uri=" + encodeURIComponent(paths.index)); + router.push("/.auth/logout?prompt=select_account&post_logout_redirect_uri=" + encodeURIComponent(paths.index)); } catch (err) { console.error(err); console.log(orgData); diff --git a/src/layouts/index.js b/src/layouts/index.js index 07f2f152a562..f3c178556ff3 100644 --- a/src/layouts/index.js +++ b/src/layouts/index.js @@ -341,7 +341,7 @@ export const Layout = (props) => { - + {!setupCompleted && ( From 16b4503f014b4d5b99ff0e49485dcc97544e3cee Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 25 May 2026 11:29:56 +0800 Subject: [PATCH 039/133] login/out testing --- src/layouts/account-popover.js | 2 +- staticwebapp.config.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/layouts/account-popover.js b/src/layouts/account-popover.js index dd45430feee4..444ee7dcd4ac 100644 --- a/src/layouts/account-popover.js +++ b/src/layouts/account-popover.js @@ -65,7 +65,7 @@ export const AccountPopover = (props) => { // delete query cache and persisted data queryClient.clear(); - router.push("/.auth/logout?prompt=select_account&post_logout_redirect_uri=" + encodeURIComponent(paths.index)); + router.push("/.auth/logout?post_logout_redirect_uri=" + encodeURIComponent(paths.index)); } catch (err) { console.error(err); console.log(orgData); diff --git a/staticwebapp.config.json b/staticwebapp.config.json index 1f57342751ca..4c6b5d68b840 100644 --- a/staticwebapp.config.json +++ b/staticwebapp.config.json @@ -20,7 +20,7 @@ }, { "route": "/login", - "rewrite": "/.auth/login/aad" + "redirect": "/.auth/login/aad?prompt=select_account" }, { "route": "/.auth/login/twitter", @@ -70,7 +70,7 @@ }, "responseOverrides": { "401": { - "redirect": "/.auth/login/aad?post_login_redirect_uri=.referrer", + "redirect": "/.auth/login/aad?prompt=select_account&post_login_redirect_uri=.referrer", "statusCode": 302, "exclude": ["/assets/illustrations/*.{png,jpg,gif}", "/css/*"] }, From c1c5693c09ef99c196b80d17b4b8aeb61f1d9852 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Mon, 25 May 2026 11:49:27 +0200 Subject: [PATCH 040/133] feat: add admin role member removal functionality --- .../identity/administration/roles/index.js | 77 ++- .../administration/users/user/index.jsx | 588 ++++++++++-------- 2 files changed, 379 insertions(+), 286 deletions(-) diff --git a/src/pages/identity/administration/roles/index.js b/src/pages/identity/administration/roles/index.js index f09fb8a01388..27b95f7ed9c9 100644 --- a/src/pages/identity/administration/roles/index.js +++ b/src/pages/identity/administration/roles/index.js @@ -1,24 +1,71 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import CippFormComponent from '../../../../components/CippComponents/CippFormComponent' +import { usePermissions } from '../../../../hooks/use-permissions' +import { PersonRemove } from '@mui/icons-material' + +const RemoveRoleMembersForm = ({ formHook, row }) => { + const memberOptions = (row?.Members ?? []).map((member) => ({ + label: member.userPrincipalName + ? `${member.displayName} (${member.userPrincipalName})` + : member.displayName, + value: member.id, + addedFields: { + displayName: member.displayName, + userPrincipalName: member.userPrincipalName, + }, + })) + + return ( + + ) +} const Page = () => { - const pageTitle = "Roles"; + const pageTitle = 'Roles' + const { checkPermissions } = usePermissions() + const canWriteRole = checkPermissions(['Identity.Role.ReadWrite']) - const actions = []; + const actions = [ + { + label: 'Remove Members', + type: 'POST', + icon: , + url: '/api/ExecRemoveAdminRole', + children: ({ formHook, row }) => , + data: { + RoleId: 'Id', + RoleName: 'DisplayName', + }, + confirmText: 'Select the members to remove from [DisplayName].', + allowResubmit: true, + hideBulk: true, + condition: (row) => canWriteRole && (row?.Members ?? []).length > 0, + }, + ] const offCanvas = { extendedInfoFields: [ - "DisplayName", // Role Group Name - "Members", // Member Names + 'DisplayName', // Role Group Name + 'Members', // Member Names ], actions: actions, - }; + } const columns = [ - "DisplayName", // Role Name - "Description", // Description - "Members", // Members - ]; + 'DisplayName', // Role Name + 'Description', // Description + 'Members', // Members + ] return ( { offCanvas={offCanvas} simpleColumns={columns} /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page diff --git a/src/pages/identity/administration/users/user/index.jsx b/src/pages/identity/administration/users/user/index.jsx index 95adbe3d4fed..cce0e8f472fd 100644 --- a/src/pages/identity/administration/users/user/index.jsx +++ b/src/pages/identity/administration/users/user/index.jsx @@ -1,33 +1,43 @@ -import { Layout as DashboardLayout } from "../../../../../layouts/index.js"; -import { useSettings } from "../../../../../hooks/use-settings"; -import { useRouter } from "next/router"; -import { ApiGetCall, ApiPostCall } from "../../../../../api/ApiCall"; -import CippFormSkeleton from "../../../../../components/CippFormPages/CippFormSkeleton"; -import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { AdminPanelSettings, Check, Group, Mail, Fingerprint, Launch, Devices } from "@mui/icons-material"; -import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; -import tabOptions from "./tabOptions"; -import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; -import { Box, Stack } from "@mui/system"; -import { Grid } from "@mui/system"; -import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard"; -import { SvgIcon, Typography } from "@mui/material"; -import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; -import { CippTimeAgo } from "../../../../../components/CippComponents/CippTimeAgo"; -import { useEffect, useState } from "react"; -import { useCippUserActions } from "../../../../../components/CippComponents/CippUserActions"; -import { EyeIcon, PencilIcon } from "@heroicons/react/24/outline"; -import { CippDataTable } from "../../../../../components/CippTable/CippDataTable"; -import dynamic from "next/dynamic"; -const CippMap = dynamic(() => import("../../../../../components/CippComponents/CippMap"), { +import { Layout as DashboardLayout } from '../../../../../layouts/index.js' +import { useSettings } from '../../../../../hooks/use-settings' +import { useRouter } from 'next/router' +import { ApiGetCall, ApiPostCall } from '../../../../../api/ApiCall' +import CippFormSkeleton from '../../../../../components/CippFormPages/CippFormSkeleton' +import CalendarIcon from '@heroicons/react/24/outline/CalendarIcon' +import { + AdminPanelSettings, + Check, + Group, + Mail, + Fingerprint, + Launch, + Devices, + PersonRemove, +} from '@mui/icons-material' +import { HeaderedTabbedLayout } from '../../../../../layouts/HeaderedTabbedLayout' +import tabOptions from './tabOptions' +import { CippCopyToClipBoard } from '../../../../../components/CippComponents/CippCopyToClipboard' +import { Box, Stack } from '@mui/system' +import { Grid } from '@mui/system' +import { CippUserInfoCard } from '../../../../../components/CippCards/CippUserInfoCard' +import { SvgIcon, Typography } from '@mui/material' +import { CippBannerListCard } from '../../../../../components/CippCards/CippBannerListCard' +import { CippTimeAgo } from '../../../../../components/CippComponents/CippTimeAgo' +import { useEffect, useState } from 'react' +import { useCippUserActions } from '../../../../../components/CippComponents/CippUserActions' +import { EyeIcon, PencilIcon } from '@heroicons/react/24/outline' +import { CippDataTable } from '../../../../../components/CippTable/CippDataTable' +import dynamic from 'next/dynamic' +const CippMap = dynamic(() => import('../../../../../components/CippComponents/CippMap'), { ssr: false, -}); +}) -import { Button, Dialog, DialogTitle, DialogContent, IconButton } from "@mui/material"; -import { Close } from "@mui/icons-material"; -import { CippPropertyList } from "../../../../../components/CippComponents/CippPropertyList"; -import { CippCodeBlock } from "../../../../../components/CippComponents/CippCodeBlock"; -import { CippHead } from "../../../../../components/CippComponents/CippHead"; +import { Button, Dialog, DialogTitle, DialogContent, IconButton } from '@mui/material' +import { Close } from '@mui/icons-material' +import { CippPropertyList } from '../../../../../components/CippComponents/CippPropertyList' +import { CippCodeBlock } from '../../../../../components/CippComponents/CippCodeBlock' +import { CippHead } from '../../../../../components/CippComponents/CippHead' +import { usePermissions } from '../../../../../hooks/use-permissions' const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { return ( @@ -37,7 +47,7 @@ const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { @@ -47,16 +57,16 @@ const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { noCard={true} title="Sign-In Logs" simpleColumns={[ - "createdDateTime", - "status", - "ipAddress", - "clientAppUsed", - "resourceDisplayName", - "status.errorCode", - "location", + 'createdDateTime', + 'status', + 'ipAddress', + 'clientAppUsed', + 'resourceDisplayName', + 'status.errorCode', + 'location', ]} api={{ - url: "/api/ListUserSigninLogs", + url: '/api/ListUserSigninLogs', data: { UserId: userId, tenantFilter: tenantFilter, @@ -67,22 +77,24 @@ const SignInLogsDialog = ({ open, onClose, userId, tenantFilter }) => { /> - ); -}; + ) +} const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { userId } = router.query; - const [waiting, setWaiting] = useState(false); - const [signInLogsDialogOpen, setSignInLogsDialogOpen] = useState(false); - const userActions = useCippUserActions(); + const userSettingsDefaults = useSettings() + const router = useRouter() + const { userId } = router.query + const [waiting, setWaiting] = useState(false) + const [signInLogsDialogOpen, setSignInLogsDialogOpen] = useState(false) + const userActions = useCippUserActions() + const { checkPermissions } = usePermissions() + const canWriteRole = checkPermissions(['Identity.Role.ReadWrite']) useEffect(() => { if (userId) { - setWaiting(true); + setWaiting(true) } - }, [userId]); + }, [userId]) const userRequest = ApiGetCall({ url: `/api/ListUsers?UserId=${userId}&tenantFilter=${ @@ -90,70 +102,75 @@ const Page = () => { }`, queryKey: `ListUsers-${userId}`, waiting: waiting, - }); + }) const userBulkRequest = ApiPostCall({ urlFromData: true, - }); + }) function refreshFunction() { - const userPrincipalName = userRequest.data?.[0]?.userPrincipalName; + const userPrincipalName = userRequest.data?.[0]?.userPrincipalName const requests = [ { - id: "userMemberOf", + id: 'userMemberOf', url: `/users/${userId}/memberOf`, - method: "GET", + method: 'GET', }, { - id: "mfaDevices", + id: 'mfaDevices', url: `/users/${userId}/authentication/methods?$top=99`, - method: "GET", + method: 'GET', }, { - id: "signInLogs", + id: 'signInLogs', url: `/auditLogs/signIns?$filter=(userId eq '${userId}')&$top=1`, - method: "GET", + method: 'GET', }, - ]; + ] // Only add managedDevices request if we have the userPrincipalName if (userPrincipalName) { requests.push({ - id: "managedDevices", + id: 'managedDevices', url: `/deviceManagement/managedDevices?$filter=userPrincipalName eq '${userPrincipalName}'`, - method: "GET", - }); + method: 'GET', + }) } userBulkRequest.mutate({ - url: "/api/ListGraphBulkRequest", + url: '/api/ListGraphBulkRequest', data: { Requests: requests, tenantFilter: userSettingsDefaults.currentTenant, - noPaginateIds: ["signInLogs"], + noPaginateIds: ['signInLogs'], }, - }); + }) } useEffect(() => { - if (userId && userSettingsDefaults.currentTenant && userRequest.isSuccess && !userBulkRequest.isSuccess) { - refreshFunction(); + if ( + userId && + userSettingsDefaults.currentTenant && + userRequest.isSuccess && + !userBulkRequest.isSuccess + ) { + refreshFunction() } - }, [userId, userSettingsDefaults.currentTenant, userRequest.isSuccess, userBulkRequest.isSuccess]); + }, [userId, userSettingsDefaults.currentTenant, userRequest.isSuccess, userBulkRequest.isSuccess]) - const bulkData = userBulkRequest?.data?.data ?? []; - const signInLogsData = bulkData?.find((item) => item.id === "signInLogs"); - const userMemberOfData = bulkData?.find((item) => item.id === "userMemberOf"); - const mfaDevicesData = bulkData?.find((item) => item.id === "mfaDevices"); - const managedDevicesData = bulkData?.find((item) => item.id === "managedDevices"); + const bulkData = userBulkRequest?.data?.data ?? [] + const signInLogsData = bulkData?.find((item) => item.id === 'signInLogs') + const userMemberOfData = bulkData?.find((item) => item.id === 'userMemberOf') + const mfaDevicesData = bulkData?.find((item) => item.id === 'mfaDevices') + const managedDevicesData = bulkData?.find((item) => item.id === 'managedDevices') - const signInLogs = signInLogsData?.body?.value || []; - const userMemberOf = userMemberOfData?.body?.value || []; - const mfaDevices = mfaDevicesData?.body?.value || []; - const managedDevices = managedDevicesData?.body?.value || []; + const signInLogs = signInLogsData?.body?.value || [] + const userMemberOf = userMemberOfData?.body?.value || [] + const mfaDevices = mfaDevicesData?.body?.value || [] + const managedDevices = managedDevicesData?.body?.value || [] // Set the title and subtitle for the layout - const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; + const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : 'Loading...' const subtitle = userRequest.isSuccess ? [ @@ -174,7 +191,7 @@ const Page = () => { ), }, { - icon: , + icon: , text: ( + + + ); +}; + +export default CippTutorialDialog; diff --git a/src/contexts/tutorial-context.js b/src/contexts/tutorial-context.js new file mode 100644 index 000000000000..954b641d8b0b --- /dev/null +++ b/src/contexts/tutorial-context.js @@ -0,0 +1,169 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; +import { driver } from "driver.js"; +import { useRouter } from "next/router"; + +const STORAGE_KEY = "cipp.tutorials.completed"; + +const getCompletedTutorials = () => { + try { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : []; + } catch { + return []; + } +}; + +const storeCompletedTutorial = (id) => { + try { + const completed = getCompletedTutorials(); + if (!completed.includes(id)) { + completed.push(id); + localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)); + } + } catch { + // ignore + } +}; + +const resetCompletedTutorials = () => { + try { + localStorage.removeItem(STORAGE_KEY); + } catch { + // ignore + } +}; + +const TutorialContext = createContext({ + tutorials: [], + activeTutorial: null, + completedIds: [], + startTutorial: () => {}, + resetProgress: () => {}, + getTutorialsForPage: () => [], +}); + +// Load all tutorial JSON files from the data/tutorials folder at build time +const loadTutorials = () => { + const context = require.context("../data/tutorials", false, /\.json$/); + return context.keys().map((key) => { + const tutorial = context(key); + return tutorial.default || tutorial; + }); +}; + +export const TutorialProvider = ({ children }) => { + const [tutorials] = useState(() => loadTutorials()); + const [completedIds, setCompletedIds] = useState([]); + const [activeTutorial, setActiveTutorial] = useState(null); + const driverRef = useRef(null); + const router = useRouter(); + + useEffect(() => { + setCompletedIds(getCompletedTutorials()); + }, []); + + // Launch tutorial from ?tutorial=$id query param + useEffect(() => { + if (!router.isReady || activeTutorial) return; + const tutorialId = router.query.tutorial; + if (!tutorialId) return; + + const match = tutorials.find((t) => t.id === tutorialId); + if (!match) return; + + // Strip the query param so it doesn't re-trigger + const { tutorial: _, ...rest } = router.query; + router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }); + + // Delay to let the page fully render + setTimeout(() => runDriver(match), 600); + }, [router.isReady, router.query.tutorial, tutorials]); + + // Cleanup driver on unmount or route change + useEffect(() => { + return () => { + if (driverRef.current) { + driverRef.current.destroy(); + driverRef.current = null; + } + }; + }, []); + + const startTutorial = useCallback( + (tutorial) => { + if (driverRef.current) { + driverRef.current.destroy(); + } + + // If tutorial specifies pages and we're not on any of them, navigate first + if (tutorial.pages?.length && !tutorial.pages.includes(router.pathname)) { + router.push(tutorial.pages[0]).then(() => { + // Small delay to let the page render before starting the tour + setTimeout(() => runDriver(tutorial), 500); + }); + return; + } + + runDriver(tutorial); + }, + [router] + ); + + const runDriver = useCallback((tutorial) => { + setActiveTutorial(tutorial); + + const driverObj = driver({ + showProgress: true, + animate: true, + allowClose: true, + overlayColor: "rgba(0, 0, 0, 0.6)", + stagePadding: 8, + stageRadius: 8, + popoverClass: "cipp-tutorial-popover", + nextBtnText: "Next →", + prevBtnText: "← Back", + doneBtnText: "Done ✓", + progressText: "{{current}} of {{total}}", + steps: tutorial.steps, + onDestroyed: () => { + storeCompletedTutorial(tutorial.id); + setCompletedIds(getCompletedTutorials()); + setActiveTutorial(null); + driverRef.current = null; + }, + }); + + driverRef.current = driverObj; + driverObj.drive(); + }, []); + + const resetProgress = useCallback(() => { + resetCompletedTutorials(); + setCompletedIds([]); + }, []); + + const getTutorialsForPage = useCallback( + (pathname) => { + return tutorials.filter( + (t) => !t.pages || t.pages.length === 0 || t.pages.includes(pathname) + ); + }, + [tutorials] + ); + + const value = useMemo( + () => ({ + tutorials, + activeTutorial, + completedIds, + startTutorial, + resetProgress, + getTutorialsForPage, + }), + [tutorials, activeTutorial, completedIds, startTutorial, resetProgress, getTutorialsForPage] + ); + + return {children}; +}; + +export const useTutorials = () => useContext(TutorialContext); diff --git a/src/data/tutorials/dashboard-overview.json b/src/data/tutorials/dashboard-overview.json new file mode 100644 index 000000000000..df2aeca19615 --- /dev/null +++ b/src/data/tutorials/dashboard-overview.json @@ -0,0 +1,111 @@ +{ + "id": "dashboard-overview", + "title": "Dashboard Overview", + "description": "A guided tour of the CIPP Dashboard — learn what each card, chart, and control does.", + "category": "General", + "pages": ["/dashboardv2"], + "steps": [ + { + "popover": { + "title": "Welcome to the Dashboard 👋", + "description": "This tour will walk you through every section of the CIPP Dashboard so you know exactly where to find the information you need." + } + }, + { + "element": "[data-tutorial='tenant-selector']", + "popover": { + "title": "Select a Tenant", + "description": "Start here. Pick the customer tenant you want to inspect. The entire dashboard updates to show data for the selected tenant.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-portals']", + "popover": { + "title": "Quick Portal Access", + "description": "Jump straight into the Microsoft admin portals (M365, Exchange, Entra, Intune, Azure, etc.) for the selected tenant — no need to look up URLs.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-test-suite']", + "popover": { + "title": "Test Suite Selector", + "description": "Choose which test suite to run against the tenant. You can create custom test suites, refresh results, or edit existing ones from here.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-tenant-info']", + "popover": { + "title": "Tenant Information", + "description": "Quick-reference card showing the tenant's display name, tenant ID, and primary domain. Click the chips to copy values to your clipboard.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-tenant-metrics']", + "popover": { + "title": "Tenant Metrics", + "description": "At-a-glance counts for Users, Guests, Groups, Service Principals, Devices, and Managed Devices. Click any metric to drill into the full list.", + "side": "bottom", + "align": "center" + } + }, + { + "element": "[data-tutorial='dashboard-assessment']", + "popover": { + "title": "Assessment Results", + "description": "A summary of the selected test suite results broken down by category (Identity, Devices, Custom). Green = passed, Red = failed, Orange = skipped.", + "side": "bottom", + "align": "end" + } + }, + { + "element": "[data-tutorial='dashboard-secure-score']", + "popover": { + "title": "Secure Score Trend", + "description": "A line chart tracking the tenant's Microsoft Secure Score over time. The reference line shows the maximum possible score, so you can gauge progress.", + "side": "right", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-mfa']", + "popover": { + "title": "User Authentication (MFA)", + "description": "This Sankey diagram shows how many enabled users are MFA-registered vs. not, and what enforcement method protects them (Conditional Access, Security Defaults, Per-user MFA, or none). Click any node to jump to the MFA report.", + "side": "left", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-auth-methods']", + "popover": { + "title": "Auth Methods Breakdown", + "description": "See how users authenticate — single-factor vs. multi-factor, phishable vs. phish-resistant — and which methods (Phone, Authenticator, Passkey, WHfB) they use. Click a node to filter the MFA report.", + "side": "right", + "align": "start" + } + }, + { + "element": "[data-tutorial='dashboard-licenses']", + "popover": { + "title": "License Overview", + "description": "Shows the top licenses in the tenant with assigned vs. available counts. Use this to spot unused or over-provisioned licenses at a glance.", + "side": "left", + "align": "start" + } + }, + { + "popover": { + "title": "You're all set! 🎉", + "description": "That covers the Dashboard. Explore the other tabs at the top for more views, or check the sidebar for all CIPP modules. You can replay this tour anytime from the Tutorials menu." + } + } + ] +} diff --git a/src/data/tutorials/getting-started.json b/src/data/tutorials/getting-started.json new file mode 100644 index 000000000000..8e2c2291350a --- /dev/null +++ b/src/data/tutorials/getting-started.json @@ -0,0 +1,48 @@ +{ + "id": "getting-started", + "title": "Getting Started with CIPP", + "description": "Learn the basics of navigating CIPP and managing your tenants.", + "category": "General", + "pages": ["/"], + "steps": [ + { + "popover": { + "title": "Welcome to CIPP! 👋", + "description": "This quick tour will show you the key features of the CIPP dashboard. Let's get started!" + } + }, + { + "element": "[data-tutorial='tenant-selector']", + "popover": { + "title": "Tenant Selector", + "description": "Use the tenant selector to switch between your managed tenants. You can search by name or select 'All Tenants' for a global view.", + "side": "bottom", + "align": "start" + } + }, + { + "element": "[data-tutorial='side-nav']", + "popover": { + "title": "Navigation Menu", + "description": "The sidebar gives you access to all CIPP modules — Identity, Endpoint, Security, Email, Teams & SharePoint, and more.", + "side": "right", + "align": "start" + } + }, + { + "element": "[data-tutorial='speed-dial']", + "popover": { + "title": "Quick Actions", + "description": "Use the help button in the bottom-right corner to report bugs, request features, join Discord, or access the documentation.", + "side": "left", + "align": "end" + } + }, + { + "popover": { + "title": "You're all set! 🎉", + "description": "You now know the basics. Explore the sidebar to discover all the tools CIPP offers. You can replay this tour anytime from the Tutorials menu." + } + } + ] +} diff --git a/src/data/tutorials/tenant-management.json b/src/data/tutorials/tenant-management.json new file mode 100644 index 000000000000..0492fd8fdac5 --- /dev/null +++ b/src/data/tutorials/tenant-management.json @@ -0,0 +1,39 @@ +{ + "id": "tenant-management", + "title": "Managing Tenants", + "description": "Learn how to view tenant details, manage tenant settings, and navigate tenant-specific pages.", + "category": "Administration", + "pages": ["/tenant/administration/tenants"], + "steps": [ + { + "popover": { + "title": "Tenant Management", + "description": "This page shows all your managed tenants. Let's walk through the key features." + } + }, + { + "element": "[data-tutorial='breadcrumb-nav']", + "popover": { + "title": "Breadcrumb Navigation", + "description": "Use the breadcrumb trail to see where you are and quickly navigate back to parent pages.", + "side": "bottom", + "align": "start" + } + }, + { + "element": ".MuiTableContainer-root", + "popover": { + "title": "Tenant List", + "description": "Here you'll find all your managed tenants. Click on any row to see detailed information about that tenant.", + "side": "top", + "align": "center" + } + }, + { + "popover": { + "title": "That's it!", + "description": "You now know how to manage your tenants. Check out other tutorials for more advanced features." + } + } + ] +} diff --git a/src/layouts/side-nav.js b/src/layouts/side-nav.js index 5b01ee107331..1dadfca01577 100644 --- a/src/layouts/side-nav.js +++ b/src/layouts/side-nav.js @@ -169,6 +169,7 @@ export const SideNav = (props) => { setHovered(true), diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index 41d5f07e0f2f..2c7e0a2507ad 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -278,7 +278,9 @@ export const TopNav = (props) => { {!mdDown && ( - + + + )} {mdDown && ( diff --git a/src/pages/_app.js b/src/pages/_app.js index aa387f0417fa..74504f43b9fa 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -11,6 +11,8 @@ import { store } from '../store' import { createTheme } from '../theme' import { createEmotionCache } from '../utils/create-emotion-cache' import '../libs/nprogress' +import 'driver.js/dist/driver.css' +import '../styles/tutorial-overrides.css' import { PrivateRoute } from '../components/PrivateRoute' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { useMediaPredicate } from 'react-media-hook' @@ -52,12 +54,15 @@ import { Gavel, ClearAll as ClearAllIcon, } from '@mui/icons-material' +import { School as TutorialIcon } from '@mui/icons-material' import { SvgIcon } from '@mui/material' import React, { useEffect, useState, useRef } from 'react' import { usePathname } from 'next/navigation' import { useRouter } from 'next/router' import { persistQueryClient } from '@tanstack/react-query-persist-client' import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister' +import { TutorialProvider } from '../contexts/tutorial-context' +import CippTutorialDialog from '../components/CippComponents/CippTutorialDialog' const ReactQueryDevtoolsProduction = React.lazy(() => import('@tanstack/react-query-devtools/build/modern/production.js').then((d) => ({ @@ -76,6 +81,7 @@ const App = (props) => { const pathname = usePathname() const route = useRouter() const [dateLocale, setDateLocale] = useState(enUS) + const [tutorialDialogOpen, setTutorialDialogOpen] = useState(false) useEffect(() => { if (typeof window === 'undefined') return @@ -243,6 +249,12 @@ const App = (props) => { href: `https://docs.cipp.app/user-documentation${pathname}`, onClick: () => window.open(`https://docs.cipp.app/user-documentation${pathname}`, '_blank'), }, + { + id: 'tutorials', + icon: , + name: 'Tutorials', + onClick: () => setTutorialDialogOpen(true), + }, ] return ( @@ -275,9 +287,15 @@ const App = (props) => { - - {getLayout()} - + + + {getLayout()} + + setTutorialDialogOpen(false)} + /> + diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index 0ea9653e3680..dfd9d9a76528 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -194,8 +194,9 @@ const Page = () => { - + { )} - + @@ -319,12 +320,12 @@ const Page = () => { {/* Tenant Overview Section - 3 Column Layout */} {/* Column 1: Tenant Information */} - + {/* Column 2: Tenant Metrics - 2x3 Grid */} - + { {/* Column 3: Assessment Results */} - + { {/* Left Column */} - + - + { {/* Right Column */} - + - + Date: Mon, 25 May 2026 18:31:00 +0200 Subject: [PATCH 045/133] add tutorials to easy deployment of steps for Ashe. --- .../CippComponents/CippBreadcrumbNav.jsx | 514 +++++++++--------- .../CippComponents/CippSpeedDial.jsx | 156 +++--- .../CippComponents/CippTutorialDialog.jsx | 77 ++- src/contexts/tutorial-context.js | 142 +++-- src/layouts/top-nav.js | 6 +- src/pages/dashboardv2/index.js | 5 +- src/styles/tutorial-overrides.css | 2 +- 7 files changed, 455 insertions(+), 447 deletions(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index 8e285d78e364..5ea88f3434cf 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -1,155 +1,155 @@ -import { useEffect, useState, useRef } from "react"; -import { useRouter } from "next/router"; -import { Breadcrumbs, Link, Typography, Box, IconButton, Tooltip } from "@mui/material"; -import { NavigateNext, History, AccountTree } from "@mui/icons-material"; -import { nativeMenuItems } from "../../layouts/config"; -import { useSettings } from "../../hooks/use-settings"; +import { useEffect, useState, useRef } from 'react' +import { useRouter } from 'next/router' +import { Breadcrumbs, Link, Typography, Box, IconButton, Tooltip } from '@mui/material' +import { NavigateNext, History, AccountTree } from '@mui/icons-material' +import { nativeMenuItems } from '../../layouts/config' +import { useSettings } from '../../hooks/use-settings' -const MAX_HISTORY_STORAGE = 20; // Maximum number of pages to keep in history -const MAX_BREADCRUMB_DISPLAY = 5; // Maximum number of breadcrumbs to display at once +const MAX_HISTORY_STORAGE = 20 // Maximum number of pages to keep in history +const MAX_BREADCRUMB_DISPLAY = 5 // Maximum number of breadcrumbs to display at once /** * Load all tabOptions.json files dynamically */ async function loadTabOptions() { const tabOptionPaths = [ - "/email/administration/exchange-retention", - "/cipp/custom-data", - "/cipp/advanced/super-admin", - "/endpoint/MEM/enrollment-profiles", - "/tenant/standards", - "/tenant/manage", - "/tenant/administration/applications", - "/tenant/administration/tenants", - "/tenant/administration/audit-logs", - "/identity/administration/users/user", - "/tenant/administration/securescore", - "/tenant/gdap-management", - "/tenant/gdap-management/relationships/relationship", - "/cipp/settings", - ]; - - const tabOptions = []; + '/email/administration/exchange-retention', + '/cipp/custom-data', + '/cipp/advanced/super-admin', + '/endpoint/MEM/enrollment-profiles', + '/tenant/standards', + '/tenant/manage', + '/tenant/administration/applications', + '/tenant/administration/tenants', + '/tenant/administration/audit-logs', + '/identity/administration/users/user', + '/tenant/administration/securescore', + '/tenant/gdap-management', + '/tenant/gdap-management/relationships/relationship', + '/cipp/settings', + ] + + const tabOptions = [] for (const basePath of tabOptionPaths) { try { - const module = await import(`../../pages${basePath}/tabOptions.json`); - const options = module.default || module; + const module = await import(`../../pages${basePath}/tabOptions.json`) + const options = module.default || module // Add each tab option with metadata options.forEach((option) => { tabOptions.push({ title: option.label, path: option.path, - type: "tab", + type: 'tab', basePath: basePath, - }); - }); + }) + }) } catch (error) { // Silently skip if file doesn't exist or can't be loaded } } - return tabOptions; + return tabOptions } export const CippBreadcrumbNav = () => { - const router = useRouter(); - const settings = useSettings(); - const [history, setHistory] = useState([]); - const [mode, setMode] = useState(settings.breadcrumbMode || "hierarchical"); - const [tabOptions, setTabOptions] = useState([]); - const lastRouteRef = useRef(null); - const titleCheckCountRef = useRef(0); - const titleCheckIntervalRef = useRef(null); + const router = useRouter() + const settings = useSettings() + const [history, setHistory] = useState([]) + const [mode, setMode] = useState(settings.breadcrumbMode || 'hierarchical') + const [tabOptions, setTabOptions] = useState([]) + const lastRouteRef = useRef(null) + const titleCheckCountRef = useRef(0) + const titleCheckIntervalRef = useRef(null) // Helper function to filter out unnecessary query parameters const getCleanQueryParams = (query) => { - const cleaned = { ...query }; + const cleaned = { ...query } // Remove tenantFilter if it's "AllTenants" or not explicitly needed - if (cleaned.tenantFilter === "AllTenants" || cleaned.tenantFilter === undefined) { - delete cleaned.tenantFilter; + if (cleaned.tenantFilter === 'AllTenants' || cleaned.tenantFilter === undefined) { + delete cleaned.tenantFilter } - return cleaned; - }; + return cleaned + } // Helper function to clean page titles const cleanPageTitle = (title) => { - if (!title) return title; + if (!title) return title // Remove AllTenants and any surrounding separators return title - .replace(/\s*-\s*AllTenants\s*/, "") - .replace(/AllTenants\s*-\s*/, "") - .replace(/AllTenants/, "") - .trim(); - }; + .replace(/\s*-\s*AllTenants\s*/, '') + .replace(/AllTenants\s*-\s*/, '') + .replace(/AllTenants/, '') + .trim() + } // Load tab options on mount useEffect(() => { - loadTabOptions().then(setTabOptions); - }, []); + loadTabOptions().then(setTabOptions) + }, []) useEffect(() => { // Only update when the route actually changes, not on every render - const currentRoute = router.asPath; + const currentRoute = router.asPath // Skip if this is the same route as last time if (lastRouteRef.current === currentRoute) { - return; + return } - lastRouteRef.current = currentRoute; + lastRouteRef.current = currentRoute // Clear any existing title check interval if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } // Reset check counter - titleCheckCountRef.current = 0; + titleCheckCountRef.current = 0 // Function to check and update title const checkTitle = () => { - titleCheckCountRef.current++; + titleCheckCountRef.current++ // Stop checking after 50 attempts (5 seconds) to prevent infinite intervals if (titleCheckCountRef.current > 50) { if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - return; + return } - let pageTitle = document.title.replace(" - CIPP", "").trim(); + let pageTitle = document.title.replace(' - CIPP', '').trim() // Remove tenant domain from title (e.g., "Groups - domain.onmicrosoft.com" -> "Groups") // But only if it looks like a domain (contains a dot) - const parts = pageTitle.split(" - "); - if (parts.length > 1 && parts[parts.length - 1].includes(".")) { - pageTitle = parts.slice(0, -1).join(" - ").trim(); + const parts = pageTitle.split(' - ') + if (parts.length > 1 && parts[parts.length - 1].includes('.')) { + pageTitle = parts.slice(0, -1).join(' - ').trim() } // Clean AllTenants from title - pageTitle = cleanPageTitle(pageTitle); + pageTitle = cleanPageTitle(pageTitle) // Skip if title is empty, generic, or error page if ( !pageTitle || - pageTitle === "CIPP" || - pageTitle.toLowerCase().includes("error") || - pageTitle === "404" || - pageTitle === "500" + pageTitle === 'CIPP' || + pageTitle.toLowerCase().includes('error') || + pageTitle === '404' || + pageTitle === '500' ) { - return; + return } // Normalize URL for comparison (remove trailing slashes and query params) const normalizeUrl = (url) => { // Remove query params and trailing slashes for comparison - return url.split("?")[0].replace(/\/$/, "").toLowerCase(); - }; + return url.split('?')[0].replace(/\/$/, '').toLowerCase() + } const currentPage = { title: pageTitle, @@ -157,190 +157,190 @@ export const CippBreadcrumbNav = () => { query: { ...router.query }, fullUrl: router.asPath, timestamp: Date.now(), - }; + } - const normalizedCurrentUrl = normalizeUrl(currentPage.fullUrl); + const normalizedCurrentUrl = normalizeUrl(currentPage.fullUrl) setHistory((prevHistory) => { // Check if last entry has same title AND similar path (prevent duplicate with same content) - const lastEntry = prevHistory[prevHistory.length - 1]; + const lastEntry = prevHistory[prevHistory.length - 1] if (lastEntry) { - const sameTitle = lastEntry.title.trim() === currentPage.title.trim(); - const samePath = normalizeUrl(lastEntry.fullUrl) === normalizedCurrentUrl; + const sameTitle = lastEntry.title.trim() === currentPage.title.trim() + const samePath = normalizeUrl(lastEntry.fullUrl) === normalizedCurrentUrl if (sameTitle && samePath) { // Exact duplicate - don't add, just stop checking if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - return prevHistory; + return prevHistory } if (samePath && !sameTitle) { // Same URL but title changed - update the entry - const updated = [...prevHistory]; + const updated = [...prevHistory] updated[prevHistory.length - 1] = { ...currentPage, query: getCleanQueryParams(currentPage.query), - }; + } if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - return updated; + return updated } } // Find if this URL exists anywhere EXCEPT the last position in history const existingIndex = prevHistory.findIndex((entry, index) => { // Skip the last entry since we already checked it above - if (index === prevHistory.length - 1) return false; - return normalizeUrl(entry.fullUrl) === normalizedCurrentUrl; - }); + if (index === prevHistory.length - 1) return false + return normalizeUrl(entry.fullUrl) === normalizedCurrentUrl + }) // URL not in history (except possibly as last entry which we handled) - add as new entry if (existingIndex === -1) { const cleanedCurrentPage = { ...currentPage, query: getCleanQueryParams(currentPage.query), - }; - const newHistory = [...prevHistory, cleanedCurrentPage]; + } + const newHistory = [...prevHistory, cleanedCurrentPage] // Keep only the last MAX_HISTORY_STORAGE pages const trimmedHistory = newHistory.length > MAX_HISTORY_STORAGE ? newHistory.slice(-MAX_HISTORY_STORAGE) - : newHistory; + : newHistory // Don't stop checking yet - title might still be loading - return trimmedHistory; + return trimmedHistory } // URL exists in history but not as last entry - user navigated back // Truncate history after this point and update the entry if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - const updated = prevHistory.slice(0, existingIndex + 1); + const updated = prevHistory.slice(0, existingIndex + 1) updated[existingIndex] = { ...currentPage, query: getCleanQueryParams(currentPage.query), - }; - return updated; - }); - }; + } + return updated + }) + } // Start checking for title updates - titleCheckIntervalRef.current = setInterval(checkTitle, 100); + titleCheckIntervalRef.current = setInterval(checkTitle, 100) return () => { if (titleCheckIntervalRef.current) { - clearInterval(titleCheckIntervalRef.current); - titleCheckIntervalRef.current = null; + clearInterval(titleCheckIntervalRef.current) + titleCheckIntervalRef.current = null } - }; - }, [router.asPath, router.pathname, router.query]); + } + }, [router.asPath, router.pathname, router.query]) const handleBreadcrumbClick = (index) => { - const page = history[index]; + const page = history[index] if (page) { - const cleanedQuery = getCleanQueryParams(page.query); + const cleanedQuery = getCleanQueryParams(page.query) router.push({ pathname: page.path, query: cleanedQuery, - }); + }) } - }; + } // State to track current page title for hierarchical mode - const [currentPageTitle, setCurrentPageTitle] = useState(null); - const hierarchicalTitleCheckRef = useRef(null); - const hierarchicalCheckCountRef = useRef(0); + const [currentPageTitle, setCurrentPageTitle] = useState(null) + const hierarchicalTitleCheckRef = useRef(null) + const hierarchicalCheckCountRef = useRef(0) // Watch for title changes to update hierarchical breadcrumbs useEffect(() => { - if (mode === "hierarchical") { + if (mode === 'hierarchical') { // Clear any existing interval if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } // Reset counter - hierarchicalCheckCountRef.current = 0; + hierarchicalCheckCountRef.current = 0 const updateTitle = () => { - hierarchicalCheckCountRef.current++; + hierarchicalCheckCountRef.current++ // Stop after 20 attempts (10 seconds) to prevent infinite checking if (hierarchicalCheckCountRef.current > 20) { if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } - return; + return } - let pageTitle = document.title.replace(" - CIPP", "").trim(); - const parts = pageTitle.split(" - "); + let pageTitle = document.title.replace(' - CIPP', '').trim() + const parts = pageTitle.split(' - ') const cleanTitle = - parts.length > 1 && parts[parts.length - 1].includes(".") - ? parts.slice(0, -1).join(" - ").trim() - : pageTitle; + parts.length > 1 && parts[parts.length - 1].includes('.') + ? parts.slice(0, -1).join(' - ').trim() + : pageTitle // Clean AllTenants from title - const finalTitle = cleanPageTitle(cleanTitle); + const finalTitle = cleanPageTitle(cleanTitle) - if (finalTitle && finalTitle !== "CIPP" && !finalTitle.toLowerCase().includes("loading")) { - setCurrentPageTitle(finalTitle); + if (finalTitle && finalTitle !== 'CIPP' && !finalTitle.toLowerCase().includes('loading')) { + setCurrentPageTitle(finalTitle) // Stop checking once we have a valid title if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } } - }; + } // Initial update - updateTitle(); + updateTitle() // Only start interval if we don't have a valid title yet - if (!currentPageTitle || currentPageTitle.toLowerCase().includes("loading")) { - hierarchicalTitleCheckRef.current = setInterval(updateTitle, 500); + if (!currentPageTitle || currentPageTitle.toLowerCase().includes('loading')) { + hierarchicalTitleCheckRef.current = setInterval(updateTitle, 500) } return () => { if (hierarchicalTitleCheckRef.current) { - clearInterval(hierarchicalTitleCheckRef.current); - hierarchicalTitleCheckRef.current = null; + clearInterval(hierarchicalTitleCheckRef.current) + hierarchicalTitleCheckRef.current = null } - }; + } } - }, [mode, router.pathname]); + }, [mode, router.pathname]) // Build hierarchical breadcrumbs from config.js navigation structure const buildHierarchicalBreadcrumbs = () => { - const currentPath = router.pathname; + const currentPath = router.pathname // Helper to check if paths match (handles dynamic routes) const pathsMatch = (menuPath, currentPath) => { - if (!menuPath) return false; + if (!menuPath) return false // Exact match - if (menuPath === currentPath) return true; + if (menuPath === currentPath) return true // Check if current path starts with menu path (for nested routes) // e.g., menu: "/identity/administration/users" matches "/identity/administration/users/edit" - if (currentPath.startsWith(menuPath + "/")) return true; + if (currentPath.startsWith(menuPath + '/')) return true - return false; - }; + return false + } const findPathInMenu = (items, path = []) => { for (const item of items) { - const currentBreadcrumb = [...path]; + const currentBreadcrumb = [...path] // Add current item to path if it has a title // Include all items (headers, groups, and pages) to show full hierarchy @@ -350,44 +350,44 @@ export const CippBreadcrumbNav = () => { path: item.path, type: item.type, query: {}, // Menu items don't have query params by default - }); + }) } // Check if this item matches the current path if (item.path && pathsMatch(item.path, currentPath)) { // If this is the current page, include current query params (cleaned) if (item.path === currentPath) { - const lastItem = currentBreadcrumb[currentBreadcrumb.length - 1]; + const lastItem = currentBreadcrumb[currentBreadcrumb.length - 1] if (lastItem) { - lastItem.query = getCleanQueryParams(router.query); + lastItem.query = getCleanQueryParams(router.query) } } - return currentBreadcrumb; + return currentBreadcrumb } // Recursively search children if (item.items && item.items.length > 0) { - const result = findPathInMenu(item.items, currentBreadcrumb); + const result = findPathInMenu(item.items, currentBreadcrumb) if (result.length > 0) { - return result; + return result } } } - return []; - }; + return [] + } - let result = findPathInMenu(nativeMenuItems); + let result = findPathInMenu(nativeMenuItems) // If we found a menu item, check if the current path matches any tab // If so, tabOptions wins and we use its label if (result.length > 0 && tabOptions.length > 0) { - const normalizedCurrentPath = currentPath.replace(/\/$/, ""); + const normalizedCurrentPath = currentPath.replace(/\/$/, '') // Check if current path matches any tab (exact match) const matchingTab = tabOptions.find((tab) => { - const normalizedTabPath = tab.path.replace(/\/$/, ""); - return normalizedTabPath === normalizedCurrentPath; - }); + const normalizedTabPath = tab.path.replace(/\/$/, '') + return normalizedTabPath === normalizedCurrentPath + }) if (matchingTab) { // Tab matches the current path - use tab's label instead of config's @@ -396,32 +396,32 @@ export const CippBreadcrumbNav = () => { return { ...item, title: matchingTab.title, - type: "tab", - }; + type: 'tab', + } } - return item; - }); + return item + }) } } // If not found in main menu, check if it's a tab page if (result.length === 0 && tabOptions.length > 0) { - const normalizedCurrentPath = currentPath.replace(/\/$/, ""); + const normalizedCurrentPath = currentPath.replace(/\/$/, '') // Find matching tab option const matchingTab = tabOptions.find((tab) => { - const normalizedTabPath = tab.path.replace(/\/$/, ""); - return normalizedTabPath === normalizedCurrentPath; - }); + const normalizedTabPath = tab.path.replace(/\/$/, '') + return normalizedTabPath === normalizedCurrentPath + }) if (matchingTab) { // Find the base page in the menu and build full path to it - const normalizedBasePath = matchingTab.basePath?.replace(/\/$/, ""); + const normalizedBasePath = matchingTab.basePath?.replace(/\/$/, '') // Recursively find the base page and build breadcrumb path const findBasePageWithPath = (items, path = []) => { for (const item of items) { - const currentBreadcrumb = [...path]; + const currentBreadcrumb = [...path] // Add current item to path if it has a title if (item.title) { @@ -430,185 +430,188 @@ export const CippBreadcrumbNav = () => { path: item.path, type: item.type, query: {}, // Menu items don't have query params by default - }); + }) } // Check if this item matches the base path if (item.path) { - const normalizedItemPath = item.path.replace(/\/$/, ""); + const normalizedItemPath = item.path.replace(/\/$/, '') if ( normalizedItemPath === normalizedBasePath || normalizedItemPath.startsWith(normalizedBasePath) ) { - return currentBreadcrumb; + return currentBreadcrumb } } // Recursively search children if (item.items && item.items.length > 0) { - const found = findBasePageWithPath(item.items, currentBreadcrumb); + const found = findBasePageWithPath(item.items, currentBreadcrumb) if (found.length > 0) { - return found; + return found } } } - return []; - }; + return [] + } - const basePagePath = findBasePageWithPath(nativeMenuItems); + const basePagePath = findBasePageWithPath(nativeMenuItems) if (basePagePath.length > 0) { - result = basePagePath; + result = basePagePath // Add the tab as the final breadcrumb with current query params (cleaned) result.push({ title: matchingTab.title, path: matchingTab.path, - type: "tab", + type: 'tab', query: getCleanQueryParams(router.query), // Include current query params for tab page - }); + }) } } } // Check if we're on a nested page under a menu item (e.g., edit page) if (result.length > 0) { - const lastItem = result[result.length - 1]; + const lastItem = result[result.length - 1] if (lastItem.path && lastItem.path !== currentPath && currentPath.startsWith(lastItem.path)) { // Use the tracked page title if available, otherwise fall back to document.title - let tabTitle = currentPageTitle || document.title.replace(" - CIPP", "").trim(); + let tabTitle = currentPageTitle || document.title.replace(' - CIPP', '').trim() // Clean AllTenants from title - tabTitle = cleanPageTitle(tabTitle); + tabTitle = cleanPageTitle(tabTitle) // Add tab as an additional breadcrumb item if ( tabTitle && tabTitle !== lastItem.title && - !tabTitle.toLowerCase().includes("loading") + !tabTitle.toLowerCase().includes('loading') ) { result.push({ title: tabTitle, path: currentPath, - type: "tab", + type: 'tab', query: getCleanQueryParams(router.query), // Include current query params (cleaned) - }); + }) } } } - return result; - }; + return result + } // Check if a path is valid and return its title from navigation or tabs const getPathInfo = (path) => { - if (!path) return { isValid: false, title: null }; + if (!path) return { isValid: false, title: null } - const normalizedPath = path.replace(/\/$/, ""); + const normalizedPath = path.replace(/\/$/, '') // Helper function to recursively search menu items const findInMenu = (items) => { for (const item of items) { if (item.path) { - const normalizedItemPath = item.path.replace(/\/$/, ""); + const normalizedItemPath = item.path.replace(/\/$/, '') if (normalizedItemPath === normalizedPath) { - return { isValid: true, title: item.title }; + return { isValid: true, title: item.title } } } if (item.items && item.items.length > 0) { - const found = findInMenu(item.items); + const found = findInMenu(item.items) if (found.isValid) { - return found; + return found } } } - return { isValid: false, title: null }; - }; + return { isValid: false, title: null } + } // Check if path exists in navigation - const menuResult = findInMenu(nativeMenuItems); + const menuResult = findInMenu(nativeMenuItems) if (menuResult.isValid) { - return menuResult; + return menuResult } // Check if path exists in tab options - const matchingTab = tabOptions.find((tab) => tab.path.replace(/\/$/, "") === normalizedPath); + const matchingTab = tabOptions.find((tab) => tab.path.replace(/\/$/, '') === normalizedPath) if (matchingTab) { - return { isValid: true, title: matchingTab.title }; + return { isValid: true, title: matchingTab.title } } - return { isValid: false, title: null }; - }; + return { isValid: false, title: null } + } // Handle click for hierarchical breadcrumbs const handleHierarchicalClick = (path, query) => { if (path) { - const cleanedQuery = getCleanQueryParams(query); + const cleanedQuery = getCleanQueryParams(query) if (cleanedQuery && Object.keys(cleanedQuery).length > 0) { router.push({ pathname: path, query: cleanedQuery, - }); + }) } else { - router.push(path); + router.push(path) } } - }; + } // Toggle between modes const toggleMode = () => { setMode((prevMode) => { - const newMode = prevMode === "hierarchical" ? "history" : "hierarchical"; - settings.handleUpdate({ breadcrumbMode: newMode }); - return newMode; - }); - }; + const newMode = prevMode === 'hierarchical' ? 'history' : 'hierarchical' + settings.handleUpdate({ breadcrumbMode: newMode }) + return newMode + }) + } // Render based on mode - if (mode === "hierarchical") { - let breadcrumbs = buildHierarchicalBreadcrumbs(); + if (mode === 'hierarchical') { + let breadcrumbs = buildHierarchicalBreadcrumbs() // Fallback: If no breadcrumbs found in navigation config, generate from URL path if (breadcrumbs.length === 0) { - const pathSegments = router.pathname.split("/").filter((segment) => segment); + const pathSegments = router.pathname.split('/').filter((segment) => segment) if (pathSegments.length > 0) { breadcrumbs = pathSegments.map((segment, index) => { // Build the path up to this segment - const path = "/" + pathSegments.slice(0, index + 1).join("/"); + const path = '/' + pathSegments.slice(0, index + 1).join('/') // Format segment as title (replace hyphens with spaces, capitalize words) const title = segment - .split("-") + .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(" "); + .join(' ') return { title, path, - type: "fallback", + type: 'fallback', query: index === pathSegments.length - 1 ? getCleanQueryParams(router.query) : {}, - }; - }); + } + }) // If we have a current page title from document.title, use it for the last breadcrumb if ( currentPageTitle && - currentPageTitle !== "CIPP" && - !currentPageTitle.toLowerCase().includes("loading") + currentPageTitle !== 'CIPP' && + !currentPageTitle.toLowerCase().includes('loading') ) { - breadcrumbs[breadcrumbs.length - 1].title = cleanPageTitle(currentPageTitle); + breadcrumbs[breadcrumbs.length - 1].title = cleanPageTitle(currentPageTitle) } } } // Don't show if still no breadcrumbs found if (breadcrumbs.length === 0) { - return null; + return null } return ( - + @@ -617,13 +620,13 @@ export const CippBreadcrumbNav = () => { } aria-label="page hierarchy" - sx={{ fontSize: "0.875rem", flexGrow: 1 }} + sx={{ fontSize: '0.875rem', flexGrow: 1 }} > {breadcrumbs.map((crumb, index) => { - const isLast = index === breadcrumbs.length - 1; - const pathInfo = getPathInfo(crumb.path); + const isLast = index === breadcrumbs.length - 1 + const pathInfo = getPathInfo(crumb.path) // Use title from nav/tabs if available, otherwise use the crumb's title - const displayTitle = pathInfo.title || crumb.title; + const displayTitle = pathInfo.title || crumb.title // Items without paths (headers/groups) - show as text if (!crumb.path) { @@ -636,7 +639,7 @@ export const CippBreadcrumbNav = () => { > {displayTitle} - ); + ) } // Items with valid paths are clickable @@ -649,48 +652,51 @@ export const CippBreadcrumbNav = () => { variant="subtitle2" onClick={() => handleHierarchicalClick(crumb.path, crumb.query)} sx={{ - textDecoration: "none", - color: isLast ? "text.primary" : "text.secondary", + textDecoration: 'none', + color: isLast ? 'text.primary' : 'text.secondary', fontWeight: isLast ? 500 : 400, - "&:hover": { - textDecoration: "underline", - color: "primary.main", + '&:hover': { + textDecoration: 'underline', + color: 'primary.main', }, }} > {displayTitle} - ); + ) } else { // Invalid path - show as text only return ( {displayTitle} - ); + ) } })} - ); + ) } // Default mode: history-based breadcrumbs // Don't show breadcrumbs if we have no history if (history.length === 0) { - return null; + return null } // Show only the last MAX_BREADCRUMB_DISPLAY items - const visibleHistory = history.slice(-MAX_BREADCRUMB_DISPLAY); + const visibleHistory = history.slice(-MAX_BREADCRUMB_DISPLAY) return ( - + @@ -700,12 +706,12 @@ export const CippBreadcrumbNav = () => { maxItems={MAX_BREADCRUMB_DISPLAY} separator={} aria-label="navigation history" - sx={{ fontSize: "0.875rem", flexGrow: 1 }} + sx={{ fontSize: '0.875rem', flexGrow: 1 }} > {visibleHistory.map((page, index) => { - const isLast = index === visibleHistory.length - 1; + const isLast = index === visibleHistory.length - 1 // Calculate the actual index in the full history - const actualIndex = history.length - visibleHistory.length + index; + const actualIndex = history.length - visibleHistory.length + index if (isLast) { return ( @@ -717,7 +723,7 @@ export const CippBreadcrumbNav = () => { > {page.title} - ); + ) } return ( @@ -727,19 +733,19 @@ export const CippBreadcrumbNav = () => { variant="subtitle2" onClick={() => handleBreadcrumbClick(actualIndex)} sx={{ - textDecoration: "none", - color: "text.secondary", - "&:hover": { - textDecoration: "underline", - color: "primary.main", + textDecoration: 'none', + color: 'text.secondary', + '&:hover': { + textDecoration: 'underline', + color: 'primary.main', }, }} > {page.title} - ); + ) })} - ); -}; + ) +} diff --git a/src/components/CippComponents/CippSpeedDial.jsx b/src/components/CippComponents/CippSpeedDial.jsx index 15a1b4547d9b..0a2365bc4707 100644 --- a/src/components/CippComponents/CippSpeedDial.jsx +++ b/src/components/CippComponents/CippSpeedDial.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from 'react' import { SpeedDial, SpeedDialAction, @@ -11,10 +11,10 @@ import { Snackbar, Alert, CircularProgress, -} from "@mui/material"; -import { Close as CloseIcon } from "@mui/icons-material"; -import { useForm } from "react-hook-form"; -import { CippFormComponent } from "../../components/CippComponents/CippFormComponent"; +} from '@mui/material' +import { Close as CloseIcon } from '@mui/icons-material' +import { useForm } from 'react-hook-form' +import { CippFormComponent } from '../../components/CippComponents/CippFormComponent' const CippSpeedDial = ({ actions = [], @@ -22,92 +22,92 @@ const CippSpeedDial = ({ icon, openIcon = , }) => { - const [openDialogs, setOpenDialogs] = useState({}); - const [loading, setLoading] = useState(false); - const [showSnackbar, setShowSnackbar] = useState(false); - const [speedDialOpen, setSpeedDialOpen] = useState(false); - const [isHovering, setIsHovering] = useState(false); - const [snackbarMessage, setSnackbarMessage] = useState(""); + const [openDialogs, setOpenDialogs] = useState({}) + const [loading, setLoading] = useState(false) + const [showSnackbar, setShowSnackbar] = useState(false) + const [speedDialOpen, setSpeedDialOpen] = useState(false) + const [isHovering, setIsHovering] = useState(false) + const [snackbarMessage, setSnackbarMessage] = useState('') const formControls = actions.reduce((acc, action) => { if (action.form) { acc[action.id] = useForm({ - mode: "onChange", + mode: 'onChange', defaultValues: action.form.defaultValues || {}, - }); + }) } - return acc; - }, {}); + return acc + }, {}) const handleSpeedDialClose = (event, reason) => { - if (reason === "toggle") { - setSpeedDialOpen(false); - setIsHovering(false); - return; + if (reason === 'toggle') { + setSpeedDialOpen(false) + setIsHovering(false) + return } if (!isHovering) { setTimeout(() => { - setSpeedDialOpen(false); - }, 200); + setSpeedDialOpen(false) + }, 200) } - }; + } const handleMouseEnter = () => { - setIsHovering(true); - setSpeedDialOpen(true); - }; + setIsHovering(true) + setSpeedDialOpen(true) + } const handleMouseLeave = () => { - setIsHovering(false); - handleSpeedDialClose(); - }; + setIsHovering(false) + handleSpeedDialClose() + } const handleDialogOpen = (actionId) => { - setOpenDialogs((prev) => ({ ...prev, [actionId]: true })); - }; + setOpenDialogs((prev) => ({ ...prev, [actionId]: true })) + } const handleDialogClose = (actionId) => { - setOpenDialogs((prev) => ({ ...prev, [actionId]: false })); - }; + setOpenDialogs((prev) => ({ ...prev, [actionId]: false })) + } const handleSubmit = async (actionId, data) => { - if (!actions.find((a) => a.id === actionId)?.onSubmit) return; + if (!actions.find((a) => a.id === actionId)?.onSubmit) return - setLoading(true); + setLoading(true) try { - const action = actions.find((a) => a.id === actionId); - const result = await action.onSubmit(data); + const action = actions.find((a) => a.id === actionId) + const result = await action.onSubmit(data) if (result.success) { - formControls[actionId]?.reset(); - handleDialogClose(actionId); + formControls[actionId]?.reset() + handleDialogClose(actionId) } - setSnackbarMessage(result.message); - setShowSnackbar(true); + setSnackbarMessage(result.message) + setShowSnackbar(true) } catch (error) { - console.error(`Error submitting ${actionId}:`, error); - setSnackbarMessage("An error occurred while submitting"); - setShowSnackbar(true); + console.error(`Error submitting ${actionId}:`, error) + setSnackbarMessage('An error occurred while submitting') + setShowSnackbar(true) } finally { - setLoading(false); + setLoading(false) } - }; + } useEffect(() => { const handleClickOutside = (event) => { if (speedDialOpen) { - const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]'); + const speedDial = document.querySelector('[aria-label="Navigation SpeedDial"]') if (speedDial && !speedDial.contains(event.target)) { - setSpeedDialOpen(false); + setSpeedDialOpen(false) } } - }; + } - document.addEventListener("click", handleClickOutside); + document.addEventListener('click', handleClickOutside) return () => { - document.removeEventListener("click", handleClickOutside); - }; - }, [speedDialOpen]); + document.removeEventListener('click', handleClickOutside) + } + }, [speedDialOpen]) return ( <> @@ -115,13 +115,13 @@ const CippSpeedDial = ({ ariaLabel="Navigation SpeedDial" data-tutorial="speed-dial" sx={{ - position: "fixed", + position: 'fixed', ...position, - "& .MuiFab-primary": { + '& .MuiFab-primary': { width: 46, height: 46, - "&:hover": { - backgroundColor: "primary.dark", + '&:hover': { + backgroundColor: 'primary.dark', }, }, }} @@ -139,27 +139,27 @@ const CippSpeedDial = ({ tooltipTitle={action.name} onClick={() => { if (action.form) { - handleDialogOpen(action.id); + handleDialogOpen(action.id) } else if (action.onClick) { - action.onClick(); + action.onClick() } - setSpeedDialOpen(false); + setSpeedDialOpen(false) }} tooltipOpen sx={{ - "&.MuiSpeedDialAction-fab": { - backgroundColor: "background.paper", - "&:hover": { - backgroundColor: "action.hover", + '&.MuiSpeedDialAction-fab': { + backgroundColor: 'background.paper', + '&:hover': { + backgroundColor: 'action.hover', }, }, - "& .MuiSpeedDialAction-staticTooltipLabel": { - cursor: "pointer", - whiteSpace: "nowrap", - marginRight: "10px", - padding: "6px 10px", - "&:hover": { - backgroundColor: "action.hover", + '& .MuiSpeedDialAction-staticTooltipLabel': { + cursor: 'pointer', + whiteSpace: 'nowrap', + marginRight: '10px', + padding: '6px 10px', + '&:hover': { + backgroundColor: 'action.hover', }, }, }} @@ -184,10 +184,10 @@ const CippSpeedDial = ({ name={action.form.fieldName} required formControl={formControls[action.id]} - style={{ minHeight: "150px" }} + style={{ minHeight: '150px' }} editorProps={{ attributes: { - style: "min-height: 150px; font-size: 1.1rem; padding: 1rem;", + style: 'min-height: 150px; font-size: 1.1rem; padding: 1rem;', }, }} /> @@ -205,7 +205,7 @@ const CippSpeedDial = ({ disabled={loading} startIcon={loading ? : null} > - {loading ? "Submitting..." : action.form.submitText || "Submit"} + {loading ? 'Submitting...' : action.form.submitText || 'Submit'} @@ -215,14 +215,14 @@ const CippSpeedDial = ({ open={showSnackbar} autoHideDuration={6000} onClose={() => setShowSnackbar(false)} - anchorOrigin={{ vertical: "bottom", horizontal: "center" }} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} > - setShowSnackbar(false)} severity="success" sx={{ width: "100%" }}> + setShowSnackbar(false)} severity="success" sx={{ width: '100%' }}> {snackbarMessage} - ); -}; + ) +} -export default CippSpeedDial; +export default CippSpeedDial diff --git a/src/components/CippComponents/CippTutorialDialog.jsx b/src/components/CippComponents/CippTutorialDialog.jsx index f52c9234d6ca..016c8ba01e04 100644 --- a/src/components/CippComponents/CippTutorialDialog.jsx +++ b/src/components/CippComponents/CippTutorialDialog.jsx @@ -1,4 +1,4 @@ -import { useState, useMemo } from "react"; +import { useState, useMemo } from 'react' import { Dialog, DialogTitle, @@ -17,54 +17,52 @@ import { Divider, IconButton, Tooltip, -} from "@mui/material"; +} from '@mui/material' import { PlayArrow as PlayIcon, CheckCircle as CompletedIcon, School as TutorialIcon, Search as SearchIcon, Replay as ResetIcon, -} from "@mui/icons-material"; -import { useTutorials } from "../../contexts/tutorial-context"; -import { useRouter } from "next/router"; +} from '@mui/icons-material' +import { useTutorials } from '../../contexts/tutorial-context' +import { useRouter } from 'next/router' const CippTutorialDialog = ({ open, onClose }) => { - const { tutorials, completedIds, startTutorial, resetProgress } = useTutorials(); - const [search, setSearch] = useState(""); - const router = useRouter(); + const { tutorials, completedIds, startTutorial, resetProgress } = useTutorials() + const [search, setSearch] = useState('') + const router = useRouter() const grouped = useMemo(() => { const filtered = tutorials.filter((t) => { - const q = search.toLowerCase(); + const q = search.toLowerCase() return ( t.title.toLowerCase().includes(q) || t.description?.toLowerCase().includes(q) || t.category?.toLowerCase().includes(q) - ); - }); + ) + }) return filtered.reduce((acc, tutorial) => { - const cat = tutorial.category || "General"; - if (!acc[cat]) acc[cat] = []; - acc[cat].push(tutorial); - return acc; - }, {}); - }, [tutorials, search]); + const cat = tutorial.category || 'General' + if (!acc[cat]) acc[cat] = [] + acc[cat].push(tutorial) + return acc + }, {}) + }, [tutorials, search]) const handleStart = (tutorial) => { - onClose(); + onClose() // Small delay to let dialog close animation finish - setTimeout(() => startTutorial(tutorial), 300); - }; + setTimeout(() => startTutorial(tutorial), 300) + } - const categoryKeys = Object.keys(grouped).sort(); + const categoryKeys = Object.keys(grouped).sort() return ( - - + + Tutorials @@ -94,7 +92,7 @@ const CippTutorialDialog = ({ open, onClose }) => { /> {categoryKeys.length === 0 && ( - + No tutorials found. )} @@ -106,9 +104,8 @@ const CippTutorialDialog = ({ open, onClose }) => { {grouped[category].map((tutorial) => { - const isCompleted = completedIds.includes(tutorial.id); - const isOnPage = - !tutorial.pages?.length || tutorial.pages.includes(router.pathname); + const isCompleted = completedIds.includes(tutorial.id) + const isOnPage = !tutorial.pages?.length || tutorial.pages.includes(router.pathname) return ( { primary={tutorial.title} secondary={tutorial.description} slotProps={{ - primary: { variant: "body2", fontWeight: 500 }, - secondary: { variant: "caption" }, + primary: { variant: 'body2', fontWeight: 500 }, + secondary: { variant: 'caption' }, }} /> - - {isCompleted && } - {!isOnPage && ( - + + {isCompleted && ( + )} + {!isOnPage && } { /> - ); + ) })} @@ -151,13 +148,13 @@ const CippTutorialDialog = ({ open, onClose }) => { ))} - + {completedIds.length} of {tutorials.length} completed - ); -}; + ) +} -export default CippTutorialDialog; +export default CippTutorialDialog diff --git a/src/contexts/tutorial-context.js b/src/contexts/tutorial-context.js index 954b641d8b0b..749f645aa15f 100644 --- a/src/contexts/tutorial-context.js +++ b/src/contexts/tutorial-context.js @@ -1,37 +1,37 @@ -import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { driver } from "driver.js"; -import { useRouter } from "next/router"; +import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { driver } from 'driver.js' +import { useRouter } from 'next/router' -const STORAGE_KEY = "cipp.tutorials.completed"; +const STORAGE_KEY = 'cipp.tutorials.completed' const getCompletedTutorials = () => { try { - const stored = localStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : []; + const stored = localStorage.getItem(STORAGE_KEY) + return stored ? JSON.parse(stored) : [] } catch { - return []; + return [] } -}; +} const storeCompletedTutorial = (id) => { try { - const completed = getCompletedTutorials(); + const completed = getCompletedTutorials() if (!completed.includes(id)) { - completed.push(id); - localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)); + completed.push(id) + localStorage.setItem(STORAGE_KEY, JSON.stringify(completed)) } } catch { // ignore } -}; +} const resetCompletedTutorials = () => { try { - localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem(STORAGE_KEY) } catch { // ignore } -}; +} const TutorialContext = createContext({ tutorials: [], @@ -40,116 +40,114 @@ const TutorialContext = createContext({ startTutorial: () => {}, resetProgress: () => {}, getTutorialsForPage: () => [], -}); +}) // Load all tutorial JSON files from the data/tutorials folder at build time const loadTutorials = () => { - const context = require.context("../data/tutorials", false, /\.json$/); + const context = require.context('../data/tutorials', false, /\.json$/) return context.keys().map((key) => { - const tutorial = context(key); - return tutorial.default || tutorial; - }); -}; + const tutorial = context(key) + return tutorial.default || tutorial + }) +} export const TutorialProvider = ({ children }) => { - const [tutorials] = useState(() => loadTutorials()); - const [completedIds, setCompletedIds] = useState([]); - const [activeTutorial, setActiveTutorial] = useState(null); - const driverRef = useRef(null); - const router = useRouter(); + const [tutorials] = useState(() => loadTutorials()) + const [completedIds, setCompletedIds] = useState([]) + const [activeTutorial, setActiveTutorial] = useState(null) + const driverRef = useRef(null) + const router = useRouter() useEffect(() => { - setCompletedIds(getCompletedTutorials()); - }, []); + setCompletedIds(getCompletedTutorials()) + }, []) // Launch tutorial from ?tutorial=$id query param useEffect(() => { - if (!router.isReady || activeTutorial) return; - const tutorialId = router.query.tutorial; - if (!tutorialId) return; + if (!router.isReady || activeTutorial) return + const tutorialId = router.query.tutorial + if (!tutorialId) return - const match = tutorials.find((t) => t.id === tutorialId); - if (!match) return; + const match = tutorials.find((t) => t.id === tutorialId) + if (!match) return // Strip the query param so it doesn't re-trigger - const { tutorial: _, ...rest } = router.query; - router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }); + const { tutorial: _, ...rest } = router.query + router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }) // Delay to let the page fully render - setTimeout(() => runDriver(match), 600); - }, [router.isReady, router.query.tutorial, tutorials]); + setTimeout(() => runDriver(match), 600) + }, [router.isReady, router.query.tutorial, tutorials]) // Cleanup driver on unmount or route change useEffect(() => { return () => { if (driverRef.current) { - driverRef.current.destroy(); - driverRef.current = null; + driverRef.current.destroy() + driverRef.current = null } - }; - }, []); + } + }, []) const startTutorial = useCallback( (tutorial) => { if (driverRef.current) { - driverRef.current.destroy(); + driverRef.current.destroy() } // If tutorial specifies pages and we're not on any of them, navigate first if (tutorial.pages?.length && !tutorial.pages.includes(router.pathname)) { router.push(tutorial.pages[0]).then(() => { // Small delay to let the page render before starting the tour - setTimeout(() => runDriver(tutorial), 500); - }); - return; + setTimeout(() => runDriver(tutorial), 500) + }) + return } - runDriver(tutorial); + runDriver(tutorial) }, [router] - ); + ) const runDriver = useCallback((tutorial) => { - setActiveTutorial(tutorial); + setActiveTutorial(tutorial) const driverObj = driver({ showProgress: true, animate: true, allowClose: true, - overlayColor: "rgba(0, 0, 0, 0.6)", + overlayColor: 'rgba(0, 0, 0, 0.6)', stagePadding: 8, stageRadius: 8, - popoverClass: "cipp-tutorial-popover", - nextBtnText: "Next →", - prevBtnText: "← Back", - doneBtnText: "Done ✓", - progressText: "{{current}} of {{total}}", + popoverClass: 'cipp-tutorial-popover', + nextBtnText: 'Next →', + prevBtnText: '← Back', + doneBtnText: 'Done ✓', + progressText: '{{current}} of {{total}}', steps: tutorial.steps, onDestroyed: () => { - storeCompletedTutorial(tutorial.id); - setCompletedIds(getCompletedTutorials()); - setActiveTutorial(null); - driverRef.current = null; + storeCompletedTutorial(tutorial.id) + setCompletedIds(getCompletedTutorials()) + setActiveTutorial(null) + driverRef.current = null }, - }); + }) - driverRef.current = driverObj; - driverObj.drive(); - }, []); + driverRef.current = driverObj + driverObj.drive() + }, []) const resetProgress = useCallback(() => { - resetCompletedTutorials(); - setCompletedIds([]); - }, []); + resetCompletedTutorials() + setCompletedIds([]) + }, []) const getTutorialsForPage = useCallback( (pathname) => { - return tutorials.filter( - (t) => !t.pages || t.pages.length === 0 || t.pages.includes(pathname) - ); + return tutorials.filter((t) => !t.pages || t.pages.length === 0 || t.pages.includes(pathname)) }, [tutorials] - ); + ) const value = useMemo( () => ({ @@ -161,9 +159,9 @@ export const TutorialProvider = ({ children }) => { getTutorialsForPage, }), [tutorials, activeTutorial, completedIds, startTutorial, resetProgress, getTutorialsForPage] - ); + ) - return {children}; -}; + return {children} +} -export const useTutorials = () => useContext(TutorialContext); +export const useTutorials = () => useContext(TutorialContext) diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index 2c7e0a2507ad..baa5d1ad9f9f 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -279,7 +279,11 @@ export const TopNav = (props) => { {!mdDown && ( - + )} {mdDown && ( diff --git a/src/pages/dashboardv2/index.js b/src/pages/dashboardv2/index.js index dfd9d9a76528..48e3bd3b3b62 100644 --- a/src/pages/dashboardv2/index.js +++ b/src/pages/dashboardv2/index.js @@ -194,7 +194,10 @@ const Page = () => { - + Date: Mon, 25 May 2026 20:37:52 +0200 Subject: [PATCH 046/133] demo data --- src/contexts/tutorial-context.js | 10 +++ src/data/dashboardv2-demo-data.js | 138 +++++++++++++++--------------- 2 files changed, 79 insertions(+), 69 deletions(-) diff --git a/src/contexts/tutorial-context.js b/src/contexts/tutorial-context.js index 749f645aa15f..b32ab50c8fa8 100644 --- a/src/contexts/tutorial-context.js +++ b/src/contexts/tutorial-context.js @@ -73,6 +73,16 @@ export const TutorialProvider = ({ children }) => { // Strip the query param so it doesn't re-trigger const { tutorial: _, ...rest } = router.query + + // If the tutorial has a target page and we're not on it, navigate there first + const targetPage = match.pages?.[0] + if (targetPage && router.pathname !== targetPage) { + router.replace({ pathname: targetPage, query: rest }, undefined).then(() => { + setTimeout(() => runDriver(match), 600) + }) + return + } + router.replace({ pathname: router.pathname, query: rest }, undefined, { shallow: true }) // Delay to let the page fully render diff --git a/src/data/dashboardv2-demo-data.js b/src/data/dashboardv2-demo-data.js index e5e23eee579a..4f0db4c12aad 100644 --- a/src/data/dashboardv2-demo-data.js +++ b/src/data/dashboardv2-demo-data.js @@ -1,8 +1,8 @@ // Demo data structure matching Zero Trust Assessment export const dashboardDemoData = { - ExecutedAt: "2025-12-16T10:00:00Z", - TenantName: "Demo Tenant", - Domain: "demo.contoso.com", + ExecutedAt: '2025-12-16T10:00:00Z', + TenantName: 'Demo Tenant', + Domain: 'demo.contoso.com', TestResultSummary: { IdentityPassed: 85, IdentityTotal: 100, @@ -13,100 +13,100 @@ export const dashboardDemoData = { }, TenantInfo: { TenantOverview: { - UserCount: 1250, - GuestCount: 85, - GroupCount: 340, - ApplicationCount: 156, - DeviceCount: 765, - ManagedDeviceCount: 733, + UserCount: 0, + GuestCount: 0, + GroupCount: 0, + ApplicationCount: 0, + DeviceCount: 0, + ManagedDeviceCount: 0, }, OverviewCaMfaAllUsers: { description: - "Over the past 30 days, 68.5% of sign-ins were protected by conditional access policies enforcing multifactor authentication.", + 'Over the past 30 days, 68.5% of sign-ins were protected by conditional access policies enforcing multifactor authentication.', nodes: [ - { source: "User sign in", target: "No CA applied", value: 394 }, - { source: "User sign in", target: "CA applied", value: 856 }, - { source: "CA applied", target: "No MFA", value: 146 }, - { source: "CA applied", target: "MFA", value: 710 }, + { source: 'User sign in', target: 'No CA applied', value: 394 }, + { source: 'User sign in', target: 'CA applied', value: 856 }, + { source: 'CA applied', target: 'No MFA', value: 146 }, + { source: 'CA applied', target: 'MFA', value: 710 }, ], }, OverviewCaDevicesAllUsers: { - description: "Over the past 30 days, 71.2% of sign-ins were from compliant devices.", + description: 'Over the past 30 days, 71.2% of sign-ins were from compliant devices.', nodes: [ - { source: "User sign in", target: "Unmanaged", value: 500 }, - { source: "User sign in", target: "Managed", value: 1150 }, - { source: "Managed", target: "Non-compliant", value: 260 }, - { source: "Managed", target: "Compliant", value: 890 }, + { source: 'User sign in', target: 'Unmanaged', value: 500 }, + { source: 'User sign in', target: 'Managed', value: 1150 }, + { source: 'Managed', target: 'Non-compliant', value: 260 }, + { source: 'Managed', target: 'Compliant', value: 890 }, ], }, OverviewAuthMethodsPrivilegedUsers: { - description: "Authentication methods used by privileged users over the past 30 days.", + description: 'Authentication methods used by privileged users over the past 30 days.', nodes: [ - { source: "Users", target: "Single factor", value: 5 }, - { source: "Users", target: "Phishable", value: 28 }, - { source: "Users", target: "Phish resistant", value: 15 }, - { source: "Phishable", target: "Phone", value: 8 }, - { source: "Phishable", target: "Authenticator", value: 20 }, - { source: "Phish resistant", target: "Passkey", value: 12 }, - { source: "Phish resistant", target: "WHfB", value: 3 }, + { source: 'Users', target: 'Single factor', value: 5 }, + { source: 'Users', target: 'Phishable', value: 28 }, + { source: 'Users', target: 'Phish resistant', value: 15 }, + { source: 'Phishable', target: 'Phone', value: 8 }, + { source: 'Phishable', target: 'Authenticator', value: 20 }, + { source: 'Phish resistant', target: 'Passkey', value: 12 }, + { source: 'Phish resistant', target: 'WHfB', value: 3 }, ], }, OverviewAuthMethodsAllUsers: { - description: "Authentication methods used by all users over the past 30 days.", + description: 'Authentication methods used by all users over the past 30 days.', nodes: [ - { source: "Users", target: "Single factor", value: 120 }, - { source: "Users", target: "Phishable", value: 580 }, - { source: "Users", target: "Phish resistant", value: 550 }, - { source: "Phishable", target: "Phone", value: 180 }, - { source: "Phishable", target: "Authenticator", value: 400 }, - { source: "Phish resistant", target: "Passkey", value: 450 }, - { source: "Phish resistant", target: "WHfB", value: 100 }, + { source: 'Users', target: 'Single factor', value: 120 }, + { source: 'Users', target: 'Phishable', value: 580 }, + { source: 'Users', target: 'Phish resistant', value: 550 }, + { source: 'Phishable', target: 'Phone', value: 180 }, + { source: 'Phishable', target: 'Authenticator', value: 400 }, + { source: 'Phish resistant', target: 'Passkey', value: 450 }, + { source: 'Phish resistant', target: 'WHfB', value: 100 }, ], }, DeviceOverview: { DesktopDevicesSummary: { - description: "Desktop devices (Windows and macOS) by join type and compliance status.", + description: 'Desktop devices (Windows and macOS) by join type and compliance status.', nodes: [ // Level 1: Desktop devices to OS - { source: "Desktop devices", target: "Windows", value: 585 }, - { source: "Desktop devices", target: "macOS", value: 75 }, + { source: 'Desktop devices', target: 'Windows', value: 585 }, + { source: 'Desktop devices', target: 'macOS', value: 75 }, // Level 2: Windows to join types - { source: "Windows", target: "Entra joined", value: 285 }, - { source: "Windows", target: "Entra registered", value: 100 }, - { source: "Windows", target: "Entra hybrid joined", value: 200 }, + { source: 'Windows', target: 'Entra joined', value: 285 }, + { source: 'Windows', target: 'Entra registered', value: 100 }, + { source: 'Windows', target: 'Entra hybrid joined', value: 200 }, // Level 3: Windows join types to compliance - { source: "Entra joined", target: "Compliant", value: 171 }, - { source: "Entra joined", target: "Non-compliant", value: 42 }, - { source: "Entra joined", target: "Unmanaged", value: 72 }, - { source: "Entra hybrid joined", target: "Compliant", value: 50 }, - { source: "Entra hybrid joined", target: "Non-compliant", value: 23 }, - { source: "Entra hybrid joined", target: "Unmanaged", value: 127 }, - { source: "Entra registered", target: "Compliant", value: 60 }, - { source: "Entra registered", target: "Non-compliant", value: 40 }, - { source: "Entra registered", target: "Unmanaged", value: 0 }, + { source: 'Entra joined', target: 'Compliant', value: 171 }, + { source: 'Entra joined', target: 'Non-compliant', value: 42 }, + { source: 'Entra joined', target: 'Unmanaged', value: 72 }, + { source: 'Entra hybrid joined', target: 'Compliant', value: 50 }, + { source: 'Entra hybrid joined', target: 'Non-compliant', value: 23 }, + { source: 'Entra hybrid joined', target: 'Unmanaged', value: 127 }, + { source: 'Entra registered', target: 'Compliant', value: 60 }, + { source: 'Entra registered', target: 'Non-compliant', value: 40 }, + { source: 'Entra registered', target: 'Unmanaged', value: 0 }, // Level 2: macOS directly to compliance - { source: "macOS", target: "Compliant", value: 56 }, - { source: "macOS", target: "Non-compliant", value: 15 }, - { source: "macOS", target: "Unmanaged", value: 4 }, + { source: 'macOS', target: 'Compliant', value: 56 }, + { source: 'macOS', target: 'Non-compliant', value: 15 }, + { source: 'macOS', target: 'Unmanaged', value: 4 }, ], }, MobileSummary: { - description: "Mobile devices by compliance status.", + description: 'Mobile devices by compliance status.', nodes: [ - { source: "Mobile devices", target: "Android", value: 105 }, - { source: "Mobile devices", target: "iOS", value: 75 }, - { source: "Android", target: "Android (Company)", value: 72 }, - { source: "Android", target: "Android (Personal)", value: 33 }, - { source: "iOS", target: "iOS (Company)", value: 58 }, - { source: "iOS", target: "iOS (Personal)", value: 17 }, - { source: "Android (Company)", target: "Compliant", value: 60 }, - { source: "Android (Company)", target: "Non-compliant", value: 12 }, - { source: "Android (Personal)", target: "Compliant", value: 10 }, - { source: "Android (Personal)", target: "Non-compliant", value: 23 }, - { source: "iOS (Company)", target: "Compliant", value: 52 }, - { source: "iOS (Company)", target: "Non-compliant", value: 6 }, - { source: "iOS (Personal)", target: "Compliant", value: 11 }, - { source: "iOS (Personal)", target: "Non-compliant", value: 6 }, + { source: 'Mobile devices', target: 'Android', value: 105 }, + { source: 'Mobile devices', target: 'iOS', value: 75 }, + { source: 'Android', target: 'Android (Company)', value: 72 }, + { source: 'Android', target: 'Android (Personal)', value: 33 }, + { source: 'iOS', target: 'iOS (Company)', value: 58 }, + { source: 'iOS', target: 'iOS (Personal)', value: 17 }, + { source: 'Android (Company)', target: 'Compliant', value: 60 }, + { source: 'Android (Company)', target: 'Non-compliant', value: 12 }, + { source: 'Android (Personal)', target: 'Compliant', value: 10 }, + { source: 'Android (Personal)', target: 'Non-compliant', value: 23 }, + { source: 'iOS (Company)', target: 'Compliant', value: 52 }, + { source: 'iOS (Company)', target: 'Non-compliant', value: 6 }, + { source: 'iOS (Personal)', target: 'Compliant', value: 11 }, + { source: 'iOS (Personal)', target: 'Non-compliant', value: 6 }, ], }, ManagedDevices: { @@ -128,4 +128,4 @@ export const dashboardDemoData = { }, }, }, -}; +} From a2d8f1918f67ed864a57e50e55f8fd3f949774a9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 25 May 2026 21:32:56 +0200 Subject: [PATCH 047/133] react-dom --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 842abc453ddd..331476b49fa7 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "react": "19.2.6", "react-apexcharts": "2.1.0", "react-beautiful-dnd": "13.1.1", - "react-dom": "19.2.5", + "react-dom": "19.2.6", "react-dropzone": "15.0.0", "react-error-boundary": "^6.1.1", "react-hook-form": "^7.72.0", From 7d1c2096f820f505684167d28bf38487534bd5ff Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 23:09:50 +0000 Subject: [PATCH 048/133] Move EnrollmentProfileTabs from pages to components and update imports Agent-Logs-Url: https://github.com/KelvinTegelaar/CIPP/sessions/bc416d42-34cd-460e-b5cb-57aec78b6c58 Co-authored-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- .../EnrollmentProfileTabs.jsx | 18 +++++++++--------- .../enrollment-profiles/android-enterprise.js | 2 +- .../MEM/enrollment-profiles/apple-ade.js | 2 +- .../endpoint/MEM/enrollment-profiles/index.js | 2 +- .../enrollment-profiles/windows-autopilot.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/{pages/endpoint/MEM/enrollment-profiles => components}/EnrollmentProfileTabs.jsx (95%) diff --git a/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx b/src/components/EnrollmentProfileTabs.jsx similarity index 95% rename from src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx rename to src/components/EnrollmentProfileTabs.jsx index 73fd35519bd0..882b73b04d9e 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/EnrollmentProfileTabs.jsx +++ b/src/components/EnrollmentProfileTabs.jsx @@ -19,15 +19,15 @@ import { QrCode2, Sync, } from '@mui/icons-material' -import { CippHead } from '../../../../components/CippComponents/CippHead.jsx' -import { CippDataTable } from '../../../../components/CippTable/CippDataTable.js' -import { CippInfoBar } from '../../../../components/CippCards/CippInfoBar.jsx' -import { CippApiDialog } from '../../../../components/CippComponents/CippApiDialog.jsx' -import { CippAutopilotProfileDrawer } from '../../../../components/CippComponents/CippAutopilotProfileDrawer.jsx' -import CippJsonView from '../../../../components/CippFormPages/CippJSONView.jsx' -import { ApiGetCall } from '../../../../api/ApiCall.jsx' -import { useDialog } from '../../../../hooks/use-dialog.js' -import { useSettings } from '../../../../hooks/use-settings.js' +import { CippHead } from './CippComponents/CippHead.jsx' +import { CippDataTable } from './CippTable/CippDataTable.js' +import { CippInfoBar } from './CippCards/CippInfoBar.jsx' +import { CippApiDialog } from './CippComponents/CippApiDialog.jsx' +import { CippAutopilotProfileDrawer } from './CippComponents/CippAutopilotProfileDrawer.jsx' +import CippJsonView from './CippFormPages/CippJSONView.jsx' +import { ApiGetCall } from '../api/ApiCall.jsx' +import { useDialog } from '../hooks/use-dialog.js' +import { useSettings } from '../hooks/use-settings.js' const pageTitle = 'Enrollment Profiles' const appleADEPageTitle = 'Apple Enrollment Profiles' diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js index 826afd75f085..58245f4a548a 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AndroidEnterpriseEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js index e6fc7bfad50d..4f4aed94df6f 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AppleADEEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { AppleADEEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/MEM/enrollment-profiles/index.js index 4c072ed7302d..ec561612656d 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/index.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js index 4c072ed7302d..ec561612656d 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from './EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => From 02c7a4341ca6d9ceb016ed45d10f0e188bce6a26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 23:13:28 +0000 Subject: [PATCH 049/133] Move EnrollmentProfileTabs to CippComponents folder and update imports Agent-Logs-Url: https://github.com/KelvinTegelaar/CIPP/sessions/08f68e6a-d4d1-481c-ba38-76d0ef9af438 Co-authored-by: Zacgoose <107489668+Zacgoose@users.noreply.github.com> --- .../EnrollmentProfileTabs.jsx | 18 +++++++++--------- .../enrollment-profiles/android-enterprise.js | 2 +- .../MEM/enrollment-profiles/apple-ade.js | 2 +- .../endpoint/MEM/enrollment-profiles/index.js | 2 +- .../enrollment-profiles/windows-autopilot.js | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) rename src/components/{ => CippComponents}/EnrollmentProfileTabs.jsx (96%) diff --git a/src/components/EnrollmentProfileTabs.jsx b/src/components/CippComponents/EnrollmentProfileTabs.jsx similarity index 96% rename from src/components/EnrollmentProfileTabs.jsx rename to src/components/CippComponents/EnrollmentProfileTabs.jsx index 882b73b04d9e..4499d1c94962 100644 --- a/src/components/EnrollmentProfileTabs.jsx +++ b/src/components/CippComponents/EnrollmentProfileTabs.jsx @@ -19,15 +19,15 @@ import { QrCode2, Sync, } from '@mui/icons-material' -import { CippHead } from './CippComponents/CippHead.jsx' -import { CippDataTable } from './CippTable/CippDataTable.js' -import { CippInfoBar } from './CippCards/CippInfoBar.jsx' -import { CippApiDialog } from './CippComponents/CippApiDialog.jsx' -import { CippAutopilotProfileDrawer } from './CippComponents/CippAutopilotProfileDrawer.jsx' -import CippJsonView from './CippFormPages/CippJSONView.jsx' -import { ApiGetCall } from '../api/ApiCall.jsx' -import { useDialog } from '../hooks/use-dialog.js' -import { useSettings } from '../hooks/use-settings.js' +import { CippHead } from './CippHead.jsx' +import { CippDataTable } from '../CippTable/CippDataTable.js' +import { CippInfoBar } from '../CippCards/CippInfoBar.jsx' +import { CippApiDialog } from './CippApiDialog.jsx' +import { CippAutopilotProfileDrawer } from './CippAutopilotProfileDrawer.jsx' +import CippJsonView from '../CippFormPages/CippJSONView.jsx' +import { ApiGetCall } from '../../api/ApiCall.jsx' +import { useDialog } from '../../hooks/use-dialog.js' +import { useSettings } from '../../hooks/use-settings.js' const pageTitle = 'Enrollment Profiles' const appleADEPageTitle = 'Apple Enrollment Profiles' diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js index 58245f4a548a..88f86700374a 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js index 4f4aed94df6f..2225de59f0a8 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { AppleADEEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { AppleADEEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/MEM/enrollment-profiles/index.js index ec561612656d..a2cf307e80d2 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/index.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/index.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js index ec561612656d..a2cf307e80d2 100644 --- a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js +++ b/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js @@ -1,6 +1,6 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' import { TabbedLayout } from '../../../../layouts/TabbedLayout' -import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/EnrollmentProfileTabs.jsx' +import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' const Page = () => From 1e59d2d43fab463a709d1dac2477f45cfacdfae6 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 11:54:57 +0800 Subject: [PATCH 050/133] Update ListTests.json --- Tests/Shapes/ListTests.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/Shapes/ListTests.json b/Tests/Shapes/ListTests.json index a4553bd14c70..e79c73d8eb9f 100644 --- a/Tests/Shapes/ListTests.json +++ b/Tests/Shapes/ListTests.json @@ -119,6 +119,7 @@ "ExoSafeLinksRules": "number", "ExoSharingPolicy": "number", "ExoTenantAllowBlockList": "number", + "ExoTransportConfig": "number", "ExoTransportRules": "number", "Groups": "number", "Guests": "number", From 1cd1ef7223672170bdce1fffe88d8bb4ddb903d9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 12:30:37 +0800 Subject: [PATCH 051/133] Update AuditLogTemplates.json --- src/data/AuditLogTemplates.json | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/data/AuditLogTemplates.json b/src/data/AuditLogTemplates.json index 63df852bd318..68f87b52bdf6 100644 --- a/src/data/AuditLogTemplates.json +++ b/src/data/AuditLogTemplates.json @@ -439,18 +439,10 @@ "label": "updated user" } }, - { - "Property": { "value": "String", "label": "SecuredAccessPassData" }, - "Operator": { "value": "ne", "label": "Not Equals to" }, - "Input": { - "value": "[]", - "label": "[]" - } - }, { "Property": { "value": "String", "label": "SecuredAccessPassData" }, "Operator": { "value": "like", "label": "Like" }, - "Input": { "value": "[*]" } + "Input": { "value": "*PassId*" } } ] } From ca150a28cfcb3922d6de42cb4db54f2afcdf6e0e Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:08:56 +0800 Subject: [PATCH 052/133] Better display standards that are missing licenses to be able to work --- src/pages/tenant/manage/applied-standards.js | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 376507ec4017..d69b3aebc81c 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -36,6 +36,7 @@ import { Construction, Schedule, Check, + Warning, } from '@mui/icons-material' import standards from '../../../data/standards.json' import { CippApiDialog } from '../../../components/CippComponents/CippApiDialog' @@ -248,6 +249,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -372,6 +374,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -504,6 +507,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -619,6 +623,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, // Use the template settings object instead of true @@ -727,6 +732,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: { displayName }, @@ -867,6 +873,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: templateSettings, @@ -1035,6 +1042,11 @@ const Page = () => { } } + // If the tenant is missing the required license, treat as compliant + if (standardObject?.LicenseAvailable === false) { + isCompliant = true + } + // Determine compliance status text based on reporting flag const complianceStatus = reportingDisabled ? 'Reporting Disabled' @@ -1061,6 +1073,7 @@ const Page = () => { TemplateId: tenantTemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, } : currentTenantStandard?.value, standardValue: standardSettings, @@ -1155,6 +1168,7 @@ const Page = () => { TemplateId: standardObject?.TemplateId, CurrentValue: standardObject?.CurrentValue, ExpectedValue: standardObject?.ExpectedValue, + LicenseAvailable: standardObject?.LicenseAvailable, }, standardValue: { templateId: itemTemplateId, @@ -1969,6 +1983,15 @@ const Page = () => { + {standard.currentTenantValue?.LicenseAvailable === false ? ( + }> + {typeof standard.currentTenantValue?.Value === 'string' && + standard.currentTenantValue.Value.startsWith('License Missing:') + ? standard.currentTenantValue.Value + : 'This tenant does not have the required licenses for this standard'} + + ) : ( + <> {/* Show Expected Configuration with property-by-property breakdown */} {standard.currentTenantValue?.ExpectedValue !== undefined ? ( @@ -2073,6 +2096,8 @@ const Page = () => { sx={{ mr: 1 }} /> + + )}
@@ -2170,6 +2195,15 @@ const Page = () => { + {standard.currentTenantValue?.LicenseAvailable === false ? ( + }> + {typeof standard.currentTenantValue?.Value === 'string' && + standard.currentTenantValue.Value.startsWith('License Missing:') + ? standard.currentTenantValue.Value + : 'This tenant does not have the required licenses for this standard'} + + ) : ( + <> {/* Existing tenant comparison content */} {typeof standard.currentTenantValue?.Value === 'object' && standard.currentTenantValue?.Value !== null ? ( @@ -2931,6 +2965,8 @@ const Page = () => { )} )} + + )} From f3c8a79e42e97ad5b0cad9889ce15e4863f6bef8 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 15:09:04 +0800 Subject: [PATCH 053/133] Update yarn.lock --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6ffd82077dd1..f679e9ff64b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6558,10 +6558,10 @@ react-colorful@^5.6.1: resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== -react-dom@19.2.5: - version "19.2.5" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.5.tgz#b8768b10837d0b8e9ca5b9e2d58dff3d880ea25e" - integrity sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag== +react-dom@19.2.6: + version "19.2.6" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.6.tgz#44a81b0bcca22da814c00847d09d01c8615529b7" + integrity sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g== dependencies: scheduler "^0.27.0" From d28e8ebfaa9517e5f2134bb1c339d294096dca91 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 26 May 2026 23:33:43 +0800 Subject: [PATCH 054/133] user sync --- .../CippSettings/CippRoleAddEdit.jsx | 7 + .../CippSettings/CippUserManagement.jsx | 163 +++++++++++++++--- .../cipp/advanced/super-admin/cipp-users.js | 11 +- 3 files changed, 148 insertions(+), 33 deletions(-) diff --git a/src/components/CippSettings/CippRoleAddEdit.jsx b/src/components/CippSettings/CippRoleAddEdit.jsx index 757215cd0f49..75192d394004 100644 --- a/src/components/CippSettings/CippRoleAddEdit.jsx +++ b/src/components/CippSettings/CippRoleAddEdit.jsx @@ -41,6 +41,13 @@ export const CippRoleAddEdit = ({ selectedRole }) => { const formControl = useForm({ mode: "onChange", + defaultValues: { + allowedTenants: [], + blockedTenants: [], + BlockedEndpoints: [], + IPRange: [], + Permissions: {}, + }, }); const formState = useFormState({ control: formControl.control }); diff --git a/src/components/CippSettings/CippUserManagement.jsx b/src/components/CippSettings/CippUserManagement.jsx index ab4be74c1b2b..b1d84c2f0b70 100644 --- a/src/components/CippSettings/CippUserManagement.jsx +++ b/src/components/CippSettings/CippUserManagement.jsx @@ -26,26 +26,30 @@ import { ApiGetCall, ApiPostCall } from "../../api/ApiCall"; export const CippUserManagement = () => { const [dialogOpen, setDialogOpen] = useState(false); const [editingUser, setEditingUser] = useState(null); + const [bulkEditUsers, setBulkEditUsers] = useState(null); const formControl = useForm({ mode: "onChange", defaultValues: { UPN: "", Roles: [] }, }); - const rolesQuery = ApiGetCall({ - url: "/api/ListCustomRole", - queryKey: "customRoleList", + const usersQuery = ApiGetCall({ + url: "/api/ListCIPPUsers", + queryKey: "cippUsersList", }); const userAction = ApiPostCall({ relatedQueryKeys: ["cippUsersList"], }); - const allRoles = Array.isArray(rolesQuery.data) ? rolesQuery.data : []; + const pageData = usersQuery.data?.pages?.[0] ?? usersQuery.data ?? {}; + const allRoles = Array.isArray(pageData.AvailableRoles) ? pageData.AvailableRoles : []; const roleOptions = allRoles.map((r) => ({ label: `${r.RoleName} (${r.Type})`, value: r.RoleName, })); + const existingUsers = Array.isArray(pageData.Users) ? pageData.Users : []; + const existingUpns = new Set(existingUsers.map((u) => u.UPN.toLowerCase())); const openAddDialog = () => { setEditingUser(null); @@ -55,33 +59,36 @@ export const CippUserManagement = () => { const openEditDialog = (row) => { setEditingUser(row); - const currentRoles = (row.Roles ?? []).map((r) => { + // Show only manual roles for editing — auto roles are managed by sync + const editableRoles = (row.ManualRoles ?? row.Roles ?? []).map((r) => { const match = roleOptions.find((opt) => opt.value === r); return match ?? { label: r, value: r }; }); - formControl.reset({ UPN: row.UPN, Roles: currentRoles }); + formControl.reset({ UPN: row.UPN, Roles: editableRoles }); setDialogOpen(true); }; const handleSaveUser = (data) => { const roles = Array.isArray(data.Roles) ? data.Roles.map((r) => r.value ?? r) : [data.Roles]; - userAction.mutate( - { + + // Bulk edit: apply same roles to all selected users + const upns = bulkEditUsers ? bulkEditUsers.map((u) => u.UPN) : [data.UPN]; + + upns.forEach((upn) => { + userAction.mutate({ url: "/api/ExecCIPPUsers", data: { Action: "AddUpdate", - UPN: data.UPN, + UPN: upn, Roles: roles, }, - }, - { - onSuccess: () => { - formControl.reset({ UPN: "", Roles: [] }); - setEditingUser(null); - setDialogOpen(false); - }, - } - ); + }); + }); + + formControl.reset({ UPN: "", Roles: [] }); + setEditingUser(null); + setBulkEditUsers(null); + setDialogOpen(false); }; const actions = [ @@ -94,6 +101,12 @@ export const CippUserManagement = () => { ), noConfirm: true, customFunction: (row) => openEditDialog(row), + customBulkHandler: ({ data, clearSelection }) => { + setBulkEditUsers(data); + setEditingUser(null); + formControl.reset({ UPN: "", Roles: [] }); + setDialogOpen(true); + }, }, { label: "Delete User", @@ -113,6 +126,19 @@ export const CippUserManagement = () => { }, ]; + const sourceLabel = (source) => { + switch (source) { + case "Auto": + return "Auto-synced from Entra groups"; + case "Both": + return "Auto-synced + Manual"; + case "Manual": + return "Manually assigned"; + default: + return source || "—"; + } + }; + const offCanvas = { children: (row) => ( @@ -123,9 +149,16 @@ export const CippUserManagement = () => { {row.UPN} + + + Source + + {sourceLabel(row.Source)} + + - Assigned Roles + Effective Roles {(row.Roles ?? []).map((role, idx) => ( @@ -133,6 +166,49 @@ export const CippUserManagement = () => { ))} + {(row.ManualRoles ?? []).length > 0 && ( + <> + + + + Manual Roles + + + {row.ManualRoles.map((role, idx) => ( + + ))} + + + + )} + {(row.AutoRoles ?? []).length > 0 && ( + <> + + + + Auto Roles (from Entra groups) + + + {row.AutoRoles.map((role, idx) => ( + + ))} + + + + )} + {row.LastSync && ( + <> + + + + Last Synced + + + {new Date(row.LastSync).toLocaleString()} + + + + )} ), }; @@ -161,7 +237,7 @@ export const CippUserManagement = () => { dataKey: "Users", }} queryKey="cippUsersList" - simpleColumns={["UPN", "Roles"]} + simpleColumns={["UPN", "Roles", "Source"]} offCanvas={offCanvas} /> @@ -169,26 +245,57 @@ export const CippUserManagement = () => { setDialogOpen(false)} + onClose={() => { + setDialogOpen(false); + setBulkEditUsers(null); + }} maxWidth="sm" fullWidth > - {editingUser ? `Edit Roles — ${editingUser.UPN}` : "Add CIPP User"} + + {bulkEditUsers + ? `Bulk Edit Roles — ${bulkEditUsers.length} users` + : editingUser + ? `Edit Roles — ${editingUser.UPN}` + : "Add CIPP User"} + - {editingUser - ? "Update the roles assigned to this user." - : "Add a user by their email address (UPN) and assign one or more roles. If the user already exists, their roles will be updated."} + {bulkEditUsers + ? `Set the manual roles for ${bulkEditUsers.length} selected users. This will replace their existing manual roles. Auto-synced roles from Entra groups will not be affected.` + : editingUser + ? "Update the manually assigned roles for this user. Auto-synced roles from Entra groups are managed separately and will not be affected." + : "Add a user by their email address (UPN) and assign one or more roles. These are stored as manual assignments and won't be overwritten by the automatic Entra group sync."} - {!editingUser && ( + {bulkEditUsers && ( + + + Selected Users + + + {bulkEditUsers.map((u, idx) => ( + + ))} + + + )} + {!editingUser && !bulkEditUsers && ( { + if (existingUpns.has(value?.trim()?.toLowerCase())) { + return "This user already exists. Use Edit Roles to update their permissions."; + } + return true; + }, + }} /> )} { - + - + + From 1b7797afd4f4c55e6a44157a87914daf8110068f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 27 May 2026 16:29:15 +0800 Subject: [PATCH 057/133] Update standards.json --- src/data/standards.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index b0fd2eabf081..1f40d86e0234 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6822,7 +6822,7 @@ { "type": "switch", "name": "standards.DefenderCompliancePolicy.ConnectWindows", - "label": "Connect Windows 10.0.15063+ to MDE", + "label": "Connect Windows 10.0.15063+ to MDE (Note: enabling this forces 'Block Windows if partner data unavailable' to on)", "defaultValue": false }, { @@ -6834,7 +6834,7 @@ { "type": "switch", "name": "standards.DefenderCompliancePolicy.windowsDeviceBlockedOnMissingPartnerData", - "label": "Block Windows if partner data unavailable", + "label": "Block Windows if partner data unavailable (Note: Microsoft enforces this to on when Connect Windows 10.0.15063+ to MDE is on)", "defaultValue": false }, { From de70889fe1c4e157ac25eb1ef43751add88056fa Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 14:39:44 +0200 Subject: [PATCH 058/133] smart lockout standard --- src/data/standards.json | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 1f40d86e0234..d78651f45ef7 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7293,5 +7293,49 @@ "addedDate": "2026-05-25", "powershellEquivalent": "Graph API PATCH /policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2", "recommendedBy": ["CIPP"] + }, + { + "name": "standards.SmartLockout", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "**Requires Entra ID P1.** Configures the Entra ID Smart Lockout settings including lockout duration, lockout threshold, and on-premises integration mode.", + "docsDescription": "Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforced).", + "addedComponent": [ + { + "type": "number", + "name": "standards.SmartLockout.LockoutDurationInSeconds", + "label": "Lockout Duration (seconds)", + "default": 60, + "required": true + }, + { + "type": "number", + "name": "standards.SmartLockout.LockoutThreshold", + "label": "Lockout Threshold (failed attempts)", + "default": 10, + "required": true + }, + { + "type": "switch", + "name": "standards.SmartLockout.EnableBannedPasswordCheckOnPremises", + "label": "Enable On-Premises Password Protection" + }, + { + "type": "radio", + "name": "standards.SmartLockout.BannedPasswordCheckOnPremisesMode", + "label": "On-Premises Mode", + "options": [ + { "label": "Audit", "value": "Audit" }, + { "label": "Enforced", "value": "Enforced" } + ] + } + ], + "label": "Configure Entra ID Smart Lockout", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-05-27", + "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", + "recommendedBy": ["CIS"], + "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] } ] From 0e527e50d858628dbe0943c33c0b67b18949d612 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 17:03:29 +0200 Subject: [PATCH 059/133] Sharepoint management functionality. --- src/data/standards.json | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index d78651f45ef7..cdb04909bc20 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7337,5 +7337,48 @@ "powershellEquivalent": "Get-MgBetaDirectorySetting, New-MgBetaDirectorySetting, Update-MgBetaDirectorySetting", "recommendedBy": ["CIS"], "requiredCapabilities": ["AAD_PREMIUM", "AAD_PREMIUM_P2"] + }, + { + "name": "standards.SPOVersionControl", + "cat": "SharePoint Standards", + "tag": [], + "helpText": "Configures SharePoint Online file versioning to either use automatic version trimming managed by Microsoft, or enforce a fixed major version limit with optional version expiration.", + "docsDescription": "Configures the SharePoint Online tenant-level file versioning policy. When automatic version trimming is enabled, Microsoft intelligently manages version cleanup. When disabled, you can set a fixed maximum number of major versions to retain and optionally expire versions after a specified number of days. This helps manage storage consumption while preserving version history as needed.", + "executiveText": "Controls how SharePoint Online manages file version history at the tenant level. Automatic trimming lets Microsoft optimize storage by cleaning up old versions intelligently. Manual limits give administrators precise control over the maximum number of versions retained and their expiration, ensuring predictable storage usage and compliance with data retention policies.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.SPOVersionControl.EnableAutoTrim", + "label": "Enable Automatic Version Trimming (Microsoft managed)" + }, + { + "type": "number", + "name": "standards.SPOVersionControl.MajorVersionLimit", + "label": "Maximum Major Versions (when auto trim is off)", + "default": 50 + }, + { + "type": "number", + "name": "standards.SPOVersionControl.ExpireVersionsAfterDays", + "label": "Expire Versions After Days (0 = never, when auto trim is off)", + "default": 0 + }, + { + "type": "switch", + "name": "standards.SPOVersionControl.ApplyToExistingSites", + "label": "Apply to all existing sites and document libraries" + } + ], + "label": "Set SharePoint File Version Limits", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-05-27", + "powershellEquivalent": "Set-SPOTenant -EnableAutoExpirationVersionTrim $true or Set-SPOTenant -EnableAutoExpirationVersionTrim $false -MajorVersionLimit 50 -ExpireVersionsAfterDays 365", + "recommendedBy": [], + "disabledFeatures": { + "report": false, + "warn": false, + "remediate": false + } } ] From 5709f85661a047dad29216739acccf6326c9231b Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 27 May 2026 17:11:46 +0200 Subject: [PATCH 060/133] fix: update terminology from "Temporary Access Password" to "Temporary Access Pass" Fixes https://github.com/KelvinTegelaar/CIPP/issues/6060 --- src/components/CippComponents/CippUserActions.jsx | 4 ++-- src/data/standards.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/CippComponents/CippUserActions.jsx b/src/components/CippComponents/CippUserActions.jsx index c1af17ab13e6..e635c5fa988f 100644 --- a/src/components/CippComponents/CippUserActions.jsx +++ b/src/components/CippComponents/CippUserActions.jsx @@ -418,7 +418,7 @@ export const useCippUserActions = () => { }, { //tested - label: 'Create Temporary Access Password', + label: 'Create Temporary Access Pass', type: 'POST', icon: , url: '/api/ExecCreateTAP', @@ -443,7 +443,7 @@ export const useCippUserActions = () => { }, ], confirmText: - 'Are you sure you want to create a Temporary Access Password for [userPrincipalName]?', + 'Are you sure you want to create a Temporary Access Pass for [userPrincipalName]?', multiPost: false, condition: () => canWriteUser, }, diff --git a/src/data/standards.json b/src/data/standards.json index cdb04909bc20..dbe64432f9d5 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -753,8 +753,8 @@ "tag": [], "appliesToTest": ["EIDSCAAT01", "EIDSCAAT02", "ZTNA21845", "ZTNA21846"], "helpText": "Enables TAP and sets the default TAP lifetime to 1 hour. This configuration also allows you to select if a TAP is single use or multi-logon.", - "docsDescription": "Enables Temporary Password generation for the tenant.", - "executiveText": "Enables temporary access passwords that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passwords provide a secure way to restore access without compromising long-term security policies.", + "docsDescription": "Enables Temporary Access Pass generation for the tenant.", + "executiveText": "Enables temporary access passes that IT administrators can generate for employees who are locked out or need emergency access to systems. These time-limited passs provide a secure way to restore access without compromising long-term security policies.", "addedComponent": [ { "type": "autoComplete", @@ -768,7 +768,7 @@ ] } ], - "label": "Enable Temporary Access Passwords", + "label": "Enable Temporary Access Passes (TAP)", "impact": "Low Impact", "impactColour": "info", "addedDate": "2022-03-15", From bf6056baf362471dba4ecbb599e0b1c85091b8d0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:24:47 +0200 Subject: [PATCH 061/133] Add version cleanup --- src/pages/teams-share/sharepoint/index.js | 68 +++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index cf1353f52b7f..657f9b8e60cf 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -9,12 +9,15 @@ import { AdminPanelSettings, NoAccounts, Delete, + CleaningServices, } from '@mui/icons-material' import Link from 'next/link' import { Stack } from '@mui/system' import { CippDataTable } from '../../../components/CippTable/CippDataTable' import { useSettings } from '../../../hooks/use-settings' import { useCippReportDB } from '../../../components/CippComponents/CippReportDBControls' +import CippFormComponent from '../../../components/CippComponents/CippFormComponent' +import { CippFormCondition } from '../../../components/CippComponents/CippFormCondition' const Page = () => { const pageTitle = 'SharePoint Sites' @@ -202,6 +205,71 @@ const Page = () => { color: 'error', multiPost: false, }, + { + label: 'Start Version Cleanup Job', + type: 'POST', + icon: , + url: '/api/ExecSPOVersionCleanup', + data: { + SiteUrl: 'webUrl', + }, + confirmText: + 'Start a file version cleanup job for [displayName]. This will trim old file versions based on the selected mode.', + children: ({ formHook }) => ( + <> + + + + + + + + + ), + defaultvalues: { + BatchDeleteMode: '2', + }, + customDataformatter: (row, action, formData) => ({ + tenantFilter: row.Tenant ?? tenantFilter, + SiteUrl: row.webUrl, + BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10), + DeleteOlderThanDays: formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, + MajorVersionLimit: formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, + }), + multiPost: false, + }, ] const offCanvas = { From 635548afd1d988518d8c392413828aec7e39eda5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:25:01 +0200 Subject: [PATCH 062/133] Add version cleanup --- src/pages/teams-share/sharepoint/index.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pages/teams-share/sharepoint/index.js b/src/pages/teams-share/sharepoint/index.js index 657f9b8e60cf..695b2bef7684 100644 --- a/src/pages/teams-share/sharepoint/index.js +++ b/src/pages/teams-share/sharepoint/index.js @@ -224,7 +224,10 @@ const Page = () => { formControl={formHook} options={[ { label: 'Sync Policy — apply site version policy to existing versions', value: '2' }, - { label: 'Delete Older Than Days — remove versions older than a set number of days', value: '0' }, + { + label: 'Delete Older Than Days — remove versions older than a set number of days', + value: '0', + }, { label: 'Count Limits — keep a maximum number of major versions', value: '1' }, ]} /> @@ -265,8 +268,10 @@ const Page = () => { tenantFilter: row.Tenant ?? tenantFilter, SiteUrl: row.webUrl, BatchDeleteMode: parseInt(formData.BatchDeleteMode, 10), - DeleteOlderThanDays: formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, - MajorVersionLimit: formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, + DeleteOlderThanDays: + formData.BatchDeleteMode === '0' ? parseInt(formData.DeleteOlderThanDays, 10) : -1, + MajorVersionLimit: + formData.BatchDeleteMode === '1' ? parseInt(formData.MajorVersionLimit, 10) : -1, }), multiPost: false, }, From 7a40854272a60617d57a28570406953a9cca913c Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:37:02 +0200 Subject: [PATCH 063/133] fix query keys --- src/pages/security/compliance/dlp/index.js | 126 ++++++++--------- src/pages/security/compliance/labels/index.js | 106 ++++++++------- .../security/compliance/retention/index.js | 128 +++++++++--------- src/pages/security/compliance/sit/index.js | 74 +++++----- 4 files changed, 221 insertions(+), 213 deletions(-) diff --git a/src/pages/security/compliance/dlp/index.js b/src/pages/security/compliance/dlp/index.js index 750cfe6ade53..5e09520f5579 100644 --- a/src/pages/security/compliance/dlp/index.js +++ b/src/pages/security/compliance/dlp/index.js @@ -1,99 +1,101 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Book, Block, Check } from "@mui/icons-material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { Book, Block, Check } from '@mui/icons-material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "DLP Compliance Policies"; - const apiUrl = "/api/ListDlpCompliancePolicy"; - const cardButtonPermissions = ["Security.DlpCompliancePolicy.ReadWrite"]; + const pageTitle = 'DLP Compliance Policies' + const apiUrl = '/api/ListDlpCompliancePolicy' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.DlpCompliancePolicy.ReadWrite'] const actions = [ { - label: "Create template based on policy", - type: "POST", + label: 'Create template based on policy', + type: 'POST', icon: , - url: "/api/AddDlpCompliancePolicyTemplate", + url: '/api/AddDlpCompliancePolicyTemplate', dataFunction: (data) => { - return { ...data }; + return { ...data } }, - confirmText: "Are you sure you want to create a template based on this DLP policy?", + confirmText: 'Are you sure you want to create a template based on this DLP policy?', }, { - label: "Enable Policy", - type: "POST", + label: 'Enable Policy', + type: 'POST', icon: , - url: "/api/EditDlpCompliancePolicy", + url: '/api/EditDlpCompliancePolicy', data: { - State: "!enable", - Identity: "Name", + State: '!enable', + Identity: 'Name', }, - confirmText: "Are you sure you want to enable this DLP policy?", + confirmText: 'Are you sure you want to enable this DLP policy?', condition: (row) => row.Enabled === false, }, { - label: "Disable Policy", - type: "POST", + label: 'Disable Policy', + type: 'POST', icon: , - url: "/api/EditDlpCompliancePolicy", + url: '/api/EditDlpCompliancePolicy', data: { - State: "!disable", - Identity: "Name", + State: '!disable', + Identity: 'Name', }, - confirmText: "Are you sure you want to disable this DLP policy?", + confirmText: 'Are you sure you want to disable this DLP policy?', condition: (row) => row.Enabled === true, }, { - label: "Delete Policy", - type: "POST", + label: 'Delete Policy', + type: 'POST', icon: , - url: "/api/RemoveDlpCompliancePolicy", + url: '/api/RemoveDlpCompliancePolicy', data: { - Identity: "Name", + Identity: 'Name', }, - confirmText: "Are you sure you want to delete this DLP policy?", - color: "danger", + confirmText: 'Are you sure you want to delete this DLP policy?', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Name", - "Comment", - "Mode", - "Enabled", - "Workload", - "ExchangeLocation", - "SharePointLocation", - "OneDriveLocation", - "TeamsLocation", - "EndpointDlpLocation", - "RuleCount", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", + 'Name', + 'Comment', + 'Mode', + 'Enabled', + 'Workload', + 'ExchangeLocation', + 'SharePointLocation', + 'OneDriveLocation', + 'TeamsLocation', + 'EndpointDlpLocation', + 'RuleCount', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', ], actions: actions, - }; + } const simpleColumns = [ - "Name", - "Mode", - "Enabled", - "Workload", - "RuleCount", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", - ]; + 'Name', + 'Mode', + 'Enabled', + 'Workload', + 'RuleCount', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/pages/security/compliance/labels/index.js b/src/pages/security/compliance/labels/index.js index e35ff5942b0f..998865acdc87 100644 --- a/src/pages/security/compliance/labels/index.js +++ b/src/pages/security/compliance/labels/index.js @@ -1,78 +1,80 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Book } from "@mui/icons-material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { Book } from '@mui/icons-material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "Sensitivity Labels"; - const apiUrl = "/api/ListSensitivityLabel"; - const cardButtonPermissions = ["Security.SensitivityLabel.ReadWrite"]; + const pageTitle = 'Sensitivity Labels' + const apiUrl = '/api/ListSensitivityLabel' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.SensitivityLabel.ReadWrite'] const actions = [ { - label: "Create template based on label", - type: "POST", + label: 'Create template based on label', + type: 'POST', icon: , - url: "/api/AddSensitivityLabelTemplate", + url: '/api/AddSensitivityLabelTemplate', dataFunction: (data) => { - return { ...data }; + return { ...data } }, - confirmText: "Are you sure you want to create a template based on this sensitivity label?", + confirmText: 'Are you sure you want to create a template based on this sensitivity label?', }, { - label: "Delete Label", - type: "POST", + label: 'Delete Label', + type: 'POST', icon: , - url: "/api/RemoveSensitivityLabel", + url: '/api/RemoveSensitivityLabel', data: { - Identity: "Guid", + Identity: 'Guid', }, confirmText: - "Are you sure you want to delete this sensitivity label? Labels currently published to users will be removed from policies.", - color: "danger", + 'Are you sure you want to delete this sensitivity label? Labels currently published to users will be removed from policies.', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "DisplayName", - "Name", - "Comment", - "Tooltip", - "ParentId", - "ContentType", - "EncryptionEnabled", - "EncryptionProtectionType", - "ContentMarkingHeaderEnabled", - "ContentMarkingFooterEnabled", - "ContentMarkingWatermarkEnabled", - "SiteAndGroupProtectionEnabled", - "Priority", - "Disabled", - "PublishedInPolicies", + 'DisplayName', + 'Name', + 'Comment', + 'Tooltip', + 'ParentId', + 'ContentType', + 'EncryptionEnabled', + 'EncryptionProtectionType', + 'ContentMarkingHeaderEnabled', + 'ContentMarkingFooterEnabled', + 'ContentMarkingWatermarkEnabled', + 'SiteAndGroupProtectionEnabled', + 'Priority', + 'Disabled', + 'PublishedInPolicies', ], actions: actions, - }; + } const simpleColumns = [ - "DisplayName", - "Name", - "ContentType", - "EncryptionEnabled", - "ContentMarkingHeaderEnabled", - "ContentMarkingWatermarkEnabled", - "SiteAndGroupProtectionEnabled", - "Priority", - "Disabled", - ]; + 'DisplayName', + 'Name', + 'ContentType', + 'EncryptionEnabled', + 'ContentMarkingHeaderEnabled', + 'ContentMarkingWatermarkEnabled', + 'SiteAndGroupProtectionEnabled', + 'Priority', + 'Disabled', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/pages/security/compliance/retention/index.js b/src/pages/security/compliance/retention/index.js index 962301013f29..940d88b4eb0a 100644 --- a/src/pages/security/compliance/retention/index.js +++ b/src/pages/security/compliance/retention/index.js @@ -1,98 +1,100 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { Book, Block, Check } from "@mui/icons-material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { Book, Block, Check } from '@mui/icons-material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "Purview Retention Policies"; - const apiUrl = "/api/ListRetentionCompliancePolicy"; - const cardButtonPermissions = ["Security.RetentionCompliancePolicy.ReadWrite"]; + const pageTitle = 'Purview Retention Policies' + const apiUrl = '/api/ListRetentionCompliancePolicy' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.RetentionCompliancePolicy.ReadWrite'] const actions = [ { - label: "Create template based on policy", - type: "POST", + label: 'Create template based on policy', + type: 'POST', icon: , - url: "/api/AddRetentionCompliancePolicyTemplate", - data: { Identity: "Name" }, - confirmText: "Are you sure you want to create a template based on this retention policy?", + url: '/api/AddRetentionCompliancePolicyTemplate', + data: { Identity: 'Name' }, + confirmText: 'Are you sure you want to create a template based on this retention policy?', }, { - label: "Enable Policy", - type: "POST", + label: 'Enable Policy', + type: 'POST', icon: , - url: "/api/EditRetentionCompliancePolicy", + url: '/api/EditRetentionCompliancePolicy', data: { - State: "!enable", - Identity: "Name", + State: '!enable', + Identity: 'Name', }, - confirmText: "Are you sure you want to enable this retention policy?", + confirmText: 'Are you sure you want to enable this retention policy?', condition: (row) => row.Enabled === false, }, { - label: "Disable Policy", - type: "POST", + label: 'Disable Policy', + type: 'POST', icon: , - url: "/api/EditRetentionCompliancePolicy", + url: '/api/EditRetentionCompliancePolicy', data: { - State: "!disable", - Identity: "Name", + State: '!disable', + Identity: 'Name', }, - confirmText: "Are you sure you want to disable this retention policy?", + confirmText: 'Are you sure you want to disable this retention policy?', condition: (row) => row.Enabled === true, }, { - label: "Delete Policy", - type: "POST", + label: 'Delete Policy', + type: 'POST', icon: , - url: "/api/RemoveRetentionCompliancePolicy", + url: '/api/RemoveRetentionCompliancePolicy', data: { - Identity: "Name", + Identity: 'Name', }, - confirmText: "Are you sure you want to delete this retention policy?", - color: "danger", + confirmText: 'Are you sure you want to delete this retention policy?', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Name", - "Comment", - "Enabled", - "Workload", - "RestrictiveRetention", - "ExchangeLocation", - "SharePointLocation", - "OneDriveLocation", - "ModernGroupLocation", - "TeamsChannelLocation", - "TeamsChatLocation", - "RuleCount", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", + 'Name', + 'Comment', + 'Enabled', + 'Workload', + 'RestrictiveRetention', + 'ExchangeLocation', + 'SharePointLocation', + 'OneDriveLocation', + 'ModernGroupLocation', + 'TeamsChannelLocation', + 'TeamsChatLocation', + 'RuleCount', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', ], actions: actions, - }; + } const simpleColumns = [ - "Name", - "Enabled", - "Workload", - "RuleCount", - "RestrictiveRetention", - "CreatedBy", - "WhenCreatedUTC", - "WhenChangedUTC", - ]; + 'Name', + 'Enabled', + 'Workload', + 'RuleCount', + 'RestrictiveRetention', + 'CreatedBy', + 'WhenCreatedUTC', + 'WhenChangedUTC', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page diff --git a/src/pages/security/compliance/sit/index.js b/src/pages/security/compliance/sit/index.js index 3101f0502218..43039cdba7fb 100644 --- a/src/pages/security/compliance/sit/index.js +++ b/src/pages/security/compliance/sit/index.js @@ -1,57 +1,59 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { CippDeployCompliancePolicyDrawer } from "../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx"; -import { PermissionButton } from "../../../../utils/permissions.js"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { TrashIcon } from '@heroicons/react/24/outline' +import { CippDeployCompliancePolicyDrawer } from '../../../../components/CippComponents/CippDeployCompliancePolicyDrawer.jsx' +import { PermissionButton } from '../../../../utils/permissions.js' +import { useSettings } from '../../../../hooks/use-settings' const Page = () => { - const pageTitle = "Sensitive Information Types"; - const apiUrl = "/api/ListSensitiveInfoType"; - const cardButtonPermissions = ["Security.SensitiveInfoType.ReadWrite"]; + const pageTitle = 'Sensitive Information Types' + const apiUrl = '/api/ListSensitiveInfoType' + const tenantFilter = useSettings().currentTenant + const cardButtonPermissions = ['Security.SensitiveInfoType.ReadWrite'] const actions = [ { - label: "Delete SIT", - type: "POST", + label: 'Delete SIT', + type: 'POST', icon: , - url: "/api/RemoveSensitiveInfoType", + url: '/api/RemoveSensitiveInfoType', data: { - Identity: "Name", + Identity: 'Name', }, confirmText: - "Are you sure you want to delete this Sensitive Information Type? Built-in Microsoft types cannot be deleted.", - color: "danger", + 'Are you sure you want to delete this Sensitive Information Type? Built-in Microsoft types cannot be deleted.', + color: 'danger', }, - ]; + ] const offCanvas = { extendedInfoFields: [ - "Name", - "Description", - "Publisher", - "Recommended", - "RulePackId", - "RulePackVersion", - "State", - "Type", + 'Name', + 'Description', + 'Publisher', + 'Recommended', + 'RulePackId', + 'RulePackVersion', + 'State', + 'Type', ], actions: actions, - }; + } const simpleColumns = [ - "Name", - "Publisher", - "Description", - "Recommended", - "RulePackVersion", - "State", - ]; + 'Name', + 'Publisher', + 'Description', + 'Recommended', + 'RulePackVersion', + 'State', + ] return ( { /> } /> - ); -}; + ) +} -Page.getLayout = (page) => {page}; -export default Page; +Page.getLayout = (page) => {page} +export default Page From de035243aa1090ac818cc2d02e651007a90723e1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 27 May 2026 20:59:42 +0200 Subject: [PATCH 064/133] fixes #6065 --- src/pages/tenant/manage/user-defaults.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 8eb4f2592f61..3a5c3a150899 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -124,7 +124,7 @@ const Page = () => { labelField: (option) => `${option.License || option.skuPartNumber} (${option.AvailableUnits || 0} available)`, valueField: 'skuId', - queryKey: 'ListLicenses', + queryKey: `ListLicenses-${userSettings.currentTenant}`, }, multiple: true, creatable: false, @@ -137,7 +137,7 @@ const Page = () => { url: '/api/ListGroups', labelField: 'displayName', valueField: 'id', - queryKey: 'ListGroups', + queryKey: `ListGroups-${userSettings.currentTenant}`, addedField: { groupType: 'calculatedGroupType', }, From d0f58cbebd48dbdeaddfcf3af0f68d7b4320bec8 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 27 May 2026 22:43:01 +0200 Subject: [PATCH 065/133] feat(mailboxes): show mailbox and archive size columns Add archive-enabled filtering and cached mailbox/archive size columns to the mailbox table. Format archive size as GB using the shared CIPP formatting utility while keeping count and quota fields available through the column picker. Fixes #6061 --- src/pages/email/administration/mailboxes/index.js | 8 ++++++++ src/utils/get-cipp-formatting.js | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pages/email/administration/mailboxes/index.js b/src/pages/email/administration/mailboxes/index.js index 01bc84afae65..4f689548fabf 100644 --- a/src/pages/email/administration/mailboxes/index.js +++ b/src/pages/email/administration/mailboxes/index.js @@ -44,6 +44,11 @@ const Page = () => { value: [{ id: 'recipientTypeDetails', value: 'EquipmentMailbox' }], type: 'column', }, + { + filterName: 'View Archive-Enabled Mailboxes', + value: [{ id: 'ArchiveEnabled', value: true }], + type: 'column', + }, ] // Simplified columns for the table @@ -54,6 +59,9 @@ const Page = () => { 'UPN', 'primarySmtpAddress', 'AdditionalEmailAddresses', + ...(reportDB.useReportDB ? ['storageUsedInBytes'] : []), + 'ArchiveEnabled', + ...(reportDB.useReportDB ? ['ArchiveSize'] : []), ...reportDB.cacheColumns.filter((c) => c !== 'Tenant'), ] diff --git a/src/utils/get-cipp-formatting.js b/src/utils/get-cipp-formatting.js index 5580ab4b5de0..fc313c53cae7 100644 --- a/src/utils/get-cipp-formatting.js +++ b/src/utils/get-cipp-formatting.js @@ -152,7 +152,11 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr ) } - if (cellName === 'prohibitSendReceiveQuotaInBytes' || cellName === 'storageUsedInBytes') { + if ( + cellName === 'prohibitSendReceiveQuotaInBytes' || + cellName === 'storageUsedInBytes' || + cellName === 'ArchiveSize' + ) { //convert bytes to GB const bytes = data if (bytes === null || bytes === undefined) { @@ -1022,7 +1026,7 @@ export const getCippFormatting = (data, cellName, type, canReceive, flatten = tr } // Internal CIPP navigation links - if ((cellName === 'cippLink') && typeof data === 'string') { + if (cellName === 'cippLink' && typeof data === 'string') { return isText ? ( data ) : ( From 072416dd95fb955625d466022ebb4a05847e76e3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 00:54:29 +0200 Subject: [PATCH 066/133] new autopatch standard --- src/data/standards.json | 238 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index dbe64432f9d5..464b433e4cb6 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -7235,6 +7235,244 @@ "powershellEquivalent": "Graph API - /deviceAppManagement/mobileApps", "recommendedBy": [] }, + { + "name": "standards.AutopatchGroup", + "cat": "Intune Standards", + "tag": [], + "beta": true, + "deprecated": true, + "disabledFeatures": { "report": false, "warn": false, "remediate": false }, + "helpText": "Deploys a Windows Autopatch group with configurable deployment ring settings for quality updates, feature updates, Edge, and Office.", + "docsDescription": "Creates or updates a Windows Autopatch deployment group with Test and Last deployment rings. Configures quality update deferrals, feature update targeting, Edge and Office update channels per ring. Uses the Autopatch API proxy to manage the group configuration.", + "executiveText": "Configures Windows Autopatch deployment groups to manage update delivery across devices. Autopatch automates Windows quality updates, feature updates, Edge, and Office updates using deployment rings with configurable deferrals and deadlines.", + "addedComponent": [ + { + "type": "textField", + "name": "standards.AutopatchGroup.GroupName", + "label": "Group Name", + "required": true, + "defaultValue": "Autopatch default group" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TargetOSVersion", + "label": "Target OS Version", + "required": true, + "options": [ + { "label": "Windows 11, version 24H2", "value": "Windows 11, version 24H2" }, + { "label": "Windows 11, version 25H2", "value": "Windows 11, version 25H2" } + ], + "defaultValue": "Windows 11, version 25H2" + }, + { + "type": "switch", + "name": "standards.AutopatchGroup.EnableDriverUpdate", + "label": "Enable Driver Updates", + "defaultValue": true + }, + { + "type": "switch", + "name": "standards.AutopatchGroup.InstallWin10OnWin11Ineligible", + "label": "Install latest Windows 10 on Windows 11 ineligible devices", + "defaultValue": false + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityDeferral", + "label": "Test Ring - Quality Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityDeadline", + "label": "Test Ring - Quality Update Deadline (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestQualityGracePeriod", + "label": "Test Ring - Quality Update Grace Period (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 7, "message": "Maximum value is 7" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestFeatureDeferral", + "label": "Test Ring - Feature Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 365, "message": "Maximum value is 365" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestFeatureDeadline", + "label": "Test Ring - Feature Update Deadline (days)", + "defaultValue": 5, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TestEdgeChannel", + "label": "Test Ring - Edge Update Channel", + "options": [ + { "label": "Stable", "value": "Stable" }, + { "label": "Beta", "value": "Beta" }, + { "label": "Dev", "value": "Dev" } + ], + "defaultValue": "Beta" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.TestOfficeChannel", + "label": "Test Ring - Office Update Channel", + "options": [ + { "label": "Current", "value": "Current" }, + { "label": "Monthly Enterprise", "value": "MonthlyEnterprise" }, + { "label": "Semi-Annual Enterprise", "value": "SemiAnnual" } + ], + "defaultValue": "MonthlyEnterprise" + }, + { + "type": "number", + "name": "standards.AutopatchGroup.TestDnfDeferral", + "label": "Test Ring - Driver & Firmware Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityDeferral", + "label": "Last Ring - Quality Update Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityDeadline", + "label": "Last Ring - Quality Update Deadline (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastQualityGracePeriod", + "label": "Last Ring - Quality Update Grace Period (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 7, "message": "Maximum value is 7" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastFeatureDeferral", + "label": "Last Ring - Feature Update Deferral (days)", + "defaultValue": 0, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 365, "message": "Maximum value is 365" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastFeatureDeadline", + "label": "Last Ring - Feature Update Deadline (days)", + "defaultValue": 5, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.LastEdgeChannel", + "label": "Last Ring - Edge Update Channel", + "options": [ + { "label": "Stable", "value": "Stable" }, + { "label": "Beta", "value": "Beta" }, + { "label": "Dev", "value": "Dev" } + ], + "defaultValue": "Stable" + }, + { + "type": "select", + "multiple": false, + "name": "standards.AutopatchGroup.LastOfficeChannel", + "label": "Last Ring - Office Update Channel", + "options": [ + { "label": "Current", "value": "Current" }, + { "label": "Monthly Enterprise", "value": "MonthlyEnterprise" }, + { "label": "Semi-Annual Enterprise", "value": "SemiAnnual" } + ], + "defaultValue": "MonthlyEnterprise" + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastOfficeDeferral", + "label": "Last Ring - Office Update Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastOfficeDeadline", + "label": "Last Ring - Office Update Deadline (days)", + "defaultValue": 2, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + }, + { + "type": "number", + "name": "standards.AutopatchGroup.LastDnfDeferral", + "label": "Last Ring - Driver & Firmware Deferral (days)", + "defaultValue": 1, + "validators": { + "min": { "value": 0, "message": "Minimum value is 0" }, + "max": { "value": 30, "message": "Maximum value is 30" } + } + } + ], + "label": "Deploy Windows Autopatch Group", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2025-05-27", + "powershellEquivalent": "Autopatch API - POST /api/autoPatch", + "recommendedBy": [] + }, { "name": "standards.FIDO2PasskeyProfiles", "cat": "Entra (AAD) Standards", From f256c253bff5710aab52bbcb21d64d2775df27b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 00:38:36 +0000 Subject: [PATCH 067/133] chore(deps): bump react-virtuoso from 4.18.5 to 4.18.7 Bumps [react-virtuoso](https://github.com/petyosi/react-virtuoso/tree/HEAD/packages/react-virtuoso) from 4.18.5 to 4.18.7. - [Release notes](https://github.com/petyosi/react-virtuoso/releases) - [Changelog](https://github.com/petyosi/react-virtuoso/blob/main/packages/react-virtuoso/CHANGELOG.md) - [Commits](https://github.com/petyosi/react-virtuoso/commits/react-virtuoso@4.18.7/packages/react-virtuoso) --- updated-dependencies: - dependency-name: react-virtuoso dependency-version: 4.18.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 331476b49fa7..2996f68d384a 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "react-redux": "9.2.0", "react-syntax-highlighter": "^16.1.0", "react-time-ago": "^7.3.3", - "react-virtuoso": "^4.18.5", + "react-virtuoso": "^4.18.7", "recharts": "^3.8.1", "redux": "5.0.1", "redux-persist": "^6.0.0", diff --git a/yarn.lock b/yarn.lock index f679e9ff64b2..1c0827c76405 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6736,10 +6736,10 @@ react-virtualized-auto-sizer@^1.0.26: resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.26.tgz#e9470ef6a778dc4f1d5fd76305fa2d8b610c357a" integrity sha512-CblNyiNVw2o+hsa5/49NH2ogGxZ+t+3aweRvNSq7TVjDIlwk7ir4lencEg5HxHeSzwNarSkNkiu0qJSOXtxm5A== -react-virtuoso@^4.18.5: - version "4.18.5" - resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.5.tgz#450108e585c7a1124b995c7ea3cf367ed4857631" - integrity sha512-QDyNjyNEuurZG67SOmzYyxEkQYSyGmAMixOI6M15L/Q4CF39EgG+88y6DgZRo0q7rmy0HPx3Fj90I8/tPdnRCQ== +react-virtuoso@^4.18.7: + version "4.18.7" + resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.18.7.tgz#e5b0497ac76526cd661d71cd11f86c398784b905" + integrity sha512-xNF5zDGEEIMB7cKwcen/pLig0YDf6OnfFrVgKFa7sHPf9fRem0CaLshyObbBcP88jzn0enavL39EgplgdyT21g== react@19.2.6: version "19.2.6" From 8316c600205322891849d6b211f01c7237971c62 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 00:38:54 +0000 Subject: [PATCH 068/133] chore(deps): bump @react-pdf/renderer from 4.3.2 to 4.5.1 Bumps [@react-pdf/renderer](https://github.com/diegomura/react-pdf/tree/HEAD/packages/renderer) from 4.3.2 to 4.5.1. - [Release notes](https://github.com/diegomura/react-pdf/releases) - [Changelog](https://github.com/diegomura/react-pdf/blob/master/packages/renderer/CHANGELOG.md) - [Commits](https://github.com/diegomura/react-pdf/commits/@react-pdf/layout@4.5.1/packages/renderer) --- updated-dependencies: - dependency-name: "@react-pdf/renderer" dependency-version: 4.5.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 231 +++++++++++++++++++++++++++------------------------ 2 files changed, 125 insertions(+), 108 deletions(-) diff --git a/package.json b/package.json index 331476b49fa7..589530552225 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@musement/iso-duration": "^1.0.0", "@nivo/core": "^0.99.0", "@nivo/sankey": "^0.99.0", - "@react-pdf/renderer": "^4.3.2", + "@react-pdf/renderer": "^4.5.1", "@reduxjs/toolkit": "^2.11.2", "@tanstack/query-sync-storage-persister": "^5.90.25", "@tanstack/react-query": "^5.100.10", diff --git a/yarn.lock b/yarn.lock index f679e9ff64b2..7cab144062e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1668,6 +1668,16 @@ "@nivo/theming" "0.99.0" "@react-spring/web" "9.4.5 || ^9.7.2 || ^10.0" +"@noble/ciphers@^1.0.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.3.0.tgz#f64b8ff886c240e644e5573c097f86e5b43676dc" + integrity sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== + +"@noble/hashes@^1.6.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1704,69 +1714,65 @@ resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-3.0.0.tgz#34ccc280ce7d8ac5c09f2b3d5fffded450bdf1a2" integrity sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ== -"@react-pdf/fns@3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.1.2.tgz#9ce7351d9fdf1cdb6e9c6ffd6801bc65f29f991c" - integrity sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g== +"@react-pdf/fns@3.1.3": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@react-pdf/fns/-/fns-3.1.3.tgz#e0437d60ac10746bfbdf080e6809ba6f20d01556" + integrity sha512-0I7pApDr1/RLAKbizuLy/IHTEa93LSPy/bEwYniboC3Xqnp6Od8xFJKbKEzGw2wh/5zKFFwl00g4t9RwgIMc3w== -"@react-pdf/font@^4.0.4": - version "4.0.4" - resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-4.0.4.tgz#7b5bed082fb159e582f22fe4c56e9a8c46736835" - integrity sha512-8YtgGtL511txIEc9AjiilpZ7yjid8uCd8OGUl6jaL3LIHnrToUupSN4IzsMQpVTCMYiDLFnDNQzpZsOYtRS/Pg== +"@react-pdf/font@^4.0.8": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@react-pdf/font/-/font-4.0.8.tgz#2279fb487f8a532e8b82e11732703a904ad05bb3" + integrity sha512-deNd+emtZAJho1IlzKL9bRoLAGv/6oXOIKO2oZfs4RuXUrK1onLHbJO7e2YoVLPFP/sQxisRTnzdJFtd35iKwA== dependencies: - "@react-pdf/pdfkit" "^4.1.0" - "@react-pdf/types" "^2.9.2" + "@react-pdf/pdfkit" "^5.1.1" + "@react-pdf/types" "^2.11.1" fontkit "^2.0.2" is-url "^1.2.4" -"@react-pdf/image@^3.0.4": - version "3.0.4" - resolved "https://registry.yarnpkg.com/@react-pdf/image/-/image-3.0.4.tgz#ee9c8928843d9680279a512138c5f597b3aae616" - integrity sha512-z0ogVQE0bKqgXQ5smgzIU857rLV7bMgVdrYsu3UfXDDLSzI7QPvzf6MFTFllX6Dx2rcsF13E01dqKPtJEM799g== +"@react-pdf/image@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@react-pdf/image/-/image-3.1.0.tgz#1ab51db9a6341ffde5049be3ef55d44c3dc777d8" + integrity sha512-ks7Ry8v711r8NvKWSELehj0BXBNPRihSnWsM09nDD8Ur175zbWBCK217LLwQMKDNYDVpkZaipdoJPom1LGaE9g== dependencies: - "@react-pdf/png-js" "^3.0.0" + "@react-pdf/svg" "^1.1.0" jay-peg "^1.1.1" - -"@react-pdf/layout@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-4.4.2.tgz#30bde1e460ec8ead6a0aed85eca41279ed6f0ed8" - integrity sha512-gNu2oh8MiGR+NJZYTJ4c4q0nWCESBI6rKFiodVhE7OeVAjtzZzd6l65wsN7HXdWJqOZD3ttD97iE+tf5SOd/Yg== - dependencies: - "@react-pdf/fns" "3.1.2" - "@react-pdf/image" "^3.0.4" - "@react-pdf/primitives" "^4.1.1" - "@react-pdf/stylesheet" "^6.1.2" - "@react-pdf/textkit" "^6.1.0" - "@react-pdf/types" "^2.9.2" + png-js "^2.0.0" + +"@react-pdf/layout@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@react-pdf/layout/-/layout-4.6.1.tgz#6777fafa2a47996d4b42de37ddd324ea1aff4557" + integrity sha512-gN6PmWoEffvlIkifLfEhMsVucRywVMyH3rnxdyOVOhGy0nWJKKGpHyPc4plbDdpP6EfZ0r8prHXujDSkIG2nSA== + dependencies: + "@react-pdf/fns" "3.1.3" + "@react-pdf/image" "^3.1.0" + "@react-pdf/primitives" "^4.3.0" + "@react-pdf/stylesheet" "^6.2.1" + "@react-pdf/textkit" "^6.3.0" + "@react-pdf/types" "^2.11.1" emoji-regex-xs "^1.0.0" queue "^6.0.1" yoga-layout "^3.2.1" -"@react-pdf/pdfkit@^4.1.0": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-4.1.0.tgz#2a32cb4bfa36e887747395d8c13ac425459eda0a" - integrity sha512-Wm/IOAv0h/U5Ra94c/PltFJGcpTUd/fwVMVeFD6X9tTTPCttIwg0teRG1Lqq617J8K4W7jpL/B0HTH0mjp3QpQ== +"@react-pdf/pdfkit@^5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@react-pdf/pdfkit/-/pdfkit-5.1.1.tgz#b3af968f94555a3d7c4cc1d5b81493bdbfbd76e0" + integrity sha512-wNcdSsNlNYyGHGAgIdt453egBF7fiF9UxpRlklUfVvu8OWCrUppG9xiUrPLVoKiqWet5tMi0w6LmuFUJuYqjEg== dependencies: "@babel/runtime" "^7.20.13" - "@react-pdf/png-js" "^3.0.0" + "@noble/ciphers" "^1.0.0" + "@noble/hashes" "^1.6.0" browserify-zlib "^0.2.0" - crypto-js "^4.2.0" fontkit "^2.0.2" jay-peg "^1.1.1" + js-md5 "^0.8.3" linebreak "^1.1.0" + png-js "^2.0.0" vite-compatible-readable-stream "^3.6.1" -"@react-pdf/png-js@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@react-pdf/png-js/-/png-js-3.0.0.tgz#c0b7dc7c77e36f0830e9b7bccca7ddd64ada1c5e" - integrity sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA== - dependencies: - browserify-zlib "^0.2.0" - -"@react-pdf/primitives@^4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@react-pdf/primitives/-/primitives-4.1.1.tgz#c7bfb7e83173661b6ec50ada4aba8dc9e94d0563" - integrity sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ== +"@react-pdf/primitives@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@react-pdf/primitives/-/primitives-4.3.0.tgz#3bb5f74294bea923392499dd46bc5196d47b918c" + integrity sha512-nYXoZ36pvwNzbc54+DbL8RCn15jU7woJ9D/svnh5tpUXekJ+CbI4mZLo6boSv24CvJgychOu6h7gxX03B4ps0A== "@react-pdf/reconciler@^2.0.0": version "2.0.0" @@ -1776,71 +1782,78 @@ object-assign "^4.1.1" scheduler "0.25.0-rc-603e6108-20241029" -"@react-pdf/render@^4.3.2": - version "4.3.2" - resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-4.3.2.tgz#ae24c363fc25c46eb25fe85a13b28e693ba97635" - integrity sha512-el5KYM1sH/PKcO4tRCIm8/AIEmhtraaONbwCrBhFdehoGv6JtgnXiMxHGAvZbI5kEg051GbyP+XIU6f6YbOu6Q== +"@react-pdf/render@^4.5.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@react-pdf/render/-/render-4.5.1.tgz#7d94bfb96f4abe0cac28f769e296db909c8c456f" + integrity sha512-IW/N4HWJWtioBXCf7n02IR24VJJ8gbdS3jGypf+vW/rSErEx3/URRzh9UK6Ma8Fpog9+T/W6GE2NHJ5AAKHhVA== dependencies: "@babel/runtime" "^7.20.13" - "@react-pdf/fns" "3.1.2" - "@react-pdf/primitives" "^4.1.1" - "@react-pdf/textkit" "^6.1.0" - "@react-pdf/types" "^2.9.2" + "@react-pdf/fns" "3.1.3" + "@react-pdf/primitives" "^4.3.0" + "@react-pdf/textkit" "^6.3.0" + "@react-pdf/types" "^2.11.1" abs-svg-path "^0.1.1" - color-string "^1.9.1" + color-string "^2.1.4" normalize-svg-path "^1.1.0" parse-svg-path "^0.1.2" svg-arc-to-cubic-bezier "^3.2.0" -"@react-pdf/renderer@^4.3.2": - version "4.3.2" - resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-4.3.2.tgz#6a08d9f19cb1221ef377fb15586db4547d59434d" - integrity sha512-EhPkj35gO9rXIyyx29W3j3axemvVY5RigMmlK4/6Ku0pXB8z9PEE/sz4ZBOShu2uot6V4xiCR3aG+t9IjJJlBQ== +"@react-pdf/renderer@^4.5.1": + version "4.5.1" + resolved "https://registry.yarnpkg.com/@react-pdf/renderer/-/renderer-4.5.1.tgz#3daa9caa572ea8c42beece01b6faa348b460d304" + integrity sha512-5r1VQrE6FRLXX5wWUxwZzM24E2BJMo6g8AQWuS8WyPs9ugu5yMnb2g8/RpPYka/Z6J+RUEWc32wty2NoUJF42Q== dependencies: "@babel/runtime" "^7.20.13" - "@react-pdf/fns" "3.1.2" - "@react-pdf/font" "^4.0.4" - "@react-pdf/layout" "^4.4.2" - "@react-pdf/pdfkit" "^4.1.0" - "@react-pdf/primitives" "^4.1.1" + "@react-pdf/fns" "3.1.3" + "@react-pdf/font" "^4.0.8" + "@react-pdf/layout" "^4.6.1" + "@react-pdf/pdfkit" "^5.1.1" + "@react-pdf/primitives" "^4.3.0" "@react-pdf/reconciler" "^2.0.0" - "@react-pdf/render" "^4.3.2" - "@react-pdf/types" "^2.9.2" + "@react-pdf/render" "^4.5.1" + "@react-pdf/types" "^2.11.1" events "^3.3.0" object-assign "^4.1.1" prop-types "^15.6.2" queue "^6.0.1" -"@react-pdf/stylesheet@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-6.1.2.tgz#6ef21e2851ee7c2dc30582e7c01efb14f0308525" - integrity sha512-E3ftGRYUQGKiN3JOgtGsLDo0hGekA6dmkmi/MYACytmPTKxQRBSO3126MebmCq+t1rgU9uRlREIEawJ+8nzSbw== +"@react-pdf/stylesheet@^6.2.1": + version "6.2.1" + resolved "https://registry.yarnpkg.com/@react-pdf/stylesheet/-/stylesheet-6.2.1.tgz#baa87869c0cc953fb334d20c63359672b872475c" + integrity sha512-2+UEk+7e+z8baaWi2l5kPLWmwtJeOI+T5wW9GGeN3iDH7vd3kbTqOpN1yt9mmfNVZFxQsnDHpznFb5v5UF983A== dependencies: - "@react-pdf/fns" "3.1.2" - "@react-pdf/types" "^2.9.2" - color-string "^1.9.1" + "@react-pdf/fns" "3.1.3" + "@react-pdf/types" "^2.11.1" + color-string "^2.1.4" hsl-to-hex "^1.0.0" media-engine "^1.0.3" postcss-value-parser "^4.1.0" -"@react-pdf/textkit@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@react-pdf/textkit/-/textkit-6.1.0.tgz#ff7667b4a67c98fecefbeabff221de4bafa37979" - integrity sha512-sFlzDC9CDFrJsnL3B/+NHrk9+Advqk7iJZIStiYQDdskbow8GF/AGYrpIk+vWSnh35YxaGbHkqXq53XOxnyrjQ== +"@react-pdf/svg@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@react-pdf/svg/-/svg-1.1.0.tgz#c3ba275d312f7e2862f2a7044944b46c44c35f41" + integrity sha512-cTIHXiz9x1HrbfqzfxfZP3FRdDwUXG77QWF6Fb5MP/lV3ONxR+g0Z3hwtBatCS9HeGBQCpxX/Lzb8wHE+co1PA== + dependencies: + "@react-pdf/primitives" "^4.3.0" + +"@react-pdf/textkit@^6.3.0": + version "6.3.0" + resolved "https://registry.yarnpkg.com/@react-pdf/textkit/-/textkit-6.3.0.tgz#fe685654f557ff861008e09308db4a2a57f6bc42" + integrity sha512-v6+V8nAcVwm7s2s1jIG2MD3Iw//x/k+XrH1foWOELBE4b32pyDgKyPXN/6KJE0dnX7+fVy27uctLNCLNMvzKzQ== dependencies: - "@react-pdf/fns" "3.1.2" + "@react-pdf/fns" "3.1.3" bidi-js "^1.0.2" hyphen "^1.6.4" unicode-properties "^1.4.1" -"@react-pdf/types@^2.9.2": - version "2.9.2" - resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.9.2.tgz#92aefa900b25bd3d0e87bb139346af545ed1ddfc" - integrity sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g== +"@react-pdf/types@^2.11.1": + version "2.11.1" + resolved "https://registry.yarnpkg.com/@react-pdf/types/-/types-2.11.1.tgz#ae37a12a883ae2d54a2b98b9b9ba32fbf4241478" + integrity sha512-i9xQgfaDU9QoeNnbp6rltXCWg1huEh195rpOuN8cE4BZ2FuLdQrsIcb2dhFF9aOxXf+XBA6LOSpIW051MDD/bw== dependencies: - "@react-pdf/font" "^4.0.4" - "@react-pdf/primitives" "^4.1.1" - "@react-pdf/stylesheet" "^6.1.2" + "@react-pdf/font" "^4.0.8" + "@react-pdf/primitives" "^4.3.0" + "@react-pdf/stylesheet" "^6.2.1" "@react-spring/animated@~10.0.3": version "10.0.3" @@ -3229,18 +3242,22 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@^1.0.0, color-name@~1.1.4: +color-name@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-2.1.0.tgz#0b677385c1c4b4edfdeaf77e38fa338e3a40b693" + integrity sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg== + +color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" - integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== +color-string@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-2.1.4.tgz#9dcf566ff976e23368c8bd673f5c35103ab41058" + integrity sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg== dependencies: - color-name "^1.0.0" - simple-swizzle "^0.2.2" + color-name "^2.0.0" combined-stream@^1.0.8: version "1.0.8" @@ -3326,11 +3343,6 @@ cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" -crypto-js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" - integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== - css-box-model@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" @@ -4313,6 +4325,11 @@ fflate@^0.8.1: resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea" integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A== +fflate@^0.8.2: + version "0.8.3" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.3.tgz#bc27d8eb30343d4d512abb03480202ce65d825fc" + integrity sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA== + file-entry-cache@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" @@ -4870,11 +4887,6 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== -is-arrayish@^0.3.1: - version "0.3.4" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.4.tgz#1ee5553818511915685d33bb13d31bf854e5059d" - integrity sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA== - is-async-function@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" @@ -5132,6 +5144,11 @@ js-base64@^3.7.2: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.8.tgz#af44496bc09fa178ed9c4adf67eb2b46f5c6d2a4" integrity sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow== +js-md5@^0.8.3: + version "0.8.3" + resolved "https://registry.yarnpkg.com/js-md5/-/js-md5-0.8.3.tgz#921bab7efa95bfc9d62b87ee08a57f8fe4305b69" + integrity sha512-qR0HB5uP6wCuRMrWPTrkMaev7MJZwJuuw4fnwAzRgP4J4/F8RwtodOKpGp4XpqsLBFzzgqIO42efFAyz2Et6KQ== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -6261,6 +6278,13 @@ picomatch@^4.0.3: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== +png-js@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/png-js/-/png-js-2.0.0.tgz#7bc521aea1d47f5e3bf42eeecdd77095cba98d5f" + integrity sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA== + dependencies: + fflate "^0.8.2" + possible-typed-array-names@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" @@ -7211,13 +7235,6 @@ side-channel@^1.1.0: side-channel-map "^1.0.1" side-channel-weakmap "^1.0.2" -simple-swizzle@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz#a8d11a45a11600d6a1ecdff6363329e3648c3667" - integrity sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw== - dependencies: - is-arrayish "^0.3.1" - simplebar-core@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/simplebar-core/-/simplebar-core-1.3.2.tgz#e249caf38625afb7c316b2d219b66afd6227e301" From 49192881a81a4599d990755b85393a767c16acf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 00:39:08 +0000 Subject: [PATCH 069/133] chore(deps): bump axios from 1.15.0 to 1.16.1 Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.16.1. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.16.1) --- updated-dependencies: - dependency-name: axios dependency-version: 1.16.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 48 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 331476b49fa7..8e3d3aa004e9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@tiptap/starter-kit": "^3.20.5", "@vvo/tzdb": "^6.198.0", "apexcharts": "5.10.4", - "axios": "1.15.0", + "axios": "1.16.1", "date-fns": "4.1.0", "diff": "^8.0.3", "dompurify": "^3.4.3", diff --git a/yarn.lock b/yarn.lock index f679e9ff64b2..537fb127418e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2811,6 +2811,13 @@ acorn@^8.15.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.16.0.tgz#4ce79c89be40afe7afe8f3adb902a1f1ce9ac08a" integrity sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw== +agent-base@6: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + ajv@^6.14.0: version "6.14.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.14.0.tgz#fd067713e228210636ebb08c60bd3765d6dbe73a" @@ -2973,13 +2980,14 @@ axe-core@^4.10.0: resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.11.1.tgz#052ff9b2cbf543f5595028b583e4763b40c78ea7" integrity sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A== -axios@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.15.0.tgz#0fcee91ef03d386514474904b27863b2c683bf4f" - integrity sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q== +axios@1.16.1: + version "1.16.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.16.1.tgz#517e29291d19d6e8cf919ff264f4fe157261ba12" + integrity sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A== dependencies: - follow-redirects "^1.15.11" + follow-redirects "^1.16.0" form-data "^4.0.5" + https-proxy-agent "^5.0.1" proxy-from-env "^2.1.0" axobject-query@^4.1.0: @@ -3556,6 +3564,13 @@ date-fns@4.1.0: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14" integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg== +debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: + version "4.4.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" + integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== + dependencies: + ms "^2.1.3" + debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -3563,13 +3578,6 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.4.0, debug@^4.4.3: - version "4.4.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" - integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== - dependencies: - ms "^2.1.3" - decimal.js-light@^2.5.1: version "2.5.1" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" @@ -4360,10 +4368,10 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.4.2.tgz#f5c23c107f0f37de8dbdf24f13722b3b98d52726" integrity sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA== -follow-redirects@^1.15.11: - version "1.15.11" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" - integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== +follow-redirects@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.16.0.tgz#28474a159d3b9d11ef62050a14ed60e4df6d61bc" + integrity sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw== fontkit@^2.0.2: version "2.0.4" @@ -4763,6 +4771,14 @@ htmlparser2@^3.9.0: inherits "^2.0.1" readable-stream "^3.1.1" +https-proxy-agent@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" + integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== + dependencies: + agent-base "6" + debug "4" + hyphen@^1.6.4: version "1.14.1" resolved "https://registry.yarnpkg.com/hyphen/-/hyphen-1.14.1.tgz#c9fbd5e1af750f00d5034aa37f6ec41f95ffed93" From c04cd7c9f6d2c487440aa334d6d4f3a8c3773f67 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 00:39:27 +0000 Subject: [PATCH 070/133] chore(deps): bump @tiptap/extension-heading from 3.20.5 to 3.22.3 Bumps [@tiptap/extension-heading](https://github.com/ueberdosis/tiptap/tree/HEAD/packages/extension-heading) from 3.20.5 to 3.22.3. - [Release notes](https://github.com/ueberdosis/tiptap/releases) - [Changelog](https://github.com/ueberdosis/tiptap/blob/main/packages/extension-heading/CHANGELOG.md) - [Commits](https://github.com/ueberdosis/tiptap/commits/v3.22.3/packages/extension-heading) --- updated-dependencies: - dependency-name: "@tiptap/extension-heading" dependency-version: 3.22.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 331476b49fa7..87b02637a726 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "@tanstack/react-query-persist-client": "^5.96.2", "@tanstack/react-table": "^8.19.2", "@tiptap/core": "^3.22.3", - "@tiptap/extension-heading": "^3.4.1", + "@tiptap/extension-heading": "^3.22.3", "@tiptap/extension-table": "^3.20.5", "@tiptap/pm": "^3.22.3", "@tiptap/react": "^3.20.5", diff --git a/yarn.lock b/yarn.lock index f679e9ff64b2..71258344ca78 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2212,10 +2212,10 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-3.20.5.tgz#79a4409e81a35c9f8b664616a9b2ecbd4cb81953" integrity sha512-+aILNDO7BsXf0IJ4/0BYh570usFK3Q1t/ZQd8zhHuO2ATeWeDVu1x2F+ouFS4X8fmoCcioMzw15aoz93GET6kQ== -"@tiptap/extension-heading@^3.20.5", "@tiptap/extension-heading@^3.4.1": - version "3.20.5" - resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.20.5.tgz#e03b32ea76cdc5ce852b9979e795ee7a7f547416" - integrity sha512-zXxuIrCSpzgXzRxgCbRE8DZ/NFuinVaniE3pp/9LYAWgRlsAyko8pI2XrVvzzXmDQqRGi2HrNVkNy1yutUWSWQ== +"@tiptap/extension-heading@^3.20.5", "@tiptap/extension-heading@^3.22.3": + version "3.22.3" + resolved "https://registry.yarnpkg.com/@tiptap/extension-heading/-/extension-heading-3.22.3.tgz#13a15da1ea93c577d9b0abc5341be64bee15a363" + integrity sha512-XBHuhiEV2EEhZHpOLcplLqAmBIhJciU3I6AtwmqeEqDC0P114uMEfAO7JGlbBZdCYotNer26PKnu44TBTeNtkw== "@tiptap/extension-horizontal-rule@^3.20.5": version "3.20.5" From 0995677d8e4f32b35699ffe220c3b8726f015356 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 May 2026 00:39:39 +0000 Subject: [PATCH 071/133] chore(deps): bump react-hook-form from 7.72.0 to 7.76.1 Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.72.0 to 7.76.1. - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.72.0...v7.76.1) --- updated-dependencies: - dependency-name: react-hook-form dependency-version: 7.76.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 331476b49fa7..ccc2fbb07894 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,7 @@ "react-dom": "19.2.6", "react-dropzone": "15.0.0", "react-error-boundary": "^6.1.1", - "react-hook-form": "^7.72.0", + "react-hook-form": "^7.76.1", "react-hot-toast": "2.6.0", "react-html-parser": "^2.0.2", "react-leaflet": "5.0.0", diff --git a/yarn.lock b/yarn.lock index f679e9ff64b2..6f6187c34263 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6584,10 +6584,10 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-hook-form@^7.72.0: - version "7.72.0" - resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.72.0.tgz#995a655b894249fd8798f36383e43f55ed66ae25" - integrity sha512-V4v6jubaf6JAurEaVnT9aUPKFbNtDgohj5CIgVGyPHvT9wRx5OZHVjz31GsxnPNI278XMu+ruFz+wGOscHaLKw== +react-hook-form@^7.76.1: + version "7.76.1" + resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.76.1.tgz#0ef905eae5c17e32b6edc2ac628921917e87aafa" + integrity sha512-rYM7tPiWlu3nZchkR/ex7piyzui2vFPyaLnXnI/RnblB/L4qfMmyses8llJVtF1NpE9WBBsJlGtcSZzPCXW1qQ== react-hot-toast@2.6.0: version "2.6.0" From b8ece5c448ee814f5400dacaaee9a3590c911cde Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 28 May 2026 13:56:39 +0800 Subject: [PATCH 072/133] Update worker-health.js --- src/pages/cipp/advanced/worker-health.js | 77 +++++++++++++++++++----- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index ccc19a335570..721f6c578799 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -33,7 +33,6 @@ import { Cancel, Delete, LowPriority, - DeleteSweep, Timeline, RocketLaunch, Pause, @@ -306,6 +305,7 @@ const CompactStatsRow = ({ snapshot }) => { const bg = snapshot.BgPool || {}; const jobs = snapshot.Jobs || {}; const limiter = snapshot.Limiter || {}; + const mem = snapshot.Memory || {}; const sections = [ { @@ -351,6 +351,18 @@ const CompactStatsRow = ({ snapshot }) => { ...(limiter.IsHttpThrottled ? [{ k: "Status", v: "Throttled", w: true }] : []), ], }, + { + label: "Memory", + color: "secondary", + stats: [ + { k: "Heap", v: `${mem.HeapMB ?? 0}MB` }, + { k: "RSS", v: `${mem.RssMB ?? 0}MB`, w: mem.UsagePct > 85 }, + { k: "Committed", v: `${mem.CommittedMB ?? 0}MB` }, + { k: "Limit", v: `${mem.ContainerLimitMB ?? 0}MB` }, + { k: "Usage", v: `${mem.UsagePct ?? 0}%`, w: mem.UsagePct > 85 }, + { k: "GC", v: `${mem.GC0 ?? 0}/${mem.GC1 ?? 0}/${mem.GC2 ?? 0}` }, + ], + }, ]; return ( @@ -427,6 +439,7 @@ const Page = () => { const [historyRange, setHistoryRange] = useState(60); const [paused, setPaused] = useState(false); const [importedData, setImportedData] = useState(null); + const [jobLimit, setJobLimit] = useState(2000); const isImported = importedData !== null; const effectivePaused = paused || isImported; @@ -573,10 +586,16 @@ const Page = () => { : `${limiter.Active ?? 0} / ${limiter.CurrentMax ?? 0} active`, color: limiter.IsHttpThrottled ? "error" : "primary", }, + { + icon: , + name: "Memory", + data: `${snapshot.Memory?.RssMB ?? 0}MB / ${snapshot.Memory?.ContainerLimitMB ?? 0}MB (${snapshot.Memory?.UsagePct ?? 0}%)`, + color: (snapshot.Memory?.UsagePct ?? 0) > 85 ? "error" : (snapshot.Memory?.UsagePct ?? 0) > 70 ? "warning" : "primary", + }, ]; }, [snapshot]); - const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "WaitSeconds", "DurationSeconds"]; + const jobSimpleColumns = ["Name", "RunName", "Priority", "Status", "QueuedUtc", "WaitSeconds", "DurationSeconds"]; const jobActions = useMemo( () => [ @@ -761,30 +780,29 @@ const Page = () => { ) : ( val !== null && setJobLimit(val)} size="small" - startIcon={} - color="warning" - onClick={() => - jobAction.mutate({ - url: "/api/ListWorkerHealth", - data: { Action: "PurgeCompleted" }, - }) - } > - Purge Completed - + {[500, 2000, 5000, 10000].map((n) => ( + + {n >= 1000 ? `${n / 1000}k` : n} + + ))} + } /> )} @@ -878,6 +896,33 @@ const Page = () => { + + } + > + {(data, t) => ( + + + + + + + + + + + )} + + Date: Thu, 28 May 2026 15:21:20 +0800 Subject: [PATCH 073/133] Update worker-health.js --- src/pages/cipp/advanced/worker-health.js | 35 ++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index 721f6c578799..a058b168882e 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -360,6 +360,7 @@ const CompactStatsRow = ({ snapshot }) => { { k: "Committed", v: `${mem.CommittedMB ?? 0}MB` }, { k: "Limit", v: `${mem.ContainerLimitMB ?? 0}MB` }, { k: "Usage", v: `${mem.UsagePct ?? 0}%`, w: mem.UsagePct > 85 }, + { k: "CPU", v: `${mem.CpuPct ?? 0}%`, w: mem.CpuPct > 80 }, { k: "GC", v: `${mem.GC0 ?? 0}/${mem.GC1 ?? 0}/${mem.GC2 ?? 0}` }, ], }, @@ -592,6 +593,12 @@ const Page = () => { data: `${snapshot.Memory?.RssMB ?? 0}MB / ${snapshot.Memory?.ContainerLimitMB ?? 0}MB (${snapshot.Memory?.UsagePct ?? 0}%)`, color: (snapshot.Memory?.UsagePct ?? 0) > 85 ? "error" : (snapshot.Memory?.UsagePct ?? 0) > 70 ? "warning" : "primary", }, + { + icon: , + name: "CPU", + data: `${snapshot.Memory?.CpuPct ?? 0}%`, + color: (snapshot.Memory?.CpuPct ?? 0) > 80 ? "error" : (snapshot.Memory?.CpuPct ?? 0) > 50 ? "warning" : "primary", + }, ]; }, [snapshot]); @@ -923,6 +930,34 @@ const Page = () => { )} + + } + > + {(data, t) => ( + + + + + + + + + )} + + + + + Date: Thu, 28 May 2026 20:51:17 +0800 Subject: [PATCH 074/133] Update worker-health.js --- src/pages/cipp/advanced/worker-health.js | 58 ++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index a058b168882e..19d68519852f 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -359,9 +359,11 @@ const CompactStatsRow = ({ snapshot }) => { { k: "RSS", v: `${mem.RssMB ?? 0}MB`, w: mem.UsagePct > 85 }, { k: "Committed", v: `${mem.CommittedMB ?? 0}MB` }, { k: "Limit", v: `${mem.ContainerLimitMB ?? 0}MB` }, + { k: "GC Limit", v: `${mem.GCHeapLimitMB ?? 0}MB` }, { k: "Usage", v: `${mem.UsagePct ?? 0}%`, w: mem.UsagePct > 85 }, { k: "CPU", v: `${mem.CpuPct ?? 0}%`, w: mem.CpuPct > 80 }, { k: "GC", v: `${mem.GC0 ?? 0}/${mem.GC1 ?? 0}/${mem.GC2 ?? 0}` }, + ...(mem.TestDataCacheCount != null ? [{ k: "Cache", v: `${mem.TestDataCacheCount} entries` }] : []), ], }, ]; @@ -469,6 +471,13 @@ const Page = () => { relatedQueryKeys: ["WorkerHealthJobs", "WorkerHealth"], }); + const cacheDiagQuery = ApiGetCall({ + url: "/api/ListWorkerHealth", + data: { Action: "CacheDiag" }, + queryKey: "WorkerCacheDiag", + refetchInterval: effectivePaused ? false : 30000, + }); + // Resolve data: imported overrides live const snapshot = isImported ? importedData.snapshot : healthQuery.data?.Results; const startupInfo = isImported ? importedData.startup : startupQuery.data?.Results; @@ -1015,6 +1024,55 @@ const Page = () => { + {/* ── TestData Cache Diagnostics ── */} + {(() => { + const diag = cacheDiagQuery.data?.Results; + if (!diag) return null; + const types = diag.TypeBreakdown ?? []; + return ( + + 5000 ? "error" : diag.TotalEntries > 1000 ? "warning" : "success"} + size="small" + /> + } + /> + {types.length > 0 && ( + + + + + + Data Type + Tenants + Items + Est. MB + + + + {types.map((t) => ( + + {t.Type} + {t.EntryCount} + {t.TotalItems?.toLocaleString()} + {t.TotalMB} + + ))} + +
+
+
+ )} +
+ ); + })()} + {/* ── Startup Timing (bottom) ── */}
From 7e44aff88a1ff1fbb29e8d3a9fb9aa5c70c1b504 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 28 May 2026 15:21:13 +0200 Subject: [PATCH 075/133] new auth methods single standard --- src/data/standards.json | 347 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index 464b433e4cb6..14f0fbcef627 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -465,6 +465,353 @@ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicy", "recommendedBy": [] }, + { + "name": "standards.AuthenticationMethods", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures all authentication methods for the tenant including Microsoft Authenticator, FIDO2, SMS, Voice, Email OTP, Temporary Access Pass, Software OATH, Hardware OATH, Certificate-based, and QR Code Pin. Enable or disable each method and optionally target specific groups.", + "docsDescription": "Unified standard to configure all authentication method policies in a single place. Each method can be independently enabled or disabled, targeted to all users or specific groups using group name wildcards, and configured with method-specific settings such as TAP lifetime, QR code pin length, and Authenticator software OTP.", + "executiveText": "Provides centralized control over all tenant authentication methods from a single standard. Administrators can enable phishing-resistant methods like FIDO2 and Microsoft Authenticator while disabling less secure options like SMS and Voice. Each method supports group-level targeting using wildcard group names, allowing staged rollouts and granular control.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "label": "Microsoft Authenticator", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorSoftwareOath", + "label": "Enable Software OTP in Authenticator", + "defaultValue": false, + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Number Matching", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorNumberMatching", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Show Application Name in Push Notifications", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorDisplayAppInfo", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Show Geographic Location in Push Notifications", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorDisplayLocation", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "Companion App (Authenticator Lite)", + "name": "standards.AuthenticationMethods.MicrosoftAuthenticatorCompanionApp", + "options": [ + { "label": "Microsoft managed", "value": "default" }, + { "label": "Enabled", "value": "enabled" }, + { "label": "Disabled", "value": "disabled" } + ], + "condition": { + "field": "standards.AuthenticationMethods.MicrosoftAuthenticatorEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.FIDO2Enabled", + "label": "FIDO2 Security Keys", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.FIDO2Group", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.FIDO2Enabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.TAPEnabled", + "label": "Temporary Access Pass", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.TAPGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "autoComplete", + "multiple": false, + "creatable": false, + "label": "TAP Usage Mode", + "name": "standards.AuthenticationMethods.TAPUsableOnce", + "options": [ + { "label": "Only Once", "value": "true" }, + { "label": "Multiple Logons", "value": "false" } + ], + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPDefaultLifetime", + "label": "TAP Default Lifetime (minutes)", + "defaultValue": 60, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPMinLifetime", + "label": "TAP Minimum Lifetime (minutes)", + "defaultValue": 60, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPMaxLifetime", + "label": "TAP Maximum Lifetime (minutes)", + "defaultValue": 480, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.TAPDefaultLength", + "label": "TAP Length (characters)", + "defaultValue": 8, + "condition": { + "field": "standards.AuthenticationMethods.TAPEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.SoftwareOathEnabled", + "label": "Third-Party Software OATH Tokens", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.SoftwareOathGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.SoftwareOathEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.HardwareOathEnabled", + "label": "Hardware OATH Tokens", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.HardwareOathGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.HardwareOathEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.SMSEnabled", + "label": "SMS", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.SMSGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.SMSEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.VoiceEnabled", + "label": "Voice Call", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.VoiceGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.VoiceEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.EmailEnabled", + "label": "Email OTP", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.EmailGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.EmailEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.x509CertificateEnabled", + "label": "Certificate-Based Authentication", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.x509CertificateGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.x509CertificateEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "switch", + "name": "standards.AuthenticationMethods.QRCodePinEnabled", + "label": "QR Code Pin", + "defaultValue": false + }, + { + "type": "textField", + "name": "standards.AuthenticationMethods.QRCodePinGroup", + "label": "Target Group Name (wildcard supported, blank = All Users)", + "required": false, + "condition": { + "field": "standards.AuthenticationMethods.QRCodePinEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.QRCodeLifetimeInDays", + "label": "QR Code Lifetime (days, 1-395)", + "defaultValue": 365, + "condition": { + "field": "standards.AuthenticationMethods.QRCodePinEnabled", + "compareType": "is", + "compareValue": true + } + }, + { + "type": "number", + "name": "standards.AuthenticationMethods.QRCodePinLength", + "label": "QR Code PIN Length (8-20)", + "defaultValue": 8, + "condition": { + "field": "standards.AuthenticationMethods.QRCodePinEnabled", + "compareType": "is", + "compareValue": true + } + } + ], + "label": "Configure Authentication Methods", + "impact": "High Impact", + "impactColour": "danger", + "addedDate": "2026-05-28", + "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", + "recommendedBy": ["CIPP"] + }, { "name": "standards.AdminSSPR", "cat": "Entra (AAD) Standards", From 605ecd8272e8e6c5d6eaba7692fa0d3ddb22c745 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 28 May 2026 21:13:05 +0200 Subject: [PATCH 076/133] fix: move ADE pages --- src/layouts/config.js | 2 +- .../enrollment-profiles/android-enterprise.js | 0 .../{MEM => autopilot}/enrollment-profiles/apple-ade.js | 0 .../endpoint/{MEM => autopilot}/enrollment-profiles/index.js | 0 .../{MEM => autopilot}/enrollment-profiles/tabOptions.json | 0 .../{MEM => autopilot}/enrollment-profiles/windows-autopilot.js | 0 6 files changed, 1 insertion(+), 1 deletion(-) rename src/pages/endpoint/{MEM => autopilot}/enrollment-profiles/android-enterprise.js (100%) rename src/pages/endpoint/{MEM => autopilot}/enrollment-profiles/apple-ade.js (100%) rename src/pages/endpoint/{MEM => autopilot}/enrollment-profiles/index.js (100%) rename src/pages/endpoint/{MEM => autopilot}/enrollment-profiles/tabOptions.json (100%) rename src/pages/endpoint/{MEM => autopilot}/enrollment-profiles/windows-autopilot.js (100%) diff --git a/src/layouts/config.js b/src/layouts/config.js index d5f25685c697..7a41ee8ca57f 100644 --- a/src/layouts/config.js +++ b/src/layouts/config.js @@ -497,7 +497,7 @@ export const nativeMenuItems = [ }, { title: 'Enrollment Profiles', - path: '/endpoint/MEM/enrollment-profiles', + path: '/endpoint/autopilot/enrollment-profiles', permissions: ['Endpoint.Autopilot.*'], }, { diff --git a/src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/autopilot/enrollment-profiles/android-enterprise.js similarity index 100% rename from src/pages/endpoint/MEM/enrollment-profiles/android-enterprise.js rename to src/pages/endpoint/autopilot/enrollment-profiles/android-enterprise.js diff --git a/src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js b/src/pages/endpoint/autopilot/enrollment-profiles/apple-ade.js similarity index 100% rename from src/pages/endpoint/MEM/enrollment-profiles/apple-ade.js rename to src/pages/endpoint/autopilot/enrollment-profiles/apple-ade.js diff --git a/src/pages/endpoint/MEM/enrollment-profiles/index.js b/src/pages/endpoint/autopilot/enrollment-profiles/index.js similarity index 100% rename from src/pages/endpoint/MEM/enrollment-profiles/index.js rename to src/pages/endpoint/autopilot/enrollment-profiles/index.js diff --git a/src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json b/src/pages/endpoint/autopilot/enrollment-profiles/tabOptions.json similarity index 100% rename from src/pages/endpoint/MEM/enrollment-profiles/tabOptions.json rename to src/pages/endpoint/autopilot/enrollment-profiles/tabOptions.json diff --git a/src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/autopilot/enrollment-profiles/windows-autopilot.js similarity index 100% rename from src/pages/endpoint/MEM/enrollment-profiles/windows-autopilot.js rename to src/pages/endpoint/autopilot/enrollment-profiles/windows-autopilot.js From 063550f430ee43d716ae24a1e7834d10e2aa3014 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 28 May 2026 21:20:53 +0200 Subject: [PATCH 077/133] chore: update tab paths and imports --- .../autopilot/enrollment-profiles/android-enterprise.js | 2 +- .../endpoint/autopilot/enrollment-profiles/apple-ade.js | 2 +- src/pages/endpoint/autopilot/enrollment-profiles/index.js | 2 +- .../endpoint/autopilot/enrollment-profiles/tabOptions.json | 6 +++--- .../autopilot/enrollment-profiles/windows-autopilot.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pages/endpoint/autopilot/enrollment-profiles/android-enterprise.js b/src/pages/endpoint/autopilot/enrollment-profiles/android-enterprise.js index 88f86700374a..1718233d4b09 100644 --- a/src/pages/endpoint/autopilot/enrollment-profiles/android-enterprise.js +++ b/src/pages/endpoint/autopilot/enrollment-profiles/android-enterprise.js @@ -1,5 +1,5 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' -import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { TabbedLayout } from '../../../../layouts/TabbedLayout.jsx' import { AndroidEnterpriseEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' diff --git a/src/pages/endpoint/autopilot/enrollment-profiles/apple-ade.js b/src/pages/endpoint/autopilot/enrollment-profiles/apple-ade.js index 2225de59f0a8..f03c313db2fd 100644 --- a/src/pages/endpoint/autopilot/enrollment-profiles/apple-ade.js +++ b/src/pages/endpoint/autopilot/enrollment-profiles/apple-ade.js @@ -1,5 +1,5 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' -import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { TabbedLayout } from '../../../../layouts/TabbedLayout.jsx' import { AppleADEEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' diff --git a/src/pages/endpoint/autopilot/enrollment-profiles/index.js b/src/pages/endpoint/autopilot/enrollment-profiles/index.js index a2cf307e80d2..00874da7788a 100644 --- a/src/pages/endpoint/autopilot/enrollment-profiles/index.js +++ b/src/pages/endpoint/autopilot/enrollment-profiles/index.js @@ -1,5 +1,5 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' -import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { TabbedLayout } from '../../../../layouts/TabbedLayout.jsx' import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' diff --git a/src/pages/endpoint/autopilot/enrollment-profiles/tabOptions.json b/src/pages/endpoint/autopilot/enrollment-profiles/tabOptions.json index 66a042cbd2ad..83abfededa9e 100644 --- a/src/pages/endpoint/autopilot/enrollment-profiles/tabOptions.json +++ b/src/pages/endpoint/autopilot/enrollment-profiles/tabOptions.json @@ -1,17 +1,17 @@ [ { "label": "Windows Autopilot", - "path": "/endpoint/MEM/enrollment-profiles", + "path": "/endpoint/autopilot/enrollment-profiles", "icon": "Window" }, { "label": "Apple ADE", - "path": "/endpoint/MEM/enrollment-profiles/apple-ade", + "path": "/endpoint/autopilot/enrollment-profiles/apple-ade", "icon": "Apple" }, { "label": "Android Enterprise", - "path": "/endpoint/MEM/enrollment-profiles/android-enterprise", + "path": "/endpoint/autopilot/enrollment-profiles/android-enterprise", "icon": "Android" } ] diff --git a/src/pages/endpoint/autopilot/enrollment-profiles/windows-autopilot.js b/src/pages/endpoint/autopilot/enrollment-profiles/windows-autopilot.js index a2cf307e80d2..00874da7788a 100644 --- a/src/pages/endpoint/autopilot/enrollment-profiles/windows-autopilot.js +++ b/src/pages/endpoint/autopilot/enrollment-profiles/windows-autopilot.js @@ -1,5 +1,5 @@ import { Layout as DashboardLayout } from '../../../../layouts/index.js' -import { TabbedLayout } from '../../../../layouts/TabbedLayout' +import { TabbedLayout } from '../../../../layouts/TabbedLayout.jsx' import { WindowsAutopilotEnrollmentProfiles } from '../../../../components/CippComponents/EnrollmentProfileTabs.jsx' import tabOptions from './tabOptions.json' From d0405ff45e5ad572438e007734a93f2b5a3b69f0 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 28 May 2026 22:45:31 +0200 Subject: [PATCH 078/133] feat: Add icons to the tabs and remove dead tab Add the icons into the registry too if they are not already there. --- .../cipp/advanced/super-admin/tabOptions.json | 27 +- src/pages/cipp/custom-data/tabOptions.json | 9 +- src/pages/cipp/settings/tabOptions.json | 29 +- src/pages/dashboardv2/tabOptions.json | 15 +- .../exchange-retention/tabOptions.json | 6 +- .../MEM/devices/device/tabOptions.json | 3 +- .../groups/group/tabOptions.json | 3 +- .../administration/users/user/devices.jsx | 380 ------------------ .../administration/users/user/tabOptions.json | 15 +- .../alert-configuration/tabOptions.json | 6 +- .../app-registration/tabOptions.json | 6 +- .../enterprise-app/tabOptions.json | 6 +- .../applications/tabOptions.json | 14 +- .../administration/audit-logs/tabOptions.json | 11 +- .../securescore/tabOptions.json | 6 +- .../administration/tenants/tabOptions.json | 9 +- .../relationship/tabOptions.json | 6 +- .../tenant/gdap-management/tabOptions.json | 21 +- src/pages/tenant/manage/tabOptions.json | 21 +- src/pages/tenant/standards/tabOptions.json | 8 +- .../tools/report-builder/tabOptions.json | 6 +- src/utils/icon-registry.js | 10 + 22 files changed, 160 insertions(+), 457 deletions(-) delete mode 100644 src/pages/identity/administration/users/user/devices.jsx diff --git a/src/pages/cipp/advanced/super-admin/tabOptions.json b/src/pages/cipp/advanced/super-admin/tabOptions.json index fbccb6b73c55..e0d9e9bad597 100644 --- a/src/pages/cipp/advanced/super-admin/tabOptions.json +++ b/src/pages/cipp/advanced/super-admin/tabOptions.json @@ -1,38 +1,47 @@ [ { "label": "Tenant Mode", - "path": "/cipp/advanced/super-admin/tenant-mode" + "path": "/cipp/advanced/super-admin/tenant-mode", + "icon": "Domain" }, { "label": "Function Offloading", - "path": "/cipp/advanced/super-admin/function-offloading" + "path": "/cipp/advanced/super-admin/function-offloading", + "icon": "Cloud" }, { "label": "Time Settings", - "path": "/cipp/advanced/super-admin/time-settings" + "path": "/cipp/advanced/super-admin/time-settings", + "icon": "AccessTime" }, { "label": "CIPP Roles", - "path": "/cipp/advanced/super-admin/cipp-roles" + "path": "/cipp/advanced/super-admin/cipp-roles", + "icon": "AdminPanelSettings" }, { "label": "SAM App Roles", - "path": "/cipp/advanced/super-admin/sam-app-roles" + "path": "/cipp/advanced/super-admin/sam-app-roles", + "icon": "Key" }, { "label": "SAM App Permissions", - "path": "/cipp/advanced/super-admin/sam-app-permissions" + "path": "/cipp/advanced/super-admin/sam-app-permissions", + "icon": "Lock" }, { "label": "CIPP Users", - "path": "/cipp/advanced/super-admin/cipp-users" + "path": "/cipp/advanced/super-admin/cipp-users", + "icon": "Group" }, { "label": "SSO", - "path": "/cipp/advanced/super-admin/sso" + "path": "/cipp/advanced/super-admin/sso", + "icon": "Shield" }, { "label": "Container Management", - "path": "/cipp/advanced/super-admin/container" + "path": "/cipp/advanced/super-admin/container", + "icon": "Storage" } ] diff --git a/src/pages/cipp/custom-data/tabOptions.json b/src/pages/cipp/custom-data/tabOptions.json index de0055483d82..bc87adf4dc60 100644 --- a/src/pages/cipp/custom-data/tabOptions.json +++ b/src/pages/cipp/custom-data/tabOptions.json @@ -1,14 +1,17 @@ [ { "label": "Directory Extensions", - "path": "/cipp/custom-data/directory-extensions" + "path": "/cipp/custom-data/directory-extensions", + "icon": "Dns" }, { "label": "Schema Extensions", - "path": "/cipp/custom-data/schema-extensions" + "path": "/cipp/custom-data/schema-extensions", + "icon": "Description" }, { "label": "Mappings", - "path": "/cipp/custom-data/mappings" + "path": "/cipp/custom-data/mappings", + "icon": "Share" } ] diff --git a/src/pages/cipp/settings/tabOptions.json b/src/pages/cipp/settings/tabOptions.json index 143f94ffbf49..da85db4cab73 100644 --- a/src/pages/cipp/settings/tabOptions.json +++ b/src/pages/cipp/settings/tabOptions.json @@ -1,38 +1,47 @@ [ { "label": "General", - "path": "/cipp/settings" + "path": "/cipp/settings", + "icon": "Settings" }, { "label": "Permissions", - "path": "/cipp/settings/permissions" + "path": "/cipp/settings/permissions", + "icon": "Key" }, { "label": "Tenants", - "path": "/cipp/settings/tenants" + "path": "/cipp/settings/tenants", + "icon": "Domain" }, { "label": "Backend", - "path": "/cipp/settings/backend" + "path": "/cipp/settings/backend", + "icon": "Cloud" }, { "label": "Notifications", - "path": "/cipp/settings/notifications" + "path": "/cipp/settings/notifications", + "icon": "Notifications" }, { "label": "Automated Onboarding", - "path": "/cipp/settings/partner-webhooks" + "path": "/cipp/settings/partner-webhooks", + "icon": "AutoMode" }, { "label": "Licenses", - "path": "/cipp/settings/licenses" + "path": "/cipp/settings/licenses", + "icon": "Assignment" }, { "label": "Features", - "path": "/cipp/settings/features" + "path": "/cipp/settings/features", + "icon": "Apps" }, { "label": "SIEM", - "path": "/cipp/settings/siem" + "path": "/cipp/settings/siem", + "icon": "Security" } -] \ No newline at end of file +] diff --git a/src/pages/dashboardv2/tabOptions.json b/src/pages/dashboardv2/tabOptions.json index 752f38c9d419..6014478ba77b 100644 --- a/src/pages/dashboardv2/tabOptions.json +++ b/src/pages/dashboardv2/tabOptions.json @@ -1,22 +1,27 @@ [ { "label": "Overview", - "path": "/dashboardv2" + "path": "/dashboardv2", + "icon": "Dashboard" }, { "label": "Identity", - "path": "/dashboardv2/identity" + "path": "/dashboardv2/identity", + "icon": "Person" }, { "label": "Devices", - "path": "/dashboardv2/devices" + "path": "/dashboardv2/devices", + "icon": "Devices" }, { "label": "Custom", - "path": "/dashboardv2/custom" + "path": "/dashboardv2/custom", + "icon": "Apps" }, { "label": "Previous Dashboard Experience", - "path": "/dashboardv1" + "path": "/dashboardv1", + "icon": "History" } ] diff --git a/src/pages/email/administration/exchange-retention/tabOptions.json b/src/pages/email/administration/exchange-retention/tabOptions.json index e6e203b5c611..f895eaae68c5 100644 --- a/src/pages/email/administration/exchange-retention/tabOptions.json +++ b/src/pages/email/administration/exchange-retention/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "Policies", - "path": "/email/administration/exchange-retention/policies" + "path": "/email/administration/exchange-retention/policies", + "icon": "Policy" }, { "label": "Tags", - "path": "/email/administration/exchange-retention/tags" + "path": "/email/administration/exchange-retention/tags", + "icon": "Label" } ] diff --git a/src/pages/endpoint/MEM/devices/device/tabOptions.json b/src/pages/endpoint/MEM/devices/device/tabOptions.json index e5e134f5566a..558cbe2391bb 100644 --- a/src/pages/endpoint/MEM/devices/device/tabOptions.json +++ b/src/pages/endpoint/MEM/devices/device/tabOptions.json @@ -1,6 +1,7 @@ [ { "label": "View Device", - "path": "/endpoint/MEM/devices/device" + "path": "/endpoint/MEM/devices/device", + "icon": "Computer" } ] diff --git a/src/pages/identity/administration/groups/group/tabOptions.json b/src/pages/identity/administration/groups/group/tabOptions.json index f092f4cb37d6..18fa62ad879f 100644 --- a/src/pages/identity/administration/groups/group/tabOptions.json +++ b/src/pages/identity/administration/groups/group/tabOptions.json @@ -1,6 +1,7 @@ [ { "label": "View Group", - "path": "/identity/administration/groups/group" + "path": "/identity/administration/groups/group", + "icon": "Group" } ] diff --git a/src/pages/identity/administration/users/user/devices.jsx b/src/pages/identity/administration/users/user/devices.jsx deleted file mode 100644 index 9fcb9086bdbc..000000000000 --- a/src/pages/identity/administration/users/user/devices.jsx +++ /dev/null @@ -1,380 +0,0 @@ -import { Layout as DashboardLayout } from "../../../../../layouts/index.js"; -import { useSettings } from "../../../../../hooks/use-settings"; -import { useRouter } from "next/router"; -import { ApiGetCall } from "../../../../../api/ApiCall"; -import CippFormSkeleton from "../../../../../components/CippFormPages/CippFormSkeleton"; -import CalendarIcon from "@heroicons/react/24/outline/CalendarIcon"; -import { Check, Mail, Fingerprint } from "@mui/icons-material"; -import { HeaderedTabbedLayout } from "../../../../../layouts/HeaderedTabbedLayout"; -import tabOptions from "./tabOptions"; -import ReactTimeAgo from "react-time-ago"; -import { CippCopyToClipBoard } from "../../../../../components/CippComponents/CippCopyToClipboard"; -import { Box, Stack } from "@mui/system"; -import { Grid } from "@mui/system"; -import { CippUserInfoCard } from "../../../../../components/CippCards/CippUserInfoCard"; -import { Typography } from "@mui/material"; -import { CippBannerListCard } from "../../../../../components/CippCards/CippBannerListCard"; - -const Page = () => { - const userSettingsDefaults = useSettings(); - const router = useRouter(); - const { userId } = router.query; - - const userRequest = ApiGetCall({ - url: `/api/ListUsers?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}`, - queryKey: `ListUsers-${userId}`, - }); - - const MFARequest = ApiGetCall({ - url: "/api/ListGraphRequest", - data: { - Endpoint: `/users/${userId}/authentication/methods`, - tenantFilter: userSettingsDefaults.currentTenant, - noPagination: true, - $top: 99, - }, - queryKey: `MFA-${userId}`, - }); - - const signInLogs = ApiGetCall({ - url: `/api/ListUserSigninLogs?UserId=${userId}&tenantFilter=${userSettingsDefaults.currentTenant}&top=1`, - queryKey: `ListSignIns-${userId}`, - }); - - // Set the title and subtitle for the layout - const title = userRequest.isSuccess ? userRequest.data?.[0]?.displayName : "Loading..."; - - const subtitle = userRequest.isSuccess - ? [ - { - icon: , - text: , - }, - { - icon: , - text: , - }, - { - icon: , - text: ( - <> - Created: {" "} - - ), - }, - ] - : []; - - const data = userRequest.data?.[0]; - - // Prepare the sign-in log item - let signInLogItem = null; - let conditionalAccessPoliciesItems = []; - let mfaDevicesItems = []; - - if (signInLogs.isSuccess && signInLogs.data && signInLogs.data.length > 0) { - const signInData = signInLogs.data[0]; - - signInLogItem = { - id: 1, - cardLabelBox: { - cardLabelBoxHeader: new Date(signInData.createdDateTime).getDate().toString(), - cardLabelBoxText: new Date(signInData.createdDateTime).toLocaleString("default", { - month: "short", - year: "numeric", - }), - }, - text: `Login ${signInData.status.errorCode === 0 ? "successful" : "failed"} from ${ - signInData.ipAddress || "unknown location" - }`, - subtext: `Logged into application ${signInData.resourceDisplayName || "Unknown Application"}`, - statusColor: signInData.status.errorCode === 0 ? "success.main" : "error.main", - statusText: signInData.status.errorCode === 0 ? "Success" : "Failed", - propertyItems: [ - { - label: "Client App Used", - value: signInData.clientAppUsed || "N/A", - }, - { - label: "Device Detail", - value: - signInData.deviceDetail?.operatingSystem || signInData.deviceDetail?.browser || "N/A", - }, - { - label: "MFA Type used", - value: signInData.mfaDetail?.authMethod || "N/A", - }, - { - label: "Additional Details", - value: signInData.status?.additionalDetails || "N/A", - }, - ], - }; - - // Prepare the conditional access policies items - if ( - signInData.appliedConditionalAccessPolicies && - Array.isArray(signInData.appliedConditionalAccessPolicies) - ) { - // Filter policies where result is "success" - const appliedPolicies = signInData.appliedConditionalAccessPolicies.filter( - (policy) => policy.result === "success" - ); - - if (appliedPolicies.length > 0) { - conditionalAccessPoliciesItems = appliedPolicies.map((policy) => ({ - id: policy.id, - cardLabelBox: { - cardLabelBoxHeader: new Date(signInData.createdDateTime).getDate().toString(), - cardLabelBoxText: new Date(signInData.createdDateTime).toLocaleString("default", { - month: "short", - year: "numeric", - }), - }, - text: policy.displayName, - subtext: `Policy applied: ${policy.result}`, - statusColor: "success.main", - statusText: "Applied", - propertyItems: [ - { - label: "Grant Controls", - value: - policy.enforcedGrantControls.length > 0 - ? policy.enforcedGrantControls.join(", ") - : "None", - }, - { - label: "Session Controls", - value: - policy.enforcedSessionControls.length > 0 - ? policy.enforcedSessionControls.join(", ") - : "None", - }, - { - label: "Conditions Satisfied", - value: policy.conditionsSatisfied || "N/A", - }, - ], - })); - } else { - // No applied policies - conditionalAccessPoliciesItems = [ - { - id: 1, - cardLabelBox: { - cardLabelBoxHeader: new Date(signInData.createdDateTime).getDate().toString(), - cardLabelBoxText: new Date(signInData.createdDateTime).toLocaleString("default", { - month: "short", - year: "numeric", - }), - }, - text: "No conditional access policies applied", - subtext: "No conditional access policies were applied during this sign-in.", - statusColor: "warning.main", - statusText: "No Policies Applied", - propertyItems: [], - }, - ]; - } - } else { - // appliedConditionalAccessPolicies is missing or not an array - conditionalAccessPoliciesItems = [ - { - id: 1, - cardLabelBox: { - cardLabelBoxHeader: new Date(signInData.createdDateTime).getDate().toString(), - cardLabelBoxText: new Date(signInData.createdDateTime).toLocaleString("default", { - month: "short", - year: "numeric", - }), - }, - text: "No conditional access policies available", - subtext: "No conditional access policies data is available for this sign-in.", - statusColor: "warning.main", - statusText: "No Data", - propertyItems: [], - }, - ]; - } - } else if (signInLogs.isError) { - signInLogItem = { - id: 1, - cardLabelBox: "!", - text: "Error loading sign-in logs. Do you have a P1 license?", - subtext: signInLogs.error.message, - statusColor: "error.main", - statusText: "Error", - propertyItems: [], - }; - - // Handle error for conditional access policies - conditionalAccessPoliciesItems = [ - { - id: 1, - cardLabelBox: "!", - text: "Error loading conditional access policies. Do you have a P1 license?", - subtext: signInLogs.error.message, - statusColor: "error.main", - statusText: "Error", - propertyItems: [], - }, - ]; - } else if (signInLogs.isSuccess && (!signInLogs.data || signInLogs.data.length === 0)) { - signInLogItem = { - id: 1, - cardLabelBox: "-", - text: "No sign-in logs available", - subtext: - "There are no sign-in logs for this user, or you do not have a P1 license to detect this data.", - statusColor: "warning.main", - statusText: "No Data", - propertyItems: [], - }; - - conditionalAccessPoliciesItems = [ - { - id: 1, - cardLabelBox: "-", - text: "No conditional access policies available", - subtext: - "There are no conditional access policies for this user, or you do not have a P1 license to detect this data.", - statusColor: "warning.main", - statusText: "No Data", - propertyItems: [], - }, - ]; - } - - // Prepare MFA devices items - if (MFARequest.isSuccess && MFARequest.data) { - const mfaResults = MFARequest.data.Results || []; - - // Exclude password authentication method - const mfaDevices = mfaResults.filter( - (method) => method["@odata.type"] !== "#microsoft.graph.passwordAuthenticationMethod" - ); - - if (mfaDevices.length > 0) { - mfaDevicesItems = mfaDevices.map((device, index) => ({ - id: index, - cardLabelBox: { - cardLabelBoxHeader: , - }, - text: device.displayName || "MFA Device", - subtext: device.deviceTag || device.clientAppName || "Unknown device", - statusColor: "success.main", - statusText: "Enabled", - propertyItems: [ - { - label: "Device Name", - value: device.displayName || "N/A", - }, - { - label: "App Version", - value: device.phoneAppVersion || "N/A", - }, - { - label: "Created Date", - value: device.createdDateTime - ? new Date(device.createdDateTime).toLocaleString() - : "N/A", - }, - { - label: "Authentication Method", - value: device["@odata.type"]?.split(".").pop() || "N/A", - }, - ], - })); - } else { - // No MFA devices other than password - mfaDevicesItems = [ - { - id: 1, - cardLabelBox: "-", - text: "No MFA devices available", - subtext: "The user does not have any MFA devices registered.", - statusColor: "warning.main", - statusText: "No Devices", - propertyItems: [], - }, - ]; - } - } else if (MFARequest.isError) { - // Error fetching MFA devices - mfaDevicesItems = [ - { - id: 1, - cardLabelBox: "!", - text: "Error loading MFA devices", - subtext: MFARequest.error.message, - statusColor: "error.main", - statusText: "Error", - propertyItems: [], - }, - ]; - } else if (MFARequest.isSuccess && (!MFARequest.data || !MFARequest.data.Results)) { - // No MFA devices data available - mfaDevicesItems = [ - { - id: 1, - cardLabelBox: "-", - text: "No MFA devices available", - subtext: "The user does not have any MFA devices registered.", - statusColor: "warning.main", - statusText: "No Devices", - propertyItems: [], - }, - ]; - } - - return ( - - {userRequest.isLoading && } - {userRequest.isSuccess && ( - - - - - - - - Latest Logon - - Applied Conditional Access Policies - 0 ? true : false} - /> - Multi-Factor Authentication Devices - 0 ? true : false} - /> - - - - - )} - - ); -}; - -Page.getLayout = (page) => {page}; - -export default Page; diff --git a/src/pages/identity/administration/users/user/tabOptions.json b/src/pages/identity/administration/users/user/tabOptions.json index 5ef89715dfa8..61fd2ffbe806 100644 --- a/src/pages/identity/administration/users/user/tabOptions.json +++ b/src/pages/identity/administration/users/user/tabOptions.json @@ -1,22 +1,27 @@ [ { "label": "View User", - "path": "/identity/administration/users/user" + "path": "/identity/administration/users/user", + "icon": "Person" }, { "label": "Edit User", - "path": "/identity/administration/users/user/edit" + "path": "/identity/administration/users/user/edit", + "icon": "ManageAccounts" }, { "label": "Exchange Settings", - "path": "/identity/administration/users/user/exchange" + "path": "/identity/administration/users/user/exchange", + "icon": "Mail" }, { "label": "Compromise Remediation", - "path": "/identity/administration/users/user/bec" + "path": "/identity/administration/users/user/bec", + "icon": "Shield" }, { "label": "Conditional Access", - "path": "/identity/administration/users/user/conditional-access" + "path": "/identity/administration/users/user/conditional-access", + "icon": "Lock" } ] diff --git a/src/pages/tenant/administration/alert-configuration/tabOptions.json b/src/pages/tenant/administration/alert-configuration/tabOptions.json index 0aef9ef1ba5f..480e89d7cdfe 100644 --- a/src/pages/tenant/administration/alert-configuration/tabOptions.json +++ b/src/pages/tenant/administration/alert-configuration/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "Alert Configuration", - "path": "/tenant/administration/alert-configuration" + "path": "/tenant/administration/alert-configuration", + "icon": "Notifications" }, { "label": "Snoozed Alerts", - "path": "/tenant/administration/alert-configuration/snoozed-alerts" + "path": "/tenant/administration/alert-configuration/snoozed-alerts", + "icon": "ShieldMoon" } ] diff --git a/src/pages/tenant/administration/applications/app-registration/tabOptions.json b/src/pages/tenant/administration/applications/app-registration/tabOptions.json index bcc5b9cbaa61..458efb66de5f 100644 --- a/src/pages/tenant/administration/applications/app-registration/tabOptions.json +++ b/src/pages/tenant/administration/applications/app-registration/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "View App Registration", - "path": "/tenant/administration/applications/app-registration" + "path": "/tenant/administration/applications/app-registration", + "icon": "Apps" }, { "label": "Permissions", - "path": "/tenant/administration/applications/app-registration/permissions" + "path": "/tenant/administration/applications/app-registration/permissions", + "icon": "Key" } ] diff --git a/src/pages/tenant/administration/applications/enterprise-app/tabOptions.json b/src/pages/tenant/administration/applications/enterprise-app/tabOptions.json index c0d82bfd8dad..346d9639fca1 100644 --- a/src/pages/tenant/administration/applications/enterprise-app/tabOptions.json +++ b/src/pages/tenant/administration/applications/enterprise-app/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "View Enterprise App", - "path": "/tenant/administration/applications/enterprise-app" + "path": "/tenant/administration/applications/enterprise-app", + "icon": "Business" }, { "label": "Permissions", - "path": "/tenant/administration/applications/enterprise-app/permissions" + "path": "/tenant/administration/applications/enterprise-app/permissions", + "icon": "Key" } ] diff --git a/src/pages/tenant/administration/applications/tabOptions.json b/src/pages/tenant/administration/applications/tabOptions.json index 05bafb200179..2f138f75fd29 100644 --- a/src/pages/tenant/administration/applications/tabOptions.json +++ b/src/pages/tenant/administration/applications/tabOptions.json @@ -1,18 +1,22 @@ [ { "label": "Enterprise Apps", - "path": "/tenant/administration/applications/enterprise-apps" + "path": "/tenant/administration/applications/enterprise-apps", + "icon": "Business" }, { "label": "App Registrations", - "path": "/tenant/administration/applications/app-registrations" + "path": "/tenant/administration/applications/app-registrations", + "icon": "Apps" }, { "label": "Permission Sets", - "path": "/tenant/administration/applications/permission-sets" + "path": "/tenant/administration/applications/permission-sets", + "icon": "Key" }, { "label": "Application Templates", - "path": "/tenant/administration/applications/templates" + "path": "/tenant/administration/applications/templates", + "icon": "Description" } -] \ No newline at end of file +] diff --git a/src/pages/tenant/administration/audit-logs/tabOptions.json b/src/pages/tenant/administration/audit-logs/tabOptions.json index 8c90389d9c68..9c5bf289488d 100644 --- a/src/pages/tenant/administration/audit-logs/tabOptions.json +++ b/src/pages/tenant/administration/audit-logs/tabOptions.json @@ -1,14 +1,17 @@ [ { "label": "Saved Logs", - "path": "/tenant/administration/audit-logs" + "path": "/tenant/administration/audit-logs", + "icon": "Storage" }, { "label": "Log Searches", - "path": "/tenant/administration/audit-logs/searches" + "path": "/tenant/administration/audit-logs/searches", + "icon": "List" }, { "label": "Directory Audits", - "path": "/tenant/administration/audit-logs/directory-audits" + "path": "/tenant/administration/audit-logs/directory-audits", + "icon": "FactCheck" } -] \ No newline at end of file +] diff --git a/src/pages/tenant/administration/securescore/tabOptions.json b/src/pages/tenant/administration/securescore/tabOptions.json index 1f7e8795f144..b4a4460ad2ec 100644 --- a/src/pages/tenant/administration/securescore/tabOptions.json +++ b/src/pages/tenant/administration/securescore/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "Tenant Overview", - "path": "/tenant/administration/securescore" + "path": "/tenant/administration/securescore", + "icon": "Dashboard" }, { "label": "Table Overview", - "path": "/tenant/administration/securescore/table" + "path": "/tenant/administration/securescore/table", + "icon": "BarChart" } ] diff --git a/src/pages/tenant/administration/tenants/tabOptions.json b/src/pages/tenant/administration/tenants/tabOptions.json index f36fc14e96d0..9bb186e4211e 100644 --- a/src/pages/tenant/administration/tenants/tabOptions.json +++ b/src/pages/tenant/administration/tenants/tabOptions.json @@ -1,14 +1,17 @@ [ { "label": "Tenants", - "path": "/tenant/administration/tenants" + "path": "/tenant/administration/tenants", + "icon": "Domain" }, { "label": "Groups", - "path": "/tenant/administration/tenants/groups" + "path": "/tenant/administration/tenants/groups", + "icon": "Groups" }, { "label": "Global Variables", - "path": "/tenant/administration/tenants/global-variables" + "path": "/tenant/administration/tenants/global-variables", + "icon": "Settings" } ] diff --git a/src/pages/tenant/gdap-management/relationships/relationship/tabOptions.json b/src/pages/tenant/gdap-management/relationships/relationship/tabOptions.json index 5977d35a1f06..2f22feb09d01 100644 --- a/src/pages/tenant/gdap-management/relationships/relationship/tabOptions.json +++ b/src/pages/tenant/gdap-management/relationships/relationship/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "Overview", - "path": "/tenant/gdap-management/relationships/relationship" + "path": "/tenant/gdap-management/relationships/relationship", + "icon": "Dashboard" }, { "label": "Mappings", - "path": "/tenant/gdap-management/relationships/relationship/mappings" + "path": "/tenant/gdap-management/relationships/relationship/mappings", + "icon": "Share" } ] diff --git a/src/pages/tenant/gdap-management/tabOptions.json b/src/pages/tenant/gdap-management/tabOptions.json index 20815ed009d0..3b41dcd84366 100644 --- a/src/pages/tenant/gdap-management/tabOptions.json +++ b/src/pages/tenant/gdap-management/tabOptions.json @@ -1,30 +1,37 @@ [ { "label": "Overview", - "path": "/tenant/gdap-management" + "path": "/tenant/gdap-management", + "icon": "Dashboard" }, { "label": "Relationships", - "path": "/tenant/gdap-management/relationships" + "path": "/tenant/gdap-management/relationships", + "icon": "Share" }, { "label": "Role Mappings", - "path": "/tenant/gdap-management/roles" + "path": "/tenant/gdap-management/roles", + "icon": "AdminPanelSettings" }, { "label": "Role Templates", - "path": "/tenant/gdap-management/role-templates" + "path": "/tenant/gdap-management/role-templates", + "icon": "Description" }, { "label": "Invites", - "path": "/tenant/gdap-management/invites" + "path": "/tenant/gdap-management/invites", + "icon": "Mail" }, { "label": "Onboarding", - "path": "/tenant/gdap-management/onboarding" + "path": "/tenant/gdap-management/onboarding", + "icon": "CheckCircle" }, { "label": "Offboarding", - "path": "/tenant/gdap-management/offboarding" + "path": "/tenant/gdap-management/offboarding", + "icon": "Warning" } ] diff --git a/src/pages/tenant/manage/tabOptions.json b/src/pages/tenant/manage/tabOptions.json index adf60d405b09..9fc74d76d3ed 100644 --- a/src/pages/tenant/manage/tabOptions.json +++ b/src/pages/tenant/manage/tabOptions.json @@ -1,30 +1,37 @@ [ { "label": "Edit Tenant", - "path": "/tenant/manage/edit" + "path": "/tenant/manage/edit", + "icon": "Settings" }, { "label": "Manage Drift", - "path": "/tenant/manage/drift" + "path": "/tenant/manage/drift", + "icon": "Sync" }, { "label": "Configuration Backup", - "path": "/tenant/manage/configuration-backup" + "path": "/tenant/manage/configuration-backup", + "icon": "Storage" }, { "label": "Applied Standards Report", - "path": "/tenant/manage/applied-standards" + "path": "/tenant/manage/applied-standards", + "icon": "FactCheck" }, { "label": "Policies and Settings Deployed", - "path": "/tenant/manage/policies-deployed" + "path": "/tenant/manage/policies-deployed", + "icon": "Policy" }, { "label": "User Defaults", - "path": "/tenant/manage/user-defaults" + "path": "/tenant/manage/user-defaults", + "icon": "Person" }, { "label": "History", - "path": "/tenant/manage/history" + "path": "/tenant/manage/history", + "icon": "Timeline" } ] diff --git a/src/pages/tenant/standards/tabOptions.json b/src/pages/tenant/standards/tabOptions.json index 26c6c751dc1a..59c2cfa5df76 100644 --- a/src/pages/tenant/standards/tabOptions.json +++ b/src/pages/tenant/standards/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "Standard & Drift Alignment", - "path": "/tenant/standards/alignment" + "path": "/tenant/standards/alignment", + "icon": "FactCheck" }, { "label": "Templates", - "path": "/tenant/standards/templates" + "path": "/tenant/standards/templates", + "icon": "Description" } -] \ No newline at end of file +] diff --git a/src/pages/tools/report-builder/tabOptions.json b/src/pages/tools/report-builder/tabOptions.json index 18f2988321a8..c1f80bcc4890 100644 --- a/src/pages/tools/report-builder/tabOptions.json +++ b/src/pages/tools/report-builder/tabOptions.json @@ -1,10 +1,12 @@ [ { "label": "Generated Reports", - "path": "/tools/report-builder/generated" + "path": "/tools/report-builder/generated", + "icon": "BarChart" }, { "label": "Templates", - "path": "/tools/report-builder/templates" + "path": "/tools/report-builder/templates", + "icon": "Description" } ] diff --git a/src/utils/icon-registry.js b/src/utils/icon-registry.js index 2863958edbb4..5f288e36c87d 100644 --- a/src/utils/icon-registry.js +++ b/src/utils/icon-registry.js @@ -1,4 +1,9 @@ import { + AccessTime, + AutoMode, + History, + Label, + Webhook, AdminPanelSettings, Android, Apple, @@ -44,6 +49,7 @@ import { } from '@mui/icons-material' export const iconRegistry = { + AccessTime, AdminPanelSettings, Android, Apple, @@ -82,8 +88,12 @@ export const iconRegistry = { Shield, ShieldMoon, Storage, + AutoMode, + History, + Label, Sync, Timeline, + Webhook, Window, Warning, } From 707873e31e3b9fae7504dea81643f7bf673a1bef Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 28 May 2026 23:24:30 +0200 Subject: [PATCH 079/133] fix: Fix tab title showing as undefined --- .../email/administration/exchange-retention/policies/index.js | 1 + src/pages/email/administration/exchange-retention/tags/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pages/email/administration/exchange-retention/policies/index.js b/src/pages/email/administration/exchange-retention/policies/index.js index 650e79e76ea6..a66011d452aa 100644 --- a/src/pages/email/administration/exchange-retention/policies/index.js +++ b/src/pages/email/administration/exchange-retention/policies/index.js @@ -64,6 +64,7 @@ const Page = () => { return ( { return ( Date: Fri, 29 May 2026 16:31:56 +0800 Subject: [PATCH 080/133] Expose missing standards and allow removal --- .../CippStandards/CippStandardAccordion.jsx | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/src/components/CippStandards/CippStandardAccordion.jsx b/src/components/CippStandards/CippStandardAccordion.jsx index 5aed7f6950a8..931a5c2cc271 100644 --- a/src/components/CippStandards/CippStandardAccordion.jsx +++ b/src/components/CippStandards/CippStandardAccordion.jsx @@ -28,6 +28,7 @@ import { NotificationImportant, Assignment, Construction, + Warning, } from "@mui/icons-material"; import { Grid } from "@mui/system"; import CippFormComponent from "../CippComponents/CippFormComponent"; @@ -401,7 +402,23 @@ const CippStandardAccordion = ({ Object.keys(selectedStandards).forEach((standardName) => { const baseStandardName = standardName.split("[")[0]; const standard = providedStandards.find((s) => s.name === baseStandardName); - if (!standard) return; + + if (!standard) { + // Unknown/deprecated standard — surface it so the user can remove it + const unknownCategory = "Unknown Standards"; + if (!result[unknownCategory]) { + result[unknownCategory] = []; + } + result[unknownCategory].push({ + standardName, + standard: { + _unknown: true, + name: baseStandardName, + label: baseStandardName, + }, + }); + return; + } const standardInfo = standards.find((s) => s.name === baseStandardName); const category = standardInfo?.cat || "Other Standards"; @@ -613,6 +630,69 @@ const CippStandardAccordion = ({ {filteredGroupedStandards[category].map(({ standardName, standard }) => { + if (standard._unknown) { + const isExpanded = expanded === standardName; + const rawData = get(watchedValues, standardName); + return ( + + + + + + + + {standard.label} + + This standard no longer exists and should be removed. + + + + + + handleRemoveStandard(standardName)}> + + + + handleAccordionToggle(standardName)}> + + + + + + + + + Stored Configuration + + + {JSON.stringify(rawData, null, 2)} + + + + + ); + } + const isExpanded = expanded === standardName; const hasAddedComponents = standard.addedComponent && standard.addedComponent.length > 0; From c0bfd7df29808523b2ab94847fff8b7736681c43 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 29 May 2026 17:43:25 +0800 Subject: [PATCH 081/133] Update standards.json --- src/data/standards.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 14f0fbcef627..ecc3e9c10025 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -6915,13 +6915,6 @@ "placeholder": "YOUR-COMPANY", "required": false }, - { - "type": "textField", - "name": "standards.DeployCheckChromeExtension.companyURL", - "label": "Company URL", - "placeholder": "https://yourcompany.com", - "required": false - }, { "type": "textField", "name": "standards.DeployCheckChromeExtension.productName", From 9e44f394d5d975b69dcc1a096a101ab27720bcf9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 30 May 2026 22:49:02 +0800 Subject: [PATCH 082/133] Update worker-health.js --- src/pages/cipp/advanced/worker-health.js | 41 ++++++++++++++++++------ 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index 19d68519852f..e22ed9f8b31d 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -128,6 +128,7 @@ const WorkerTable = ({ workers, title }) => { Min Max Last + Alloc Faults @@ -150,6 +151,13 @@ const WorkerTable = ({ workers, title }) => { {formatDuration(w.MinDurationMs)} {formatDuration(w.MaxDurationMs)} {formatDuration(w.LastDurationMs)} + + + + {w.TotalAllocMB != null ? `${w.TotalAllocMB} MB` : "—"} + + + {w.TotalFaults > 0 ? ( @@ -355,17 +363,26 @@ const CompactStatsRow = ({ snapshot }) => { label: "Memory", color: "secondary", stats: [ - { k: "Heap", v: `${mem.HeapMB ?? 0}MB` }, - { k: "RSS", v: `${mem.RssMB ?? 0}MB`, w: mem.UsagePct > 85 }, + { k: "Container", v: `${mem.ContainerUsedMB ?? mem.RssMB ?? 0} / ${mem.ContainerLimitMB ?? 0}MB`, w: mem.UsagePct > 85 }, + { k: "App RSS", v: `${mem.RssMB ?? 0}MB` }, + { k: "Other", v: `${mem.OtherRssMB ?? 0}MB` }, + { k: "GC Heap", v: `${mem.HeapMB ?? 0}MB` }, { k: "Committed", v: `${mem.CommittedMB ?? 0}MB` }, - { k: "Limit", v: `${mem.ContainerLimitMB ?? 0}MB` }, { k: "GC Limit", v: `${mem.GCHeapLimitMB ?? 0}MB` }, { k: "Usage", v: `${mem.UsagePct ?? 0}%`, w: mem.UsagePct > 85 }, - { k: "CPU", v: `${mem.CpuPct ?? 0}%`, w: mem.CpuPct > 80 }, { k: "GC", v: `${mem.GC0 ?? 0}/${mem.GC1 ?? 0}/${mem.GC2 ?? 0}` }, ...(mem.TestDataCacheCount != null ? [{ k: "Cache", v: `${mem.TestDataCacheCount} entries` }] : []), ], }, + { + label: "CPU", + color: "warning", + stats: [ + { k: "Container", v: `${mem.ContainerCpuPct ?? mem.CpuPct ?? 0}%`, w: (mem.ContainerCpuPct ?? 0) > 80 }, + { k: "App", v: `${mem.CpuPct ?? 0}%`, w: mem.CpuPct > 80 }, + { k: "Other", v: `${mem.OtherCpuPct ?? 0}%` }, + ], + }, ]; return ( @@ -599,14 +616,14 @@ const Page = () => { { icon: , name: "Memory", - data: `${snapshot.Memory?.RssMB ?? 0}MB / ${snapshot.Memory?.ContainerLimitMB ?? 0}MB (${snapshot.Memory?.UsagePct ?? 0}%)`, + data: `${snapshot.Memory?.ContainerUsedMB ?? snapshot.Memory?.RssMB ?? 0}MB / ${snapshot.Memory?.ContainerLimitMB ?? 0}MB (${snapshot.Memory?.UsagePct ?? 0}%)`, color: (snapshot.Memory?.UsagePct ?? 0) > 85 ? "error" : (snapshot.Memory?.UsagePct ?? 0) > 70 ? "warning" : "primary", }, { icon: , name: "CPU", - data: `${snapshot.Memory?.CpuPct ?? 0}%`, - color: (snapshot.Memory?.CpuPct ?? 0) > 80 ? "error" : (snapshot.Memory?.CpuPct ?? 0) > 50 ? "warning" : "primary", + data: `${snapshot.Memory?.ContainerCpuPct ?? snapshot.Memory?.CpuPct ?? 0}% container / ${snapshot.Memory?.CpuPct ?? 0}% app`, + color: (snapshot.Memory?.ContainerCpuPct ?? snapshot.Memory?.CpuPct ?? 0) > 80 ? "error" : (snapshot.Memory?.ContainerCpuPct ?? snapshot.Memory?.CpuPct ?? 0) > 50 ? "warning" : "primary", }, ]; }, [snapshot]); @@ -932,9 +949,11 @@ const Page = () => { }} /> - + + + - + )} @@ -959,7 +978,9 @@ const Page = () => { }} /> - + + + )} From 6737dcb7df0ad4e21a4edf4979dfc2f30e49510f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:08:49 +0800 Subject: [PATCH 083/133] Licence Universal Search --- .../CippCards/CippUniversalSearchV2.jsx | 128 +++++++++++- .../CippLicenseDetailsDrawer.jsx | 196 ++++++++++++++++++ src/data/M365Licenses.json | 8 - src/layouts/top-nav.js | 17 ++ src/utils/get-cipp-license-catalog.js | 88 ++++++++ src/utils/icon-registry.js | 2 + 6 files changed, 427 insertions(+), 12 deletions(-) create mode 100644 src/components/CippComponents/CippLicenseDetailsDrawer.jsx create mode 100644 src/utils/get-cipp-license-catalog.js diff --git a/src/components/CippCards/CippUniversalSearchV2.jsx b/src/components/CippCards/CippUniversalSearchV2.jsx index 83e127352851..b4591cca26ee 100644 --- a/src/components/CippCards/CippUniversalSearchV2.jsx +++ b/src/components/CippCards/CippUniversalSearchV2.jsx @@ -20,6 +20,7 @@ import { CippOffCanvas } from "../CippComponents/CippOffCanvas"; import { CippBitlockerKeySearch } from "../CippComponents/CippBitlockerKeySearch"; import { nativeMenuItems } from "../../layouts/config"; import { usePermissions } from "../../hooks/use-permissions"; +import { searchLocalLicenseCatalog } from "../../utils/get-cipp-license-catalog"; function getLeafItems(items = []) { let result = []; @@ -111,6 +112,7 @@ export const CippUniversalSearchV2 = React.forwardRef( { onConfirm = () => {}, onChange = () => {}, + onLicenseSelect, maxResults = 10, value = "", autoFocus = false, @@ -148,6 +150,14 @@ export const CippUniversalSearchV2 = React.forwardRef( waiting: false, }); + // Local-first license lookup. The frontend ships the full Microsoft SKU + // catalog in M365Licenses.json, so for the Licenses type we match locally + // and only fall back to the API when the catalog has no hit. + const localLicenseResults = useMemo(() => { + if (searchType !== "Licenses") return []; + return searchLocalLicenseCatalog(searchValue, maxResults); + }, [searchType, searchValue, maxResults]); + const bitlockerSearch = ApiGetCall({ url: "/api/ExecBitlockerSearch", data: { @@ -272,6 +282,11 @@ export const CippUniversalSearchV2 = React.forwardRef( } else if (searchType === "Pages") { updateDropdownPosition(); setShowDropdown(true); + } else if (searchType === "Licenses") { + // Local catalog is in-memory, so reveal results as the user types. + // The API fallback still requires the Search button (handleSearch). + updateDropdownPosition(); + setShowDropdown(true); } }; @@ -331,7 +346,12 @@ export const CippUniversalSearchV2 = React.forwardRef( const handleSearch = () => { if (searchValue.length > 0) { updateDropdownPosition(); - if (searchType !== "Pages") { + if (searchType === "Licenses") { + // Only hit the API when the local catalog produced nothing. + if (localLicenseResults.length === 0) { + activeSearch?.refetch(); + } + } else if (searchType !== "Pages") { activeSearch?.refetch(); } setShowDropdown(true); @@ -361,6 +381,10 @@ export const CippUniversalSearchV2 = React.forwardRef( } } else if (searchType === "Pages") { router.push(match.path, undefined, { shallow: true }); + } else if (searchType === "Licenses") { + if (typeof onLicenseSelect === "function") { + onLicenseSelect(itemData); + } } setSearchValue(""); setShowDropdown(false); @@ -405,6 +429,11 @@ export const CippUniversalSearchV2 = React.forwardRef( icon: "Apps", onClick: () => handleTypeChange("Applications"), }, + { + label: "Licenses", + icon: "VpnKey", + onClick: () => handleTypeChange("Licenses"), + }, { label: "BitLocker", icon: "FilePresent", @@ -498,18 +527,28 @@ export const CippUniversalSearchV2 = React.forwardRef( ? bitlockerSearch.data.Results : []; const universalResults = Array.isArray(universalSearch?.data) ? universalSearch.data : []; + const licenseResults = + searchType === "Licenses" + ? localLicenseResults.length > 0 + ? localLicenseResults + : universalResults + : universalResults; const activeResults = searchType === "BitLocker" ? bitlockerResults : searchType === "Pages" ? pageResults - : universalResults; + : searchType === "Licenses" + ? licenseResults + : universalResults; const hasResults = searchType === "BitLocker" ? bitlockerResults.length > 0 : searchType === "Pages" ? pageResults.length > 0 - : universalResults.length > 0; + : searchType === "Licenses" + ? licenseResults.length > 0 + : universalResults.length > 0; const shouldShowDropdown = showDropdown && searchValue.length > 0; const getLabel = () => { @@ -523,6 +562,8 @@ export const CippUniversalSearchV2 = React.forwardRef( : "Search BitLocker by Recovery Key ID"; } else if (searchType === "Pages") { return "Search pages, tabs, paths, or scope"; + } else if (searchType === "Licenses") { + return "Search licenses by SKU ID, part number, name, or service plan"; } return "Search"; }; @@ -633,7 +674,7 @@ export const CippUniversalSearchV2 = React.forwardRef( /> ) : ( p?.servicePlanName) + .filter(Boolean) + .join(", "); + return ( + onResultClick(match)} + onMouseEnter={() => setHighlightedIndex(index)} + selected={highlightedIndex === index} + sx={{ + py: 1.5, + px: 2, + borderBottom: index < items.length - 1 ? "1px solid" : "none", + borderColor: "divider", + alignItems: "flex-start", + whiteSpace: "normal", + backgroundColor: highlightedIndex === index ? "action.selected" : "transparent", + "&:hover": { backgroundColor: "action.hover" }, + }} + > + + + {highlightMatch(itemData.displayName || itemData.skuPartNumber || "Unknown SKU")} + + {itemData.skuPartNumber && ( + + {highlightMatch(itemData.skuPartNumber)} + + )} + + } + secondary={ + + {itemData.skuId && ( + + {highlightMatch(itemData.skuId)} + + )} + + {itemData.tenantCount || 0} tenant{itemData.tenantCount === 1 ? "" : "s"} + {" · "} + {itemData.totalAssigned || 0}/{itemData.totalAvailable || 0} assigned + {servicePlans.length > 0 && ` · ${servicePlans.length} service plan${servicePlans.length === 1 ? "" : "s"}`} + + {planNames && ( + + {highlightMatch(planNames)} + + )} + + } + /> + + ); + } + return ( { + if (value === null || value === undefined || value === "") return null; + return ( + + + + {label} + + + + {String(value)} + + + + ); +}; + +export const CippLicenseDetailsDrawer = ({ data }) => { + if (!data) return null; + const fromCatalog = data.source === "catalog"; + + // For catalog-only hits, backfill tenant usage from the API by skuId. + const usageQuery = ApiGetCall({ + url: `/api/ExecUniversalSearchV2`, + data: { + searchTerms: data.skuId || data.skuPartNumber || "", + limit: 1, + type: "Licenses", + }, + queryKey: `licenseUsage-${data.skuId || data.skuPartNumber || ""}`, + waiting: fromCatalog && Boolean(data.skuId || data.skuPartNumber), + }); + + const apiMatch = (() => { + if (!fromCatalog || !usageQuery.isSuccess) return null; + const rows = Array.isArray(usageQuery.data) ? usageQuery.data : []; + const target = String(data.skuId || "").toLowerCase(); + const hit = + rows.find((r) => String(r?.Data?.skuId || "").toLowerCase() === target) || rows[0]; + return hit?.Data || null; + })(); + + const merged = apiMatch + ? { + ...data, + tenantCount: apiMatch.tenantCount ?? data.tenantCount, + totalAssigned: apiMatch.totalAssigned ?? data.totalAssigned, + totalAvailable: apiMatch.totalAvailable ?? data.totalAvailable, + tenants: Array.isArray(apiMatch.tenants) ? apiMatch.tenants : [], + } + : data; + + const servicePlans = Array.isArray(merged.servicePlans) ? merged.servicePlans : []; + const tenants = Array.isArray(merged.tenants) ? merged.tenants : []; + const hasUsage = !fromCatalog || apiMatch !== null; + const usageLoading = fromCatalog && usageQuery.isFetching && !apiMatch; + const usageNotFound = fromCatalog && usageQuery.isSuccess && !apiMatch; + + return ( + + + + {merged.displayName || merged.skuPartNumber || "License"} + + + {hasUsage + ? `${merged.tenantCount || 0} tenant${merged.tenantCount === 1 ? "" : "s"} · ${ + merged.totalAssigned || 0 + }/${merged.totalAvailable || 0} assigned` + : usageLoading + ? "Microsoft published catalog · loading tenant usage…" + : usageNotFound + ? "Microsoft published catalog · not assigned in any tenant" + : "Microsoft published catalog"} + + + + + + + + {hasUsage && ( + <> + + + + + )} + + {usageLoading && ( + + + + Looking up tenant usage… + + + )} + + {servicePlans.length > 0 && ( + <> + + Service plans ({servicePlans.length}) + + + + + + Name + Friendly name + Service plan ID + + + + + {servicePlans.map((plan, idx) => { + const id = plan.servicePlanId || plan.servicePlanid || ""; + const name = plan.servicePlanName || ""; + const friendly = plan.friendlyName || ""; + return ( + + {name} + {friendly} + {id} + {id && } + + ); + })} + +
+
+ + )} + + {tenants.length > 0 && ( + <> + + + Tenants ({tenants.length}) + + + + + + Tenant + Used + Total + + + + + {tenants.map((t, idx) => ( + + {t.tenant} + {t.used ?? "-"} + {t.total ?? "-"} + + {t.tenant && } + + + ))} + +
+
+ + )} +
+ ); +}; diff --git a/src/data/M365Licenses.json b/src/data/M365Licenses.json index 3f725239335b..0dfed3375a94 100644 --- a/src/data/M365Licenses.json +++ b/src/data/M365Licenses.json @@ -11151,14 +11151,6 @@ "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014", "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365" }, - { - "Product_Display_Name": "Microsoft 365 Business Premium", - "String_Id": "SPB", - "GUID": "cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46", - "Service_Plan_Name": "SHAREPOINTSTANDARD", - "Service_Plan_Id": "c7699d2e-19aa-44de-8edf-1736da088ca1", - "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 1)" - }, { "Product_Display_Name": "Microsoft 365 Business Premium", "String_Id": "SPB", diff --git a/src/layouts/top-nav.js b/src/layouts/top-nav.js index baa5d1ad9f9f..10bdcefd9581 100644 --- a/src/layouts/top-nav.js +++ b/src/layouts/top-nav.js @@ -41,6 +41,8 @@ import { CippTenantSelector } from '../components/CippComponents/CippTenantSelec import { NotificationsPopover } from './notifications-popover' import { useDialog } from '../hooks/use-dialog' import { CippUniversalSearchV2 } from '../components/CippCards/CippUniversalSearchV2' +import { CippOffCanvas } from '../components/CippComponents/CippOffCanvas' +import { CippLicenseDetailsDrawer } from '../components/CippComponents/CippLicenseDetailsDrawer' const TOP_NAV_HEIGHT = 64 @@ -69,6 +71,8 @@ export const TopNav = (props) => { const [flashSort, setFlashSort] = useState(false) const [flashLock, setFlashLock] = useState(false) const [universalSearchKey, setUniversalSearchKey] = useState(0) + const [licenseDrawerVisible, setLicenseDrawerVisible] = useState(false) + const [licenseDrawerData, setLicenseDrawerData] = useState(null) const [universalSearchDefaultType, setUniversalSearchDefaultType] = useState('Users') const itemRefs = useRef({}) const touchDragRef = useRef({ startIdx: null, overIdx: null }) @@ -633,10 +637,23 @@ export const TopNav = (props) => { autoFocus={true} defaultSearchType={universalSearchDefaultType} onConfirm={closeUniversalSearch} + onLicenseSelect={(licenseData) => { + setLicenseDrawerData(licenseData) + setLicenseDrawerVisible(true) + }} />
+ setLicenseDrawerVisible(false)} + size="xl" + contentPadding={0} + > + + { + const map = new Map(); + for (const row of [...M365LicensesDefault, ...M365LicensesAdditional]) { + if (!row?.GUID) continue; + const key = row.GUID.toLowerCase(); + let entry = map.get(key); + if (!entry) { + entry = { + skuId: row.GUID, + skuPartNumber: row.String_Id || "", + displayName: row.Product_Display_Name || row.String_Id || "", + servicePlans: [], + _planSet: new Set(), + }; + map.set(key, entry); + } + if (!entry.skuPartNumber && row.String_Id) entry.skuPartNumber = row.String_Id; + if (!entry.displayName && row.Product_Display_Name) entry.displayName = row.Product_Display_Name; + + if (row.Service_Plan_Id) { + const planKey = row.Service_Plan_Id.toLowerCase(); + if (!entry._planSet.has(planKey)) { + entry._planSet.add(planKey); + entry.servicePlans.push({ + servicePlanId: row.Service_Plan_Id, + servicePlanName: row.Service_Plan_Name || "", + friendlyName: row.Service_Plans_Included_Friendly_Names || "", + }); + } + } + } + // Drop the helper Set before exposing entries. + return Array.from(map.values()).map(({ _planSet, ...rest }) => rest); +}; + +const getCatalog = () => { + if (!catalogCache) catalogCache = buildCatalog(); + return catalogCache; +}; + +/** + * Search the local M365 license catalog. Matches against skuId (GUID), + * skuPartNumber (String_Id), display name, and any service plan name or + * friendly name. Returns up to `limit` results in the same envelope shape + * Universal Search uses for API results. + */ +export const searchLocalLicenseCatalog = (query, limit = 10) => { + if (!query || typeof query !== "string") return []; + const q = query.trim().toLowerCase(); + if (!q) return []; + + const matches = []; + for (const entry of getCatalog()) { + if (matches.length >= limit) break; + const haystacks = [ + entry.skuId, + entry.skuPartNumber, + entry.displayName, + ...entry.servicePlans.flatMap((p) => [p.servicePlanName, p.friendlyName]), + ]; + if (haystacks.some((v) => v && v.toLowerCase().includes(q))) { + matches.push({ + Tenant: "", + Type: "Licenses", + RowKey: `Licenses-${entry.skuId}`, + Data: { + skuId: entry.skuId, + skuPartNumber: entry.skuPartNumber, + displayName: entry.displayName, + servicePlans: entry.servicePlans, + tenantCount: 0, + totalAssigned: 0, + totalAvailable: 0, + tenants: [], + source: "catalog", + }, + }); + } + } + return matches; +}; diff --git a/src/utils/icon-registry.js b/src/utils/icon-registry.js index 2863958edbb4..1eeabb7f01b3 100644 --- a/src/utils/icon-registry.js +++ b/src/utils/icon-registry.js @@ -41,6 +41,7 @@ import { Timeline, Window, Warning, + VpnKey, } from '@mui/icons-material' export const iconRegistry = { @@ -86,6 +87,7 @@ export const iconRegistry = { Timeline, Window, Warning, + VpnKey, } export const getIconComponentByName = (iconName) => iconRegistry[iconName] ?? null From e2c39b26d95394b3095a28f03e9dccb7a8085afa Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 00:09:49 +0800 Subject: [PATCH 084/133] Update M365Licenses.json --- src/data/M365Licenses.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/data/M365Licenses.json b/src/data/M365Licenses.json index 0dfed3375a94..3f725239335b 100644 --- a/src/data/M365Licenses.json +++ b/src/data/M365Licenses.json @@ -11151,6 +11151,14 @@ "Service_Plan_Id": "ded3d325-1bdc-453e-8432-5bac26d7a014", "Service_Plans_Included_Friendly_Names": "Power Virtual Agents for Office 365" }, + { + "Product_Display_Name": "Microsoft 365 Business Premium", + "String_Id": "SPB", + "GUID": "cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46", + "Service_Plan_Name": "SHAREPOINTSTANDARD", + "Service_Plan_Id": "c7699d2e-19aa-44de-8edf-1736da088ca1", + "Service_Plans_Included_Friendly_Names": "SharePoint (Plan 1)" + }, { "Product_Display_Name": "Microsoft 365 Business Premium", "String_Id": "SPB", From 3734adeeaec92e6d83974590c36a9e91bf024a98 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 1 Jun 2026 23:44:55 +0200 Subject: [PATCH 085/133] Fix template trigger --- src/pages/tenant/manage/user-defaults.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/manage/user-defaults.js b/src/pages/tenant/manage/user-defaults.js index 3a5c3a150899..62f5250922df 100644 --- a/src/pages/tenant/manage/user-defaults.js +++ b/src/pages/tenant/manage/user-defaults.js @@ -122,7 +122,7 @@ const Page = () => { api: { url: '/api/ListLicenses', labelField: (option) => - `${option.License || option.skuPartNumber} (${option.AvailableUnits || 0} available)`, + `${option.License || option.skuPartNumber} (${option.availableUnits || 0} available)`, valueField: 'skuId', queryKey: `ListLicenses-${userSettings.currentTenant}`, }, From 49cda6e0b465ad7bb9a0a259a9ee31a2584e0835 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 07:28:27 +0800 Subject: [PATCH 086/133] Update index.js --- src/pages/tenant/reports/list-licenses/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/tenant/reports/list-licenses/index.js b/src/pages/tenant/reports/list-licenses/index.js index 4d877df75f8f..61fcfc5b74b6 100644 --- a/src/pages/tenant/reports/list-licenses/index.js +++ b/src/pages/tenant/reports/list-licenses/index.js @@ -5,7 +5,7 @@ import CippFormComponent from '../../../../components/CippComponents/CippFormCom const Page = () => { const pageTitle = 'Licences Report' - const apiUrl = '/api/ListLicenses' + const apiUrl = '/api/ListLicensesReport' const simpleColumns = [ 'Tenant', From f1703f041f517d7d07ca35074a89743f1991141f Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:15:54 +0800 Subject: [PATCH 087/133] Update CippTenantModeDeploy.jsx --- src/components/CippWizard/CippTenantModeDeploy.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/CippWizard/CippTenantModeDeploy.jsx b/src/components/CippWizard/CippTenantModeDeploy.jsx index c14bb0aa573b..c1f5b2669c43 100644 --- a/src/components/CippWizard/CippTenantModeDeploy.jsx +++ b/src/components/CippWizard/CippTenantModeDeploy.jsx @@ -47,6 +47,13 @@ export const CippTenantModeDeploy = (props) => { } }, [updateRefreshToken.isSuccess, formControl, addTenant.isSuccess]); + useEffect(() => { + if (partnerTenantInfo?.data?.authenticatedUserPrincipalName) { + formControl.setValue("GDAPAuth", true); + formControl.trigger("GDAPAuth"); + } + }, [partnerTenantInfo?.data?.authenticatedUserPrincipalName, formControl]); + return ( {/* Partner Tenant (GDAP) */} From 89abbf507b2f96ea9a9238536bd95fe99a1b3b37 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:03:13 +0200 Subject: [PATCH 088/133] fix: improve stale issue and close messages for clarity --- .github/workflows/Close_Stale_Issues.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Close_Stale_Issues.yml b/.github/workflows/Close_Stale_Issues.yml index b1878078ac90..ec88789e827d 100644 --- a/.github/workflows/Close_Stale_Issues.yml +++ b/.github/workflows/Close_Stale_Issues.yml @@ -10,8 +10,8 @@ jobs: steps: - uses: actions/stale@v10 with: - stale-issue-message: "This issue is stale because it has been open 10 days with no activity. We will close this issue soon. If you want this feature implemented you can contribute it. See: https://docs.cipp.app/dev-documentation/contributing-to-the-code . Please notify the team if you are working on this yourself." - close-issue-message: "This issue was closed because it has been stalled for 14 days with no activity." + stale-issue-message: "This issue is stale because it has been open for 10 days with no activity. Please do not bump feature requests unless you are actively working on them, as bumps interfere with our triage process and make it harder to maintain a current list of feature requests. If you want this feature implemented, you can contribute it yourself; see https://docs.cipp.app/dev-documentation/contributing-to-the-code. Please notify the team if you are working on this." + close-issue-message: "This issue was closed because it has been stalled for 14 days without activity. We auto-close inactive feature requests to keep the backlog focused and actionable. If this request is still needed, you may submit it again after 30 days." stale-issue-label: "no-activity" exempt-issue-labels: "planned,bug,roadmap" days-before-stale: 9 From d4570de5058eba87c7f9e122a39d41bdedbc401c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:10:49 +0800 Subject: [PATCH 089/133] Update CippReportToolbar.jsx --- .../CippComponents/CippReportToolbar.jsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/CippComponents/CippReportToolbar.jsx b/src/components/CippComponents/CippReportToolbar.jsx index 7a25a9976075..bc7a9274a56c 100644 --- a/src/components/CippComponents/CippReportToolbar.jsx +++ b/src/components/CippComponents/CippReportToolbar.jsx @@ -109,13 +109,6 @@ export const CippReportToolbar = () => { onClick={() => { setRefreshDialog({ open: true, - title: 'Refresh Test Data', - message: `Are you sure you want to refresh the test data for ${currentTenant}? This might take up to 2 hours to update.`, - api: { - url: '/api/ExecTestRun', - data: { tenantFilter: currentTenant }, - method: 'POST', - }, handleClose: () => setRefreshDialog({ open: false }), }) }} @@ -187,13 +180,26 @@ export const CippReportToolbar = () => { From cbd6faefb184a7ee611063726862d2663bb56d4b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:15:25 +0800 Subject: [PATCH 090/133] Correct report builder permissions --- src/pages/tools/report-builder/builder/index.js | 4 ++-- src/pages/tools/report-builder/view/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/tools/report-builder/builder/index.js b/src/pages/tools/report-builder/builder/index.js index ed99486b4fb4..fa9df1b354fa 100644 --- a/src/pages/tools/report-builder/builder/index.js +++ b/src/pages/tools/report-builder/builder/index.js @@ -764,7 +764,7 @@ const Page = () => { /* ── API hooks ── */ const templatesApi = ApiGetCall({ - url: '/api/ListReportBuilderTemplates', + url: '/api/ListReportBuilderTemplates?tenantFilter=' + currentTenant, queryKey: `ListReportBuilderTemplates-builder-${templateId}`, waiting: !!templateId, }) @@ -1065,7 +1065,7 @@ const Page = () => { const name = saveForm.getValues('templateName') if (!name?.trim()) return saveTemplateCall.mutate({ - url: '/api/ExecReportBuilderTemplate', + url: '/api/ExecReportBuilderTemplate?tenantFilter=' + currentTenant, data: { Action: 'save', GUID: templateGUID || undefined, diff --git a/src/pages/tools/report-builder/view/index.js b/src/pages/tools/report-builder/view/index.js index d1bc6430637a..51a0dd058767 100644 --- a/src/pages/tools/report-builder/view/index.js +++ b/src/pages/tools/report-builder/view/index.js @@ -32,7 +32,7 @@ const Page = () => { }, [router.isReady, router.query.id]) const reportApi = ApiGetCall({ - url: '/api/ListGeneratedReports', + url: '/api/ListGeneratedReports?tenantFilter=' + settings.currentTenant, data: { ReportGUID: reportId }, queryKey: `ListGeneratedReports-${reportId}`, waiting: !!reportId, From 4d88e456cf5e581dbbd30cc6e348059e410d7f9c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:49:31 +0800 Subject: [PATCH 091/133] Update CippAutocomplete.jsx --- src/components/CippComponents/CippAutocomplete.jsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CippAutocomplete.jsx b/src/components/CippComponents/CippAutocomplete.jsx index 8afcb74d7415..075f47ed29a1 100644 --- a/src/components/CippComponents/CippAutocomplete.jsx +++ b/src/components/CippComponents/CippAutocomplete.jsx @@ -418,7 +418,11 @@ export const CippAutoComplete = React.forwardRef((props, ref) => { }) newValue = newValue.filter( (item) => - item.value && item.value !== '' && item.value !== 'error' && item.value !== -1 + item.value !== null && + item.value !== undefined && + item.value !== '' && + item.value !== 'error' && + item.value !== -1 ) } else { if (newValue?.manual || !newValue?.label) { @@ -430,7 +434,7 @@ export const CippAutoComplete = React.forwardRef((props, ref) => { newValue = onCreateOption(newValue, newValue?.addedFields) } } - if (!newValue?.value || newValue.value === 'error') { + if (newValue?.value === null || newValue?.value === undefined || newValue?.value === '' || newValue.value === 'error') { newValue = null } } From 51d2828b77990f8a3d017d69206c7b0a3df0f832 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:37:01 +0200 Subject: [PATCH 092/133] add mcp allowed --- .../CippApiClientManagement.jsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/CippIntegrations/CippApiClientManagement.jsx b/src/components/CippIntegrations/CippApiClientManagement.jsx index a9a2d2960ef1..539295619a88 100644 --- a/src/components/CippIntegrations/CippApiClientManagement.jsx +++ b/src/components/CippIntegrations/CippApiClientManagement.jsx @@ -179,6 +179,11 @@ const CippApiClientManagement = () => { name: "Enabled", label: "Enable this client", }, + { + type: "switch", + name: "MCPAllowed", + label: "MCP Access Allowed", + }, ], type: "POST", url: "/api/ExecApiClient", @@ -363,7 +368,7 @@ const CippApiClientManagement = () => { data: { Action: "List" }, dataKey: "Results", }} - simpleColumns={["Enabled", "AppName", "ClientId", "Role", "IPRange"]} + simpleColumns={["Enabled", "MCPAllowed", "AppName", "ClientId", "Role", "IPRange"]} queryKey={`ApiClients`} /> @@ -417,6 +422,11 @@ const CippApiClientManagement = () => { name: "Enabled", label: "Enable this client", }, + { + type: "switch", + name: "MCPAllowed", + label: "MCP Access Allowed", + }, ]} api={{ type: "POST", @@ -486,6 +496,11 @@ const CippApiClientManagement = () => { name: "Enabled", label: "Enable this client", }, + { + type: "switch", + name: "MCPAllowed", + label: "MCP Access Allowed", + }, ]} api={{ type: "POST", From e4009f28e0aeb0f338cfeeed606b24fec26b05b7 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:54:12 +0200 Subject: [PATCH 093/133] feat: add Email as alternate login ID standard --- src/data/standards.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/data/standards.json b/src/data/standards.json index ecc3e9c10025..fc2b40fc34ec 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -1680,6 +1680,28 @@ "powershellEquivalent": "Update-MgBetaPolicyAuthenticationMethodPolicyAuthenticationMethodConfiguration", "recommendedBy": [] }, + { + "name": "standards.EmailAsAlternateLoginId", + "cat": "Entra (AAD) Standards", + "tag": [], + "helpText": "Configures the tenant-wide Email as alternate login ID setting in Home Realm Discovery policy. Enabling this can help during migrations, if users are changing UPN.", + "docsDescription": "Sets the Home Realm Discovery policy AlternateIdLogin setting to enable or disable using email as an alternate sign-in ID.", + "executiveText": "Controls whether users can sign in with email as an alternate identifier, allowing organizations to align sign-in behavior with their identity strategy and reduce authentication ambiguity.", + "addedComponent": [ + { + "type": "switch", + "name": "standards.EmailAsAlternateLoginId.Enabled", + "label": "Enable Email as Alternate Login ID", + "defaultValue": true + } + ], + "label": "Configure Email as alternate login ID", + "impact": "Medium Impact", + "impactColour": "warning", + "addedDate": "2026-06-03", + "powershellEquivalent": "Invoke-MgGraphRequest https://graph.microsoft.com/v1.0/policies/homeRealmDiscoveryPolicies/", + "recommendedBy": ["CIPP"] + }, { "name": "standards.Disablex509Certificate", "cat": "Entra (AAD) Standards", From 38ac0c4a46f6c1e5f405bd429bae7accd43a1917 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:25:42 +0200 Subject: [PATCH 094/133] MCP warning --- .../CippComponents/CippFormComponent.jsx | 8 +++++++ .../CippApiClientManagement.jsx | 21 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/components/CippComponents/CippFormComponent.jsx b/src/components/CippComponents/CippFormComponent.jsx index 638d67b50e81..049337ccaf25 100644 --- a/src/components/CippComponents/CippFormComponent.jsx +++ b/src/components/CippComponents/CippFormComponent.jsx @@ -12,6 +12,7 @@ import { Box, Input, Tooltip, + Alert, } from "@mui/material"; import { CippAutoComplete } from "./CippAutocomplete"; import { CippTextFieldWithVariables } from "./CippTextFieldWithVariables"; @@ -94,6 +95,13 @@ export const CippFormComponent = (props) => { ); + case "alert": + return ( + + {label} + + ); + case "hidden": return ( { name: "MCPAllowed", label: "MCP Access Allowed", }, + { + type: "alert", + name: "mcpAccessWarning", + severity: "warning", + label: + "Enabling MCP Access converts this client into the MCP resource app — it can no longer be used as a normal API client, and only one client per tenant can hold this role.", + }, ], type: "POST", url: "/api/ExecApiClient", @@ -427,6 +434,13 @@ const CippApiClientManagement = () => { name: "MCPAllowed", label: "MCP Access Allowed", }, + { + type: "alert", + name: "mcpAccessWarning", + severity: "warning", + label: + "Enabling MCP Access converts this client into the MCP resource app — it can no longer be used as a normal API client, and only one client per tenant can hold this role.", + }, ]} api={{ type: "POST", @@ -501,6 +515,13 @@ const CippApiClientManagement = () => { name: "MCPAllowed", label: "MCP Access Allowed", }, + { + type: "alert", + name: "mcpAccessWarning", + severity: "warning", + label: + "Enabling MCP Access converts this client into the MCP resource app — it can no longer be used as a normal API client, and only one client per tenant can hold this role.", + }, ]} api={{ type: "POST", From b2f8f80372d5c6a28ef57ea373708cd3287f3a8a Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Wed, 3 Jun 2026 19:24:53 +0200 Subject: [PATCH 095/133] feat: add actions for managing mailbox client access protocols --- .../reports/mailbox-cas-settings/index.js | 109 +++++++++++++++--- 1 file changed, 91 insertions(+), 18 deletions(-) diff --git a/src/pages/email/reports/mailbox-cas-settings/index.js b/src/pages/email/reports/mailbox-cas-settings/index.js index 1058c83e93b2..5ae9d75cc3d8 100644 --- a/src/pages/email/reports/mailbox-cas-settings/index.js +++ b/src/pages/email/reports/mailbox-cas-settings/index.js @@ -1,29 +1,102 @@ -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippTablePage } from "../../../../components/CippComponents/CippTablePage.jsx"; +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippTablePage } from '../../../../components/CippComponents/CippTablePage.jsx' +import { useSettings } from '../../../../hooks/use-settings.js' +import { Tune } from '@mui/icons-material' + +// CAS protocols that can be toggled, keyed by their Set-CASMailbox parameter. +// Most flags follow "*Enabled" (true = on). The inverted "*Disabled" flag +// (SMTP client auth) is the opposite: true = off. +const casProtocols = { + ECPEnabled: 'ECP (Exchange Control Panel)', + EWSEnabled: 'EWS (Exchange Web Services)', + IMAPEnabled: 'IMAP', + MAPIEnabled: 'MAPI', + OWAEnabled: 'OWA (Outlook on the Web)', + POPEnabled: 'POP', + ActiveSyncEnabled: 'ActiveSync', + SmtpClientAuthenticationDisabled: 'SMTP Client Authentication', +} const Page = () => { + const tenantFilter = useSettings().currentTenant + + // A single action lets the operator pick which protocols to enable or disable. + // Each selected protocol becomes a Set-CASMailbox flag, accounting for the inverted + // "*Disabled" parameter, and one request is sent per selected mailbox. + const actions = [ + { + label: 'Set Client Access Protocols', + type: 'POST', + icon: , + url: '/api/ExecSetCASMailbox', + fields: [ + { + type: 'radio', + name: 'enable', + label: 'Action', + options: [ + { label: 'Enable', value: true }, + { label: 'Disable', value: false }, + ], + validators: { required: 'Please choose whether to enable or disable' }, + }, + { + type: 'autoComplete', + name: 'protocols', + label: 'Protocols', + multiple: true, + creatable: false, + options: Object.entries(casProtocols).map(([value, label]) => ({ label, value })), + validators: { required: 'Please select at least one protocol' }, + }, + ], + confirmText: + 'Enable or disable the selected client access protocols for the chosen mailbox(es).', + customDataformatter: (rows, action, formData) => { + const mailboxes = Array.isArray(rows) ? rows : [rows] + const rawEnable = + typeof formData.enable === 'object' ? formData.enable?.value : formData.enable + const enable = rawEnable === true || rawEnable === 'true' + const protocolFlags = (formData.protocols ?? []).reduce((flags, selection) => { + const param = typeof selection === 'object' ? selection.value : selection + // "*Disabled" params are inverted: enabling the protocol means setting them false. + // SMTP client auth is disable-only; the API rejects an enable attempt with a message. + flags[param] = param.endsWith('Disabled') ? !enable : enable + return flags + }, {}) + + return mailboxes.map((row) => ({ + tenantFilter, + Identity: row.Guid, + DisplayName: row.DisplayName, + ...protocolFlags, + })) + }, + color: 'info', + }, + ] + return ( - ); -}; - -// No actions were specified in the original code, so no actions are added here. -// No off-canvas configuration was provided or specified in the original code. + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page From 692c67d6451811f01fdcd7fd021a9b9273efd233 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jun 2026 21:33:18 -0400 Subject: [PATCH 096/133] fix: quarantine deny action fixes #5930 --- src/pages/email/administration/quarantine/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/email/administration/quarantine/index.js b/src/pages/email/administration/quarantine/index.js index 7443eb5e714f..a52606123181 100644 --- a/src/pages/email/administration/quarantine/index.js +++ b/src/pages/email/administration/quarantine/index.js @@ -126,6 +126,7 @@ const Page = () => { data: { Identity: 'Identity', Type: '!Deny', + RecipientAddress: 'RecipientAddress', }, confirmText: 'Are you sure you want to deny this message?', icon: , From a80484765aea42b43c1e663d0a9a9295f3795ad9 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 4 Jun 2026 16:03:41 +0800 Subject: [PATCH 097/133] Exclude partner tenant --- src/components/CippComponents/CippAddEditTenantGroups.jsx | 6 ++++++ src/pages/tenant/administration/tenants/groups/edit.js | 1 + 2 files changed, 7 insertions(+) diff --git a/src/components/CippComponents/CippAddEditTenantGroups.jsx b/src/components/CippComponents/CippAddEditTenantGroups.jsx index 4208c18eaf7c..2ead662a5cd5 100644 --- a/src/components/CippComponents/CippAddEditTenantGroups.jsx +++ b/src/components/CippComponents/CippAddEditTenantGroups.jsx @@ -83,6 +83,12 @@ const CippAddEditTenantGroups = ({ formControl, initialValues, title, backButton compareType="is" compareValue="dynamic" > + { groupDescription: groupData?.Description ?? "", groupType: isDynamic ? "dynamic" : "static", ruleLogic: groupData?.RuleLogic || "and", + excludePartnerTenant: groupData?.ExcludePartnerTenant ?? false, members: !isDynamic ? groupData?.Members?.map((member) => ({ label: member.displayName, From 726232611922cf2c302a7e77254ba881e074f4d8 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:21:35 +0200 Subject: [PATCH 098/133] add excludeFromAlert to licenses. --- src/pages/cipp/settings/licenses.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index d734f7eac437..2bada2c1f8f8 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -4,7 +4,7 @@ import { Layout as DashboardLayout } from "../../../layouts/index.js"; import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; import { Button, SvgIcon, Stack, Box } from "@mui/material"; import { TrashIcon } from "@heroicons/react/24/outline"; -import { Add, RestartAlt } from "@mui/icons-material"; +import { Add, RestartAlt, NotificationsOff } from "@mui/icons-material"; import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; import { useDialog } from "../../../hooks/use-dialog"; import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; @@ -18,7 +18,7 @@ const Page = () => { const apiUrl = "/api/ListExcludedLicenses"; const createDialog = useDialog(); const resetDialog = useDialog(); - const simpleColumns = ["Product_Display_Name", "GUID"]; + const simpleColumns = ["Product_Display_Name", "GUID", "ExclusionType"]; const allLicenseOptions = useMemo(() => { const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional]; @@ -49,6 +49,15 @@ const Page = () => { }, []); const actions = [ + { + label: "Only Exclude from Alerts", + type: "POST", + url: "/api/ExecExcludeLicenses", + data: { Action: "!AlertOnly", GUID: "GUID", SKUName: "Product_Display_Name" }, + confirmText: + "This license will remain visible in CIPP but will be excluded from alerts. Continue?", + icon: , + }, { label: "Delete Exclusion", type: "POST", @@ -94,7 +103,7 @@ const Page = () => { }; const offCanvas = { - extendedInfoFields: ["Product_Display_Name", "GUID"], + extendedInfoFields: ["Product_Display_Name", "GUID", "ExclusionType"], actions: actions, }; From cf30b4b19c0feee4b4429777e51b1c06720d51c0 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:25:03 +0200 Subject: [PATCH 099/133] add excluded from alerts to licenses --- src/pages/cipp/settings/licenses.js | 140 ++++++++++++++-------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/src/pages/cipp/settings/licenses.js b/src/pages/cipp/settings/licenses.js index 2bada2c1f8f8..d8b033cbdbd1 100644 --- a/src/pages/cipp/settings/licenses.js +++ b/src/pages/cipp/settings/licenses.js @@ -1,28 +1,28 @@ -import tabOptions from "./tabOptions"; -import { TabbedLayout } from "../../../layouts/TabbedLayout"; -import { Layout as DashboardLayout } from "../../../layouts/index.js"; -import { CippTablePage } from "../../../components/CippComponents/CippTablePage.jsx"; -import { Button, SvgIcon, Stack, Box } from "@mui/material"; -import { TrashIcon } from "@heroicons/react/24/outline"; -import { Add, RestartAlt, NotificationsOff } from "@mui/icons-material"; -import { CippApiDialog } from "../../../components/CippComponents/CippApiDialog"; -import { useDialog } from "../../../hooks/use-dialog"; -import CippFormComponent from "../../../components/CippComponents/CippFormComponent"; -import { CippFormCondition } from "../../../components/CippComponents/CippFormCondition"; -import M365LicensesDefault from "../../../data/M365Licenses.json"; -import M365LicensesAdditional from "../../../data/M365Licenses-additional.json"; -import { useMemo, useCallback } from "react"; +import tabOptions from './tabOptions' +import { TabbedLayout } from '../../../layouts/TabbedLayout' +import { Layout as DashboardLayout } from '../../../layouts/index.js' +import { CippTablePage } from '../../../components/CippComponents/CippTablePage.jsx' +import { Button, SvgIcon, Stack, Box } from '@mui/material' +import { TrashIcon } from '@heroicons/react/24/outline' +import { Add, RestartAlt, NotificationsOff } from '@mui/icons-material' +import { CippApiDialog } from '../../../components/CippComponents/CippApiDialog' +import { useDialog } from '../../../hooks/use-dialog' +import CippFormComponent from '../../../components/CippComponents/CippFormComponent' +import { CippFormCondition } from '../../../components/CippComponents/CippFormCondition' +import M365LicensesDefault from '../../../data/M365Licenses.json' +import M365LicensesAdditional from '../../../data/M365Licenses-additional.json' +import { useMemo, useCallback } from 'react' const Page = () => { - const pageTitle = "Excluded Licenses"; - const apiUrl = "/api/ListExcludedLicenses"; - const createDialog = useDialog(); - const resetDialog = useDialog(); - const simpleColumns = ["Product_Display_Name", "GUID", "ExclusionType"]; + const pageTitle = 'Excluded Licenses' + const apiUrl = '/api/ListExcludedLicenses' + const createDialog = useDialog() + const resetDialog = useDialog() + const simpleColumns = ['Product_Display_Name', 'GUID', 'ExclusionType'] const allLicenseOptions = useMemo(() => { - const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional]; - const uniqueLicenses = new Map(); + const allLicenses = [...M365LicensesDefault, ...M365LicensesAdditional] + const uniqueLicenses = new Map() allLicenses.forEach((license) => { if (license.GUID && license.Product_Display_Name) { @@ -30,48 +30,48 @@ const Page = () => { uniqueLicenses.set(license.GUID, { label: license.Product_Display_Name, value: license.GUID, - }); + }) } } - }); + }) - const options = Array.from(uniqueLicenses.values()); - const nameCounts = {}; + const options = Array.from(uniqueLicenses.values()) + const nameCounts = {} options.forEach((opt) => { - nameCounts[opt.label] = (nameCounts[opt.label] || 0) + 1; - }); + nameCounts[opt.label] = (nameCounts[opt.label] || 0) + 1 + }) return options .map((opt) => nameCounts[opt.label] > 1 ? { ...opt, label: `${opt.label} (${opt.value})` } : opt ) - .sort((a, b) => a.label.localeCompare(b.label)); - }, []); + .sort((a, b) => a.label.localeCompare(b.label)) + }, []) const actions = [ { - label: "Only Exclude from Alerts", - type: "POST", - url: "/api/ExecExcludeLicenses", - data: { Action: "!AlertOnly", GUID: "GUID", SKUName: "Product_Display_Name" }, + label: 'Only Exclude from Alerts', + type: 'POST', + url: '/api/ExecExcludeLicenses', + data: { Action: '!AlertOnly', GUID: 'GUID', SKUName: 'Product_Display_Name' }, confirmText: - "This license will remain visible in CIPP but will be excluded from alerts. Continue?", + 'This license will remain visible in CIPP but will be excluded from alerts. Continue?', icon: , }, { - label: "Delete Exclusion", - type: "POST", - url: "/api/ExecExcludeLicenses", - data: { Action: "!RemoveExclusion", GUID: "GUID" }, - confirmText: "Do you want to delete this exclusion?", - color: "error", + label: 'Delete Exclusion', + type: 'POST', + url: '/api/ExecExcludeLicenses', + data: { Action: '!RemoveExclusion', GUID: 'GUID' }, + confirmText: 'Do you want to delete this exclusion?', + color: 'error', icon: ( ), }, - ]; + ] const CardButtons = () => { return ( @@ -99,28 +99,28 @@ const Page = () => { Restore Defaults - ); - }; + ) + } const offCanvas = { - extendedInfoFields: ["Product_Display_Name", "GUID", "ExclusionType"], + extendedInfoFields: ['Product_Display_Name', 'GUID', 'ExclusionType'], actions: actions, - }; + } const addExclusionFormatter = useCallback((row, action, formData) => { if (formData.advancedMode) { return { - Action: "AddExclusion", + Action: 'AddExclusion', GUID: formData.GUID, SKUName: formData.SKUName, - }; + } } return { - Action: "AddExclusion", + Action: 'AddExclusion', GUID: formData.selectedLicense?.value, SKUName: formData.selectedLicense?.label, - }; - }, []); + } + }, []) return ( <> @@ -139,13 +139,13 @@ const Page = () => { title="Add Excluded License" createDialog={createDialog} api={{ - url: "/api/ExecExcludeLicenses", + url: '/api/ExecExcludeLicenses', confirmText: - "Add a license to the exclusion table. Select from the list or use Advanced Mode to enter a custom GUID.", - type: "POST", - data: { Action: "!AddExclusion" }, - replacementBehaviour: "removeNulls", - relatedQueryKeys: ["ExcludedLicenses"], + 'Add a license to the exclusion table. Select from the list or use Advanced Mode to enter a custom GUID.', + type: 'POST', + data: { Action: '!AddExclusion' }, + replacementBehaviour: 'removeNulls', + relatedQueryKeys: ['ExcludedLicenses'], customDataformatter: addExclusionFormatter, }} > @@ -174,7 +174,7 @@ const Page = () => { formControl={formHook} multiple={false} creatable={false} - validators={{ required: "Please select a license" }} + validators={{ required: 'Please select a license' }} /> @@ -191,7 +191,7 @@ const Page = () => { label="GUID" formControl={formHook} disableVariables={true} - validators={{ required: "GUID is required" }} + validators={{ required: 'GUID is required' }} /> { label="SKU Name" formControl={formHook} disableVariables={true} - validators={{ required: "SKU Name is required" }} + validators={{ required: 'SKU Name is required' }} /> @@ -211,28 +211,28 @@ const Page = () => { createDialog={resetDialog} fields={[ { - type: "switch", - name: "FullReset", - label: "Full Reset (clear all entries including manually added ones)", + type: 'switch', + name: 'FullReset', + label: 'Full Reset (clear all entries including manually added ones)', }, ]} api={{ - url: "/api/ExecExcludeLicenses", + url: '/api/ExecExcludeLicenses', confirmText: "This will restore default licenses from the config file. If 'Full Reset' is enabled, all existing entries will be cleared first.", - type: "POST", - data: { Action: "!RestoreDefaults" }, - relatedQueryKeys: ["ExcludedLicenses"], + type: 'POST', + data: { Action: '!RestoreDefaults' }, + relatedQueryKeys: ['ExcludedLicenses'], }} /> - ); -}; + ) +} Page.getLayout = (page) => ( {page} -); +) -export default Page; +export default Page From 11ecd3342338bf46f214a62137d6ad4482410ab3 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:25:26 +0200 Subject: [PATCH 100/133] remove unneeded results key --- .../CippFormLicenseSelector.jsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/components/CippComponents/CippFormLicenseSelector.jsx b/src/components/CippComponents/CippFormLicenseSelector.jsx index 8ec20ec6478a..cbc6455beaeb 100644 --- a/src/components/CippComponents/CippFormLicenseSelector.jsx +++ b/src/components/CippComponents/CippFormLicenseSelector.jsx @@ -1,6 +1,6 @@ -import { CippFormComponent } from "./CippFormComponent"; -import { getCippLicenseTranslation } from "../../utils/get-cipp-license-translation"; -import { useSettings } from "../../hooks/use-settings"; +import { CippFormComponent } from './CippFormComponent' +import { getCippLicenseTranslation } from '../../utils/get-cipp-license-translation' +import { useSettings } from '../../hooks/use-settings' export const CippFormLicenseSelector = ({ formControl, @@ -12,7 +12,7 @@ export const CippFormLicenseSelector = ({ showRefresh = false, ...other }) => { - const userSettingsDefaults = useSettings(); + const userSettingsDefaults = useSettings() return ( `${getCippLicenseTranslation([option])} (${option?.availableUnits} available)`, - valueField: "skuId", + valueField: 'skuId', queryKey: `ListLicenses-${userSettingsDefaults?.currentTenant ?? undefined}`, data: { - Endpoint: "subscribedSkus", + Endpoint: 'subscribedSkus', $count: true, }, showRefresh, }} /> - ); -}; + ) +} From 3daf8b33cad3a4c9df81c87703758cab2d3150aa Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:49:15 +0800 Subject: [PATCH 101/133] Add named location editing to CA template editor --- .../CippComponents/CippCAPolicyBuilder.jsx | 335 +++++++++++++++++- .../conditional/list-template/create.jsx | 2 +- .../tenant/conditional/list-template/edit.jsx | 1 + 3 files changed, 335 insertions(+), 3 deletions(-) diff --git a/src/components/CippComponents/CippCAPolicyBuilder.jsx b/src/components/CippComponents/CippCAPolicyBuilder.jsx index 30a38673bd4e..ed17aae9375b 100644 --- a/src/components/CippComponents/CippCAPolicyBuilder.jsx +++ b/src/components/CippComponents/CippCAPolicyBuilder.jsx @@ -11,16 +11,22 @@ import { Tooltip, IconButton, Paper, + Button, + Box, } from "@mui/material"; import { Grid } from "@mui/system"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import WarningAmberIcon from "@mui/icons-material/WarningAmber"; -import { useWatch } from "react-hook-form"; +import AddIcon from "@mui/icons-material/Add"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import PublicIcon from "@mui/icons-material/Public"; +import { useWatch, useFieldArray } from "react-hook-form"; import CippFormComponent from "./CippFormComponent"; import { CippFormCondition } from "./CippFormCondition"; import caSchema from "../../data/conditionalAccessSchema.json"; import gdapRoles from "../../data/GDAPRoles.json"; +import countryList from "../../data/countryList.json"; /** * CippCAPolicyBuilder — A schema-driven Conditional Access policy builder. @@ -840,10 +846,290 @@ function SessionControlsSection({ formControl, disabled }) { ); } +// --------------------------------------------------------------------------- +// Named Locations (template-embedded) section +// --------------------------------------------------------------------------- +// +// CIPP CA templates persist the named locations referenced by a policy under +// the top-level `LocationInfo` array (one entry per named location). Each +// entry uses Microsoft Graph shape: +// - country: { "@odata.type": "#microsoft.graph.countryNamedLocation", +// displayName, countriesAndRegions: [iso2…], +// includeUnknownCountriesAndRegions, countryLookupMethod } +// - ip: { "@odata.type": "#microsoft.graph.ipNamedLocation", +// displayName, isTrusted, +// ipRanges: [{ "@odata.type": "#microsoft.graph.iPv4CidrRange" +// | "#microsoft.graph.iPv6CidrRange", +// cidrAddress }] } +// +// We can't bind react-hook-form directly to keys containing dots +// (`@odata.type`), so the form uses a sanitised shape with `_type` and +// `_ipRangesText` fields that we map back on save. + +const COUNTRY_TYPE = "#microsoft.graph.countryNamedLocation"; +const IP_TYPE = "#microsoft.graph.ipNamedLocation"; +const IPV4_RANGE_TYPE = "#microsoft.graph.iPv4CidrRange"; +const IPV6_RANGE_TYPE = "#microsoft.graph.iPv6CidrRange"; + +const countryOptions = countryList.map(({ Code, Name }) => ({ value: Code, label: Name })); + +/** Convert one Graph-shape named location to form-shape. */ +function namedLocationToForm(loc) { + if (!loc || typeof loc !== "object") return null; + const type = loc["@odata.type"] === IP_TYPE ? "ip" : "country"; + if (type === "ip") { + const ipRangesText = Array.isArray(loc.ipRanges) + ? loc.ipRanges + .map((r) => r?.cidrAddress) + .filter((v) => typeof v === "string" && v.trim() !== "") + .join("\n") + : ""; + return { + _type: { label: "IP Ranges", value: "ip" }, + displayName: loc.displayName ?? "", + isTrusted: !!loc.isTrusted, + _ipRangesText: ipRangesText, + }; + } + const countries = Array.isArray(loc.countriesAndRegions) ? loc.countriesAndRegions : []; + const lookup = loc.countryLookupMethod ?? "clientIpAddress"; + return { + _type: { label: "Countries / Regions", value: "country" }, + displayName: loc.displayName ?? "", + countriesAndRegions: countries.map((code) => { + const match = countryOptions.find((o) => o.value === code); + return match ?? { label: code, value: code }; + }), + includeUnknownCountriesAndRegions: !!loc.includeUnknownCountriesAndRegions, + countryLookupMethod: { + label: lookup === "authenticatorAppGps" ? "Authenticator app GPS" : "Client IP address", + value: lookup, + }, + }; +} + +/** Unwrap an autoComplete `{label,value}` object to its underlying value. */ +function unwrapAC(v) { + if (v && typeof v === "object" && !Array.isArray(v) && "value" in v) return v.value; + return v; +} + +/** Convert one form-shape named location back to Graph shape. */ +function namedLocationToGraph(item) { + if (!item || !item.displayName || !item.displayName.trim()) return null; + const typeRaw = unwrapAC(item._type); + if (!typeRaw) return null; + const type = typeRaw === "ip" ? "ip" : "country"; + if (type === "ip") { + const lines = String(item._ipRangesText ?? "") + .split(/\r?\n/) + .map((s) => s.trim()) + .filter((s) => s !== ""); + if (lines.length === 0) return null; + return { + "@odata.type": IP_TYPE, + displayName: item.displayName.trim(), + isTrusted: !!item.isTrusted, + ipRanges: lines.map((cidr) => ({ + "@odata.type": cidr.includes(":") ? IPV6_RANGE_TYPE : IPV4_RANGE_TYPE, + cidrAddress: cidr, + })), + }; + } + // Country shape — unwrap autoComplete {label,value} objects if present + const countries = Array.isArray(item.countriesAndRegions) + ? item.countriesAndRegions + .map((c) => unwrapAC(c)) + .filter((v) => typeof v === "string" && v !== "") + : []; + if (countries.length === 0) return null; + const lookup = unwrapAC(item.countryLookupMethod); + return { + "@odata.type": COUNTRY_TYPE, + displayName: item.displayName.trim(), + countriesAndRegions: countries, + includeUnknownCountriesAndRegions: !!item.includeUnknownCountriesAndRegions, + countryLookupMethod: lookup || "clientIpAddress", + }; +} + +function NamedLocationsSection({ formControl, disabled }) { + const { fields, append, remove } = useFieldArray({ + control: formControl.control, + name: "LocationInfo", + }); + + return ( + + }> + Named locations defined here are stored inside the template and recreated (or matched by + display name) in the target tenant when the template is deployed. Reference them by name + in the Include Locations / Exclude Locations fields + under Conditions. + + + {fields.length === 0 && ( + + No named locations embedded in this template. + + )} + + {fields.map((field, index) => ( + + + + Named Location #{index + 1} + + + + remove(index)} + disabled={disabled} + aria-label="remove named location" + > + + + + + + + + + + + + + + + {/* IP fields */} + + + + + + + + + + {/* Country fields */} + + + + + + + + + + + + + + ))} + + + + + + ); +} + // --------------------------------------------------------------------------- // Main component // --------------------------------------------------------------------------- -const CippCAPolicyBuilder = ({ formControl, existingPolicy, disabled = false }) => { +const CippCAPolicyBuilder = ({ + formControl, + existingPolicy, + disabled = false, + showNamedLocations = false, +}) => { const policySchema = caSchema; // Pre-populate form from existing policy when editing @@ -876,6 +1162,20 @@ const CippCAPolicyBuilder = ({ formControl, existingPolicy, disabled = false }) const path = prefix ? `${prefix}.${key}` : key; + // Special handling for LocationInfo (template-embedded named locations). + // Graph-shape entries contain `@odata.type` keys that react-hook-form + // would interpret as nested paths, so we map them onto a form-friendly + // shape (`_type`, `_ipRangesText`, …) that NamedLocationsSection consumes. + if (key === "LocationInfo" && Array.isArray(value) && !prefix) { + const formItems = value + .map((item) => namedLocationToForm(item)) + .filter((v) => v !== null); + if (formItems.length > 0) { + formControl.setValue("LocationInfo", formItems); + } + return; + } + // Special handling for authenticationStrength — only extract the policy ID, // not the full expanded object (displayName, description, allowedCombinations, etc.) if (key === "authenticationStrength" && typeof value === "object" && !Array.isArray(value)) { @@ -1013,6 +1313,23 @@ const CippCAPolicyBuilder = ({ formControl, existingPolicy, disabled = false }) + + {/* Named Locations (template only) */} + {showNamedLocations && ( + + }> + + + Named Locations + + + + + + + + + )} ); }; @@ -1137,5 +1454,19 @@ export function extractCAPolicyJSON(formValues) { } } + // Post-process: convert template-embedded named locations from form-shape + // back to Graph shape. We read from the raw form values (not `cleaned`) + // because `clean()` strips internal keys prefixed with `_` (e.g. `_type`, + // `_ipRangesText`) that the conversion needs. + delete cleaned.LocationInfo; + if (Array.isArray(formValues?.LocationInfo)) { + const graphLocations = formValues.LocationInfo + .map((item) => namedLocationToGraph(item)) + .filter((v) => v !== null); + if (graphLocations.length > 0) { + cleaned.LocationInfo = graphLocations; + } + } + return cleaned; } diff --git a/src/pages/tenant/conditional/list-template/create.jsx b/src/pages/tenant/conditional/list-template/create.jsx index 1843c369c323..4e94e4327498 100644 --- a/src/pages/tenant/conditional/list-template/create.jsx +++ b/src/pages/tenant/conditional/list-template/create.jsx @@ -28,7 +28,7 @@ const CreateCATemplate = () => { formPageType="Add" > - + ); diff --git a/src/pages/tenant/conditional/list-template/edit.jsx b/src/pages/tenant/conditional/list-template/edit.jsx index 6d82be777062..85cd5bbc6397 100644 --- a/src/pages/tenant/conditional/list-template/edit.jsx +++ b/src/pages/tenant/conditional/list-template/edit.jsx @@ -162,6 +162,7 @@ const EditCATemplate = () => { ) : ( Date: Thu, 4 Jun 2026 23:28:04 +0800 Subject: [PATCH 102/133] Make breadcrumb text and > selectable/copyable --- .../CippComponents/CippBreadcrumbNav.jsx | 44 ++++++++++++++++--- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/components/CippComponents/CippBreadcrumbNav.jsx b/src/components/CippComponents/CippBreadcrumbNav.jsx index 5ea88f3434cf..a6bef3c6eeef 100644 --- a/src/components/CippComponents/CippBreadcrumbNav.jsx +++ b/src/components/CippComponents/CippBreadcrumbNav.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react' import { useRouter } from 'next/router' import { Breadcrumbs, Link, Typography, Box, IconButton, Tooltip } from '@mui/material' -import { NavigateNext, History, AccountTree } from '@mui/icons-material' +import { History, AccountTree } from '@mui/icons-material' import { nativeMenuItems } from '../../layouts/config' import { useSettings } from '../../hooks/use-settings' @@ -618,9 +618,14 @@ export const CippBreadcrumbNav = () => { } + separator=">" aria-label="page hierarchy" - sx={{ fontSize: '0.875rem', flexGrow: 1 }} + sx={{ + fontSize: '0.875rem', + flexGrow: 1, + userSelect: 'text', + '& .MuiBreadcrumbs-separator': { userSelect: 'text' }, + }} > {breadcrumbs.map((crumb, index) => { const isLast = index === breadcrumbs.length - 1 @@ -648,13 +653,23 @@ export const CippBreadcrumbNav = () => { return ( handleHierarchicalClick(crumb.path, crumb.query)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleHierarchicalClick(crumb.path, crumb.query) + } + }} sx={{ textDecoration: 'none', color: isLast ? 'text.primary' : 'text.secondary', fontWeight: isLast ? 500 : 400, + cursor: 'pointer', + userSelect: 'text', '&:hover': { textDecoration: 'underline', color: 'primary.main', @@ -704,9 +719,14 @@ export const CippBreadcrumbNav = () => { } + separator=">" aria-label="navigation history" - sx={{ fontSize: '0.875rem', flexGrow: 1 }} + sx={{ + fontSize: '0.875rem', + flexGrow: 1, + userSelect: 'text', + '& .MuiBreadcrumbs-separator': { userSelect: 'text' }, + }} > {visibleHistory.map((page, index) => { const isLast = index === visibleHistory.length - 1 @@ -729,12 +749,22 @@ export const CippBreadcrumbNav = () => { return ( handleBreadcrumbClick(actualIndex)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + handleBreadcrumbClick(actualIndex) + } + }} sx={{ textDecoration: 'none', color: 'text.secondary', + cursor: 'pointer', + userSelect: 'text', '&:hover': { textDecoration: 'underline', color: 'primary.main', From b7c051fa865a06eaf3747be60635740f4837c4ac Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:52:25 +0200 Subject: [PATCH 103/133] Auth changes to use sedndmessage --- .../CippComponents/CIPPM365OAuthButton.jsx | 552 +++++++++--------- src/pages/authredirect.js | 73 ++- 2 files changed, 295 insertions(+), 330 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index 0818190ca1cc..cc4086470dab 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -1,29 +1,29 @@ -import { useState, useEffect } from "react"; -import { Alert, Button, Typography, CircularProgress, Box } from "@mui/material"; -import { Microsoft, Login, Refresh } from "@mui/icons-material"; -import { ApiGetCall } from "../../api/ApiCall"; -import { CippCopyToClipBoard } from "./CippCopyToClipboard"; -import { CippApiDialog } from "./CippApiDialog"; +import { useState, useEffect } from 'react' +import { Alert, Button, Typography, CircularProgress, Box } from '@mui/material' +import { Microsoft, Login, Refresh } from '@mui/icons-material' +import { ApiGetCall } from '../../api/ApiCall' +import { CippCopyToClipBoard } from './CippCopyToClipboard' +import { CippApiDialog } from './CippApiDialog' export const CIPPM365OAuthButton = ({ onAuthSuccess, onAuthError, - buttonText = "Login with Microsoft", + buttonText = 'Login with Microsoft', showResults = true, showSuccessAlert = true, - scope = "https://graph.microsoft.com/.default offline_access profile openid", + scope = 'https://graph.microsoft.com/.default offline_access profile openid', useDeviceCode = false, applicationId = null, autoStartDeviceLogon = false, validateServiceAccount = true, promptBeforeAuth = false, }) => { - const [authInProgress, setAuthInProgress] = useState(false); - const [authError, setAuthError] = useState(null); - const [deviceCodeInfo, setDeviceCodeInfo] = useState(null); - const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false); - const [isServiceAccount, setIsServiceAccount] = useState(true); - const [promptDialog, setPromptDialog] = useState({ open: false }); + const [authInProgress, setAuthInProgress] = useState(false) + const [authError, setAuthError] = useState(null) + const [deviceCodeInfo, setDeviceCodeInfo] = useState(null) + const [codeRetrievalInProgress, setCodeRetrievalInProgress] = useState(false) + const [isServiceAccount, setIsServiceAccount] = useState(true) + const [promptDialog, setPromptDialog] = useState({ open: false }) const [tokens, setTokens] = useState({ accessToken: null, refreshToken: null, @@ -32,84 +32,84 @@ export const CIPPM365OAuthButton = ({ username: null, tenantId: null, onmicrosoftDomain: null, - }); + }) const appIdInfo = ApiGetCall({ url: `/api/ExecListAppId`, - queryKey: "listAppId", + queryKey: 'listAppId', waiting: true, - }); + }) const handleCloseError = () => { - setAuthError(null); - }; + setAuthError(null) + } const checkIsServiceAccount = (username) => { - if (!username || !validateServiceAccount) return true; // If no username or validation disabled, don't show warning + if (!username || !validateServiceAccount) return true // If no username or validation disabled, don't show warning - const lowerUsername = username.toLowerCase(); - return lowerUsername.includes("service") || lowerUsername.includes("cipp"); - }; + const lowerUsername = username.toLowerCase() + return lowerUsername.includes('service') || lowerUsername.includes('cipp') + } // Function to retrieve device code const retrieveDeviceCode = async () => { - setCodeRetrievalInProgress(true); - setAuthError(null); + setCodeRetrievalInProgress(true) + setAuthError(null) // Only refetch appId if not already present if (!applicationId && !appIdInfo?.data?.applicationId) { - await appIdInfo.refetch(); + await appIdInfo.refetch() } try { // Get the application ID to use const appId = - applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + applicationId || appIdInfo?.data?.applicationId || '1b730954-1685-4b74-9bfd-dac224a7b894' // Default to MS Graph Explorer app ID // Request device code from our API endpoint const deviceCodeResponse = await fetch( `/api/ExecDeviceCodeLogon?operation=getDeviceCode&clientId=${appId}&scope=${encodeURIComponent( - scope, - )}`, - ); - const deviceCodeData = await deviceCodeResponse.json(); + scope + )}` + ) + const deviceCodeData = await deviceCodeResponse.json() if (deviceCodeResponse.ok && deviceCodeData.user_code) { // Store device code info - setDeviceCodeInfo(deviceCodeData); + setDeviceCodeInfo(deviceCodeData) } else { // Error getting device code setAuthError({ - errorCode: deviceCodeData.error || "device_code_error", - errorMessage: deviceCodeData.error_description || "Failed to get device code", + errorCode: deviceCodeData.error || 'device_code_error', + errorMessage: deviceCodeData.error_description || 'Failed to get device code', timestamp: new Date().toISOString(), - }); + }) } } catch (error) { setAuthError({ - errorCode: "device_code_error", - errorMessage: error.message || "An error occurred retrieving device code", + errorCode: 'device_code_error', + errorMessage: error.message || 'An error occurred retrieving device code', timestamp: new Date().toISOString(), - }); + }) } finally { - setCodeRetrievalInProgress(false); + setCodeRetrievalInProgress(false) } - }; + } // Device code authentication function - opens popup and starts polling const handleDeviceCodeAuthentication = async () => { // Only refetch appId if not already present if (!applicationId && !appIdInfo?.data?.applicationId) { - await appIdInfo.refetch(); + await appIdInfo.refetch() } if (!deviceCodeInfo) { // If we don't have a device code yet, retrieve it first - await retrieveDeviceCode(); - return; + await retrieveDeviceCode() + return } - setAuthInProgress(true); + setAuthInProgress(true) setTokens({ accessToken: null, refreshToken: null, @@ -118,131 +118,131 @@ export const CIPPM365OAuthButton = ({ username: null, tenantId: null, onmicrosoftDomain: null, - }); + }) try { // Get the application ID to use - refetch already happened at the start of this function const appId = - applicationId || appIdInfo?.data?.applicationId || "1b730954-1685-4b74-9bfd-dac224a7b894"; // Default to MS Graph Explorer app ID + applicationId || appIdInfo?.data?.applicationId || '1b730954-1685-4b74-9bfd-dac224a7b894' // Default to MS Graph Explorer app ID // Open popup to device login page - const width = 500; - const height = 600; - const left = window.screen.width / 2 - width / 2; - const top = window.screen.height / 2 - height / 2; + const width = 500 + const height = 600 + const left = window.screen.width / 2 - width / 2 + const top = window.screen.height / 2 - height / 2 const popup = window.open( - "https://microsoft.com/devicelogin", - "deviceLoginPopup", - `width=${width},height=${height},left=${left},top=${top}`, - ); + 'https://microsoft.com/devicelogin', + 'deviceLoginPopup', + `width=${width},height=${height},left=${left},top=${top}` + ) // Start polling for token - const pollInterval = deviceCodeInfo.interval || 5; - const expiresIn = deviceCodeInfo.expires_in || 900; - const startTime = Date.now(); + const pollInterval = deviceCodeInfo.interval || 5 + const expiresIn = deviceCodeInfo.expires_in || 900 + const startTime = Date.now() const pollForToken = async () => { // Check if we've exceeded the expiration time if (Date.now() - startTime >= expiresIn * 1000) { if (popup && !popup.closed) { - popup.close(); + popup.close() } setAuthError({ - errorCode: "timeout", - errorMessage: "Device code authentication timed out", + errorCode: 'timeout', + errorMessage: 'Device code authentication timed out', timestamp: new Date().toISOString(), - }); - setAuthInProgress(false); - return; + }) + setAuthInProgress(false) + return } try { // Poll for token using our API endpoint const tokenResponse = await fetch( - `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}`, - ); - const tokenData = await tokenResponse.json(); + `/api/ExecDeviceCodeLogon?operation=checkToken&clientId=${appId}&deviceCode=${deviceCodeInfo.device_code}` + ) + const tokenData = await tokenResponse.json() - if (tokenResponse.ok && tokenData.status === "success") { + if (tokenResponse.ok && tokenData.status === 'success') { // Successfully got token if (popup && !popup.closed) { - popup.close(); + popup.close() } - handleTokenResponse(tokenData); + handleTokenResponse(tokenData) } else if ( - tokenData.error === "authorization_pending" || - tokenData.status === "pending" + tokenData.error === 'authorization_pending' || + tokenData.status === 'pending' ) { // User hasn't completed authentication yet, continue polling - setTimeout(pollForToken, pollInterval * 1000); - } else if (tokenData.error === "slow_down") { + setTimeout(pollForToken, pollInterval * 1000) + } else if (tokenData.error === 'slow_down') { // Server asking us to slow down polling - setTimeout(pollForToken, (pollInterval + 5) * 1000); + setTimeout(pollForToken, (pollInterval + 5) * 1000) } else { // Other error if (popup && !popup.closed) { - popup.close(); + popup.close() } setAuthError({ - errorCode: tokenData.error || "token_error", - errorMessage: tokenData.error_description || "Failed to get token", + errorCode: tokenData.error || 'token_error', + errorMessage: tokenData.error_description || 'Failed to get token', timestamp: new Date().toISOString(), - }); - setAuthInProgress(false); + }) + setAuthInProgress(false) } } catch (error) { - setTimeout(pollForToken, pollInterval * 1000); + setTimeout(pollForToken, pollInterval * 1000) } - }; + } // Start polling - setTimeout(pollForToken, pollInterval * 1000); + setTimeout(pollForToken, pollInterval * 1000) } catch (error) { setAuthError({ - errorCode: "device_code_error", - errorMessage: error.message || "An error occurred during device code authentication", + errorCode: 'device_code_error', + errorMessage: error.message || 'An error occurred during device code authentication', timestamp: new Date().toISOString(), - }); - setAuthInProgress(false); + }) + setAuthInProgress(false) } - }; + } // Process token response (common for both auth methods) const handleTokenResponse = (tokenData) => { // Extract token information - const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000); + const accessTokenExpiresOn = new Date(Date.now() + tokenData.expires_in * 1000) // Refresh tokens typically last for 90 days, but this can vary - const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); + const refreshTokenExpiresOn = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // Extract information from ID token if available - let username = "unknown user"; - let tenantId = "unknown tenant"; - let onmicrosoftDomain = null; + let username = 'unknown user' + let tenantId = 'unknown tenant' + let onmicrosoftDomain = null if (tokenData.id_token) { try { - const idTokenPayload = JSON.parse(atob(tokenData.id_token.split(".")[1])); + const idTokenPayload = JSON.parse(atob(tokenData.id_token.split('.')[1])) username = idTokenPayload.preferred_username || idTokenPayload.email || idTokenPayload.upn || idTokenPayload.name || - "unknown user"; + 'unknown user' if (idTokenPayload.tid) { - tenantId = idTokenPayload.tid; + tenantId = idTokenPayload.tid } - if (username && username.includes("@") && username.includes(".onmicrosoft.com")) { - onmicrosoftDomain = username.split("@")[1]; + if (username && username.includes('@') && username.includes('.onmicrosoft.com')) { + onmicrosoftDomain = username.split('@')[1] } else if (idTokenPayload.iss) { - const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//); + const issuerMatch = idTokenPayload.iss.match(/https:\/\/sts\.windows\.net\/([^/]+)\//) if (issuerMatch && issuerMatch[1]) { } } - setIsServiceAccount(checkIsServiceAccount(username)); + setIsServiceAccount(checkIsServiceAccount(username)) } catch (error) {} } @@ -255,25 +255,25 @@ export const CIPPM365OAuthButton = ({ username: username, tenantId: tenantId, onmicrosoftDomain: onmicrosoftDomain, - }; + } - setTokens(tokenResult); - setDeviceCodeInfo(null); + setTokens(tokenResult) + setDeviceCodeInfo(null) - if (onAuthSuccess) onAuthSuccess(tokenResult); + if (onAuthSuccess) onAuthSuccess(tokenResult) // Update UI state - setAuthInProgress(false); - setIsServiceAccount(checkIsServiceAccount(username)); - }; + setAuthInProgress(false) + setIsServiceAccount(checkIsServiceAccount(username)) + } // MSAL-like authentication function const handleMsalAuthentication = async (retryCount = 0) => { - const maxRetries = 3; + const maxRetries = 3 // Clear previous authentication state when starting a new authentication - setAuthInProgress(true); - setAuthError(null); + setAuthInProgress(true) + setAuthError(null) setTokens({ accessToken: null, refreshToken: null, @@ -282,15 +282,15 @@ export const CIPPM365OAuthButton = ({ username: null, tenantId: null, onmicrosoftDomain: null, - }); + }) // Only refetch app ID if not already present if (!applicationId && !appIdInfo?.data?.applicationId) { - await appIdInfo.refetch(); + await appIdInfo.refetch() } // Get the application ID to use - const appId = applicationId || appIdInfo?.data?.applicationId; + const appId = applicationId || appIdInfo?.data?.applicationId // Generate MSAL-like authentication parameters const msalConfig = { @@ -299,23 +299,23 @@ export const CIPPM365OAuthButton = ({ authority: `https://login.microsoftonline.com/common`, redirectUri: `${window.location.origin}/authredirect`, }, - }; + } // Define the request object similar to MSAL const loginRequest = { scopes: [scope], - }; + } // Generate PKCE code verifier and challenge const generateCodeVerifier = () => { - const array = new Uint8Array(32); - window.crypto.getRandomValues(array); - return Array.from(array, (byte) => ("0" + (byte & 0xff).toString(16)).slice(-2)).join(""); - }; - - const codeVerifier = generateCodeVerifier(); - const codeChallenge = codeVerifier; - const state = Math.random().toString(36).substring(2, 15); + const array = new Uint8Array(32) + window.crypto.getRandomValues(array) + return Array.from(array, (byte) => ('0' + (byte & 0xff).toString(16)).slice(-2)).join('') + } + + const codeVerifier = generateCodeVerifier() + const codeChallenge = codeVerifier + const state = Math.random().toString(36).substring(2, 15) const authUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/authorize?` + `client_id=${appId}` + @@ -325,96 +325,96 @@ export const CIPPM365OAuthButton = ({ `&code_challenge=${codeChallenge}` + `&code_challenge_method=plain` + `&state=${state}` + - `&prompt=select_account`; + `&prompt=select_account` // Open popup for authentication - const width = 500; - const height = 600; - const left = window.screen.width / 2 - width / 2; - const top = window.screen.height / 2 - height / 2; + const width = 500 + const height = 600 + const left = window.screen.width / 2 - width / 2 + const top = window.screen.height / 2 - height / 2 const popup = window.open( authUrl, - "msalAuthPopup", - `width=${width},height=${height},left=${left},top=${top}`, - ); + 'msalAuthPopup', + `width=${width},height=${height},left=${left},top=${top}` + ) // Function to actually exchange the authorization code for tokens const handleAuthorizationCode = async (code, receivedState) => { // Verify the state parameter matches what we sent (security check) if (receivedState !== state) { - const errorMessage = "State mismatch in auth response - possible CSRF attack"; + const errorMessage = 'State mismatch in auth response - possible CSRF attack' const error = { - errorCode: "state_mismatch", + errorCode: 'state_mismatch', errorMessage: errorMessage, timestamp: new Date().toISOString(), - }; - setAuthError(error); - if (onAuthError) onAuthError(error); - setAuthInProgress(false); - return; + } + setAuthError(error) + if (onAuthError) onAuthError(error) + setAuthInProgress(false) + return } try { // Prepare the token request const tokenRequest = { - grant_type: "authorization_code", + grant_type: 'authorization_code', client_id: appId, code: code, redirect_uri: `${window.location.origin}/authredirect`, code_verifier: codeVerifier, - }; + } // Make the token request through our API proxy to avoid origin header issues // Retry logic for AADSTS650051 (service principal already exists) - let retryCount = 0; - const maxRetries = 3; - let tokenResponse; - let tokenData; + let retryCount = 0 + const maxRetries = 3 + let tokenResponse + let tokenData while (retryCount <= maxRetries) { tokenResponse = await fetch(`/api/ExecTokenExchange`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ tokenRequest, - tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", + tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', tenantId: appId, // Pass the tenant ID to retrieve the correct client secret }), - }); + }) // Parse the token response - tokenData = await tokenResponse.json(); + tokenData = await tokenResponse.json() // Check if it's the AADSTS650051 error (service principal already exists) if ( - tokenData.error === "invalid_client" && - tokenData.error_description?.includes("AADSTS650051") + tokenData.error === 'invalid_client' && + tokenData.error_description?.includes('AADSTS650051') ) { - retryCount++; + retryCount++ if (retryCount <= maxRetries) { // Wait before retrying (exponential backoff) - await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount)); - continue; + await new Promise((resolve) => setTimeout(resolve, 2000 * retryCount)) + continue } } // If no error or different error, break out of retry loop - break; + break } // Check if the response contains an error if (tokenData.error) { const error = { - errorCode: tokenData.error || "token_error", + errorCode: tokenData.error || 'token_error', errorMessage: - tokenData.error_description || "Failed to exchange authorization code for tokens", + tokenData.error_description || 'Failed to exchange authorization code for tokens', timestamp: new Date().toISOString(), - }; - setAuthError(error); - if (onAuthError) onAuthError(error); - setAuthInProgress(false); - return; + } + setAuthError(error) + if (onAuthError) onAuthError(error) + setAuthInProgress(false) + return } if (tokenResponse.ok) { @@ -422,13 +422,13 @@ export const CIPPM365OAuthButton = ({ if (tokenData.refresh_token) { try { // Extract tid from access_token jwt base64 - const accessTokenParts = tokenData.access_token.split("."); - const accessTokenPayload = JSON.parse(atob(accessTokenParts[1] || "")); - tokenData.tid = accessTokenPayload.tid; + const accessTokenParts = tokenData.access_token.split('.') + const accessTokenPayload = JSON.parse(atob(accessTokenParts[1] || '')) + tokenData.tid = accessTokenPayload.tid const refreshResponse = await fetch(`/api/ExecUpdateRefreshToken`, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ tenantId: tokenData.tid, @@ -436,68 +436,100 @@ export const CIPPM365OAuthButton = ({ tenantMode: tokenData.tenantMode, allowPartnerTenantManagement: tokenData.allowPartnerTenantManagement, }), - }); + }) if (!refreshResponse.ok) { - console.warn("Failed to store refresh token, but continuing with authentication"); + console.warn('Failed to store refresh token, but continuing with authentication') } else { // Invalidate the listAppId and tenants-table queryKeys to refresh data - appIdInfo.refetch(); + appIdInfo.refetch() } } catch (error) { - console.error("Failed to store refresh token:", error); + console.error('Failed to store refresh token:', error) } } - handleTokenResponse(tokenData); + handleTokenResponse(tokenData) } else { // Handle token error - display in error box instead of throwing const error = { - errorCode: tokenData.error || "token_error", + errorCode: tokenData.error || 'token_error', errorMessage: - tokenData.error_description || "Failed to exchange authorization code for tokens", + tokenData.error_description || 'Failed to exchange authorization code for tokens', timestamp: new Date().toISOString(), - }; - setAuthError(error); - if (onAuthError) onAuthError(error); + } + setAuthError(error) + if (onAuthError) onAuthError(error) } } catch (error) { const errorObj = { - errorCode: "token_exchange_error", - errorMessage: error.message || "Failed to exchange authorization code for tokens", + errorCode: 'token_exchange_error', + errorMessage: error.message || 'Failed to exchange authorization code for tokens', timestamp: new Date().toISOString(), - }; - setAuthError(errorObj); - if (onAuthError) onAuthError(errorObj); + } + setAuthError(errorObj) + if (onAuthError) onAuthError(errorObj) } finally { // Close the popup window if it's still open if (popup && !popup.closed) { - popup.close(); + popup.close() } // Update UI state - setAuthInProgress(false); + setAuthInProgress(false) } - }; + } - // Monitor for the redirect with the authorization code - // This is what MSAL does internally - const checkPopupLocation = setInterval(() => { - if (!popup || popup.closed) { - clearInterval(checkPopupLocation); + // Listen for postMessage from the authredirect page + let codeReceived = false + + const handleMessage = (event) => { + if (event.origin !== window.location.origin) return + + if (event.data?.type === 'auth_code') { + codeReceived = true + cleanup() + handleAuthorizationCode(event.data.code, event.data.state) + } else if (event.data?.type === 'auth_error') { + codeReceived = true + cleanup() + + // Check if it's the AADSTS650051 error (service principal already exists during consent) + if ( + event.data.error === 'invalid_client' && + event.data.errorDescription?.includes('AADSTS650051') && + retryCount < maxRetries + ) { + if (popup && !popup.closed) popup.close() + setAuthInProgress(false) + setTimeout(() => handleMsalAuthentication(retryCount + 1), 2000 * (retryCount + 1)) + return + } + + const error = { + errorCode: event.data.error || 'auth_error', + errorMessage: event.data.errorDescription || 'Unknown authentication error', + timestamp: new Date().toISOString(), + } + setAuthError(error) + if (onAuthError) onAuthError(error) + if (popup && !popup.closed) popup.close() + setAuthInProgress(false) + } + } - // If authentication is still in progress when popup closes, it's an error - if (authInProgress) { - const errorMessage = "Authentication was cancelled. Please try again."; + // Check if popup was closed before we received the code + const popupClosedCheck = setInterval(() => { + if (!popup || popup.closed) { + if (!codeReceived) { + cleanup() const error = { - errorCode: "user_cancelled", - errorMessage: errorMessage, + errorCode: 'user_cancelled', + errorMessage: 'Authentication was cancelled. Please try again.', timestamp: new Date().toISOString(), - }; - setAuthError(error); - if (onAuthError) onAuthError(error); - - // Ensure we're not showing any previous success state + } + setAuthError(error) + if (onAuthError) onAuthError(error) setTokens({ accessToken: null, refreshToken: null, @@ -506,79 +538,19 @@ export const CIPPM365OAuthButton = ({ username: null, tenantId: null, onmicrosoftDomain: null, - }); + }) + setAuthInProgress(false) } - - setAuthInProgress(false); - return; } + }, 500) - try { - // Try to access the popup location to check for the authorization code - const currentUrl = popup.location.href; - - // Check if the URL contains a code parameter (authorization code) - if (currentUrl.includes("code=") && currentUrl.includes("state=")) { - clearInterval(checkPopupLocation); - // Parse the URL to extract the code and state - const urlParams = new URLSearchParams(popup.location.search); - const code = urlParams.get("code"); - const receivedState = urlParams.get("state"); - - // Process the authorization code - handleAuthorizationCode(code, receivedState); - } - - // Check for error in the URL - if (currentUrl.includes("error=")) { - clearInterval(checkPopupLocation); - // Parse the URL to extract the error details - const urlParams = new URLSearchParams(popup.location.search); - const errorCode = urlParams.get("error"); - const errorDescription = urlParams.get("error_description"); - - // Check if it's the AADSTS650051 error (service principal already exists during consent) - if ( - errorCode === "invalid_client" && - errorDescription?.includes("AADSTS650051") && - retryCount < maxRetries - ) { - // Close the popup - popup.close(); - setAuthInProgress(false); - - // Wait before retrying (exponential backoff) - setTimeout( - () => { - handleMsalAuthentication(retryCount + 1); - }, - 2000 * (retryCount + 1), - ); - return; - } - - // Set the error state for non-retryable errors - const error = { - errorCode: errorCode, - errorMessage: errorDescription || "Unknown authentication error", - timestamp: new Date().toISOString(), - }; - setAuthError(error); - if (onAuthError) onAuthError(error); - - // Close the popup - popup.close(); - setAuthInProgress(false); - } - } catch (error) { - // This will throw an error when the popup is on a different domain - // due to cross-origin restrictions, which is normal during auth flow - // Just continue monitoring - } - }, 500); + const cleanup = () => { + window.removeEventListener('message', handleMessage) + clearInterval(popupClosedCheck) + } - // Also monitor for popup closing as a fallback - }; + window.addEventListener('message', handleMessage) + } // Auto-start device code retrieval if requested useEffect(() => { @@ -590,7 +562,7 @@ export const CIPPM365OAuthButton = ({ !tokens.accessToken && appIdInfo?.data ) { - retrieveDeviceCode(); + retrieveDeviceCode() } }, [ useDeviceCode, @@ -599,7 +571,7 @@ export const CIPPM365OAuthButton = ({ deviceCodeInfo, tokens.accessToken, appIdInfo?.data, - ]); + ]) return (
@@ -607,7 +579,7 @@ export const CIPPM365OAuthButton = ({ !appIdInfo.isLoading && appIdInfo?.data?.applicationId && // Only check if applicationId is present in data !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( - appIdInfo?.data?.applicationId, + appIdInfo?.data?.applicationId ) && ( The Application ID is not valid. Please check your configuration. @@ -622,14 +594,14 @@ export const CIPPM365OAuthButton = ({ {authInProgress ? ( <> - When asked to log onto an account, please use a{" "} - CIPP Service Account. Enter this code to authenticate:{" "} + When asked to log onto an account, please use a{' '} + CIPP Service Account. Enter this code to authenticate:{' '} ) : ( <> Click the button below to authenticate. When asked to log onto an account, please use a CIPP Service Account. You will need to enter this - code:{" "} + code:{' '} )} @@ -637,13 +609,13 @@ export const CIPPM365OAuthButton = ({ {authInProgress ? ( <> - If the popup was blocked or you closed it, you can also go to{" "} + If the popup was blocked or you closed it, you can also go to{' '} microsoft.com/devicelogin manually and enter the code shown above. ) : ( <> - When you click the button below, a popup will open to{" "} + When you click the button below, a popup will open to{' '} microsoft.com/devicelogin where you'll enter this code. )} @@ -665,7 +637,7 @@ export const CIPPM365OAuthButton = ({ Tenant ID: {tokens.tenantId} {tokens.onmicrosoftDomain && ( <> - {" "} + {' '} | Domain: {tokens.onmicrosoftDomain} )} @@ -711,21 +683,21 @@ export const CIPPM365OAuthButton = ({ {promptBeforeAuth !== false && ( setPromptDialog({ open: false }), }} api={{ - type: "POST", + type: 'POST', confirmText: promptBeforeAuth, noConfirm: false, customFunction: () => { - setPromptDialog({ open: false }); + setPromptDialog({ open: false }) const authFunction = useDeviceCode ? handleDeviceCodeAuthentication - : handleMsalAuthentication; - authFunction(); + : handleMsalAuthentication + authFunction() }, }} fields={[]} @@ -740,17 +712,17 @@ export const CIPPM365OAuthButton = ({ codeRetrievalInProgress || (!applicationId && !/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test( - appIdInfo?.data?.applicationId, + appIdInfo?.data?.applicationId )) } onClick={() => { if (promptBeforeAuth !== false) { - setPromptDialog({ open: true }); + setPromptDialog({ open: true }) } else { const authFunction = useDeviceCode ? handleDeviceCodeAuthentication - : handleMsalAuthentication; - authFunction(); + : handleMsalAuthentication + authFunction() } }} color="primary" @@ -765,11 +737,11 @@ export const CIPPM365OAuthButton = ({ } > {authInProgress || codeRetrievalInProgress - ? "Authenticating..." + ? 'Authenticating...' : deviceCodeInfo && useDeviceCode - ? "Authenticate with Code" + ? 'Authenticate with Code' : buttonText}
- ); -}; + ) +} diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js index ea8edfe0937d..a5f4186ea206 100644 --- a/src/pages/authredirect.js +++ b/src/pages/authredirect.js @@ -1,47 +1,40 @@ -import { Box, Container, Stack } from "@mui/material"; -import { Grid } from "@mui/system"; +import { useEffect } from "react"; import Head from "next/head"; -import { CippImageCard } from "../components/CippCards/CippImageCard.jsx"; -import { Layout as DashboardLayout } from "../layouts/index.js"; -const Page = () => ( - <> - +const Page = () => { + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const code = params.get("code"); + const state = params.get("state"); + const error = params.get("error"); + const errorDescription = params.get("error_description"); + + if (window.opener) { + if (code && state) { + window.opener.postMessage( + { type: "auth_code", code, state }, + window.location.origin + ); + } else if (error) { + window.opener.postMessage( + { type: "auth_error", error, errorDescription }, + window.location.origin + ); + } + window.close(); + } + }, []); + + return ( + <> Authentication complete - - - - - - - - - - - - - -); +
+

Authentication complete. This window will close automatically.

+
+ + ); +}; export default Page; From 67039061cb901709c0cd35b2baa1fca1026cdc1e Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 21:52:28 +0200 Subject: [PATCH 104/133] Auth changes to use sedndmessage --- src/pages/authredirect.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js index a5f4186ea206..8acf21eee84e 100644 --- a/src/pages/authredirect.js +++ b/src/pages/authredirect.js @@ -1,40 +1,39 @@ -import { useEffect } from "react"; -import Head from "next/head"; +import { useEffect } from 'react' +import Head from 'next/head' const Page = () => { useEffect(() => { - const params = new URLSearchParams(window.location.search); - const code = params.get("code"); - const state = params.get("state"); - const error = params.get("error"); - const errorDescription = params.get("error_description"); + const params = new URLSearchParams(window.location.search) + const code = params.get('code') + const state = params.get('state') + const error = params.get('error') + const errorDescription = params.get('error_description') if (window.opener) { if (code && state) { - window.opener.postMessage( - { type: "auth_code", code, state }, - window.location.origin - ); + window.opener.postMessage({ type: 'auth_code', code, state }, window.location.origin) } else if (error) { window.opener.postMessage( - { type: "auth_error", error, errorDescription }, + { type: 'auth_error', error, errorDescription }, window.location.origin - ); + ) } - window.close(); + window.close() } - }, []); + }, []) return ( <> Authentication complete -
+

Authentication complete. This window will close automatically.

- ); -}; + ) +} -export default Page; +export default Page From 5b09ef3264fef015af58826d1977c75e1960190f Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 16:55:52 -0400 Subject: [PATCH 105/133] fix: add popup grace period --- .../CippComponents/CIPPM365OAuthButton.jsx | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index cc4086470dab..61e240579b5a 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -327,18 +327,31 @@ export const CIPPM365OAuthButton = ({ `&state=${state}` + `&prompt=select_account` - // Open popup for authentication + // Open a blank popup first, then navigate it. This keeps the window reference stable and + // avoids treating slow Microsoft page loads as an immediate user cancellation. const width = 500 const height = 600 const left = window.screen.width / 2 - width / 2 const top = window.screen.height / 2 - height / 2 const popup = window.open( - authUrl, + 'about:blank', 'msalAuthPopup', `width=${width},height=${height},left=${left},top=${top}` ) + if (!popup) { + setAuthError({ + errorCode: 'popup_blocked', + errorMessage: 'Authentication popup was blocked by the browser. Please allow popups and try again.', + timestamp: new Date().toISOString(), + }) + setAuthInProgress(false) + return + } + + popup.location.href = authUrl + // Function to actually exchange the authorization code for tokens const handleAuthorizationCode = async (code, receivedState) => { // Verify the state parameter matches what we sent (security check) @@ -482,6 +495,8 @@ export const CIPPM365OAuthButton = ({ // Listen for postMessage from the authredirect page let codeReceived = false + const popupGracePeriodMs = 5000 + const popupOpenedAt = Date.now() const handleMessage = (event) => { if (event.origin !== window.location.origin) return @@ -520,6 +535,10 @@ export const CIPPM365OAuthButton = ({ // Check if popup was closed before we received the code const popupClosedCheck = setInterval(() => { + if (Date.now() - popupOpenedAt < popupGracePeriodMs) { + return + } + if (!popup || popup.closed) { if (!codeReceived) { cleanup() From ffefbbb337754727297ecec9be79e1e7b16a4b32 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:24:15 +0200 Subject: [PATCH 106/133] Use broadcast channel --- .../CippComponents/CIPPM365OAuthButton.jsx | 75 +++++++------------ src/pages/authredirect.js | 17 ++--- 2 files changed, 34 insertions(+), 58 deletions(-) diff --git a/src/components/CippComponents/CIPPM365OAuthButton.jsx b/src/components/CippComponents/CIPPM365OAuthButton.jsx index cc4086470dab..5342b51d931c 100644 --- a/src/components/CippComponents/CIPPM365OAuthButton.jsx +++ b/src/components/CippComponents/CIPPM365OAuthButton.jsx @@ -333,11 +333,7 @@ export const CIPPM365OAuthButton = ({ const left = window.screen.width / 2 - width / 2 const top = window.screen.height / 2 - height / 2 - const popup = window.open( - authUrl, - 'msalAuthPopup', - `width=${width},height=${height},left=${left},top=${top}` - ) + window.open(authUrl, 'msalAuthPopup', `width=${width},height=${height},left=${left},top=${top}`) // Function to actually exchange the authorization code for tokens const handleAuthorizationCode = async (code, receivedState) => { @@ -470,28 +466,41 @@ export const CIPPM365OAuthButton = ({ setAuthError(errorObj) if (onAuthError) onAuthError(errorObj) } finally { - // Close the popup window if it's still open - if (popup && !popup.closed) { - popup.close() - } - // Update UI state setAuthInProgress(false) } } - // Listen for postMessage from the authredirect page - let codeReceived = false + // Listen for auth result via BroadcastChannel (works regardless of COOP) + const channel = new BroadcastChannel('cipp_auth') - const handleMessage = (event) => { - if (event.origin !== window.location.origin) return + const authTimeout = setTimeout(() => { + // If no response after 10 minutes, treat as cancelled + cleanup() + const error = { + errorCode: 'timeout', + errorMessage: 'Authentication timed out. Please try again.', + timestamp: new Date().toISOString(), + } + setAuthError(error) + if (onAuthError) onAuthError(error) + setTokens({ + accessToken: null, + refreshToken: null, + accessTokenExpiresOn: null, + refreshTokenExpiresOn: null, + username: null, + tenantId: null, + onmicrosoftDomain: null, + }) + setAuthInProgress(false) + }, 600000) + channel.onmessage = (event) => { if (event.data?.type === 'auth_code') { - codeReceived = true cleanup() handleAuthorizationCode(event.data.code, event.data.state) } else if (event.data?.type === 'auth_error') { - codeReceived = true cleanup() // Check if it's the AADSTS650051 error (service principal already exists during consent) @@ -500,7 +509,6 @@ export const CIPPM365OAuthButton = ({ event.data.errorDescription?.includes('AADSTS650051') && retryCount < maxRetries ) { - if (popup && !popup.closed) popup.close() setAuthInProgress(false) setTimeout(() => handleMsalAuthentication(retryCount + 1), 2000 * (retryCount + 1)) return @@ -513,43 +521,14 @@ export const CIPPM365OAuthButton = ({ } setAuthError(error) if (onAuthError) onAuthError(error) - if (popup && !popup.closed) popup.close() setAuthInProgress(false) } } - // Check if popup was closed before we received the code - const popupClosedCheck = setInterval(() => { - if (!popup || popup.closed) { - if (!codeReceived) { - cleanup() - const error = { - errorCode: 'user_cancelled', - errorMessage: 'Authentication was cancelled. Please try again.', - timestamp: new Date().toISOString(), - } - setAuthError(error) - if (onAuthError) onAuthError(error) - setTokens({ - accessToken: null, - refreshToken: null, - accessTokenExpiresOn: null, - refreshTokenExpiresOn: null, - username: null, - tenantId: null, - onmicrosoftDomain: null, - }) - setAuthInProgress(false) - } - } - }, 500) - const cleanup = () => { - window.removeEventListener('message', handleMessage) - clearInterval(popupClosedCheck) + channel.close() + clearTimeout(authTimeout) } - - window.addEventListener('message', handleMessage) } // Auto-start device code retrieval if requested diff --git a/src/pages/authredirect.js b/src/pages/authredirect.js index 8acf21eee84e..7295c7f1f9c9 100644 --- a/src/pages/authredirect.js +++ b/src/pages/authredirect.js @@ -9,17 +9,14 @@ const Page = () => { const error = params.get('error') const errorDescription = params.get('error_description') - if (window.opener) { - if (code && state) { - window.opener.postMessage({ type: 'auth_code', code, state }, window.location.origin) - } else if (error) { - window.opener.postMessage( - { type: 'auth_error', error, errorDescription }, - window.location.origin - ) - } - window.close() + const channel = new BroadcastChannel('cipp_auth') + if (code && state) { + channel.postMessage({ type: 'auth_code', code, state }) + } else if (error) { + channel.postMessage({ type: 'auth_error', error, errorDescription }) } + channel.close() + window.close() }, []) return ( From 0a8252e3a2a954a734f58637179e7eed91dcb16c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 20:15:21 -0400 Subject: [PATCH 107/133] fix: version encoding --- src/components/CippSettings/CippVersionProperties.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippSettings/CippVersionProperties.jsx b/src/components/CippSettings/CippVersionProperties.jsx index 4b19e1d9e328..97542aa3ebbc 100644 --- a/src/components/CippSettings/CippVersionProperties.jsx +++ b/src/components/CippSettings/CippVersionProperties.jsx @@ -11,7 +11,7 @@ const CippVersionProperties = () => { }); const cippVersion = ApiGetCall({ - url: `/api/GetVersion?LocalVersion=${version?.data?.version}`, + url: `/api/GetVersion?LocalVersion=${encodeURIComponent(version?.data?.version ?? "")}`, queryKey: "CippVersion", waiting: false, }); From c8d61c0757853359cde98f9a00e6e3ba6fe6e98e Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 4 Jun 2026 23:13:18 -0400 Subject: [PATCH 108/133] fix: JIT admin, remove creatable on autocomplete --- .../identity/administration/jit-admin/add.jsx | 367 +++++++++--------- 1 file changed, 184 insertions(+), 183 deletions(-) diff --git a/src/pages/identity/administration/jit-admin/add.jsx b/src/pages/identity/administration/jit-admin/add.jsx index d99a1b6518c1..478f7ca51bfc 100644 --- a/src/pages/identity/administration/jit-admin/add.jsx +++ b/src/pages/identity/administration/jit-admin/add.jsx @@ -1,112 +1,111 @@ -import { Box, Divider } from "@mui/material"; -import { Grid } from "@mui/system"; -import CippFormPage from "../../../../components/CippFormPages/CippFormPage"; -import { Layout as DashboardLayout } from "../../../../layouts/index.js"; -import { CippFormTenantSelector } from "../../../../components/CippComponents/CippFormTenantSelector"; -import { useForm, useWatch } from "react-hook-form"; -import CippFormComponent from "../../../../components/CippComponents/CippFormComponent"; -import { CippFormCondition } from "../../../../components/CippComponents/CippFormCondition"; -import gdaproles from "../../../../data/GDAPRoles.json"; -import countryList from "../../../../data/countryList.json"; -import { CippFormDomainSelector } from "../../../../components/CippComponents/CippFormDomainSelector"; -import { CippFormUserSelector } from "../../../../components/CippComponents/CippFormUserSelector"; -import { CippFormGroupSelector } from "../../../../components/CippComponents/CippFormGroupSelector"; -import { ApiGetCall } from "../../../../api/ApiCall"; -import { useEffect, useState } from "react"; +import { Box, Divider } from '@mui/material' +import { Grid } from '@mui/system' +import CippFormPage from '../../../../components/CippFormPages/CippFormPage' +import { Layout as DashboardLayout } from '../../../../layouts/index.js' +import { CippFormTenantSelector } from '../../../../components/CippComponents/CippFormTenantSelector' +import { useForm, useWatch } from 'react-hook-form' +import CippFormComponent from '../../../../components/CippComponents/CippFormComponent' +import { CippFormCondition } from '../../../../components/CippComponents/CippFormCondition' +import gdaproles from '../../../../data/GDAPRoles.json' +import countryList from '../../../../data/countryList.json' +import { CippFormDomainSelector } from '../../../../components/CippComponents/CippFormDomainSelector' +import { CippFormUserSelector } from '../../../../components/CippComponents/CippFormUserSelector' +import { CippFormGroupSelector } from '../../../../components/CippComponents/CippFormGroupSelector' +import { ApiGetCall } from '../../../../api/ApiCall' +import { useEffect, useState } from 'react' const Page = () => { - const formControl = useForm({ mode: "onChange" }); - const selectedTenant = useWatch({ control: formControl.control, name: "tenantFilter" }); - const [selectedTemplate, setSelectedTemplate] = useState(null); + const formControl = useForm({ mode: 'onChange' }) + const selectedTenant = useWatch({ control: formControl.control, name: 'tenantFilter' }) + const [selectedTemplate, setSelectedTemplate] = useState(null) const jitAdminTemplates = ApiGetCall({ url: selectedTenant ? `/api/ListJITAdminTemplates?TenantFilter=${selectedTenant.value}` : undefined, - queryKey: selectedTenant ? `JITAdminTemplates-${selectedTenant.value}` : "JITAdminTemplates", + queryKey: selectedTenant ? `JITAdminTemplates-${selectedTenant.value}` : 'JITAdminTemplates', refetchOnMount: false, refetchOnReconnect: false, waiting: !!selectedTenant, - }); + }) - const watcher = useWatch({ control: formControl.control }); - const useTAP = useWatch({ control: formControl.control, name: "UseTAP" }); - const startDate = useWatch({ control: formControl.control, name: "startDate" }); - const endDate = useWatch({ control: formControl.control, name: "endDate" }); + const watcher = useWatch({ control: formControl.control }) + const useTAP = useWatch({ control: formControl.control, name: 'UseTAP' }) + const startDate = useWatch({ control: formControl.control, name: 'startDate' }) + const endDate = useWatch({ control: formControl.control, name: 'endDate' }) const tapLifetimeInMinutes = useWatch({ control: formControl.control, - name: "tapLifetimeInMinutes", - }); + name: 'tapLifetimeInMinutes', + }) const tapPolicy = ApiGetCall({ - url: selectedTenant - ? `/api/ListGraphRequest` - : undefined, + url: selectedTenant ? `/api/ListGraphRequest` : undefined, data: { - Endpoint: "policies/authenticationMethodsPolicy/authenticationMethodConfigurations/TemporaryAccessPass", + Endpoint: + 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/TemporaryAccessPass', tenantFilter: selectedTenant?.value, }, - queryKey: selectedTenant ? `TAPPolicy-${selectedTenant.value}` : "TAPPolicy", + queryKey: selectedTenant ? `TAPPolicy-${selectedTenant.value}` : 'TAPPolicy', waiting: !!selectedTenant, - }); - const tapEnabled = tapPolicy.isSuccess && tapPolicy.data?.Results?.[0]?.state === "enabled"; - const useRoles = useWatch({ control: formControl.control, name: "useRoles" }); - const useGroups = useWatch({ control: formControl.control, name: "useGroups" }); + }) + const tapEnabled = tapPolicy.isSuccess && tapPolicy.data?.Results?.[0]?.state === 'enabled' + const useRoles = useWatch({ control: formControl.control, name: 'useRoles' }) + const useGroups = useWatch({ control: formControl.control, name: 'useGroups' }) useEffect(() => { if (!useTAP || !startDate || !endDate) { - formControl.setValue("tapLifetimeInMinutes", null); - return; + formControl.setValue('tapLifetimeInMinutes', null) + return } - const requestedMinutes = Math.max(1, Math.round((endDate - startDate) / 60)); - const tapPolicyConfig = tapPolicy.data?.Results?.[0]; - const policyMax = tapPolicyConfig?.maximumLifetimeInMinutes ?? 1440; - const policyMin = Math.min(tapPolicyConfig?.minimumLifetimeInMinutes ?? 1, policyMax); + const requestedMinutes = Math.max(1, Math.round((endDate - startDate) / 60)) + const tapPolicyConfig = tapPolicy.data?.Results?.[0] + const policyMax = tapPolicyConfig?.maximumLifetimeInMinutes ?? 1440 + const policyMin = Math.min(tapPolicyConfig?.minimumLifetimeInMinutes ?? 1, policyMax) formControl.setValue( - "tapLifetimeInMinutes", + 'tapLifetimeInMinutes', Math.min(Math.max(requestedMinutes, policyMin), policyMax) - ); - }, [useTAP, startDate, endDate, tapPolicy.data, formControl]); + ) + }, [useTAP, startDate, endDate, tapPolicy.data, formControl]) // Clear fields when switches are toggled off useEffect(() => { if (!useRoles) { - formControl.setValue("adminRoles", []); + formControl.setValue('adminRoles', []) } - }, [useRoles]); + }, [useRoles]) useEffect(() => { if (!useGroups) { - formControl.setValue("groupMemberships", []); + formControl.setValue('groupMemberships', []) } - }, [useGroups]); + }, [useGroups]) // Reset expiration action when switches change useEffect(() => { - const currentAction = formControl.getValues("expireAction"); - if (!currentAction?.value) return; + const currentAction = formControl.getValues('expireAction') + if (!currentAction?.value) return - if (!useRoles && currentAction.value === "RemoveRoles") { - formControl.setValue("expireAction", null); - } else if (!useGroups && currentAction.value === "RemoveGroups") { - formControl.setValue("expireAction", null); - } else if ((!useRoles || !useGroups) && currentAction.value === "RemoveRolesAndGroups") { - formControl.setValue("expireAction", null); - } else if (useRoles && useGroups && currentAction.value === "RemoveRoles") { - formControl.setValue("expireAction", null); - } else if (useRoles && useGroups && currentAction.value === "RemoveGroups") { - formControl.setValue("expireAction", null); + if (!useRoles && currentAction.value === 'RemoveRoles') { + formControl.setValue('expireAction', null) + } else if (!useGroups && currentAction.value === 'RemoveGroups') { + formControl.setValue('expireAction', null) + } else if ((!useRoles || !useGroups) && currentAction.value === 'RemoveRolesAndGroups') { + formControl.setValue('expireAction', null) + } else if (useRoles && useGroups && currentAction.value === 'RemoveRoles') { + formControl.setValue('expireAction', null) + } else if (useRoles && useGroups && currentAction.value === 'RemoveGroups') { + formControl.setValue('expireAction', null) } - }, [useRoles, useGroups]); + }, [useRoles, useGroups]) // Simple duration parser for basic ISO 8601 durations const parseDuration = (duration) => { - if (!duration) return null; + if (!duration) return null const matches = duration.match( /P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?/ - ); - if (!matches) return null; + ) + if (!matches) return null return { years: parseInt(matches[1] || 0), months: parseInt(matches[2] || 0), @@ -115,159 +114,159 @@ const Page = () => { hours: parseInt(matches[5] || 0), minutes: parseInt(matches[6] || 0), seconds: parseInt(matches[7] || 0), - }; - }; + } + } const addDurationToDate = (date, duration) => { - if (!date || !duration) return null; - const parsed = parseDuration(duration); - if (!parsed) return null; + if (!date || !duration) return null + const parsed = parseDuration(duration) + if (!parsed) return null - const result = new Date(date); - result.setFullYear(result.getFullYear() + parsed.years); - result.setMonth(result.getMonth() + parsed.months); - result.setDate(result.getDate() + parsed.weeks * 7); - result.setDate(result.getDate() + parsed.days); - result.setHours(result.getHours() + parsed.hours); - result.setMinutes(result.getMinutes() + parsed.minutes); - result.setSeconds(result.getSeconds() + parsed.seconds); - return result; - }; + const result = new Date(date) + result.setFullYear(result.getFullYear() + parsed.years) + result.setMonth(result.getMonth() + parsed.months) + result.setDate(result.getDate() + parsed.weeks * 7) + result.setDate(result.getDate() + parsed.days) + result.setHours(result.getHours() + parsed.hours) + result.setMinutes(result.getMinutes() + parsed.minutes) + result.setSeconds(result.getSeconds() + parsed.seconds) + return result + } // Auto-select default template for tenant // Priority: tenant-specific default > AllTenants default useEffect(() => { if (jitAdminTemplates.isSuccess && !watcher.jitAdminTemplate) { - const templates = jitAdminTemplates.data || []; + const templates = jitAdminTemplates.data || [] // First, try to find a tenant-specific default template let defaultTemplate = templates.find( (template) => template.defaultForTenant === true && - template.tenantFilter !== "AllTenants" && + template.tenantFilter !== 'AllTenants' && template.tenantFilter === selectedTenant?.value - ); + ) // If not found, fall back to AllTenants default template if (!defaultTemplate) { defaultTemplate = templates.find( - (template) => template.defaultForTenant === true && template.tenantFilter === "AllTenants" - ); + (template) => template.defaultForTenant === true && template.tenantFilter === 'AllTenants' + ) } if (defaultTemplate) { - formControl.setValue("jitAdminTemplate", { + formControl.setValue('jitAdminTemplate', { label: defaultTemplate.templateName, value: defaultTemplate.GUID, addedFields: defaultTemplate, - }); - setSelectedTemplate(defaultTemplate); + }) + setSelectedTemplate(defaultTemplate) } } - }, [jitAdminTemplates.isSuccess, selectedTenant]); + }, [jitAdminTemplates.isSuccess, selectedTenant]) // Only set template-driven fields when the template actually changes - const [lastTemplate, setLastTemplate] = useState(null); + const [lastTemplate, setLastTemplate] = useState(null) useEffect(() => { - const template = watcher.jitAdminTemplate?.addedFields; - if (!template || template.GUID === lastTemplate) return; - setSelectedTemplate(template); - setLastTemplate(template.GUID); + const template = watcher.jitAdminTemplate?.addedFields + if (!template || template.GUID === lastTemplate) return + setSelectedTemplate(template) + setLastTemplate(template.GUID) // Helpers const roundDown15 = (date) => { - const d = new Date(date); - d.setMilliseconds(0); - d.setSeconds(0); - d.setMinutes(Math.floor(d.getMinutes() / 15) * 15); - return d; - }; + const d = new Date(date) + d.setMilliseconds(0) + d.setSeconds(0) + d.setMinutes(Math.floor(d.getMinutes() / 15) * 15) + return d + } const roundUp15 = (date) => { - const d = new Date(date); - d.setMilliseconds(0); - d.setSeconds(0); - let min = d.getMinutes(); - d.setMinutes(min % 15 === 0 ? min : Math.ceil(min / 15) * 15); + const d = new Date(date) + d.setMilliseconds(0) + d.setSeconds(0) + let min = d.getMinutes() + d.setMinutes(min % 15 === 0 ? min : Math.ceil(min / 15) * 15) if (d.getMinutes() === 60) { - d.setHours(d.getHours() + 1); - d.setMinutes(0); + d.setHours(d.getHours() + 1) + d.setMinutes(0) } - return d; - }; + return d + } // Set all template-driven fields - formControl.setValue("useRoles", template.defaultUseRoles ?? true, { shouldDirty: true }); - formControl.setValue("useGroups", template.defaultUseGroups ?? false, { shouldDirty: true }); - formControl.setValue("adminRoles", template.defaultRoles || [], { shouldDirty: true }); - formControl.setValue("groupMemberships", template.defaultGroups || [], { shouldDirty: true }); - formControl.setValue("expireAction", template.defaultExpireAction || null, { + formControl.setValue('useRoles', template.defaultUseRoles ?? true, { shouldDirty: true }) + formControl.setValue('useGroups', template.defaultUseGroups ?? false, { shouldDirty: true }) + formControl.setValue('adminRoles', template.defaultRoles || [], { shouldDirty: true }) + formControl.setValue('groupMemberships', template.defaultGroups || [], { shouldDirty: true }) + formControl.setValue('expireAction', template.defaultExpireAction || null, { shouldDirty: true, - }); - formControl.setValue("postExecution", template.defaultNotificationActions || [], { + }) + formControl.setValue('postExecution', template.defaultNotificationActions || [], { shouldDirty: true, - }); - formControl.setValue("UseTAP", template.generateTAPByDefault ?? false, { shouldDirty: true }); - formControl.setValue("reason", template.reasonTemplate || "", { shouldDirty: true }); + }) + formControl.setValue('UseTAP', template.generateTAPByDefault ?? false, { shouldDirty: true }) + formControl.setValue('reason', template.reasonTemplate || '', { shouldDirty: true }) // User action and user details if (template.defaultUserAction) { - formControl.setValue("userAction", template.defaultUserAction, { shouldDirty: true }); + formControl.setValue('userAction', template.defaultUserAction, { shouldDirty: true }) } if (template.defaultFirstName) { - formControl.setValue("firstName", template.defaultFirstName, { shouldDirty: true }); + formControl.setValue('firstName', template.defaultFirstName, { shouldDirty: true }) } if (template.defaultLastName) { - formControl.setValue("lastName", template.defaultLastName, { shouldDirty: true }); + formControl.setValue('lastName', template.defaultLastName, { shouldDirty: true }) } if (template.defaultUserName) { - formControl.setValue("userName", template.defaultUserName, { shouldDirty: true }); + formControl.setValue('userName', template.defaultUserName, { shouldDirty: true }) } if (template.defaultDomain) { - formControl.setValue("domain", template.defaultDomain, { shouldDirty: true }); + formControl.setValue('domain', template.defaultDomain, { shouldDirty: true }) } if (template.defaultExistingUser) { - formControl.setValue("existingUser", template.defaultExistingUser, { shouldDirty: true }); + formControl.setValue('existingUser', template.defaultExistingUser, { shouldDirty: true }) } if (template.defaultUsageLocation) { - formControl.setValue("usageLocation", template.defaultUsageLocation, { shouldDirty: true }); + formControl.setValue('usageLocation', template.defaultUsageLocation, { shouldDirty: true }) } // Dates if (template.defaultDuration) { const duration = - typeof template.defaultDuration === "object" && template.defaultDuration !== null + typeof template.defaultDuration === 'object' && template.defaultDuration !== null ? template.defaultDuration.value - : template.defaultDuration; - const start = roundDown15(new Date()); - const unixStart = Math.floor(start.getTime() / 1000); - formControl.setValue("startDate", unixStart, { shouldDirty: true }); - const end = roundUp15(addDurationToDate(start, duration)); - const unixEnd = Math.floor(end.getTime() / 1000); - formControl.setValue("endDate", unixEnd, { shouldDirty: true }); + : template.defaultDuration + const start = roundDown15(new Date()) + const unixStart = Math.floor(start.getTime() / 1000) + formControl.setValue('startDate', unixStart, { shouldDirty: true }) + const end = roundUp15(addDurationToDate(start, duration)) + const unixEnd = Math.floor(end.getTime() / 1000) + formControl.setValue('endDate', unixEnd, { shouldDirty: true }) } - }, [watcher.jitAdminTemplate, lastTemplate]); + }, [watcher.jitAdminTemplate, lastTemplate]) // Recalculate end date when start date changes and template has default duration useEffect(() => { if (watcher.startDate && selectedTemplate?.defaultDuration) { const durationValue = - typeof selectedTemplate.defaultDuration === "object" && + typeof selectedTemplate.defaultDuration === 'object' && selectedTemplate.defaultDuration !== null ? selectedTemplate.defaultDuration.value - : selectedTemplate.defaultDuration; - const startDateDate = new Date(watcher.startDate * 1000); - const endDateObj = addDurationToDate(startDateDate, durationValue); + : selectedTemplate.defaultDuration + const startDateDate = new Date(watcher.startDate * 1000) + const endDateObj = addDurationToDate(startDateDate, durationValue) if (endDateObj) { - const unixEnd = Math.floor(endDateObj.getTime() / 1000); - formControl.setValue("endDate", unixEnd); + const unixEnd = Math.floor(endDateObj.getTime() / 1000) + formControl.setValue('endDate', unixEnd) } } - }, [watcher.startDate]); + }, [watcher.startDate]) return ( <> { @@ -313,11 +312,11 @@ const Page = () => { row formControl={formControl} options={[ - { label: "New User", value: "create" }, - { label: "Existing User", value: "select" }, + { label: 'New User', value: 'create' }, + { label: 'Existing User', value: 'select' }, ]} required={true} - validators={{ required: "You must select an option" }} + validators={{ required: 'You must select an option' }} /> @@ -335,7 +334,7 @@ const Page = () => { name="firstName" formControl={formControl} required={true} - validators={{ required: "First Name is required" }} + validators={{ required: 'First Name is required' }} /> @@ -346,7 +345,7 @@ const Page = () => { name="lastName" formControl={formControl} required={true} - validators={{ required: "Last Name is required" }} + validators={{ required: 'Last Name is required' }} /> @@ -357,7 +356,7 @@ const Page = () => { name="userName" formControl={formControl} required={true} - validators={{ required: "Username is required" }} + validators={{ required: 'Username is required' }} /> @@ -366,7 +365,7 @@ const Page = () => { name="domain" label="Domain Name" required={true} - validators={{ required: "Domain is required" }} + validators={{ required: 'Domain is required' }} /> @@ -380,6 +379,7 @@ const Page = () => { value: Code, }))} formControl={formControl} + creatable={false} /> @@ -400,7 +400,7 @@ const Page = () => { name="existingUser" label="User" required={true} - validators={{ required: "User is required" }} + validators={{ required: 'User is required' }} /> @@ -413,7 +413,7 @@ const Page = () => { name="startDate" formControl={formControl} required={true} - validators={{ required: "Start date is required" }} + validators={{ required: 'Start date is required' }} /> @@ -425,13 +425,13 @@ const Page = () => { formControl={formControl} required={true} validators={{ - required: "End date is required", + required: 'End date is required', validate: (value) => { - const startDate = formControl.getValues("startDate"); + const startDate = formControl.getValues('startDate') if (value && startDate && new Date(value) < new Date(startDate)) { - return "End date must be after start date"; + return 'End date must be after start date' } - return true; + return true }, }} /> @@ -452,7 +452,7 @@ const Page = () => { {!useRoles && !useGroups && ( - + Please select at least "Admin Roles" or "Group Membership" @@ -462,7 +462,7 @@ const Page = () => { field="useRoles" compareType="is" compareValue={true} - > + > { formControl={formControl} required={true} validators={{ - required: "At least one role is required", + required: 'At least one role is required', validate: (options) => { if (!options?.length) { - return "At least one role is required"; + return 'At least one role is required' } - return true; + return true }, }} + creatable={false} /> @@ -498,12 +499,12 @@ const Page = () => { multiple={true} required={true} validators={{ - required: "At least one group is required", + required: 'At least one group is required', validate: (options) => { if (!options?.length) { - return "At least one group is required"; + return 'At least one group is required' } - return true; + return true }, }} /> @@ -519,7 +520,7 @@ const Page = () => { rows={3} formControl={formControl} required={true} - validators={{ required: "A reason is required" }} + validators={{ required: 'A reason is required' }} /> @@ -535,12 +536,12 @@ const Page = () => { formControl={formControl} /> {useTAP && tapPolicy.isSuccess && !tapEnabled && ( - + TAP is not enabled in this tenant. TAP generation will fail. )} {useTAP && tapLifetimeInMinutes && ( - + TAP will be valid for {tapLifetimeInMinutes} minutes. )} @@ -556,20 +557,20 @@ const Page = () => { required={true} options={(() => { const opts = [ - { label: "Delete User", value: "DeleteUser" }, - { label: "Disable User", value: "DisableUser" }, - ]; + { label: 'Delete User', value: 'DeleteUser' }, + { label: 'Disable User', value: 'DisableUser' }, + ] if (useRoles && useGroups) { - opts.push({ label: "Remove Roles and Groups", value: "RemoveRolesAndGroups" }); + opts.push({ label: 'Remove Roles and Groups', value: 'RemoveRolesAndGroups' }) } else if (useRoles) { - opts.push({ label: "Remove Roles", value: "RemoveRoles" }); + opts.push({ label: 'Remove Roles', value: 'RemoveRoles' }) } else if (useGroups) { - opts.push({ label: "Remove Groups", value: "RemoveGroups" }); + opts.push({ label: 'Remove Groups', value: 'RemoveGroups' }) } - return opts; + return opts })()} formControl={formControl} - validators={{ required: "Expiration action is required" }} + validators={{ required: 'Expiration action is required' }} /> @@ -581,9 +582,9 @@ const Page = () => { multiple={true} creatable={false} options={[ - { label: "Webhook", value: "Webhook" }, - { label: "Email", value: "email" }, - { label: "PSA", value: "PSA" }, + { label: 'Webhook', value: 'Webhook' }, + { label: 'Email', value: 'email' }, + { label: 'PSA', value: 'PSA' }, ]} formControl={formControl} /> @@ -592,9 +593,9 @@ const Page = () => { - ); -}; + ) +} -Page.getLayout = (page) => {page}; +Page.getLayout = (page) => {page} -export default Page; +export default Page From 98a96fcbcf72717d266c53eeaca45ab4a074b28a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:17:15 +0200 Subject: [PATCH 109/133] CA expansion for tags --- src/pages/tenant/manage/applied-standards.js | 4 ++-- src/pages/tenant/manage/policies-deployed.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index d69b3aebc81c..234e5cb46b23 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -163,7 +163,7 @@ const Page = () => { templateItem['TemplateList-Tags']?.addedFields?.templates || templateItem['TemplateList-Tags']?.rawData?.templates - if (templateItem['TemplateList-Tags']?.value && tagTemplates) { + if (templateItem['TemplateList-Tags']?.value && tagTemplates?.length > 0) { tagTemplates.forEach((expandedTemplate) => { const itemTemplateId = expandedTemplate.GUID const standardId = `standards.IntuneTemplate.${itemTemplateId}` @@ -430,7 +430,7 @@ const Page = () => { templateItem['TemplateList-Tags']?.rawData?.templates // Check if this item has TemplateList-Tags and expand them - if (templateItem['TemplateList-Tags']?.value && tagTemplates) { + if (templateItem['TemplateList-Tags']?.value && tagTemplates?.length > 0) { tagTemplates.forEach((expandedTemplate) => { const itemTemplateId = expandedTemplate.GUID const standardId = `standards.ConditionalAccessTemplate.${itemTemplateId}` diff --git a/src/pages/tenant/manage/policies-deployed.js b/src/pages/tenant/manage/policies-deployed.js index 616f5a64491f..3b24143cee55 100644 --- a/src/pages/tenant/manage/policies-deployed.js +++ b/src/pages/tenant/manage/policies-deployed.js @@ -255,7 +255,7 @@ const PoliciesDeployedPage = () => { const templateListTags = template['TemplateList-Tags'] || template.TemplateListTags // Check if this template has TemplateList-Tags and expand them - if (templateListTags?.value && templateListTags?.addedFields?.templates) { + if (templateListTags?.value && templateListTags?.addedFields?.templates?.length > 0) { console.log( 'Found TemplateList-Tags for IntuneTemplate in policies-deployed:', templateListTags @@ -359,7 +359,7 @@ const PoliciesDeployedPage = () => { const templateListTags = template['TemplateList-Tags'] || template.TemplateListTags // Check if this template has TemplateList-Tags and expand them - if (templateListTags?.value && templateListTags?.addedFields?.templates) { + if (templateListTags?.value && templateListTags?.addedFields?.templates?.length > 0) { console.log( 'Found TemplateList-Tags for ConditionalAccessTemplate in policies-deployed:', templateListTags From a4aac4a128a847a5af059c4edc73f66af7014a60 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:17:18 +0200 Subject: [PATCH 110/133] CA expansion for tags --- src/pages/tenant/manage/applied-standards.js | 1191 +++++++++--------- 1 file changed, 614 insertions(+), 577 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 234e5cb46b23..6a8c0d5bea3f 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -1991,112 +1991,114 @@ const Page = () => { : 'This tenant does not have the required licenses for this standard'} ) : ( - <> - {/* Show Expected Configuration with property-by-property breakdown */} - {standard.currentTenantValue?.ExpectedValue !== undefined ? ( - - - Expected Configuration - - {typeof standard.currentTenantValue.ExpectedValue === 'object' && - standard.currentTenantValue.ExpectedValue !== null ? ( - - {Object.entries(standard.currentTenantValue.ExpectedValue).map( - ([key, val]) => ( - - - {key} - - + <> + {/* Show Expected Configuration with property-by-property breakdown */} + {standard.currentTenantValue?.ExpectedValue !== undefined ? ( + + + Expected Configuration + + {typeof standard.currentTenantValue.ExpectedValue === + 'object' && + standard.currentTenantValue.ExpectedValue !== null ? ( + + {Object.entries( + standard.currentTenantValue.ExpectedValue + ).map(([key, val]) => ( + - {val !== undefined - ? JSON.stringify(val, null, 2) - : 'Not set'} + {key} + + + {val !== undefined + ? JSON.stringify(val, null, 2) + : 'Not set'} + + - - ) + ))} + + ) : ( + + + {String(standard.currentTenantValue.ExpectedValue)} + + )} - - ) : ( - - - {String(standard.currentTenantValue.ExpectedValue)} - + ) : ( + + This data has not yet been collected. Collect the data by + selecting Refresh Data from the Actions dropdown on the top of + the page. + )} - - ) : ( - - This data has not yet been collected. Collect the data by selecting - Refresh Data from the Actions dropdown on the top of the page. - - )} - - - - + + + + )} @@ -2203,35 +2205,496 @@ const Page = () => { : 'This tenant does not have the required licenses for this standard'} ) : ( - <> - {/* Existing tenant comparison content */} - {typeof standard.currentTenantValue?.Value === 'object' && - standard.currentTenantValue?.Value !== null ? ( - - {standard.complianceStatus === 'Reporting Disabled' ? ( - - Reporting is disabled for this standard in the template - configuration. - + <> + {/* Existing tenant comparison content */} + {typeof standard.currentTenantValue?.Value === 'object' && + standard.currentTenantValue?.Value !== null ? ( + + {standard.complianceStatus === 'Reporting Disabled' ? ( + + Reporting is disabled for this standard in the template + configuration. + + ) : ( + <> + {standard.complianceStatus === 'Overridden' ? ( + + This setting is configured by template:{' '} + {standard.overridingTemplateName || + standard.overridingTemplateId} + + ) : standard.complianceStatus === 'Compliant' ? ( + <> + {/* 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]) => ( + + + {key} + + + + + + + {val !== undefined + ? JSON.stringify(val, null, 2) + : 'Not set'} + + + + ))} + + ) : ( + + + Current Configuration + + + + {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]) => { + // Compare with expected value for this property + const expectedVal = + standard.currentTenantValue?.ExpectedValue?.[ + key + ] + const isMatch = (() => { + if (expectedVal === undefined) return false + // Deep comparison handling nested objects and case-insensitive strings + const compareDeep = (v1, v2) => { + if ( + typeof v1 === 'string' && + typeof v2 === 'string' + ) { + return ( + v1.toLowerCase() === v2.toLowerCase() + ) + } + if ( + typeof v1 === 'object' && + v1 !== null && + typeof v2 === 'object' && + v2 !== null + ) { + return ( + JSON.stringify(v1) === + JSON.stringify(v2) + ) + } + return ( + JSON.stringify(v1) === JSON.stringify(v2) + ) + } + return compareDeep(val, expectedVal) + })() + + return ( + + + {key} + + + {isMatch && ( + + + + )} + + {val !== undefined + ? JSON.stringify(val, null, 2) + : 'Not set'} + + + + ) + })} + + ) : ( + + + Current Configuration + + + + {String( + standard.currentTenantValue.CurrentValue + )} + + + + ))} + + )} + + {/* Only show values if they're not simple true/false that's already covered by the alerts above */} + {!( + standard.complianceStatus === 'Compliant' && + (standard.currentTenantValue?.Value === true || + standard.currentTenantValue?.Value === false) + ) && + Object.entries(standard.currentTenantValue) + .filter( + ([key]) => + key !== 'LastRefresh' && + key !== 'CurrentValue' && + key !== 'ExpectedValue' && + // Skip showing the Value field separately if it's just true/false + !( + key === 'Value' && + (standard.currentTenantValue?.Value === true || + standard.currentTenantValue?.Value === false) + ) + ) + .map(([key, value]) => { + const actualValue = key === 'Value' ? value : value + + const standardValueForKey = + standard.standardValue && + typeof standard.standardValue === 'object' + ? standard.standardValue[key] + : undefined + + const isDifferent = + standardValueForKey !== undefined && + JSON.stringify(actualValue) !== + JSON.stringify(standardValueForKey) + + // Format the display value + let displayValue + if (typeof value === 'object' && value !== null) { + displayValue = + value?.label || JSON.stringify(value, null, 2) + } else if (value === true) { + displayValue = 'Enabled' + } else if (value === false) { + displayValue = 'Disabled' + } else { + displayValue = String(value) + } + + return ( + + + {key}: + + + {displayValue} + + + ) + })} + + )} + ) : ( - <> - {standard.complianceStatus === 'Overridden' ? ( - + + {standard.complianceStatus === 'Reporting Disabled' ? ( + + Reporting is disabled for this standard in the template + configuration. + + ) : standard.complianceStatus === 'Overridden' ? ( + This setting is configured by template:{' '} {standard.overridingTemplateName || standard.overridingTemplateId} ) : standard.complianceStatus === 'Compliant' ? ( <> - {/* Show Current value property-by-property for compliant standards */} + {/* Show Current value property-by-property in card view */} {standard.currentTenantValue?.CurrentValue !== undefined ? ( typeof standard.currentTenantValue.CurrentValue === 'object' && @@ -2351,25 +2814,18 @@ const Page = () => { ) : ( <> - {standard.currentTenantValue?.Value === false && ( - + {(standard.currentTenantValue?.Value === false || + standard.currentTenantValue === 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 === + {/* 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 ? ( - + { })} ) : ( - + { - ))} - - )} - - {/* Only show values if they're not simple true/false that's already covered by the alerts above */} - {!( - standard.complianceStatus === 'Compliant' && - (standard.currentTenantValue?.Value === true || - standard.currentTenantValue?.Value === false) - ) && - Object.entries(standard.currentTenantValue) - .filter( - ([key]) => - key !== 'LastRefresh' && - key !== 'CurrentValue' && - key !== 'ExpectedValue' && - // Skip showing the Value field separately if it's just true/false - !( - key === 'Value' && - (standard.currentTenantValue?.Value === true || - standard.currentTenantValue?.Value === false) - ) - ) - .map(([key, value]) => { - const actualValue = key === 'Value' ? value : value - - const standardValueForKey = - standard.standardValue && - typeof standard.standardValue === 'object' - ? standard.standardValue[key] - : undefined - - const isDifferent = - standardValueForKey !== undefined && - JSON.stringify(actualValue) !== - JSON.stringify(standardValueForKey) - - // Format the display value - let displayValue - if (typeof value === 'object' && value !== null) { - displayValue = - value?.label || JSON.stringify(value, null, 2) - } else if (value === true) { - displayValue = 'Enabled' - } else if (value === false) { - displayValue = 'Disabled' - } else { - displayValue = String(value) - } - - return ( - - - {key}: - - - {displayValue} - - ) - })} - - )} - - ) : ( - - {standard.complianceStatus === 'Reporting Disabled' ? ( - - Reporting is disabled for this standard in the template - configuration. - - ) : standard.complianceStatus === 'Overridden' ? ( - - This setting is configured by template:{' '} - {standard.overridingTemplateName || - standard.overridingTemplateId} - - ) : standard.complianceStatus === 'Compliant' ? ( - <> - {/* 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)} - + ) : standard.currentTenantValue !== undefined && + standard.currentTenantValue?.Value !== true && + standard.currentTenantValue?.Value !== false ? ( + + {String( + standard.currentTenantValue?.Value !== undefined + ? standard.currentTenantValue?.Value + : standard.currentTenantValue + )} - - ) - ) : null} - - ) : ( - <> - {(standard.currentTenantValue?.Value === false || - standard.currentTenantValue === false) && ( - - This setting is not configured correctly - + ) : 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} + )} - {/* 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]) => { - // Compare with expected value for this property - const expectedVal = - standard.currentTenantValue?.ExpectedValue?.[key] - const isMatch = (() => { - if (expectedVal === undefined) return false - // Deep comparison handling nested objects and case-insensitive strings - const compareDeep = (v1, v2) => { - if ( - typeof v1 === 'string' && - typeof v2 === 'string' - ) { - return v1.toLowerCase() === v2.toLowerCase() - } - if ( - typeof v1 === 'object' && - v1 !== null && - typeof v2 === 'object' && - v2 !== null - ) { - return JSON.stringify(v1) === JSON.stringify(v2) - } - return JSON.stringify(v1) === JSON.stringify(v2) - } - return compareDeep(val, expectedVal) - })() - - return ( - - - {key} - - - {isMatch && ( - - - - )} - - {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 7b0c86990e1e5765992c31ad0650d1c63fcf7917 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:11:18 +0800 Subject: [PATCH 111/133] Update SsoMigrationDialog.jsx --- src/components/CippComponents/SsoMigrationDialog.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CippComponents/SsoMigrationDialog.jsx b/src/components/CippComponents/SsoMigrationDialog.jsx index 071b8032e8ab..26ce87aa338d 100644 --- a/src/components/CippComponents/SsoMigrationDialog.jsx +++ b/src/components/CippComponents/SsoMigrationDialog.jsx @@ -75,7 +75,7 @@ export const SsoMigrationDialog = ({ meData }) => { To get ready, CIPP needs to create an app registration in your tenant called{' '} - CIPP-SSO with minimal permissions (OpenID, Profile, Email only). + CIPP-SSO with minimal permissions (OpenID, Profile, Email only). This won't change how you log in today — it just prepares your tenant for when the update rolls out. From 8ae6ad1fae73702c180047ba374b491542dc0b4d Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Fri, 5 Jun 2026 22:19:13 +0800 Subject: [PATCH 112/133] typo --- src/components/CippComponents/ForcedSsoMigrationDialog.jsx | 2 +- src/components/CippComponents/SsoMigrationDialog.jsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx index abc303fe1797..7326ae56df0c 100644 --- a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx +++ b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx @@ -61,7 +61,7 @@ export const ForcedSsoMigrationDialog = () => { {!submitted ? ( <> - Your CIPP instance requires a dedicated CIPP-SSO app registration in + Your CIPP instance requires a dedicated CIPP-SSO app registration in your tenant for authentication. This gives you full control over Conditional Access policies, MFA requirements, and session management for your CIPP users. diff --git a/src/components/CippComponents/SsoMigrationDialog.jsx b/src/components/CippComponents/SsoMigrationDialog.jsx index 26ce87aa338d..03026daec36a 100644 --- a/src/components/CippComponents/SsoMigrationDialog.jsx +++ b/src/components/CippComponents/SsoMigrationDialog.jsx @@ -74,8 +74,8 @@ export const SsoMigrationDialog = ({ meData }) => { CIPP users. - To get ready, CIPP needs to create an app registration in your tenant called{' '} - CIPP-SSO with minimal permissions (OpenID, Profile, Email only). + To get ready, CIPP needs to create an app registration in your tenant called + CIPP-SSO with minimal permissions (OpenID, Profile, Email only). This won't change how you log in today — it just prepares your tenant for when the update rolls out. From c15d1d0df12c5aef9bd1d301a255ef5224c05f7d Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jun 2026 10:41:56 -0400 Subject: [PATCH 113/133] fix: sherweb integration conditional fields --- src/data/Extensions.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/Extensions.json b/src/data/Extensions.json index 7bb91b6165f9..83872799e810 100644 --- a/src/data/Extensions.json +++ b/src/data/Extensions.json @@ -144,7 +144,7 @@ ], "multiple": false, "condition": { - "field": "Sherweb.migrationMethods", + "field": "Sherweb.migrationMethods.value", "compareType": "is", "compareValue": "buyAndCancel" } @@ -181,7 +181,7 @@ "placeholder": "Enter your Pax8 Client ID", "required": true, "condition": { - "field": "Sherweb.migrateFrom", + "field": "Sherweb.migrateFrom.value", "compareType": "is", "compareValue": "Pax8" } @@ -193,7 +193,7 @@ "placeholder": "Enter your Pax Client Secret", "required": true, "condition": { - "field": "Sherweb.migrateFrom", + "field": "Sherweb.migrateFrom.value", "compareType": "is", "compareValue": "Pax8" } From 6c968c4bd3eb95439caf2c669e63bc7f1dd2fdd7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jun 2026 14:36:10 -0400 Subject: [PATCH 114/133] fix: bad math --- src/pages/tenant/manage/applied-standards.js | 81 +++++++++++--------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/src/pages/tenant/manage/applied-standards.js b/src/pages/tenant/manage/applied-standards.js index 6a8c0d5bea3f..acd6a06a5061 100644 --- a/src/pages/tenant/manage/applied-standards.js +++ b/src/pages/tenant/manage/applied-standards.js @@ -1286,6 +1286,14 @@ const Page = () => { const filteredGroupedStandards = useMemo(() => { if (!groupedStandards) return {} + const isLicenseMissingStandard = (standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue + return ( + standard.currentTenantValue?.LicenseAvailable === false || + (typeof tenantValue === 'string' && tenantValue.startsWith('License Missing:')) + ) + } + if (!searchQuery && filter === 'all') { return groupedStandards } @@ -1297,9 +1305,7 @@ const Page = () => { const categoryMatchesSearch = !searchQuery || category.toLowerCase().includes(searchLower) const filteredStandards = groupedStandards[category].filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue - const hasLicenseMissing = - typeof tenantValue === 'string' && tenantValue.startsWith('License Missing:') + const hasLicenseMissing = isLicenseMissingStandard(standard) const matchesFilter = filter === 'all' || @@ -1312,9 +1318,7 @@ const Page = () => { (filter === 'nonCompliantWithLicense' && standard.complianceStatus === 'Non-Compliant' && !hasLicenseMissing) || - (filter === 'nonCompliantWithoutLicense' && - standard.complianceStatus === 'Non-Compliant' && - hasLicenseMissing) + (filter === 'nonCompliantWithoutLicense' && hasLicenseMissing) const matchesSearch = !searchQuery || @@ -1350,52 +1354,59 @@ const Page = () => { const overriddenCount = comparisonData?.filter((standard) => standard.complianceStatus === 'Overridden').length || 0 + const isIncludedInScoring = (standard) => + standard.complianceStatus !== 'Reporting Disabled' && standard.complianceStatus !== 'Overridden' + + const isLicenseMissingStandard = (standard) => { + const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue + return ( + standard.currentTenantValue?.LicenseAvailable === false || + (typeof tenantValue === 'string' && tenantValue.startsWith('License Missing:')) + ) + } + // Calculate license-related metrics const missingLicenseCount = - comparisonData?.filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue - return typeof tenantValue === 'string' && tenantValue.startsWith('License Missing:') - }).length || 0 + comparisonData?.filter( + (standard) => isIncludedInScoring(standard) && isLicenseMissingStandard(standard) + ).length || 0 const nonCompliantWithLicenseCount = comparisonData?.filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue - return ( - standard.complianceStatus === 'Non-Compliant' && - !(typeof tenantValue === 'string' && tenantValue.startsWith('License Missing:')) - ) + return standard.complianceStatus === 'Non-Compliant' && !isLicenseMissingStandard(standard) }).length || 0 const nonCompliantWithoutLicenseCount = - comparisonData?.filter((standard) => { - const tenantValue = standard.currentTenantValue?.Value || standard.currentTenantValue - return ( - standard.complianceStatus === 'Non-Compliant' && - typeof tenantValue === 'string' && - tenantValue.startsWith('License Missing:') - ) - }).length || 0 + comparisonData?.filter((standard) => isLicenseMissingStandard(standard)).length || 0 + + const compliantWithAvailableLicenseCount = + comparisonData?.filter( + (standard) => + isIncludedInScoring(standard) && + (standard.complianceStatus === 'Compliant' || + standard.complianceStatus === 'Accepted Deviation' || + standard.complianceStatus === 'Customer Specific') && + !isLicenseMissingStandard(standard) + ).length || 0 + + const scoredStandardsCount = Math.max(allCount - reportingDisabledCount - overriddenCount, 0) const compliancePercentage = - allCount > 0 - ? Math.round( - ((compliantCount + acceptedDeviationCount) / - (allCount - reportingDisabledCount - overriddenCount || 1)) * - 100 - ) + scoredStandardsCount > 0 + ? Math.round((compliantWithAvailableLicenseCount / scoredStandardsCount) * 100) : 0 const missingLicensePercentage = - allCount > 0 + scoredStandardsCount > 0 ? Math.round((missingLicenseCount / scoredStandardsCount) * 100) : 0 + + // Combined score: standards either compliant with available licensing or blocked by missing license. + const combinedScore = + scoredStandardsCount > 0 ? Math.round( - (missingLicenseCount / (allCount - reportingDisabledCount - overriddenCount || 1)) * 100 + ((compliantWithAvailableLicenseCount + missingLicenseCount) / scoredStandardsCount) * 100 ) : 0 - // Combined score: compliance percentage + missing license percentage - // This represents the total "addressable" compliance (compliant + could be compliant if licensed) - const combinedScore = compliancePercentage + missingLicensePercentage - // Simple filter for all templates (no type filtering) const templateOptions = templates ? templates.map((template) => ({ From e90b0ff971de99d70d5ee1e10a98c183dc1f97e7 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Sat, 6 Jun 2026 00:46:30 +0200 Subject: [PATCH 115/133] renumber for cis7 --- src/data/standards.json | 212 ++++++++++++++++++++-------------------- 1 file changed, 107 insertions(+), 105 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index fc2b40fc34ec..c1e87f49e615 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -126,7 +126,7 @@ { "name": "standards.AuditLog", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], + "tag": ["CIS M365 7.0.0 (3.1.1)", "mip_search_auditlog", "NIST CSF 2.0 (DE.CM-09)"], "appliesToTest": ["CISAMSEXO171", "CISAMSEXO173", "CIS_3_1_1"], "helpText": "Enables the Unified Audit Log for tracking and auditing activities. Also runs Enable-OrganizationCustomization if necessary.", "executiveText": "Activates comprehensive activity logging across Microsoft 365 services to track user actions, system changes, and security events. This provides essential audit trails for compliance requirements, security investigations, and regulatory reporting.", @@ -148,7 +148,7 @@ { "name": "standards.RestrictThirdPartyStorageServices", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (1.3.7)"], + "tag": ["CIS M365 7.0.0 (1.3.7)"], "appliesToTest": ["CIS_1_3_7"], "helpText": "Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers.", "docsDescription": "Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so.", @@ -267,7 +267,7 @@ { "name": "standards.EnableCustomerLockbox", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (1.3.6)", "CustomerLockBoxEnabled"], + "tag": ["CIS M365 7.0.0 (1.3.6)", "CustomerLockBoxEnabled"], "appliesToTest": ["CIS_1_3_6"], "helpText": "**Requires Entra ID P2.** Enables Customer Lockbox that offers an approval process for Microsoft support to access organization data", "docsDescription": "**Requires Entra ID P2.** Customer Lockbox ensures that Microsoft can't access your content to do service operations without your explicit approval. Customer Lockbox ensures only authorized requests allow access to your organizations data.", @@ -328,7 +328,7 @@ "name": "standards.DisableGuestDirectory", "cat": "Global Standards", "tag": [ - "CIS M365 6.0.1 (5.1.6.2)", + "CIS M365 7.0.0 (5.1.6.2)", "CISA (MS.AAD.5.1v1)", "EIDSCA.AP14", "EIDSCA.ST08", @@ -359,7 +359,7 @@ { "name": "standards.DisableBasicAuthSMTP", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], + "tag": ["CIS M365 7.0.0 (6.5.4)", "NIST CSF 2.0 (PR.IR-01)"], "appliesToTest": ["CISAMSEXO51", "CIS_6_5_4", "ZTNA21799"], "helpText": "Disables SMTP AUTH organization-wide, impacting POP and IMAP clients that rely on SMTP for sending emails. Default for new tenants. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission)", "docsDescription": "Disables tenant-wide SMTP basic authentication, including for all explicitly enabled users, impacting POP and IMAP clients that rely on SMTP for sending emails. For more information, see the [Microsoft documentation](https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission).", @@ -382,7 +382,7 @@ { "name": "standards.ActivityBasedTimeout", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], + "tag": ["CIS M365 7.0.0 (1.3.2)", "spo_idle_session_timeout", "NIST CSF 2.0 (PR.AA-03)"], "appliesToTest": ["CIS_1_3_2", "ZTNA21813", "ZTNA21814", "ZTNA21815"], "helpText": "Enables and sets Idle session timeout for Microsoft 365 to 1 hour. This policy affects most M365 web apps", "executiveText": "Automatically logs out inactive users from Microsoft 365 applications after a specified time period to prevent unauthorized access to company data on unattended devices. This security measure protects against data breaches when employees leave workstations unlocked.", @@ -413,7 +413,7 @@ "name": "standards.AuthMethodsSettings", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.2.3.6)", + "CIS M365 7.0.0 (5.2.3.6)", "EIDSCA.AG01", "EIDSCA.AG02", "EIDSCA.AG03", @@ -915,7 +915,7 @@ { "name": "standards.laps", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.5)", "SMB1001 (2.2)"], + "tag": ["CIS M365 7.0.0 (5.1.4.5)", "SMB1001 (2.2)"], "appliesToTest": ["CIS_5_1_4_5", "SMB1001_2_2", "ZTNA21953", "ZTNA21955", "ZTNA24560"], "helpText": "Enables the tenant to use LAPS. You must still create a policy for LAPS to be active on all devices. Use the template standards to deploy this by default.", "docsDescription": "Enables the LAPS functionality on the tenant. Prerequisite for using Windows LAPS via Azure AD.", @@ -932,7 +932,7 @@ "name": "standards.PWdisplayAppInformationRequiredState", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.2.3.1)", + "CIS M365 7.0.0 (5.2.3.1)", "EIDSCA.AM03", "EIDSCA.AM04", "EIDSCA.AM06", @@ -981,8 +981,8 @@ { "name": "standards.PWcompanionAppAllowedState", "cat": "Entra (AAD) Standards", - "tag": ["EIDSCA.AM01"], - "appliesToTest": ["EIDSCAAM01"], + "tag": ["CIS M365 7.0.0 (5.2.3.10)", "EIDSCA.AM01"], + "appliesToTest": ["CIS_5_2_3_10", "EIDSCAAM01"], "helpText": "Sets the state of Authenticator Lite, Authenticator lite is a companion app for passwordless authentication.", "docsDescription": "Sets the Authenticator Lite state to enabled. This allows users to use the Authenticator Lite built into the Outlook app instead of the full Authenticator app.", "executiveText": "Enables a simplified authentication experience by allowing users to authenticate directly through Outlook without requiring a separate authenticator app. This improves user convenience while maintaining security standards for passwordless authentication.", @@ -1081,7 +1081,7 @@ { "name": "standards.FormsPhishingProtection", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (1.3.5)", "Security", "PhishingProtection"], + "tag": ["CIS M365 7.0.0 (1.3.5)", "Security", "PhishingProtection"], "appliesToTest": ["CIS_1_3_5"], "helpText": "Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns.", "docsDescription": "Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization.", @@ -1125,7 +1125,7 @@ { "name": "standards.PasswordExpireDisabled", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (1.3.1)", "PWAgePolicyNew"], + "tag": ["CIS M365 7.0.0 (1.3.1)", "PWAgePolicyNew"], "appliesToTest": ["CIS_1_3_1", "ZTNA21811"], "helpText": "Disables the expiration of passwords for the tenant by setting the password expiration policy to never expire for any user.", "docsDescription": "Sets passwords to never expire for tenant, recommended to use in conjunction with secure password requirements.", @@ -1141,7 +1141,7 @@ { "name": "standards.CustomBannedPasswordList", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.2.3.2)", "SMB1001 (2.1)"], + "tag": ["CIS M365 7.0.0 (5.2.3.2)", "SMB1001 (2.1)"], "appliesToTest": [ "CIS_5_2_3_2", "EIDSCAPR01", @@ -1203,7 +1203,7 @@ { "name": "standards.DisableTenantCreation", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "SMB1001 (2.8)"], + "tag": ["CIS M365 7.0.0 (5.1.2.3)", "CISA (MS.AAD.6.1v1)", "SMB1001 (2.8)"], "appliesToTest": ["CIS_5_1_2_3", "SMB1001_2_8", "ZTNA21772", "ZTNA21787"], "helpText": "Restricts creation of M365 tenants to the Global Administrator or Tenant Creator roles.", "docsDescription": "Users by default are allowed to create M365 tenants. This disables that so only admins can create new M365 tenants.", @@ -1220,7 +1220,7 @@ "name": "standards.EnableAppConsentRequests", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.1.5.2)", + "CIS M365 7.0.0 (5.1.5.2)", "CISA (MS.AAD.9.1v1)", "EIDSCA.CP04", "EIDSCA.CR01", @@ -1323,7 +1323,7 @@ "name": "standards.DisableAppCreation", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.1.2.2)", + "CIS M365 7.0.0 (5.1.2.2)", "CISA (MS.AAD.4.1v1)", "EIDSCA.AP10", "Essential 8 (1175)", @@ -1345,7 +1345,7 @@ { "name": "standards.BitLockerKeysForOwnedDevice", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.6)"], + "tag": ["CIS M365 7.0.0 (5.1.4.6)"], "appliesToTest": ["CIS_5_1_4_6", "ZTNA21954"], "helpText": "Controls whether standard users can recover BitLocker keys for devices they own.", "docsDescription": "Updates the Microsoft Entra authorization policy that controls whether standard users can read BitLocker recovery keys for devices they own. Choose to restrict access for tighter security or allow self-service recovery when operational needs require it.", @@ -1374,12 +1374,12 @@ "name": "standards.DisableSecurityGroupUsers", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.1.3.2)", + "CIS M365 7.0.0 (5.1.3.1)", "CISA (MS.AAD.20.1v1)", "NIST CSF 2.0 (PR.AA-05)", "SMB1001 (2.8)" ], - "appliesToTest": ["CIS_5_1_3_2", "SMB1001_2_8", "ZTNA21868"], + "appliesToTest": ["CIS_5_1_3_1", "SMB1001_2_8", "ZTNA21868"], "helpText": "Completely disables the creation of security groups by users. This also breaks the ability to manage groups themselves, or create Teams", "executiveText": "Restricts the creation of security groups to IT administrators only, preventing employees from creating unauthorized access groups that could bypass security controls. This ensures proper governance of access permissions and maintains centralized control over who can access what resources.", "addedComponent": [], @@ -1407,7 +1407,7 @@ { "name": "standards.DisableSelfServiceLicenses", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (1.3.4)", "SMB1001 (2.8)"], + "tag": ["CIS M365 7.0.0 (1.3.4)", "SMB1001 (2.8)"], "appliesToTest": ["CIS_1_3_4", "EIDSCAAP05", "SMB1001_2_8"], "helpText": "**Requires 'Billing Administrator' GDAP role.** This standard disables all self service licenses and enables all exclusions", "executiveText": "Prevents employees from purchasing Microsoft 365 licenses independently, ensuring all software acquisitions go through proper procurement channels. This maintains budget control, prevents unauthorized spending, and ensures compliance with corporate licensing agreements.", @@ -1459,7 +1459,7 @@ "name": "standards.OauthConsent", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.1.5.1)", + "CIS M365 7.0.0 (5.1.5.1)", "CISA (MS.AAD.4.2v1)", "EIDSCA.AP08", "EIDSCA.AP09", @@ -1603,7 +1603,7 @@ "name": "standards.DisableSMS", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.2.3.5)", + "CIS M365 7.0.0 (5.2.3.5)", "EIDSCA.AS04", "NIST CSF 2.0 (PR.AA-03)", "SMB1001 (2.5)", @@ -1633,7 +1633,7 @@ "name": "standards.DisableVoice", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.2.3.5)", + "CIS M365 7.0.0 (5.2.3.5)", "EIDSCA.AV01", "NIST CSF 2.0 (PR.AA-03)", "SMB1001 (2.5)", @@ -1663,7 +1663,7 @@ "name": "standards.DisableEmail", "cat": "Entra (AAD) Standards", "tag": [ - "CIS M365 6.0.1 (5.2.3.7)", + "CIS M365 7.0.0 (5.2.3.7)", "NIST CSF 2.0 (PR.AA-03)", "SMB1001 (2.5)", "SMB1001 (2.6)", @@ -1790,7 +1790,8 @@ { "name": "standards.AppManagementPolicy", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (5.1.5.3)", "CIS M365 7.0.0 (5.1.5.4)", "CIS M365 7.0.0 (5.1.5.5)", "CIS M365 7.0.0 (5.1.5.6)"], + "appliesToTest": ["CIS_5_1_5_3", "CIS_5_1_5_4", "CIS_5_1_5_5", "CIS_5_1_5_6"], "helpText": "Configures the default app management policy to control application and service principal credential restrictions such as password and key credential lifetimes.", "docsDescription": "Configures the default app management policy to control application and service principal credential restrictions. This includes password addition restrictions, custom password addition, symmetric key addition, and credential lifetime limits for both applications and service principals.", "executiveText": "Enforces credential restrictions on application registrations and service principals to limit how secrets and certificates are created and how long they remain valid. This reduces the risk of long-lived or unmanaged credentials being used to access your tenant.", @@ -1843,7 +1844,7 @@ { "name": "standards.OutBoundSpamAlert", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.6)"], + "tag": ["CIS M365 7.0.0 (2.1.6)"], "appliesToTest": ["CIS_2_1_6"], "helpText": "Set the Outbound Spam Alert e-mail address", "docsDescription": "Sets the e-mail address to which outbound spam alerts are sent.", @@ -2121,7 +2122,7 @@ { "name": "standards.SpoofWarn", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (6.2.3)"], + "tag": ["CIS M365 7.0.0 (6.2.3)"], "appliesToTest": ["CISAMSEXO71", "CIS_6_2_3", "ORCA111", "ORCA240"], "helpText": "Adds or removes indicators to e-mail messages received from external senders in Outlook. Works on all Outlook clients/OWA", "docsDescription": "Adds or removes indicators to e-mail messages received from external senders in Outlook. You can read more about this feature on [Microsoft's Exchange Team Blog.](https://techcommunity.microsoft.com/t5/exchange-team-blog/native-external-sender-callouts-on-email-in-outlook/ba-p/2250098)", @@ -2163,7 +2164,7 @@ { "name": "standards.EnableMailTips", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (6.5.2)", "exo_mailtipsenabled"], + "tag": ["CIS M365 7.0.0 (6.5.2)", "exo_mailtipsenabled"], "appliesToTest": ["CIS_6_5_2"], "helpText": "Enables all MailTips in Outlook. MailTips are the notifications Outlook and Outlook on the web shows when an email you create, meets some requirements", "executiveText": "Enables helpful notifications in Outlook that warn users about potential email issues, such as sending to large groups, external recipients, or invalid addresses. This reduces email mistakes and improves communication efficiency by providing real-time guidance to employees.", @@ -2241,7 +2242,7 @@ { "name": "standards.RotateDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], + "tag": ["CIS M365 7.0.0 (2.1.9)", "SMB1001 (2.12)"], "appliesToTest": ["CIS_2_1_9", "SMB1001_2_12"], "helpText": "Rotate DKIM keys that are 1024 bit to 2048 bit", "executiveText": "Upgrades email security by replacing older 1024-bit encryption keys with stronger 2048-bit keys for email authentication. This improves the organization's email security posture and helps prevent email spoofing and tampering, maintaining trust with email recipients.", @@ -2295,7 +2296,7 @@ { "name": "standards.AddDKIM", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.9)", "SMB1001 (2.12)"], + "tag": ["CIS M365 7.0.0 (2.1.9)", "SMB1001 (2.12)"], "appliesToTest": ["CISAMSEXO31", "CIS_2_1_9", "ORCA108", "ORCA108_1", "SMB1001_2_12"], "helpText": "Enables DKIM for all domains that currently support it", "executiveText": "Enables email authentication technology that digitally signs outgoing emails to verify they actually came from your organization. This prevents email spoofing, improves email deliverability, and protects the company's reputation by ensuring recipients can trust emails from your domains.", @@ -2317,7 +2318,7 @@ { "name": "standards.AddDMARCToMOERA", "cat": "Global Standards", - "tag": ["CIS M365 6.0.1 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], + "tag": ["CIS M365 7.0.0 (2.1.10)", "Security", "PhishingProtection", "SMB1001 (2.12)"], "appliesToTest": ["CIS_2_1_10", "SMB1001_2_12"], "helpText": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", "docsDescription": "** Remediation is not available ** Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%", @@ -2348,9 +2349,9 @@ "name": "standards.EnableMailboxAuditing", "cat": "Exchange Standards", "tag": [ - "CIS M365 6.0.1 (6.1.1)", - "CIS M365 6.0.1 (6.1.2)", - "CIS M365 6.0.1 (6.1.3)", + "CIS M365 7.0.0 (6.1.1)", + "CIS M365 7.0.0 (6.1.2)", + "CIS M365 7.0.0 (6.1.3)", "exo_mailboxaudit", "Essential 8 (1509)", "Essential 8 (1683)", @@ -2557,7 +2558,7 @@ { "name": "standards.EXOOutboundSpamLimits", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (2.1.15)"], + "tag": ["CIS M365 7.0.0 (2.1.15)"], "appliesToTest": ["CIS_2_1_15"], "helpText": "Configures the outbound spam recipient limits (external per hour, internal per hour, per day) and the action to take when a limit is reached. The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one. ", "docsDescription": "Configures the Exchange Online outbound spam recipient limits for external per hour, internal per hour, and per day, along with the action to take (e.g., BlockUser, Alert) when these limits are exceeded. This helps prevent abuse and manage email flow. Microsoft's recommendations can be found [here.](https://learn.microsoft.com/en-us/defender-office-365/recommended-settings-for-eop-and-office365#eop-outbound-spam-policy-settings) The 'Set Outbound Spam Alert e-mail' standard is recommended to configure together with this one.", @@ -2626,7 +2627,7 @@ { "name": "standards.DisableExternalCalendarSharing", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (1.3.3)", "exo_individualsharing"], + "tag": ["CIS M365 7.0.0 (1.3.3)", "exo_individualsharing"], "appliesToTest": ["CISAMSEXO62", "CIS_1_3_3", "ZTNA21803"], "helpText": "Disables the ability for users to share their calendar with external users. Only for the default policy, so exclusions can be made if needed.", "docsDescription": "Disables external calendar sharing for the entire tenant. This is not a widely used feature, and it's therefore unlikely that this will impact users. Only for the default policy, so exclusions can be made if needed by making a new policy and assigning it to users.", @@ -2665,7 +2666,7 @@ { "name": "standards.DisableAdditionalStorageProviders", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (6.5.3)", "exo_storageproviderrestricted"], + "tag": ["CIS M365 7.0.0 (6.5.3)", "exo_storageproviderrestricted"], "appliesToTest": ["CIS_6_5_3", "ZTNA21817"], "helpText": "Disables the ability for users to open files in Outlook on the Web, from other providers such as Box, Dropbox, Facebook, Google Drive, OneDrive Personal, etc.", "docsDescription": "Disables additional storage providers in OWA. This is to prevent users from using personal storage providers like Dropbox, Google Drive, etc. Usually this has little user impact.", @@ -2688,7 +2689,7 @@ { "name": "standards.AntiSpamSafeList", "cat": "Defender Standards", - "tag": ["CIS M365 6.0.1 (2.1.13)"], + "tag": ["CIS M365 7.0.0 (2.1.13)"], "appliesToTest": ["CIS_2_1_13"], "helpText": "Sets the anti-spam connection filter policy option 'safe list' in Defender.", "docsDescription": "Sets [Microsoft's built-in 'safe list'](https://learn.microsoft.com/en-us/powershell/module/exchange/set-hostedconnectionfilterpolicy?view=exchange-ps#-enablesafelist) in the anti-spam connection filter policy, rather than setting a custom safe/block list of IPs.", @@ -2770,7 +2771,7 @@ { "name": "standards.Bookings", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (1.3.9)"], + "tag": ["CIS M365 7.0.0 (1.3.9)"], "appliesToTest": ["CIS_1_3_9"], "helpText": "Sets the state of Bookings on the tenant. Bookings is a scheduling tool that allows users to book appointments with others both internal and external.", "docsDescription": "", @@ -2804,7 +2805,7 @@ { "name": "standards.EXODirectSend", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (6.5.5)"], + "tag": ["CIS M365 7.0.0 (6.5.5)"], "appliesToTest": ["CIS_6_5_5"], "helpText": "Sets the state of Direct Send in Exchange Online. Direct Send allows applications to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication.", "docsDescription": "Controls whether applications can use Direct Send to send emails directly to Exchange Online mailboxes as the tenants domains, without requiring authentication. A detailed explanation from Microsoft can be found [here.](https://learn.microsoft.com/en-us/exchange/mail-flow-best-practices/how-to-set-up-a-multifunction-device-or-application-to-send-email-using-microsoft-365-or-office-365)", @@ -2833,7 +2834,7 @@ "name": "standards.DisableOutlookAddins", "cat": "Exchange Standards", "tag": [ - "CIS M365 6.0.1 (6.3.1)", + "CIS M365 7.0.0 (6.3.1)", "exo_outlookaddins", "NIST CSF 2.0 (PR.AA-05)", "NIST CSF 2.0 (PR.PS-05)" @@ -2978,7 +2979,7 @@ { "name": "standards.UserSubmissions", "cat": "Exchange Standards", - "tag": ["CIS M365 6.0.1 (8.6.1)"], + "tag": ["CIS M365 7.0.0 (8.6.1)"], "appliesToTest": ["CIS_8_6_1"], "helpText": "Set the state of the spam submission button in Outlook", "docsDescription": "Set the state of the built-in Report button in Outlook. This gives the users the ability to report emails as spam or phish.", @@ -3019,7 +3020,7 @@ "name": "standards.DisableSharedMailbox", "cat": "Exchange Standards", "tag": [ - "CIS M365 6.0.1 (1.2.2)", + "CIS M365 7.0.0 (1.2.2)", "CISA (MS.AAD.10.1v1)", "NIST CSF 2.0 (PR.AA-01)", "SMB1001 (2.3)" @@ -3063,7 +3064,7 @@ "name": "standards.EXODisableAutoForwarding", "cat": "Exchange Standards", "tag": [ - "CIS M365 6.0.1 (6.2.1)", + "CIS M365 7.0.0 (6.2.1)", "mdo_autoforwardingmode", "mdo_blockmailforward", "CISA (MS.EXO.4.1v1)", @@ -3220,7 +3221,7 @@ "name": "standards.SafeLinksPolicy", "cat": "Defender Standards", "tag": [ - "CIS M365 6.0.1 (2.1.1)", + "CIS M365 7.0.0 (2.1.1)", "mdo_safelinksforemail", "mdo_safelinksforOfficeApps", "NIST CSF 2.0 (DE.CM-09)" @@ -3304,7 +3305,7 @@ "mdo_spam_notifications_only_for_admins", "mdo_antiphishingpolicies", "mdo_phishthresholdlevel", - "CIS M365 6.0.1 (2.1.7)", + "CIS M365 7.0.0 (2.1.7)", "NIST CSF 2.0 (DE.CM-09)" ], "appliesToTest": [ @@ -3498,7 +3499,7 @@ "name": "standards.SafeAttachmentPolicy", "cat": "Defender Standards", "tag": [ - "CIS M365 6.0.1 (2.1.4)", + "CIS M365 7.0.0 (2.1.4)", "mdo_safedocuments", "mdo_commonattachmentsfilter", "mdo_safeattachmentpolicy", @@ -3570,7 +3571,7 @@ { "name": "standards.AtpPolicyForO365", "cat": "Defender Standards", - "tag": ["CIS M365 6.0.1 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], + "tag": ["CIS M365 7.0.0 (2.1.5)", "NIST CSF 2.0 (DE.CM-09)"], "appliesToTest": ["CIS_2_1_5", "ORCA225"], "helpText": "This creates a Atp policy that enables Defender for Office 365 for SharePoint, OneDrive and Microsoft Teams.", "addedComponent": [ @@ -3654,9 +3655,9 @@ "name": "standards.MalwareFilterPolicy", "cat": "Defender Standards", "tag": [ - "CIS M365 6.0.1 (2.1.2)", - "CIS M365 6.0.1 (2.1.3)", - "CIS M365 6.0.1 (2.1.11)", + "CIS M365 7.0.0 (2.1.2)", + "CIS M365 7.0.0 (2.1.3)", + "CIS M365 7.0.0 (2.1.11)", "mdo_zapspam", "mdo_zapphish", "mdo_zapmalware", @@ -4336,7 +4337,7 @@ { "name": "standards.IntuneComplianceSettings", "cat": "Intune Standards", - "tag": ["CIS M365 6.0.1 (4.1)"], + "tag": ["CIS M365 7.0.0 (4.1)"], "appliesToTest": ["CIS_4_1"], "helpText": "Sets the mark devices with no compliance policy assigned as compliance/non compliant and Compliance status validity period.", "executiveText": "Configures how the system treats devices that don't have specific compliance policies and sets how often devices must check in to maintain their compliance status. This ensures proper security oversight of all corporate devices and maintains current compliance information.", @@ -4408,7 +4409,7 @@ { "name": "standards.DefaultPlatformRestrictions", "cat": "Intune Standards", - "tag": ["CIS M365 6.0.1 (4.2)", "CISA (MS.AAD.19.1v1)"], + "tag": ["CIS M365 7.0.0 (4.2)", "CISA (MS.AAD.19.1v1)"], "appliesToTest": ["CIS_4_2"], "helpText": "Sets the default platform restrictions for enrolling devices into Intune. Note: Do not block personally owned if platform is blocked.", "executiveText": "Controls which types of devices (iOS, Android, Windows, macOS) and ownership models (corporate vs. personal) can be enrolled in the company's device management system. This helps maintain security standards while supporting necessary business device types and usage scenarios.", @@ -4650,7 +4651,7 @@ { "name": "standards.intuneDeviceReg", "cat": "Intune Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.2)", "CISA (MS.AAD.17.1v1)"], + "tag": ["CIS M365 7.0.0 (5.1.4.2)", "CISA (MS.AAD.17.1v1)"], "appliesToTest": ["CIS_5_1_4_2", "ZTNA21801", "ZTNA21802", "ZTNA21837"], "helpText": "Sets the maximum number of devices that can be registered by a user. A value of 0 disables device registration by users", "executiveText": "Limits how many devices each employee can register for corporate access, preventing excessive device proliferation while accommodating legitimate business needs. This helps maintain security oversight and prevents potential abuse of device registration privileges.", @@ -4673,7 +4674,7 @@ { "name": "standards.intuneDeviceRegLocalAdmins", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.3)", "CIS M365 6.0.1 (5.1.4.4)", "SMB1001 (2.2)"], + "tag": ["CIS M365 7.0.0 (5.1.4.3)", "CIS M365 7.0.0 (5.1.4.4)", "SMB1001 (2.2)"], "appliesToTest": ["CIS_5_1_4_3", "CIS_5_1_4_4", "SMB1001_2_2"], "helpText": "Controls whether users who register Microsoft Entra joined devices are granted local administrator rights on those devices and if Global Administrators are added as local admins.", "docsDescription": "Configures the Device Registration Policy local administrator behavior for registering users. When enabled, users who register devices are not granted local administrator rights, you can also configure if Global Administrators are added as local admins.", @@ -4725,7 +4726,7 @@ { "name": "standards.intuneRestrictUserDeviceJoin", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.4.1)", "SMB1001 (2.8)"], + "tag": ["CIS M365 7.0.0 (5.1.4.1)", "SMB1001 (2.8)"], "appliesToTest": ["CIS_5_1_4_1", "SMB1001_2_8"], "helpText": "Controls whether users can join devices to Entra.", "docsDescription": "Configures whether users can join devices to Entra. When disabled, users are unable to Entra-join devices, which prevents them from creating new Entra-joined (cloud-managed) device identities.", @@ -4874,7 +4875,7 @@ { "name": "standards.SPAzureB2B", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.2)"], + "tag": ["CIS M365 7.0.0 (7.2.2)"], "appliesToTest": ["CIS_7_2_2"], "helpText": "Ensure SharePoint and OneDrive integration with Azure AD B2B is enabled", "executiveText": "Enables secure collaboration with external partners through SharePoint and OneDrive by integrating with Azure B2B guest access. This allows controlled sharing with external organizations while maintaining security oversight and proper access management.", @@ -4897,7 +4898,7 @@ { "name": "standards.SPDisallowInfectedFiles", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], + "tag": ["CIS M365 7.0.0 (7.3.1)", "CISA (MS.SPO.3.1v1)", "NIST CSF 2.0 (DE.CM-09)"], "appliesToTest": ["CIS_7_3_1", "ZTNA21817"], "helpText": "Ensure Office 365 SharePoint infected files are disallowed for download", "executiveText": "Prevents employees from downloading files that have been identified as containing malware or viruses from SharePoint and OneDrive. This security measure protects against malware distribution through file sharing while maintaining access to clean, safe documents.", @@ -4964,7 +4965,7 @@ { "name": "standards.SPExternalUserExpiration", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.9)", "CISA (MS.SPO.1.5v1)"], + "tag": ["CIS M365 7.0.0 (7.2.9)", "CISA (MS.SPO.1.5v1)"], "appliesToTest": ["CIS_7_2_9", "ZTNA21803", "ZTNA21804", "ZTNA21858"], "helpText": "Ensure guest access to a site or OneDrive will expire automatically", "executiveText": "Automatically expires external user access to SharePoint sites and OneDrive after a specified period, reducing security risks from forgotten or unnecessary guest accounts. This ensures external access is regularly reviewed and maintained only when actively needed.", @@ -4998,7 +4999,7 @@ { "name": "standards.SPEmailAttestation", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.10)", "CISA (MS.SPO.1.6v1)"], + "tag": ["CIS M365 7.0.0 (7.2.10)", "CISA (MS.SPO.1.6v1)"], "appliesToTest": ["CIS_7_2_10", "ZTNA21803", "ZTNA21804"], "helpText": "Ensure re-authentication with verification code is restricted", "executiveText": "Requires external users to periodically re-verify their identity through email verification codes when accessing SharePoint resources, adding an extra security layer for external collaboration. This helps ensure continued legitimacy of external access over time.", @@ -5032,7 +5033,7 @@ { "name": "standards.DefaultSharingLink", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.7)", "CIS M365 6.0.1 (7.2.11)", "CISA (MS.SPO.1.4v1)"], + "tag": ["CIS M365 7.0.0 (7.2.7)", "CIS M365 7.0.0 (7.2.11)", "CISA (MS.SPO.1.4v1)"], "appliesToTest": ["CIS_7_2_11", "CIS_7_2_7", "ZTNA21803", "ZTNA21804"], "helpText": "Configure the SharePoint default sharing link type and permission. This setting controls both the type of sharing link created by default and the permission level assigned to those links.", "docsDescription": "Sets the default sharing link type (Direct or Internal) and permission (View) in SharePoint and OneDrive. Direct sharing means links only work for specific people, while Internal sharing means links work for anyone in the organization. Setting the view permission as the default ensures that users must deliberately select the edit permission when sharing a link, reducing the risk of unintentionally granting edit privileges.", @@ -5138,7 +5139,7 @@ "name": "standards.DisableSharePointLegacyAuth", "cat": "SharePoint Standards", "tag": [ - "CIS M365 6.0.1 (7.2.1)", + "CIS M365 7.0.0 (7.2.1)", "spo_legacy_auth", "CISA (MS.AAD.3.1v1)", "NIST CSF 2.0 (PR.IR-01)" @@ -5167,8 +5168,8 @@ "name": "standards.sharingCapability", "cat": "SharePoint Standards", "tag": [ - "CIS M365 6.0.1 (7.2.3)", - "CIS M365 6.0.1 (7.2.4)", + "CIS M365 7.0.0 (7.2.3)", + "CIS M365 7.0.0 (7.2.4)", "CISA (MS.AAD.14.1v1)", "CISA (MS.SPO.1.1v1)" ], @@ -5219,7 +5220,7 @@ { "name": "standards.DisableReshare", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], + "tag": ["CIS M365 7.0.0 (7.2.5)", "CISA (MS.AAD.14.2v1)", "CISA (MS.SPO.1.2v1)"], "appliesToTest": ["CIS_7_2_5", "ZTNA21803", "ZTNA21804"], "helpText": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access", "docsDescription": "Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level", @@ -5317,8 +5318,8 @@ { "name": "standards.unmanagedSync", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.3.2)", "CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], - "appliesToTest": ["CIS_7_3_2", "ZTNA24824"], + "tag": ["CISA (MS.SPO.2.1v1)", "NIST CSF 2.0 (PR.AA-05)"], + "appliesToTest": ["ZTNA24824"], "helpText": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.", "docsDescription": "Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)", "executiveText": "Restricts access to company files from personal or unmanaged devices, ensuring corporate data can only be accessed from properly secured and monitored devices. This critical security control prevents data leaks while allowing controlled access through web browsers when necessary.", @@ -5347,7 +5348,7 @@ { "name": "standards.sharingDomainRestriction", "cat": "SharePoint Standards", - "tag": ["CIS M365 6.0.1 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], + "tag": ["CIS M365 7.0.0 (7.2.6)", "CISA (MS.AAD.14.3v1)", "CISA (MS.SPO.1.3v1)"], "appliesToTest": ["CIS_7_2_6", "ZTNA21803", "ZTNA21804"], "helpText": "Restricts sharing to only users with the specified domain. This is useful for organizations that only want to share with their own domain.", "executiveText": "Controls which external domains employees can share files with, enabling secure collaboration with trusted partners while blocking sharing with unauthorized organizations. This targeted approach maintains necessary business relationships while preventing data exposure to unknown entities.", @@ -5389,15 +5390,15 @@ "name": "standards.TeamsGlobalMeetingPolicy", "cat": "Teams Standards", "tag": [ - "CIS M365 6.0.1 (8.5.1)", - "CIS M365 6.0.1 (8.5.2)", - "CIS M365 6.0.1 (8.5.3)", - "CIS M365 6.0.1 (8.5.4)", - "CIS M365 6.0.1 (8.5.5)", - "CIS M365 6.0.1 (8.5.6)", - "CIS M365 6.0.1 (8.5.7)", - "CIS M365 6.0.1 (8.5.8)", - "CIS M365 6.0.1 (8.5.9)" + "CIS M365 7.0.0 (8.5.1)", + "CIS M365 7.0.0 (8.5.2)", + "CIS M365 7.0.0 (8.5.3)", + "CIS M365 7.0.0 (8.5.4)", + "CIS M365 7.0.0 (8.5.5)", + "CIS M365 7.0.0 (8.5.6)", + "CIS M365 7.0.0 (8.5.7)", + "CIS M365 7.0.0 (8.5.8)", + "CIS M365 7.0.0 (8.5.9)" ], "appliesToTest": [ "CIS_8_5_1", @@ -5531,7 +5532,7 @@ { "name": "standards.TeamsExternalChatWithAnyone", "cat": "Teams Standards", - "tag": ["CIS M365 6.0.1 (8.2.3)"], + "tag": ["CIS M365 7.0.0 (8.2.3)"], "appliesToTest": ["CIS_8_2_3"], "helpText": "Controls whether users can start Teams chats with any email address, inviting external recipients as guests via email.", "docsDescription": "Manages the Teams messaging policy setting UseB2BInvitesToAddExternalUsers. When enabled, users can start chats with any email address and recipients receive an invitation to join the chat as guests. Disabling the setting prevents these external email chats from being created, keeping conversations limited to internal users and approved guests.", @@ -5575,7 +5576,7 @@ "addedDate": "2024-07-30", "powershellEquivalent": "Set-CsTeamsClientConfiguration -AllowEmailIntoChannel $false", "recommendedBy": ["CIS"], - "tag": ["CIS M365 6.0.1 (8.1.2)"], + "tag": ["CIS M365 7.0.0 (8.1.2)"], "appliesToTest": ["CIS_8_1_2"], "requiredCapabilities": ["MCOSTANDARD", "MCOEV", "MCOIMP", "TEAMS1", "Teams_Room_Standard"] }, @@ -5635,7 +5636,7 @@ { "name": "standards.TeamsExternalFileSharing", "cat": "Teams Standards", - "tag": ["CIS M365 6.0.1 (8.1.1)"], + "tag": ["CIS M365 7.0.0 (8.1.1)"], "appliesToTest": ["CIS_8_1_1"], "helpText": "Ensure external file sharing in Teams is enabled for only approved cloud storage services.", "executiveText": "Controls which external cloud storage services (like Google Drive, Dropbox, Box) employees can access through Teams, ensuring file sharing occurs only through approved and secure platforms. This helps maintain data governance while supporting necessary business integrations.", @@ -5706,7 +5707,7 @@ { "name": "standards.TeamsExternalAccessPolicy", "cat": "Teams Standards", - "tag": ["CIS M365 6.0.1 (8.2.1)", "CIS M365 6.0.1 (8.2.2)"], + "tag": ["CIS M365 7.0.0 (8.2.1)", "CIS M365 7.0.0 (8.2.2)"], "appliesToTest": ["CIS_8_2_1", "CIS_8_2_2"], "helpText": "Sets the properties of the Global external access policy.", "docsDescription": "Sets the properties of the Global external access policy. External access policies determine whether or not your users can: 1) communicate with users who have Session Initiation Protocol (SIP) accounts with a federated organization; 2) communicate with users who are using custom applications built with Azure Communication Services; 3) access Skype for Business Server over the Internet, without having to log on to your internal network; 4) communicate with users who have SIP accounts with a public instant messaging (IM) provider such as Skype; and, 5) communicate with people who are using Teams with an account that's not managed by an organization.", @@ -5734,7 +5735,7 @@ { "name": "standards.TeamsFederationConfiguration", "cat": "Teams Standards", - "tag": ["CIS M365 6.0.1 (8.2.1)"], + "tag": ["CIS M365 7.0.0 (8.2.1)"], "appliesToTest": ["CIS_8_2_1", "CIS_8_2_4"], "helpText": "Sets the properties of the Global federation configuration.", "docsDescription": "Sets the properties of the Global federation configuration. Federation configuration settings determine whether or not your users can communicate with users who have SIP accounts with a federated organization.", @@ -5810,7 +5811,7 @@ { "name": "standards.TeamsMessagingPolicy", "cat": "Teams Standards", - "tag": ["CIS M365 6.0.1 (8.6.1)"], + "tag": ["CIS M365 7.0.0 (8.6.1)"], "appliesToTest": ["CIS_8_6_1"], "helpText": "Sets the properties of the Global messaging policy.", "docsDescription": "Sets the properties of the Global messaging policy. Messaging policies control which chat and channel messaging features are available to users in Teams.", @@ -6405,18 +6406,18 @@ "impact": "High Impact", "addedDate": "2023-12-30", "tag": [ - "CIS M365 6.0.1 (5.2.2.1)", - "CIS M365 6.0.1 (5.2.2.2)", - "CIS M365 6.0.1 (5.2.2.3)", - "CIS M365 6.0.1 (5.2.2.4)", - "CIS M365 6.0.1 (5.2.2.5)", - "CIS M365 6.0.1 (5.2.2.6)", - "CIS M365 6.0.1 (5.2.2.7)", - "CIS M365 6.0.1 (5.2.2.8)", - "CIS M365 6.0.1 (5.2.2.9)", - "CIS M365 6.0.1 (5.2.2.10)", - "CIS M365 6.0.1 (5.2.2.11)", - "CIS M365 6.0.1 (5.2.2.12)", + "CIS M365 7.0.0 (5.2.2.1)", + "CIS M365 7.0.0 (5.2.2.2)", + "CIS M365 7.0.0 (5.2.2.3)", + "CIS M365 7.0.0 (5.2.2.4)", + "CIS M365 7.0.0 (5.2.2.5)", + "CIS M365 7.0.0 (5.2.2.6)", + "CIS M365 7.0.0 (5.2.2.7)", + "CIS M365 7.0.0 (5.2.2.8)", + "CIS M365 7.0.0 (5.2.2.9)", + "CIS M365 7.0.0 (5.2.2.10)", + "CIS M365 7.0.0 (5.2.2.11)", + "CIS M365 7.0.0 (5.2.2.12)", "SMB1001 (2.5)", "SMB1001 (2.6)", "SMB1001 (2.8)", @@ -6543,8 +6544,8 @@ "label": "Group Template", "multi": true, "cat": "Templates", - "tag": ["CIS M365 6.0.1 (5.1.3.1)"], - "appliesToTest": ["CIS_5_1_3_1"], + "tag": [], + "appliesToTest": [], "disabledFeatures": { "report": true, "warn": true, "remediate": false }, "impact": "Medium Impact", "addedDate": "2023-12-30", @@ -7467,10 +7468,10 @@ { "name": "standards.EnforcePrivateGroups", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (1.2.1)"], + "tag": ["CIS M365 7.0.0 (1.2.1)"], "appliesToTest": ["CIS_1_2_1"], "helpText": "Sets all public Microsoft 365 groups to private automatically. Groups can be excluded by display name keyword.", - "docsDescription": "Ensures only organisation-managed or approved public groups exist by automatically switching public Microsoft 365 (Unified) groups to private visibility. Groups whose display name matches any of the configured exclusion keywords are left unchanged. This aligns with CIS M365 6.0.1 benchmark control 1.2.1.", + "docsDescription": "Ensures only organisation-managed or approved public groups exist by automatically switching public Microsoft 365 (Unified) groups to private visibility. Groups whose display name matches any of the configured exclusion keywords are left unchanged. This aligns with CIS M365 7.0.0 benchmark control 1.2.1.", "executiveText": "Enforces private visibility on all Microsoft 365 groups to prevent unauthorised external access to group resources such as Teams, SharePoint sites, and Planner boards. Approved public groups can be excluded by name, ensuring governance while retaining flexibility for intentionally public collaboration spaces.", "addedComponent": [ { @@ -7501,7 +7502,7 @@ { "name": "standards.EmptyFilterIPAllowList", "cat": "Defender Standards", - "tag": ["CIS M365 6.0.1 (2.1.12)"], + "tag": ["CIS M365 7.0.0 (2.1.12)"], "appliesToTest": ["CIS_2_1_12"], "helpText": "Ensures the connection filter IP allow list is not used. IPs on this list bypass spam, spoof, and authentication checks.", "docsDescription": "IPs on the connection filter allow list bypass spam, spoof, and authentication checks. CIS recommends keeping this list empty to ensure all inbound email is properly scanned. This standard checks that the IPAllowList on the Default hosted connection filter policy is empty and can remediate by clearing it.", @@ -7524,10 +7525,10 @@ { "name": "standards.TeamsZAP", "cat": "Defender Standards", - "tag": ["CIS M365 6.0.1 (2.4.4)"], + "tag": ["CIS M365 7.0.0 (2.4.4)"], "appliesToTest": ["CIS_2_4_4"], "helpText": "Ensures Zero-hour auto purge (ZAP) is enabled for Microsoft Teams, automatically removing malicious messages after delivery.", - "docsDescription": "Zero-hour auto purge (ZAP) for Microsoft Teams retroactively detects and neutralises malicious messages that have already been delivered in Teams chats. Enabling ZAP ensures that phishing, malware, and high confidence phishing messages are automatically purged even after initial delivery, aligning with CIS M365 6.0.1 benchmark control 2.4.4.", + "docsDescription": "Zero-hour auto purge (ZAP) for Microsoft Teams retroactively detects and neutralises malicious messages that have already been delivered in Teams chats. Enabling ZAP ensures that phishing, malware, and high confidence phishing messages are automatically purged even after initial delivery, aligning with CIS M365 7.0.0 benchmark control 2.4.4.", "executiveText": "Enables Zero-hour auto purge for Microsoft Teams to automatically detect and remove malicious messages after delivery. This provides an additional layer of protection against phishing and malware that may bypass initial scanning, ensuring threats are neutralised even after they reach users.", "addedComponent": [], "label": "Ensure Zero-hour auto purge for Microsoft Teams is on", @@ -7547,7 +7548,7 @@ { "name": "standards.CollaborationDomainRestriction", "cat": "Entra (AAD) Standards", - "tag": ["CIS M365 6.0.1 (5.1.6.1)"], + "tag": ["CIS M365 7.0.0 (5.1.6.1)"], "appliesToTest": ["CIS_5_1_6_1"], "helpText": "Restricts B2B collaboration invitations to a specified list of allowed domains. If no domains are provided, the standard will alert and report on whether any domain restrictions are currently configured.", "docsDescription": "By default, Microsoft Entra ID allows collaboration invitations to be sent to any external domain. CIS recommends restricting B2B collaboration invitations to only approved domains to reduce the risk of data exfiltration and unauthorized access. This standard checks the B2B management policy for an allow list of domains and can remediate by setting the allowed domains list.", @@ -7897,7 +7898,8 @@ { "name": "standards.SmartLockout", "cat": "Entra (AAD) Standards", - "tag": [], + "tag": ["CIS M365 7.0.0 (5.2.3.8)", "CIS M365 7.0.0 (5.2.3.9)"], + "appliesToTest": ["CIS_5_2_3_8", "CIS_5_2_3_9"], "helpText": "**Requires Entra ID P1.** Configures the Entra ID Smart Lockout settings including lockout duration, lockout threshold, and on-premises integration mode.", "docsDescription": "Configures the Entra ID Smart Lockout policy which protects against brute-force password attacks. Smart Lockout locks out bad actors who try to guess user passwords or use brute-force methods. It recognizes sign-ins from valid users and treats them differently from attackers. Settings include lockout duration (seconds), lockout threshold (failed attempts before lockout), and on-premises password protection mode (Audit or Enforced).", "addedComponent": [ From 4c2843c74331a4af685b700ab82f20b4cdbe33b7 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 7 Jun 2026 13:24:08 +0800 Subject: [PATCH 116/133] more secure pipeline configuration --- .npmrc | 26 ++++++++++++++++++++++++++ .yarnrc | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 .npmrc create mode 100644 .yarnrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000000..12937d4c275f --- /dev/null +++ b/.npmrc @@ -0,0 +1,26 @@ +# Supply-chain hardening for CIPP +# This file is honored by BOTH npm and yarn (yarn classic reads .npmrc). +# Any change here should be reviewed for CI/CD impact. + +# Refuse to execute package lifecycle scripts (pre/postinstall, prepare, etc.) +# on dependency install. CIPP has zero of its own lifecycle scripts in +# package.json, so the only scripts this would block are from third-party +# packages — exactly the attack surface we want to close. +ignore-scripts=true + +# Pin the registry explicitly so an inherited per-user .npmrc cannot redirect +# CI / contributor installs to a malicious mirror. +registry=https://registry.npmjs.org/ + +# Require integrity hashes (sha512) to match the lockfile on install. +# npm honors this directly; yarn classic always verifies lockfile integrity +# but this makes the intent explicit. +audit-level=high + +# Don't auto-save changes to the lockfile from arbitrary install commands. +# Lockfile edits should only happen via Dependabot PRs or explicit upgrades. +save-exact=true + +# Disable funding/notifier noise so CI logs only show real signal. +fund=false +update-notifier=false diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 000000000000..c0a88a3c68ee --- /dev/null +++ b/.yarnrc @@ -0,0 +1,18 @@ +# Supply-chain hardening for CIPP (yarn 1 / classic) +# +# This complements .npmrc — yarn 1 honors `ignore-scripts` from .npmrc, but +# we set the per-command equivalents here as defense in depth so the +# protection survives even if .npmrc is missing or ignored. + +# Refuse to execute lifecycle scripts on `yarn install` / `yarn add` / +# `yarn upgrade`. Mirrors `ignore-scripts=true` in .npmrc. +--install.ignore-scripts true +--add.ignore-scripts true +--upgrade.ignore-scripts true + +# Pin the registry so a poisoned per-user .yarnrc cannot redirect installs. +registry "https://registry.npmjs.org/" + +# Disable yarn's self-update check — CI should never auto-update its own +# yarn binary mid-build. +disable-self-update-check true From 55e8eef2399713f966791554ee214f928cb72f81 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:06:59 +0800 Subject: [PATCH 117/133] Update worker-health.js --- src/pages/cipp/advanced/worker-health.js | 84 +++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/src/pages/cipp/advanced/worker-health.js b/src/pages/cipp/advanced/worker-health.js index e22ed9f8b31d..29d8530cbe9e 100644 --- a/src/pages/cipp/advanced/worker-health.js +++ b/src/pages/cipp/advanced/worker-health.js @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { Fragment, useCallback, useMemo, useRef, useState } from "react"; import Head from "next/head"; import { Box, @@ -1050,12 +1050,45 @@ const Page = () => { const diag = cacheDiagQuery.data?.Results; if (!diag) return null; const types = diag.TypeBreakdown ?? []; + const trackedMB = diag.TrackedTotalMB ?? 0; + const maxMB = diag.MaxMB ?? 0; + const memPct = maxMB > 0 ? (trackedMB / maxMB) * 100 : 0; + const totalReads = (diag.Hits ?? 0) + (diag.Misses ?? 0); + const fmtUtc = (s) => (s ? new Date(s).toLocaleString() : "—"); + + const cacheStats = [ + { k: "Hits", v: (diag.Hits ?? 0).toLocaleString() }, + { k: "Misses", v: (diag.Misses ?? 0).toLocaleString() }, + { + k: "Hit Rate", + v: `${diag.HitRate ?? 0}%`, + w: totalReads > 100 && diag.HitRate < 50, + }, + { + k: "Evictions", + v: (diag.Evictions ?? 0).toLocaleString(), + w: (diag.Evictions ?? 0) > 0, + }, + { + k: "Oversized", + v: (diag.Oversized ?? 0).toLocaleString(), + w: (diag.Oversized ?? 0) > 0, + tip: "Values that exceeded the per-entry size cap and were silently dropped — they were never cached.", + }, + { k: "Accesses", v: (diag.AccessCount ?? 0).toLocaleString() }, + { + k: "TTL", + v: `${diag.TtlSeconds ?? 0}s`, + tip: `Earliest expiry: ${fmtUtc(diag.EarliestExpiryUtc)} • Latest expiry: ${fmtUtc(diag.LatestExpiryUtc)}`, + }, + ]; + return ( { /> } /> + 0 ? 2 : "12px !important" }}> + {/* Capacity bar */} + + + + Capacity + + + {trackedMB} / {maxMB} MB ({Math.round(memPct)}%) + + + 85 ? "error" : memPct > 70 ? "warning" : "primary"} + sx={{ height: 6, borderRadius: 3 }} + /> + + {/* Stats row */} + + {cacheStats.map((s) => { + const cell = ( + + + {s.k} + + + {s.v} + + + ); + if (s.tip) { + return ( + + {cell} + + ); + } + return {cell}; + })} + + {types.length > 0 && ( From 4c0c058f42dcd7cdb23fb83d64df0884a38493c4 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:28:09 +0800 Subject: [PATCH 118/133] Update alerts.json --- src/data/alerts.json | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/data/alerts.json b/src/data/alerts.json index ac8a28fa6cc0..e0f7ffc9b9a8 100644 --- a/src/data/alerts.json +++ b/src/data/alerts.json @@ -122,9 +122,19 @@ "name": "QuotaUsed", "label": "Alert on % mailbox quota used", "requiresInput": true, - "inputType": "textField", - "inputLabel": "Enter quota percentage", - "inputName": "QuotaUsedQuota", + "multipleInput": true, + "inputs": [ + { + "inputType": "textField", + "inputLabel": "Quota percentage threshold (default: 90)", + "inputName": "QuotaUsedQuota" + }, + { + "inputType": "textField", + "inputLabel": "Excluded mailbox UPNs (comma-separated). Supports custom variables like %excludefrommailboxalert% defined under CIPP > Settings > Custom Variables at tenant or AllTenants scope.", + "inputName": "QuotaUsedExcludedMailboxes" + } + ], "recommendedRunInterval": "1d" }, { From 4df01a60857ef84b8cf7085ddc1684aabc135767 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:16:54 +0800 Subject: [PATCH 119/133] Update unauthenticated.js --- src/pages/unauthenticated.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/unauthenticated.js b/src/pages/unauthenticated.js index 6bf4cb995902..49c6d26861b6 100644 --- a/src/pages/unauthenticated.js +++ b/src/pages/unauthenticated.js @@ -26,6 +26,9 @@ const Page = () => { } return []; }, [orgData.isSuccess, orgData.data?.clientPrincipal?.userRoles]); + + const canReturnHome = + swaStatus.isSuccess && !!swaStatus?.data?.clientPrincipal && userRoles.length > 0; return ( <> @@ -57,13 +60,9 @@ const Page = () => { "You're not allowed to be here, or are logged in under the wrong account." } title="Access Denied" - linkText={ - swaStatus?.data?.clientPrincipal !== null && userRoles.length > 0 - ? "Return to Home" - : "Login" - } + linkText={canReturnHome ? "Return to Home" : "Login"} link={ - swaStatus?.data?.clientPrincipal !== null && userRoles.length > 0 + canReturnHome ? "/" : `/.auth/login/aad?prompt=select_account&post_login_redirect_uri=${encodeURIComponent( window.location.href From 9c82a214263d290a46901fb7f4e804b4bfb573b5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:32:48 +0200 Subject: [PATCH 120/133] 10.5.0 version up --- package.json | 2 +- public/version.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 331476b49fa7..45e4b83f482d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cipp", - "version": "10.4.5", + "version": "10.5.0", "author": "CIPP Contributors", "homepage": "https://cipp.app/", "bugs": { diff --git a/public/version.json b/public/version.json index a09d0fcf2ccd..b7e5a36d3a96 100644 --- a/public/version.json +++ b/public/version.json @@ -1,3 +1,3 @@ { - "version": "10.4.5" + "version": "10.5.0" } From 580b66f91e2d658e81516e6a7f298fd9688e544c Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Tue, 9 Jun 2026 01:03:26 +0800 Subject: [PATCH 121/133] repair and fix failed SSO app creations and password addition failures --- .../ForcedSsoMigrationDialog.jsx | 4 +- .../CippComponents/SsoMigrationDialog.jsx | 17 +- .../CippSettings/CippSSOSettings.jsx | 153 ++++++++++++++---- 3 files changed, 143 insertions(+), 31 deletions(-) diff --git a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx index 7326ae56df0c..1d5add90b7ee 100644 --- a/src/components/CippComponents/ForcedSsoMigrationDialog.jsx +++ b/src/components/CippComponents/ForcedSsoMigrationDialog.jsx @@ -99,7 +99,9 @@ export const ForcedSsoMigrationDialog = () => { 'SSO migration failed. Please try again.'} - If this error persists, contact your CIPP administrator. + The app registration may have been created already — clicking Try Again{' '} + will pick up where it left off rather than starting over. If the error persists, + contact your CIPP administrator. ) : null} diff --git a/src/components/CippComponents/SsoMigrationDialog.jsx b/src/components/CippComponents/SsoMigrationDialog.jsx index 03026daec36a..cb4d9691b187 100644 --- a/src/components/CippComponents/SsoMigrationDialog.jsx +++ b/src/components/CippComponents/SsoMigrationDialog.jsx @@ -60,7 +60,8 @@ export const SsoMigrationDialog = ({ meData }) => { const result = ssoSetup.data?.data?.Results ?? ssoSetup.data?.Results const isSuccess = result?.severity === 'success' - const isError = ssoSetup.isError || result?.severity === 'failed' + const isPartial = result?.severity === 'warning' && result?.canRepair + const isError = ssoSetup.isError || result?.severity === 'failed' || (result?.severity === 'warning' && !result?.canRepair) return ( @@ -109,6 +110,20 @@ export const SsoMigrationDialog = ({ meData }) => { {result.message} + ) : isPartial ? ( + + + App created — secret creation failed + + + The CIPP-SSO app registration ({result.appId}) was created successfully, but the + client secret could not be generated. The app ID is saved. + + + Open Advanced → Super Admin → SSO and click{' '} + Repair to finish setup. + + ) : isError ? ( {result?.message || ssoSetup.error?.message || 'SSO setup failed. It will be retried automatically.'} diff --git a/src/components/CippSettings/CippSSOSettings.jsx b/src/components/CippSettings/CippSSOSettings.jsx index e22cd1ab6edf..5499b4e694d1 100644 --- a/src/components/CippSettings/CippSSOSettings.jsx +++ b/src/components/CippSettings/CippSSOSettings.jsx @@ -1,10 +1,9 @@ -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { Alert, Button, CardActions, CardContent, - CardHeader, Chip, Divider, Skeleton, @@ -20,16 +19,16 @@ import { CippApiResults } from "../CippComponents/CippApiResults"; const statusLabels = { none: { label: "Not Configured", color: "default" }, - app_created: { label: "App Created", color: "info" }, - appid_stored: { label: "App ID Stored", color: "info" }, + app_created: { label: "App Created — Secret Pending", color: "warning" }, + appid_stored: { label: "App ID Stored — Secret Pending", color: "warning" }, secrets_stored: { label: "Secrets Stored", color: "success" }, complete: { label: "Complete", color: "success" }, error: { label: "Error", color: "error" }, }; -export const CippSSOSettings = () => { - const [showCreate, setShowCreate] = useState(false); +const repairableStatuses = new Set(["error", "app_created", "appid_stored"]); +export const CippSSOSettings = () => { const formControl = useForm({ mode: "onChange", defaultValues: { multiTenant: false }, @@ -49,32 +48,82 @@ export const CippSSOSettings = () => { if (ssoStatus.isSuccess && ssoStatus.data?.Results) { const data = ssoStatus.data.Results; formControl.reset({ multiTenant: data.multiTenant ?? false }); - setShowCreate(!data.configured); } }, [ssoStatus.isSuccess, ssoStatus.data]); - const handleUpdate = () => { + const data = ssoStatus.data?.Results; + const statusKey = data?.status ?? "none"; + const statusInfo = statusLabels[statusKey] ?? statusLabels.none; + const hasAppId = Boolean(data?.appId); + // Server-provided canRepair is authoritative when present; fall back to local heuristic. + const canRepair = + data?.canRepair ?? + (hasAppId && repairableStatuses.has(statusKey)); + const isProvisioned = + statusKey === "complete" || (statusKey === "secrets_stored" && hasAppId); + // Show "Create SSO App" whenever there isn't a working app AND there's nothing to repair — + // covers fresh installs AND legacy broken installs where the AppId was never persisted + // (the original "Failed to create client secret after 5 attempts" bug). + const showCreate = !isProvisioned && !canRepair; + const isOrphanedError = statusKey === "error" && !hasAppId; + + const handleCreate = () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { + Action: "Create", + multiTenant: formControl.getValues("multiTenant"), + }, + }); + }; + + const handleRepair = () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { Action: "Repair" }, + }); + }; + + const handleRecreate = () => { if ( !window.confirm( - "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?" + "Recreate will clear the current SSO record and provision a brand new CIPP-SSO app. The previous app registration will be left in your Entra tenant (you can delete it manually). Continue?" ) ) { return; } - ssoAction.mutate({ - url: "/api/ExecSSOSetup", - data: { - Action: "Update", - multiTenant: formControl.getValues("multiTenant"), + // Clear first, then create. ApiPostCall chains via the success refetch — call sequentially. + ssoAction.mutate( + { + url: "/api/ExecSSOSetup", + data: { Action: "Recreate" }, }, - }); + { + onSuccess: () => { + ssoAction.mutate({ + url: "/api/ExecSSOSetup", + data: { + Action: "Create", + multiTenant: formControl.getValues("multiTenant"), + }, + }); + }, + } + ); }; - const handleCreate = () => { + const handleUpdate = () => { + if ( + !window.confirm( + "Updating SSO settings will restart the CIPP instance. Changes may take up to 60 seconds to reflect. Do you want to continue?" + ) + ) { + return; + } ssoAction.mutate({ url: "/api/ExecSSOSetup", data: { - Action: "Create", + Action: "Update", multiTenant: formControl.getValues("multiTenant"), }, }); @@ -87,9 +136,6 @@ export const CippSSOSettings = () => { }); }; - const data = ssoStatus.data?.Results; - const statusInfo = statusLabels[data?.status] ?? statusLabels.none; - return ( @@ -141,13 +187,38 @@ export const CippSSOSettings = () => { )} {data?.lastError && ( - <> - - - {data.lastError} - - - + + + + {canRepair + ? "Setup did not finish" + : isOrphanedError + ? "Previous setup failed" + : "Error"} + + {data.lastError} + {canRepair && ( + + The app registration ({data.appId}) was created successfully but the + client secret could not be generated. Click Repair to + retry the secret on the existing app, or Recreate to + start over with a fresh app registration. + + )} + {isOrphanedError && ( + + A previous attempt to set up SSO did not save an App ID, so there's + nothing to repair. An orphaned CIPP-SSO app + registration may exist in your Entra tenant — you can delete it + manually. Click Create SSO App to provision a fresh + app registration. + + )} + + )} @@ -158,6 +229,7 @@ export const CippSSOSettings = () => { name="multiTenant" label="Multi-tenant mode (allow users from multiple Entra ID tenants)" formControl={formControl} + disabled={!isProvisioned && !showCreate} /> @@ -167,7 +239,7 @@ export const CippSSOSettings = () => { {!ssoStatus.isLoading && ( - {showCreate ? ( + {showCreate && ( - ) : ( + )} + + {canRepair && ( + <> + + + + )} + + {isProvisioned && ( <> + + {needsSync && ( + + No cached data found for this tenant yet. Click "Sync data" to collect the Intune + and Entra datasets; the report populates once the sync completes. + + )} + + + , + name: 'AI Tools Detected', + data: `${summary.aiToolsDetected ?? 0}`, + }, + { + icon: , + name: 'Device Installs', + data: `${summary.deviceInstalls ?? 0}`, + }, + { + icon: , + name: 'AI Apps in Entra', + data: `${summary.consentedAiApps ?? 0}`, + }, + { + icon: , + name: 'High-Risk AI Tools', + data: `${summary.highRiskTools ?? 0}`, + color: 'error', + }, + ]} + /> + + + {showCharts && ( + <> + + item.category)} + chartSeries={byCategory.map((item) => item.tools)} + totalLabel="Tools" + /> + + + item.tool)} + chartSeries={topTools.map((item) => item.footprint)} + totalLabel="Devices + Users" + /> + + + item.risk)} + chartSeries={byRisk.map((item) => item.tools)} + totalLabel="Tools" + /> + + + )} + + + + + + + , + customComponent: (row, { drawerVisible, setDrawerVisible }) => ( + + ), + multiPost: false, + }, + ]} + data={data.consentedApps ?? []} + simpleColumns={[ + 'application', + 'aiTool', + 'category', + 'risk', + 'applicationId', + 'approvedPermissions', + 'signInsLast7Days', + 'activeUsersLast7Days', + 'firstConsentedDateTime', + ]} + /> + + + + + )} + + + ) +} + +Page.getLayout = (page) => {page} + +export default Page From 482b15e2dbffdb7a39523cfe6cf3af8256987606 Mon Sep 17 00:00:00 2001 From: Bobby <31723128+kris6673@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:00:58 +0200 Subject: [PATCH 132/133] refactor: change default to private when creating teams Fixes #6168 --- src/pages/teams-share/teams/list-team/add.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/teams-share/teams/list-team/add.jsx b/src/pages/teams-share/teams/list-team/add.jsx index fe625e628eea..bc2f64c4f3d2 100644 --- a/src/pages/teams-share/teams/list-team/add.jsx +++ b/src/pages/teams-share/teams/list-team/add.jsx @@ -17,7 +17,7 @@ const TeamsAddTeamForm = () => { displayName: "", description: "", owner: null, - visibility: "public", + visibility: "private", }, }); From b83689b2ca79eb0c20409d9f0265f1f3204908f2 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Thu, 11 Jun 2026 18:20:55 +0200 Subject: [PATCH 133/133] fixes required props --- src/data/standards.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data/standards.json b/src/data/standards.json index 9139734f7aa1..a698010ce141 100644 --- a/src/data/standards.json +++ b/src/data/standards.json @@ -8001,12 +8001,14 @@ "options": [ { "label": "Allow listed AAGUIDs only", "value": "allow" }, { "label": "Block listed AAGUIDs", "value": "block" } - ] + ], + "required": false }, { "type": "textField", "name": "standards.FIDO2PasskeyProfiles.AAGUIDs", - "label": "AAGUIDs (comma-separated list of authenticator AAGUIDs)" + "label": "AAGUIDs (comma-separated list of authenticator AAGUIDs)", + "required": false } ], "label": "Configure FIDO2 Passkey Profile",