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 (
+
+ )
+}
+
+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={
+
+ }>
+ Sync DEP
+
+
+ }
+ />
+
+
+
+ >
+ )
+}
+
+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) => {
>
- );
-};
+ )
+}
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 (
+
+ )
+}
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 (
+
+ )
+}
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={
@@ -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 = () => {
+ 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 */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+ }
+ variant="outlined"
+ size="small"
+ disabled={disabled}
+ onClick={() =>
+ append({
+ _type: null,
+ displayName: "",
+ })
+ }
+ >
+ Add Named Location
+
+
+
+ );
+}
+
// ---------------------------------------------------------------------------
// 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 (