diff --git a/package.json b/package.json
index a6f3527b81e2..6fadedda3a77 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "cipp",
- "version": "10.9.0",
+ "version": "10.9.1",
"author": "CIPP Contributors",
"homepage": "https://cipp.app/",
"bugs": {
@@ -60,7 +60,7 @@
"@tiptap/extension-table": "^3.20.5",
"@tiptap/pm": "^3.29.2",
"@tiptap/react": "^3.20.5",
- "@tiptap/starter-kit": "^3.20.5",
+ "@tiptap/starter-kit": "^3.29.2",
"@vvo/tzdb": "^6.198.0",
"apexcharts": "6.6.1",
"axios": "1.18.1",
@@ -91,7 +91,7 @@
"react-apexcharts": "2.1.1",
"react-beautiful-dnd": "13.1.1",
"react-dom": "19.2.8",
- "react-dropzone": "15.0.0",
+ "react-dropzone": "20.0.0",
"react-error-boundary": "^6.1.2",
"react-hook-form": "^7.76.1",
"react-hot-toast": "2.6.0",
@@ -140,7 +140,7 @@
"msw-storybook-addon": "3.0.0",
"playwright": "1.59.1",
"prettier": "^3.9.6",
- "storybook": "10.3.5",
+ "storybook": "10.5.7",
"typescript": "5.9.3",
"vite": "7.3.6",
"vitest": "4.1.10"
diff --git a/public/secureScore.json b/public/secureScore.json
index 3e59bbd95d5d..9a3bf87d4774 100644
--- a/public/secureScore.json
+++ b/public/secureScore.json
@@ -49,7 +49,7 @@
"vendor": "Microsoft"
},
"id": "aad_limited_administrative_roles",
- "title": "Ensure \u0027Microsoft Azure Management\u0027 is limited to administrative roles"
+ "title": "Ensure 'Microsoft Azure Management' is limited to administrative roles"
},
{
"service": "AzureAD",
@@ -62,7 +62,7 @@
"vendor": "Microsoft"
},
"id": "aad_linkedin_connection_disables",
- "title": "Ensure \u0027LinkedIn account connections\u0027 is disabled"
+ "title": "Ensure 'LinkedIn account connections' is disabled"
},
{
"service": "AzureAD",
@@ -101,7 +101,7 @@
"vendor": "Microsoft"
},
"id": "aad_phishing_MFA_strength",
- "title": "Ensure \u0027Phishing-resistant MFA strength\u0027 is required for Administrators"
+ "title": "Ensure 'Phishing-resistant MFA strength' is required for Administrators"
},
{
"service": "AzureAD",
@@ -129,6 +129,19 @@
"id": "aad_third_party_apps",
"title": "Ensure third party integrated applications are not allowed"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "high",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_AccountsInOperatorGroups",
+ "title": "Locate accounts in built-in Operator Groups"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -296,7 +309,7 @@
"vendor": "Microsoft"
},
"id": "AATP_ADCSMisconfiguredRpcEnrollmentSigning",
- "title": "Enforce encryption for RPC certificate enrollment interface (ESC8)"
+ "title": "Enforce encryption for RPC certificate enrollment interface (ESC11)"
},
{
"service": "Azure ATP",
@@ -441,19 +454,6 @@
"id": "AATP_CyberArk_HighNumberOfSystemAdmins",
"title": "Limit the number of CyberArk Identity accounts with system admin role"
},
- {
- "service": "Azure ATP",
- "tier": "Core",
- "userImpact": "Medium",
- "vendorInformation": {
- "provider": "SecureScore",
- "providerVersion": null,
- "subProvider": null,
- "vendor": "Microsoft"
- },
- "id": "AATP_CyberArk_MFAforPriviledgedUserAccounts",
- "title": "Assign multi-factor authentication for CyberArk Identity privileged user accounts "
- },
{
"service": "Azure ATP",
"tier": "Core",
@@ -532,6 +532,19 @@
"id": "AATP_DormantAccounts",
"title": "Remove dormant accounts from sensitive groups"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "high",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_EnsureNoPrivilegedSaasAppAccountsExistOutsideOfIdpControl",
+ "title": "Ensure no privileged SaaS apps accounts exist outside of IDP control"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -610,6 +623,19 @@
"id": "AATP_ExposedPasswordsInADAttributes",
"title": "Remove discoverable passwords in Active Directory account attributes"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "high",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_ExternalAndGuestUsersWithPrivilegedRoles",
+ "title": "Ensure all External and Guest accounts are not granted privileged roles"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -714,6 +740,32 @@
"id": "AATP_NonAdminDCSyncAccounts",
"title": "Remove non-admin accounts with DCSync permissions "
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "high",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_NonAdminWithPasswordResetOnSensitiveGroups",
+ "title": "Ensure non-admin accounts cannot reset passwords for sensitive groups"
+ },
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "high",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_NonAdminWriteDaclOnSensitiveGroups",
+ "title": "Ensure non-admin identities cannot Write DACL permissions on sensitive groups"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -896,6 +948,19 @@
"id": "AATP_PrivilegedAccountsWithDelegationAllowed",
"title": "Ensure privileged accounts are not delegated"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "High",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_PrivilegedEntraIdAndActiveDirectoryAccounts",
+ "title": "Identify Entra ID privileged accounts that are also privileged in Active Directory"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -909,6 +974,71 @@
"id": "AATP_PwdLAPS",
"title": "Protect and manage local admin passwords with Microsoft LAPS"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "Medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_SailPoint_DormantPriviledgedAccounts",
+ "title": "Remove stale SailPoint privileged accounts "
+ },
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "Medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_SailPoint_HighNumberOfPriviledgedIdentityAccounts",
+ "title": "High number of SailPoint accounts with a privileged role assigned "
+ },
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "Medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_SailPoint_HighNumberOfSystemAdmins",
+ "title": "Limit the number of SailPoint accounts with system admin role "
+ },
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "Medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_SailPoint_MFAforPriviledgedUserAccounts",
+ "title": "Assign multi-factor authentication for SailPoint privileged user accounts "
+ },
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "Medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_SailPoint_PriviledgedUserAccountsWithOldPasswords",
+ "title": "Change password for SailPoint privileged User accounts "
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -922,6 +1052,32 @@
"id": "AATP_Sensor",
"title": "Install Defender for Identity Sensor on all Domain Controllers"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "Medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_ServiceAccountsInPrivilegedGroup",
+ "title": " Identify service accounts in privileged groups"
+ },
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "high",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_ServiceAccountsWithDomainOrGlobalAdminRole",
+ "title": "Ensure service accounts are not assigned Domain Name Admin or Global Admin roles"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -948,6 +1104,19 @@
"id": "AATP_SingleManagedServiceAccountsWithOldPassword",
"title": "Rotate old password for sMSA and set up valid rotation interval in the GPO"
},
+ {
+ "service": "Azure ATP",
+ "tier": "Core",
+ "userImpact": "medium",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "AATP_StaleAccounts",
+ "title": "Remove stale Active Directory accounts"
+ },
{
"service": "Azure ATP",
"tier": "Core",
@@ -1011,7 +1180,7 @@
"vendor": "Microsoft"
},
"id": "admincenter_owned_apps_and_services",
- "title": "Ensure \u0027User owned apps and services\u0027 is restricted"
+ "title": "Ensure 'User owned apps and services' is restricted"
},
{
"service": "AzureAD",
@@ -1102,7 +1271,7 @@
"vendor": "Microsoft"
},
"id": "exo_individualsharing",
- "title": "Ensure \u0027External sharing\u0027 of calendars is not available"
+ "title": "Ensure 'External sharing' of calendars is not available"
},
{
"service": "EXO",
@@ -1388,7 +1557,7 @@
"vendor": "Microsoft"
},
"id": "MDA_CitrixSF_LoginFailLockoutSecs",
- "title": "Enhance \u0027login maximum attempts\u0027 - Lockout timer"
+ "title": "Enhance 'login maximum attempts' - Lockout timer"
},
{
"service": "MDA_CitrixSF",
@@ -1401,7 +1570,7 @@
"vendor": "Microsoft"
},
"id": "MDA_CitrixSF_LoginFailMaxAttempts",
- "title": "Enhance \u0027login maximum attempts\u0027 - Number of attempts"
+ "title": "Enhance 'login maximum attempts' - Number of attempts"
},
{
"service": "MDA_CitrixSF",
@@ -1518,7 +1687,7 @@
"vendor": "Microsoft"
},
"id": "MDA_GitHub_DependencyInsights",
- "title": "Disable \u0027Allow members to view dependency insights\u0027"
+ "title": "Disable 'Allow members to view dependency insights'"
},
{
"service": "MDA_GitHub",
@@ -1531,7 +1700,7 @@
"vendor": "Microsoft"
},
"id": "MDA_GitHub_EmailNotificationRestrictedToVerifiedOrApprovedDomains",
- "title": "Enabled \u0027email notification delivery for this enterprise is restricted to verified or approved domains\u0027"
+ "title": "Enabled 'email notification delivery for this enterprise is restricted to verified or approved domains'"
},
{
"service": "MDA_GitHub",
@@ -1557,7 +1726,7 @@
"vendor": "Microsoft"
},
"id": "MDA_GitHub_OutsideCollabInvitation",
- "title": "Disable \u0027Allow repository administrators to invite outside collaborators to repositories for this organization"
+ "title": "Disable 'Allow repository administrators to invite outside collaborators to repositories for this organization"
},
{
"service": "MDA_GitHub",
@@ -1583,7 +1752,7 @@
"vendor": "Microsoft"
},
"id": "MDA_GitHub_PublicRepoCreation",
- "title": "Disable \u0027Members will be able to create public repositories, visible to anyone\u0027"
+ "title": "Disable 'Members will be able to create public repositories, visible to anyone'"
},
{
"service": "MDA_GitHub",
@@ -1596,7 +1765,7 @@
"vendor": "Microsoft"
},
"id": "MDA_GitHub_RepoTransferOrDeletion",
- "title": "Disable \u0027members with admin permissions for repositories can delete or transfer repositories\u0027"
+ "title": "Disable 'members with admin permissions for repositories can delete or transfer repositories'"
},
{
"service": "MDA_GitHub",
@@ -1609,7 +1778,7 @@
"vendor": "Microsoft"
},
"id": "MDA_GitHub_RepoVisibility_change",
- "title": "Disable \u0027Allow members to change repository visibilities for this organization\u0027"
+ "title": "Disable 'Allow members to change repository visibilities for this organization'"
},
{
"service": "MDA_GitHub",
@@ -2701,7 +2870,7 @@
"vendor": "Microsoft"
},
"id": "mdo_connectionfilter",
- "title": "Don\u0027t add allowed IP addresses in the connection filter policy "
+ "title": "Don't add allowed IP addresses in the connection filter policy "
},
{
"service": "MDO",
@@ -3039,7 +3208,7 @@
"vendor": "Microsoft"
},
"id": "mdo_unusualcharacterssafetytips",
- "title": "Enable the user impersonation unusual characters safety tip "
+ "title": "Enable the user impersonation unusual characters safety tip"
},
{
"service": "MDO",
@@ -3286,7 +3455,7 @@
"vendor": "Microsoft"
},
"id": "PWAgePolicyNew",
- "title": "Ensure the \u0027Password expiration policy\u0027 is set to \u0027Set passwords to never expire (recommended)\u0027"
+ "title": "Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'"
},
{
"service": "AzureAD",
@@ -3301,6 +3470,2385 @@
"id": "RoleOverlap",
"title": "Use least privileged administrative roles"
},
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_100",
+ "title": "Disable JavaScript on Adobe Reader 2015"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_10000",
+ "title": "Disable insecure administration protocol Telnet"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_10001",
+ "title": "Require authentication for Telnet management interface"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_10002",
+ "title": "Remove insecure administration protocols SNMP V1 and SNMP V2"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_10003",
+ "title": "Require authentication for VNC management interface"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_101",
+ "title": "Disable JavaScript on Adobe 2015"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_102",
+ "title": "Enable 'Local Security Authority (LSA) protection' on Windows 11 22h2 and higher"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_103",
+ "title": "Require LDAP client signing to prevent tampering and protect directory authentication"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_104",
+ "title": "Encrypt LDAP client traffic to protect sensitive data in transit"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_105",
+ "title": "Enforce LDAP channel binding to protect authentication sessions from interception"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_106",
+ "title": "Require LDAP server signing to ensure integrity of directory traffic"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_107",
+ "title": "Block outbound network connections from Microsoft HTML Application Host (mshta.exe)"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_108",
+ "title": "Disable Remote Registry Service on Windows"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_109",
+ "title": "Disable NTLM authentication for Windows"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_110",
+ "title": "Block file transfer over RDP"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_111",
+ "title": "SMB server security hardening against authentication relay attacks"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_112",
+ "title": "Ensure devices are updated to Secure Boot 2023 certificates and boot manager"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_113",
+ "title": "Ensure LAPS is enabled on every endpoint and server"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_114",
+ "title": "Reduce unnecessary inbound internet exposure on internet-facing devices"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_115",
+ "title": "Ensure Microsoft Vulnerable Driver Blocklist is enabled"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_118",
+ "title": "scid_118"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_15",
+ "title": "Enable Automatic Updates"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_16",
+ "title": "Enable 'Hide Option to Enable or Disable Updates'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_17",
+ "title": "Disable 'Allow running plugins that are outdated'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_19",
+ "title": "Disable 'Continue running background apps when Google Chrome is closed'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_20",
+ "title": "Disable 'AutoFill'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2000",
+ "title": "Turn on Microsoft Defender for Endpoint sensor"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_20000",
+ "title": "Onboard devices to Microsoft Defender for Endpoint"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2001",
+ "title": "Fix Microsoft Defender for Endpoint sensor data collection"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2002",
+ "title": "Fix Microsoft Defender for Endpoint impaired communications"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2003",
+ "title": "Turn on Tamper Protection"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2004",
+ "title": "Enable EDR in block mode"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2010",
+ "title": "Turn on Microsoft Defender Antivirus"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2011",
+ "title": "Update Microsoft Defender Antivirus definitions"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2012",
+ "title": "Turn on real-time protection"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2013",
+ "title": "Turn on PUA protection in block mode"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2014",
+ "title": "Fix Windows Defender Antivirus cloud service connectivity"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2016",
+ "title": "Enable cloud-delivered protection"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2020",
+ "title": "Turn on all system-level Exploit protection settings"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2021",
+ "title": "Set controlled folder access to enabled or audit mode"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2030",
+ "title": "Update Microsoft Defender for Endpoint core components"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2060",
+ "title": "Set Microsoft Defender SmartScreen app and file checking to block or warn"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2061",
+ "title": "Set Microsoft Defender SmartScreen Microsoft Edge site and download checking to block or warn"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2070",
+ "title": "Turn on Microsoft Defender Firewall"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2071",
+ "title": "Secure Microsoft Defender Firewall domain profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2072",
+ "title": "Secure Microsoft Defender firewall private profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2073",
+ "title": "Secure Microsoft Defender Firewall public profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2080",
+ "title": "Turn on Microsoft Defender Credential Guard"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2090",
+ "title": "Encrypt all BitLocker-supported drives"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2091",
+ "title": "Resume BitLocker protection on all drives"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2093",
+ "title": "Ensure BitLocker drive compatibility"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_21",
+ "title": "Block webpages from automatically running Flash plugins"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2100",
+ "title": "Enable UEFI Secure Boot mode"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_22",
+ "title": "Disable 'Password Manager'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_23",
+ "title": "Enable 'Block third party cookies'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_24",
+ "title": "Set 'Remote Desktop security level' to 'TLS'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_25",
+ "title": "Enable 'Local Security Authority (LSA) protection'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2500",
+ "title": "Block executable content from email client and webmail"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2501",
+ "title": "Block all Office applications from creating child processes"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2502",
+ "title": "Block Office applications from creating executable content"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2503",
+ "title": "Block Office applications from injecting code into other processes"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2504",
+ "title": "Block JavaScript or VBScript from launching downloaded executable content"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2505",
+ "title": "Block execution of potentially obfuscated scripts"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2506",
+ "title": "Block Win32 API calls from Office macros"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2507",
+ "title": "Block executable files from running unless they meet a prevalence, age, or trusted list criterion"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2508",
+ "title": "Use advanced protection against ransomware"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2509",
+ "title": "Block credential stealing from the Windows local security authority subsystem (lsass.exe)"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2510",
+ "title": "Block process creations originating from PSExec and WMI commands"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2511",
+ "title": "Block untrusted and unsigned processes that run from USB"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2512",
+ "title": "Block Office communication application from creating child processes"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2513",
+ "title": "Block Adobe Reader from creating child processes"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2514",
+ "title": "Block persistence through WMI event subscription"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2515",
+ "title": "Block abuse of exploited vulnerable signed drivers"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2516",
+ "title": "Block Webshell creation for Servers"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2517",
+ "title": "Block use of copied or impersonated system tools"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_2518",
+ "title": "Block rebooting machine in Safe Mode"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_26",
+ "title": "Enable 'Safe DLL Search Mode'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_27",
+ "title": "Set User Account Control (UAC) to automatically deny elevation requests"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_28",
+ "title": "Set 'Interactive logon: Machine inactivity limit' to '1-900 seconds'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_29",
+ "title": "Disable 'Enumerate administrator accounts on elevation'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_30",
+ "title": "Disable 'Insecure guest logons' in SMB"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_3001",
+ "title": "Fix unquoted service path for Windows services"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_3002",
+ "title": "Change service executable path to a common protected location"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_3003",
+ "title": "Change service account to avoid cached password in windows registry"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_3010",
+ "title": "Disable the built-in Administrator account"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_3011",
+ "title": "Disable the built-in Guest account"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_32",
+ "title": "Set 'Minimum password length' to '14 or more characters'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_33",
+ "title": "Set 'Enforce password history' to '24 or more password(s)'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_34",
+ "title": "Set 'Maximum password age' to '60 or fewer days, but not 0'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_35",
+ "title": "Set 'Minimum password age' to '1 or more day(s)'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_36",
+ "title": "Enable 'Domain member: Require strong (Windows 2000 or later) session key'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_37",
+ "title": "Enable 'Domain member: Digitally encrypt or sign secure channel data (always)'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_38",
+ "title": "Enable Set 'Domain member: Digitally encrypt secure channel data (when possible)'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_39",
+ "title": "Enable 'Domain member: Digitally sign secure channel data (when possible)'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_40",
+ "title": "Disable 'Domain member: Disable machine account password changes'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_4000",
+ "title": "Disallow offline access to shares"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_4001",
+ "title": "Remove share write permission set to Everyone"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_4002",
+ "title": "Remove shares from the root folder"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_4003",
+ "title": "Set folder access-based enumeration for shares"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_41",
+ "title": "Set 'Account lockout duration' to 15 minutes or more"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_42",
+ "title": "Set 'Reset account lockout counter after' to 15 minutes or more"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_43",
+ "title": "Disable Microsoft Defender Firewall notifications when programs are blocked for Domain profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_44",
+ "title": "Set 'Account lockout threshold' to 1-10 invalid login attempts"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_45",
+ "title": "Set user authentication for remote connections by using Network Level Authentication to 'Enabled'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_46",
+ "title": "Disable Microsoft Defender Firewall notifications when programs are blocked for Private profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_49",
+ "title": "Disable Microsoft Defender Firewall notifications when programs are blocked for Public profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_50",
+ "title": "Disable merging of local Microsoft Defender Firewall rules with group policy firewall rules for the Public profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5001",
+ "title": "Fix Microsoft Defender for Endpoint sensor data collection in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5002",
+ "title": "Fix Microsoft Defender for Endpoint impaired communications in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5003",
+ "title": "Set minimum password length to 15 or more characters in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5004",
+ "title": "Set 'Enforce password history' to '24 or more password(s)' in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5005",
+ "title": "Set 'Maximum password age' to '90 or fewer days, but not 0' in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5006",
+ "title": "Set account lockout threshold to 5 or lower in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5007",
+ "title": "Turn on Firewall in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5009",
+ "title": "Enable Gatekeeper in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5010",
+ "title": "Enable System Integrity Protection (SIP) in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5011",
+ "title": "Enable FileVault Disk Encryption in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5013",
+ "title": "Ensure screensaver is set to start in 20 minutes or less in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5014",
+ "title": "Secure Home Folders in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5090",
+ "title": "Turn on Microsoft Defender Antivirus real-time protection in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5091",
+ "title": "Turn on Microsoft Defender Antivirus PUA protection in block mode in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5092",
+ "title": "Turn on Tamper Protection for MacOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5093",
+ "title": "Enable Microsoft Defender Antivirus real-time behavior monitoring in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5094",
+ "title": "Enable Microsoft Defender Antivirus cloud-delivered protection in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_5095",
+ "title": "Update Microsoft Defender Antivirus definitions in macOS"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_51",
+ "title": "Disable merging of local Microsoft Defender Firewall connection rules with group policy firewall rules for the Public profile"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_52",
+ "title": "Enable 'Apply UAC restrictions to local accounts on network logons'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_53",
+ "title": "Disable SMBv1 client driver"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_54",
+ "title": "Disable SMBv1 server"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_55",
+ "title": "Disable 'Network access: Let Everyone permissions apply to anonymous users'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_57",
+ "title": "Disable 'WDigest Authentication'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_58",
+ "title": "Disable 'Installation and configuration of Network Bridge on your DNS domain network'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_59",
+ "title": "Enable 'Require domain users to elevate when setting a network's location'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_60",
+ "title": "Prohibit use of Internet Connection Sharing on your DNS domain network"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6001",
+ "title": "Fix Microsoft Defender for Endpoint sensor data collection for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6002",
+ "title": "Fix Microsoft Defender for Endpoint impaired communications for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6014",
+ "title": "Unrestricted Access Accounts for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6090",
+ "title": "Turn on Microsoft Defender Antivirus real-time protection for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6091",
+ "title": "Turn on Microsoft Defender Antivirus PUA protection in block mode for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6092",
+ "title": "Turn on Microsoft Defender Antivirus Tamper Protection for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6093",
+ "title": "Enable Microsoft Defender Antivirus real-time behavior monitoring for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6094",
+ "title": "Enable Microsoft Defender Antivirus cloud-delivered protection for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6095",
+ "title": "Update Microsoft Defender Antivirus definitions for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_61",
+ "title": "Set 'Minimum PIN length for startup' to '6 or more characters'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6100",
+ "title": "Enable 'Microsoft Defender for Endpoint Plug-in for WSL'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_6101",
+ "title": "Turn off custom kernel/commandline in Windows Subsystem for Linux"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_62",
+ "title": "Enable 'Require additional authentication at startup'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_63",
+ "title": "Disable 'Configure Offer Remote Assistance'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_64",
+ "title": "Restrict anonymous access to named pipes and Shares"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_65",
+ "title": "Disable 'Store LAN Manager hash value on next password change'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_66",
+ "title": "Disable 'Always install with elevated privileges'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_67",
+ "title": "Disable 'Autoplay for non-volume devices'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_68",
+ "title": "Disable 'Anonymous enumeration of SAM accounts'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_69",
+ "title": "Disable 'Autoplay' for all drives"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_70",
+ "title": "Set default behavior for 'AutoRun' to 'Enabled: Do not execute any autorun commands'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_71",
+ "title": "Enable 'Limit local account use of blank passwords to console logon only'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_72",
+ "title": "Set LAN Manager authentication level to 'Send NTLMv2 response only. Refuse LM & NTLM'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_73",
+ "title": "Disable 'Allow Basic authentication' for WinRM Client"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_74",
+ "title": "Disable 'Allow Basic authentication' for WinRM Service"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_75",
+ "title": "Disable Flash on Adobe Reader DC"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_76",
+ "title": "Disable JavaScript on Adobe Reader DC"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_77",
+ "title": "Disable Flash on Adobe Acrobat Pro XI"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_78",
+ "title": "Disable JavaScript on Adobe Acrobat Pro XI"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_79",
+ "title": "Disable running or installing downloaded software with invalid signature"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_80",
+ "title": "Block Flash activation in Office documents"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_81",
+ "title": "Set IPv6 source routing to highest protection"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_82",
+ "title": "Disable IP source routing"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_83",
+ "title": "Enable Explorer Data Execution Prevention (DEP)"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_85",
+ "title": "Block outdated ActiveX controls for Internet Explorer"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_87",
+ "title": "Disable Solicited Remote Assistance"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_88",
+ "title": "Disable Anonymous enumeration of shares"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_89",
+ "title": "Enable scanning of removable drives during a full scan"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_9",
+ "title": "Enable 'Local Machine Zone Lockdown Security'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_90",
+ "title": "Enable Microsoft Defender Antivirus email scanning"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_91",
+ "title": "Enable Microsoft Defender Antivirus real-time behavior monitoring"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_92",
+ "title": "Enable Microsoft Defender Antivirus scanning of downloaded files and attachments"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_93",
+ "title": "Disable the local storage of passwords and credentials"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_94",
+ "title": "Disable sending unencrypted password to third-party SMB servers"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_95",
+ "title": "Enable 'Microsoft network client: Digitally sign communications (always)'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_96",
+ "title": "Enable 'Network Protection'"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_97",
+ "title": "Disable JavaScript on Adobe DC"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_98",
+ "title": "Disable JavaScript on Adobe Reader 2017"
+ },
+ {
+ "service": "MDATP",
+ "tier": "Core",
+ "userImpact": "Unknown",
+ "vendorInformation": {
+ "provider": "SecureScore",
+ "providerVersion": null,
+ "subProvider": null,
+ "vendor": "Microsoft"
+ },
+ "id": "scid_99",
+ "title": "Disable JavaScript on Adobe Acrobat 2017"
+ },
{
"service": "AzureAD",
"tier": "Defense In Depth",
@@ -3312,7 +5860,7 @@
"vendor": "Microsoft"
},
"id": "SelfServicePasswordReset",
- "title": "Ensure \u0027Self service password reset enabled\u0027 is set to \u0027All\u0027"
+ "title": "Ensure 'Self service password reset enabled' is set to 'All'"
},
{
"service": "AzureAD",
@@ -3364,7 +5912,7 @@
"vendor": "Microsoft"
},
"id": "spo_external_users_sharing",
- "title": "Ensure that SharePoint guest users cannot share items they don\u0027t own"
+ "title": "Ensure that SharePoint guest users cannot share items they don't own"
},
{
"service": "SPO",
diff --git a/public/version.json b/public/version.json
index b4196392f644..0453cde03069 100644
--- a/public/version.json
+++ b/public/version.json
@@ -1,3 +1,3 @@
{
- "version": "10.9.0"
-}
+ "version": "10.9.1"
+}
\ No newline at end of file
diff --git a/src/components/CippBaselines/CippBaselineStandardSettings.jsx b/src/components/CippBaselines/CippBaselineStandardSettings.jsx
index abc76cf7926b..5683be389001 100644
--- a/src/components/CippBaselines/CippBaselineStandardSettings.jsx
+++ b/src/components/CippBaselines/CippBaselineStandardSettings.jsx
@@ -30,20 +30,33 @@ export const CippBaselineStandardSettings = ({
}) => {
const variableEntries = Object.entries(standard?.variables ?? {})
- // Seed each field: current applied value > recommended > default. Api-driven
+ // Seed value per field: current applied value > recommended > default. Api-driven
// autoCompletes (e.g. the CA template picker) seed with a bare value; the
// autocomplete resolves the label once its option list loads.
+ const resolveSeed = (key, definition) => {
+ const seed =
+ initialValues?.[key] ?? definition.recommended ?? definition.default
+ if (seed === undefined) return undefined
+ return definition.type === 'autoComplete'
+ ? (definition.options?.find((option) => option.value === seed) ?? seed)
+ : seed
+ }
+
+ // The seed is ALSO passed as each field's defaultValue below - that is the load-bearing
+ // path. CippFormComponent's Controllers register with defaultValue '' during render, so
+ // by the time this effect runs on a lazily-mounted details pane (accordion expand), the
+ // form already holds '' for every field and a plain undefined-check never seeds. That is
+ // exactly how a saved baseline's variables rendered blank in the editor. The effect stays
+ // for values a Controller default cannot reach (a form reset that wipes mounted fields):
+ // it re-seeds untouched empties but never overwrites a value the operator typed.
useEffect(() => {
variableEntries.forEach(([key, definition]) => {
const name = `${namePrefix}.${key}`
- const seed =
- initialValues?.[key] ?? definition.recommended ?? definition.default
- if (formControl.getValues(name) === undefined && seed !== undefined) {
- const seedValue =
- definition.type === 'autoComplete'
- ? (definition.options?.find((option) => option.value === seed) ??
- seed)
- : seed
+ const seedValue = resolveSeed(key, definition)
+ if (seedValue === undefined || seedValue === '') return
+ const currentValue = formControl.getValues(name)
+ const untouched = !formControl.getFieldState(name).isDirty
+ if (currentValue === undefined || (currentValue === '' && untouched)) {
formControl.setValue(name, seedValue)
}
})
@@ -67,6 +80,9 @@ export const CippBaselineStandardSettings = ({
name={`${namePrefix}.${key}`}
label={definition.label}
formControl={formControl}
+ // Saved/recommended value rides the Controller's own defaultValue so a
+ // lazily-mounted field initializes correctly regardless of effect order.
+ defaultValue={resolveSeed(key, definition)}
options={definition.options}
// Definitions may source options from an API instead of a static list
// (e.g. the CA template picker) - CippFormComponent handles the fetch.
diff --git a/src/components/CippComponents/CippAppRegistrationPermissions.jsx b/src/components/CippComponents/CippAppRegistrationPermissions.jsx
index 09c209bde948..6d432c3d5103 100644
--- a/src/components/CippComponents/CippAppRegistrationPermissions.jsx
+++ b/src/components/CippComponents/CippAppRegistrationPermissions.jsx
@@ -161,8 +161,18 @@ const ResourcePermissionsAccordion = ({
}}
disableGutters
>
- }>
-
+ }
+ // summary is a centered ButtonBase, an unshrinkable row spills both edges
+ sx={{ "& .MuiAccordionSummary-content": { minWidth: 0 } }}
+ >
+
{title}
@@ -180,7 +190,12 @@ const ResourcePermissionsAccordion = ({
/>
)}
-
+
diff --git a/src/components/CippComponents/CippEnterpriseAppPermissions.jsx b/src/components/CippComponents/CippEnterpriseAppPermissions.jsx
index 7538755ecef5..7b283ba99bde 100644
--- a/src/components/CippComponents/CippEnterpriseAppPermissions.jsx
+++ b/src/components/CippComponents/CippEnterpriseAppPermissions.jsx
@@ -191,8 +191,18 @@ const ResourceAccordion = ({ title, resourceId, chipLabel, children, riskSummary
}}
disableGutters
>
- }>
-
+ }
+ // summary is a centered ButtonBase, an unshrinkable row spills both edges
+ sx={{ "& .MuiAccordionSummary-content": { minWidth: 0 } }}
+ >
+
{title}
@@ -210,7 +220,12 @@ const ResourceAccordion = ({ title, resourceId, chipLabel, children, riskSummary
/>
)}
-
+
{chipLabel != null && }
diff --git a/src/components/CippComponents/CippTranslations.jsx b/src/components/CippComponents/CippTranslations.jsx
index 4aab78290cd5..b184235e8072 100644
--- a/src/components/CippComponents/CippTranslations.jsx
+++ b/src/components/CippComponents/CippTranslations.jsx
@@ -122,4 +122,6 @@ export const CippTranslations = {
resellerPartnerDelegatedAdmin: 'Direct Reseller',
valueAddedResellerPartnerDelegatedAdmin: 'Indirect Reseller',
unknownFutureValue: 'Unknown',
+ devicePrepData: 'Corporate Identifiers',
+ overwriteExisting: 'Overwrite Existing Identifiers',
}
diff --git a/src/components/CippTable/CippDataTableButton.jsx b/src/components/CippTable/CippDataTableButton.jsx
index 9c99484e72e9..aa8702a17448 100644
--- a/src/components/CippTable/CippDataTableButton.jsx
+++ b/src/components/CippTable/CippDataTableButton.jsx
@@ -37,7 +37,7 @@ const CippDataTableButton = ({
const settings = useSettings();
const isLive = Boolean(api?.url);
- const nestedTitle = tableProps.title ?? tableTitle ?? title ?? "Data";
+ const nestedTitle = title ?? tableTitle ?? "Data";
const handleOpenStaticDialog = (event) => {
event?.stopPropagation();
diff --git a/src/components/CippTable/CippMobileCardList.jsx b/src/components/CippTable/CippMobileCardList.jsx
index f817177544bd..fa208ae5eb2f 100644
--- a/src/components/CippTable/CippMobileCardList.jsx
+++ b/src/components/CippTable/CippMobileCardList.jsx
@@ -285,6 +285,8 @@ export const CippMobileCardList = (props) => {
display: "inline-flex",
alignItems: "center",
gap: 0.5,
+ // long column names: caption ellipsizes, icon stays inside the card clip
+ maxWidth: "100%",
...(isBareBoolean && {
border: 1,
borderColor: "divider",
diff --git a/src/components/CippWizard/CippWizardAutopilotTypeSelection.jsx b/src/components/CippWizard/CippWizardAutopilotTypeSelection.jsx
new file mode 100644
index 000000000000..1c2bf3ae554a
--- /dev/null
+++ b/src/components/CippWizard/CippWizardAutopilotTypeSelection.jsx
@@ -0,0 +1,131 @@
+import {
+ Avatar,
+ Card,
+ CardContent,
+ Stack,
+ SvgIcon,
+ Typography,
+} from '@mui/material'
+import { useState, useEffect } from 'react'
+import { CippWizardStepButtons } from './CippWizardStepButtons'
+import {
+ IdentificationIcon,
+ RocketLaunchIcon,
+} from '@heroicons/react/24/outline'
+
+export const CippWizardAutopilotTypeSelection = (props) => {
+ const { onNextStep, formControl, currentStep, onPreviousStep } = props
+
+ const [selectedOption, setSelectedOption] = useState(() =>
+ formControl.getValues('deploymentType')
+ )
+
+ // Register the deploymentType field in react-hook-form
+ formControl.register('deploymentType', {
+ required: true,
+ })
+
+ useEffect(() => {
+ if (formControl.getValues('deploymentType')) {
+ formControl.trigger('deploymentType')
+ }
+ }, [formControl])
+
+ const handleOptionClick = (value) => {
+ setSelectedOption(value)
+ formControl.setValue('deploymentType', value)
+
+ // Clear the other path's fields so switching back and forth doesn't submit
+ // stale device data or keep its validation rules active
+ if (value === 'autopilot') {
+ formControl.unregister('devicePrepData')
+ formControl.unregister('overwriteExisting')
+ } else if (value === 'devicePrep') {
+ formControl.unregister('autopilotData')
+ formControl.unregister('GroupName')
+ }
+
+ formControl.trigger()
+ }
+
+ const options = [
+ {
+ value: 'autopilot',
+ label: 'Windows Autopilot',
+ description:
+ 'Upload devices to Windows Autopilot using their serial number, product ID or hardware hash.',
+ icon: ,
+ },
+ {
+ value: 'devicePrep',
+ label: 'Device Preparation (Corporate Identifiers)',
+ description:
+ 'Upload corporate device identifiers (manufacturer, model and serial number) so devices are recognized as corporate-owned and can enroll using Windows Autopilot device preparation.',
+ icon: ,
+ },
+ ]
+
+ return (
+
+
+ Select Deployment Type
+
+ Choose how you want to register the devices for this tenant.
+
+
+
+ {options.map((option) => {
+ const isSelected = selectedOption === option.value
+
+ return (
+ handleOptionClick(option.value)}
+ variant="outlined"
+ sx={{
+ cursor: 'pointer',
+ ...(isSelected && {
+ boxShadow: (theme) =>
+ `0px 0px 0px 2px ${theme.palette.primary.main}`,
+ }),
+ '&:hover': {
+ ...(isSelected ? {} : { boxShadow: 8 }),
+ },
+ }}
+ >
+
+
+
+ {option.icon}
+
+
+ {option.label}
+
+ {option.description}
+
+
+
+
+
+ )
+ })}
+
+
+
+ )
+}
+
+export default CippWizardAutopilotTypeSelection
diff --git a/src/components/CippWizard/CippWizardDevicePrepImport.jsx b/src/components/CippWizard/CippWizardDevicePrepImport.jsx
new file mode 100644
index 000000000000..157b525977b0
--- /dev/null
+++ b/src/components/CippWizard/CippWizardDevicePrepImport.jsx
@@ -0,0 +1,619 @@
+import {
+ Button,
+ Link,
+ Stack,
+ Box,
+ Typography,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Alert,
+ Paper,
+ IconButton,
+} from '@mui/material'
+import { CippWizardStepButtons } from './CippWizardStepButtons'
+import CippFormComponent from '../CippComponents/CippFormComponent'
+import { CippDataTable } from '../CippTable/CippDataTable'
+import { useWatch } from 'react-hook-form'
+import { Delete, FileDownload, Upload, Add } from '@mui/icons-material'
+import { useEffect, useState } from 'react'
+import React from 'react'
+import { useIsMobileLayout } from '../../hooks/use-breakpoint'
+
+// Modified version of CippWizardAutopilotImport for corporate device identifiers
+// (Autopilot device preparation): every device is a manufacturer, model and serial
+// number triplet that Graph combines into a single comma-separated identifier, so
+// all three fields are required and none of them may contain a comma.
+export const CippWizardDevicePrepImport = (props) => {
+ const {
+ onNextStep,
+ formControl,
+ currentStep,
+ onPreviousStep,
+ fields,
+ name,
+ fileName = 'template',
+ } = props
+ const tableData = useWatch({ control: formControl.control, name: name })
+ // Seed from the form so navigating back to this step keeps the imported rows
+ const [newTableData, setTableData] = useState(
+ () => formControl.getValues(name) || []
+ )
+ const fileInputRef = React.useRef(null)
+ const [manualDialogOpen, setManualDialogOpen] = useState(false)
+ const [manualInputs, setManualInputs] = useState([{}])
+ const inputRefs = React.useRef([])
+ const isMobile = useIsMobileLayout()
+ const [validationErrors, setValidationErrors] = useState([])
+ const [importErrors, setImportErrors] = useState([])
+
+ // At least one identifier is needed before the wizard can continue
+ formControl.register(name, {
+ validate: (value) => Array.isArray(value) && value.length > 0,
+ })
+
+ const handleRemoveItem = (row) => {
+ if (row === undefined) return false
+ const index = tableData?.findIndex((item) => item === row)
+ const newTableData = [...tableData]
+ newTableData.splice(index, 1)
+ setTableData(newTableData)
+ }
+
+ const collectRowErrors = (rows) => {
+ const errors = []
+ const seenIdentifiers = new Set()
+
+ rows.forEach((row, index) => {
+ const missingFields = fields.filter(
+ (field) =>
+ !row[field.propertyName] || row[field.propertyName].trim() === ''
+ )
+ if (missingFields.length > 0) {
+ errors.push(
+ `Row ${index + 1}: ${missingFields.map((f) => f.friendlyName).join(', ')} ${
+ missingFields.length === 1 ? 'is' : 'are'
+ } required`
+ )
+ return
+ }
+
+ const commaFields = fields.filter((field) =>
+ row[field.propertyName].includes(',')
+ )
+ if (commaFields.length > 0) {
+ errors.push(
+ `Row ${index + 1}: ${commaFields
+ .map((f) => f.friendlyName)
+ .join(', ')} may not contain a comma`
+ )
+ return
+ }
+
+ const identifier = fields
+ .map((field) => row[field.propertyName].trim().toLowerCase())
+ .join(',')
+ if (seenIdentifiers.has(identifier)) {
+ errors.push(`Row ${index + 1}: Duplicate device "${identifier}"`)
+ }
+ seenIdentifiers.add(identifier)
+ })
+
+ return errors
+ }
+
+ const handleFileSelect = (event) => {
+ const file = event.target.files[0]
+ if (file) {
+ const reader = new FileReader()
+ reader.onload = (e) => {
+ const text = e.target.result
+ const lines = text.split('\n')
+ const firstLine = lines[0].split(',').map((header) => header.trim())
+
+ // Check if this is a headerless CSV (no recognizable headers). The Intune
+ // portal's corporate identifier CSV has no header row.
+ const hasHeaders = firstLine.some((header) => {
+ return fields.some(
+ (field) =>
+ header === field.propertyName ||
+ header === field.friendlyName ||
+ (field.alternativePropertyNames &&
+ field.alternativePropertyNames.includes(header))
+ )
+ })
+
+ let headers, headerMapping
+
+ if (hasHeaders) {
+ headers = firstLine
+
+ // Create mapping for property names and alternative property names
+ headerMapping = {}
+ fields.forEach((field) => {
+ headerMapping[field.propertyName] = field.propertyName
+ headerMapping[field.friendlyName] = field.propertyName
+ if (field.alternativePropertyNames) {
+ field.alternativePropertyNames.forEach((altName) => {
+ headerMapping[altName] = field.propertyName
+ })
+ }
+ })
+
+ // All three columns are required for corporate identifiers
+ const missingColumns = fields.filter((field) => {
+ const hasPropertyName = headers.includes(field.propertyName)
+ const hasFriendlyName = headers.includes(field.friendlyName)
+ const hasAlternativeName = field.alternativePropertyNames
+ ? field.alternativePropertyNames.some((altName) =>
+ headers.includes(altName)
+ )
+ : false
+ return !hasPropertyName && !hasFriendlyName && !hasAlternativeName
+ })
+
+ if (missingColumns.length > 0) {
+ const missingFormats = missingColumns
+ .map((f) => {
+ const formats = [f.propertyName, f.friendlyName]
+ if (f.alternativePropertyNames) {
+ formats.push(...f.alternativePropertyNames)
+ }
+ return `"${formats.join('" or "')}"`
+ })
+ .join(', ')
+ setImportErrors([
+ `CSV is missing required columns: ${missingFormats}`,
+ ])
+ return
+ }
+ } else {
+ // Headerless CSV - assume the Intune portal order: manufacturer, model, serial number
+ headers = fields.map((field) => field.propertyName)
+ headerMapping = {}
+ headers.forEach((header) => {
+ headerMapping[header] = header
+ })
+
+ if (firstLine.length < fields.length) {
+ setImportErrors([
+ `Headerless CSV must have ${fields.length} columns in order: ${fields
+ .map((f) => f.friendlyName)
+ .join(', ')}`,
+ ])
+ return
+ }
+ }
+
+ const data = lines
+ .slice(hasHeaders ? 1 : 0) // Skip first line only if it has headers
+ .filter((line) => line.trim() !== '') // Remove empty lines
+ .map((line) => {
+ const values = line.split(',')
+ const row = fields.reduce((obj, field) => {
+ obj[field.propertyName] = ''
+ return obj
+ }, {})
+ headers.forEach((header, i) => {
+ const propertyName = headerMapping[header]
+ if (propertyName) {
+ row[propertyName] = values[i]?.trim() || ''
+ }
+ })
+ return row
+ })
+
+ const errors = collectRowErrors(data)
+ if (errors.length > 0) {
+ setImportErrors(errors)
+ return
+ }
+
+ setImportErrors([])
+ setTableData(data)
+ formControl.setValue(name, data, { shouldValidate: true })
+ }
+ reader.readAsText(file)
+ }
+ }
+
+ const handleManualInputChange = (rowIndex, field, value) => {
+ setManualInputs((prev) => {
+ const newInputs = [...prev]
+ if (!newInputs[rowIndex]) {
+ newInputs[rowIndex] = {}
+ }
+ newInputs[rowIndex][field] = value
+ return newInputs
+ })
+ }
+
+ const handleAddRow = () => {
+ setManualInputs((prev) => [...prev, {}])
+ }
+
+ const validateRows = (rows) => {
+ const errors = collectRowErrors(
+ rows.filter((row) =>
+ Object.values(row).some((value) => value && value.trim() !== '')
+ )
+ )
+ setValidationErrors(errors)
+ return errors.length === 0
+ }
+
+ const handleManualAdd = () => {
+ const newRows = manualInputs
+ .filter((row) =>
+ Object.values(row).some((value) => value && value.trim() !== '')
+ )
+ .map((row) => {
+ return fields.reduce((obj, field) => {
+ obj[field.propertyName] = row[field.propertyName] || ''
+ return obj
+ }, {})
+ })
+
+ if (newRows.length === 0) {
+ setManualDialogOpen(false)
+ setManualInputs([{}])
+ return
+ }
+
+ if (!validateRows(newRows)) {
+ return
+ }
+
+ const updatedData = [...(tableData || []), ...newRows]
+ setTableData(updatedData)
+ formControl.setValue(name, updatedData, { shouldValidate: true })
+ setManualInputs([{}])
+ setManualDialogOpen(false)
+ }
+
+ const handleDialogClose = () => {
+ setManualDialogOpen(false)
+ setManualInputs([{}])
+ }
+
+ const lastField = fields[fields.length - 1]
+
+ const handleKeyPress = (event, rowIndex) => {
+ if (
+ event.key === 'Enter' &&
+ manualInputs[rowIndex]?.[lastField.propertyName]
+ ) {
+ if (rowIndex === manualInputs.length - 1) {
+ const newRowIndex = manualInputs.length
+ setManualInputs((prev) => [...prev, {}])
+ // Wait for the next render cycle to set focus
+ setTimeout(() => {
+ const newInput =
+ inputRefs.current[newRowIndex]?.[fields[0].propertyName]
+ if (newInput) {
+ newInput.focus()
+ }
+ }, 0)
+ }
+ }
+ }
+
+ const handleRemoveRow = (rowIndex) => {
+ setManualInputs((prev) => prev.filter((_, index) => index !== rowIndex))
+ }
+
+ useEffect(() => {
+ formControl.setValue(name, newTableData, {
+ shouldValidate: true,
+ })
+ }, [newTableData])
+
+ // Add effect to validate rows when manualInputs changes
+ useEffect(() => {
+ validateRows(manualInputs)
+ }, [manualInputs])
+
+ const actions = [
+ {
+ icon: ,
+ label: 'Delete Row',
+ confirmText: 'Are you sure you want to delete this row?',
+ customFunction: handleRemoveItem,
+ noConfirm: true,
+ },
+ ]
+
+ return (
+
+ {importErrors.length > 0 && (
+ setImportErrors([])}>
+
+ The file could not be imported:
+
+ {importErrors.map((error, index) => (
+
+ • {error}
+
+ ))}
+
+ )}
+ f.propertyName)}
+ cardButton={
+
+
+
+ }
+ onClick={() => fileInputRef.current?.click()}
+ size="small"
+ >
+ Import from CSV
+
+ }
+ onClick={() => setManualDialogOpen(true)}
+ size="small"
+ >
+ Manual Import
+
+
+ }
+ />
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/data/standards.json b/src/data/standards.json
index d4a68dc0e98c..57c45550d2fb 100644
--- a/src/data/standards.json
+++ b/src/data/standards.json
@@ -5173,6 +5173,27 @@
"ONEDRIVE_ENTERPRISE"
]
},
+ {
+ "name": "standards.OneDriveLicensedQuota",
+ "cat": "SharePoint Standards",
+ "tag": [],
+ "helpText": "Raises the OneDrive storage quota to 5 TB for users whose license includes that entitlement. Microsoft provisions every OneDrive at 1 TB and does not apply the licensed entitlement automatically. Users already at or above 5 TB, for example raised further by Microsoft support, are left untouched.",
+ "docsDescription": "Microsoft provisions every OneDrive with a 1 TB quota regardless of license. Users holding OneDrive for Business (Plan 2), SharePoint Online (Plan 2), or a bundle that includes one of these (Microsoft 365/Office 365 E3/E5, A3/A5, G3/G5) are entitled to 5 TB, but an admin has to raise the quota manually. This standard finds enabled users with a qualifying service plan whose OneDrive quota is below 5 TB and raises it to 5 TB, with the warning level at 90%. Users whose quota is already at or above 5 TB, for example increased to 25 TB by Microsoft support, are skipped. Microsoft only permits quotas above 1 TB when the subscription has five or more users on a qualifying plan, so tenants below that threshold are reported as compliant and left unchanged.",
+ "executiveText": "Ensures employees receive the full OneDrive storage their licenses already include. Microsoft grants 5 TB of storage with most enterprise licenses but only provisions 1 TB by default, leaving paid-for capacity unused. Automatically correcting the allocation prevents storage shortages and support tickets without any additional licensing cost.",
+ "addedComponent": [],
+ "label": "Raise OneDrive storage quota for entitled users",
+ "impact": "Low Impact",
+ "impactColour": "info",
+ "addedDate": "2026-08-21",
+ "powershellEquivalent": "Set-SPOSite -Identity https://tenant-my.sharepoint.com/personal/user -StorageQuota 5242880",
+ "recommendedBy": [],
+ "requiredCapabilities": [
+ "SHAREPOINTENTERPRISE",
+ "SHAREPOINTENTERPRISE_EDU",
+ "SHAREPOINTENTERPRISE_GOV",
+ "ONEDRIVEENTERPRISE"
+ ]
+ },
{
"name": "standards.SPFileRequests",
"cat": "SharePoint Standards",
@@ -7808,15 +7829,15 @@
"name": "standards.MessageEncryption",
"cat": "Exchange Standards",
"tag": [],
- "helpText": "Enables Microsoft Purview Message Encryption by turning on Azure RMS licensing for Exchange Online. Skipped with a warning when the tenant still points at an on-premises AD RMS cluster, because AD RMS has to be migrated to Azure RMS first. This standard only turns the feature on: branding, one-time passcodes, and social ID sign-in for encrypted messages are configured in the [Configure Encrypted Message Branding (OME)](https://standards.cipp.app/standards/omebranding) standard. [Read more](https://learn.microsoft.com/en-us/purview/set-up-new-message-encryption-capabilities)",
- "docsDescription": "Sets AzureRMSLicensingEnabled to true, the only prerequisite for Microsoft Purview Message Encryption. Reports the IRM licensing state per tenant, including the licensing location, so you can see at a glance which tenants have message encryption available. Remediation is deliberately skipped for tenants with an on-premises AD RMS licensing location, as Purview Message Encryption is not compatible with AD RMS and those tenants need to be migrated to Azure RMS first.",
+ "helpText": "Enables Microsoft Purview Message Encryption by turning on Azure RMS licensing for Exchange Online, and turns on simplified client access so the Encrypt button appears in Outlook on the web and the new Outlook. Skipped with a warning when the tenant still points at an on-premises AD RMS cluster, because AD RMS has to be migrated to Azure RMS first. This standard only turns the feature on: branding, one-time passcodes, and social ID sign-in for encrypted messages are configured in the [Configure Encrypted Message Branding (OME)](https://standards.cipp.app/standards/omebranding) standard. [Read more](https://learn.microsoft.com/en-us/purview/set-up-new-message-encryption-capabilities)",
+ "docsDescription": "Sets AzureRMSLicensingEnabled to true, the prerequisite for Microsoft Purview Message Encryption, and SimplifiedClientAccessEnabled to true so the Encrypt button appears when composing mail in Outlook on the web and the new Outlook. Reports the IRM licensing state per tenant, including the licensing location, so you can see at a glance which tenants have message encryption available. Remediation is deliberately skipped for tenants with an on-premises AD RMS licensing location, as Purview Message Encryption is not compatible with AD RMS and those tenants need to be migrated to Azure RMS first.",
"executiveText": "Turns on the built-in encryption that lets staff send protected email to anyone, including recipients outside the organization. Uses licensing the organization already owns, removing the need for a separate secure-email product.",
"addedComponent": [],
"label": "Enable Purview Message Encryption",
"impact": "Low Impact",
"impactColour": "info",
"addedDate": "2026-08-04",
- "powershellEquivalent": "Set-IRMConfiguration -AzureRMSLicensingEnabled $true",
+ "powershellEquivalent": "Set-IRMConfiguration -AzureRMSLicensingEnabled $true -SimplifiedClientAccessEnabled $true",
"recommendedBy": [],
"requiredCapabilities": [
"EXCHANGE_S_STANDARD",
diff --git a/src/layouts/HeaderedTabbedLayout.jsx b/src/layouts/HeaderedTabbedLayout.jsx
index 9ab87cf40918..94ef13bcfa42 100644
--- a/src/layouts/HeaderedTabbedLayout.jsx
+++ b/src/layouts/HeaderedTabbedLayout.jsx
@@ -21,6 +21,7 @@ import { useActionsDispatch } from "../hooks/use-actions-dispatch";
import { TabNavigationContext, useTabNavigationValue } from "./tab-navigation-context";
import { CippPageActionsFab } from "../components/CippComponents/CippPageActionsFab";
import { CippTabPicker } from "../components/CippComponents/CippTabPicker";
+import { ApiGetCall } from "../api/ApiCall";
export const HeaderedTabbedLayout = (props) => {
const {
@@ -62,7 +63,30 @@ export const HeaderedTabbedLayout = (props) => {
const handleTabsChange = useCallback((event, value) => navigateToTab(value), [navigateToTab]);
- const currentTab = tabOptions.find((option) => option.path === pathname);
+ // Feature-flag gating, same rules as TabbedLayout: a DISABLED flag hides its Pages;
+ // an ENABLED flag hides its HidesPages (the pages it replaces - e.g. Baselines
+ // supersedes the classic Standards and Drift tabs on Manage Tenant).
+ const featureFlags = ApiGetCall({
+ url: "/api/ListFeatureFlags",
+ queryKey: "featureFlags",
+ staleTime: 600000,
+ });
+ const visibleTabs = useMemo(() => {
+ if (!featureFlags.isSuccess || !Array.isArray(featureFlags.data)) return tabOptions;
+ const disabledPages = featureFlags.data
+ .filter((flag) => flag.Enabled === false || flag.enabled === false)
+ .flatMap((flag) => flag.Pages || flag.pages || [])
+ .filter((page) => typeof page === "string");
+ const replacedPages = featureFlags.data
+ .filter((flag) => flag.Enabled === true || flag.enabled === true)
+ .flatMap((flag) => flag.HidesPages || flag.hidesPages || [])
+ .filter((page) => typeof page === "string");
+ const hiddenPages = [...disabledPages, ...replacedPages];
+ if (hiddenPages.length === 0) return tabOptions;
+ return tabOptions.filter((option) => !hiddenPages.includes(option.path));
+ }, [tabOptions, featureFlags.isSuccess, featureFlags.data]);
+
+ const currentTab = visibleTabs.find((option) => option.path === pathname);
// Below md the tab row scrolls horizontally and still hides tabs off the right edge, so
// navigation collapses to a picker in the title row — the one part of that row that is
@@ -88,7 +112,7 @@ export const HeaderedTabbedLayout = (props) => {
);
const tabNavValue = useTabNavigationValue({
- tabs: tabOptions,
+ tabs: visibleTabs,
currentPath: pathname,
onNavigate: navigateToTab,
actions: sheetActions,
@@ -227,7 +251,7 @@ export const HeaderedTabbedLayout = (props) => {
},
}}
>
- {tabOptions.map((option) => {
+ {visibleTabs.map((option) => {
const icon = getIconByName(option.icon, { fontSize: "small" });
const iconPosition = option.iconPosition ?? "start";
const compactIcon = icon && ["end", "start"].includes(iconPosition);
diff --git a/src/layouts/TabbedLayout.jsx b/src/layouts/TabbedLayout.jsx
index cc89fd11a794..2ac692e10ce9 100644
--- a/src/layouts/TabbedLayout.jsx
+++ b/src/layouts/TabbedLayout.jsx
@@ -30,14 +30,21 @@ export const TabbedLayout = (props) => {
if (!featureFlags.isSuccess || !Array.isArray(featureFlags.data)) return tabs
+ // A DISABLED flag hides its Pages; an ENABLED flag hides its HidesPages (the
+ // pages it replaces - e.g. Baselines supersedes the classic Standards tabs).
const disabledPages = featureFlags.data
.filter((flag) => flag.Enabled === false || flag.enabled === false)
.flatMap((flag) => flag.Pages || flag.pages || [])
.filter((page) => typeof page === 'string')
+ const replacedPages = featureFlags.data
+ .filter((flag) => flag.Enabled === true || flag.enabled === true)
+ .flatMap((flag) => flag.HidesPages || flag.hidesPages || [])
+ .filter((page) => typeof page === 'string')
+ const hiddenPages = [...disabledPages, ...replacedPages]
- if (disabledPages.length === 0) return tabs
+ if (hiddenPages.length === 0) return tabs
- return tabs.filter((option) => !disabledPages.includes(option.path))
+ return tabs.filter((option) => !hiddenPages.includes(option.path))
}, [tabOptions, featureFlags.isSuccess, featureFlags.data, showAdvanced])
const navigateToTab = useCallback(
diff --git a/src/layouts/config.js b/src/layouts/config.js
index 8dab949ae483..c54b951f3ec4 100644
--- a/src/layouts/config.js
+++ b/src/layouts/config.js
@@ -216,42 +216,32 @@ export const nativeMenuItems = [
permissions: ['Tenant.Relationship.*'],
scope: 'global',
},
+ // Flag-gated swap: the Baselines feature flag lists this path in its Pages
+ // (hidden while the flag is off) and the classic Standards/Drift paths in
+ // HidesPages (hidden while it is on) - the two never show together.
+ {
+ title: 'Baselines',
+ path: '/tenant/baselines',
+ permissions: ['Tenant.Baselines.*'],
+ scope: 'global',
+ },
+ {
+ title: 'Domains Analyser',
+ path: '/tenant/standards/domains-analyser',
+ permissions: ['Tenant.DomainAnalyser.*'],
+ scope: 'global',
+ },
{
title: 'Standards & Drift',
- permissions: [
- 'Tenant.Standards.*',
- 'Tenant.Baselines.*',
- 'Tenant.BestPracticeAnalyser.*',
- 'Tenant.DomainAnalyser.*',
- ],
- items: [
- {
- title: 'Standards Management',
- path: '/tenant/standards/alignment',
- permissions: ['Tenant.Standards.*'],
- scope: 'global',
- },
- // Baselines - hidden from the nav for now; reach it directly
- // at /tenant/baselines
- // {
- // title: 'Baselines (Preview)',
- // path: '/tenant/baselines',
- // permissions: ['Tenant.Baselines.*'],
- // scope: 'global',
- // },
- {
- title: 'Best Practice Analyser',
- path: '/tenant/standards/bpa-report',
- permissions: ['Tenant.BestPracticeAnalyser.*'],
- scope: 'global',
- },
- {
- title: 'Domains Analyser',
- path: '/tenant/standards/domains-analyser',
- permissions: ['Tenant.DomainAnalyser.*'],
- scope: 'global',
- },
- ],
+ path: '/tenant/standards/alignment',
+ permissions: ['Tenant.Standards.*'],
+ scope: 'global',
+ },
+ {
+ title: 'Best Practice Analyser',
+ path: '/tenant/standards/bpa-report',
+ permissions: ['Tenant.BestPracticeAnalyser.*'],
+ scope: 'global',
},
{
title: 'Conditional Access',
diff --git a/src/layouts/index.js b/src/layouts/index.js
index 44c33201f056..db2db462a98a 100644
--- a/src/layouts/index.js
+++ b/src/layouts/index.js
@@ -123,20 +123,28 @@ export const Layout = (props) => {
return
}
- // Get disabled pages from feature flags - only filter if we have valid data
- let disabledPages = []
+ // Get hidden pages from feature flags - only filter if we have valid data.
+ // A DISABLED flag hides its Pages (features gated behind the flag); an ENABLED
+ // flag hides its HidesPages (features it replaces - e.g. Baselines supersedes
+ // the classic Standards and Drift pages).
+ let hiddenPages = []
if (featureFlags.isSuccess && Array.isArray(featureFlags.data)) {
- disabledPages = featureFlags.data
+ const disabledPages = featureFlags.data
.filter((flag) => flag.Enabled === false || flag.enabled === false)
.flatMap((flag) => flag.Pages || flag.pages || [])
.filter((page) => typeof page === 'string')
+ const replacedPages = featureFlags.data
+ .filter((flag) => flag.Enabled === true || flag.enabled === true)
+ .flatMap((flag) => flag.HidesPages || flag.hidesPages || [])
+ .filter((page) => typeof page === 'string')
+ hiddenPages = [...disabledPages, ...replacedPages]
}
const filterItemsByRole = (items) => {
return items
.map((item) => {
- // Check if page is disabled by feature flag
- if (item.path && disabledPages.length > 0 && disabledPages.includes(item.path)) {
+ // Check if page is hidden by feature flag
+ if (item.path && hiddenPages.length > 0 && hiddenPages.includes(item.path)) {
return null
}
diff --git a/src/pages/email/tools/message-encryption/index.js b/src/pages/email/tools/message-encryption/index.js
index b32058bf0c82..7fc7c1641d9b 100644
--- a/src/pages/email/tools/message-encryption/index.js
+++ b/src/pages/email/tools/message-encryption/index.js
@@ -34,6 +34,7 @@ const Page = () => {
// pending value against B's tenantFilter, and A's test output would read as B's.
formControl.reset({
AzureRMSLicensingEnabled: false,
+ SimplifiedClientAccessEnabled: false,
Sender: '',
Recipient: '',
})
@@ -46,6 +47,7 @@ const Page = () => {
formControl.reset({
...formControl.getValues(),
AzureRMSLicensingEnabled: !!irm?.AzureRMSLicensingEnabled,
+ SimplifiedClientAccessEnabled: !!irm?.SimplifiedClientAccessEnabled,
})
}
}, [irmRequest.isSuccess, irm])
@@ -78,7 +80,7 @@ const Page = () => {
value: yesNo(irm?.ExternalLicensingEnabled),
},
{
- label: 'Protect Button in Outlook on the Web',
+ label: 'Encrypt Button in Outlook',
value: yesNo(irm?.SimplifiedClientAccessEnabled),
},
{
@@ -110,6 +112,7 @@ const Page = () => {
tenantFilter: tenant,
Action: 'Set',
AzureRMSLicensingEnabled: !!values?.AzureRMSLicensingEnabled,
+ SimplifiedClientAccessEnabled: !!values?.SimplifiedClientAccessEnabled,
})}
addedButtons={
)}
+ {conditionType === 'group' && (
+
+
+
+ )}
{conditionType === 'success' && (
Advances when every standard from the previous stages
@@ -591,6 +611,15 @@ const Page = () => {
url: '/api/ListCustomVariables',
queryKey: 'ListCustomVariables',
})
+ // Same query key as the Edit Tenant group picker so both share one cached list.
+ const tenantGroupsApi = ApiGetCall({
+ url: '/api/ListTenantGroups',
+ queryKey: 'AllTenantGroups',
+ })
+ const groupOptions = (tenantGroupsApi.data?.Results ?? []).map((group) => ({
+ label: group.Name,
+ value: group.Id,
+ }))
// Graduation conditions compare against CIPP custom variables; reserved tenant tokens
// are not useful graduation signals. Creatable, so any variable name can be typed.
const variableOptions = (customVariablesApi.data?.Results ?? [])
@@ -611,6 +640,7 @@ const Page = () => {
description: '',
alertEmails: '',
alertWebhookUrl: '',
+ disableScheduledRuns: false,
},
})
const watchForm = useWatch({ control: formControl.control })
@@ -630,6 +660,7 @@ const Page = () => {
description: template.description,
alertEmails: template.alertEmails ?? '',
alertWebhookUrl: template.alertWebhookUrl ?? '',
+ disableScheduledRuns: template.disableScheduledRuns === true,
// The tenant selector's own option objects round-trip verbatim through the API
// (assignments/exclusions); older saves fall back to name-based options.
tenantFilter:
@@ -849,6 +880,7 @@ const Page = () => {
),
alertEmails: values.alertEmails,
alertWebhookUrl: values.alertWebhookUrl,
+ disableScheduledRuns: values.disableScheduledRuns === true,
stages: stages.map(
(stage, index) =>
stageSerializers.current[index]?.() ?? {
@@ -885,14 +917,19 @@ const Page = () => {
{pageTitle}
-
+
{
required={false}
disableClearable={false}
/>
+
+
+ With scheduled runs disabled, this baseline only executes
+ when you run it yourself - drift is not detected or
+ remediated in between.
+
@@ -1118,6 +1166,7 @@ const Page = () => {
catalogByName={catalogByName}
registerSerializer={registerSerializer}
variableOptions={variableOptions}
+ groupOptions={groupOptions}
/>
))}
diff --git a/src/pages/tenant/baselines/templates/index.js b/src/pages/tenant/baselines/templates/index.js
index 2c28006c27f1..4eeda181f51d 100644
--- a/src/pages/tenant/baselines/templates/index.js
+++ b/src/pages/tenant/baselines/templates/index.js
@@ -1,11 +1,19 @@
import {
+ Alert,
Box,
Button,
+ Checkbox,
Chip,
+ CircularProgress,
Divider,
+ FormControlLabel,
LinearProgress,
+ List,
+ ListItem,
+ ListItemText,
Stack,
SvgIcon,
+ Switch,
Typography,
} from '@mui/material'
import Link from 'next/link'
@@ -18,6 +26,7 @@ import {
Edit,
GitHub,
PlayArrow,
+ Upgrade,
} from '@mui/icons-material'
import { Layout as DashboardLayout } from '../../../../layouts/index.js'
import { TabbedLayout } from '../../../../layouts/TabbedLayout'
@@ -28,7 +37,8 @@ import { CippOffCanvas } from '../../../../components/CippComponents/CippOffCanv
import { CippTemplateCatalog } from '../../../../components/CippComponents/CippTemplateCatalog'
import { describeStageConditions } from '../../../../components/CippBaselines/CippBaselineWhatIfReport'
import { parseCippDate } from '../../../../utils/parse-cipp-date'
-import { ApiGetCall } from '../../../../api/ApiCall'
+import { ApiGetCall, ApiPostCall } from '../../../../api/ApiCall'
+import { CippApiResults } from '../../../../components/CippComponents/CippApiResults'
// The API serializes single-element arrays as a bare object; the selector needs a real array.
const asOptionArray = (value) =>
@@ -39,10 +49,52 @@ const asOptionArray = (value) =>
const Page = () => {
const pageTitle = 'Baselines'
const [catalogVisible, setCatalogVisible] = useState(false)
+ const [migrateVisible, setMigrateVisible] = useState(false)
+ const [migrateSelected, setMigrateSelected] = useState([])
+ const [migrateReportOnly, setMigrateReportOnly] = useState(true)
+ const [migrateAddDetect, setMigrateAddDetect] = useState(false)
const integrations = ApiGetCall({
url: '/api/ListExtensionsConfig',
queryKey: 'Integrations',
})
+ const migratePreview = ApiPostCall({
+ onResult: (result) => {
+ // Pre-select everything migratable; skipped/up-to-date rows stay untouched.
+ setMigrateSelected(
+ (result?.Metadata?.templates ?? [])
+ .filter((template) =>
+ ['Ready', 'WillUpdate'].includes(template.status)
+ )
+ .map((template) => template.v2Guid)
+ )
+ },
+ })
+ const migrateCommit = ApiPostCall({
+ relatedQueryKeys: ['ListBaseline*'],
+ })
+ const openMigrate = () => {
+ setMigrateVisible(true)
+ migratePreview.mutate({
+ url: '/api/ExecBaselineMigrate',
+ data: { action: 'preview' },
+ })
+ }
+ // A finished commit replaces the preview as the list's source, so each row shows
+ // what actually happened to it.
+ const migrationReport =
+ migrateCommit.data?.data?.Metadata ?? migratePreview.data?.data?.Metadata
+ const migrationTemplates = Array.isArray(migrationReport?.templates)
+ ? migrationReport.templates
+ : []
+ const migrateStatusChip = {
+ Ready: { color: 'info', label: 'Ready' },
+ WillUpdate: { color: 'info', label: 'Will update' },
+ Migrated: { color: 'success', label: 'Migrated' },
+ Updated: { color: 'success', label: 'Updated' },
+ UpToDate: { color: 'default', label: 'Up to date' },
+ Skipped: { color: 'default', label: 'Skipped' },
+ Failed: { color: 'error', label: 'Failed' },
+ }
const actions = [
{
@@ -155,6 +207,10 @@ const Page = () => {
{ label: 'Description', value: row.description },
{ label: 'Standards', value: row.standardsCount },
{ label: 'Remediation', value: row.remediationPosture },
+ {
+ label: 'Scheduled Runs',
+ value: row.disableScheduledRuns ? 'Disabled' : 'Enabled',
+ },
{
label: 'Last Updated',
value: row.updatedAt
@@ -332,6 +388,203 @@ const Page = () => {
>
Browse Catalog
+
+ setMigrateVisible(false)}
+ size="lg"
+ footer={
+
+
+
+
+ }
+ >
+
+
+ Converts your classic Standards templates (including drift
+ templates) into baselines. The originals are never modified,
+ but while the Baselines feature is enabled the classic
+ Standards and Drift pages and their scheduled runs are turned
+ off - only one engine manages your tenants at a time.
+
+
+ setMigrateReportOnly(event.target.checked)
+ }
+ />
+ }
+ label="Import everything as report-only (recommended) - re-enable auto-remediation per standard once you have reviewed the results"
+ />
+
+ setMigrateAddDetect(event.target.checked)
+ }
+ />
+ }
+ label="Migrated drift templates should also alert on Intune and Conditional Access policies that were not created from a template"
+ />
+
+ {migratePreview.isPending && (
+
+
+
+ )}
+ {!migratePreview.isPending && migrationTemplates.length === 0 && (
+
+ No classic Standards templates were found to migrate.
+
+ )}
+
+ {migrationTemplates.map((template) => {
+ const selectable = ['Ready', 'WillUpdate'].includes(
+ template.status
+ )
+ const chip =
+ migrateStatusChip[template.status] ??
+ migrateStatusChip.Ready
+ return (
+
+
+ setMigrateSelected((prev) =>
+ prev.includes(template.v2Guid)
+ ? prev.filter((id) => id !== template.v2Guid)
+ : [...prev, template.v2Guid]
+ )
+ }
+ sx={{ mt: 0.5 }}
+ />
+
+
+ {template.templateName || '(unnamed template)'}
+
+ {template.type === 'drift' && (
+
+ )}
+
+
+ {template.standardsCount} standard
+ {template.standardsCount === 1 ? '' : 's'}
+
+
+ }
+ secondary={
+
+ {(template.tenants ?? []).length > 0 && (
+
+ Tenants: {(template.tenants ?? []).join(', ')}
+
+ )}
+ {template.detail && (
+
+ {template.detail}
+
+ )}
+ {(template.warnings ?? []).map((warning) => (
+
+ {warning}
+
+ ))}
+
+ }
+ />
+
+ )
+ })}
+
+
+
{
}
return (
- <>
- } onClick={createDialog.handleOpen} sx={{ mr: 1 }}>
- Add Template
-
- }
- />
+
+
+
+ } onClick={createDialog.handleOpen} sx={{ mr: 1 }}>
+ Add Template
+
+ }
+ />
+
{
...templateFields,
]}
/>
- >
+
)
}
-Page.getLayout = (page) => (
-
- {page}
-
-)
+Page.getLayout = (page) => {page}
export default Page
diff --git a/src/pages/tenant/standards/templates/template.jsx b/src/pages/tenant/standards/templates/template.jsx
index 7e442863b3f1..95c1398f260a 100644
--- a/src/pages/tenant/standards/templates/template.jsx
+++ b/src/pages/tenant/standards/templates/template.jsx
@@ -367,10 +367,10 @@ const Page = () => {
@@ -382,7 +382,12 @@ const Page = () => {
? 'Add Drift Template'
: 'Add Standards Template'}
-
+
+
{
+ const formControl = useForm({ mode: 'onBlur' })
+ const [expanded, setExpanded] = React.useState(startExpanded)
+ return (
+ setExpanded((prev) => !prev)}
+ onRemove={() => {}}
+ />
+ )
+}
+
+// MUI's filled TextField in this tree doesn't associate label->input the way
+// getByLabelText needs, so assertions read the inputs and map to their labels.
+const fieldValues = () => {
+ const byLabel = {}
+ for (const input of screen.queryAllByRole('textbox')) {
+ const label = input.closest('.MuiFormControl-root')?.querySelector('label')?.textContent
+ if (label) byLabel[label] = input.value
+ }
+ return byLabel
+}
+
+const expectSeeded = () => {
+ const values = fieldValues()
+ expect(values['Security contact email']).toBe('support@bezalu.com')
+ expect(values['Technical contact email']).toBe('support@bezalu.com')
+ expect(values['General/privacy contact email']).toBe('support@bezalu.com')
+ expect(values['Marketing contact email']).toBe('')
+}
+
+describe('CippBaselineStandardItem saved-variable seeding', () => {
+ it('seeds the settings fields from savedConfig.variables when mounted expanded', async () => {
+ renderWithProviders()
+ await waitFor(expectSeeded)
+ })
+
+ it('seeds the settings fields when expanded LATER (details mount lazily)', async () => {
+ const { default: userEvent } = await import('@testing-library/user-event')
+ const user = userEvent.setup()
+ renderWithProviders()
+ await user.click(screen.getByText('Set contact e-mails'))
+ await waitFor(expectSeeded)
+ })
+})
diff --git a/tests/components/CippComponents/CippAddUserDrawer.test.jsx b/tests/components/CippComponents/CippAddUserDrawer.test.jsx
index f77592a792cd..29b6fca9dd6e 100644
--- a/tests/components/CippComponents/CippAddUserDrawer.test.jsx
+++ b/tests/components/CippComponents/CippAddUserDrawer.test.jsx
@@ -54,6 +54,31 @@ vi.mock('../../../src/components/CippComponents/CippOffCanvas', () => ({
const idleGet = { isSuccess: false, isFetching: false, isError: false, data: undefined, refetch: vi.fn() }
const okGet = (data) => ({ isSuccess: true, isFetching: false, isError: false, data, refetch: vi.fn() })
+// built once: CippAutoComplete's option mapping keys on data identity, a fresh literal per call never settles
+const userDefaults = okGet([])
+const extensionsConfig = okGet({})
+const groups = okGet([])
+const customDataMappings = okGet({ Results: [] })
+const userGroups = okGet([])
+const tenantDomains = {
+ isSuccess: true,
+ isFetching: false,
+ isError: false,
+ data: {
+ pages: [
+ {
+ Results: [
+ { id: 'testdomain.com', isDefault: true, isInitial: false, isVerified: true },
+ { id: 'other.com', isDefault: false, isInitial: false, isVerified: true },
+ ],
+ },
+ ],
+ },
+ fetchNextPage: vi.fn(),
+ refetch: vi.fn(),
+}
+const idlePaginated = { ...idleGet, fetchNextPage: vi.fn() }
+
// Mutable state backing the ApiPostCall mock: flipping it and re-rendering imitates the
// react-query mutation lifecycle (idle -> pending -> success) the drawer sees in production.
let postState
@@ -61,35 +86,16 @@ let mutateSpy
function mockApis() {
ApiGetCall.mockImplementation(({ url }) => {
- if (url.startsWith('/api/ListNewUserDefaults')) return okGet([])
- if (url.startsWith('/api/ListExtensionsConfig')) return okGet({})
- if (url.startsWith('/api/ListGroups')) return okGet([])
- if (url.startsWith('/api/ListCustomDataMappings')) return okGet({ Results: [] })
- if (url.startsWith('/api/ListUserGroups')) return okGet([])
+ if (url.startsWith('/api/ListNewUserDefaults')) return userDefaults
+ if (url.startsWith('/api/ListExtensionsConfig')) return extensionsConfig
+ if (url.startsWith('/api/ListGroups')) return groups
+ if (url.startsWith('/api/ListCustomDataMappings')) return customDataMappings
+ if (url.startsWith('/api/ListUserGroups')) return userGroups
return idleGet
})
- ApiGetCallWithPagination.mockImplementation(({ url }) => {
- if (url === '/api/ListGraphRequest') {
- return {
- isSuccess: true,
- isFetching: false,
- isError: false,
- data: {
- pages: [
- {
- Results: [
- { id: 'testdomain.com', isDefault: true, isInitial: false, isVerified: true },
- { id: 'other.com', isDefault: false, isInitial: false, isVerified: true },
- ],
- },
- ],
- },
- fetchNextPage: vi.fn(),
- refetch: vi.fn(),
- }
- }
- return { ...idleGet, fetchNextPage: vi.fn() }
- })
+ ApiGetCallWithPagination.mockImplementation(({ url }) =>
+ url === '/api/ListGraphRequest' ? tenantDomains : idlePaginated
+ )
ApiPostCall.mockImplementation(() => ({ ...postState, mutate: mutateSpy }))
}
@@ -193,5 +199,6 @@ describe('CippAddUserDrawer - create another user without a page refresh (issue
username: 'second.user',
primDomain: { value: 'testdomain.com' },
})
- })
+ // two full form fills through userEvent.type
+ }, 15000)
})
diff --git a/tests/components/CippSettings/CippContainerManagement.test.jsx b/tests/components/CippSettings/CippContainerManagement.test.jsx
index 4a2527c37610..f2cbdebcee27 100644
--- a/tests/components/CippSettings/CippContainerManagement.test.jsx
+++ b/tests/components/CippSettings/CippContainerManagement.test.jsx
@@ -9,7 +9,10 @@ vi.mock('../../../src/api/ApiCall', async () => (await import('../../mocks/api-c
import { api, getResult, paginatedResult, postResult } from '../../mocks/api-call'
// stable references, fresh literals per call spin CippAutoComplete's mapping effect
-api.get = getResult()
+const statusGet = getResult()
+// status payload only answers its own url, the page's other GETs stay idle
+const idleGet = getResult({ isSuccess: false })
+api.get = (opts) => (opts.url === '/api/ExecContainerManagement' ? statusGet : idleGet)
api.paginated = paginatedResult()
api.post = postResult()
@@ -55,26 +58,26 @@ const ALERT_RE = /unsupported build from an unmerged branch/
describe('CippContainerManagement branch-build flagging', () => {
beforeEach(() => {
vi.clearAllMocks()
- api.get.data = undefined
+ statusGet.data = undefined
api.paginated.data = { pages: [{ Results: [] }] }
})
it('running pinned branch build chips the split tag, not Unknown', () => {
- api.get.data = { Results: statusResults('fix-sso-thing-a1b2c3d') }
+ statusGet.data = { Results: statusResults('fix-sso-thing-a1b2c3d') }
renderWithProviders()
expect(screen.getByText(PINNED_PRETTY)).toBeInTheDocument()
expect(screen.queryByText('Unknown')).not.toBeInTheDocument()
})
it('running branch build shows the unsupported-build alert and seeds the picker with its tag', async () => {
- api.get.data = { Results: statusResults('feat-new-widget') }
+ statusGet.data = { Results: statusResults('feat-new-widget') }
renderWithProviders()
expect(await screen.findByText(ALERT_RE)).toBeInTheDocument()
expect(screen.getByRole('combobox', { name: 'Release Channel' })).toHaveValue('feat-new-widget')
})
it('standard channel chips its friendly label and raises no branch alert', async () => {
- api.get.data = { Results: statusResults('dev') }
+ statusGet.data = { Results: statusResults('dev') }
renderWithProviders()
expect(screen.getByText('Dev')).toBeInTheDocument()
// wait for the seed effect so the alert-absence check runs against the settled form
@@ -86,7 +89,7 @@ describe('CippContainerManagement branch-build flagging', () => {
it('unrecognized non-branch tag chips Unknown without raising the branch alert', async () => {
// bare version tag: not a valid channel, does not match BuildChannelPattern
- api.get.data = { Results: statusResults('8.0.1') }
+ statusGet.data = { Results: statusResults('8.0.1') }
renderWithProviders()
expect(screen.getByText('Unknown')).toBeInTheDocument()
await waitFor(() => {
@@ -96,7 +99,7 @@ describe('CippContainerManagement branch-build flagging', () => {
})
it('picking a branch build raises the alert, switching back to a standard channel clears it', async () => {
- api.get.data = { Results: statusResults('latest') }
+ statusGet.data = { Results: statusResults('latest') }
api.paginated.data = { pages: [{ Results: channelListResults }] }
const user = userEvent.setup()
renderWithProviders()
diff --git a/tests/components/CippTable/CIPPTableToptoolbar.test.jsx b/tests/components/CippTable/CIPPTableToptoolbar.test.jsx
index bfa93e89bb11..8c80d6516a45 100644
--- a/tests/components/CippTable/CIPPTableToptoolbar.test.jsx
+++ b/tests/components/CippTable/CIPPTableToptoolbar.test.jsx
@@ -220,14 +220,15 @@ describe('CIPPTableToptoolbar - preset list refresh', () => {
})
}, 30000)
+ // pageName '' (jsdom router is '/') means no persistence, the slot tests name their key
it('restores both persisted slots and discards garbage global values', async () => {
- renderGraphTable({}, {
+ renderGraphTable({ persistenceKey: 'SlotsTest' }, {
settings: settingsWith({
persistFilters: true,
setLastUsedFilter: vi.fn(),
lastUsedFilters: {
// legacy single-slot shape with a non-string global value
- '': { type: 'global', value: [{ id: 'department', value: 'IT' }], name: 'Legacy Garbage' },
+ SlotsTest: { type: 'global', value: [{ id: 'department', value: 'IT' }], name: 'Legacy Garbage' },
},
}),
})
@@ -241,13 +242,13 @@ describe('CIPPTableToptoolbar - preset list refresh', () => {
})
it('restores both persisted slots and discards new-shape garbage global values', async () => {
- renderGraphTable({}, {
+ renderGraphTable({ persistenceKey: 'SlotsTest' }, {
settings: settingsWith({
persistFilters: true,
setLastUsedFilter: vi.fn(),
lastUsedFilters: {
// new shape can carry the same non-string global garbage the legacy branch discards
- '': {
+ SlotsTest: {
graph: null,
table: { id: 'Garbage', name: 'Garbage', type: 'global', value: [{ id: 'department', value: 'IT' }] },
},
@@ -264,12 +265,12 @@ describe('CIPPTableToptoolbar - preset list refresh', () => {
})
it('restores a legacy column filter into the table slot', async () => {
- renderGraphTable({}, {
+ renderGraphTable({ persistenceKey: 'SlotsTest' }, {
settings: settingsWith({
persistFilters: true,
setLastUsedFilter: vi.fn(),
lastUsedFilters: {
- '': { type: 'column', value: [{ id: 'department', value: 'IT' }], name: 'IT only' },
+ SlotsTest: { type: 'column', value: [{ id: 'department', value: 'IT' }], name: 'IT only' },
},
}),
})
@@ -284,12 +285,12 @@ describe('CIPPTableToptoolbar - preset list refresh', () => {
// overwriting whatever the user had just applied with the persisted filter.
it('does not clobber a user filter applied after the persisted one was restored', async () => {
const user = userEvent.setup()
- renderGraphTable({}, {
+ renderGraphTable({ persistenceKey: 'SlotsTest' }, {
settings: settingsWith({
persistFilters: true,
setLastUsedFilter: vi.fn(),
lastUsedFilters: {
- '': { type: 'column', value: [{ id: 'department', value: 'IT' }], name: 'IT only' },
+ SlotsTest: { type: 'column', value: [{ id: 'department', value: 'IT' }], name: 'IT only' },
},
}),
})
diff --git a/tests/components/CippTable/CippDataTable.stories.jsx b/tests/components/CippTable/CippDataTable.stories.jsx
index eed8a4c817f8..5c35b7d67eba 100644
--- a/tests/components/CippTable/CippDataTable.stories.jsx
+++ b/tests/components/CippTable/CippDataTable.stories.jsx
@@ -480,3 +480,40 @@ export const GraphBackedEditFilters = {
})
},
}
+
+// cached report column (membersCsv) wears the subTable header, no nested-table button. MRT renders no header cells in jsdom
+export const CachedReportColumns = {
+ args: {
+ title: 'Groups',
+ data: [{ id: 'parent-1', displayName: 'Finance', membersCsv: 'Jane, Bob' }],
+ simpleColumns: ['displayName', 'members'],
+ subTables: [
+ {
+ id: 'members',
+ header: 'Members',
+ label: 'View members',
+ cachedColumn: 'membersCsv',
+ table: {
+ title: 'Members of [displayName]',
+ api: { url: '/api/TestRelated', dataKey: 'Results' },
+ simpleColumns: ['displayName'],
+ },
+ },
+ ],
+ },
+ play: async ({ canvasElement, step }) => {
+ const canvas = within(canvasElement)
+
+ await step('cached csv column takes the subTable header', async () => {
+ await waitFor(() => {
+ expect(canvas.getByRole('columnheader', { name: /Members/ })).toBeVisible()
+ })
+ expect(canvas.queryByRole('columnheader', { name: /csv/i })).toBeNull()
+ })
+
+ await step('cell shows the cached value, no nested table button', async () => {
+ await expect(canvas.getByText('Jane, Bob')).toBeVisible()
+ expect(canvas.queryByRole('button', { name: 'View members' })).toBeNull()
+ })
+ },
+}
diff --git a/tests/components/CippTable/CippDataTable.test.jsx b/tests/components/CippTable/CippDataTable.test.jsx
index 2b7c1dd21658..20173be8bb03 100644
--- a/tests/components/CippTable/CippDataTable.test.jsx
+++ b/tests/components/CippTable/CippDataTable.test.jsx
@@ -6,6 +6,15 @@ import { renderWithProviders } from '../../test-utils'
import { CippDataTable } from '../../../src/components/CippTable/CippDataTable'
import { resetOverlayHistory } from '../../../src/utils/overlay-history'
+vi.mock('../../../src/api/ApiCall', async () => (await import('../../mocks/api-call')).apiCallMock())
+import { api, paginatedResult } from '../../mocks/api-call'
+
+// idle keeps static-data tables on their data prop; the nested result is re-wrapped per call like react-query's tracked copy, the memo'd toolbar needs it to see selection
+const nestedRows = [{ id: 'child-1', displayName: 'Jane Doe' }]
+const nestedResult = paginatedResult(nestedRows)
+const idlePaginated = paginatedResult([], { isSuccess: false })
+api.paginated = (opts) => (opts?.url === '/api/TestRelated' ? { ...nestedResult } : idlePaginated)
+
const basicData = [
{ displayName: 'Alice Smith', mail: 'alice@contoso.com', department: 'IT', accountEnabled: true },
{ displayName: 'Bob Johnson', mail: 'bob@contoso.com', department: 'Sales', accountEnabled: true },
@@ -718,7 +727,13 @@ describe('CippDataTable cards->table toggle scroll', () => {
describe('CippDataTable subTables', () => {
const parentRows = [{ id: 'parent-1', displayName: 'Finance' }]
- const relatedRows = [{ id: 'child-1', displayName: 'Jane Doe' }]
+ // live nested table, the shape groups/index.js ships
+ const nestedTable = {
+ title: 'Related for [displayName]',
+ api: { url: '/api/TestRelated', dataKey: 'Results' },
+ simpleColumns: ['displayName'],
+ viewMode: 'cards',
+ }
it('injects a button column that opens a nested table', async () => {
const user = userEvent.setup()
@@ -733,12 +748,7 @@ describe('CippDataTable subTables', () => {
id: 'related',
header: 'Related',
label: 'View',
- table: {
- title: 'Related for [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
- },
+ table: nestedTable,
},
]}
/>
@@ -771,10 +781,7 @@ describe('CippDataTable subTables', () => {
header: 'Related',
label: 'View',
table: {
- title: 'Related for [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
+ ...nestedTable,
actions: [
{
label: 'Remove',
@@ -797,15 +804,18 @@ describe('CippDataTable subTables', () => {
await user.click(within(dialog).getByRole('button', { name: 'Row actions' }))
await user.click(await screen.findByText('Remove'))
- expect(rowFn).toHaveBeenCalledWith(
- expect.objectContaining({
- id: 'child-1',
- displayName: 'Jane Doe',
- parent: expect.objectContaining({ id: 'parent-1', displayName: 'Finance' }),
- }),
- expect.anything(),
- expect.anything()
- )
+ // the row sheet hands the action off to its exit transition
+ await waitFor(() => {
+ expect(rowFn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'child-1',
+ displayName: 'Jane Doe',
+ parent: expect.objectContaining({ id: 'parent-1', displayName: 'Finance' }),
+ }),
+ expect.anything(),
+ expect.anything()
+ )
+ })
rowFn.mockClear()
await user.click(within(dialog).getByRole('button', { name: 'Select' }))
@@ -813,14 +823,16 @@ describe('CippDataTable subTables', () => {
await user.click(within(dialog).getByRole('button', { name: 'Actions' }))
await user.click(await screen.findByText('Remove'))
- expect(rowFn).toHaveBeenCalledWith(
- expect.objectContaining({
- id: 'child-1',
- parent: expect.objectContaining({ id: 'parent-1' }),
- }),
- expect.anything(),
- expect.anything()
- )
+ await waitFor(() => {
+ expect(rowFn).toHaveBeenCalledWith(
+ expect.objectContaining({
+ id: 'child-1',
+ parent: expect.objectContaining({ id: 'parent-1' }),
+ }),
+ expect.anything(),
+ expect.anything()
+ )
+ })
})
it('replaces a data column that shares the subTable id', async () => {
@@ -836,12 +848,7 @@ describe('CippDataTable subTables', () => {
id: 'related',
header: 'Related',
label: 'View',
- table: {
- title: 'Related for [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
- },
+ table: nestedTable,
},
]}
/>
@@ -869,12 +876,7 @@ describe('CippDataTable subTables', () => {
id: 'related',
header: 'Related',
label: 'View',
- table: {
- title: 'Related for [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
- },
+ table: nestedTable,
},
]}
/>
@@ -897,12 +899,7 @@ describe('CippDataTable subTables', () => {
header: 'Members',
label: 'View members',
cachedColumn: 'membersCsv',
- table: {
- title: 'Members of [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
- },
+ table: { ...nestedTable, title: 'Members of [displayName]' },
},
]}
/>
@@ -913,34 +910,6 @@ describe('CippDataTable subTables', () => {
expect(screen.getByText('Jane, Bob')).toBeInTheDocument()
})
- it('renders cached report columns in table view without a stale column order crash', async () => {
- renderWithProviders(
-
- )
-
- await waitFor(() => expect(screen.getByText('Finance')).toBeInTheDocument())
- expect(screen.getByRole('columnheader', { name: 'Members' })).toBeInTheDocument()
- expect(screen.queryByRole('button', { name: 'View members' })).not.toBeInTheDocument()
- })
-
it('still shows the nested table button when cachedColumn is configured but missing from the data', async () => {
renderWithProviders(
{
header: 'Members',
label: 'View members',
cachedColumn: 'membersCsv',
- table: {
- title: 'Members of [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
- },
+ table: { ...nestedTable, title: 'Members of [displayName]' },
},
]}
/>
@@ -972,7 +936,7 @@ describe('CippDataTable subTables', () => {
it('shows the nested table button when cachedColumn exists but is empty (live API shape)', async () => {
renderWithProviders(
{
header: 'Members',
label: 'View members',
cachedColumn: 'membersCsv',
- table: {
- title: 'Members of [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- },
+ table: { ...nestedTable, title: 'Members of [displayName]' },
},
]}
/>
@@ -1010,10 +970,9 @@ describe('CippDataTable subTables', () => {
header: 'Related',
label: 'View',
table: {
- title: 'Related for [displayName]',
- data: relatedRows,
- simpleColumns: ['displayName'],
- viewMode: 'cards',
+ ...nestedTable,
+ // table view, the card header is the only cardButton slot inside a dialog
+ viewMode: 'table',
cardButton: {
label: 'Add Members',
url: '/api/EditGroup',
diff --git a/tests/components/CippWizard/CippWizardAutopilotTypeSelection.stories.jsx b/tests/components/CippWizard/CippWizardAutopilotTypeSelection.stories.jsx
new file mode 100644
index 000000000000..ff5d2dfdb6f1
--- /dev/null
+++ b/tests/components/CippWizard/CippWizardAutopilotTypeSelection.stories.jsx
@@ -0,0 +1,30 @@
+import React from 'react'
+import { useForm } from 'react-hook-form'
+import { CippWizardAutopilotTypeSelection } from '../../../src/components/CippWizard/CippWizardAutopilotTypeSelection'
+
+// Mirrors the add-device wizard's initialState: autopilot is preselected so the
+// user can click Next without touching the step.
+const Harness = () => {
+ const formControl = useForm({
+ mode: 'onChange',
+ defaultValues: { deploymentType: 'autopilot' },
+ })
+ return (
+ {}}
+ onPreviousStep={() => {}}
+ />
+ )
+}
+
+export default {
+ title: 'Components/CippWizard/CippWizardAutopilotTypeSelection',
+ component: CippWizardAutopilotTypeSelection,
+}
+
+export const Default = {
+ render: () => ,
+}
diff --git a/tests/components/CippWizard/CippWizardAutopilotTypeSelection.test.jsx b/tests/components/CippWizard/CippWizardAutopilotTypeSelection.test.jsx
new file mode 100644
index 000000000000..9a7882c19d5e
--- /dev/null
+++ b/tests/components/CippWizard/CippWizardAutopilotTypeSelection.test.jsx
@@ -0,0 +1,75 @@
+import React from 'react'
+import { describe, it, expect } from 'vitest'
+import { screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { useForm } from 'react-hook-form'
+import { renderWithProviders } from '../../test-utils'
+import { CippWizardAutopilotTypeSelection } from '../../../src/components/CippWizard/CippWizardAutopilotTypeSelection'
+
+const Harness = ({ onForm, defaultValues }) => {
+ const formControl = useForm({
+ mode: 'onChange',
+ // Mirrors the add-device wizard's initialState
+ defaultValues: { deploymentType: 'autopilot', ...defaultValues },
+ })
+ onForm?.(formControl)
+ return (
+ {}}
+ onPreviousStep={() => {}}
+ />
+ )
+}
+
+describe('CippWizardAutopilotTypeSelection', () => {
+ it('preselects autopilot so Next is enabled without a click', async () => {
+ let form
+ renderWithProviders( (form = f)} />)
+
+ expect(form.getValues('deploymentType')).toBe('autopilot')
+ await waitFor(() => {
+ expect(screen.getByRole('button', { name: /next step/i })).toBeEnabled()
+ })
+ })
+
+ it('switches to device prep and clears the autopilot fields', async () => {
+ const user = userEvent.setup()
+ let form
+ renderWithProviders(
+ (form = f)}
+ defaultValues={{ autopilotData: [{ SerialNumber: 'SN1' }], GroupName: 'group' }}
+ />
+ )
+
+ await user.click(screen.getByText('Device Preparation (Corporate Identifiers)'))
+
+ expect(form.getValues('deploymentType')).toBe('devicePrep')
+ expect(form.getValues('autopilotData')).toBeUndefined()
+ expect(form.getValues('GroupName')).toBeUndefined()
+ })
+
+ it('switches back to autopilot and clears the device prep fields', async () => {
+ const user = userEvent.setup()
+ let form
+ renderWithProviders(
+ (form = f)}
+ defaultValues={{
+ deploymentType: 'devicePrep',
+ devicePrepData: [{ manufacturer: 'Dell', model: 'XPS', serialNumber: 'SN1' }],
+ overwriteExisting: true,
+ }}
+ />
+ )
+
+ await user.click(screen.getByText('Windows Autopilot'))
+
+ expect(form.getValues('deploymentType')).toBe('autopilot')
+ expect(form.getValues('devicePrepData')).toBeUndefined()
+ expect(form.getValues('overwriteExisting')).toBeUndefined()
+ })
+})
diff --git a/tests/components/CippWizard/CippWizardDevicePrepImport.stories.jsx b/tests/components/CippWizard/CippWizardDevicePrepImport.stories.jsx
new file mode 100644
index 000000000000..daf9810b70b6
--- /dev/null
+++ b/tests/components/CippWizard/CippWizardDevicePrepImport.stories.jsx
@@ -0,0 +1,58 @@
+import React from 'react'
+import { http, HttpResponse } from 'msw'
+import { useForm } from 'react-hook-form'
+import { CippWizardDevicePrepImport } from '../../../src/components/CippWizard/CippWizardDevicePrepImport'
+
+// The three the device prep wizard passes — a corporate identifier is exactly this triplet.
+const fields = [
+ { friendlyName: 'Manufacturer', propertyName: 'manufacturer' },
+ { friendlyName: 'Model', propertyName: 'model' },
+ { friendlyName: 'Serial Number', propertyName: 'serialNumber' },
+]
+
+const handlers = [
+ http.get('*/api/ListGraphRequest', () => HttpResponse.json({ Results: [] })),
+ http.get('*/api/ListGraphExplorerPresets', () => HttpResponse.json({ Results: [] })),
+]
+
+const Harness = ({ defaultValues }) => {
+ const formControl = useForm({
+ mode: 'onChange',
+ defaultValues: { devicePrepData: [], ...defaultValues },
+ })
+ return (
+ {}}
+ onPreviousStep={() => {}}
+ />
+ )
+}
+
+export default {
+ title: 'Components/CippWizard/CippWizardDevicePrepImport',
+ component: CippWizardDevicePrepImport,
+ parameters: { msw: { handlers } },
+}
+
+export const Empty = {
+ render: () => ,
+}
+
+export const WithDevices = {
+ render: () => (
+
+ ),
+}
diff --git a/tests/components/CippWizard/CippWizardDevicePrepImport.test.jsx b/tests/components/CippWizard/CippWizardDevicePrepImport.test.jsx
new file mode 100644
index 000000000000..bcb0b857a89e
--- /dev/null
+++ b/tests/components/CippWizard/CippWizardDevicePrepImport.test.jsx
@@ -0,0 +1,124 @@
+import React from 'react'
+import { describe, it, expect, vi } from 'vitest'
+import { screen, within, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { useForm } from 'react-hook-form'
+import { renderWithProviders } from '../../test-utils'
+import { CippWizardDevicePrepImport } from '../../../src/components/CippWizard/CippWizardDevicePrepImport'
+
+vi.mock('../../../src/hooks/use-breakpoint', async (importOriginal) => ({
+ ...(await importOriginal()),
+ useIsMobileLayout: () => false,
+ useIsTabletLayout: () => false,
+ useTableViewMode: () => 'table',
+}))
+
+vi.mock('../../../src/api/ApiCall', () => ({
+ ApiGetCall: vi.fn(() => ({ data: undefined, isFetching: false, isSuccess: false })),
+ ApiPostCall: vi.fn(() => ({ mutate: vi.fn(), isPending: false })),
+ ApiGetCallWithPagination: vi.fn(() => ({ data: undefined, isFetching: false })),
+}))
+
+// The three the device prep wizard passes — a corporate identifier is exactly this triplet.
+const fields = [
+ { friendlyName: 'Manufacturer', propertyName: 'manufacturer' },
+ { friendlyName: 'Model', propertyName: 'model' },
+ { friendlyName: 'Serial Number', propertyName: 'serialNumber' },
+]
+
+const Harness = () => {
+ const formControl = useForm({ mode: 'onChange', defaultValues: { devicePrepData: [] } })
+ return (
+ {}}
+ onPreviousStep={() => {}}
+ />
+ )
+}
+
+const openManualImport = async () => {
+ const user = userEvent.setup()
+ renderWithProviders()
+ await user.click(await screen.findByRole('button', { name: /manual import/i }))
+ return { user, dialog: within(await screen.findByRole('dialog')) }
+}
+
+describe('CippWizardDevicePrepImport manual entry', () => {
+ it('requires manufacturer, model and serial number before a row can be added', async () => {
+ const { user, dialog } = await openManualImport()
+
+ await user.type(dialog.getByLabelText('Manufacturer'), 'Dell')
+
+ expect(await dialog.findByText(/Model, Serial Number are required/)).toBeInTheDocument()
+ expect(dialog.getByRole('button', { name: 'Add' })).toBeDisabled()
+ }, 20000)
+
+ it('rejects values containing a comma', async () => {
+ const { user, dialog } = await openManualImport()
+
+ await user.type(dialog.getByLabelText('Manufacturer'), 'Dell, Inc')
+ await user.type(dialog.getByLabelText('Model'), 'XPS 13')
+ await user.type(dialog.getByLabelText('Serial Number'), 'SN001')
+
+ expect(await dialog.findByText(/Manufacturer may not contain a comma/)).toBeInTheDocument()
+ expect(dialog.getByRole('button', { name: 'Add' })).toBeDisabled()
+ }, 20000)
+
+ it('adds a complete device to the table', async () => {
+ const { user, dialog } = await openManualImport()
+
+ await user.type(dialog.getByLabelText('Manufacturer'), 'Dell')
+ await user.type(dialog.getByLabelText('Model'), 'XPS 13')
+ await user.type(dialog.getByLabelText('Serial Number'), 'SN001')
+ await user.click(dialog.getByRole('button', { name: 'Add' }))
+
+ await waitFor(() => {
+ expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
+ })
+ // MRT virtualizes rows and jsdom has no layout engine, so cells are not
+ // rendered; the pagination summary is the observable proof the row landed.
+ expect(await screen.findByText('1-1 of 1', {}, { timeout: 10000 })).toBeInTheDocument()
+ }, 20000)
+})
+
+describe('CippWizardDevicePrepImport CSV import', () => {
+ const uploadCsv = async (content) => {
+ const user = userEvent.setup()
+ const { container } = renderWithProviders()
+ const input = container.querySelector('input[type="file"]')
+ const file = new File([content], 'identifiers.csv', { type: 'text/csv' })
+ await user.upload(input, file)
+ }
+
+ it('imports a headerless CSV in the Intune portal order', async () => {
+ await uploadCsv('Dell,XPS 13,SN001\nHP,EliteBook,SN002\n')
+
+ expect(await screen.findByText('1-2 of 2', {}, { timeout: 10000 })).toBeInTheDocument()
+ }, 20000)
+
+ it('imports a CSV with headers', async () => {
+ await uploadCsv('manufacturer,model,serialNumber\nDell,XPS 13,SN001\n')
+
+ expect(await screen.findByText('1-1 of 1', {}, { timeout: 10000 })).toBeInTheDocument()
+ }, 20000)
+
+ it('rejects rows with missing values instead of importing them', async () => {
+ await uploadCsv('Dell,,SN001\n')
+
+ expect(await screen.findByText(/could not be imported/)).toBeInTheDocument()
+ expect(screen.getByText(/Model is required/)).toBeInTheDocument()
+ expect(screen.queryByText('1-1 of 1')).not.toBeInTheDocument()
+ }, 20000)
+
+ it('rejects duplicate devices', async () => {
+ await uploadCsv('Dell,XPS 13,SN001\nDell,XPS 13,SN001\n')
+
+ expect(await screen.findByText(/could not be imported/)).toBeInTheDocument()
+ expect(screen.getByText(/Duplicate device/)).toBeInTheDocument()
+ }, 20000)
+})
diff --git a/tests/components/PrivateRoute.test.jsx b/tests/components/PrivateRoute.test.jsx
index a8f8f4dc51a8..d3872b242cf9 100644
--- a/tests/components/PrivateRoute.test.jsx
+++ b/tests/components/PrivateRoute.test.jsx
@@ -19,6 +19,7 @@ vi.mock('../../src/api/ApiCall', () => ({
// /.auth/me
return authState.swa
},
+ ApiPostCall: () => ({ mutate: vi.fn(), isPending: false }),
}))
// the gate page hosts the entire setup wizard via next/dynamic - the routing
diff --git a/tests/mocks/api-call.js b/tests/mocks/api-call.js
index 65c406fd0890..fe53d865b35e 100644
--- a/tests/mocks/api-call.js
+++ b/tests/mocks/api-call.js
@@ -31,6 +31,7 @@ export const paginatedResult = (rows = [], overrides = {}) => ({
export const postResult = (overrides = {}) => ({
mutate: vi.fn(),
+ reset: vi.fn(),
isPending: false,
isSuccess: false,
isError: false,
diff --git a/tests/mocks/baseline-tenant-fixture.json b/tests/mocks/baseline-tenant-fixture.json
new file mode 100644
index 000000000000..07857a1799b2
--- /dev/null
+++ b/tests/mocks/baseline-tenant-fixture.json
@@ -0,0 +1,613 @@
+{
+ "baseline": {
+ "GUID": "3ca8c0ec-9294-4060-8191-f1f2f3af37da",
+ "templateName": ".Baseline - Tenant",
+ "baselineName": ".Baseline - Tenant",
+ "description": "description
",
+ "assignedTenants": [
+ "Exported Template"
+ ],
+ "assignments": {
+ "label": "Exported Template",
+ "value": "Exported Template",
+ "type": "Tenant"
+ },
+ "exclusions": null,
+ "excludedTenants": [],
+ "alertEmails": "",
+ "alertWebhookUrl": "",
+ "disableScheduledRuns": false,
+ "standardsCount": 9,
+ "stageNames": [
+ "Default"
+ ],
+ "stages": [
+ {
+ "name": "Default",
+ "logic": "and",
+ "conditions": [],
+ "standards": [
+ "ActivityBasedTimeout",
+ "AnonReportDisable",
+ "AuditLog",
+ "DisableBasicAuthSMTP",
+ "DisableGuestDirectory",
+ "EnablePronouns",
+ "FormsPhishingProtection",
+ "MailContacts",
+ "PhishProtection"
+ ],
+ "standardsConfig": [
+ {
+ "standard": "ActivityBasedTimeout",
+ "instance": "ActivityBasedTimeout",
+ "variables": {
+ "timeout": "06:00:00"
+ },
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "AnonReportDisable",
+ "instance": "AnonReportDisable",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "AuditLog",
+ "instance": "AuditLog",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "DisableBasicAuthSMTP",
+ "instance": "DisableBasicAuthSMTP",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "DisableGuestDirectory",
+ "instance": "DisableGuestDirectory",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "EnablePronouns",
+ "instance": "EnablePronouns",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "FormsPhishingProtection",
+ "instance": "FormsPhishingProtection",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "MailContacts",
+ "instance": "MailContacts",
+ "variables": {
+ "SecurityContact": "support@bezalu.com",
+ "TechContact": "support@bezalu.com",
+ "GeneralContact": "support@bezalu.com",
+ "MarketingContact": ""
+ },
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ },
+ {
+ "standard": "PhishProtection",
+ "instance": "PhishProtection",
+ "variables": null,
+ "remediateEnabled": false,
+ "alertEnabled": false,
+ "alertOnRemediate": false
+ }
+ ]
+ }
+ ],
+ "remediationPosture": "Report",
+ "updatedAt": 1787415499,
+ "updatedBy": "developer@localhost",
+ "occupancy": [
+ {
+ "stage": 1,
+ "name": "Default",
+ "standardsCount": 9,
+ "tenants": [
+ null
+ ],
+ "nextAdvanceAt": null
+ }
+ ],
+ "tenantStates": []
+ },
+ "definitions": [
+ {
+ "name": "DisableBasicAuthSMTP",
+ "label": "Disable SMTP Basic Authentication",
+ "cat": "Exchange Standards",
+ "tag": [
+ "CIS M365 7.0.0 (6.5.4)",
+ "NIST CSF 2.0 (PR.IR-01)"
+ ],
+ "impact": "Medium Impact",
+ "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)",
+ "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.",
+ "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).",
+ "impactColour": "warning",
+ "addedDate": "2021-11-16",
+ "powershellEquivalent": "Set-TransportConfig -SmtpClientAuthenticationDisabled $true",
+ "appliesToTest": [
+ "CISAMSEXO51",
+ "CIS_6_5_4",
+ "ZTNA21799"
+ ],
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ],
+ "requiredCapabilities": [
+ "EXCHANGE_S_STANDARD",
+ "EXCHANGE_S_ENTERPRISE",
+ "EXCHANGE_S_STANDARD_GOV",
+ "EXCHANGE_S_ENTERPRISE_GOV",
+ "EXCHANGE_LITE"
+ ],
+ "secureScoreImpact": 10,
+ "compare": "subset",
+ "variables": {
+ "disabled": {
+ "type": "switch",
+ "label": "SMTP basic authentication disabled",
+ "default": true,
+ "recommended": true
+ }
+ },
+ "expected": {
+ "SmtpClientAuthenticationDisabled": "%disabled%",
+ "UsersWithSmtpAuthEnabled": []
+ },
+ "read": {
+ "cacheType": "ExoTransportConfig"
+ },
+ "prepare": "Get-CIPPBaselineDisableBasicAuthSMTPState",
+ "remediate": {
+ "executor": "DisableBasicAuthSMTP",
+ "disabled": "%disabled%"
+ }
+ },
+ {
+ "name": "ActivityBasedTimeout",
+ "label": "Enable Activity based Timeout",
+ "cat": "Global Standards",
+ "tag": [
+ "CIS M365 7.0.0 (1.3.2)",
+ "spo_idle_session_timeout",
+ "NIST CSF 2.0 (PR.AA-03)"
+ ],
+ "impact": "Medium Impact",
+ "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.",
+ "impactColour": "warning",
+ "addedDate": "2022-04-13",
+ "powershellEquivalent": "Portal or Graph API",
+ "appliesToTest": [
+ "CIS_1_3_2",
+ "ZTNA21813",
+ "ZTNA21814",
+ "ZTNA21815"
+ ],
+ "recommendedBy": [
+ "CIS"
+ ],
+ "requiredCapabilities": [],
+ "secureScoreImpact": 5,
+ "compare": "subset",
+ "variables": {
+ "timeout": {
+ "type": "autoComplete",
+ "label": "Idle session timeout",
+ "options": [
+ {
+ "label": "1 Hour",
+ "value": "01:00:00"
+ },
+ {
+ "label": "3 Hours",
+ "value": "03:00:00"
+ },
+ {
+ "label": "6 Hours",
+ "value": "06:00:00"
+ },
+ {
+ "label": "12 Hours",
+ "value": "12:00:00"
+ },
+ {
+ "label": "24 Hours",
+ "value": "1.00:00:00"
+ }
+ ],
+ "default": "01:00:00",
+ "recommended": "01:00:00"
+ }
+ },
+ "expected": {
+ "timeout": "%timeout%"
+ },
+ "read": {
+ "cacheType": "ActivityBasedTimeoutPolicy"
+ },
+ "prepare": "Get-CIPPBaselineActivityBasedTimeoutState",
+ "remediate": {
+ "executor": "ActivityBasedTimeout",
+ "timeout": "%timeout%"
+ }
+ },
+ {
+ "name": "AnonReportDisable",
+ "label": "Enable Usernames instead of pseudo anonymised names in reports",
+ "cat": "Global Standards",
+ "tag": [],
+ "impact": "Low Impact",
+ "helpText": "Shows usernames instead of pseudo anonymised names in reports. This standard is required for reporting to work correctly.",
+ "executiveText": "Configures Microsoft 365 reports to display actual usernames instead of anonymized identifiers, enabling IT administrators to effectively troubleshoot issues and generate meaningful usage reports. This improves operational efficiency and system management capabilities.",
+ "docsDescription": "Microsoft announced some APIs and reports no longer return names, to comply with compliance and legal requirements in specific countries. This proves an issue for a lot of MSPs because those reports are often helpful for engineers. This standard applies a setting that shows usernames in those API calls / reports.",
+ "impactColour": "info",
+ "addedDate": "2021-11-16",
+ "powershellEquivalent": "Update-MgBetaAdminReportSetting -BodyParameter @{displayConcealedNames = $true}",
+ "recommendedBy": [
+ "CIPP"
+ ],
+ "requiredCapabilities": [],
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": null,
+ "expected": {
+ "displayConcealedNames": false
+ },
+ "read": {
+ "cacheType": "AdminReportSettings"
+ },
+ "remediate": {
+ "executor": "GraphRequest",
+ "requests": [
+ {
+ "method": "PATCH",
+ "uri": "admin/reportSettings",
+ "body": {
+ "displayConcealedNames": false
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "AuditLog",
+ "label": "Enable the Unified Audit Log",
+ "cat": "Global Standards",
+ "tag": [
+ "CIS M365 7.0.0 (3.1.1)",
+ "mip_search_auditlog",
+ "NIST CSF 2.0 (DE.CM-09)"
+ ],
+ "impact": "Low Impact",
+ "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.",
+ "impactColour": "info",
+ "addedDate": "2021-11-16",
+ "powershellEquivalent": "Enable-OrganizationCustomization",
+ "appliesToTest": [
+ "CISAMSEXO171",
+ "CISAMSEXO173",
+ "CIS_3_1_1"
+ ],
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ],
+ "requiredCapabilities": [
+ "EXCHANGE_S_STANDARD",
+ "EXCHANGE_S_ENTERPRISE",
+ "EXCHANGE_S_STANDARD_GOV",
+ "EXCHANGE_S_ENTERPRISE_GOV",
+ "EXCHANGE_LITE"
+ ],
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": {
+ "enabled": {
+ "type": "switch",
+ "label": "Unified Audit Log ingestion enabled",
+ "default": true,
+ "recommended": true,
+ "locked": true
+ }
+ },
+ "expected": {
+ "UnifiedAuditLogIngestionEnabled": "%enabled%"
+ },
+ "read": {
+ "cacheType": "ExoAdminAuditLogConfig"
+ },
+ "remediate": {
+ "executor": "ExoRequest",
+ "cmdlets": [
+ {
+ "cmdlet": "Enable-OrganizationCustomization",
+ "params": null,
+ "continueOnError": true
+ },
+ {
+ "cmdlet": "Set-AdminAuditLogConfig",
+ "params": {
+ "UnifiedAuditLogIngestionEnabled": "%enabled%"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "DisableGuestDirectory",
+ "label": "Restrict guest user access to directory objects",
+ "cat": "Global Standards",
+ "tag": [
+ "CIS M365 7.0.0 (5.1.6.2)",
+ "CISA (MS.AAD.5.1v1)",
+ "EIDSCA.AP14",
+ "EIDSCA.ST08",
+ "EIDSCA.ST09",
+ "NIST CSF 2.0 (PR.AA-05)",
+ "SMB1001 (2.8)"
+ ],
+ "impact": "Low Impact",
+ "helpText": "Disables Guest access to enumerate directory objects. This prevents guest users from seeing other users or guests in the directory.",
+ "executiveText": "Restricts external guest users from viewing the company's employee directory and organizational structure, protecting sensitive information about staff and internal groups. This security measure prevents unauthorized access to corporate contact information while still allowing necessary collaboration.",
+ "docsDescription": "Sets it so guests can view only their own user profile. Permission to view other users isn't allowed. Also restricts guest users from seeing the membership of groups they're in. See exactly what get locked down in the [Microsoft documentation.](https://learn.microsoft.com/en-us/entra/fundamentals/users-default-permissions)",
+ "impactColour": "info",
+ "addedDate": "2022-05-04",
+ "powershellEquivalent": "Set-AzureADMSAuthorizationPolicy -GuestUserRoleId '2af84b1e-32c8-42b7-82bc-daa82404023b'",
+ "appliesToTest": [
+ "CIS_5_1_6_2",
+ "EIDSCAAP07",
+ "EIDSCAAP14",
+ "EIDSCAST08",
+ "EIDSCAST09",
+ "SMB1001_2_8",
+ "ZTNA21792"
+ ],
+ "recommendedBy": [
+ "CIPP"
+ ],
+ "requiredCapabilities": [],
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": {
+ "guestUserRoleId": {
+ "type": "autoComplete",
+ "label": "Guest user access level",
+ "options": [
+ {
+ "label": "Restricted access (guests can only see their own profile)",
+ "value": "2af84b1e-32c8-42b7-82bc-daa82404023b"
+ },
+ {
+ "label": "Limited access (guests can see membership of non-hidden groups)",
+ "value": "10dae51f-b6af-4016-8d66-8c2a99b929b3"
+ },
+ {
+ "label": "Same access as member users",
+ "value": "a0b1b346-4d3e-4e8b-98f8-753987be4970"
+ }
+ ],
+ "default": "2af84b1e-32c8-42b7-82bc-daa82404023b",
+ "recommended": "2af84b1e-32c8-42b7-82bc-daa82404023b"
+ }
+ },
+ "expected": {
+ "guestUserRoleId": "%guestUserRoleId%"
+ },
+ "read": {
+ "cacheType": "AuthorizationPolicy"
+ },
+ "remediate": {
+ "executor": "GraphRequest",
+ "requests": [
+ {
+ "method": "PATCH",
+ "asApp": false,
+ "uri": "policies/authorizationPolicy/authorizationPolicy",
+ "body": {
+ "guestUserRoleId": "%guestUserRoleId%"
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "EnablePronouns",
+ "label": "Enable Pronouns",
+ "cat": "Global Standards",
+ "tag": [],
+ "impact": "Low Impact",
+ "helpText": "Enables the Pronouns feature for the tenant. This allows users to set their pronouns in their profile.",
+ "executiveText": "Allows employees to display their preferred pronouns in their Microsoft 365 profiles, supporting inclusive workplace practices and helping colleagues communicate respectfully. This feature enhances diversity and inclusion initiatives while fostering a more welcoming work environment.",
+ "impactColour": "info",
+ "addedDate": "2024-06-05",
+ "powershellEquivalent": "Update-MgBetaAdminPeoplePronoun -IsEnabledInOrganization:$true",
+ "recommendedBy": [],
+ "requiredCapabilities": [],
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": null,
+ "expected": {
+ "isEnabledInOrganization": true
+ },
+ "read": {
+ "cacheType": "Pronouns"
+ },
+ "remediate": {
+ "executor": "GraphRequest",
+ "requests": [
+ {
+ "method": "PATCH",
+ "uri": "admin/people/pronouns",
+ "body": {
+ "isEnabledInOrganization": true
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "FormsPhishingProtection",
+ "label": "Enable internal phishing protection for Forms",
+ "cat": "Global Standards",
+ "tag": [
+ "CIS M365 7.0.0 (1.3.5)",
+ "Security",
+ "PhishingProtection"
+ ],
+ "impact": "Low Impact",
+ "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.",
+ "executiveText": "Automatically scans Microsoft Forms created by employees for malicious content and phishing attempts, preventing the creation and distribution of harmful forms within the organization. This protects against both internal threats and compromised accounts that might be used to distribute malicious content.",
+ "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.",
+ "impactColour": "info",
+ "addedDate": "2025-06-06",
+ "powershellEquivalent": "Graph API",
+ "appliesToTest": [
+ "CIS_1_3_5"
+ ],
+ "recommendedBy": [
+ "CIS",
+ "CIPP"
+ ],
+ "requiredCapabilities": [],
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": null,
+ "expected": {
+ "isInOrgFormsPhishingScanEnabled": true
+ },
+ "read": {
+ "cacheType": "FormsSettings"
+ },
+ "remediate": {
+ "executor": "GraphRequest",
+ "requests": [
+ {
+ "method": "PATCH",
+ "asApp": false,
+ "uri": "admin/forms/settings",
+ "body": {
+ "isInOrgFormsPhishingScanEnabled": true
+ }
+ }
+ ]
+ }
+ },
+ {
+ "name": "MailContacts",
+ "label": "Set contact e-mails",
+ "cat": "Global Standards",
+ "tag": [],
+ "impact": "Low Impact",
+ "helpText": "Sets the organization's notification contacts: technical, security, marketing and general/privacy. Only configured contacts are enforced.",
+ "executiveText": "Keeps Microsoft's service, security and privacy notifications flowing to the right mailboxes instead of a former employee's.",
+ "docsDescription": "Grades only the configured contacts: marketing as contains, security+technical as a set against the technical notification list, and the general contact against the privacy profile. Remediation writes only the configured members.",
+ "impactColour": "info",
+ "addedDate": "2026-08-16",
+ "powershellEquivalent": "Graph: PATCH organization",
+ "recommendedBy": [],
+ "requiredCapabilities": [],
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": {
+ "GeneralContact": {
+ "type": "textField",
+ "label": "General/privacy contact email",
+ "omitWhenBlank": true
+ },
+ "SecurityContact": {
+ "type": "textField",
+ "label": "Security contact email",
+ "omitWhenBlank": true
+ },
+ "MarketingContact": {
+ "type": "textField",
+ "label": "Marketing contact email",
+ "omitWhenBlank": true
+ },
+ "TechContact": {
+ "type": "textField",
+ "label": "Technical contact email",
+ "omitWhenBlank": true
+ }
+ },
+ "read": {
+ "cacheType": "Organization"
+ },
+ "prepare": "Get-CIPPBaselineMailContactsState",
+ "remediate": {
+ "executor": "MailContacts",
+ "generalContact": "%GeneralContact%",
+ "securityContact": "%SecurityContact%",
+ "marketingContact": "%MarketingContact%",
+ "techContact": "%TechContact%"
+ }
+ },
+ {
+ "name": "PhishProtection",
+ "label": "Enable Phishing Protection system via branding CSS",
+ "cat": "Global Standards",
+ "tag": [],
+ "impact": "Low Impact",
+ "helpText": "Adds branding to the logon page that only appears if the url is not login.microsoftonline.com. This potentially prevents AITM attacks via EvilNginx, and automatically generates alerts if a clone of your login page is found.",
+ "executiveText": "Adds a hidden canary to the company sign-in page that exposes cloned phishing pages and alerts when one is found, protecting staff credentials from adversary-in-the-middle attacks.",
+ "docsDescription": "Grades whether the default branding localization's custom CSS contains the tenant's clone-detection canary (the clone.cipp.app background-image URL carrying this instance's CIPPURL from the Config table). The branding singleton reads live. Remediation strips a known malformed variant, creates the default localization when missing (Accept-Language 0, tolerating already-exists), and APPENDS the canary to the existing CSS - operator customizations are never overwritten. Report and warn are disabled as in the classic: this standard acts through remediation only.",
+ "impactColour": "info",
+ "addedDate": "2026-08-16",
+ "powershellEquivalent": "Portal only",
+ "recommendedBy": [],
+ "requiredCapabilities": [
+ "AAD_PREMIUM",
+ "AAD_PREMIUM_P2",
+ "OFFICE_BUSINESS"
+ ],
+ "disabledFeatures": {
+ "report": true,
+ "warn": true,
+ "remediate": false
+ },
+ "secureScoreImpact": 0,
+ "compare": "subset",
+ "variables": null,
+ "read": null,
+ "prepare": "Get-CIPPBaselinePhishProtectionState",
+ "remediate": {
+ "executor": "PhishProtection"
+ }
+ }
+ ]
+}
diff --git a/tests/pages/BaselineTemplateEditor.seeding.test.jsx b/tests/pages/BaselineTemplateEditor.seeding.test.jsx
new file mode 100644
index 000000000000..26f878079616
--- /dev/null
+++ b/tests/pages/BaselineTemplateEditor.seeding.test.jsx
@@ -0,0 +1,66 @@
+import React from 'react'
+import { describe, it, expect, vi } from 'vitest'
+import { screen, waitFor } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { renderWithProviders } from '../test-utils'
+import router from '../mocks/next-router'
+import fixture from '../mocks/baseline-tenant-fixture.json'
+
+vi.mock('../../src/api/ApiCall', async () => (await import('../mocks/api-call')).apiCallMock())
+import { api, getResult, postResult } from '../mocks/api-call'
+
+import Page from '../../src/pages/tenant/baselines/template.jsx'
+
+// Route ApiGetCall by url with STABLE result identities (fresh literals per call loop
+// data-sync effects - see mocks/api-call.js).
+const baselinesResult = getResult({ data: [fixture.baseline] })
+const definitionsResult = getResult({ data: fixture.definitions })
+const customVariablesResult = getResult({ data: { Results: [] } })
+const emptyResult = getResult({ isSuccess: false })
+api.get = (opts) => {
+ if (opts?.url === '/api/ListBaselines') return baselinesResult
+ if (opts?.url === '/api/ListBaselineStandards') return definitionsResult
+ if (opts?.url === '/api/ListCustomVariables') return customVariablesResult
+ return emptyResult
+}
+api.post = postResult()
+
+router.query = { id: fixture.baseline.GUID }
+router.pathname = '/tenant/baselines/template'
+
+describe('Baseline template editor - migrated variable seeding', () => {
+ it('shows the saved MailContacts addresses after expanding the standard', async () => {
+ const user = userEvent.setup()
+ renderWithProviders()
+
+ // Template loaded: its name is in the form and the standard is listed.
+ await waitFor(() => {
+ expect(screen.getByText('Set contact e-mails')).toBeInTheDocument()
+ })
+
+ // Expand the accordion the way an operator does (details mount lazily).
+ await user.click(screen.getByText('Set contact e-mails'))
+
+ const inputs = await screen.findAllByRole('textbox')
+ const byLabel = {}
+ for (const input of inputs) {
+ const label = input.closest('.MuiFormControl-root')?.querySelector('label')?.textContent
+ if (label) byLabel[label] = input.value
+ }
+ console.log('DBG editor fields:', JSON.stringify(byLabel))
+
+ await waitFor(() => {
+ const security = screen
+ .getAllByRole('textbox')
+ .find(
+ (input) =>
+ input
+ .closest('.MuiFormControl-root')
+ ?.querySelector('label')
+ ?.textContent?.includes('Security contact email')
+ )
+ expect(security).toBeTruthy()
+ expect(security.value).toBe('support@bezalu.com')
+ })
+ }, 30000)
+})
diff --git a/tests/pages/MessageEncryptionPage.test.jsx b/tests/pages/MessageEncryptionPage.test.jsx
index c378ae2bb994..c6e2e72cbe9f 100644
--- a/tests/pages/MessageEncryptionPage.test.jsx
+++ b/tests/pages/MessageEncryptionPage.test.jsx
@@ -32,10 +32,15 @@ describe('Message Encryption page', () => {
beforeEach(() => {
vi.clearAllMocks()
api.post = postResult()
- api.paginated = paginatedResult([
- { displayName: 'Admin', UPN: 'admin@contoso.com' },
- { displayName: 'Helpdesk', UPN: 'helpdesk@contoso.com' },
- ])
+ // ListMailboxes returns a bare array (no Results wrapper, the page's api sets no dataKey)
+ api.paginated = paginatedResult([], {
+ data: {
+ pages: [[
+ { displayName: 'Admin', UPN: 'admin@contoso.com' },
+ { displayName: 'Helpdesk', UPN: 'helpdesk@contoso.com' },
+ ]],
+ },
+ })
})
it('renders the current IRM state for the tenant', async () => {
@@ -123,6 +128,27 @@ describe('Message Encryption page', () => {
})
})
+ it('posts the Set action with both encryption switches', async () => {
+ const user = userEvent.setup()
+ // AzureRMS already on, Encrypt button off — the state the standard fix was about
+ api.get = getResult({ data: irmConfig() })
+ renderWithProviders()
+
+ await screen.findByText('Current Configuration')
+ await user.click(screen.getByRole('switch', { name: /Show the Encrypt button/i }))
+ await user.click(screen.getByRole('button', { name: 'Submit' }))
+
+ expect(api.post.mutate).toHaveBeenCalledWith({
+ url: '/api/ExecIRMConfiguration',
+ data: {
+ tenantFilter: 'testdomain.com',
+ Action: 'Set',
+ AzureRMSLicensingEnabled: true,
+ SimplifiedClientAccessEnabled: true,
+ },
+ })
+ })
+
it('surfaces a load failure', async () => {
api.get = getResult({ isSuccess: false, isError: true, data: undefined })
renderWithProviders()
diff --git a/tests/pages/UnauthenticatedPage.test.jsx b/tests/pages/UnauthenticatedPage.test.jsx
index 49507e700c88..1a4d9e315cf6 100644
--- a/tests/pages/UnauthenticatedPage.test.jsx
+++ b/tests/pages/UnauthenticatedPage.test.jsx
@@ -18,6 +18,7 @@ vi.mock('../../src/api/ApiCall', () => ({
// /.auth/me and /version.json
return authState.swa
},
+ ApiPostCall: () => ({ mutate: vi.fn(), isPending: false }),
}))
const successResult = (data) => ({