diff --git a/CIPP-Permissions.json b/CIPP-Permissions.json
new file mode 100644
index 000000000000..96368090acd7
--- /dev/null
+++ b/CIPP-Permissions.json
@@ -0,0 +1,789 @@
+[
+ {
+ "AppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85",
+ "DisplayName": "M365 License Manager",
+ "DelegatedPermissions": [
+ {
+ "Id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f",
+ "Name": "LicenseManager.AccessAsUser",
+ "Description": "Allows the application to impersonate the signed-in user when communicating with the M365 License Manager service."
+ }
+ ],
+ "ApplicationPermissions": []
+ },
+ {
+ "AppId": "00000003-0000-0000-c000-000000000000",
+ "DisplayName": "Microsoft Graph",
+ "DelegatedPermissions": [
+ {
+ "Id": "bdfbf15f-ee85-4955-8675-146e8e5296b5",
+ "Name": "Application.ReadWrite.All",
+ "Description": "Allows the app to create, read, update and delete applications and service principals on your behalf. Does not allow management of consent grants."
+ },
+ {
+ "Id": "84bccea3-f856-4a8a-967b-dbe0a3d53a64",
+ "Name": "AppRoleAssignment.ReadWrite.All",
+ "Description": "Allows the app to manage permission grants for application permissions to any API (including Microsoft Graph) and application assignments for any app, on your behalf."
+ },
+ {
+ "Id": "e4c9e354-4dc5-45b8-9e7c-e1393b0b1a20",
+ "Name": "AuditLog.Read.All",
+ "Description": "Allows the app to read and query your audit log activities, on your behalf."
+ },
+ {
+ "Id": "b27a61ec-b99c-4d6a-b126-c4375d08ae30",
+ "Name": "BitlockerKey.Read.All",
+ "Description": "Allows the app to read BitLocker keys for your owned devices. Allows read of the recovery key."
+ },
+ {
+ "Id": "101147cf-4178-4455-9d58-02b5c164e759",
+ "Name": "Channel.Create",
+ "Description": "Create channels in any team, on your behalf."
+ },
+ {
+ "Id": "cc83893a-e232-4723-b5af-bd0b01bcfe65",
+ "Name": "Channel.Delete.All",
+ "Description": "Delete channels in any team, on your behalf."
+ },
+ {
+ "Id": "9d8982ae-4365-4f57-95e9-d6032a4c0b87",
+ "Name": "Channel.ReadBasic.All",
+ "Description": "Read channel names and channel descriptions, on your behalf."
+ },
+ {
+ "Id": "2eadaff8-0bce-4198-a6b9-2cfc35a30075",
+ "Name": "ChannelMember.Read.All",
+ "Description": "Read the members of channels, on your behalf."
+ },
+ {
+ "Id": "0c3e411a-ce45-4cd1-8f30-f99a3efa7b11",
+ "Name": "ChannelMember.ReadWrite.All",
+ "Description": "Add and remove members from channels, on your behalf. Also allows changing a member's role, for example from owner to non-owner."
+ },
+ {
+ "Id": "2b61aa8a-6d36-4b2f-ac7b-f29867937c53",
+ "Name": "ChannelMessage.Edit",
+ "Description": "Allows the app to edit channel messages in Microsoft Teams, on your behalf."
+ },
+ {
+ "Id": "767156cb-16ae-4d10-8f8b-41b657c8c8c8",
+ "Name": "ChannelMessage.Read.All",
+ "Description": "Allows the app to read a channel's messages in Microsoft Teams, on your behalf."
+ },
+ {
+ "Id": "ebf0f66e-9fb1-49e4-a278-222f76911cf4",
+ "Name": "ChannelMessage.Send",
+ "Description": "Allows the app to send channel messages in Microsoft Teams, on your behalf."
+ },
+ {
+ "Id": "233e0cf1-dd62-48bc-b65b-b38fe87fcf8e",
+ "Name": "ChannelSettings.Read.All",
+ "Description": "Read all channel names, channel descriptions, and channel settings, on your behalf."
+ },
+ {
+ "Id": "d649fb7c-72b4-4eec-b2b4-b15acf79e378",
+ "Name": "ChannelSettings.ReadWrite.All",
+ "Description": "Read and write the names, descriptions, and settings of all channels, on your behalf."
+ },
+ {
+ "Id": "f3bfad56-966e-4590-a536-82ecf548ac1e",
+ "Name": "ConsentRequest.Read.All",
+ "Description": "Allows the app to read consent requests and approvals, on your behalf."
+ },
+ {
+ "Id": "885f682f-a990-4bad-a642-36736a74b0c7",
+ "Name": "DelegatedAdminRelationship.ReadWrite.All",
+ "Description": "Allows the app to manage (create-update-terminate) Delegated Admin relationships with customers and role assignments to security groups for active Delegated Admin relationships on your behalf."
+ },
+ {
+ "Id": "41ce6ca6-6826-4807-84f1-1c82854f7ee5",
+ "Name": "DelegatedPermissionGrant.ReadWrite.All",
+ "Description": "Allows the app to manage permission grants for delegated permissions exposed by any API (including Microsoft Graph), on your behalf."
+ },
+ {
+ "Id": "bac3b9c2-b516-4ef4-bd3b-c2ef73d8d804",
+ "Name": "Device.Command",
+ "Description": "Allows the app to launch another app or communicate with another app on a device that you own."
+ },
+ {
+ "Id": "11d4cd79-5ba5-460f-803f-e22c8ab85ccd",
+ "Name": "Device.Read",
+ "Description": "Allows the app to see your list of devices."
+ },
+ {
+ "Id": "951183d1-1a61-466f-a6d1-1fde911bfd95",
+ "Name": "Device.Read.All",
+ "Description": "Allows the app to read devices' configuration information on your behalf."
+ },
+ {
+ "Id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9",
+ "Name": "DeviceLocalCredential.Read.All",
+ "Description": "Allows the app to read device local credential properties including passwords, on your behalf."
+ },
+ {
+ "Id": "7b3f05d5-f68c-4b8d-8c59-a2ecd12f24af",
+ "Name": "DeviceManagementApps.ReadWrite.All",
+ "Description": "Allows the app to read and write the properties, group assignments and status of apps, app configurations and app protection policies managed by Microsoft Intune."
+ },
+ {
+ "Id": "0883f392-0a7a-443d-8c76-16a6d39c7b63",
+ "Name": "DeviceManagementConfiguration.ReadWrite.All",
+ "Description": "Allows the app to read and write properties of Microsoft Intune-managed device configuration and device compliance policies and their assignment to groups."
+ },
+ {
+ "Id": "3404d2bf-2b13-457e-a330-c24615765193",
+ "Name": "DeviceManagementManagedDevices.PrivilegedOperations.All",
+ "Description": "Allows the app to perform remote high impact actions such as wiping the device or resetting the passcode on devices managed by Microsoft Intune."
+ },
+ {
+ "Id": "44642bfe-8385-4adc-8fc6-fe3cb2c375c3",
+ "Name": "DeviceManagementManagedDevices.ReadWrite.All",
+ "Description": "Allows the app to read and write the properties of devices managed by Microsoft Intune. Does not allow high impact operations such as remote wipe and password reset on the device’s owner."
+ },
+ {
+ "Id": "0c5e8a55-87a6-4556-93ab-adc52c4d862d",
+ "Name": "DeviceManagementRBAC.ReadWrite.All",
+ "Description": "Allows the app to read and write the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings."
+ },
+ {
+ "Id": "662ed50a-ac44-4eef-ad86-62eed9be2a29",
+ "Name": "DeviceManagementServiceConfig.ReadWrite.All",
+ "Description": "Allows the app to read and write Microsoft Intune service properties including device enrollment and third party service connection configuration."
+ },
+ {
+ "Id": "0e263e50-5827-48a4-b97c-d940288653c7",
+ "Name": "Directory.AccessAsUser.All",
+ "Description": "Allows the app to have the same access to information in your work or school directory as you do."
+ },
+ {
+ "Id": "c5366453-9fb0-48a5-a156-24f0c49a4b84",
+ "Name": "Directory.ReadWrite.All",
+ "Description": "Allows the app to read and write data in your organization's directory, such as other users, groups. It does not allow the app to delete users or groups, or reset user passwords."
+ },
+ {
+ "Id": "2f9ee017-59c1-4f1d-9472-bd5529a7b311",
+ "Name": "Domain.Read.All",
+ "Description": "Allows the app to read all domain properties on your behalf."
+ },
+ {
+ "Id": "4e46008b-f24c-477d-8fff-7bb4ec7aafe0",
+ "Name": "Group.ReadWrite.All",
+ "Description": "Allows the app to create groups and read all group properties and memberships on your behalf. Additionally allows the app to manage your groups and to update group content for groups you are a member of."
+ },
+ {
+ "Id": "f81125ac-d3b7-4573-a3b2-7099cc39df9e",
+ "Name": "GroupMember.ReadWrite.All",
+ "Description": "Allows the app to list groups, read basic properties, read and update the membership of your groups. Group properties and owners cannot be updated and groups cannot be deleted."
+ },
+ {
+ "Id": "9e4862a5-b68f-479e-848a-4e07e25c9916",
+ "Name": "IdentityRiskEvent.ReadWrite.All",
+ "Description": "Allows the app to read and update identity risk event information for all users in your organization on your behalf. Update operations include confirming risk event detections. "
+ },
+ {
+ "Id": "bb6f654c-d7fd-4ae3-85c3-fc380934f515",
+ "Name": "IdentityRiskyServicePrincipal.ReadWrite.All",
+ "Description": "Allows the app to read and update identity risky service principal information for all service principals in your organization, on your behalf. Update operations include dismissing risky service principals."
+ },
+ {
+ "Id": "e0a7cdbb-08b0-4697-8264-0069786e9674",
+ "Name": "IdentityRiskyUser.ReadWrite.All",
+ "Description": "Allows the app to read and update identity risky user information for all users in your organization on your behalf. Update operations include dismissing risky users."
+ },
+ {
+ "Id": "e383f46e-2787-4529-855e-0e479a3ffac0",
+ "Name": "Mail.Send",
+ "Description": "Allows the app to send mail as you."
+ },
+ {
+ "Id": "a367ab51-6b49-43bf-a716-a1fb06d2a174",
+ "Name": "Mail.Send.Shared",
+ "Description": "Allows the app to send mail as you or on-behalf of someone else."
+ },
+ {
+ "Id": "818c620a-27a9-40bd-a6a5-d96f7d610b4b",
+ "Name": "MailboxSettings.ReadWrite",
+ "Description": "Allows the app to read, update, create, and delete your mailbox settings."
+ },
+ {
+ "Id": "f6a3db3e-f7e8-4ed2-a414-557c8c9830be",
+ "Name": "Member.Read.Hidden",
+ "Description": "Allows the app to read the memberships of hidden groups or administrative units on your behalf, for those hidden groups or adminstrative units that you have access to."
+ },
+ {
+ "Id": "7427e0e9-2fba-42fe-b0c0-848c9e6a8182",
+ "Name": "offline_access",
+ "Description": "Allows the app to see and update the data you gave it access to, even when you are not currently using the app. This does not give the app any additional permissions."
+ },
+ {
+ "Id": "37f7f235-527c-4136-accd-4a02d197296e",
+ "Name": "openid",
+ "Description": "Allows you to sign in to the app with your work or school account and allows the app to read your basic profile information."
+ },
+ {
+ "Id": "46ca0847-7e6b-426e-9775-ea810a948356",
+ "Name": "Organization.ReadWrite.All",
+ "Description": "Allows the app to read and write the organization and related resources, on your behalf. Related resources include things like subscribed skus and tenant branding information."
+ },
+ {
+ "Id": "e67e6727-c080-415e-b521-e3f35d5248e9",
+ "Name": "PeopleSettings.ReadWrite.All",
+ "Description": "Allows the application to read and write tenant-wide people settings on your behalf."
+ },
+ {
+ "Id": "4c06a06a-098a-4063-868e-5dfee3827264",
+ "Name": "Place.ReadWrite.All",
+ "Description": "Allows the app to manage organization places (conference rooms and room lists) for calendar events and other applications, on your behalf."
+ },
+ {
+ "Id": "572fea84-0151-49b2-9301-11cb16974376",
+ "Name": "Policy.Read.All",
+ "Description": "Allows the app to read your organization's policies on your behalf."
+ },
+ {
+ "Id": "b27add92-efb2-4f16-84f5-8108ba77985c",
+ "Name": "Policy.ReadWrite.ApplicationConfiguration",
+ "Description": "Allows the app to read and write your organization's application configuration policies on your behalf. This includes policies such as activityBasedTimeoutPolicy, claimsMappingPolicy, homeRealmDiscoveryPolicy, tokenIssuancePolicy and tokenLifetimePolicy."
+ },
+ {
+ "Id": "edb72de9-4252-4d03-a925-451deef99db7",
+ "Name": "Policy.ReadWrite.AuthenticationFlows",
+ "Description": "Allows the app to read and write the authentication flow policies for your tenant, on your behalf."
+ },
+ {
+ "Id": "7e823077-d88e-468f-a337-e18f1f0e6c7c",
+ "Name": "Policy.ReadWrite.AuthenticationMethod",
+ "Description": "Allows the app to read and write the authentication method policies for your tenant, on your behalf."
+ },
+ {
+ "Id": "edd3c878-b384-41fd-95ad-e7407dd775be",
+ "Name": "Policy.ReadWrite.Authorization",
+ "Description": "Allows the app to read and write your organization's authorization policy on your behalf. For example, authorization policies can control some of the permissions that the out-of-the-box user role has by default."
+ },
+ {
+ "Id": "ad902697-1014-4ef5-81ef-2b4301988e8c",
+ "Name": "Policy.ReadWrite.ConditionalAccess",
+ "Description": "Allows the app to read and write your organization's conditional access policies on your behalf."
+ },
+ {
+ "Id": "4d135e65-66b8-41a8-9f8b-081452c91774",
+ "Name": "Policy.ReadWrite.ConsentRequest",
+ "Description": "Allows the app to read and write your organization's consent request policy on your behalf."
+ },
+ {
+ "Id": "40b534c3-9552-4550-901b-23879c90bcf9",
+ "Name": "Policy.ReadWrite.DeviceConfiguration",
+ "Description": "Allows the app to read and write your organization's device configuration policies on your behalf. For example, device registration policy can limit initial provisioning controls using quota restrictions, additional authentication and authorization checks."
+ },
+ {
+ "Id": "a8ead177-1889-4546-9387-f25e658e2a79",
+ "Name": "Policy.ReadWrite.MobilityManagement",
+ "Description": "Allows the app to read and write your organization's mobility management policies on your behalf. For example, a mobility management policy can set the enrollment scope for a given mobility management application."
+ },
+ {
+ "Id": "1d89d70c-dcac-4248-b214-903c457af83a",
+ "Name": "PrivilegedAccess.Read.AzureResources",
+ "Description": "Allows the app to read time-based assignment and just-in-time elevation of Azure resources (like your subscriptions, resource groups, storage, compute) on your behalf."
+ },
+ {
+ "Id": "a84a9652-ffd3-496e-a991-22ba5529156a",
+ "Name": "PrivilegedAccess.ReadWrite.AzureResources",
+ "Description": "Allows the app to request and manage time-based assignment and just-in-time elevation of user privileges to manage your Azure resources (like your subscriptions, resource groups, storage, compute) on your behalf."
+ },
+ {
+ "Id": "14dad69e-099b-42c9-810b-d002981feec1",
+ "Name": "profile",
+ "Description": "Allows the app to see your basic profile (e.g., name, picture, user name, email address)"
+ },
+ {
+ "Id": "02e97553-ed7b-43d0-ab3c-f8bace0d040c",
+ "Name": "Reports.Read.All",
+ "Description": "Allows an app to read all service usage reports on your behalf. Services that provide usage reports include Office 365 and Azure Active Directory."
+ },
+ {
+ "Id": "b955410e-7715-4a88-a940-dfd551018df3",
+ "Name": "ReportSettings.ReadWrite.All",
+ "Description": "Allows the app to read and update admin report settings, such as whether to display concealed information in reports, on your behalf."
+ },
+ {
+ "Id": "d01b97e9-cbc0-49fe-810a-750afd5527a3",
+ "Name": "RoleManagement.ReadWrite.Directory",
+ "Description": "Allows the app to read and manage the role-based access control (RBAC) settings for your company's directory, on your behalf. This includes instantiating directory roles and managing directory role membership, and reading directory role templates, directory roles and memberships."
+ },
+ {
+ "Id": "dc38509c-b87d-4da0-bd92-6bec988bac4a",
+ "Name": "SecurityActions.ReadWrite.All",
+ "Description": "Allows the app to read and update security actions, on your behalf."
+ },
+ {
+ "Id": "6aedf524-7e1c-45a7-bd76-ded8cab8d0fc",
+ "Name": "SecurityEvents.ReadWrite.All",
+ "Description": "Allows the app to read your organization’s security events on your behalf. Also allows you to update editable properties in security events."
+ },
+ {
+ "Id": "128ca929-1a19-45e6-a3b8-435ec44a36ba",
+ "Name": "SecurityIncident.ReadWrite.All",
+ "Description": "Allows the app to read and write to all security incidents that you have access to."
+ },
+ {
+ "Id": "55896846-df78-47a7-aa94-8d3d4442ca7f",
+ "Name": "ServiceHealth.Read.All",
+ "Description": "Allows the app to read your tenant's service health information on your behalf.Health information may include service issues or service health overviews."
+ },
+ {
+ "Id": "eda39fa6-f8cf-4c3c-a909-432c683e4c9b",
+ "Name": "ServiceMessage.Read.All",
+ "Description": "Allows the app to read your tenant's service announcement messages on your behalf. Messages may include information about new or changed features."
+ },
+ {
+ "Id": "aa07f155-3612-49b8-a147-6c590df35536",
+ "Name": "SharePointTenantSettings.ReadWrite.All",
+ "Description": "Allows the application to read and change the tenant-level settings of SharePoint and OneDrive on your behalf."
+ },
+ {
+ "Id": "89fe6a52-be36-487e-b7d8-d061c450a026",
+ "Name": "Sites.ReadWrite.All",
+ "Description": "Allow the application to edit or delete documents and list items in all site collections on your behalf."
+ },
+ {
+ "Id": "7825d5d6-6049-4ce7-bdf6-3b8d53f4bcd0",
+ "Name": "Team.Create",
+ "Description": "Allows the app to create teams on your behalf. "
+ },
+ {
+ "Id": "485be79e-c497-4b35-9400-0e3fa7f2a5d4",
+ "Name": "Team.ReadBasic.All",
+ "Description": "Read the names and descriptions of teams, on your behalf."
+ },
+ {
+ "Id": "4a06efd2-f825-4e34-813e-82a57b03d1ee",
+ "Name": "TeamMember.ReadWrite.All",
+ "Description": "Add and remove members from teams, on your behalf. Also allows changing a member's role, for example from owner to non-owner."
+ },
+ {
+ "Id": "2104a4db-3a2f-4ea0-9dba-143d457dc666",
+ "Name": "TeamMember.ReadWriteNonOwnerRole.All",
+ "Description": "Add and remove members from all teams, on your behalf. Does not allow adding or removing a member with the owner role. Additionally, does not allow the app to elevate an existing member to the owner role."
+ },
+ {
+ "Id": "0e755559-83fb-4b44-91d0-4cc721b9323e",
+ "Name": "TeamsActivity.Read",
+ "Description": "Allows the app to read your teamwork activity feed."
+ },
+ {
+ "Id": "48638b3c-ad68-4383-8ac4-e6880ee6ca57",
+ "Name": "TeamSettings.Read.All",
+ "Description": "Read all teams' settings, on your behalf."
+ },
+ {
+ "Id": "39d65650-9d3e-4223-80db-a335590d027e",
+ "Name": "TeamSettings.ReadWrite.All",
+ "Description": "Read and change all teams' settings, on your behalf."
+ },
+ {
+ "Id": "a9ff19c2-f369-4a95-9a25-ba9d460efc8e",
+ "Name": "TeamsTab.Create",
+ "Description": "Allows the app to create tabs in any team in Microsoft Teams, on your behalf. This does not grant the ability to read, modify or delete tabs after they are created, or give access to the content inside the tabs."
+ },
+ {
+ "Id": "b98bfd41-87c6-45cc-b104-e2de4f0dafb9",
+ "Name": "TeamsTab.ReadWrite.All",
+ "Description": "Read and write tabs in any team in Microsoft Teams, on your behalf. This does not give access to the content inside the tabs."
+ },
+ {
+ "Id": "cac97e40-6730-457d-ad8d-4852fddab7ad",
+ "Name": "ThreatAssessment.ReadWrite.All",
+ "Description": "Allows an app to read your organization's threat assessment requests on your behalf. Also allows the app to create new requests to assess threats received by your organization on your behalf."
+ },
+ {
+ "Id": "73e75199-7c3e-41bb-9357-167164dbb415",
+ "Name": "UnifiedGroupMember.Read.AsGuest",
+ "Description": "Allows the app to read basic unified group properties, memberships and owners of the group you are a member of."
+ },
+ {
+ "Id": "637d7bec-b31e-4deb-acc9-24275642a2c9",
+ "Name": "User.ManageIdentities.All",
+ "Description": "Allows the app to read, update and delete identities that are associated with a user's account that you have access to. This controls the identities users can sign-in with."
+ },
+ {
+ "Id": "204e0828-b5ca-4ad8-b9f3-f32a958e7cc4",
+ "Name": "User.ReadWrite.All",
+ "Description": "Allows the app to read and write the full set of profile properties, reports, and managers of other users in your organization, on your behalf."
+ },
+ {
+ "Id": "aec28ec7-4d02-4e8c-b864-50163aea77eb",
+ "Name": "UserAuthenticationMethod.Read.All",
+ "Description": "Allows the app to read authentication methods of all users you have access to in your organization. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods."
+ },
+ {
+ "Id": "48971fc1-70d7-4245-af77-0beb29b53ee2",
+ "Name": "UserAuthenticationMethod.ReadWrite",
+ "Description": "Allows the app to read and write your authentication methods, including phone numbers and Authenticator app settings.This does not allow the app to see secret information like your passwords, or to sign-in or otherwise use your authentication methods."
+ },
+ {
+ "Id": "b7887744-6746-4312-813d-72daeaee7e2d",
+ "Name": "UserAuthenticationMethod.ReadWrite.All",
+ "Description": "Allows the app to read and write authentication methods of all users you have access to in your organization. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods."
+ }
+ ],
+ "ApplicationPermissions": [
+ {
+ "Id": "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9",
+ "Name": "Application.ReadWrite.All",
+ "Description": "Allows the app to create, read, update and delete applications and service principals without a signed-in user. Does not allow management of consent grants."
+ },
+ {
+ "Id": "b0afded3-3588-46d8-8b3d-9842eff778da",
+ "Name": "AuditLog.Read.All",
+ "Description": "Allows the app to read and query your audit log activities, without a signed-in user."
+ },
+ {
+ "Id": "5e1e9171-754d-478c-812c-f1755a9a4c2d",
+ "Name": "AuditLogsQuery.Read.All",
+ "Description": "Allows the app to read and query audit logs from all services."
+ },
+ {
+ "Id": "f3a65bd4-b703-46df-8f7e-0174fea562aa",
+ "Name": "Channel.Create",
+ "Description": "Create channels in any team, without a signed-in user."
+ },
+ {
+ "Id": "59a6b24b-4225-4393-8165-ebaec5f55d7a",
+ "Name": "Channel.ReadBasic.All",
+ "Description": "Read all channel names and channel descriptions, without a signed-in user."
+ },
+ {
+ "Id": "3b55498e-47ec-484f-8136-9013221c06a9",
+ "Name": "ChannelMember.Read.All",
+ "Description": "Read the members of all channels, without a signed-in user."
+ },
+ {
+ "Id": "35930dcf-aceb-4bd1-b99a-8ffed403c974",
+ "Name": "ChannelMember.ReadWrite.All",
+ "Description": "Add and remove members from all channels, without a signed-in user. Also allows changing a member's role, for example from owner to non-owner."
+ },
+ {
+ "Id": "cac88765-0581-4025-9725-5ebc13f729ee",
+ "Name": "CrossTenantInformation.ReadBasic.All",
+ "Description": "Allows the application to obtain basic tenant information about another target tenant within the Azure AD ecosystem without a signed-in user."
+ },
+ {
+ "Id": "1138cb37-bd11-4084-a2b7-9f71582aeddb",
+ "Name": "Device.ReadWrite.All",
+ "Description": "Allows the app to read and write all device properties without a signed in user. Does not allow device creation, device deletion or update of device alternative security identifiers."
+ },
+ {
+ "Id": "78145de6-330d-4800-a6ce-494ff2d33d07",
+ "Name": "DeviceManagementApps.ReadWrite.All",
+ "Description": "Allows the app to read and write the properties, group assignments and status of apps, app configurations and app protection policies managed by Microsoft Intune, without a signed-in user."
+ },
+ {
+ "Id": "9241abd9-d0e6-425a-bd4f-47ba86e767a4",
+ "Name": "DeviceManagementConfiguration.ReadWrite.All",
+ "Description": "Allows the app to read and write properties of Microsoft Intune-managed device configuration and device compliance policies and their assignment to groups, without a signed-in user."
+ },
+ {
+ "Id": "5b07b0dd-2377-4e44-a38d-703f09a0dc3c",
+ "Name": "DeviceManagementManagedDevices.PrivilegedOperations.All",
+ "Description": "Allows the app to perform remote high impact actions such as wiping the device or resetting the passcode on devices managed by Microsoft Intune, without a signed-in user."
+ },
+ {
+ "Id": "2f51be20-0bb4-4fed-bf7b-db946066c75e",
+ "Name": "DeviceManagementManagedDevices.Read.All",
+ "Description": "Allows the app to read the properties of devices managed by Microsoft Intune, without a signed-in user."
+ },
+ {
+ "Id": "243333ab-4d21-40cb-a475-36241daa0842",
+ "Name": "DeviceManagementManagedDevices.ReadWrite.All",
+ "Description": "Allows the app to read and write the properties of devices managed by Microsoft Intune, without a signed-in user. Does not allow high impact operations such as remote wipe and password reset on the device’s owner"
+ },
+ {
+ "Id": "58ca0d9a-1575-47e1-a3cb-007ef2e4583b",
+ "Name": "DeviceManagementRBAC.Read.All",
+ "Description": "Allows the app to read the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings, without a signed-in user."
+ },
+ {
+ "Id": "e330c4f0-4170-414e-a55a-2f022ec2b57b",
+ "Name": "DeviceManagementRBAC.ReadWrite.All",
+ "Description": "Allows the app to read and write the properties relating to the Microsoft Intune Role-Based Access Control (RBAC) settings, without a signed-in user."
+ },
+ {
+ "Id": "9255e99d-faf5-445e-bbf7-cb71482737c4",
+ "Name": "DeviceManagementScripts.ReadWrite.All",
+ "Description": "Allows the app to read and write Microsoft Intune device compliance scripts, device management scripts, device shell scripts, device custom attribute shell scripts and device health scripts, without a signed-in user."
+ },
+ {
+ "Id": "06a5fe6d-c49d-46a7-b082-56b1b14103c7",
+ "Name": "DeviceManagementServiceConfig.Read.All",
+ "Description": "Allows the app to read Microsoft Intune service properties including device enrollment and third party service connection configuration, without a signed-in user."
+ },
+ {
+ "Id": "5ac13192-7ace-4fcf-b828-1a26f28068ee",
+ "Name": "DeviceManagementServiceConfig.ReadWrite.All",
+ "Description": "Allows the app to read and write Microsoft Intune service properties including device enrollment and third party service connection configuration, without a signed-in user."
+ },
+ {
+ "Id": "7ab1d382-f21e-4acd-a863-ba3e13f7da61",
+ "Name": "Directory.Read.All",
+ "Description": "Allows the app to read data in your organization's directory, such as users, groups and apps, without a signed-in user."
+ },
+ {
+ "Id": "19dbc75e-c2e2-444c-a770-ec69d8559fc7",
+ "Name": "Directory.ReadWrite.All",
+ "Description": "Allows the app to read and write data in your organization's directory, such as users, and groups, without a signed-in user. Does not allow user or group deletion."
+ },
+ {
+ "Id": "dbb9058a-0e50-45d7-ae91-66909b5d4664",
+ "Name": "Domain.Read.All",
+ "Description": "Allows the app to read all domain properties without a signed-in user."
+ },
+ {
+ "Id": "75359482-378d-4052-8f01-80520e7db3cd",
+ "Name": "Files.ReadWrite.All",
+ "Description": "Allows the app to read, create, update and delete all files in all site collections without a signed in user."
+ },
+ {
+ "Id": "bf7b1a76-6e77-406b-b258-bf5c7720e98f",
+ "Name": "Group.Create",
+ "Description": "Allows the app to create groups without a signed-in user."
+ },
+ {
+ "Id": "5b567255-7703-4780-807c-7be8301ae99b",
+ "Name": "Group.Read.All",
+ "Description": "Allows the app to read group properties and memberships, and read conversations for all groups, without a signed-in user."
+ },
+ {
+ "Id": "62a82d76-70ea-41e2-9197-370581804d09",
+ "Name": "Group.ReadWrite.All",
+ "Description": "Allows the app to create groups, read all group properties and memberships, update group properties and memberships, and delete groups. Also allows the app to read and write conversations. All of these operations can be performed by the app without a signed-in user."
+ },
+ {
+ "Id": "dbaae8cf-10b5-4b86-a4a1-f871c94c6695",
+ "Name": "GroupMember.ReadWrite.All",
+ "Description": "Allows the app to list groups, read basic properties, read and update the membership of the groups this app has access to without a signed-in user. Group properties and owners cannot be updated and groups cannot be deleted."
+ },
+ {
+ "Id": "19da66cb-0fb0-4390-b071-ebc76a349482",
+ "Name": "InformationProtectionPolicy.Read.All",
+ "Description": "Allows an app to read published sensitivity labels and label policy settings for the entire organization or a specific user, without a signed in user."
+ },
+ {
+ "Id": "6931bccd-447a-43d1-b442-00a195474933",
+ "Name": "MailboxSettings.ReadWrite",
+ "Description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail."
+ },
+ {
+ "Id": "292d869f-3427-49a8-9dab-8c70152b74e9",
+ "Name": "Organization.ReadWrite.All",
+ "Description": "Allows the app to read and write the organization and related resources, without a signed-in user. Related resources include things like subscribed skus and tenant branding information."
+ },
+ {
+ "Id": "b6890674-9dd5-4e42-bb15-5af07f541ae1",
+ "Name": "PeopleSettings.ReadWrite.All",
+ "Description": "Allows the application to read and write tenant-wide people settings without a signed-in user."
+ },
+ {
+ "Id": "913b9306-0ce1-42b8-9137-6a7df690a760",
+ "Name": "Place.Read.All",
+ "Description": "Allows the app to read company places (conference rooms and room lists) for calendar events and other applications, without a signed-in user."
+ },
+ {
+ "Id": "246dd0d5-5bd0-4def-940b-0421030a5b68",
+ "Name": "Policy.Read.All",
+ "Description": "Allows the app to read all your organization's policies without a signed in user."
+ },
+ {
+ "Id": "be74164b-cff1-491c-8741-e671cb536e13",
+ "Name": "Policy.ReadWrite.ApplicationConfiguration",
+ "Description": "Allows the app to read and write your organization's application configuration policies, without a signed-in user. This includes policies such as activityBasedTimeoutPolicy, claimsMappingPolicy, homeRealmDiscoveryPolicy, tokenIssuancePolicy and tokenLifetimePolicy."
+ },
+ {
+ "Id": "25f85f3c-f66c-4205-8cd5-de92dd7f0cec",
+ "Name": "Policy.ReadWrite.AuthenticationFlows",
+ "Description": "Allows the app to read and write all authentication flow policies for the tenant, without a signed-in user."
+ },
+ {
+ "Id": "29c18626-4985-4dcd-85c0-193eef327366",
+ "Name": "Policy.ReadWrite.AuthenticationMethod",
+ "Description": "Allows the app to read and write all authentication method policies for the tenant, without a signed-in user. "
+ },
+ {
+ "Id": "01c0a623-fc9b-48e9-b794-0756f8e8f067",
+ "Name": "Policy.ReadWrite.ConditionalAccess",
+ "Description": "Allows the app to read and write your organization's conditional access policies, without a signed-in user."
+ },
+ {
+ "Id": "999f8c63-0a38-4f1b-91fd-ed1947bdd1a9",
+ "Name": "Policy.ReadWrite.ConsentRequest",
+ "Description": "Allows the app to read and write your organization's consent requests policy without a signed-in user."
+ },
+ {
+ "Id": "338163d7-f101-4c92-94ba-ca46fe52447c",
+ "Name": "Policy.ReadWrite.CrossTenantAccess",
+ "Description": "Allows the app to read and write your organization's cross tenant access policies without a signed-in user."
+ },
+ {
+ "Id": "2f6817f8-7b12-4f0f-bc18-eeaf60705a9e",
+ "Name": "PrivilegedAccess.ReadWrite.AzureADGroup",
+ "Description": "Allows the app to request and manage time-based assignment and just-in-time elevation (including scheduled elevation) of Azure AD groups in your organization, without a signed-in user."
+ },
+ {
+ "Id": "230c1aed-a721-4c5d-9cb4-a90514e508ef",
+ "Name": "Reports.Read.All",
+ "Description": "Allows an app to read all service usage reports without a signed-in user. Services that provide usage reports include Office 365 and Azure Active Directory."
+ },
+ {
+ "Id": "2a60023f-3219-47ad-baa4-40e17cd02a1d",
+ "Name": "ReportSettings.ReadWrite.All",
+ "Description": "Allows the app to read and update all admin report settings, such as whether to display concealed information in reports, without a signed-in user."
+ },
+ {
+ "Id": "04c55753-2244-4c25-87fc-704ab82a4f69",
+ "Name": "SecurityAnalyzedMessage.ReadWrite.All",
+ "Description": "Read email metadata and security detection details, and execute remediation actions like deleting an email, without a signed-in user."
+ },
+ {
+ "Id": "bf394140-e372-4bf9-a898-299cfc7564e5",
+ "Name": "SecurityEvents.Read.All",
+ "Description": "Allows the app to read your organization’s security events without a signed-in user."
+ },
+ {
+ "Id": "45cc0394-e837-488b-a098-1918f48d186c",
+ "Name": "SecurityIncident.Read.All",
+ "Description": "Allows the app to read all security incidents, without a signed-in user."
+ },
+ {
+ "Id": "34bf0e97-1971-4929-b999-9e2442d941d7",
+ "Name": "SecurityIncident.ReadWrite.All",
+ "Description": "Allows the app to read and write to all security incidents, without a signed-in user."
+ },
+ {
+ "Id": "19b94e34-907c-4f43-bde9-38b1909ed408",
+ "Name": "SharePointTenantSettings.ReadWrite.All",
+ "Description": "Allows the application to read and change the tenant-level settings of SharePoint and OneDrive, without a signed-in user."
+ },
+ {
+ "Id": "a82116e5-55eb-4c41-a434-62fe8a61c773",
+ "Name": "Sites.FullControl.All",
+ "Description": "Allows the app to have full control of all site collections without a signed in user."
+ },
+ {
+ "Id": "0121dc95-1b9f-4aed-8bac-58c5ac466691",
+ "Name": "TeamMember.ReadWrite.All",
+ "Description": "Add and remove members from all teams, without a signed-in user. Also allows changing a team member's role, for example from owner to non-owner."
+ },
+ {
+ "Id": "4437522e-9a86-4a41-a7da-e380edd4a97d",
+ "Name": "TeamMember.ReadWriteNonOwnerRole.All",
+ "Description": "Add and remove members from all teams, without a signed-in user. Does not allow adding or removing a member with the owner role. Additionally, does not allow the app to elevate an existing member to the owner role."
+ },
+ {
+ "Id": "741f803b-c850-494e-b5df-cde7c675a1ca",
+ "Name": "User.ReadWrite.All",
+ "Description": "Allows the app to read and update user profiles without a signed in user."
+ },
+ {
+ "Id": "50483e42-d915-4231-9639-7fdb7fd190e5",
+ "Name": "UserAuthenticationMethod.ReadWrite.All",
+ "Description": "Allows the application to read and write authentication methods of all users in your organization, without a signed-in user. Authentication methods include things like a user’s phone numbers and Authenticator app settings. This does not allow the app to see secret information like passwords, or to sign-in or otherwise use the authentication methods"
+ }
+ ]
+ },
+ {
+ "AppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd",
+ "DisplayName": "Microsoft Partner Center",
+ "DelegatedPermissions": [
+ {
+ "Id": "1cebfa2a-fb4d-419e-b5f9-839b4383e05a",
+ "Name": "user_impersonation",
+ "Description": "Allow the application to access Partner Center on your behalf"
+ }
+ ],
+ "ApplicationPermissions": []
+ },
+ {
+ "AppId": "00000002-0000-0ff1-ce00-000000000000",
+ "DisplayName": "Office 365 Exchange Online",
+ "DelegatedPermissions": [
+ {
+ "Id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c",
+ "Name": "Exchange.Manage",
+ "Description": "Allows the app to manage your organization's Exchange environment, such as mailboxes, groups, and other configuration objects. To enable management actions, an admin must assign you the appropriate roles."
+ },
+ {
+ "Id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415",
+ "Name": "Calendars.ReadWrite.All",
+ "Description": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars. "
+ },
+ {
+ "Id": "2e83d72d-8895-4b66-9eea-abb43449ab8b",
+ "Name": "MailboxSettings.ReadWrite",
+ "Description": "Allows the app to read, update, create, and delete your mailbox settings."
+ }
+ ],
+ "ApplicationPermissions": [
+ {
+ "Id": "dc50a0fb-09a3-484d-be87-e023b12c6440",
+ "Name": "Exchange.ManageAsApp",
+ "Description": "Allows the app to manage the organization's Exchange environment without any user interaction. This includes mailboxes, groups, and other configuration objects. To enable management actions, an admin must assign the appropriate roles directly to the app."
+ },
+ {
+ "Id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99",
+ "Name": "Calendars.ReadWrite.All",
+ "Description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user."
+ },
+ {
+ "Id": "f9156939-25cd-4ba8-abfe-7fabcf003749",
+ "Name": "MailboxSettings.ReadWrite",
+ "Description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail."
+ }
+ ]
+ },
+ {
+ "AppId": "00000003-0000-0ff1-ce00-000000000000",
+ "DisplayName": "Office 365 SharePoint Online",
+ "DelegatedPermissions": [
+ {
+ "Id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0",
+ "Name": "AllSites.FullControl",
+ "Description": "Allows the app to have full control of all site collections on your behalf."
+ },
+ {
+ "Id": "AllProfiles.Manage",
+ "Name": "AllProfiles.Manage",
+ "Description": "Manually added"
+ }
+ ],
+ "ApplicationPermissions": []
+ },
+ {
+ "AppId": "48ac35b8-9aa8-4d74-927d-1f4a14a0b239",
+ "DisplayName": "Skype and Teams Tenant Admin API",
+ "DelegatedPermissions": [
+ {
+ "Id": "e60370c1-e451-437e-aa6e-d76df38e5f15",
+ "Name": "user_impersonation",
+ "Description": "Access Microsoft Teams and Skype for Business data based on the user's role membership"
+ }
+ ],
+ "ApplicationPermissions": []
+ },
+ {
+ "AppId": "fc780465-2017-40d4-a0c5-307022471b92",
+ "DisplayName": "WindowsDefenderATP",
+ "DelegatedPermissions": [
+ {
+ "Id": "63a677ce-818c-4409-9d12-5c6d2e2a6bfe",
+ "Name": "Vulnerability.Read",
+ "Description": "Allows the app to read Threat and Vulnerability Management vulnerability information on behalf of the signed-in user"
+ }
+ ],
+ "ApplicationPermissions": [
+ {
+ "Id": "41269fc5-d04d-4bfd-bce7-43a51cea049a",
+ "Name": "Vulnerability.Read.All",
+ "Description": "Allows the app to read any Threat and Vulnerability Management vulnerability information"
+ }
+ ]
+ }
+]
diff --git a/CIPPTimers.json b/CIPPTimers.json
index 51b0debdbb9f..cbd1366a5363 100644
--- a/CIPPTimers.json
+++ b/CIPPTimers.json
@@ -80,14 +80,6 @@
"RunOnProcessor": true,
"PreferredProcessor": "standards"
},
- {
- "Id": "5113c66d-c040-42df-9565-39dff90ddd55",
- "Command": "Start-CIPPGraphSubscriptionCleanupTimer",
- "Description": "Orchestrator to cleanup old Graph subscriptions",
- "Cron": "0 0 0 * * *",
- "Priority": 5,
- "RunOnProcessor": true
- },
{
"Id": "97145a1d-28f0-4bb2-b929-5a43517d23cc",
"Command": "Start-SchedulerOrchestrator",
diff --git a/Config/standards.json b/Config/standards.json
index 92ef2e2806f5..dcc0335ddb71 100644
--- a/Config/standards.json
+++ b/Config/standards.json
@@ -1722,6 +1722,35 @@
"powershellEquivalent": "New-ProtectionAlert and Set-ProtectionAlert",
"recommendedBy": []
},
+ {
+ "name": "standards.SafeLinksTemplatePolicy",
+ "label": "SafeLinks Policy Template",
+ "cat": "Templates",
+ "multiple": false,
+ "disabledFeatures": {
+ "report": false,
+ "warn": false,
+ "remediate": false
+ },
+ "impact": "Medium Impact",
+ "addedDate": "2025-04-29",
+ "helpText": "Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.",
+ "addedComponent": [
+ {
+ "type": "autoComplete",
+ "multiple": true,
+ "creatable": false,
+ "name": "standards.SafeLinksTemplatePolicy.TemplateIds",
+ "label": "Select SafeLinks Policy Templates",
+ "api": {
+ "url": "/api/ListSafeLinksPolicyTemplates",
+ "labelField": "TemplateName",
+ "valueField": "GUID",
+ "queryKey": "ListSafeLinksPolicyTemplates"
+ }
+ }
+ ]
+ },
{
"name": "standards.SafeLinksPolicy",
"cat": "Defender Standards",
diff --git a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1 b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1
index 455b2dcad9c4..1b6674d6e203 100644
--- a/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1
+++ b/Modules/CIPPCore/Public/Add-CIPPGroupMember.ps1
@@ -7,21 +7,22 @@ function Add-CIPPGroupMember(
[string]$APIName = 'Add Group Member'
) {
try {
- if ($member -like '*#EXT#*') { $member = [System.Web.HttpUtility]::UrlEncode($member) }
- $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($member)" -tenantid $TenantFilter).id
- $addmemberbody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }"
+ if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) }
+ $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Member)" -tenantid $TenantFilter).id
+ $AddMemberBody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }"
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
- $Params = @{ Identity = $GroupId; Member = $member; BypassSecurityGroupManagerCheck = $true }
- $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true
+ $Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true }
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $Params -UseSystemMailbox $true
} else {
- $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $addmemberbody -Verbose
+ $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $TenantFilter -type patch -body $AddMemberBody -Verbose
}
- $Message = "Successfully added user $($Member) to $($GroupId)."
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -Sev 'Info'
- return $message
+ $Results = "Successfully added user $($Member) to $($GroupId)."
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'Info'
+ return $Results
} catch {
- $message = "Failed to add user $($Member) to $($GroupId) - $($_.Exception.Message)"
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $message -Sev 'error' -LogData (Get-CippException -Exception $_)
- return $message
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Failed to add user $($Member) to $($GroupId) - $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev 'error' -LogData $ErrorMessage
+ throw $Results
}
}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1
index cef5ba03ba14..ac0a0026ae58 100644
--- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertAppSecretExpiry.ps1
@@ -18,13 +18,22 @@ function Get-CIPPAlertAppSecretExpiry {
return
}
- $AlertData = foreach ($App in $applist) {
+ $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new()
+
+ foreach ($App in $applist) {
Write-Host "checking $($App.displayName)"
if ($App.passwordCredentials) {
foreach ($Credential in $App.passwordCredentials) {
if ($Credential.endDateTime -lt (Get-Date).AddDays(30) -and $Credential.endDateTime -gt (Get-Date).AddDays(-7)) {
Write-Host ("Application '{0}' has secrets expiring on {1}" -f $App.displayName, $Credential.endDateTime)
- @{ DisplayName = $App.displayName; Expires = $Credential.endDateTime }
+
+ $Message = [PSCustomObject]@{
+ AppName = $App.displayName
+ AppId = $App.appId
+ Expires = $Credential.endDateTime
+ Tenant = $TenantFilter
+ }
+ $AlertData.Add($Message)
}
}
}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1
new file mode 100644
index 000000000000..3d9805f1fa0a
--- /dev/null
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertGlobalAdminNoAltEmail.ps1
@@ -0,0 +1,31 @@
+function Get-CIPPAlertGlobalAdminNoAltEmail {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ #>
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory = $false)]
+ [Alias('input')]
+ $InputValue,
+ $TenantFilter
+ )
+ try {
+ # Get all Global Admin accounts using the role template ID
+ $globalAdmins = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$select=id,displayName,userPrincipalName,otherMails" -tenantid $($TenantFilter) -AsApp $true | Where-Object {
+ $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' -and $_.'@odata.type' -eq '#microsoft.graph.user'
+ }
+
+ # Filter for Global Admins without alternate email addresses
+ $adminsWithoutAltEmail = $globalAdmins | Where-Object {
+ $null -eq $_.otherMails -or $_.otherMails.Count -eq 0
+ }
+
+ if ($adminsWithoutAltEmail.Count -gt 0) {
+ $AlertData = "The following Global Admin accounts do not have an alternate email address set: $($adminsWithoutAltEmail.userPrincipalName -join ', ')"
+ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
+ }
+ } catch {
+ Write-LogMessage -message "Failed to check alternate email status for Global Admins: $($_.exception.message)" -API 'Global Admin Alt Email Alerts' -tenant $TenantFilter -sev Error
+ }
+}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1
index 0efe50733dc5..a949986da6a3 100644
--- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertHuntressRogueApps.ps1
@@ -13,10 +13,8 @@ function Get-CIPPAlertHuntressRogueApps {
Param (
[Parameter(Mandatory = $false)]
[Alias('input')]
- $InputValue,
- $TenantFilter,
- [Parameter(Mandatory = $false)]
- [bool]$IgnoreDisabledApps = $false
+ [bool]$InputValue = $false,
+ $TenantFilter
)
try {
@@ -24,7 +22,7 @@ function Get-CIPPAlertHuntressRogueApps {
$RogueAppFilter = $RogueApps.appId -join "','"
$ServicePrincipals = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId in ('$RogueAppFilter')" -tenantid $TenantFilter
# If IgnoreDisabledApps is true, filter out disabled service principals
- if ($IgnoreDisabledApps) {
+ if ($InputValue) {
$ServicePrincipals = $ServicePrincipals | Where-Object { $_.accountEnabled -eq $true }
}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1
index c8907e481339..2a0fb9ff869e 100644
--- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertInactiveLicensedUsers.ps1
@@ -8,28 +8,60 @@ function Get-CIPPAlertInactiveLicensedUsers {
[Parameter(Mandatory = $false)]
[Alias('input')]
$InputValue,
+ [Parameter(Mandatory = $false)]
+ [switch]$IncludeNeverSignedIn, # Include users who have never signed in (default is to skip them), future use would allow this to be set in an alert configuration
$TenantFilter
)
try {
try {
+ $Lookup = (Get-Date).AddDays(-90).ToUniversalTime()
+
+ # Build base filter - cannot filter assignedLicenses server-side
+ $BaseFilter = if ($InputValue -eq $true) { "accountEnabled eq true" } else { "" }
- $Lookup = (Get-Date).AddDays(-90).ToUniversalTime().ToString('o')
- $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$filter=(signInActivity/lastNonInteractiveSignInDateTime le $Lookup)&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses" -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter |
- Where-Object { $null -ne $_.assignedLicenses.skuId }
+ $Uri = if ($BaseFilter) {
+ "https://graph.microsoft.com/beta/users?`$filter=$BaseFilter&`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
+ } else {
+ "https://graph.microsoft.com/beta/users?`$select=id,UserPrincipalName,signInActivity,mail,userType,accountEnabled,assignedLicenses"
+ }
+
+ $GraphRequest = New-GraphGetRequest -uri $Uri -scope 'https://graph.microsoft.com/.default' -tenantid $TenantFilter |
+ Where-Object { $null -ne $_.assignedLicenses -and $_.assignedLicenses.Count -gt 0 }
- # true = only active users
- if ($InputValue -eq $true) { $GraphRequest = $GraphRequest | Where-Object { $_.accountEnabled -eq $true } }
$AlertData = foreach ($user in $GraphRequest) {
- $Message = 'User {0} has been inactive for 90 days, but still has a license assigned.' -f $user.UserPrincipalName
- $user | Select-Object -Property UserPrincipalName, signInActivity, @{Name = 'Message'; Expression = { $Message } }
+ $lastInteractive = $user.signInActivity.lastSignInDateTime
+ $lastNonInteractive = $user.signInActivity.lastNonInteractiveSignInDateTime
- }
- Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
+ # Find most recent sign-in
+ $lastSignIn = $null
+ if ($lastInteractive -and $lastNonInteractive) {
+ $lastSignIn = if ([DateTime]$lastInteractive -gt [DateTime]$lastNonInteractive) { $lastInteractive } else { $lastNonInteractive }
+ } elseif ($lastInteractive) {
+ $lastSignIn = $lastInteractive
+ } elseif ($lastNonInteractive) {
+ $lastSignIn = $lastNonInteractive
+ }
- } catch {}
+ # Check if inactive
+ $isInactive = (-not $lastSignIn) -or ([DateTime]$lastSignIn -le $Lookup)
+ # Skip users who have never signed in by default (unless IncludeNeverSignedIn is specified)
+ if (-not $IncludeNeverSignedIn -and -not $lastSignIn) { continue }
+ # Only process inactive users
+ if ($isInactive) {
+ if (-not $lastSignIn) {
+ $Message = 'User {0} has never signed in but still has a license assigned.' -f $user.UserPrincipalName
+ } else {
+ $daysSinceSignIn = [Math]::Round(((Get-Date) - [DateTime]$lastSignIn).TotalDays)
+ $Message = 'User {0} has been inactive for {1} days but still has a license assigned. Last sign-in: {2}' -f $user.UserPrincipalName, $daysSinceSignIn, $lastSignIn
+ }
+ $user | Select-Object -Property UserPrincipalName, signInActivity, @{Name = 'Message'; Expression = { $Message } }
+ }
+ }
+ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
+ } catch {}
} catch {
Write-AlertMessage -tenant $($TenantFilter) -message "Failed to check inactive users with licenses for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1
new file mode 100644
index 000000000000..5e2601a7bc36
--- /dev/null
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertLowDomainScore.ps1
@@ -0,0 +1,25 @@
+function Get-CIPPAlertLowDomainScore {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ #>
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory)]
+ $TenantFilter,
+ [Alias('input')]
+ [ValidateRange(0, 100)]
+ [int]$InputValue = 70
+ )
+
+ $DomainData = Get-CIPPDomainAnalyser -TenantFilter $TenantFilter
+ $LowScoreDomains = $DomainData | Where-Object {
+ $_.ScorePercentage -lt $InputValue -and $_.ScorePercentage -ne ''
+ } | ForEach-Object {
+ "$($_.Domain): Domain security score is $($_.ScorePercentage)%, which is below the threshold of $InputValue%. Issues: $($_.ScoreExplanation)"
+ }
+
+ if ($LowScoreDomains) {
+ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $LowScoreDomains
+ }
+}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1
new file mode 100644
index 000000000000..d8252ad12f65
--- /dev/null
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewRiskyUsers.ps1
@@ -0,0 +1,77 @@
+function Get-CIPPAlertNewRiskyUsers {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ #>
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory = $false)]
+ [Alias('input')]
+ $TenantFilter
+ )
+ $Deltatable = Get-CIPPTable -Table DeltaCompare
+ try {
+ # Check if tenant has P2 capabilities
+ $Capabilities = Get-CIPPTenantCapabilities -TenantFilter $TenantFilter
+ if (-not $Capabilities.AADPremiumService) {
+ Write-AlertMessage -tenant $($TenantFilter) -message 'Tenant does not have Azure AD Premium P2 licensing required for risky users detection'
+ return
+ }
+
+ $Filter = "PartitionKey eq 'RiskyUsersDelta' and RowKey eq '{0}'" -f $TenantFilter
+ $RiskyUsersDelta = (Get-CIPPAzDataTableEntity @Deltatable -Filter $Filter).delta | ConvertFrom-Json -ErrorAction SilentlyContinue
+
+ # Get current risky users with more detailed information
+ $NewDelta = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/identityProtection/riskyUsers' -tenantid $TenantFilter) | Select-Object userPrincipalName, riskLevel, riskState, riskDetail, riskLastUpdatedDateTime, isProcessing, history
+
+ $NewDeltatoSave = $NewDelta | ConvertTo-Json -Depth 10 -Compress -ErrorAction SilentlyContinue | Out-String
+ $DeltaEntity = @{
+ PartitionKey = 'RiskyUsersDelta'
+ RowKey = [string]$TenantFilter
+ delta = "$NewDeltatoSave"
+ }
+ Add-CIPPAzDataTableEntity @DeltaTable -Entity $DeltaEntity -Force
+
+ if ($RiskyUsersDelta) {
+ $AlertData = $NewDelta | Where-Object {
+ $_.userPrincipalName -notin $RiskyUsersDelta.userPrincipalName
+ } | ForEach-Object {
+ $riskHistory = if ($_.history) {
+ $latestHistory = $_.history | Sort-Object -Property riskLastUpdatedDateTime -Descending | Select-Object -First 1
+ "Previous Risk Level: $($latestHistory.riskLevel), Last Updated: $($latestHistory.riskLastUpdatedDateTime)"
+ }
+ else {
+ 'No previous risk history'
+ }
+
+ # Map risk level to severity
+ $severity = switch ($_.riskLevel) {
+ 'high' { 'Critical' }
+ 'medium' { 'Warning' }
+ 'low' { 'Info' }
+ default { 'Info' }
+ }
+
+ @{
+ Message = "New risky user detected: $($_.userPrincipalName)"
+ Details = @{
+ RiskLevel = $_.riskLevel
+ RiskState = $_.riskState
+ RiskDetail = $_.riskDetail
+ LastUpdated = $_.riskLastUpdatedDateTime
+ IsProcessing = $_.isProcessing
+ RiskHistory = $riskHistory
+ Severity = $severity
+ }
+ }
+ }
+
+ if ($AlertData) {
+ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
+ }
+ }
+ }
+ catch {
+ Write-AlertMessage -tenant $($TenantFilter) -message "Could not get risky users for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)"
+ }
+}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1
new file mode 100644
index 000000000000..f0bff82aecfb
--- /dev/null
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertOnedriveQuota.ps1
@@ -0,0 +1,46 @@
+function Get-CIPPAlertOneDriveQuota {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ #>
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory)]
+ $TenantFilter,
+ [Alias('input')]
+ [ValidateRange(0,100)]
+ [int]$InputValue = 90
+ )
+
+ try {
+ $Usage = New-GraphGetRequest -tenantid $TenantFilter -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')?`$format=application/json&`$top=999" -AsApp $true
+ if (!$Usage) {
+ Write-AlertMessage -tenant $($TenantFilter) -message "OneDrive quota Alert: Unable to get OneDrive usage: Error occurred: No data returned from API."
+ return
+ }
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-AlertMessage -tenant $($TenantFilter) -message "OneDrive quota Alert: Unable to get OneDrive usage: Error occurred: $ErrorMessage"
+ return
+ }
+
+ #Check if the OneDrive quota is over the threshold
+ $OverQuota = $Usage | ForEach-Object {
+ if ($_.StorageUsedInBytes -eq 0 -or $_.storageAllocatedInBytes -eq 0) { return }
+ try {
+ $UsagePercent = [math]::Round(($_.storageUsedInBytes / $_.storageAllocatedInBytes) * 100)
+ } catch { $UsagePercent = 100 }
+
+ if ($UsagePercent -gt $InputValue) {
+ $GBLeft = [math]::Round(($_.storageAllocatedInBytes - $_.storageUsedInBytes) / 1GB)
+ "$($_.ownerPrincipalName): OneDrive is $UsagePercent% full. OneDrive has $($GBLeft)GB storage left"
+ }
+
+ }
+
+ #If the quota is over the threshold, send an alert
+ if ($OverQuota) {
+ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $OverQuota
+ }
+}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1
index ed3ed22e4b95..2de5890c7f85 100644
--- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSharepointQuota.ps1
@@ -1,4 +1,3 @@
-
function Get-CIPPAlertSharepointQuota {
<#
.FUNCTIONALITY
@@ -12,10 +11,11 @@ function Get-CIPPAlertSharepointQuota {
$TenantFilter
)
Try {
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
- $sharepointToken = (Get-GraphToken -scope "https://$($tenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter)
- $sharepointToken.Add('accept', 'application/json')
- $sharepointQuota = (Invoke-RestMethod -Method 'GET' -Headers $sharepointToken -Uri "https://$($tenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
+ $extraHeaders = @{
+ 'Accept' = 'application/json'
+ }
+ $sharepointQuota = (New-GraphGetRequest -extraHeaders $extraHeaders -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2")
} catch {
return
}
@@ -31,4 +31,4 @@ function Get-CIPPAlertSharepointQuota {
Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
}
}
-}
\ No newline at end of file
+}
diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1
new file mode 100644
index 000000000000..ad1ff546b89c
--- /dev/null
+++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertVulnerabilities.ps1
@@ -0,0 +1,65 @@
+function Get-CIPPAlertVulnerabilities {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ #>
+ [CmdletBinding()]
+ Param (
+ [Parameter(Mandatory = $false)]
+ [Alias('input')]
+ $InputValue,
+ $TenantFilter
+ )
+
+ try {
+ $VulnerabilityRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999&`$filter=cveId ne null" -scope 'https://api.securitycenter.microsoft.com/.default'
+
+ if ($VulnerabilityRequest) {
+ $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new()
+
+ # Group by CVE ID and create objects for each vulnerability
+ $VulnerabilityGroups = $VulnerabilityRequest | Where-Object { $_.cveId } | Group-Object cveId
+
+ foreach ($Group in $VulnerabilityGroups) {
+ $FirstVuln = $Group.Group | Sort-Object firstSeenTimestamp | Select-Object -First 1
+ $HoursOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalHours)
+
+ # Skip if vulnerability is not old enough
+ if ($HoursOld -lt [int]$InputValue) {
+ continue
+ }
+
+ $DaysOld = [math]::Round(((Get-Date) - [datetime]$FirstVuln.firstSeenTimestamp).TotalDays)
+ $AffectedDevices = ($Group.Group | Select-Object -ExpandProperty deviceName -Unique) -join ', '
+
+ $VulnerabilityAlert = [PSCustomObject]@{
+ CVE = $Group.Name
+ Severity = $FirstVuln.vulnerabilitySeverityLevel
+ FirstSeenTimestamp = $FirstVuln.firstSeenTimestamp
+ LastSeenTimestamp = $FirstVuln.lastSeenTimestamp
+ DaysOld = $DaysOld
+ HoursOld = $HoursOld
+ AffectedDeviceCount = $Group.Count
+ AffectedDevices = $AffectedDevices
+ SoftwareName = $FirstVuln.softwareName
+ SoftwareVendor = $FirstVuln.softwareVendor
+ SoftwareVersion = $FirstVuln.softwareVersion
+ CVSSScore = $FirstVuln.cvssScore
+ ExploitabilityLevel = $FirstVuln.exploitabilityLevel
+ RecommendedUpdate = $FirstVuln.recommendedSecurityUpdate
+ RecommendedUpdateId = $FirstVuln.recommendedSecurityUpdateId
+ RecommendedUpdateUrl = $FirstVuln.recommendedSecurityUpdateUrl
+ Tenant = $TenantFilter
+ }
+ $AlertData.Add($VulnerabilityAlert)
+ }
+
+ # Only send alert if we have vulnerabilities that meet the criteria
+ if ($AlertData.Count -gt 0) {
+ Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData
+ }
+ }
+ } catch {
+ Write-LogMessage -message "Failed to check vulnerabilities: $($_.exception.message)" -API 'Vulnerability Alerts' -tenant $TenantFilter -sev Error
+ }
+}
diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1
index 004b327c21a5..af8a7ff8dc2f 100644
--- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1
+++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1
@@ -123,6 +123,12 @@ function Test-CIPPAccess {
}
$BaseRole = $null
+
+ if ($User.userRoles -contains 'superadmin') {
+ $User.userRoles = @('superadmin')
+ } elseif ($User.userRoles -contains 'admin') {
+ $User.userRoles = @('admin')
+ }
foreach ($Role in $BaseRoles.PSObject.Properties) {
foreach ($UserRole in $User.userRoles) {
if ($Role.Name -eq $UserRole) {
@@ -131,6 +137,7 @@ function Test-CIPPAccess {
}
}
}
+
}
# Check base role permissions before continuing to custom roles
diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1
index 34ed6784f272..96b434147f30 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1
@@ -30,6 +30,8 @@ function Push-DomainAnalyserTenant {
'*.signature365.net'
'*.myteamsconnect.io'
'*.teams.dstny.com'
+ '*.msteams.8x8.com'
+ '*.ucconnect.co.uk'
)
$Domains = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $Tenant.customerId | Where-Object { $_.isVerified -eq $true } | ForEach-Object {
$Domain = $_
diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1
new file mode 100644
index 000000000000..180c7c06fda9
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Maintenance/Push-TableCleanupTask.ps1
@@ -0,0 +1,67 @@
+function Push-TableCleanupTask {
+ [CmdletBinding(SupportsShouldProcess = $true)]
+ param (
+ [Parameter(Mandatory = $true)]
+ $Item
+ )
+
+ $Type = $Item.Type
+ Write-Information "#### Starting $($Type) task..."
+ if ($PSCmdlet.ShouldProcess('Start-TableCleanup', 'Starting Table Cleanup')) {
+ if ($Type -eq 'DeleteTable') {
+ $DeleteTables = $Item.Tables
+ foreach ($Table in $DeleteTables) {
+ try {
+ $Table = Get-CIPPTable -tablename $Table
+ if ($Table) {
+ Write-Information "Deleting table $($Table.Context.TableName)"
+ try {
+ Remove-AzDataTable -Context $Table.Context -Force
+ } catch {
+ #Write-LogMessage -API 'TableCleanup' -message "Failed to delete table $($Table.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_)
+ }
+ }
+ } catch {
+ Write-Information "Table $Table not found"
+ }
+ }
+ } elseif ($Type -eq 'CleanupRule') {
+ if ($Item.Where) {
+ $Where = [scriptblock]::Create($Item.Where)
+ } else {
+ $Where = { $true }
+ }
+
+ $DataTableProps = $Item.DataTableProps | ConvertTo-Json | ConvertFrom-Json -AsHashtable
+ $Table = Get-CIPPTable -tablename $Item.TableName
+ $CleanupCompleted = $false
+ do {
+ Write-Information "Fetching entities from $($Item.TableName) with filter: $($DataTableProps.Filter)"
+ try {
+ $Entities = Get-AzDataTableEntity @Table @DataTableProps | Where-Object $Where
+ if ($Entities) {
+ Write-Information "Removing $($Entities.Count) entities from $($Item.TableName)"
+ try {
+ Remove-AzDataTableEntity @Table -Entity $Entities -Force
+ if ($DataTableProps.First -and $Entities.Count -lt $DataTableProps.First) {
+ $CleanupCompleted = $true
+ }
+ } catch {
+ Write-LogMessage -API 'TableCleanup' -message "Failed to remove entities from $($Item.TableName)" -sev Error -LogData (Get-CippException -Exception $_)
+ $CleanupCompleted = $true
+ }
+ } else {
+ Write-Information "No entities found for cleanup in $($Item.TableName)"
+ $CleanupCompleted = $true
+ }
+ } catch {
+ Write-Warning "Failed to fetch entities from $($Item.TableName): $($_.Exception.Message)"
+ $CleanupCompleted = $true
+ }
+ } while (!$CleanupCompleted)
+ } else {
+ Write-Warning "Unknown task type: $Type"
+ }
+ }
+ Write-Information "#### $($Type) task complete"
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1
index 210dbda78e27..5473c27351ca 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecOnboardTenantQueue.ps1
@@ -366,6 +366,9 @@ function Push-ExecOnboardTenantQueue {
foreach ($AllTenantsTemplate in $ExistingTemplates) {
$object = $AllTenantesTemplate.JSON | ConvertFrom-Json
$NewExcludedTenants = [system.collections.generic.list[object]]::new()
+ if (!$object.excludedTenants) {
+ $object | Add-Member -MemberType NoteProperty -Name 'excludedTenants' -Value @() -Force
+ }
foreach ($Tenant in $object.excludedTenants) {
$NewExcludedTenants.Add($Tenant)
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1
new file mode 100644
index 000000000000..d194984ff027
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListAdminPortalLicenses.ps1
@@ -0,0 +1,24 @@
+function Invoke-ListAdminPortalLicenses {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ CIPP.Core.Read
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $TenantFilter = $Request.Query.tenantFilter
+
+ try {
+ $AdminPortalLicenses = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $TenantFilter -Uri 'https://admin.microsoft.com/admin/api/tenant/accountSkus'
+ } catch {
+ Write-Warning 'Failed to get Admin Portal Licenses'
+ $AdminPortalLicenses = @()
+ }
+
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::OK
+ Body = @($AdminPortalLicenses)
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1
index 612918dfe20b..e584f92092a9 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecAccessChecks.ps1
@@ -1,6 +1,6 @@
using namespace System.Net
-Function Invoke-ExecAccessChecks {
+function Invoke-ExecAccessChecks {
<#
.FUNCTIONALITY
Entrypoint
@@ -50,16 +50,19 @@ Function Invoke-ExecAccessChecks {
$Results = foreach ($Tenant in $Tenants) {
$TenantCheck = $AccessChecks | Where-Object -Property RowKey -EQ $Tenant.customerId | Select-Object -Property Data
$TenantResult = [PSCustomObject]@{
- TenantId = $Tenant.customerId
- TenantName = $Tenant.displayName
- DefaultDomainName = $Tenant.defaultDomainName
- GraphStatus = 'Not run yet'
- ExchangeStatus = 'Not run yet'
- GDAPRoles = ''
- MissingRoles = ''
- LastRun = ''
- GraphTest = ''
- ExchangeTest = ''
+ TenantId = $Tenant.customerId
+ TenantName = $Tenant.displayName
+ DefaultDomainName = $Tenant.defaultDomainName
+ GraphStatus = 'Not run yet'
+ ExchangeStatus = 'Not run yet'
+ GDAPRoles = ''
+ MissingRoles = ''
+ LastRun = ''
+ GraphTest = ''
+ ExchangeTest = ''
+ OrgManagementRoles = @()
+ OrgManagementRolesMissing = @()
+ OrgManagementRepairNeeded = $false
}
if ($TenantCheck) {
$Data = @($TenantCheck.Data | ConvertFrom-Json -ErrorAction Stop)
@@ -70,6 +73,9 @@ Function Invoke-ExecAccessChecks {
$TenantResult.LastRun = $Data.LastRun
$TenantResult.GraphTest = $Data.GraphTest
$TenantResult.ExchangeTest = $Data.ExchangeTest
+ $TenantResult.OrgManagementRoles = $Data.OrgManagementRoles ? @($Data.OrgManagementRoles) : @()
+ $TenantResult.OrgManagementRolesMissing = $Data.OrgManagementRolesMissing ? @($Data.OrgManagementRolesMissing) : @()
+ $TenantResult.OrgManagementRepairNeeded = $Data.OrgManagementRolesMissing.Count -gt 0
}
$TenantResult
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1
new file mode 100644
index 000000000000..ab721161c8c1
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBrandingSettings.ps1
@@ -0,0 +1,123 @@
+using namespace System.Net
+
+Function Invoke-ExecBrandingSettings {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ CIPP.AppSettings.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $StatusCode = [HttpStatusCode]::OK
+ @{}
+
+ try {
+ $Table = Get-CIPPTable -TableName Config
+ $Filter = "PartitionKey eq 'BrandingSettings' and RowKey eq 'BrandingSettings'"
+ $BrandingConfig = Get-CIPPAzDataTableEntity @Table -Filter $Filter
+
+ if (-not $BrandingConfig) {
+ $BrandingConfig = @{
+ PartitionKey = 'BrandingSettings'
+ RowKey = 'BrandingSettings'
+ colour = '#F77F00'
+ logo = $null
+ }
+ }
+
+ $Action = if ($Request.Body.Action) { $Request.Body.Action } else { $Request.Query.Action }
+
+ $Results = switch ($Action) {
+ 'Get' {
+ @{
+ colour = $BrandingConfig.colour
+ logo = $BrandingConfig.logo
+ }
+ }
+ 'Set' {
+ $Updated = $false
+
+ if ($Request.Body.colour) {
+ $Colour = $Request.Body.colour
+ if ($Colour -match '^#[0-9A-Fa-f]{6}$') {
+ $BrandingConfig.colour = $Colour
+ $Updated = $true
+ } else {
+ $StatusCode = [HttpStatusCode]::BadRequest
+ 'Error: Invalid color format. Please use hex format (e.g., #F77F00)'
+ }
+ }
+
+ if ($Request.Body.logo) {
+ $Logo = $Request.Body.logo
+ if ($Logo -match '^data:image\/') {
+ $Base64Data = $Logo -replace '^data:image\/[^;]+;base64,', ''
+ try {
+ $ImageBytes = [Convert]::FromBase64String($Base64Data)
+ if ($ImageBytes.Length -le 2097152) {
+ Write-Host 'updating logo'
+ $BrandingConfig | Add-Member -MemberType NoteProperty -Name 'logo' -Value $Logo -Force
+ $Updated = $true
+ } else {
+ $StatusCode = [HttpStatusCode]::BadRequest
+ 'Error: Image size must be less than 2MB'
+ }
+ } catch {
+ $StatusCode = [HttpStatusCode]::BadRequest
+ 'Error: Invalid base64 image data: ' + $_.Exception.Message
+ }
+ } elseif ($Logo -eq $null -or $Logo -eq '') {
+ $BrandingConfig | Add-Member -MemberType NoteProperty -Name 'logo' -Value $null -Force
+ $Updated = $true
+ }
+ }
+
+ if ($Updated) {
+ $BrandingConfig.PartitionKey = 'BrandingSettings'
+ $BrandingConfig.RowKey = 'BrandingSettings'
+
+ Add-CIPPAzDataTableEntity @Table -Entity $BrandingConfig -Force | Out-Null
+ Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message 'Updated branding settings' -Sev 'Info'
+ 'Successfully updated branding settings'
+ } else {
+ $StatusCode = [HttpStatusCode]::BadRequest
+ 'Error: No valid branding data provided'
+ }
+ }
+ 'Reset' {
+ $DefaultConfig = @{
+ PartitionKey = 'BrandingSettings'
+ RowKey = 'BrandingSettings'
+ colour = '#F77F00'
+ logo = $null
+ }
+
+ Add-CIPPAzDataTableEntity @Table -Entity $DefaultConfig -Force | Out-Null
+ Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message 'Reset branding settings to defaults' -Sev 'Info'
+ 'Successfully reset branding settings to defaults'
+ }
+ default {
+ $StatusCode = [HttpStatusCode]::BadRequest
+ 'Error: Invalid action specified'
+ }
+ }
+ } catch {
+ Write-LogMessage -API $APIName -tenant 'Global' -headers $Request.Headers -message "Branding Settings API failed: $($_.Exception.Message)" -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ "Failed to process branding settings: $($_.Exception.Message)"
+ }
+
+ $body = [pscustomobject]@{'Results' = $Results }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1
new file mode 100644
index 000000000000..76cea5d8a031
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExchangeRoleRepair.ps1
@@ -0,0 +1,94 @@
+function Invoke-ExecExchangeRoleRepair {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ CIPP.AppSettings.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $Headers = $Request.Headers
+
+ $TenantId = $Request.Query.tenantId ?? $Request.Body.tenantId
+ $Tenant = Get-Tenants -TenantFilter $TenantId
+
+ try {
+ Write-Information "Starting Exchange Organization Management role repair for tenant: $($Tenant.defaultDomainName)"
+ $OrgManagementRoles = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-ManagementRoleAssignment' -cmdParams @{ RoleAssignee = 'Organization Management'; Delegating = $false } | Select-Object -Property Role, Guid
+ Write-Information "Found $($OrgManagementRoles.Count) Organization Management roles in Exchange"
+
+ $RoleDefinitions = New-GraphGetRequest -tenantid $Tenant.customerId -uri 'https://graph.microsoft.com/beta/roleManagement/exchange/roleDefinitions'
+ Write-Information "Found $($RoleDefinitions.Count) Exchange role definitions"
+
+ $BasePath = Get-Module -Name 'CIPPCore' | Select-Object -ExpandProperty ModuleBase
+ $AllOrgManagementRoles = Get-Content -Path "$BasePath\Public\OrganizationManagementRoles.json" -ErrorAction Stop | ConvertFrom-Json
+
+ $AvailableRoles = $RoleDefinitions | Where-Object -Property displayName -In $AllOrgManagementRoles | Select-Object -Property displayName, id, description
+ Write-Information "Found $($AvailableRoles.Count) available Organization Management roles in Exchange"
+ $MissingOrgMgmtRoles = $AvailableRoles | Where-Object { $OrgManagementRoles.Role -notcontains $_.displayName }
+
+ if ($MissingOrgMgmtRoles.Count -gt 0) {
+ $Requests = foreach ($Role in $MissingOrgMgmtRoles) {
+ [PSCustomObject]@{
+ id = $Role.id
+ method = 'POST'
+ url = 'roleManagement/exchange/roleAssignments'
+ body = @{
+ principalId = '/RoleGroups/Organization Management'
+ roleDefinitionId = $Role.id
+ directoryScopeId = '/'
+ appScopeId = $null
+ }
+ headers = @{
+ 'Content-Type' = 'application/json'
+ }
+ }
+ }
+
+ $RepairResults = New-GraphBulkRequest -tenantid $Tenant.customerId -Requests @($Requests) -asapp $true
+ $RepairSuccess = $RepairResults.status -eq 201
+ if ($RepairSuccess) {
+ $Results = @{
+ state = 'success'
+ resultText = "Successfully repaired the missing Organization Management roles: $($MissingOrgMgmtRoles.displayName -join ', ')"
+ }
+ Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Successfully repaired the missing Organization Management roles: $($MissingOrgMgmtRoles.displayName -join ', '). Run another Tenant Access check after waiting a bit for replication." -sev 'Info'
+ } else {
+ # Get roles that failed to repair
+ $FailedRoles = $RepairResults | Where-Object { $_.status -ne 201 } | ForEach-Object {
+ $RoleId = $_.id
+ $Role = $MissingOrgMgmtRoles | Where-Object { $_.id -eq $RoleId }
+ $Role.displayName
+ }
+ $PermissionError = $false
+ if ($RepairResults.status -in (401, 403, 500)) {
+ $PermissionError = $true
+ }
+ $Results = @{
+ state = 'error'
+ resultText = "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ').$(if ($PermissionError) { " This may be due to insufficient permissions. The required Graph Permission is 'Application - RoleManagement.ReadWrite.Exchange'" })"
+ }
+ Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Failed to repair the missing Organization Management roles: $($FailedRoles -join ', ')" -sev 'Error'
+ }
+ } else {
+ $Results = @{
+ state = 'success'
+ resultText = 'No missing Organization Management roles found.'
+ }
+ }
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-Warning "Exception during Exchange Organization Management role repair: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -tenant $Tenant.defaultDomainName -tenantid $Tenant.customerId -Message "Exchange Organization Management role repair failed: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage
+ $Results = @{
+ state = 'error'
+ resultText = "Exchange Organization Management role repair failed: $($ErrorMessage.NormalizedError)"
+ }
+ }
+
+ Push-OutputBinding -Name 'Response' -Value ([HttpResponseContext]@{
+ StatusCode = [System.Net.HttpStatusCode]::OK
+ Body = $Results
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1
index d0ec882cc7ba..029f21fe25be 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTenantGroup.ps1
@@ -7,7 +7,7 @@ function Invoke-ExecTenantGroup {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
- Tenant.Config.ReadWrite
+ TenantGroups.Config.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
@@ -57,9 +57,9 @@ function Invoke-ExecTenantGroup {
}
$MemberEntity = @{
PartitionKey = 'Member'
- RowKey = '{0}-{1}' -f $groupId, $member.value
- GroupId = $groupId
- customerId = $member.value
+ RowKey = '{0}-{1}' -f $groupId, $member.value
+ GroupId = $groupId
+ customerId = $member.value
}
Add-CIPPAzDataTableEntity @MembersTable -Entity $MemberEntity -Force
$Adds.Add('Added member {0}' -f $member.label)
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1
index 0bd359ccb156..be1c4131ea69 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1
@@ -22,7 +22,10 @@ function Invoke-ExecAddTenant {
# Check if tenant already exists
$ExistingTenant = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and RowKey eq '$tenantId'"
- if ($ExistingTenant) {
+ if ($tenantId -eq $env:TenantID) {
+ # If the tenant is the partner tenant, return an error because you cannot add the partner tenant as direct tenant
+ $Results = @{'message' = 'You cannot add the partner tenant as a direct tenant. Please connect the tenant using the "Connect to Partner Tenant" option. '; 'severity' = 'error'; }
+ } elseif ($ExistingTenant) {
# Update existing tenant
$ExistingTenant.delegatedPrivilegeStatus = 'directTenant'
Add-CIPPAzDataTableEntity @TenantsTable -Entity $ExistingTenant -Force | Out-Null
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1
index 6331d1e2fdc9..3fa7ac8df368 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecCombinedSetup.ps1
@@ -1,17 +1,25 @@
using namespace System.Net
-Function Invoke-ExecCombinedSetup {
+function Invoke-ExecCombinedSetup {
<#
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
CIPP.AppSettings.ReadWrite
#>
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
[CmdletBinding()]
param($Request, $TriggerMetadata)
#Make arraylist of Results
$Results = [System.Collections.ArrayList]::new()
try {
+ # Set up Azure context if needed for Key Vault access
+ if ($env:AzureWebJobsStorage -ne 'UseDevelopmentStorage=true' -and $env:MSI_SECRET) {
+ Disable-AzContextAutosave -Scope Process | Out-Null
+ $null = Connect-AzAccount -Identity
+ $SubscriptionId = $env:WEBSITE_OWNER_NAME -split '\+' | Select-Object -First 1
+ $null = Set-AzContext -SubscriptionId $SubscriptionId
+ }
if ($request.body.selectedBaselines -and $request.body.baselineOption -eq 'downloadBaselines') {
#do a single download of the selected baselines.
foreach ($template in $request.body.selectedBaselines) {
@@ -56,6 +64,47 @@ Function Invoke-ExecCombinedSetup {
$notificationResults = Set-CIPPNotificationConfig @notificationConfig
$Results.add($notificationResults)
}
+ if ($Request.Body.selectedOption -eq 'Manual') {
+ $KV = $env:WEBSITE_DEPLOYMENT_ID
+
+ if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') {
+ $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets'
+ $Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'Secret' and RowKey eq 'Secret'"
+ if (!$Secret) {
+ $Secret = [PSCustomObject]@{
+ 'PartitionKey' = 'Secret'
+ 'RowKey' = 'Secret'
+ 'TenantId' = ''
+ 'RefreshToken' = ''
+ 'ApplicationId' = ''
+ 'ApplicationSecret' = ''
+ }
+ Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force
+ }
+
+ if ($Request.Body.tenantId) { $Secret.TenantId = $Request.Body.tenantid }
+ if ($Request.Body.applicationId) { $Secret.ApplicationId = $Request.Body.applicationId }
+ if ($Request.Body.ApplicationSecret) { $Secret.ApplicationSecret = $Request.Body.ApplicationSecret }
+ Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force
+ $Results.add('Manual credentials have been set in the DevSecrets table.')
+ } else {
+ if ($Request.Body.tenantId) {
+ Set-AzKeyVaultSecret -VaultName $kv -Name 'tenantid' -SecretValue (ConvertTo-SecureString -String $Request.Body.tenantId -AsPlainText -Force)
+ $Results.add('Set tenant ID in Key Vault.')
+ }
+ if ($Request.Body.applicationId) {
+ Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationId -AsPlainText -Force)
+ $Results.add('Set application ID in Key Vault.')
+ }
+ if ($Request.Body.applicationSecret) {
+ Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationSecret -AsPlainText -Force)
+ $Results.add('Set application secret in Key Vault.')
+ }
+ }
+
+ $Results.add('Manual credentials setup has been completed.')
+ }
+
$Results.add('Setup is now complete. You may navigate away from this page and start using CIPP.')
#one more force of reauth so env vars update.
$auth = Get-CIPPAuthentication
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1
index 6a8254483607..7afce7c01f65 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1
@@ -20,8 +20,9 @@ Function Invoke-ExecUpdateRefreshToken {
if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') {
$DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets'
$Secret = Get-CIPPAzDataTableEntity @DevSecretsTable -Filter "PartitionKey eq 'Secret' and RowKey eq 'Secret'"
+
if ($env:TenantID -eq $Request.body.tenantId) {
- $Secret.RefreshToken = $Request.body.RefreshToken
+ $Secret | Add-Member -MemberType NoteProperty -Name 'RefreshToken' -Value $Request.body.refreshtoken -Force
} else {
Write-Host "$($env:TenantID) does not match $($Request.body.tenantId)"
$name = $Request.body.tenantId -replace '-', '_'
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1
new file mode 100644
index 000000000000..2568ccebe9e5
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContact.ps1
@@ -0,0 +1,107 @@
+using namespace System.Net
+
+Function Invoke-AddContact {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Contact.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $ContactObject = $Request.Body
+ $TenantId = $ContactObject.tenantid
+
+ try {
+ # Prepare the body for New-MailContact cmdlet
+ $BodyToship = @{
+ displayName = $ContactObject.displayName
+ name = $ContactObject.displayName
+ ExternalEmailAddress = $ContactObject.email
+ FirstName = $ContactObject.firstName
+ LastName = $ContactObject.lastName
+ }
+
+ # Create the mail contact first
+ $NewContact = New-ExoRequest -tenantid $TenantId -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true
+
+ # Build SetContactParams efficiently with only provided values
+ $SetContactParams = @{
+ Identity = $NewContact.id
+ }
+
+ # Helper to add non-empty values
+ $PropertyMap = @{
+ 'Title' = $ContactObject.Title
+ 'Company' = $ContactObject.Company
+ 'StreetAddress' = $ContactObject.StreetAddress
+ 'City' = $ContactObject.City
+ 'StateOrProvince' = $ContactObject.State
+ 'PostalCode' = $ContactObject.PostalCode
+ 'CountryOrRegion' = $ContactObject.CountryOrRegion
+ 'Phone' = $ContactObject.phone
+ 'MobilePhone' = $ContactObject.mobilePhone
+ 'WebPage' = $ContactObject.website
+ }
+
+ # Add only non-null/non-empty properties
+ foreach ($Property in $PropertyMap.GetEnumerator()) {
+ if (![string]::IsNullOrWhiteSpace($Property.Value)) {
+ $SetContactParams[$Property.Key] = $Property.Value
+ }
+ }
+
+ # Update the contact with additional details only if we have properties to set
+ if ($SetContactParams.Count -gt 1) {
+ Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating
+ $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true
+ }
+
+ # Check if we need to update MailContact properties
+ $needsMailContactUpdate = $false
+ $MailContactParams = @{
+ Identity = $NewContact.id
+ }
+
+ # Only add HiddenFromAddressListsEnabled if we're actually hiding from GAL
+ if ([bool]$ContactObject.hidefromGAL) {
+ $MailContactParams.HiddenFromAddressListsEnabled = $true
+ $needsMailContactUpdate = $true
+ }
+
+ # Add MailTip if provided
+ if (![string]::IsNullOrWhiteSpace($ContactObject.mailTip)) {
+ $MailContactParams.MailTip = $ContactObject.mailTip
+ $needsMailContactUpdate = $true
+ }
+
+ # Only call Set-MailContact if we have changes to make
+ if ($needsMailContactUpdate) {
+ Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating
+ $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true
+ }
+
+ # Log the result
+ $Result = "Successfully created contact $($ContactObject.displayName) with email address $($ContactObject.email)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to create contact. $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Error' -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
+
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1
new file mode 100644
index 000000000000..0537e503e980
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-AddContactTemplates.ps1
@@ -0,0 +1,68 @@
+using namespace System.Net
+
+Function Invoke-AddContactTemplates {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug
+ Write-Host ($request | ConvertTo-Json -Depth 10 -Compress)
+
+ try {
+ $GUID = (New-Guid).GUID
+
+ # Create a new ordered hashtable to store selected properties
+ $contactObject = [ordered]@{}
+
+ # Set name and comments first
+ $contactObject["name"] = $Request.body.displayName
+ $contactObject["comments"] = "Contact template created $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
+
+ # Copy specific properties we want to keep
+ $propertiesToKeep = @(
+ "displayName", "firstName", "lastName", "email", "hidefromGAL", "streetAddress", "postalCode",
+ "city", "state", "country", "companyName", "mobilePhone", "businessPhone", "jobTitle", "website", "mailTip"
+ )
+
+ # Copy each property if it exists
+ foreach ($prop in $propertiesToKeep) {
+ if ($null -ne $Request.body.$prop) {
+ $contactObject[$prop] = $Request.body.$prop
+ }
+ }
+
+ # Convert to JSON
+ $JSON = $contactObject | ConvertTo-Json -Depth 10
+
+ # Save the template to Azure Table Storage
+ $Table = Get-CippTable -tablename 'templates'
+ $Table.Force = $true
+ Add-CIPPAzDataTableEntity @Table -Entity @{
+ JSON = "$JSON"
+ RowKey = "$GUID"
+ PartitionKey = 'ContactTemplate'
+ }
+
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Created Contact Template $($contactObject.name) with GUID $GUID" -Sev Info
+ $body = [pscustomobject]@{'Results' = "Created Contact Template $($contactObject.name) with GUID $GUID" }
+ $StatusCode = [HttpStatusCode]::OK
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create Contact template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $body = [pscustomobject]@{'Results' = "Failed to create Contact template: $($ErrorMessage.NormalizedError)" }
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1
new file mode 100644
index 000000000000..14fa69806c3f
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-DeployContactTemplates.ps1
@@ -0,0 +1,184 @@
+using namespace System.Net
+
+Function Invoke-DeployContactTemplates {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Contact.ReadWrite
+ .DESCRIPTION
+ This function deploys contact(s) from template(s) to selected tenants.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ try {
+ $RequestBody = $Request.Body
+
+ # Extract tenant IDs from the selectedTenants objects - get the value property
+ $SelectedTenants = [System.Collections.Generic.List[string]]::new()
+
+ foreach ($TenantItem in $RequestBody.selectedTenants) {
+ if ($TenantItem.value) {
+ $SelectedTenants.Add($TenantItem.value)
+ } else {
+ Write-LogMessage -headers $Headers -API $APIName -message "Tenant item missing value property: $($TenantItem | ConvertTo-Json -Compress)" -Sev 'Warning'
+ }
+ }
+
+ # Handle AllTenants selection
+ if ('AllTenants' -in $SelectedTenants) {
+ $SelectedTenants = [System.Collections.Generic.List[string]]::new()
+ $AllTenantsList = (Get-Tenants).defaultDomainName
+ foreach ($Tenant in $AllTenantsList) {
+ $SelectedTenants.Add($Tenant)
+ }
+ }
+
+ # Get the contact templates from TemplateList
+ $ContactTemplates = [System.Collections.Generic.List[object]]::new()
+
+ if ($RequestBody.TemplateList -and $RequestBody.TemplateList.Count -gt 0) {
+ # Templates are provided in TemplateList format
+ foreach ($TemplateItem in $RequestBody.TemplateList) {
+ if ($TemplateItem.value) {
+ $ContactTemplates.Add($TemplateItem.value)
+ } else {
+ Write-LogMessage -headers $Headers -API $APIName -message "Template item missing value property: $($TemplateItem | ConvertTo-Json -Compress)" -Sev 'Warning'
+ }
+ }
+ } else {
+ throw "TemplateList is required and must contain at least one template"
+ }
+
+ if ($ContactTemplates.Count -eq 0) {
+ throw "No valid contact templates found to deploy"
+ }
+
+ $Results = foreach ($TenantFilter in $SelectedTenants) {
+ foreach ($ContactTemplate in $ContactTemplates) {
+ try {
+ # Check if contact with this email already exists
+ $ExistingContactsParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Get-MailContact'
+ cmdParams = @{
+ Filter = "ExternalEmailAddress -eq '$($ContactTemplate.email)'"
+ }
+ useSystemMailbox = $true
+ }
+
+ $ExistingContacts = New-ExoRequest @ExistingContactsParam
+ $ContactExists = $ExistingContacts | Where-Object { $_.ExternalEmailAddress -eq $ContactTemplate.email }
+
+ if ($ContactExists) {
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Contact with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter" -Sev 'Warning'
+ "Contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)' already exists in tenant $TenantFilter"
+ continue
+ }
+
+ # Prepare the body for New-MailContact cmdlet
+ $BodyToship = @{
+ displayName = $ContactTemplate.displayName
+ name = $ContactTemplate.displayName
+ ExternalEmailAddress = $ContactTemplate.email
+ FirstName = $ContactTemplate.firstName
+ LastName = $ContactTemplate.lastName
+ }
+
+ # Create the mail contact first
+ $NewContact = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true
+
+ # Build SetContactParams efficiently with only provided values
+ $SetContactParams = @{
+ Identity = $NewContact.id
+ }
+
+ # Helper to add non-empty values
+ $PropertyMap = @{
+ 'Title' = $ContactTemplate.jobTitle
+ 'Company' = $ContactTemplate.companyName
+ 'StreetAddress' = $ContactTemplate.streetAddress
+ 'City' = $ContactTemplate.city
+ 'StateOrProvince' = $ContactTemplate.state
+ 'PostalCode' = $ContactTemplate.postalCode
+ 'CountryOrRegion' = $ContactTemplate.country
+ 'Phone' = $ContactTemplate.businessPhone
+ 'MobilePhone' = $ContactTemplate.mobilePhone
+ 'WebPage' = $ContactTemplate.website
+ }
+
+ # Add only non-null/non-empty properties
+ foreach ($Property in $PropertyMap.GetEnumerator()) {
+ if (![string]::IsNullOrWhiteSpace($Property.Value)) {
+ $SetContactParams[$Property.Key] = $Property.Value
+ }
+ }
+
+ # Update the contact with additional details only if we have properties to set
+ if ($SetContactParams.Count -gt 1) {
+ Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true
+ }
+
+ # Check if we need to update MailContact properties
+ $needsMailContactUpdate = $false
+ $MailContactParams = @{
+ Identity = $NewContact.id
+ }
+
+ # Only add HiddenFromAddressListsEnabled if we're actually hiding from GAL
+ if ([bool]$ContactTemplate.hidefromGAL) {
+ $MailContactParams.HiddenFromAddressListsEnabled = $true
+ $needsMailContactUpdate = $true
+ }
+
+ # Add MailTip if provided
+ if (![string]::IsNullOrWhiteSpace($ContactTemplate.mailTip)) {
+ $MailContactParams.MailTip = $ContactTemplate.mailTip
+ $needsMailContactUpdate = $true
+ }
+
+ # Only call Set-MailContact if we have changes to make
+ if ($needsMailContactUpdate) {
+ Start-Sleep -Milliseconds 500 # Ensure the contact is created before updating
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true
+ }
+
+ # Log the result
+ $ContactResult = "Successfully created contact '$($ContactTemplate.displayName)' with email '$($ContactTemplate.email)'"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ContactResult -Sev 'Info'
+
+ # Return success message as a simple string
+ "Successfully deployed contact '$($ContactTemplate.displayName)' to tenant $TenantFilter"
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $ErrorDetail = "Failed to deploy contact '$($ContactTemplate.displayName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error'
+
+ # Return error message as a simple string
+ "Failed to deploy contact '$($ContactTemplate.displayName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)"
+ }
+ }
+ }
+
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Failed to process contact template deployment request. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Results}
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1
new file mode 100644
index 000000000000..3b3d458bcba2
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContact.ps1
@@ -0,0 +1,94 @@
+using namespace System.Net
+
+Function Invoke-EditContact {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Contact.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $TenantID = $Request.Body.tenantID
+
+ try {
+ # Extract contact information from the request body
+ $contactInfo = $Request.Body
+
+ # Build contact parameters with only provided values
+ $bodyForSetContact = @{
+ Identity = $contactInfo.ContactID
+ }
+
+ # Map of properties to check and add
+ $ContactPropertyMap = @{
+ 'DisplayName' = $contactInfo.displayName
+ 'WindowsEmailAddress' = $contactInfo.email
+ 'FirstName' = $contactInfo.firstName
+ 'LastName' = $contactInfo.LastName
+ 'Title' = $contactInfo.Title
+ 'StreetAddress' = $contactInfo.StreetAddress
+ 'PostalCode' = $contactInfo.PostalCode
+ 'City' = $contactInfo.City
+ 'StateOrProvince' = $contactInfo.State
+ 'CountryOrRegion' = $contactInfo.CountryOrRegion
+ 'Company' = $contactInfo.Company
+ 'MobilePhone' = $contactInfo.mobilePhone
+ 'Phone' = $contactInfo.phone
+ 'WebPage' = $contactInfo.website
+ }
+
+ # Add only non-null/non-empty properties
+ foreach ($Property in $ContactPropertyMap.GetEnumerator()) {
+ if (![string]::IsNullOrWhiteSpace($Property.Value)) {
+ $bodyForSetContact[$Property.Key] = $Property.Value
+ }
+ }
+
+ # Update contact only if we have properties to set beyond Identity
+ if ($bodyForSetContact.Count -gt 1) {
+ $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true
+ }
+
+ # Prepare mail contact specific parameters
+ $MailContactParams = @{
+ Identity = $contactInfo.ContactID
+ }
+
+ # Handle boolean conversion safely
+ if ($null -ne $contactInfo.hidefromGAL) {
+ $MailContactParams.HiddenFromAddressListsEnabled = [bool]$contactInfo.hidefromGAL
+ }
+
+ # Add MailTip if provided
+ if (![string]::IsNullOrWhiteSpace($contactInfo.mailTip)) {
+ $MailContactParams.MailTip = $contactInfo.mailTip
+ }
+
+ # Update mail contact only if we have properties to set beyond Identity
+ if ($MailContactParams.Count -gt 1) {
+ $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true
+ }
+
+ $Results = "Successfully edited contact $($contactInfo.displayName)"
+ Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Info
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Failed to edit contact. $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Error -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Results }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1
new file mode 100644
index 000000000000..de2770151466
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-EditContactTemplates.ps1
@@ -0,0 +1,84 @@
+using namespace System.Net
+
+Function Invoke-EditContactTemplates {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug
+ Write-Host ($request | ConvertTo-Json -Depth 10 -Compress)
+
+ try {
+ # Get the ContactTemplateID from the request body
+ $ContactTemplateID = $Request.body.ContactTemplateID
+
+ if (-not $ContactTemplateID) {
+ throw "ContactTemplateID is required for editing a template"
+ }
+
+ # Check if the template exists
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$ContactTemplateID'"
+ $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter
+
+ if (-not $ExistingTemplate) {
+ throw "Contact template with ID $ContactTemplateID not found"
+ }
+
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Updating Contact Template with ID: $ContactTemplateID" -Sev Info
+
+ # Create a new ordered hashtable to store selected properties
+ $contactObject = [ordered]@{}
+
+ # Set name and comments
+ $contactObject["name"] = $Request.body.displayName
+ $contactObject["comments"] = "Contact template updated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
+
+ # Copy specific properties we want to keep
+ $propertiesToKeep = @(
+ "displayName", "firstName", "lastName", "email", "hidefromGAL", "streetAddress", "postalCode",
+ "city", "state", "country", "companyName", "mobilePhone", "businessPhone", "jobTitle", "website", "mailTip"
+ )
+
+ # Copy each property from the request
+ foreach ($prop in $propertiesToKeep) {
+ if ($null -ne $Request.body.$prop) {
+ $contactObject[$prop] = $Request.body.$prop
+ }
+ }
+
+ # Convert to JSON
+ $JSON = $contactObject | ConvertTo-Json -Depth 10
+
+ # Overwrite the template in Azure Table Storage
+ $Table = Get-CippTable -tablename 'templates'
+ $Table.Force = $true
+ Add-CIPPAzDataTableEntity @Table -Entity @{
+ JSON = "$JSON"
+ RowKey = "$ContactTemplateID"
+ PartitionKey = 'ContactTemplate'
+ }
+
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Updated Contact Template $($contactObject.name) with GUID $ContactTemplateID" -Sev Info
+ $body = [pscustomobject]@{'Results' = "Updated Contact Template $($contactObject.name) with GUID $ContactTemplateID" }
+ $StatusCode = [HttpStatusCode]::OK
+
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to update Contact template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $body = [pscustomobject]@{'Results' = "Failed to update Contact template: $($ErrorMessage.NormalizedError)" }
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1
new file mode 100644
index 000000000000..7bd3bb63ef74
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContactTemplates.ps1
@@ -0,0 +1,66 @@
+using namespace System.Net
+Function Invoke-ListContactTemplates {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.Read
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $Table = Get-CippTable -tablename 'templates'
+ $Templates = Get-ChildItem 'Config\*.ContactTemplate.json' | ForEach-Object {
+ $Entity = @{
+ JSON = "$(Get-Content $_)"
+ RowKey = "$($_.name)"
+ PartitionKey = 'ContactTemplate'
+ GUID = "$($_.name)"
+ }
+ Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force
+ }
+
+ # Check if a specific template ID is requested
+ if ($Request.query.ID -or $Request.query.id) {
+ $RequestedID = $Request.query.ID ?? $Request.query.id
+ Write-LogMessage -headers $Headers -API $APIName -message "Retrieving specific template with ID: $RequestedID" -Sev 'Debug'
+
+ # Query directly for the specific template by RowKey for efficiency
+ $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$RequestedID'"
+ $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object {
+ $GUID = $_.RowKey
+ $data = $_.JSON | ConvertFrom-Json
+ $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID
+ $data
+ }
+
+ if (-not $Templates) {
+ Write-LogMessage -headers $Headers -API $APIName -message "Template with ID $RequestedID not found" -Sev 'Warning'
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::NotFound
+ Body = @{ Error = "Template with ID $RequestedID not found" }
+ })
+ return
+ }
+ } else {
+ # List all policies if no specific ID requested
+ Write-LogMessage -headers $Headers -API $APIName -message 'Retrieving all contact templates' -Sev 'Debug'
+
+ $Filter = "PartitionKey eq 'ContactTemplate'"
+ $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object {
+ $GUID = $_.RowKey
+ $data = $_.JSON | ConvertFrom-Json
+ $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID
+ $data
+ }
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::OK
+ Body = @($Templates)
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1
new file mode 100644
index 000000000000..02b9e60c623c
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-ListContacts.ps1
@@ -0,0 +1,135 @@
+using namespace System.Net
+using namespace System.Collections.Generic
+using namespace System.Text.RegularExpressions
+
+Function Invoke-ListContacts {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Contact.Read
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ # Get query parameters
+ $TenantFilter = $Request.Query.tenantFilter
+ $ContactID = $Request.Query.id
+
+ # Early validation and exit
+ if (-not $TenantFilter) {
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::BadRequest
+ Body = 'tenantFilter is required'
+ })
+ return
+ }
+
+ # Pre-compiled regex for MailTip cleaning
+ $script:HtmlTagRegex ??= [regex]::new('<[^>]+>', [RegexOptions]::Compiled)
+ $script:LineBreakRegex ??= [regex]::new('\\n|\r\n|\r', [RegexOptions]::Compiled)
+ $script:SmtpPrefixRegex ??= [regex]::new('^SMTP:', [RegexOptions]::Compiled -bor [RegexOptions]::IgnoreCase)
+
+ function ConvertTo-ContactObject {
+ param($Contact, $MailContact)
+
+ # Early exit if essential data missing
+ if (!$Contact.Id) { return $null }
+
+ $mailAddress = if ($MailContact.ExternalEmailAddress) {
+ $script:SmtpPrefixRegex.Replace($MailContact.ExternalEmailAddress, [string]::Empty, 1)
+ } else { $null }
+
+ $cleanMailTip = if ($MailContact.MailTip -and $MailContact.MailTip.Length -gt 0) {
+ $cleaned = $script:HtmlTagRegex.Replace($MailContact.MailTip, [string]::Empty)
+ $cleaned = $script:LineBreakRegex.Replace($cleaned, "`n")
+ $cleaned.Trim()
+ } else { $null }
+
+ $phoneCapacity = 0
+ if ($Contact.Phone) { $phoneCapacity++ }
+ if ($Contact.MobilePhone) { $phoneCapacity++ }
+
+ $phones = if ($phoneCapacity -gt 0) {
+ $phoneList = [List[hashtable]]::new($phoneCapacity)
+ if ($Contact.Phone) {
+ $phoneList.Add(@{ type = "business"; number = $Contact.Phone })
+ }
+ if ($Contact.MobilePhone) {
+ $phoneList.Add(@{ type = "mobile"; number = $Contact.MobilePhone })
+ }
+ $phoneList.ToArray()
+ } else { @() }
+
+ return @{
+ id = $Contact.Id
+ displayName = $Contact.DisplayName
+ givenName = $Contact.FirstName
+ surname = $Contact.LastName
+ mail = $mailAddress
+ companyName = $Contact.Company
+ jobTitle = $Contact.Title
+ website = $Contact.WebPage
+ notes = $Contact.Notes
+ hidefromGAL = $MailContact.HiddenFromAddressListsEnabled
+ mailTip = $cleanMailTip
+ onPremisesSyncEnabled = $Contact.IsDirSynced
+ addresses = @(@{
+ street = $Contact.StreetAddress
+ city = $Contact.City
+ state = $Contact.StateOrProvince
+ countryOrRegion = $Contact.CountryOrRegion
+ postalCode = $Contact.PostalCode
+ })
+ phones = $phones
+ }
+ }
+
+ try {
+ if (![string]::IsNullOrWhiteSpace($ContactID)) {
+ # Single contact request - keep existing complex formatting
+ Write-Host "Getting specific contact: $ContactID"
+
+ $Contact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{
+ Identity = $ContactID
+ }
+
+ $MailContact = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-MailContact' -cmdParams @{
+ Identity = $ContactID
+ }
+
+ if (!$Contact -or !$MailContact) {
+ throw "Contact not found or insufficient permissions"
+ }
+
+ $ContactResponse = ConvertTo-ContactObject -Contact $Contact -MailContact $MailContact
+
+ } else {
+ # Get all contacts - simplified approach
+ Write-Host "Getting all contacts"
+
+ $ContactResponse = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Contact' -cmdParams @{
+ Filter = "RecipientTypeDetails -eq 'MailContact'"
+ ResultSize = 'Unlimited'
+ } | Select-Object -Property City, Company, Department, DisplayName, FirstName, LastName, IsDirSynced, Guid, WindowsEmailAddress
+
+ # Return empty array if no contacts found
+ if (!$ContactResponse) {
+ $ContactResponse = @()
+ }
+ }
+
+ $StatusCode = [HttpStatusCode]::OK
+
+ } catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ $ContactResponse = $ErrorMessage
+ Write-Host "Error in ListContacts: $ErrorMessage"
+ }
+
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $ContactResponse
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-RemoveContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContact.ps1
similarity index 100%
rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-RemoveContact.ps1
rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContact.ps1
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1
new file mode 100644
index 000000000000..137b03e18a9b
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Contacts/Invoke-RemoveContactTemplates.ps1
@@ -0,0 +1,37 @@
+using namespace System.Net
+
+Function Invoke-RemoveContactTemplates {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $User = $Request.Headers
+
+ Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $ID = $request.query.ID ?? $request.body.ID
+
+ try {
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$id'"
+ $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey
+ Remove-AzDataTableEntity -Force @Table -Entity $ClearRow
+ $Result = "Removed Contact Template with ID $ID."
+ Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to remove Contact template with ID $ID. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Error' -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{ Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1
deleted file mode 100644
index 1e7332d359b9..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-AddContact.ps1
+++ /dev/null
@@ -1,53 +0,0 @@
-using namespace System.Net
-
-Function Invoke-AddContact {
- <#
- .FUNCTIONALITY
- Entrypoint
- .ROLE
- Exchange.Contact.ReadWrite
- #>
- [CmdletBinding()]
- param($Request, $TriggerMetadata)
-
- $APIName = $Request.Params.CIPPEndpoint
- $Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
- $ContactObject = $Request.Body
- $TenantId = $ContactObject.tenantid
-
- try {
-
- $BodyToship = [pscustomobject] @{
- displayName = $ContactObject.displayName
- name = $ContactObject.displayName
- ExternalEmailAddress = $ContactObject.email
- FirstName = $ContactObject.firstName
- LastName = $ContactObject.lastName
-
- }
- # Create the contact
- $NewContact = New-ExoRequest -tenantid $TenantId -cmdlet 'New-MailContact' -cmdParams $BodyToship -UseSystemMailbox $true
- $null = New-ExoRequest -tenantid $TenantId -cmdlet 'Set-MailContact' -cmdParams @{Identity = $NewContact.id; HiddenFromAddressListsEnabled = [boolean]$ContactObject.hidefromGAL } -UseSystemMailbox $true
-
- # Log the result
- $Result = "Created contact $($ContactObject.displayName) with email address $($ContactObject.email)"
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Info'
- $StatusCode = [HttpStatusCode]::OK
-
- } catch {
- $ErrorMessage = Get-CippException -Exception $_
- $Result = "Failed to create contact. $($ErrorMessage.NormalizedError)"
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Result -Sev 'Error' -LogData $ErrorMessage
- $StatusCode = [HttpStatusCode]::InternalServerError
-
- }
-
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = $StatusCode
- Body = @{Results = $Result }
- })
-
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1
deleted file mode 100644
index d46143ce376c..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-EditContact.ps1
+++ /dev/null
@@ -1,62 +0,0 @@
-using namespace System.Net
-
-Function Invoke-EditContact {
- <#
- .FUNCTIONALITY
- Entrypoint
- .ROLE
- Exchange.Contact.ReadWrite
- #>
- [CmdletBinding()]
- param($Request, $TriggerMetadata)
-
- $APIName = $Request.Params.CIPPEndpoint
- $Headers = $Request.Headers
- Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
- $TenantID = $Request.Body.tenantID
- try {
- # Extract contact information from the request body
- $contactInfo = $Request.Body
-
- # Log the received contact object
- Write-Host "Received contact object: $($contactInfo | ConvertTo-Json)"
-
- # Prepare the body for the Set-Contact cmdlet
- $bodyForSetContact = [pscustomobject] @{
- 'Identity' = $contactInfo.ContactID
- 'DisplayName' = $contactInfo.displayName
- 'WindowsEmailAddress' = $contactInfo.email
- 'FirstName' = $contactInfo.firstName
- 'LastName' = $contactInfo.LastName
- 'Title' = $contactInfo.Title
- 'StreetAddress' = $contactInfo.StreetAddress
- 'PostalCode' = $contactInfo.PostalCode
- 'City' = $contactInfo.City
- 'CountryOrRegion' = $contactInfo.CountryOrRegion
- 'Company' = $contactInfo.Company
- 'mobilePhone' = $contactInfo.mobilePhone
- 'phone' = $contactInfo.phone
- }
-
- # Call the Set-Contact cmdlet to update the contact
- $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-Contact' -cmdParams $bodyForSetContact -UseSystemMailbox $true
- $null = New-ExoRequest -tenantid $TenantID -cmdlet 'Set-MailContact' -cmdParams @{Identity = $contactInfo.ContactID; HiddenFromAddressListsEnabled = [System.Convert]::ToBoolean($contactInfo.hidefromGAL) } -UseSystemMailbox $true
- $Results = "Successfully edited contact $($contactInfo.DisplayName)"
- Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Info
- $StatusCode = [HttpStatusCode]::OK
-
- } catch {
- $ErrorMessage = Get-CippException -Exception $_
- $Results = "Failed to edit contact. $($ErrorMessage.NormalizedError)"
- Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantID -message $Results -Sev Error -LogData $ErrorMessage
- $StatusCode = [HttpStatusCode]::InternalServerError
- }
-
-
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = $StatusCode
- Body = @{Results = $Results }
- })
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1
index 955606318b02..c9839dd12cf3 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEmailForward.ps1
@@ -12,15 +12,19 @@ Function Invoke-ExecEmailForward {
$Tenantfilter = $request.body.tenantfilter
$username = $request.body.userid
- $ForwardingAddress = $request.body.ForwardInternal.value
+ if ($request.body.ForwardInternal -is [string]) {
+ $ForwardingAddress = $request.body.ForwardInternal
+ } else {($request.body.ForwardInternal.value)
+ $ForwardingAddress = $request.body.ForwardInternal.value
+ }
$ForwardingSMTPAddress = $request.body.ForwardExternal
$ForwardOption = $request.body.forwardOption
$APIName = $Request.Params.CIPPEndpoint
- [bool]$KeepCopy = if ($request.body.keepCopy -eq 'true') { $true } else { $false }
+ [bool]$KeepCopy = if ($request.body.KeepCopy -eq 'true') { $true } else { $false }
if ($ForwardOption -eq 'internalAddress') {
try {
- Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -Forward $ForwardingAddress -keepCopy $KeepCopy
+ Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -Forward $ForwardingAddress -KeepCopy $KeepCopy
if (-not $request.body.KeepCopy) {
$results = "Forwarding all email for $($username) to $($ForwardingAddress) and not keeping a copy"
} else {
@@ -35,7 +39,7 @@ Function Invoke-ExecEmailForward {
if ($ForwardOption -eq 'ExternalAddress') {
try {
- Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -forwardingSMTPAddress $ForwardingSMTPAddress -keepCopy $KeepCopy
+ Set-CIPPForwarding -userid $username -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -forwardingSMTPAddress $ForwardingSMTPAddress -KeepCopy $KeepCopy
if (-not $request.body.KeepCopy) {
$results = "Forwarding all email for $($username) to $($ForwardingSMTPAddress) and not keeping a copy"
} else {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1
index e2eefeff06ce..957b0acb0dad 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyCalPerms.ps1
@@ -5,22 +5,23 @@ Function Invoke-ExecModifyCalPerms {
.FUNCTIONALITY
Entrypoint
.ROLE
- Exchange.Calendar.ReadWrite
+ Exchange.Mailbox.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug'
-
- $Username = $request.body.userID
- $Tenantfilter = $request.body.tenantfilter
- $Permissions = $request.body.permissions
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing request for user: $Username, tenant: $Tenantfilter" -Sev 'Debug'
+ $Username = $Request.Body.userID
+ $TenantFilter = $Request.Body.tenantFilter
+ $Permissions = $Request.Body.permissions
- if ($username -eq $null) {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Username is null' -Sev 'Error'
+ Write-LogMessage -headers $Headers -API $APIName -message "Processing request for user: $Username, tenant: $TenantFilter" -Sev 'Debug'
+
+ if ($null -eq $Username) {
+ Write-LogMessage -headers $Headers -API $APIName -message 'Username is null' -Sev 'Error'
$body = [pscustomobject]@{'Results' = @('Username is required') }
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::BadRequest
@@ -28,13 +29,12 @@ Function Invoke-ExecModifyCalPerms {
})
return
}
-
+
try {
- $userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Retrieved user ID: $userid" -Sev 'Debug'
- }
- catch {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error'
+ $UserId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Username)" -tenantid $TenantFilter).id
+ Write-LogMessage -headers $Headers -API $APIName -message "Retrieved user ID: $UserId" -Sev 'Debug'
+ } catch {
+ Write-LogMessage -headers $Headers -API $APIName -message "Failed to get user ID: $($_.Exception.Message)" -Sev 'Error'
$body = [pscustomobject]@{'Results' = @("Failed to get user ID: $($_.Exception.Message)") }
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::NotFound
@@ -43,98 +43,73 @@ Function Invoke-ExecModifyCalPerms {
return
}
- $Results = [System.Collections.ArrayList]::new()
+ $Results = [System.Collections.Generic.List[string]]::new()
$HasErrors = $false
# Convert permissions to array format if it's an object with numeric keys
if ($Permissions -is [PSCustomObject]) {
if ($Permissions.PSObject.Properties.Name -match '^\d+$') {
$Permissions = $Permissions.PSObject.Properties.Value
- }
- else {
+ } else {
$Permissions = @($Permissions)
}
}
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing $($Permissions.Count) permission entries" -Sev 'Debug'
+ Write-LogMessage -headers $Headers -API $APIName -message "Processing $($Permissions.Count) permission entries" -Sev 'Debug'
foreach ($Permission in $Permissions) {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug'
-
+ Write-LogMessage -headers $Headers -API $APIName -message "Processing permission: $($Permission | ConvertTo-Json)" -Sev 'Debug'
+
$PermissionLevel = $Permission.PermissionLevel.value ?? $Permission.PermissionLevel
$Modification = $Permission.Modification
$CanViewPrivateItems = $Permission.CanViewPrivateItems ?? $false
-
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems" -Sev 'Debug'
-
+ $FolderName = $Permission.FolderName ?? 'Calendar'
+
+ Write-LogMessage -headers $Headers -API $APIName -message "Permission Level: $PermissionLevel, Modification: $Modification, CanViewPrivateItems: $CanViewPrivateItems, FolderName: $FolderName" -Sev 'Debug'
+
# Handle UserID as array or single value
$TargetUsers = @($Permission.UserID | ForEach-Object { $_.value ?? $_ })
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Target Users: $($TargetUsers -join ', ')" -Sev 'Debug'
+ Write-LogMessage -headers $Headers -API $APIName -message "Target Users: $($TargetUsers -join ', ')" -Sev 'Debug'
foreach ($TargetUser in $TargetUsers) {
try {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Processing target user: $TargetUser" -Sev 'Debug'
-
- if ($Modification -eq 'Remove') {
- try {
- $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{
- Identity = "$($userid):\Calendar"
- User = $TargetUser
- Confirm = $false
- }
- $null = $results.Add("Removed $($TargetUser) from $($username) Calendar permissions")
- }
- catch {
- $null = $results.Add("No existing permissions to remove for $($TargetUser)")
- }
- }
- else {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Setting permissions with AccessRights: $PermissionLevel" -Sev 'Debug'
-
- $cmdParams = @{
- Identity = "$($userid):\Calendar"
- User = $TargetUser
- AccessRights = $PermissionLevel
- Confirm = $false
- }
-
- if ($CanViewPrivateItems) {
- $cmdParams['SharingPermissionFlags'] = 'Delegate,CanViewPrivateItems'
- }
-
- try {
- # Try Add first
- $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxFolderPermission' -cmdParams $cmdParams
- $null = $results.Add("Granted $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')")
- }
- catch {
- # If Add fails, try Set
- $CalPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-MailboxFolderPermission' -cmdParams $cmdParams
- $null = $results.Add("Updated $($TargetUser) $($PermissionLevel) access to $($username) Calendar$($CanViewPrivateItems ? ' with access to private items' : '')")
- }
+ Write-LogMessage -headers $Headers -API $APIName -message "Processing target user: $TargetUser" -Sev 'Debug'
+ $Params = @{
+ APIName = $APIName
+ Headers = $Headers
+ RemoveAccess = if ($Modification -eq 'Remove') { $TargetUser } else { $null }
+ TenantFilter = $TenantFilter
+ UserID = $UserId
+ folderName = $FolderName
+ UserToGetPermissions = $TargetUser
+ LoggingName = $TargetUser
+ Permissions = $PermissionLevel
+ CanViewPrivateItems = $CanViewPrivateItems
}
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Successfully executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter
- }
- catch {
+
+ # Write-Host "Request params: $($Params | ConvertTo-Json)"
+ $Result = Set-CIPPCalendarPermission @Params
+
+ $null = $Results.Add($Result)
+ } catch {
$HasErrors = $true
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter
- $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)")
+ $null = $Results.Add("$($_.Exception.Message)")
}
}
}
- if ($results.Count -eq 0) {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message 'No results were generated from the operation' -Sev 'Warning'
- $null = $results.Add('No results were generated from the operation. Please check the logs for more details.')
+ if ($Results.Count -eq 0) {
+ Write-LogMessage -headers $Headers -API $APIName -message 'No results were generated from the operation' -Sev 'Warning'
+ $null = $Results.Add('No results were generated from the operation. Please check the logs for more details.')
$HasErrors = $true
}
- $body = [pscustomobject]@{'Results' = @($results) }
+ $Body = [pscustomobject]@{'Results' = @($Results) }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = if ($HasErrors) { [HttpStatusCode]::InternalServerError } else { [HttpStatusCode]::OK }
Body = $Body
})
-}
\ No newline at end of file
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1
index 222c3561e579..cddc705a3556 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecModifyMBPerms.ps1
@@ -12,13 +12,13 @@ Function Invoke-ExecModifyMBPerms {
$APIName = $Request.Params.CIPPEndpoint
Write-LogMessage -headers $Request.Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug'
-
+
$Username = $request.body.userID
$Tenantfilter = $request.body.tenantfilter
$Permissions = $request.body.permissions
if ($username -eq $null) { exit }
-
+
$userid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($username)" -tenantid $Tenantfilter).id
$Results = [System.Collections.ArrayList]::new()
@@ -33,10 +33,18 @@ Function Invoke-ExecModifyMBPerms {
}
foreach ($Permission in $Permissions) {
- $PermissionLevel = $Permission.PermissionLevel
+ $PermissionLevels = $Permission.PermissionLevel
$Modification = $Permission.Modification
$AutoMap = if ($Permission.PSObject.Properties.Name -contains 'AutoMap') { $Permission.AutoMap } else { $true }
-
+
+ # Handle multiple permission levels separated by commas
+ if ($PermissionLevels -like "*,*") {
+ $PermissionLevelArray = $PermissionLevels -split ',' | ForEach-Object { $_.Trim() }
+ }
+ else {
+ $PermissionLevelArray = @($PermissionLevels.Trim())
+ }
+
# Handle UserID as array of objects or single value
$TargetUsers = if ($Permission.UserID -is [array]) {
$Permission.UserID | ForEach-Object { $_.value }
@@ -46,79 +54,136 @@ Function Invoke-ExecModifyMBPerms {
}
foreach ($TargetUser in $TargetUsers) {
- try {
- switch ($PermissionLevel) {
- 'FullAccess' {
- if ($Modification -eq 'Remove') {
- $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-mailboxpermission' -cmdParams @{
- Identity = $userid
- user = $TargetUser
- accessRights = @('FullAccess')
- Confirm = $false
+ foreach ($PermissionLevel in $PermissionLevelArray) {
+ try {
+ switch ($PermissionLevel) {
+ 'FullAccess' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-mailboxpermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('FullAccess')
+ Confirm = $false
+ }
+ $null = $results.Add("Removed $($TargetUser) from $($username) Shared Mailbox permissions (FullAccess)")
+ }
+ else {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxPermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('FullAccess')
+ automapping = $AutoMap
+ Confirm = $false
+ }
+ $null = $results.Add("Granted $($TargetUser) access to $($username) Mailbox (FullAccess) with automapping set to $($AutoMap)")
}
- $null = $results.Add("Removed $($TargetUser) from $($username) Shared Mailbox permissions")
}
- else {
- $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-MailboxPermission' -cmdParams @{
- Identity = $userid
- user = $TargetUser
- accessRights = @('FullAccess')
- automapping = $AutoMap
- Confirm = $false
+ 'SendAs' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{
+ Identity = $userid
+ Trustee = $TargetUser
+ accessRights = @('SendAs')
+ Confirm = $false
+ }
+ $null = $results.Add("Removed $($TargetUser) from $($username) with Send As permissions")
+ }
+ else {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-RecipientPermission' -cmdParams @{
+ Identity = $userid
+ Trustee = $TargetUser
+ accessRights = @('SendAs')
+ Confirm = $false
+ }
+ $null = $results.Add("Granted $($TargetUser) access to $($username) with Send As permissions")
}
- $null = $results.Add("Granted $($TargetUser) access to $($username) Mailbox with automapping set to $($AutoMap)")
}
- }
- 'SendAs' {
- if ($Modification -eq 'Remove') {
- $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-RecipientPermission' -cmdParams @{
- Identity = $userid
- Trustee = $TargetUser
- accessRights = @('SendAs')
- Confirm = $false
+ 'SendOnBehalf' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{
+ Identity = $userid
+ GrantSendonBehalfTo = @{
+ '@odata.type' = '#Exchange.GenericHashTable'
+ remove = $TargetUser
+ }
+ Confirm = $false
+ }
+ $null = $results.Add("Removed $($TargetUser) from $($username) Send on Behalf Permissions")
+ }
+ else {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{
+ Identity = $userid
+ GrantSendonBehalfTo = @{
+ '@odata.type' = '#Exchange.GenericHashTable'
+ add = $TargetUser
+ }
+ Confirm = $false
+ }
+ $null = $results.Add("Granted $($TargetUser) access to $($username) with Send On Behalf Permissions")
}
- $null = $results.Add("Removed $($TargetUser) from $($username) with Send As permissions")
}
- else {
- $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Add-RecipientPermission' -cmdParams @{
- Identity = $userid
- Trustee = $TargetUser
- accessRights = @('SendAs')
- Confirm = $false
+ 'ReadPermission' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('ReadPermission')
+ Confirm = $false
+ }
+ $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions")
}
- $null = $results.Add("Granted $($TargetUser) access to $($username) with Send As permissions")
}
- }
- 'SendOnBehalf' {
- if ($Modification -eq 'Remove') {
- $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{
- Identity = $userid
- GrantSendonBehalfTo = @{
- '@odata.type' = '#Exchange.GenericHashTable'
- remove = $TargetUser
+ 'ExternalAccount' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('ExternalAccount')
+ Confirm = $false
}
- Confirm = $false
+ $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions")
}
- $null = $results.Add("Removed $($TargetUser) from $($username) Send on Behalf Permissions")
}
- else {
- $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Set-Mailbox' -cmdParams @{
- Identity = $userid
- GrantSendonBehalfTo = @{
- '@odata.type' = '#Exchange.GenericHashTable'
- add = $TargetUser
+ 'DeleteItem' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('DeleteItem')
+ Confirm = $false
}
- Confirm = $false
+ $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions")
+ }
+ }
+ 'ChangePermission' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('ChangePermission')
+ Confirm = $false
+ }
+ $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions")
+ }
+ }
+ 'ChangeOwner' {
+ if ($Modification -eq 'Remove') {
+ $MailboxPerms = New-ExoRequest -Anchor $username -tenantid $Tenantfilter -cmdlet 'Remove-MailboxPermission' -cmdParams @{
+ Identity = $userid
+ user = $TargetUser
+ accessRights = @('ChangeOwner')
+ Confirm = $false
+ }
+ $null = $results.Add("Removed $($TargetUser) from $($username) Read Permissions")
}
- $null = $results.Add("Granted $($TargetUser) access to $($username) with Send On Behalf Permissions")
}
}
+ Write-LogMessage -headers $Request.Headers -API $APINAME-message "Executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter
+ }
+ catch {
+ Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Error' -tenant $TenantFilter
+ $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)")
}
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Executed $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Info' -tenant $TenantFilter
- }
- catch {
- Write-LogMessage -headers $Request.Headers -API $APINAME-message "Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username)" -Sev 'Error' -tenant $TenantFilter
- $null = $results.Add("Could not execute $($PermissionLevel) permission modification for $($TargetUser) on $($username). Error: $($_.Exception.Message)")
}
}
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1
new file mode 100644
index 000000000000..5853374618ac
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetCalendarProcessing.ps1
@@ -0,0 +1,66 @@
+using namespace System.Net
+
+function Invoke-ExecSetCalendarProcessing {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Mailbox.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = 'ExecSetCalendarProcessing'
+ Write-LogMessage -Headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ try {
+ $cmdParams = @{
+ Identity = $Request.Body.UPN
+ AutomateProcessing = if ($Request.Body.automaticallyAccept -as [bool]) { 'AutoAccept' } elseif ($Request.Body.automaticallyProcess -as [bool]) { 'AutoUpdate' } else { 'None' }
+ AllowConflicts = $Request.Body.allowConflicts -as [bool]
+ AllowRecurringMeetings = $Request.Body.allowRecurringMeetings -as [bool]
+ ScheduleOnlyDuringWorkHours = $Request.Body.scheduleOnlyDuringWorkHours -as [bool]
+ AddOrganizerToSubject = $Request.Body.addOrganizerToSubject -as [bool]
+ DeleteComments = $Request.Body.deleteComments -as [bool]
+ DeleteSubject = $Request.Body.deleteSubject -as [bool]
+ RemovePrivateProperty = $Request.Body.removePrivateProperty -as [bool]
+ RemoveCanceledMeetings = $Request.Body.removeCanceledMeetings -as [bool]
+ RemoveOldMeetingMessages = $Request.Body.removeOldMeetingMessages -as [bool]
+ ProcessExternalMeetingMessages = $Request.Body.processExternalMeetingMessages -as [bool]
+ }
+
+ # Add optional numeric parameters only if they have values
+ if ($Request.Body.maxConflicts) {
+ $cmdParams['MaximumConflictInstances'] = $Request.Body.maxConflicts -as [int]
+ }
+ if ($Request.Body.maximumDurationInMinutes) {
+ $cmdParams['MaximumDurationInMinutes'] = $Request.Body.maximumDurationInMinutes -as [int]
+ }
+ if ($Request.Body.minimumDurationInMinutes) {
+ $cmdParams['MinimumDurationInMinutes'] = $Request.Body.minimumDurationInMinutes -as [int]
+ }
+ if ($Request.Body.bookingWindowInDays) {
+ $cmdParams['BookingWindowInDays'] = $Request.Body.bookingWindowInDays -as [int]
+ }
+ if ($Request.Body.additionalResponse) {
+ $cmdParams['AdditionalResponse'] = $Request.Body.additionalResponse
+ }
+
+ $null = New-ExoRequest -tenantid $Request.Body.tenantFilter -cmdlet 'Set-CalendarProcessing' -cmdParams $cmdParams
+
+ $Results = "Calendar processing settings for $($Request.Body.UPN) have been updated successfully"
+ Write-LogMessage -API $APIName -tenant $Request.Body.tenantFilter -message $Results -sev Info
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Could not update calendar processing settings for $($Request.Body.UPN). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -API $APIName -tenant $Request.Body.tenantFilter -message $Results -sev Error -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{ Results = $Results }
+ })
+}
\ No newline at end of file
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1
index c3c814a7712c..1bd62dac8eb9 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecSetOoO.ps1
@@ -11,39 +11,64 @@ Function Invoke-ExecSetOoO {
param($Request, $TriggerMetadata)
try {
$APIName = $Request.Params.CIPPEndpoint
- Write-LogMessage -headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+
$Username = $Request.Body.userId
$TenantFilter = $Request.Body.tenantFilter
+ $State = $Request.Body.AutoReplyState.value
+
+ $SplatParams = @{
+ userid = $Username
+ tenantFilter = $TenantFilter
+ APIName = $APIName
+ Headers = $Headers
+ State = $State
+ }
+
+ # User action uses input, edit exchange uses InternalMessage and ExternalMessage
+ # User action disable OoO doesn't send any input
if ($Request.Body.input) {
$InternalMessage = $Request.Body.input
$ExternalMessage = $Request.Body.input
} else {
$InternalMessage = $Request.Body.InternalMessage
$ExternalMessage = $Request.Body.ExternalMessage
- }
- #if starttime and endtime are a number, they are unix timestamps and need to be converted to datetime, otherwise just use them.
- $StartTime = if ($Request.Body.StartTime -match '^\d+$') { [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime } else { $Request.Body.StartTime }
- $EndTime = if ($Request.Body.EndTime -match '^\d+$') { [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime } else { $Request.Body.EndTime }
-
- $Results = try {
- if ($Request.Body.AutoReplyState.value -ne 'Scheduled') {
- Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Request.Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -State $Request.Body.AutoReplyState.value
- } else {
- Set-CIPPOutOfOffice -userid $Username -tenantFilter $TenantFilter -APIName $APIName -Headers $Request.Headers -InternalMessage $InternalMessage -ExternalMessage $ExternalMessage -StartTime $StartTime -EndTime $EndTime -State $Request.Body.AutoReplyState.value
+
+ # Only add the internal and external message if they are not empty/null. Done to be able to set the OOO to disabled, while keeping the existing messages intact.
+ # This works because the frontend always sends some HTML even if the fields are empty.
+ if (-not [string]::IsNullOrWhiteSpace($InternalMessage)) {
+ $SplatParams.InternalMessage = $InternalMessage
+ }
+ if (-not [string]::IsNullOrWhiteSpace($ExternalMessage)) {
+ $SplatParams.ExternalMessage = $ExternalMessage
}
- } catch {
- "Could not add out of office message for $($Username). Error: $($_.Exception.Message)"
}
- $Body = [PSCustomObject]@{'Results' = $($Results) }
+
+ # If the state is scheduled, add the start and end times to the splat params
+ if ($State -eq 'Scheduled') {
+ # If starttime and endtime are a number, they are unix timestamps and need to be converted to datetime, otherwise just use them.
+ $StartTime = $Request.Body.StartTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.StartTime).DateTime : $Request.Body.StartTime
+ $EndTime = $Request.Body.EndTime -match '^\d+$' ? [DateTimeOffset]::FromUnixTimeSeconds([int]$Request.Body.EndTime).DateTime : $Request.Body.EndTime
+ $SplatParams.StartTime = $StartTime
+ $SplatParams.EndTime = $EndTime
+ }
+
+ $Results = Set-CIPPOutOfOffice @SplatParams
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- $Body = [PSCustomObject]@{'Results' = "Could not set Out of Office user: $($_.Exception.Message)" }
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Could not set Out of Office for user: $($Username). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Body
+ StatusCode = $StatusCode
+ Body = @{'Results' = $($Results) }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1
deleted file mode 100644
index 6fb5562635a4..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListContacts.ps1
+++ /dev/null
@@ -1,71 +0,0 @@
-using namespace System.Net
-
-Function Invoke-ListContacts {
- <#
- .FUNCTIONALITY
- Entrypoint
- .ROLE
- Exchange.Contact.Read
- #>
- [CmdletBinding()]
- param($Request, $TriggerMetadata)
-
-
- # Define fields to retrieve
- $selectList = @(
- 'id',
- 'companyName',
- 'displayName',
- 'mail',
- 'onPremisesSyncEnabled',
- 'editURL',
- 'givenName',
- 'jobTitle',
- 'surname',
- 'addresses',
- 'phones'
- )
-
- # Get query parameters
- $TenantFilter = $Request.Query.tenantFilter
- $ContactID = $Request.Query.id
-
- # Validate required parameters
- if (-not $TenantFilter) {
- $StatusCode = [HttpStatusCode]::BadRequest
- $GraphRequest = 'tenantFilter is required'
- Write-Host 'Error: Missing tenantFilter parameter'
- } else {
- try {
- # Construct Graph API URI based on whether an ID is provided
- $graphUri = if ([string]::IsNullOrWhiteSpace($ContactID) -eq $false) {
- "https://graph.microsoft.com/beta/contacts/$($ContactID)?`$select=$($selectList -join ',')"
- } else {
- "https://graph.microsoft.com/beta/contacts?`$top=999&`$select=$($selectList -join ',')"
- }
-
- # Make the Graph API request
- $GraphRequest = New-GraphGetRequest -uri $graphUri -tenantid $TenantFilter
-
- if ([string]::IsNullOrWhiteSpace($ContactID) -eq $false) {
- $HiddenFromGAL = New-EXORequest -tenantid $TenantFilter -cmdlet 'Get-Recipient' -cmdParams @{RecipientTypeDetails = 'MailContact' } -Select 'HiddenFromAddressListsEnabled,ExternalDirectoryObjectId' | Where-Object { $_.ExternalDirectoryObjectId -eq $ContactID }
- $GraphRequest | Add-Member -NotePropertyName 'hidefromGAL' -NotePropertyValue $HiddenFromGAL.HiddenFromAddressListsEnabled
- }
- # Ensure single result when ID is provided
- if ($ContactID -and $GraphRequest -is [array]) {
- $GraphRequest = $GraphRequest | Select-Object -First 1
- }
- $StatusCode = [HttpStatusCode]::OK
- } catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- $StatusCode = [HttpStatusCode]::InternalServerError
- $GraphRequest = $ErrorMessage
- }
- }
-
- # Return response
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = $StatusCode
- Body = @($GraphRequest | Where-Object { $null -ne $_.id })
- })
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1
index 2a1842db4d5b..3bc1e1b42701 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListOoO.ps1
@@ -11,19 +11,23 @@ Function Invoke-ListOoO {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- $Tenantfilter = $request.query.tenantFilter
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $TenantFilter = $Request.Query.tenantFilter
+ $UserID = $Request.Query.userid
try {
- $Body = Get-CIPPOutOfOffice -UserID $Request.query.userid -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers
+ $Results = Get-CIPPOutOfOffice -UserID $UserID -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- $Body = [pscustomobject]@{'Results' = "Failed. $ErrorMessage" }
-
+ $Results = $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Body
+ StatusCode = $StatusCode
+ Body = $Results
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1
deleted file mode 100644
index c9e395c05de7..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Reports/Invoke-ListSafeLinksFilters.ps1
+++ /dev/null
@@ -1,31 +0,0 @@
-function Invoke-ListSafeLinksFilters {
- <#
- .FUNCTIONALITY
- Entrypoint
- .ROLE
- Exchange.SpamFilter.Read
- #>
- [CmdletBinding()]
- param($Request, $TriggerMetadata)
-
- $APIName = $Request.Params.CIPPEndpoint
- $Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
- # Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
- $Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property *
- $Rules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property *
-
- $Output = $Policies | Select-Object -Property *,
- @{ Name = 'RuleName'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.Name } } } },
- @{ Name = 'Priority'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.Priority } } } },
- @{ Name = 'RecipientDomainIs'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.RecipientDomainIs } } } },
- @{ Name = 'State'; Expression = { foreach ($item in $Rules) { if ($item.SafeLinksPolicy -eq $_.Name) { $item.State } } } }
-
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Output
- })
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1
new file mode 100644
index 000000000000..93497681a93a
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddEquipmentMailbox.ps1
@@ -0,0 +1,62 @@
+using namespace System.Net
+
+Function Invoke-AddEquipmentMailbox {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Equipment.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $Tenant = $Request.Body.tenantID
+
+ $Results = [System.Collections.Generic.List[Object]]::new()
+ $MailboxObject = $Request.Body
+
+ # Create the equipment mailbox
+ $NewMailboxParams = @{
+ Name = $MailboxObject.username
+ DisplayName = $MailboxObject.displayName
+ Equipment = $true
+ PrimarySmtpAddress = $MailboxObject.userPrincipalName
+ }
+
+ try {
+ # Create the equipment mailbox
+ $AddEquipmentRequest = New-ExoRequest -tenantid $Tenant -cmdlet 'New-Mailbox' -cmdParams $NewMailboxParams
+ $Results.Add("Successfully created equipment mailbox: $($MailboxObject.displayName)")
+
+ # Block sign-in for the mailbox
+ try {
+ $BlockSignInRequest = Set-CIPPSignInState -userid $AddEquipmentRequest.ExternalDirectoryObjectId -TenantFilter $Tenant -APIName $APINAME -Headers $Headers -AccountEnabled $false
+ if ($BlockSignInRequest -like 'Could not disable*') { throw $BlockSignInRequest }
+ $Results.Add("Blocked sign-in for Equipment mailbox; $($MailboxObject.userPrincipalName)")
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results.Add("Failed to block sign-in for Equipment mailbox: $($MailboxObject.userPrincipalName). Error: $($ErrorMessage.NormalizedError)")
+ }
+ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Created equipment mailbox $($MailboxObject.displayName)" -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
+
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Message = "Failed to create equipment mailbox: $($MailboxObject.displayName). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Error' -LogData $ErrorMessage
+ $Results.Add($Message)
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ $Body = [pscustomobject]@{ 'Results' = @($Results) }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $Body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1
index 0b7f139a7832..d174b644ab39 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-AddRoomMailbox.ps1
@@ -32,7 +32,8 @@ Function Invoke-AddRoomMailbox {
# Block sign-in for the mailbox
try {
- $Request = Set-CIPPSignInState -userid $AddRoomRequest.ExternalDirectoryObjectId -TenantFilter $Tenant -APIName $APINAME -Headers $Headers -AccountEnabled $false
+ $BlockSignInRequest = Set-CIPPSignInState -userid $AddRoomRequest.ExternalDirectoryObjectId -TenantFilter $Tenant -APIName $APINAME -Headers $Headers -AccountEnabled $false
+ if ($BlockSignInRequest -like 'Could not disable*') { throw $BlockSignInRequest }
$Results.Add("Blocked sign-in for Room mailbox; $($MailboxObject.userPrincipalName)")
} catch {
$ErrorMessage = Get-CippException -Exception $_
@@ -41,8 +42,9 @@ Function Invoke-AddRoomMailbox {
$StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message "Failed to create room: $($MailboxObject.DisplayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
- $Results.Add("Failed to create Room mailbox $($MailboxObject.userPrincipalName). $($ErrorMessage.NormalizedError)")
+ $Message = "Failed to create room mailbox: $($MailboxObject.DisplayName). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -Headers $Headers -API $APIName -tenant $Tenant -message $Message -Sev 'Error' -LogData $ErrorMessage
+ $Results.Add($Message)
$StatusCode = [HttpStatusCode]::InternalServerError
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1
new file mode 100644
index 000000000000..ec80257aa684
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditEquipmentMailbox.ps1
@@ -0,0 +1,115 @@
+using namespace System.Net
+
+Function Invoke-EditEquipmentMailbox {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Equipment.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ $Tenant = $Request.Body.tenantID
+
+ $Results = [System.Collections.Generic.List[Object]]::new()
+ $MailboxObject = $Request.Body
+
+ # First update the mailbox properties
+ $UpdateMailboxParams = @{
+ Identity = $MailboxObject.equipmentId
+ DisplayName = $MailboxObject.displayName
+ }
+
+ if (![string]::IsNullOrWhiteSpace($MailboxObject.hiddenFromAddressListsEnabled)) {
+ $UpdateMailboxParams.Add('HiddenFromAddressListsEnabled', $MailboxObject.hiddenFromAddressListsEnabled)
+ }
+
+ # Then update the user properties
+ $UpdateUserParams = @{
+ Identity = $MailboxObject.equipmentId
+ }
+
+ # Add optional parameters if they exist
+ $UserProperties = @(
+ 'Location', 'Department', 'Company',
+ 'Phone', 'Tags',
+ 'StreetAddress', 'City', 'StateOrProvince', 'CountryOrRegion',
+ 'PostalCode'
+ )
+
+ foreach ($prop in $UserProperties) {
+ if (![string]::IsNullOrWhiteSpace($MailboxObject.$prop)) {
+ $UpdateUserParams[$prop] = $MailboxObject.$prop
+ }
+ }
+
+ # Then update the calendar properties
+ $UpdateCalendarParams = @{
+ Identity = $MailboxObject.equipmentId
+ }
+
+ $CalendarProperties = @(
+ 'AllowConflicts', 'AllowRecurringMeetings', 'BookingWindowInDays',
+ 'MaximumDurationInMinutes', 'ProcessExternalMeetingMessages',
+ 'ForwardRequestsToDelegates', 'ScheduleOnlyDuringWorkHours', 'AutomateProcessing'
+ )
+
+ foreach ($prop in $CalendarProperties) {
+ if (![string]::IsNullOrWhiteSpace($MailboxObject.$prop)) {
+ $UpdateCalendarParams[$prop] = $MailboxObject.$prop
+ }
+ }
+
+ # Then update the calendar configuration
+ $UpdateCalendarConfigParams = @{
+ Identity = $MailboxObject.equipmentId
+ }
+
+ $CalendarConfiguration = @(
+ 'WorkDays', 'WorkHoursStartTime', 'WorkHoursEndTime', 'WorkingHoursTimeZone'
+ )
+
+ foreach ($prop in $CalendarConfiguration) {
+ if (![string]::IsNullOrWhiteSpace($MailboxObject.$prop)) {
+ $UpdateCalendarConfigParams[$prop] = $MailboxObject.$prop
+ }
+ }
+
+ try {
+ # Update mailbox properties
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Mailbox' -cmdParams $UpdateMailboxParams
+
+ # Update user properties
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-User' -cmdParams $UpdateUserParams
+ $Results.Add("Successfully updated equipment: $($MailboxObject.DisplayName) (User Properties)")
+
+ # Update calendar properties
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-CalendarProcessing' -cmdParams $UpdateCalendarParams
+ $Results.Add("Successfully updated equipment: $($MailboxObject.DisplayName) (Calendar Properties)")
+
+ # Update calendar configuration properties
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailboxCalendarConfiguration' -cmdParams $UpdateCalendarConfigParams
+ $Results.Add("Successfully updated equipment: $($MailboxObject.DisplayName) (Calendar Configuration)")
+
+ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Updated equipment $($MailboxObject.DisplayName)" -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
+
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message "Failed to update equipment: $($MailboxObject.DisplayName). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
+ $Results.Add("Failed to update Equipment mailbox $($MailboxObject.userPrincipalName). $($ErrorMessage.NormalizedError)")
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ $Body = [pscustomobject]@{ 'Results' = @($Results) }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $Body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1
index 6e206791b411..2b3f42107b80 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-EditRoomMailbox.ps1
@@ -10,13 +10,15 @@ Function Invoke-EditRoomMailbox {
[CmdletBinding()]
param($Request, $TriggerMetadata)
- $APIName = $TriggerMetadata.FunctionName
- $Tenant = $Request.body.tenantid
- Write-LogMessage -headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $Tenant = $Request.Body.tenantID
$Results = [System.Collections.Generic.List[Object]]::new()
- $MailboxObject = $Request.body
+ $MailboxObject = $Request.Body
# First update the mailbox properties
$UpdateMailboxParams = @{
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1
new file mode 100644
index 000000000000..fe890424f3ed
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListEquipment.ps1
@@ -0,0 +1,99 @@
+using namespace System.Net
+
+Function Invoke-ListEquipment {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.Equipment.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $EquipmentId = $Request.Query.EquipmentId
+ $Tenant = $Request.Query.TenantFilter
+
+ try {
+ if ($EquipmentId) {
+ # Get specific equipment details
+ $Equipment = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{
+ Identity = $EquipmentId
+ RecipientTypeDetails = 'EquipmentMailbox'
+ }
+
+ $UserDetails = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-User' -cmdParams @{
+ Identity = $EquipmentId
+ }
+
+ $CalendarProcessing = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-CalendarProcessing' -cmdParams @{
+ Identity = $EquipmentId
+ }
+
+ $CalendarConfig = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxCalendarConfiguration' -cmdParams @{
+ Identity = $EquipmentId
+ }
+
+ $Results = [PSCustomObject]@{
+ # Core mailbox properties
+ displayName = $Equipment.DisplayName
+ hiddenFromAddressListsEnabled = $Equipment.HiddenFromAddressListsEnabled
+ userPrincipalName = $Equipment.UserPrincipalName
+ primarySmtpAddress = $Equipment.PrimarySmtpAddress
+
+ # Equipment details from Get-User
+ department = $UserDetails.Department
+ company = $UserDetails.Company
+
+ # Location information from Get-User
+ street = $UserDetails.Street
+ city = $UserDetails.City
+ state = $UserDetails.State
+ postalCode = $UserDetails.PostalCode
+ countryOrRegion = $UserDetails.CountryOrRegion
+
+ # Equipment features
+ phone = $UserDetails.Phone
+ tags = $UserDetails.Tags
+
+ # Calendar properties from Get-CalendarProcessing
+ allowConflicts = $CalendarProcessing.AllowConflicts
+ allowRecurringMeetings = $CalendarProcessing.AllowRecurringMeetings
+ bookingWindowInDays = $CalendarProcessing.BookingWindowInDays
+ maximumDurationInMinutes = $CalendarProcessing.MaximumDurationInMinutes
+ processExternalMeetingMessages = $CalendarProcessing.ProcessExternalMeetingMessages
+ forwardRequestsToDelegates = $CalendarProcessing.ForwardRequestsToDelegates
+ scheduleOnlyDuringWorkHours = $CalendarProcessing.ScheduleOnlyDuringWorkHours
+ automateProcessing = $CalendarProcessing.AutomateProcessing
+
+ # Calendar configuration from Get-MailboxCalendarConfiguration
+ workDays = $CalendarConfig.WorkDays
+ workHoursStartTime = $CalendarConfig.WorkHoursStartTime
+ workHoursEndTime = $CalendarConfig.WorkHoursEndTime
+ workingHoursTimeZone = $CalendarConfig.WorkingHoursTimeZone
+ }
+ } else {
+ # List all equipment mailboxes
+ $Results = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{
+ RecipientTypeDetails = 'EquipmentMailbox'
+ ResultSize = 'Unlimited'
+ } | Select-Object -ExcludeProperty *data.type*
+ }
+ $StatusCode = [HttpStatusCode]::OK
+
+ } catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::Forbidden
+ $Results = $ErrorMessage
+ }
+
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @($Results | Sort-Object displayName)
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1
index ad7625bd61c6..fc58bfe47db8 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Resources/Invoke-ListRooms.ps1
@@ -46,57 +46,57 @@ Function Invoke-ListRooms {
$GraphRequest = @(
[PSCustomObject]@{
# Core Mailbox Properties
- id = $RoomMailbox.ExternalDirectoryObjectId
- displayName = $RoomMailbox.DisplayName
- mail = $RoomMailbox.PrimarySmtpAddress
- mailNickname = $RoomMailbox.Alias
- accountDisabled = $RoomMailbox.AccountDisabled
- hiddenFromAddressListsEnabled = $RoomMailbox.HiddenFromAddressListsEnabled
- isDirSynced = $RoomMailbox.IsDirSynced
+ id = $RoomMailbox.ExternalDirectoryObjectId
+ displayName = $RoomMailbox.DisplayName
+ mail = $RoomMailbox.PrimarySmtpAddress
+ mailNickname = $RoomMailbox.Alias
+ accountDisabled = $RoomMailbox.AccountDisabled
+ hiddenFromAddressListsEnabled = $RoomMailbox.HiddenFromAddressListsEnabled
+ isDirSynced = $RoomMailbox.IsDirSynced
# Room Booking Settings
- bookingType = $PlaceDetails.BookingType
- resourceDelegates = $PlaceDetails.ResourceDelegates
- capacity = [int]($PlaceDetails.Capacity ?? $RoomMailbox.ResourceCapacity ?? 0)
+ bookingType = $PlaceDetails.BookingType
+ resourceDelegates = $PlaceDetails.ResourceDelegates
+ capacity = [int]($PlaceDetails.Capacity ?? $RoomMailbox.ResourceCapacity ?? 0)
# Location Information
- building = $PlaceDetails.Building
- floor = $PlaceDetails.Floor
- floorLabel = $PlaceDetails.FloorLabel
- street = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Street)) { $null } else { $PlaceDetails.Street }
- city = if ([string]::IsNullOrWhiteSpace($PlaceDetails.City)) { $null } else { $PlaceDetails.City }
- state = if ([string]::IsNullOrWhiteSpace($PlaceDetails.State)) { $null } else { $PlaceDetails.State }
- postalCode = if ([string]::IsNullOrWhiteSpace($PlaceDetails.PostalCode)) { $null } else { $PlaceDetails.PostalCode }
- countryOrRegion = if ([string]::IsNullOrWhiteSpace($PlaceDetails.CountryOrRegion)) { $null } else { $PlaceDetails.CountryOrRegion }
+ building = $PlaceDetails.Building
+ floor = $PlaceDetails.Floor
+ floorLabel = $PlaceDetails.FloorLabel
+ street = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Street)) { $null } else { $PlaceDetails.Street }
+ city = if ([string]::IsNullOrWhiteSpace($PlaceDetails.City)) { $null } else { $PlaceDetails.City }
+ state = if ([string]::IsNullOrWhiteSpace($PlaceDetails.State)) { $null } else { $PlaceDetails.State }
+ postalCode = if ([string]::IsNullOrWhiteSpace($PlaceDetails.PostalCode)) { $null } else { $PlaceDetails.PostalCode }
+ countryOrRegion = if ([string]::IsNullOrWhiteSpace($PlaceDetails.CountryOrRegion)) { $null } else { $PlaceDetails.CountryOrRegion }
# Room Equipment
- audioDeviceName = $PlaceDetails.AudioDeviceName
- videoDeviceName = $PlaceDetails.VideoDeviceName
- displayDeviceName = $PlaceDetails.DisplayDeviceName
- mtrEnabled = $PlaceDetails.MTREnabled
+ audioDeviceName = $PlaceDetails.AudioDeviceName
+ videoDeviceName = $PlaceDetails.VideoDeviceName
+ displayDeviceName = $PlaceDetails.DisplayDeviceName
+ mtrEnabled = $PlaceDetails.MTREnabled
# Room Features
- isWheelChairAccessible = $PlaceDetails.IsWheelChairAccessible
- phone = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Phone)) { $null } else { $PlaceDetails.Phone }
- tags = $PlaceDetails.Tags
- spaceType = $PlaceDetails.SpaceType
+ isWheelChairAccessible = $PlaceDetails.IsWheelChairAccessible
+ phone = if ([string]::IsNullOrWhiteSpace($PlaceDetails.Phone)) { $null } else { $PlaceDetails.Phone }
+ tags = $PlaceDetails.Tags
+ spaceType = $PlaceDetails.SpaceType
# Calendar Properties
- AllowConflicts = $CalendarProperties.AllowConflicts
- AllowRecurringMeetings = $CalendarProperties.AllowRecurringMeetings
- BookingWindowInDays = $CalendarProperties.BookingWindowInDays
- MaximumDurationInMinutes = $CalendarProperties.MaximumDurationInMinutes
- ProcessExternalMeetingMessages= $CalendarProperties.ProcessExternalMeetingMessages
- EnforceCapacity = $CalendarProperties.EnforceCapacity
- ForwardRequestsToDelegates = $CalendarProperties.ForwardRequestsToDelegates
- ScheduleOnlyDuringWorkHours = $CalendarProperties.ScheduleOnlyDuringWorkHours
- AutomateProcessing = $CalendarProperties.AutomateProcessing
+ AllowConflicts = $CalendarProperties.AllowConflicts
+ AllowRecurringMeetings = $CalendarProperties.AllowRecurringMeetings
+ BookingWindowInDays = $CalendarProperties.BookingWindowInDays
+ MaximumDurationInMinutes = $CalendarProperties.MaximumDurationInMinutes
+ ProcessExternalMeetingMessages = $CalendarProperties.ProcessExternalMeetingMessages
+ EnforceCapacity = $CalendarProperties.EnforceCapacity
+ ForwardRequestsToDelegates = $CalendarProperties.ForwardRequestsToDelegates
+ ScheduleOnlyDuringWorkHours = $CalendarProperties.ScheduleOnlyDuringWorkHours
+ AutomateProcessing = $CalendarProperties.AutomateProcessing
# Calendar Configuration Properties
- WorkDays = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkDays)) { $null } else { $CalendarConfigurationProperties.WorkDays }
- WorkHoursStartTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursStartTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursStartTime }
- WorkHoursEndTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursEndTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursEndTime }
- WorkingHoursTimeZone = $CalendarConfigurationProperties.WorkingHoursTimeZone
+ WorkDays = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkDays)) { $null } else { $CalendarConfigurationProperties.WorkDays }
+ WorkHoursStartTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursStartTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursStartTime }
+ WorkHoursEndTime = if ([string]::IsNullOrWhiteSpace($CalendarConfigurationProperties.WorkHoursEndTime)) { $null } else { $CalendarConfigurationProperties.WorkHoursEndTime }
+ WorkingHoursTimeZone = $CalendarConfigurationProperties.WorkingHoursTimeZone
}
)
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1
deleted file mode 100644
index fd7b11144b48..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-EditSafeLinksFilter.ps1
+++ /dev/null
@@ -1,57 +0,0 @@
-function Invoke-EditSafeLinksFilter {
- <#
- .FUNCTIONALITY
- Entrypoint
- .ROLE
- Exchange.SpamFilter.Read
- #>
- [CmdletBinding()]
- param($Request, $TriggerMetadata)
-
- $APIName = $Request.Params.CIPPEndpoint
- $Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
- # Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
- $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName
- $State = $Request.Query.State ?? $Request.Body.State
-
- try {
- $ExoRequestParam = @{
- tenantid = $TenantFilter
- cmdParams = @{
- Identity = $RuleName
- }
- useSystemMailbox = $true
- }
-
- switch ($State) {
- 'Enable' {
- $ExoRequestParam.Add('cmdlet', 'Enable-SafeLinksRule')
- }
- 'Disable' {
- $ExoRequestParam.Add('cmdlet', 'Disable-SafeLinksRule')
- }
- Default {
- throw 'Invalid state'
- }
- }
- $null = New-ExoRequest @ExoRequestParam
-
- $Result = "Successfully set SafeLinks rule $($RuleName) to $($State)"
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Info
- $StatusCode = [HttpStatusCode]::OK
- } catch {
- $ErrorMessage = Get-CippException -Exception $_
- $Result = "Failed setting SafeLinks rule $($RuleName) to $($State). Error: $($ErrorMessage.NormalizedError)"
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error'
- $StatusCode = [HttpStatusCode]::InternalServerError
- }
-
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = $StatusCode
- Body = @{Results = $Result }
- })
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1
index 55b13d960f3a..7cc4ed4204d5 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListQuarantinePolicy.ps1
@@ -18,8 +18,6 @@ function Invoke-ListQuarantinePolicy {
$Policies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-QuarantinePolicy' -cmdParams @{QuarantinePolicyType=$QuarantinePolicyType} | Select-Object -Property * -ExcludeProperty *odata*, *data.type*
- write-host $($Request | ConvertTo-Json -Depth 10)
-
if ($QuarantinePolicyType -eq 'QuarantinePolicy') {
# Convert the string EndUserQuarantinePermissions to individual properties
$Policies | ForEach-Object {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1
index a212a7111946..8037b40239f5 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddChocoApp.ps1
@@ -16,7 +16,7 @@ Function Invoke-AddChocoApp {
$ChocoApp = $Request.Body
$intuneBody = Get-Content 'AddChocoApp\Choco.app.json' | ConvertFrom-Json
- $AssignTo = $Request.Body.AssignTo
+ $AssignTo = $Request.Body.AssignTo -eq 'customGroup' ? $Request.Body.CustomGroup : $Request.Body.AssignTo
$intuneBody.description = $ChocoApp.description
$intuneBody.displayName = $ChocoApp.ApplicationName
$intuneBody.installExperience.runAsAccount = if ($ChocoApp.InstallAsSystem) { 'system' } else { 'user' }
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1
new file mode 100644
index 000000000000..1d34f8196a72
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditIntunePolicy.ps1
@@ -0,0 +1,51 @@
+using namespace System.Net
+
+Function Invoke-EditIntunePolicy {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Endpoint.MEM.Read
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
+ $ID = $Request.Query.ID ?? $Request.Body.ID
+ $DisplayName = $Request.Query.newDisplayName ?? $Request.Body.newDisplayName
+ $PolicyType = $Request.Query.policyType ?? $Request.Body.policyType
+
+ try {
+ $properties = @{}
+
+ # Only add displayName if it's provided
+ if ($DisplayName) {
+ $properties["displayName"] = $DisplayName
+ }
+
+ # Update the policy
+ $Request = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$PolicyType/$ID" -tenantid $TenantFilter -type PATCH -body ($properties | ConvertTo-Json) -asapp $true
+
+ $Result = "Successfully updated Intune policy $($ID)"
+ if ($DisplayName) { $Result += " name to '$($DisplayName)'" }
+
+ Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to update Intune policy $($ID): $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Error' -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{ 'Results' = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1
index 85a0572a77cf..53586dd663c2 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-EditPolicy.ps1
@@ -10,6 +10,8 @@ Function Invoke-EditPolicy {
[CmdletBinding()]
param($Request, $TriggerMetadata)
+ # Note, suspect this is deprecated - rvdwegen
+
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1
index d699ab7fbb10..fde70dcc558a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListDefenderTVM.ps1
@@ -18,21 +18,31 @@ Function Invoke-ListDefenderTVM {
# Interact with query parameters or the body of the request.
try {
- $GraphRequest = New-GraphgetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999" -scope 'https://api.securitycenter.microsoft.com/.default' | Group-Object cveid
+ $GraphRequest = New-GraphGetRequest -tenantid $TenantFilter -uri "https://api.securitycenter.microsoft.com/api/machines/SoftwareVulnerabilitiesByMachine?`$top=999" -scope 'https://api.securitycenter.microsoft.com/.default' | Group-Object cveId
$GroupObj = foreach ($cve in $GraphRequest) {
- [pscustomobject]@{
- customerId = $TenantFilter
- affectedDevicesCount = $cve.count
- cveId = $cve.name
- affectedDevices = ($cve.group.deviceName -join ', ')
- osPlatform = ($cve.group.osplatform | Sort-Object -Unique)
- softwareVendor = ($cve.group.softwareVendor | Sort-Object -Unique)
- softwareName = ($cve.group.softwareName | Sort-Object -Unique)
- vulnerabilitySeverityLevel = ($cve.group.vulnerabilitySeverityLevel | Sort-Object -Unique)
- cvssScore = ($cve.group.cvssScore | Sort-Object -Unique)
- securityUpdateAvailable = ($cve.group.securityUpdateAvailable | Sort-Object -Unique)
- exploitabilityLevel = ($cve.group.exploitabilityLevel | Sort-Object -Unique)
+ # Start with base properties
+ $obj = [ordered]@{
+ customerId = $TenantFilter
+ affectedDevicesCount = $cve.count
+ cveId = $cve.name
}
+
+ # Get all unique property names from the group
+ $allProperties = $cve.group | Get-Member -MemberType Properties | Select-Object -ExpandProperty Name | Sort-Object -Unique
+
+ # Add all properties from the group with appropriate processing
+ foreach ($property in $allProperties) {
+ if ($property -eq 'deviceName') {
+ # Special handling for deviceName - join with comma
+ $obj['affectedDevices'] = ($cve.group.$property -join ', ')
+ } else {
+ # For all other properties, get unique values
+ $obj[$property] = ($cve.group.$property | Sort-Object -Unique) | Select-Object -First 1
+ }
+ }
+
+ # Convert and output as PSCustomObject. Not really needed, but hey, why not.
+ [pscustomobject]$obj
}
$StatusCode = [HttpStatusCode]::OK
} catch {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1
index 1916328d51da..8ca7b8fef050 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Devices/Invoke-ExecDeviceDelete.ps1
@@ -12,28 +12,25 @@ Function Invoke-ExecDeviceDelete {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
- Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with body parameters or the body of the request.
- $TenantFilter = $Request.body.tenantFilter ?? $Request.Query.tenantFilter
- $Action = $Request.body.action ?? $Request.Query.action
- $DeviceID = $Request.body.ID ?? $Request.Query.ID
+ $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter
+ $Action = $Request.Body.action ?? $Request.Query.action
+ $DeviceID = $Request.Body.ID ?? $Request.Query.ID
try {
- $Results = Set-CIPPDeviceState -Action $Action -DeviceID $DeviceID -TenantFilter $TenantFilter -Headers $Request.Headers -APIName $APINAME
+ $Results = Set-CIPPDeviceState -Action $Action -DeviceID $DeviceID -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
$StatusCode = [HttpStatusCode]::OK
} catch {
$Results = $_.Exception.Message
$StatusCode = [HttpStatusCode]::BadRequest
}
- Write-Host $Results
- $body = [pscustomobject]@{'Results' = "$Results" }
-
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $body
+ Body = @{ 'Results' = $Results }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1
index 29c47135f18c..8a14f5d0928a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroup.ps1
@@ -32,17 +32,24 @@ function Invoke-AddGroup {
}
if ($GroupObject.membershipRules) {
$BodyParams | Add-Member -NotePropertyName 'membershipRule' -NotePropertyValue ($GroupObject.membershipRules)
- $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership')
$BodyParams | Add-Member -NotePropertyName 'membershipRuleProcessingState' -NotePropertyValue 'On'
- }
- if ($GroupObject.groupType -eq 'm365') {
+ if ($GroupObject.groupType -eq 'm365') {
+ $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified', 'DynamicMembership')
+ $BodyParams.mailEnabled = $true
+ } else {
+ $BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('DynamicMembership')
+ }
+ # Skip adding static members if we're using dynamic membership
+ $SkipStaticMembers = $true
+ } elseif ($GroupObject.groupType -eq 'm365') {
$BodyParams | Add-Member -NotePropertyName 'groupTypes' -NotePropertyValue @('Unified')
+ $BodyParams.mailEnabled = $true
}
if ($GroupObject.owners) {
$BodyParams | Add-Member -NotePropertyName 'owners@odata.bind' -NotePropertyValue (($GroupObject.owners) | ForEach-Object { "https://graph.microsoft.com/v1.0/users/$($_.value)" })
$BodyParams.'owners@odata.bind' = @($BodyParams.'owners@odata.bind')
}
- if ($GroupObject.members) {
+ if ($GroupObject.members -and -not $SkipStaticMembers) {
$BodyParams | Add-Member -NotePropertyName 'members@odata.bind' -NotePropertyValue (($GroupObject.members) | ForEach-Object { "https://graph.microsoft.com/v1.0/users/$($_.value)" })
$BodyParams.'members@odata.bind' = @($BodyParams.'members@odata.bind')
}
@@ -76,18 +83,18 @@ function Invoke-AddGroup {
"Successfully created group $($GroupObject.displayName) for $($tenant)"
Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Created group $($GroupObject.displayName) with id $($GraphRequest.id)" -Sev Info
-
+ $StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
Write-LogMessage -headers $Request.Headers -API $APIName -tenant $tenant -message "Group creation API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
"Failed to create group. $($GroupObject.displayName) for $($tenant) $($ErrorMessage.NormalizedError)"
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
}
- $ResponseBody = [pscustomobject]@{'Results' = @($Results) }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $ResponseBody
+ StatusCode = $StatusCode
+ Body = @{'Results' = @($Results) }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1
index 12925c3fafbf..51380b38dc68 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-AddGroupTemplate.ps1
@@ -16,11 +16,22 @@ function Invoke-AddGroupTemplate {
$GUID = $Request.Body.GUID ?? (New-Guid).GUID
try {
if (!$Request.Body.displayname) { throw 'You must enter a displayname' }
-
+ $groupType = switch -wildcard ($Request.Body.groupType) {
+ '*dynamic*' { 'dynamic' }
+ '*azurerole*' { 'azurerole' }
+ '*unified*' { 'm365' }
+ '*Microsoft*' { 'm365' }
+ '*generic*' { 'generic' }
+ '*mail*' { 'mailenabledsecurity' }
+ '*Distribution*' { 'distribution' }
+ '*security*' { 'security' }
+ default { $Request.Body.groupType }
+ }
+ if ($Request.body.membershipRules) { $groupType = 'dynamic' }
$object = [PSCustomObject]@{
displayName = $Request.Body.displayName
description = $Request.Body.description
- groupType = $Request.Body.groupType
+ groupType = $groupType
membershipRules = $Request.Body.membershipRules
allowExternal = $Request.Body.allowExternal
username = $Request.Body.username
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1
index ada79c9ca21f..86f1abdd4922 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-EditGroup.ps1
@@ -13,16 +13,16 @@ function Invoke-EditGroup {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
$Results = [System.Collections.Generic.List[string]]@()
- $userobj = $Request.body
- $GroupType = $userobj.groupId.addedFields.groupType ? $userobj.groupId.addedFields.groupType : $userobj.groupType
- $GroupName = $userobj.groupName ? $userobj.groupName : $userobj.groupId.addedFields.groupName
+ $UserObj = $Request.Body
+ $GroupType = $UserObj.groupId.addedFields.groupType ? $UserObj.groupId.addedFields.groupType : $UserObj.groupType
+ $GroupName = $UserObj.groupName ? $UserObj.groupName : $UserObj.groupId.addedFields.groupName
+ $GroupId = $UserObj.groupId.value ?? $UserObj.groupId
+ $OrgGroup = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $UserObj.tenantFilter
- #Write-Warning ($Request.Body | ConvertTo-Json -Depth 10)
+ $AddMembers = $UserObj.AddMember
- $AddMembers = $userobj.AddMember
- $userobj.groupId = $userobj.groupId.value ?? $userobj.groupId
- $TenantId = $userobj.tenantid ?? $userobj.tenantFilter
+ $TenantId = $UserObj.tenantId ?? $UserObj.tenantFilter
$MemberODataBindString = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}'
$BulkRequests = [System.Collections.Generic.List[object]]::new()
@@ -30,17 +30,54 @@ function Invoke-EditGroup {
$ExoBulkRequests = [System.Collections.Generic.List[object]]::new()
$ExoLogs = [System.Collections.Generic.List[object]]::new()
+ if ($UserObj.displayName -or $UserObj.description -or $UserObj.mailNickname -or $UserObj.membershipRules) {
+ #Edit properties:
+ if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') {
+ $Params = @{ Identity = $GroupId; DisplayName = $UserObj.displayName; Description = $UserObj.description; name = $UserObj.mailNickname }
+ $ExoBulkRequests.Add(@{
+ CmdletInput = @{
+ CmdletName = 'Set-DistributionGroup'
+ Parameters = $Params
+ }
+ })
+ $ExoLogs.Add(@{
+ message = "Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes."
+ target = $GroupId
+ })
+ } else {
+ $PatchObj = @{
+ displayName = $UserObj.displayName
+ description = $UserObj.description
+ mailNickname = $UserObj.mailNickname
+ mailEnabled = $OrgGroup.mailEnabled
+ securityEnabled = $OrgGroup.securityEnabled
+ }
+ Write-Host "body: $($PatchObj | ConvertTo-Json -Depth 10 -Compress)" -ForegroundColor Yellow
+ if ($UserObj.membershipRules) { $PatchObj | Add-Member -MemberType NoteProperty -Name 'membershipRule' -Value $UserObj.membershipRules -Force }
+ try {
+ $patch = New-GraphPOSTRequest -type PATCH -uri "https://graph.microsoft.com/beta/groups/$($GroupId)" -tenantid $UserObj.tenantFilter -body ($PatchObj | ConvertTo-Json -Depth 10 -Compress)
+ $Results.Add("Success - Edited group properties for $($GroupName) group. It might take some time to reflect the changes.")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Edited group properties for $($GroupName) group" -Sev 'Info'
+ } catch {
+ $Results.Add("Error - Failed to edit group properties: $($_.Exception.Message)")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Failed to patch group: $($_.Exception.Message)" -Sev 'Error'
+ }
+ }
+ }
+
if ($AddMembers) {
$AddMembers | ForEach-Object {
try {
- $member = $_.value
- $memberid = $_.addedFields.id
- if (!$memberid) {
- $memberid = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$member" -tenantid $TenantId).id
+ # Add to group user action and edit group page sends in different formats, so we need to handle both
+ $Member = $_.value ?? $_
+ $MemberID = $_.addedFields.id
+ if (!$MemberID) {
+ $MemberID = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$Member" -tenantid $TenantId).id
}
if ($GroupType -eq 'Distribution List' -or $GroupType -eq 'Mail-Enabled Security') {
- $Params = @{ Identity = $userobj.groupid; Member = $member; BypassSecurityGroupManagerCheck = $true }
+ $Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true }
+ # Write-Host ($UserObj | ConvertTo-Json -Depth 10) #Debugging line
$ExoBulkRequests.Add(@{
CmdletInput = @{
CmdletName = 'Add-DistributionGroupMember'
@@ -48,27 +85,27 @@ function Invoke-EditGroup {
}
})
$ExoLogs.Add(@{
- message = "Added member $member to $($GroupName) group"
- target = $member
+ message = "Added member $Member to $($GroupName) group"
+ target = $Member
})
} else {
- $MemberIDs = $MemberODataBindString -f $memberid
+ $MemberIDs = $MemberODataBindString -f $MemberID
$AddMemberBody = @{
'members@odata.bind' = @($MemberIDs)
}
$BulkRequests.Add(@{
- id = "addMember-$member"
+ id = "addMember-$Member"
method = 'PATCH'
- url = "groups/$($userobj.groupid)"
+ url = "groups/$($GroupId)"
body = $AddMemberBody
headers = @{
'Content-Type' = 'application/json'
}
})
$GraphLogs.Add(@{
- message = "Added member $member to $($GroupName) group"
- id = "addMember-$member"
+ message = "Added member $Member to $($GroupName) group"
+ id = "addMember-$Member"
})
}
} catch {
@@ -78,13 +115,13 @@ function Invoke-EditGroup {
}
- $AddContacts = $userobj.AddContact
+ $AddContacts = $UserObj.AddContact
if ($AddContacts) {
$AddContacts | ForEach-Object {
try {
- $member = $_
+ $Member = $_
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
- $Params = @{ Identity = $userobj.groupid; Member = $member.value; BypassSecurityGroupManagerCheck = $true }
+ $Params = @{ Identity = $GroupId; Member = $Member.value; BypassSecurityGroupManagerCheck = $true }
$ExoBulkRequests.Add(@{
CmdletInput = @{
CmdletName = 'Add-DistributionGroupMember'
@@ -92,12 +129,12 @@ function Invoke-EditGroup {
}
})
$ExoLogs.Add(@{
- message = "Added contact $($member.label) to $($GroupName) group"
- target = $member.value
+ message = "Added contact $($Member.label) to $($GroupName) group"
+ target = $Member.value
})
} else {
- Write-LogMessage -API $APINAME -tenant $TenantId -headers $Request.Headers -message 'You cannot add a Contact to a Security Group or a M365 Group' -Sev 'Error'
- $null = $results.add('Error - You cannot add a contact to a Security Group or a M365 Group')
+ Write-LogMessage -API $APIName -tenant $TenantId -headers $Headers -message 'You cannot add a Contact to a Security Group or a M365 Group' -Sev 'Error'
+ $null = $Results.Add('Error - You cannot add a contact to a Security Group or a M365 Group')
}
} catch {
Write-Warning "Error in AddContacts: $($_.Exception.Message)"
@@ -105,14 +142,14 @@ function Invoke-EditGroup {
}
}
- $RemoveContact = $userobj.RemoveContact
+ $RemoveContact = $UserObj.RemoveContact
try {
if ($RemoveContact) {
$RemoveContact | ForEach-Object {
- $member = $_.value
- $memberid = $_.addedFields.id
+ $Member = $_.value
+ $MemberID = $_.addedFields.id
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
- $Params = @{ Identity = $userobj.groupid; Member = $memberid ; BypassSecurityGroupManagerCheck = $true }
+ $Params = @{ Identity = $GroupId; Member = $MemberID ; BypassSecurityGroupManagerCheck = $true }
$ExoBulkRequests.Add(@{
CmdletInput = @{
CmdletName = 'Remove-DistributionGroupMember'
@@ -120,12 +157,12 @@ function Invoke-EditGroup {
}
})
$ExoLogs.Add(@{
- message = "Removed contact $member from $($GroupName) group"
- target = $memberid
+ message = "Removed contact $Member from $($GroupName) group"
+ target = $MemberID
})
} else {
- Write-LogMessage -API $APINAME -tenant $TenantId -headers $Request.Headers -message 'You cannot remove a contact from a Security Group' -Sev 'Error'
- $null = $results.add('You cannot remove a contact from a Security Group')
+ Write-LogMessage -API $APIName-tenant $TenantId -headers $Headers -message 'You cannot remove a contact from a Security Group' -Sev 'Error'
+ $null = $Results.Add('You cannot remove a contact from a Security Group')
}
}
}
@@ -133,14 +170,14 @@ function Invoke-EditGroup {
Write-Warning "Error in RemoveContact: $($_.Exception.Message)"
}
- $RemoveMembers = $userobj.Removemember
+ $RemoveMembers = $UserObj.RemoveMember
try {
if ($RemoveMembers) {
$RemoveMembers | ForEach-Object {
- $member = $_.value
- $memberid = $_.addedFields.id
+ $Member = $_.value
+ $MemberID = $_.addedFields.id
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
- $Params = @{ Identity = $userobj.groupid; Member = $member ; BypassSecurityGroupManagerCheck = $true }
+ $Params = @{ Identity = $GroupId; Member = $Member ; BypassSecurityGroupManagerCheck = $true }
$ExoBulkRequests.Add(@{
CmdletInput = @{
CmdletName = 'Remove-DistributionGroupMember'
@@ -148,18 +185,18 @@ function Invoke-EditGroup {
}
})
$ExoLogs.Add(@{
- message = "Removed member $member from $($GroupName) group"
- target = $member
+ message = "Removed member $Member from $($GroupName) group"
+ target = $Member
})
} else {
$BulkRequests.Add(@{
- id = "removeMember-$member"
+ id = "removeMember-$Member"
method = 'DELETE'
- url = "groups/$($userobj.groupid)/members/$memberid/`$ref"
+ url = "groups/$($GroupId)/members/$MemberID/`$ref"
})
$GraphLogs.Add(@{
- message = "Removed member $member from $($GroupName) group"
- id = "removeMember-$member"
+ message = "Removed member $Member from $($GroupName) group"
+ id = "removeMember-$Member"
})
}
}
@@ -168,7 +205,7 @@ function Invoke-EditGroup {
Write-Warning "Error in RemoveMembers: $($_.Exception.Message)"
}
- $AddOwners = $userobj.AddOwner
+ $AddOwners = $UserObj.AddOwner
try {
if ($AddOwners) {
if ($GroupType -notin @('Distribution List', 'Mail-Enabled Security')) {
@@ -179,7 +216,7 @@ function Invoke-EditGroup {
$BulkRequests.Add(@{
id = "addOwner-$Owner"
method = 'POST'
- url = "groups/$($userobj.groupid)/owners/`$ref"
+ url = "groups/$($GroupId)/owners/`$ref"
body = @{
'@odata.id' = $MemberODataBindString -f $ID
}
@@ -198,7 +235,7 @@ function Invoke-EditGroup {
Write-Warning "Error in AddOwners: $($_.Exception.Message)"
}
- $RemoveOwners = $userobj.RemoveOwner
+ $RemoveOwners = $UserObj.RemoveOwner
try {
if ($RemoveOwners) {
if ($GroupType -notin @('Distribution List', 'Mail-Enabled Security')) {
@@ -207,7 +244,7 @@ function Invoke-EditGroup {
$BulkRequests.Add(@{
id = "removeOwner-$ID"
method = 'DELETE'
- url = "groups/$($userobj.groupid)/owners/$ID/`$ref"
+ url = "groups/$($GroupId)/owners/$ID/`$ref"
})
$GraphLogs.Add(@{
message = "Removed $($_.value) from $($GroupName) group"
@@ -221,7 +258,7 @@ function Invoke-EditGroup {
}
if ($GroupType -in @( 'Distribution List', 'Mail-Enabled Security') -and ($AddOwners -or $RemoveOwners)) {
- $CurrentOwners = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-DistributionGroup' -cmdParams @{ Identity = $userobj.groupid } -UseSystemMailbox $true | Select-Object -ExpandProperty ManagedBy
+ $CurrentOwners = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-DistributionGroup' -cmdParams @{ Identity = $GroupId } -UseSystemMailbox $true | Select-Object -ExpandProperty ManagedBy
$NewManagedBy = [system.collections.generic.list[string]]::new()
foreach ($CurrentOwner in $CurrentOwners) {
@@ -229,7 +266,7 @@ function Invoke-EditGroup {
$OwnerToRemove = $RemoveOwners | Where-Object { $_.addedFields.id -eq $CurrentOwner }
$ExoLogs.Add(@{
message = "Removed owner $($OwnerToRemove.label) from $($GroupName) group"
- target = $userobj.groupid
+ target = $GroupId
})
continue
}
@@ -240,21 +277,23 @@ function Invoke-EditGroup {
$NewManagedBy.Add($NewOwner.addedFields.id)
$ExoLogs.Add(@{
message = "Added owner $($NewOwner.label) to $($GroupName) group"
- target = $userobj.groupid
+ target = $GroupId
})
}
}
$NewManagedBy = $NewManagedBy | Sort-Object -Unique
- $params = @{ Identity = $userobj.groupid; ManagedBy = $NewManagedBy }
+ $Params = @{ Identity = $GroupId; ManagedBy = $NewManagedBy }
$ExoBulkRequests.Add(@{
CmdletInput = @{
CmdletName = 'Set-DistributionGroup'
- Parameters = $params
+ Parameters = $Params
}
})
}
+
+
Write-Information "Graph Bulk Requests: $($BulkRequests.Count)"
if ($BulkRequests.Count -gt 0) {
#Write-Warning 'EditUser - Executing Graph Bulk Requests'
@@ -274,7 +313,7 @@ function Invoke-EditGroup {
$Sev = 'Info'
$Results.Add("Success - $Message")
}
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message $Message -Sev $Sev
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Message -Sev $Sev
}
}
@@ -291,7 +330,7 @@ function Invoke-EditGroup {
foreach ($ExoError in $LastError.error) {
$Sev = 'Error'
$Results.Add("Error - $ExoError")
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message $Message -Sev $Sev
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Message -Sev $Sev
}
foreach ($ExoLog in $ExoLogs) {
@@ -300,48 +339,48 @@ function Invoke-EditGroup {
$Message = $ExoLog.message
$Sev = 'Info'
$Results.Add("Success - $Message")
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message $Message -Sev $Sev
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message $Message -Sev $Sev
}
}
}
- if ($userobj.allowExternal -eq $true -and $GroupType -ne 'Security') {
+ if ($UserObj.allowExternal -eq $true -and $GroupType -ne 'Security') {
try {
- Set-CIPPGroupAuthentication -ID $userobj.mail -OnlyAllowInternal (!$userobj.allowExternal) -GroupType $GroupType -tenantFilter $TenantId -APIName $APINAME -Headers $Request.Headers
- $body = $results.add("Allowed external senders to send to $($userobj.mail).")
+ Set-CIPPGroupAuthentication -ID $UserObj.mail -OnlyAllowInternal (!$UserObj.allowExternal) -GroupType $GroupType -tenantFilter $TenantId -APIName $APIName -Headers $Headers
+ $body = $Results.Add("Allowed external senders to send to $($UserObj.mail).")
} catch {
- $body = $results.add("Failed to allow external senders to send to $($userobj.mail).")
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message "Failed to allow external senders for $($userobj.mail). Error:$($_.Exception.Message)" -Sev 'Error'
+ $body = $Results.Add("Failed to allow external senders to send to $($UserObj.mail).")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message "Failed to allow external senders for $($UserObj.mail). Error:$($_.Exception.Message)" -Sev 'Error'
}
}
- if ($userobj.sendCopies -eq $true) {
+ if ($UserObj.sendCopies -eq $true) {
try {
- $Params = @{ Identity = $userobj.Groupid; subscriptionEnabled = $true; AutoSubscribeNewMembers = $true }
- New-ExoRequest -tenantid $TenantId -cmdlet 'Set-UnifiedGroup' -cmdParams $params -useSystemMailbox $true
+ $Params = @{ Identity = $GroupId; subscriptionEnabled = $true; AutoSubscribeNewMembers = $true }
+ New-ExoRequest -tenantid $TenantId -cmdlet 'Set-UnifiedGroup' -cmdParams $Params -useSystemMailbox $true
- $MemberParams = @{ Identity = $userobj.Groupid; LinkType = 'members' }
- $Members = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-UnifiedGrouplinks' -cmdParams $MemberParams
+ $MemberParams = @{ Identity = $GroupId; LinkType = 'members' }
+ $Members = New-ExoRequest -tenantid $TenantId -cmdlet 'Get-UnifiedGroupLinks' -cmdParams $MemberParams
$MemberSmtpAddresses = $Members | ForEach-Object { $_.PrimarySmtpAddress }
if ($MemberSmtpAddresses) {
- $subscriberParams = @{ Identity = $userobj.Groupid; LinkType = 'subscribers'; Links = @($MemberSmtpAddresses | Where-Object { $_ }) }
- New-ExoRequest -tenantid $TenantId -cmdlet 'Add-UnifiedGrouplinks' -cmdParams $subscriberParams -Anchor $userobj.mail
+ $subscriberParams = @{ Identity = $GroupId; LinkType = 'subscribers'; Links = @($MemberSmtpAddresses | Where-Object { $_ }) }
+ New-ExoRequest -tenantid $TenantId -cmdlet 'Add-UnifiedGroupLinks' -cmdParams $subscriberParams -Anchor $UserObj.mail
}
- $body = $results.add("Send Copies of team emails and events to team members inboxes for $($userobj.mail) enabled.")
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message "Send Copies of team emails and events to team members inboxes for $($userobj.mail) enabled." -Sev 'Info'
+ $body = $Results.Add("Send Copies of team emails and events to team members inboxes for $($UserObj.mail) enabled.")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message "Send Copies of team emails and events to team members inboxes for $($UserObj.mail) enabled." -Sev 'Info'
} catch {
Write-Warning "Error in SendCopies: $($_.Exception.Message) - $($_.InvocationInfo.ScriptLineNumber)"
Write-Warning ($_.InvocationInfo.PositionMessage)
- $body = $results.add("Failed to Send Copies of team emails and events to team members inboxes for $($userobj.mail).")
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $TenantId -message "Failed to Send Copies of team emails and events to team members inboxes for $($userobj.mail). Error:$($_.Exception.Message)" -Sev 'Error'
+ $body = $Results.Add("Failed to Send Copies of team emails and events to team members inboxes for $($UserObj.mail).")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantId -message "Failed to Send Copies of team emails and events to team members inboxes for $($UserObj.mail). Error:$($_.Exception.Message)" -Sev 'Error'
}
}
- $body = @{'Results' = @($results) }
+ $body = @{'Results' = @($Results) }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1
index d84dd6bc8bcb..cc9db658bf78 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddGuest.ps1
@@ -15,47 +15,45 @@ Function Invoke-AddGuest {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
$TenantFilter = $Request.Body.tenantFilter
-
- $Results = [System.Collections.ArrayList]@()
$UserObject = $Request.Body
+
try {
if ($UserObject.RedirectURL) {
- $BodyToship = [pscustomobject] @{
+ $BodyToShip = [pscustomobject] @{
'InvitedUserDisplayName' = $UserObject.DisplayName
'InvitedUserEmailAddress' = $($UserObject.mail)
'inviteRedirectUrl' = $($UserObject.RedirectURL)
'sendInvitationMessage' = [bool]$UserObject.SendInvite
}
} else {
- $BodyToship = [pscustomobject] @{
+ $BodyToShip = [pscustomobject] @{
'InvitedUserDisplayName' = $UserObject.DisplayName
'InvitedUserEmailAddress' = $($UserObject.mail)
'sendInvitationMessage' = [bool]$UserObject.SendInvite
'inviteRedirectUrl' = 'https://myapps.microsoft.com'
}
}
- $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToship -Compress
- $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/invitations' -tenantid $TenantFilter -type POST -body $BodyToship -verbose
+ $bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToShip -Compress
+ $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/invitations' -tenantid $TenantFilter -type POST -body $BodyToShip -Verbose
if ($UserObject.SendInvite -eq $true) {
- $Results.Add('Invited Guest. Invite Email sent')
- Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message "Invited Guest $($UserObject.DisplayName) with Email Invite " -Sev 'Info'
+ $Result = "Invited Guest $($UserObject.DisplayName) with Email Invite"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info'
} else {
- $Results.Add('Invited Guest. No Invite Email was sent')
- Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message "Invited Guest $($UserObject.DisplayName) with no Email Invite " -Sev 'Info'
+ $Result = "Invited Guest $($UserObject.DisplayName) with no Email Invite"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info'
}
$StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
$Result = "Failed to Invite Guest. $($ErrorMessage.NormalizedError)"
Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Error' -LogData $ErrorMessage
- $Results.Add($Result)
$StatusCode = [HttpStatusCode]::BadRequest
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = @{Results = @($Results) }
+ Body = @{ 'Results' = @($Result) }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1
index 019ab34d981f..f67050101183 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUser.ps1
@@ -10,20 +10,21 @@ Function Invoke-AddUser {
[CmdletBinding()]
param($Request, $TriggerMetadata)
- $APIName = 'AddUser'
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $UserObj = $Request.body
+ $UserObj = $Request.Body
if ($UserObj.Scheduled.Enabled) {
$TaskBody = [pscustomobject]@{
- TenantFilter = $UserObj.tenantfilter
+ TenantFilter = $UserObj.tenantFilter
Name = "New user creation: $($UserObj.mailNickname)@$($UserObj.PrimDomain.value)"
Command = @{
value = 'New-CIPPUserTask'
label = 'New-CIPPUserTask'
}
- Parameters = [pscustomobject]@{ userobj = $UserObj }
+ Parameters = [pscustomobject]@{ UserObj = $UserObj }
ScheduledTime = $UserObj.Scheduled.date
PostExecution = @{
Webhook = [bool]$Request.Body.PostExecution.Webhook
@@ -31,20 +32,20 @@ Function Invoke-AddUser {
PSA = [bool]$Request.Body.PostExecution.PSA
}
}
- Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true -Headers $Request.Headers
+ Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true -Headers $Headers
$body = [pscustomobject] @{
'Results' = @("Successfully created scheduled task to create user $($UserObj.DisplayName)")
}
} else {
- $CreationResults = New-CIPPUserTask -userobj $UserObj -APIName $APINAME -Headers $Request.Headers
+ $CreationResults = New-CIPPUserTask -UserObj $UserObj -APIName $APIName -Headers $Headers
$body = [pscustomobject] @{
'Results' = @(
$CreationResults.Results[0],
$CreationResults.Results[1],
@{
'resultText' = $CreationResults.Results[2]
- 'copyField' = $CreationResults.password
- 'state' = 'success'
+ 'copyField' = $CreationResults.password
+ 'state' = 'success'
}
)
'CopyFrom' = @{
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1
index 9a017652bd39..3bb2c6b06a53 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-AddUserBulk.ps1
@@ -10,9 +10,12 @@ function Invoke-AddUserBulk {
[CmdletBinding()]
param($Request, $TriggerMetadata)
- $APIName = 'AddUserBulk'
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
- $TenantFilter = $Request.body.TenantFilter
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with body parameters or the body of the request.
+ $TenantFilter = $Request.Body.tenantFilter
$BulkUsers = $Request.Body.BulkUser
$AssignedLicenses = $Request.Body.licenses
@@ -78,7 +81,7 @@ function Invoke-AddUserBulk {
# Add all other properties
foreach ($key in $User.PSObject.Properties.Name) {
if ($key -notin @('displayName', 'mailNickName', 'domain', 'password', 'usageLocation', 'businessPhones')) {
- if (![string]::IsNullOrEmpty($User.$key) -and $UserBody.$key -eq $null) {
+ if (![string]::IsNullOrEmpty($User.$key) -and $null -eq $UserBody.$key) {
$UserBody.$key = $User.$key
}
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1
index 3bb2fc720957..d0ad323f12f7 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1
@@ -27,7 +27,7 @@ function Invoke-CIPPOffboardingJob {
Remove-CIPPGroups -userid $userid -tenantFilter $TenantFilter -Headers $Headers -APIName $APIName -Username "$Username"
}
{ $_.HideFromGAL -eq $true } {
- Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -hidefromgal $true -Headers $Headers -APIName $APIName
+ Set-CIPPHideFromGAL -tenantFilter $TenantFilter -UserID $username -HideFromGAL $true -Headers $Headers -APIName $APIName
}
{ $_.DisableSignIn -eq $true } {
Set-CIPPSignInState -TenantFilter $TenantFilter -userid $username -AccountEnabled $false -Headers $Headers -APIName $APIName
@@ -42,13 +42,17 @@ function Invoke-CIPPOffboardingJob {
$Options.AccessAutomap | ForEach-Object { Set-CIPPMailboxAccess -tenantFilter $TenantFilter -userid $username -AccessUser $_.value -Automap $true -AccessRights @('FullAccess') -Headers $Headers -APIName $APIName }
}
{ $_.OOO } {
- Set-CIPPOutOfOffice -tenantFilter $TenantFilter -userid $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -Headers $Headers -APIName $APIName -state 'Enabled'
+ try {
+ Set-CIPPOutOfOffice -tenantFilter $TenantFilter -UserID $username -InternalMessage $Options.OOO -ExternalMessage $Options.OOO -Headers $Headers -APIName $APIName -state 'Enabled'
+ } catch {
+ $_.Exception.Message
+ }
}
{ $_.forward } {
- if (!$Options.keepCopy) {
+ if (!$Options.KeepCopy) {
Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Forward $Options.forward.value -Headers $Headers -APIName $APIName
} else {
- $KeepCopy = [boolean]$Options.keepCopy
+ $KeepCopy = [boolean]$Options.KeepCopy
Set-CIPPForwarding -userid $userid -username $username -tenantFilter $TenantFilter -Forward $Options.forward.value -KeepCopy $KeepCopy -Headers $Headers -APIName $APIName
}
}
@@ -99,7 +103,7 @@ function Invoke-CIPPOffboardingJob {
Remove-CIPPUserMFA -UserPrincipalName $Username -TenantFilter $TenantFilter -Headers $Headers
}
{ $_.'ClearImmutableId' -eq $true } {
- Clear-CIPPImmutableId -userid $userid -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
+ Clear-CIPPImmutableID -UserID $userid -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
}
}
return $Return
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1
index 33d5b08ceea5..d40f20130797 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUser.ps1
@@ -12,7 +12,7 @@ function Invoke-EditUser {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $ApiName -message 'Accessed this API' -Sev 'Debug'
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
$UserObj = $Request.Body
if ([string]::IsNullOrWhiteSpace($UserObj.id)) {
@@ -69,18 +69,18 @@ function Invoke-EditUser {
}
$bodyToShip = ConvertTo-Json -Depth 10 -InputObject $BodyToship -Compress
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $BodyToship -verbose
- $null = $Results.Add( 'Success. The user has been edited.' )
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Edited user $($UserObj.DisplayName) with id $($UserObj.id)" -Sev Info
+ $Results.Add( 'Success. The user has been edited.' )
+ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Edited user $($UserObj.DisplayName) with id $($UserObj.id)" -Sev Info
if ($UserObj.password) {
$passwordProfile = [pscustomobject]@{'passwordProfile' = @{ 'password' = $UserObj.password; 'forceChangePasswordNextSignIn' = [boolean]$UserObj.MustChangePass } } | ConvertTo-Json
- $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $PasswordProfile -verbose
- $null = $Results.Add("Success. The password has been set to $($UserObj.password)")
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Reset $($UserObj.DisplayName)'s Password" -Sev Info
+ $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type PATCH -body $PasswordProfile -Verbose
+ $Results.Add("Success. The password has been set to $($UserObj.password)")
+ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Reset $($UserObj.DisplayName)'s Password" -Sev Info
}
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "User edit API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
- $null = $Results.Add( "Failed to edit user. $($ErrorMessage.NormalizedError)")
+ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "User edit API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $Results.Add( "Failed to edit user. $($ErrorMessage.NormalizedError)")
}
@@ -90,7 +90,7 @@ function Invoke-EditUser {
if ($licenses -or $UserObj.removeLicenses) {
if ($UserObj.sherwebLicense.value) {
$null = Set-SherwebSubscription -Headers $Headers -TenantFilter $UserObj.tenantFilter -SKU $UserObj.sherwebLicense.value -Add 1
- $null = $Results.Add('Added Sherweb License, scheduling assignment')
+ $Results.Add('Added Sherweb License, scheduling assignment')
$taskObject = [PSCustomObject]@{
TenantFilter = $UserObj.tenantFilter
Name = "Assign License: $UserPrincipalName"
@@ -115,16 +115,16 @@ function Invoke-EditUser {
#if the list of skuIds in $CurrentLicenses.assignedLicenses is EXACTLY the same as $licenses, we don't need to do anything, but the order in both can be different.
if (($CurrentLicenses.assignedLicenses.skuId -join ',') -eq ($licenses -join ',') -and $UserObj.removeLicenses -eq $false) {
Write-Host "$($CurrentLicenses.assignedLicenses.skuId -join ',') $(($licenses -join ','))"
- $null = $results.Add( 'Success. User license is already correct.' )
+ $Results.Add( 'Success. User license is already correct.' )
} else {
if ($UserObj.removeLicenses) {
$licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $CurrentLicenses.assignedLicenses.skuId -Headers $Headers
- $null = $results.Add($licResults)
+ $Results.Add($licResults)
} else {
#Remove all objects from $CurrentLicenses.assignedLicenses.skuId that are in $licenses
$RemoveLicenses = $CurrentLicenses.assignedLicenses.skuId | Where-Object { $_ -notin $licenses }
$licResults = Set-CIPPUserLicense -UserId $UserObj.id -TenantFilter $UserObj.tenantFilter -RemoveLicenses $RemoveLicenses -AddLicenses $licenses -Headers $headers
- $null = $results.Add($licResults)
+ $Results.Add($licResults)
}
}
@@ -133,8 +133,8 @@ function Invoke-EditUser {
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "License assign API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
- $null = $results.Add( "We've failed to assign the license. $($ErrorMessage.NormalizedError)")
+ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "License assign API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $Results.Add( "We've failed to assign the license. $($ErrorMessage.NormalizedError)")
Write-Warning "License assign API failed. $($_.Exception.Message)"
Write-Information $_.InvocationInfo.PositionMessage
}
@@ -147,19 +147,20 @@ function Invoke-EditUser {
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type 'patch' -body "{`"mail`": `"$Alias`"}" -Verbose
}
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)" -tenantid $UserObj.tenantFilter -type 'patch' -body "{`"mail`": `"$UserPrincipalName`"}" -Verbose
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Added Aliases to $($UserObj.DisplayName)" -Sev Info
- $null = $results.Add( 'Success. added aliases to user.')
+ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Added Aliases to $($UserObj.DisplayName)" -Sev Info
+ $null = $Results.Add( 'Success. Added aliases to user.')
}
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Alias API failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
- $null = $results.Add( "Successfully edited user. The password is $password. We've failed to create the Aliases: $($ErrorMessage.NormalizedError)")
+ $Message = "Failed to add aliases to user $($UserObj.DisplayName). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -API $APIName -tenant ($UserObj.tenantFilter) -headers $Headers -message $Message -Sev Error -LogData $ErrorMessage
+ $null = $Results.Add($Message)
}
if ($Request.Body.CopyFrom.value) {
$CopyFrom = Set-CIPPCopyGroupMembers -Headers $Headers -CopyFromId $Request.Body.CopyFrom.value -UserID $UserPrincipalName -TenantFilter $UserObj.tenantFilter
- $null = $results.AddRange(@($CopyFrom))
+ $null = $Results.AddRange(@($CopyFrom))
}
if ($AddToGroups) {
@@ -183,13 +184,13 @@ function Invoke-EditUser {
$UserBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $UserBody
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$GroupID/members/`$ref" -tenantid $UserObj.tenantFilter -type POST -body $UserBodyJSON -Verbose
}
- Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Added $($UserObj.DisplayName) to $GroupName group" -Sev Info
- $null = $results.Add("Success. $($UserObj.DisplayName) has been added to $GroupName")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Added $($UserObj.DisplayName) to $GroupName group" -Sev Info
+ $null = $Results.Add("Success. $($UserObj.DisplayName) has been added to $GroupName")
} catch {
$ErrorMessage = Get-CippException -Exception $_
$Message = "Failed to add member $($UserObj.DisplayName) to $GroupName. Error: $($ErrorMessage.NormalizedError)"
- Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage
- $null = $results.Add($Message)
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage
+ $null = $Results.Add($Message)
}
}
}
@@ -211,13 +212,13 @@ function Invoke-EditUser {
Write-Host 'Removing From group via Graph'
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$GroupID/members/$($UserObj.id)/`$ref" -tenantid $UserObj.tenantFilter -type DELETE
}
- Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Removed $($UserObj.DisplayName) from $GroupName group" -Sev Info
- $null = $results.Add("Success. $($UserObj.DisplayName) has been removed from $GroupName")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Removed $($UserObj.DisplayName) from $GroupName group" -Sev Info
+ $null = $Results.Add("Success. $($UserObj.DisplayName) has been removed from $GroupName")
} catch {
$ErrorMessage = Get-CippException -Exception $_
$Message = "Failed to remove member $($UserObj.DisplayName) from $GroupName. Error: $($ErrorMessage.NormalizedError)"
- Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage
- $null = $results.Add($Message)
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message $Message -Sev Error -LogData $ErrorMessage
+ $null = $Results.Add($Message)
}
}
}
@@ -226,16 +227,16 @@ function Invoke-EditUser {
$ManagerBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Request.body.setManager.value)" }
$ManagerBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $ManagerBody
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)/manager/`$ref" -tenantid $UserObj.tenantFilter -type PUT -body $ManagerBodyJSON -Verbose
- Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)" -Sev Info
- $null = $results.Add("Success. Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)" -Sev Info
+ $null = $Results.Add("Success. Set $($UserObj.DisplayName)'s manager to $($Request.body.setManager.label)")
}
if ($Request.body.setSponsor.value) {
$SponsorBody = [PSCustomObject]@{'@odata.id' = "https://graph.microsoft.com/beta/users/$($Request.body.setSponsor.value)" }
$SponsorBodyJSON = ConvertTo-Json -Compress -Depth 10 -InputObject $SponsorBody
$null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserObj.id)/sponsors/`$ref" -tenantid $UserObj.tenantFilter -type POST -body $SponsorBodyJSON -Verbose
- Write-LogMessage -headers $Headers -API $ApiName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)" -Sev Info
- $null = $results.Add("Success. Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)")
+ Write-LogMessage -headers $Headers -API $APIName -tenant $UserObj.tenantFilter -message "Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)" -Sev Info
+ $null = $Results.Add("Success. Set $($UserObj.DisplayName)'s sponsor to $($Request.body.setSponsor.label)")
}
$body = @{'Results' = @($results) }
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUserAliases.ps1
similarity index 71%
rename from Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1
rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUserAliases.ps1
index 3ca90f60b72a..d8f254cead0a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-SetUserAliases.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-EditUserAliases.ps1
@@ -1,6 +1,6 @@
using namespace System.Net
-Function Invoke-SetUserAliases {
+Function Invoke-EditUserAliases {
<#
.FUNCTIONALITY
Entrypoint
@@ -12,9 +12,11 @@ Function Invoke-SetUserAliases {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $ApiName -message 'Accessed this API' -Sev 'Debug'
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
$UserObj = $Request.Body
+ $TenantFilter = $UserObj.tenantFilter
+
if ([string]::IsNullOrWhiteSpace($UserObj.id)) {
$body = @{'Results' = @('Failed to manage aliases. No user ID provided') }
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
@@ -31,8 +33,8 @@ Function Invoke-SetUserAliases {
try {
if ($Aliases -or $RemoveAliases -or $UserObj.MakePrimary) {
# Get current mailbox
- $CurrentMailbox = New-ExoRequest -tenantid $UserObj.tenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{ Identity = $UserObj.id } -UseSystemMailbox $true
-
+ $CurrentMailbox = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-Mailbox' -cmdParams @{ Identity = $UserObj.id } -UseSystemMailbox $true
+
if (-not $CurrentMailbox) {
throw 'Could not find mailbox for user'
}
@@ -40,26 +42,26 @@ Function Invoke-SetUserAliases {
$CurrentProxyAddresses = @($CurrentMailbox.EmailAddresses)
Write-Host "Current proxy addresses: $($CurrentProxyAddresses -join ', ')"
$NewProxyAddresses = @($CurrentProxyAddresses)
-
+
# Handle setting primary address
if ($UserObj.MakePrimary) {
$PrimaryAddress = $UserObj.MakePrimary
Write-Host "Attempting to set primary address: $PrimaryAddress"
-
+
# Normalize the primary address format
if ($PrimaryAddress -notlike 'SMTP:*') {
$PrimaryAddress = "SMTP:$($PrimaryAddress -replace '^smtp:', '')"
}
Write-Host "Normalized primary address: $PrimaryAddress"
-
+
# Check if the address exists in the current addresses (case-insensitive)
- $ExistingAddress = $CurrentProxyAddresses | Where-Object {
+ $ExistingAddress = $CurrentProxyAddresses | Where-Object {
$current = $_.ToLower()
$target = $PrimaryAddress.ToLower()
Write-Host "Comparing: '$current' with '$target'"
$current -eq $target
}
-
+
if (-not $ExistingAddress) {
Write-Host "Available addresses: $($CurrentProxyAddresses -join ', ')"
throw "Cannot set primary address. Address $($PrimaryAddress -replace '^SMTP:', '') not found in user's addresses."
@@ -69,23 +71,22 @@ Function Invoke-SetUserAliases {
$NewProxyAddresses = $NewProxyAddresses | ForEach-Object {
if ($_ -like 'SMTP:*') {
$_.ToLower()
- }
- else {
+ } else {
$_
}
}
# Remove any existing version of the address (case-insensitive)
- $NewProxyAddresses = $NewProxyAddresses | Where-Object {
- $_.ToLower() -ne $PrimaryAddress.ToLower()
+ $NewProxyAddresses = $NewProxyAddresses | Where-Object {
+ $_.ToLower() -ne $PrimaryAddress.ToLower()
}
# Add the new primary address at the beginning
$NewProxyAddresses = @($PrimaryAddress) + $NewProxyAddresses
-
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Set primary address for $($CurrentMailbox.DisplayName)" -Sev Info
- $null = $results.Add('Success. Set new primary address.')
+
+ Write-LogMessage -API $APIName -tenant $TenantFilter -headers $Headers -message "Set primary address for $($CurrentMailbox.DisplayName)" -Sev Info
+ $Results.Add('Success. Set new primary address.')
}
-
+
# Remove specified aliases
if ($RemoveAliases) {
foreach ($Alias in $RemoveAliases) {
@@ -94,12 +95,12 @@ Function Invoke-SetUserAliases {
$Alias = "smtp:$Alias"
}
# Remove the alias case-insensitively
- $NewProxyAddresses = $NewProxyAddresses | Where-Object {
- $_.ToLower() -ne $Alias.ToLower()
+ $NewProxyAddresses = $NewProxyAddresses | Where-Object {
+ $_.ToLower() -ne $Alias.ToLower()
}
}
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Removed Aliases from $($CurrentMailbox.DisplayName)" -Sev Info
- $null = $results.Add('Success. Removed specified aliases from user.')
+ Write-LogMessage -API $ApiName -tenant $TenantFilter -headers $Headers -message "Removed Aliases from $($CurrentMailbox.DisplayName)" -Sev Info
+ $Results.Add('Success. Removed specified aliases from user.')
}
# Add new aliases
@@ -117,8 +118,8 @@ Function Invoke-SetUserAliases {
}
if ($AliasesToAdd.Count -gt 0) {
$NewProxyAddresses = $NewProxyAddresses + $AliasesToAdd
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Added Aliases to $($CurrentMailbox.DisplayName)" -Sev Info
- $null = $results.Add('Success. Added new aliases to user.')
+ Write-LogMessage -API $ApiName -tenant ($TenantFilter) -headers $Headers -message "Added Aliases to $($CurrentMailbox.DisplayName)" -Sev Info
+ $Results.Add('Success. Added new aliases to user.')
}
}
@@ -127,21 +128,18 @@ Function Invoke-SetUserAliases {
Identity = $UserObj.id
EmailAddresses = $NewProxyAddresses
}
- $null = New-ExoRequest -tenantid $UserObj.tenantFilter -cmdlet 'Set-Mailbox' -cmdParams $Params -UseSystemMailbox $true
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-Mailbox' -cmdParams $Params -UseSystemMailbox $true
+ } else {
+ $Results.Add('No alias changes specified.')
}
- else {
- $null = $results.Add('No alias changes specified.')
- }
- }
- catch {
+ } catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -API $ApiName -tenant ($UserObj.tenantFilter) -headers $Headers -message "Alias management failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
- $null = $results.Add("Failed to manage aliases: $($ErrorMessage.NormalizedError)")
+ Write-LogMessage -API $ApiName -tenant ($TenantFilter) -headers $Headers -message "Alias management failed. $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $Results.Add("Failed to manage aliases: $($ErrorMessage.NormalizedError)")
}
- $body = @{'Results' = @($results) }
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
- Body = $Body
+ Body = @{'Results' = @($Results) }
})
-}
\ No newline at end of file
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1
index 27389fffd877..533e02eea899 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1
@@ -19,7 +19,7 @@ Function Invoke-ExecBECCheck {
$Batch = @{
'FunctionName' = 'BECRun'
'UserID' = $Request.Query.userid
- 'TenantFilter' = $Request.Query.tenantfilter
+ 'TenantFilter' = $Request.Query.tenantFilter
'userName' = $Request.Query.userName
}
@@ -40,7 +40,7 @@ Function Invoke-ExecBECCheck {
SkipLog = $true
}
#Write-Host ($InputObject | ConvertTo-Json)
- $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
+ $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ( ConvertTo-Json -InputObject $InputObject -Depth 5 -Compress )
@{ GUID = $Request.Query.userid }
} else {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1
index d647d7fc5dff..51d8203a77c6 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECRemediate.ps1
@@ -1,6 +1,6 @@
using namespace System.Net
-Function Invoke-ExecBECRemediate {
+function Invoke-ExecBECRemediate {
<#
.FUNCTIONALITY
Entrypoint
@@ -54,7 +54,7 @@ Function Invoke-ExecBECRemediate {
"Failed to disable $RuleFailed Inbox Rules for $Username"
}
$StatusCode = [HttpStatusCode]::OK
- Write-LogMessage -API 'BECRemediate' -tenant $TenantFilter -message "Executed Remediation for $Username" -sev 'Info'
+ Write-LogMessage -API 'BECRemediate' -tenant $TenantFilter -message "Executed Remediation for $Username" -sev 'Info' -LogData @($Results)
} catch {
$ErrorMessage = Get-CippException -Exception $_
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1
new file mode 100644
index 000000000000..fd81b158e865
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBulkLicense.ps1
@@ -0,0 +1,85 @@
+Function Invoke-ExecBulkLicense {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Identity.User.ReadWrite
+ #>
+ [CmdletBinding()]
+ param (
+ $Request,
+ $TriggerMetadata
+ )
+
+ $APIName = $TriggerMetadata.FunctionName
+ $Results = [System.Collections.Generic.List[string]]::new()
+ $StatusCode = [HttpStatusCode]::OK
+
+ try {
+ $UserRequests = $Request.Body
+ $TenantGroups = $UserRequests | Group-Object -Property tenantFilter
+
+ foreach ($TenantGroup in $TenantGroups) {
+ $TenantFilter = $TenantGroup.Name
+ $TenantRequests = $TenantGroup.Group
+ $AllUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?&`$select=id,userPrincipalName,assignedLicenses" -tenantid $TenantFilter
+ $UserLookup = @{}
+ foreach ($User in $AllUsers) {
+ $UserLookup[$User.id] = $User
+ }
+
+ # Process each user request
+ foreach ($UserRequest in $TenantRequests) {
+ try {
+ $UserId = $UserRequest.userIds
+ $User = $UserLookup[$UserId]
+ $UserPrincipalName = $User.userPrincipalName
+ $LicenseOperation = $UserRequest.LicenseOperation
+ $RemoveAllLicenses = [bool]$UserRequest.RemoveAllLicenses
+ $Licenses = $UserRequest.Licenses | ForEach-Object { $_.value }
+ # Handle license operations
+ if ($LicenseOperation -eq 'Add' -or $LicenseOperation -eq 'Replace') {
+ $AddLicenses = $Licenses
+ }
+
+ if ($LicenseOperation -eq 'Remove' -and $RemoveAllLicenses) {
+ $RemoveLicenses = $User.assignedLicenses.skuId
+ } elseif ($LicenseOperation -eq 'Remove') {
+ $RemoveLicenses = $Licenses
+ } elseif ($LicenseOperation -eq 'Replace') {
+ $RemoveReplace = $User.assignedLicenses.skuId
+ if ($RemoveReplace) { Set-CIPPUserLicense -UserId $UserId -TenantFilter $TenantFilter -RemoveLicenses $RemoveReplace }
+ } elseif ($RemoveAllLicenses) {
+ $RemoveLicenses = $User.assignedLicenses.skuId
+ }
+ #todo: Actually build bulk support into set-cippuserlicense.
+ $TaskResults = Set-CIPPUserLicense -UserId $UserId -TenantFilter $TenantFilter -AddLicenses $AddLicenses -RemoveLicenses $RemoveLicenses
+
+ $Results.Add($TaskResults)
+ Write-LogMessage -API $APIName -tenant $TenantFilter -message "Successfully processed licenses for user $UserPrincipalName" -Sev 'Info'
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results.Add("Failed to process licenses for user $($UserRequest.userIds). Error: $($ErrorMessage.NormalizedError)")
+ Write-LogMessage -API $APIName -tenant $TenantFilter -message "Failed to process licenses for user $($UserRequest.userIds). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
+ }
+ }
+ }
+
+ $Body = @{
+ Results = @($Results)
+ }
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $StatusCode = [HttpStatusCode]::BadRequest
+ $Body = @{
+ Results = @("Failed to process bulk license operation: $($ErrorMessage.NormalizedError)")
+ }
+ Write-LogMessage -API $APIName -message "Failed to process bulk license operation: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
+ }
+
+ # Return response
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $Body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1
index 50374bc33a1b..b234dfbbf136 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecClrImmId.ps1
@@ -11,23 +11,24 @@ Function Invoke-ExecClrImmId {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev Debug
+
+ # Interact with body parameters or the body of the request.
$TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
- Write-LogMessage -headers $Request.Headers -API $APIName -message 'Accessed this API' -Sev Debug
$UserID = $Request.Query.ID ?? $Request.Body.ID
- Try {
- $Result = Clear-CIPPImmutableId -userid $UserID -TenantFilter $TenantFilter -Headers $Request.Headers -APIName $APIName
+ try {
+ $Result = Clear-CIPPImmutableID -UserID $UserID -TenantFilter $TenantFilter -Headers $Headers -APIName $APIName
$StatusCode = [HttpStatusCode]::OK
} catch {
- $ErrorMessage = Get-CippException -Exception $_
- $Result = $ErrorMessage.NormalizedError
+ $Result = $_.Exception.Message
$StatusCode = [HttpStatusCode]::InternalServerError
}
- $Results = [pscustomobject]@{'Results' = $Result }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $Results
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1
index 58187b2f6640..ef3282aa4243 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecCreateTAP.ps1
@@ -17,23 +17,37 @@ Function Invoke-ExecCreateTAP {
# Interact with query parameters or the body of the request.
$TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
$UserID = $Request.Query.ID ?? $Request.Body.ID
+ $LifetimeInMinutes = $Request.Query.lifetimeInMinutes ?? $Request.Body.lifetimeInMinutes
+ $IsUsableOnce = $Request.Query.isUsableOnce ?? $Request.Body.isUsableOnce
+ $StartDateTime = $Request.Query.startDateTime ?? $Request.Body.startDateTime
try {
- $TAPResult = New-CIPPTAP -userid $UserID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers
+ # Create parameter hashtable for splatting
+ $TAPParams = @{
+ UserID = $UserID
+ TenantFilter = $TenantFilter
+ APIName = $APIName
+ Headers = $Headers
+ LifetimeInMinutes = $LifetimeInMinutes
+ IsUsableOnce = $IsUsableOnce
+ StartDateTime = $StartDateTime
+ }
+
+ $TAPResult = New-CIPPTAP @TAPParams
# Create results array with both TAP and UserID as separate items
$Results = @(
$TAPResult,
@{
resultText = "User ID: $UserID"
- copyField = $UserID
- state = 'success'
+ copyField = $UserID
+ state = 'success'
}
)
$StatusCode = [HttpStatusCode]::OK
} catch {
- $Results = Get-NormalizedError -message $($_.Exception.Message)
+ $Results = $_.Exception.Message
$StatusCode = [HttpStatusCode]::InternalServerError
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1
index 67a2b036cffb..c1e6748b7f32 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDisableUser.ps1
@@ -29,11 +29,10 @@ Function Invoke-ExecDisableUser {
$StatusCode = [HttpStatusCode]::InternalServerError
}
- $Results = [pscustomobject]@{'Results' = "$Result" }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $Results
+ Body = @{ 'Results' = "$Result" }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1
index d22cca0f8e9e..a1e07c97a31a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecDismissRiskyUser.ps1
@@ -29,8 +29,8 @@ function Invoke-ExecDismissRiskyUser {
try {
$GraphResults = New-GraphPostRequest @GraphRequest
- Write-LogMessage -API $APIName -tenant $TenantFilter -message "Dismissed user risk for $userDisplayName" -sev 'Info'
$Result = "Successfully dismissed User Risk for user $userDisplayName. $GraphResults"
+ Write-LogMessage -API $APIName -tenant $TenantFilter -message $Result -sev 'Info'
$StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1
index e67fb1cb32d0..ae45ad066f45 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1
@@ -125,14 +125,14 @@ function Invoke-ExecJITAdmin {
if ($Request.Body.existingUser.value -match '^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$') {
$Username = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($Request.Body.existingUser.value)" -tenantid $TenantFilter).userPrincipalName
}
- Write-LogMessage -Headers $User -API $APINAME -message "Executing JIT Admin for $Username" -tenant $TenantFilter -Sev 'Info'
+ Write-LogMessage -Headers $User -API $APIName -message "Executing JIT Admin for $Username" -tenant $TenantFilter -Sev 'Info'
$Start = ([System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate)).DateTime.ToLocalTime()
$Expiration = ([System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.EndDate)).DateTime.ToLocalTime()
$Results = [System.Collections.Generic.List[string]]::new()
if ($Request.Body.useraction -eq 'Create') {
- Write-LogMessage -Headers $User -API $APINAME -tenant $TenantFilter -message "Creating JIT Admin user $($Request.Body.Username)" -Sev 'Info'
+ Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Creating JIT Admin user $($Request.Body.Username)" -Sev 'Info'
Write-Information "Creating JIT Admin user $($Request.Body.username)"
$JITAdmin = @{
User = @{
@@ -262,6 +262,8 @@ function Invoke-ExecJITAdmin {
}
}
+ # TODO - We should find a way to have this return a HTTP status code based on the success or failure of the operation. This also doesn't return the results of the operation in a Results hash table, like most of the rest of the API.
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
Body = $Body
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1
deleted file mode 100644
index 10b4c8576330..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboard_Mailboxpermissions.ps1
+++ /dev/null
@@ -1,17 +0,0 @@
-using namespace System.Net
-
-Function Invoke-ExecOffboard_Mailboxpermissions {
- <#
- .FUNCTIONALITY
- Entrypoint
- .ROLE
- Exchange.Mailbox.ReadWrite
- #>
- [CmdletBinding()]
- param($Request, $TriggerMetadata)
-
- foreach ($Mailbox in $Mailboxes) {
- Remove-CIPPMailboxPermissions -PermissionsLevel @('FullAccess', 'SendAs', 'SendOnBehalf') -userid $Mailbox.UserPrincipalName -AccessUser $QueueItem.User -TenantFilter $QueueItem.TenantFilter -APIName $APINAME -Headers $QueueItem.Headers
- }
-
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1
index 5da791680ea4..5bf461f6a0d5 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOneDriveShortCut.ps1
@@ -15,16 +15,17 @@ Function Invoke-ExecOneDriveShortCut {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
Try {
- $MessageResult = New-CIPPOneDriveShortCut -username $Request.Body.username -userid $Request.Body.userid -TenantFilter $Request.Body.tenantFilter -URL $Request.Body.siteUrl.value -Headers $Request.Headers
- $Results = [pscustomobject]@{ 'Results' = "$MessageResult" }
+ $Result = New-CIPPOneDriveShortCut -username $Request.Body.username -userid $Request.Body.userid -TenantFilter $Request.Body.tenantFilter -URL $Request.Body.siteUrl.value -Headers $Request.Headers
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- $Results = [pscustomobject]@{'Results' = "OneDrive Shortcut creation failed: $($_.Exception.Message)" }
+ $Result = $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Results
+ StatusCode = $StatusCode
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1
index d86806c39f68..48d6be9274fb 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOnedriveProvision.ps1
@@ -11,18 +11,23 @@ Function Invoke-ExecOneDriveProvision {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- $Params = $Request.Body ?? $Request.Query
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ $UserPrincipalName = $Request.Body.UserPrincipalName ?? $Request.Query.UserPrincipalName
+ $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter
+
try {
- $State = Request-CIPPSPOPersonalSite -TenantFilter $Params.TenantFilter -UserEmails $Params.UserPrincipalName -Headers $Request.Headers -APIName $APINAME
- $Results = [pscustomobject]@{'Results' = "$State" }
+ $Result = Request-CIPPSPOPersonalSite -TenantFilter $TenantFilter -UserEmails $UserPrincipalName -Headers $Headers -APIName $APIName
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- $Results = [pscustomobject]@{'Results' = "Failed. $ErrorMessage" }
+ $Result = $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Results
+ StatusCode = $StatusCode
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1
deleted file mode 100644
index 7a7c296b4016..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFAAllUsers.ps1
+++ /dev/null
@@ -1,34 +0,0 @@
-function Invoke-ExecPerUserMFAAllUsers {
- <#
- .FUNCTIONALITY
- Entrypoint
-
- .ROLE
- Identity.User.ReadWrite
- #>
- Param($Request, $TriggerMetadata)
-
- $APIName = $Request.Params.CIPPEndpoint
- $Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
- # XXX Seems to be an unused endpoint? - Bobby
-
- $TenantFilter = $request.Query.tenantFilter
- $Users = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter
- $Request = @{
- userId = $Users.id
- TenantFilter = $TenantFilter
- State = $Request.Query.State
- Headers = $Request.Headers
- APIName = $APIName
- }
- $Result = Set-CIPPPerUserMFA @Request
- $Body = @{
- Results = @($Result)
- }
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Body
- })
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1
index 4cb4c25db418..a72a7f82e40e 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetMFA.ps1
@@ -12,17 +12,17 @@ Function Invoke-ExecResetMFA {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with query parameters or the body of the request.
$TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
$UserID = $Request.Query.ID ?? $Request.Body.ID
try {
$Result = Remove-CIPPUserMFA -UserPrincipalName $UserID -TenantFilter $TenantFilter -Headers $Headers
- if ($Result -match 'Failed') { throw $Result }
+ if ($Result -match '^Failed') { throw $Result }
$StatusCode = [HttpStatusCode]::OK
} catch {
- $Result = "$($_.Exception.Message)"
+ $Result = $_.Exception.Message
$StatusCode = [HttpStatusCode]::InternalServerError
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1
index 11fed934022b..d2080b726ab8 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecResetPass.ps1
@@ -23,21 +23,18 @@ Function Invoke-ExecResetPass {
$MustChange = [System.Convert]::ToBoolean($MustChange)
try {
- $Result = Set-CIPPResetPassword -UserID $ID -tenantFilter $TenantFilter -APIName $APINAME -Headers $Request.Headers -forceChangePasswordNextSignIn $MustChange -DisplayName $DisplayName
+ $Result = Set-CIPPResetPassword -UserID $ID -tenantFilter $TenantFilter -APIName $APIName -Headers $Headers -forceChangePasswordNextSignIn $MustChange -DisplayName $DisplayName
if ($Result.state -eq 'Error') { throw $Result.resultText }
$StatusCode = [HttpStatusCode]::OK
} catch {
$Result = $_.Exception.Message
- Write-LogMessage -headers $Request.Headers -API $APINAME -message $Result -Sev 'Error'
$StatusCode = [HttpStatusCode]::InternalServerError
-
}
- $Results = [pscustomobject]@{'Results' = $Result }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $Results
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1
index cf4300a8054d..36daf3d8914c 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRestoreDeleted.ps1
@@ -39,11 +39,10 @@ Function Invoke-ExecRestoreDeleted {
$StatusCode = [HttpStatusCode]::InternalServerError
}
- $Results = [pscustomobject]@{'Results' = $Result }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $Results
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1
index 822c6356dcf1..4d39d67fbbec 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecRevokeSessions.ps1
@@ -21,18 +21,17 @@ Function Invoke-ExecRevokeSessions {
try {
$Result = Revoke-CIPPSessions -UserID $ID -TenantFilter $TenantFilter -Username $Username -APIName $APIName -Headers $Request.Headers
- if ($Result -like 'Revoke Session Failed*') { throw $Result }
+ if ($Result -match '^Failed') { throw $Result }
$StatusCode = [HttpStatusCode]::OK
} catch {
$Result = $_.Exception.Message
$StatusCode = [HttpStatusCode]::InternalServerError
}
- $Results = [pscustomobject]@{'Results' = $Result }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $Results
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1
index 78b099d6eee6..d9af4f6e232c 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecSendPush.ps1
@@ -52,7 +52,7 @@ function Invoke-ExecSendPush {
$SPBody = [pscustomobject]@{
appId = $MFAAppID
- }
+ } | ConvertTo-Json -Depth 5
$SPID = (New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $TenantFilter -type POST -body $SPBody -AsApp $true).id
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1
index 7e6d040a18d9..2c6db5ca62ef 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListPerUserMFA.ps1
@@ -12,9 +12,7 @@ function Invoke-ListPerUserMFA {
$APIName = $Request.Params.CIPPEndpoint
$User = $Request.Headers
- Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug'
-
-
+ Write-LogMessage -Headers $User -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Parse query parameters
$Tenant = $Request.query.tenantFilter
@@ -30,13 +28,13 @@ function Invoke-ListPerUserMFA {
if ($AllUsers -eq $true) {
$Results = Get-CIPPPerUserMFA -TenantFilter $Tenant -AllUsers $true
} else {
- $Results = Get-CIPPPerUserMFA -TenantFilter $Tenant -userId $UserId
+ $Results = Get-CIPPPerUserMFA -TenantFilter $Tenant -UserId $UserId
}
$StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
$Results = "Failed to get MFA State for $UserId : $ErrorMessage"
- $StatusCode = [HttpStatusCode]::Forbidden
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1
index bd8ce3a7eb27..585fb8c71bbc 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserConditionalAccessPolicies.ps1
@@ -14,11 +14,10 @@ Function Invoke-ListUserConditionalAccessPolicies {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
-
+ # XXX - Unused endpoint?
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
$UserID = $Request.Query.UserID
try {
@@ -30,14 +29,14 @@ Function Invoke-ListUserConditionalAccessPolicies {
$ConditionalAccessWhatIfDefinition = @{
'conditionalAccessWhatIfSubject' = @{
'@odata.type' = '#microsoft.graph.userSubject'
- 'userId' = "$userId"
+ 'userId' = "$UserID"
}
'conditionalAccessContext' = $CAContext
'conditionalAccessWhatIfConditions' = @{}
}
- $JSONBody = $ConditionalAccessWhatIfDefinition | ConvertTo-Json -Depth 10
+ $JSONBody = ConvertTo-Json -Depth 10 -InputObject $ConditionalAccessWhatIfDefinition -Compress
- $GraphRequest = (New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate' -tenantid $tenantFilter -type POST -body $JsonBody -AsApp $true).value
+ $GraphRequest = (New-GraphPostRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate' -tenantid $TenantFilter -type POST -body $JsonBody -AsApp $true).value
} catch {
$GraphRequest = @{}
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1
index f9522b7648a9..e7d995268ad2 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserCounts.ps1
@@ -14,28 +14,25 @@ Function Invoke-ListUserCounts {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
-
-
# Interact with query parameters or the body of the request.
$TenantFilter = $Request.Query.TenantFilter
if ($Request.Query.TenantFilter -eq 'AllTenants') {
- $users = 'Not Supported'
+ $Users = 'Not Supported'
$LicUsers = 'Not Supported'
$GAs = 'Not Supported'
$Guests = 'Not Supported'
} else {
try { $Users = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Users = 'Not available' }
- try { $LicUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=assignedLicenses/`$count ne 0" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Licusers = 'Not available' }
- try { $GAs = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$count=true" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Gas = 'Not available' }
- try { $guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=userType eq 'Guest'" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Guests = 'Not available' }
+ try { $LicUsers = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=assignedLicenses/`$count ne 0" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $LicUsers = 'Not available' }
+ try { $GAs = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directoryRoles/roleTemplateId=62e90394-69f5-4237-9190-012177145e10/members?`$count=true" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $GAs = 'Not available' }
+ try { $Guests = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$count=true&`$top=1&`$filter=userType eq 'Guest'" -CountOnly -ComplexFilter -tenantid $TenantFilter } catch { $Guests = 'Not available' }
}
$StatusCode = [HttpStatusCode]::OK
$Counts = @{
- Users = $users
+ Users = $Users
LicUsers = $LicUsers
- Gas = $Gas
- Guests = $guests
+ Gas = $GAs
+ Guests = $Guests
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1
index d37bc6e836b2..427b61b05f60 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserDevices.ps1
@@ -14,11 +14,8 @@ Function Invoke-ListUserDevices {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
-
-
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
$UserID = $Request.Query.UserID
function Get-EPMID {
@@ -33,8 +30,8 @@ Function Invoke-ListUserDevices {
}
}
try {
- $EPMDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/managedDevices" -Tenantid $tenantfilter
- $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/ownedDevices?`$top=999" -Tenantid $tenantfilter | Select-Object @{ Name = 'ID'; Expression = { $_.'id' } },
+ $EPMDevices = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/managedDevices" -Tenantid $TenantFilter
+ $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$UserID/ownedDevices?`$top=999" -Tenantid $TenantFilter | Select-Object @{ Name = 'ID'; Expression = { $_.'id' } },
@{ Name = 'accountEnabled'; Expression = { $_.'accountEnabled' } },
@{ Name = 'approximateLastSignInDateTime'; Expression = { $_.'approximateLastSignInDateTime' | Out-String } },
@{ Name = 'createdDateTime'; Expression = { $_.'createdDateTime' | Out-String } },
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1
index e1181a6893fc..1f5ed5a1a1bf 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxDetails.ps1
@@ -64,7 +64,7 @@ function Invoke-ListUserMailboxDetails {
}
)
Write-Host $UserID
- $usernames = New-GraphGetRequest -tenantid $TenantFilter -uri 'https://graph.microsoft.com/beta/users?$select=id,userPrincipalName&$top=999'
+ $usernames = New-GraphGetRequest -tenantid $TenantFilter -uri 'https://graph.microsoft.com/beta/users?$select=id,userPrincipalName,displayName,mailNickname&$top=999'
$Results = New-ExoBulkRequest -TenantId $TenantFilter -CmdletArray $Requests -returnWithCommand $true -Anchor $username
Write-Host "First line of usernames is $($usernames[0] | ConvertTo-Json)"
@@ -115,7 +115,7 @@ function Invoke-ListUserMailboxDetails {
# Parse permissions
- #Implemented as an arraylist that uses .add().
+ #Implemented as an ArrayList that uses .add().
$ParsedPerms = [System.Collections.ArrayList]::new()
foreach ($PermSet in @($PermsRequest, $PermsRequest2)) {
foreach ($Perm in $PermSet) {
@@ -144,21 +144,48 @@ function Invoke-ListUserMailboxDetails {
$ParsedPerms = @()
}
- # Get forwarding address
- $ForwardingAddress = if ($MailboxDetailedRequest.ForwardingAddress) {
- try {
- (New-GraphGetRequest -TenantId $TenantFilter -Uri "https://graph.microsoft.com/beta/users/$($MailboxDetailedRequest.ForwardingAddress)").UserPrincipalName
- } catch {
- try {
- '{0} ({1})' -f $MailboxDetailedRequest.ForwardingAddress, (($((New-GraphGetRequest -TenantId $TenantFilter -Uri "https://graph.microsoft.com/beta/users?`$filter=displayName eq '$($MailboxDetailedRequest.ForwardingAddress)'") | Select-Object -First 1 -ExpandProperty UserPrincipalName)))
- } catch {
- $MailboxDetailedRequest.ForwardingAddress
+ # Get forwarding address - lazy load contacts only if needed
+ $ForwardingAddress = $null
+ if ($MailboxDetailedRequest.ForwardingSmtpAddress) {
+ # External forwarding
+ $ForwardingAddress = $MailboxDetailedRequest.ForwardingSmtpAddress -replace '^smtp:', ''
+ } elseif ($MailboxDetailedRequest.ForwardingAddress) {
+ # Internal forwarding
+ $rawAddress = $MailboxDetailedRequest.ForwardingAddress
+
+ if ($rawAddress -match '@') {
+ # Already an email address
+ $ForwardingAddress = $rawAddress
+ } else {
+ # First try users array
+ $matchedUser = $usernames | Where-Object {
+ $_.id -eq $rawAddress -or
+ $_.displayName -eq $rawAddress -or
+ $_.mailNickname -eq $rawAddress
+ }
+
+ if ($matchedUser) {
+ $ForwardingAddress = $matchedUser.userPrincipalName
+ } else {
+ # Query for the specific contact only
+ try {
+ # Escape single quotes in the filter value
+ $escapedAddress = $rawAddress -replace "'", "''"
+ $filterQuery = "displayName eq '$escapedAddress' or mailNickname eq '$escapedAddress'"
+ $contactUri = "https://graph.microsoft.com/beta/contacts?`$filter=$filterQuery&`$select=displayName,mail,mailNickname"
+
+ $matchedContacts = New-GraphGetRequest -tenantid $TenantFilter -uri $contactUri
+
+ if ($matchedContacts -and $matchedContacts.Count -gt 0) {
+ $ForwardingAddress = $matchedContacts[0].mail
+ } else {
+ $ForwardingAddress = $rawAddress
+ }
+ } catch {
+ $ForwardingAddress = $rawAddress
+ }
}
}
- } elseif ($MailboxDetailedRequest.ForwardingSmtpAddress -and $MailboxDetailedRequest.ForwardingAddress) {
- "$($MailboxDetailedRequest.ForwardingAddress) $($MailboxDetailedRequest.ForwardingSmtpAddress)"
- } else {
- $MailboxDetailedRequest.ForwardingSmtpAddress
}
$ProhibitSendQuotaString = $MailboxDetailedRequest.ProhibitSendQuota -split ' '
@@ -177,7 +204,7 @@ function Invoke-ListUserMailboxDetails {
$TotalArchiveItemCount = try { [math]::Round($ArchiveSizeRequest.ItemCount, 2) } catch { 0 }
}
- # Parse InPlaceHolds to determine hold types if avaliable
+ # Parse InPlaceHolds to determine hold types if available
$InPlaceHold = $false
$EDiscoveryHold = $false
$PurviewRetentionHold = $false
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1
index 74def3815f6d..750062e50315 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserMailboxRules.ps1
@@ -11,33 +11,28 @@ Function Invoke-ListUserMailboxRules {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- $User = $Request.Headers
- Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug'
-
-
-
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter
+ $UserID = $Request.Query.UserID
try {
- $TenantFilter = $Request.Query.TenantFilter
- $UserID = $Request.Query.UserID
$UserEmail = if ([string]::IsNullOrWhiteSpace($Request.Query.userEmail)) { $UserID } else { $Request.Query.userEmail }
- $GraphRequest = New-ExoRequest -Anchor $UserID -tenantid $TenantFilter -cmdlet 'Get-InboxRule' -cmdParams @{mailbox = $UserID; IncludeHidden = $true } | Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' } | Select-Object * -ExcludeProperty RuleIdentity
+ $Result = New-ExoRequest -Anchor $UserID -tenantid $TenantFilter -cmdlet 'Get-InboxRule' -cmdParams @{mailbox = $UserID; IncludeHidden = $true } |
+ Where-Object { $_.Name -ne 'Junk E-Mail Rule' -and $_.Name -notlike 'Microsoft.Exchange.OOF.*' } | Select-Object * -ExcludeProperty RuleIdentity
+ $StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -Headers $User -API $APINAME -message "Failed to retrieve mailbox rules $($UserEmail): $($ErrorMessage.NormalizedError) " -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = '500'
- Body = $ErrorMessage.NormalizedError
- })
- exit
+ $Result = "Failed to retrieve mailbox rules for $UserEmail : Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -Headers $Headers -tenant $TenantFilter -API $APIName -message $Result -Sev Error -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = @($GraphRequest)
+ StatusCode = $StatusCode
+ Body = @($Result)
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1
index 154dc1fffaa6..19ceb7562341 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserPhoto.ps1
@@ -10,7 +10,12 @@ Function Invoke-ListUserPhoto {
[CmdletBinding()]
param($Request, $TriggerMetadata)
- $tenantFilter = $Request.Query.TenantFilter
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $tenantFilter = $Request.Query.tenantFilter
$userId = $Request.Query.UserID
$URI = "/users/$userId/photo/`$value"
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1
index 858ddfb27da4..901678b7b6ea 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSettings.ps1
@@ -12,13 +12,22 @@ function Invoke-ListUserSettings {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($request.headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails
+
+ $Username = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json).userDetails
try {
$Table = Get-CippTable -tablename 'UserSettings'
$UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq 'allUsers'"
- if (!$UserSettings) { $userSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$username'" }
+ if (!$UserSettings) { $UserSettings = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$Username'" }
$UserSettings = $UserSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue
+ #Get branding settings
+ if ($UserSettings) {
+ $brandingTable = Get-CippTable -tablename 'Config'
+ $BrandingSettings = Get-CIPPAzDataTableEntity @brandingTable -Filter "RowKey eq 'BrandingSettings'"
+ if ($BrandingSettings) {
+ $UserSettings | Add-Member -MemberType NoteProperty -Name 'BrandingSettings' -Value $BrandingSettings -Force | Out-Null
+ }
+ }
$StatusCode = [HttpStatusCode]::OK
$Results = $UserSettings
} catch {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1
index a0009394dd72..1804d4b4384a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserSigninLogs.ps1
@@ -18,26 +18,22 @@ Function Invoke-ListUserSigninLogs {
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
$UserID = $Request.Query.UserID
+ $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$UserID')&`$top=$top&`$orderby=createdDateTime desc"
+
try {
- $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$UserID')&`$top=$top&`$orderby=createdDateTime desc"
- Write-Host $URI
- $GraphRequest = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose
- Write-Host $GraphRequest
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = @($GraphRequest)
- })
+ $Result = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- Write-LogMessage -headers $Request.Headers -API $APINAME -message "Failed to retrieve Sign In report: $($_.Exception.message) " -Sev 'Error' -tenant $TenantFilter
- # Associate values to output bindings by calling 'Push-OutputBinding'.
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = '500'
- Body = $(Get-NormalizedError -message $_.Exception.message)
- })
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to retrieve Sign In report for user $UserID : Error: $($ErrorMessage.NormalizedError)"
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
-
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @($Result)
+ })
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1
index d2a5c2685bd3..5e81ea1c684b 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUsers.ps1
@@ -16,7 +16,7 @@ Function Invoke-ListUsers {
$ConvertTable = Import-Csv ConversionTable.csv | Sort-Object -Property 'guid' -Unique
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
$GraphFilter = $Request.Query.graphFilter
$userid = $Request.Query.UserID
@@ -26,7 +26,7 @@ Function Invoke-ListUsers {
$_ | Add-Member -MemberType NoteProperty -Name 'username' -Value ($_.userPrincipalName -split '@' | Select-Object -First 1) -Force
$_ | Add-Member -MemberType NoteProperty -Name 'Aliases' -Value ($_.ProxyAddresses -join ', ') -Force
$SkuID = $_.AssignedLicenses.skuid
- $_ | Add-Member -MemberType NoteProperty -Name 'LicJoined' -Value (($ConvertTable | Where-Object { $_.guid -in $skuid }).'Product_Display_Name' -join ', ') -Force
+ $_ | Add-Member -MemberType NoteProperty -Name 'LicJoined' -Value (($ConvertTable | Where-Object { $_.guid -in $SkuID }).'Product_Display_Name' -join ', ') -Force
$_ | Add-Member -MemberType NoteProperty -Name 'primDomain' -Value @{value = ($_.userPrincipalName -split '@' | Select-Object -Last 1); label = ($_.userPrincipalName -split '@' | Select-Object -Last 1); } -Force
$_
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1
index da4e516d6462..f41f79d68485 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-RemoveDeletedObject.ps1
@@ -39,11 +39,10 @@ Function Invoke-RemoveDeletedObject {
$StatusCode = [HttpStatusCode]::InternalServerError
}
- $Results = [pscustomobject]@{'Results' = $Result }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $Results
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1
new file mode 100644
index 000000000000..fe506d605367
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyFromTemplate.ps1
@@ -0,0 +1,227 @@
+using namespace System.Net
+
+Function Invoke-AddSafeLinksPolicyFromTemplate {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.SafeLinks.ReadWrite
+ .DESCRIPTION
+ This function deploys SafeLinks policies and rules from templates to selected tenants.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ try {
+ $RequestBody = $Request.Body
+
+ # Extract tenant IDs from selectedTenants
+ $SelectedTenants = $RequestBody.selectedTenants | ForEach-Object { $_.value }
+ if ('AllTenants' -in $SelectedTenants) {
+ $SelectedTenants = (Get-Tenants).defaultDomainName
+ }
+
+ # Extract templates from TemplateList
+ $Templates = $RequestBody.TemplateList | ForEach-Object { $_.value }
+
+ if (-not $Templates -or $Templates.Count -eq 0) {
+ throw "No templates provided in TemplateList"
+ }
+
+ # Helper function to process array fields with cleaner logic
+ function ConvertTo-SafeArray {
+ param($Field)
+
+ if ($null -eq $Field) { return @() }
+
+ # Handle arrays
+ if ($Field -is [array]) {
+ return $Field | ForEach-Object {
+ if ($_ -is [string]) { $_ }
+ elseif ($_.value) { $_.value }
+ elseif ($_.userPrincipalName) { $_.userPrincipalName }
+ elseif ($_.id) { $_.id }
+ else { $_.ToString() }
+ }
+ }
+
+ # Handle single objects
+ if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) {
+ if ($Field.value) { return @($Field.value) }
+ if ($Field.userPrincipalName) { return @($Field.userPrincipalName) }
+ if ($Field.id) { return @($Field.id) }
+ }
+
+ # Handle strings
+ if ($Field -is [string]) { return @($Field) }
+
+ return @($Field)
+ }
+
+ function Test-PolicyExists {
+ param($TenantFilter, $PolicyName)
+
+ $ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true
+ return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName }
+ }
+
+ function Test-RuleExists {
+ param($TenantFilter, $RuleName)
+
+ $ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true
+ return $ExistingRules | Where-Object { $_.Name -eq $RuleName }
+ }
+
+ function New-SafeLinksPolicyFromTemplate {
+ param($TenantFilter, $Template)
+
+ $PolicyName = $Template.PolicyName
+ $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule"
+
+ # Check if policy already exists
+ if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) {
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning'
+ return "Policy '$PolicyName' already exists in tenant $TenantFilter"
+ }
+
+ # Check if rule already exists
+ if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) {
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning'
+ return "Rule '$RuleName' already exists in tenant $TenantFilter"
+ }
+
+ # Process array fields
+ $DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls
+ $SentTo = ConvertTo-SafeArray -Field $Template.SentTo
+ $SentToMemberOf = ConvertTo-SafeArray -Field $Template.SentToMemberOf
+ $RecipientDomainIs = ConvertTo-SafeArray -Field $Template.RecipientDomainIs
+ $ExceptIfSentTo = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo
+ $ExceptIfSentToMemberOf = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf
+ $ExceptIfRecipientDomainIs = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs
+
+ # Create policy parameters
+ $PolicyParams = @{ Name = $PolicyName }
+
+ # Policy configuration mapping
+ $PolicyMappings = @{
+ 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail'
+ 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams'
+ 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice'
+ 'TrackClicks' = 'TrackClicks'
+ 'AllowClickThrough' = 'AllowClickThrough'
+ 'ScanUrls' = 'ScanUrls'
+ 'EnableForInternalSenders' = 'EnableForInternalSenders'
+ 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan'
+ 'DisableUrlRewrite' = 'DisableUrlRewrite'
+ 'AdminDisplayName' = 'AdminDisplayName'
+ 'CustomNotificationText' = 'CustomNotificationText'
+ 'EnableOrganizationBranding' = 'EnableOrganizationBranding'
+ }
+
+ foreach ($templateKey in $PolicyMappings.Keys) {
+ if ($null -ne $Template.$templateKey) {
+ $PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey
+ }
+ }
+
+ if ($DoNotRewriteUrls.Count -gt 0) {
+ $PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls
+ }
+
+ # Create SafeLinks Policy
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks policy '$PolicyName'" -Sev 'Info'
+
+ # Create rule parameters
+ $RuleParams = @{
+ Name = $RuleName
+ SafeLinksPolicy = $PolicyName
+ }
+
+ # Rule configuration mapping
+ $RuleMappings = @{
+ 'Priority' = 'Priority'
+ 'TemplateDescription' = 'Comments'
+ }
+
+ foreach ($templateKey in $RuleMappings.Keys) {
+ if ($null -ne $Template.$templateKey) {
+ $RuleParams[$RuleMappings[$templateKey]] = $Template.$templateKey
+ }
+ }
+
+ # Add array parameters if they have values
+ $ArrayMappings = @{
+ 'SentTo' = $SentTo
+ 'SentToMemberOf' = $SentToMemberOf
+ 'RecipientDomainIs' = $RecipientDomainIs
+ 'ExceptIfSentTo' = $ExceptIfSentTo
+ 'ExceptIfSentToMemberOf' = $ExceptIfSentToMemberOf
+ 'ExceptIfRecipientDomainIs' = $ExceptIfRecipientDomainIs
+ }
+
+ foreach ($paramName in $ArrayMappings.Keys) {
+ if ($ArrayMappings[$paramName].Count -gt 0) {
+ $RuleParams[$paramName] = $ArrayMappings[$paramName]
+ }
+ }
+
+ # Create SafeLinks Rule
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Created SafeLinks rule '$RuleName'" -Sev 'Info'
+
+ # Handle rule state
+ $StateMessage = ""
+ if ($null -ne $Template.State) {
+ $IsState = switch ($Template.State) {
+ "Enabled" { $true }
+ "Disabled" { $false }
+ $true { $true }
+ $false { $false }
+ default { $null }
+ }
+
+ if ($null -ne $IsState) {
+ $Cmdlet = $IsState ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule'
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true
+ $StateMessage = " (rule $($IsState ? 'enabled' : 'disabled'))"
+ }
+ }
+
+ return "Successfully deployed SafeLinks policy '$PolicyName' and rule '$RuleName' to tenant $TenantFilter$StateMessage"
+ }
+
+ # Process each tenant and template combination
+ $Results = foreach ($TenantFilter in $SelectedTenants) {
+ foreach ($Template in $Templates) {
+ try {
+ New-SafeLinksPolicyFromTemplate -TenantFilter $TenantFilter -Template $Template
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $ErrorDetail = "Failed to deploy template '$($Template.TemplateName)' to tenant $TenantFilter. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ErrorDetail -Sev 'Error'
+ $ErrorDetail
+ }
+ }
+ }
+
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Failed to process template deployment request. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Return response
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{ Results = $Results }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1
new file mode 100644
index 000000000000..c4e4a467fa0e
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-AddSafeLinksPolicyTemplate.ps1
@@ -0,0 +1,96 @@
+using namespace System.Net
+Function Invoke-AddSafeLinksPolicyTemplate {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.SafeLinks.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug
+
+ # Debug: Log the incoming request body
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Request body: $($Request.body | ConvertTo-Json -Depth 5 -Compress)" -Sev Debug
+
+ try {
+ $GUID = (New-Guid).GUID
+
+ # Validate required fields
+ if ([string]::IsNullOrEmpty($Request.body.Name)) {
+ throw "Template name is required but was not provided"
+ }
+
+ if ([string]::IsNullOrEmpty($Request.body.PolicyName)) {
+ throw "Policy name is required but was not provided"
+ }
+
+ # Create a new ordered hashtable to store selected properties
+ $policyObject = [ordered]@{}
+
+ # Set name and comments - prioritize template-specific fields
+ $policyObject["TemplateName"] = $Request.body.TemplateName
+ $policyObject["TemplateDescription"] = $Request.body.TemplateDescription
+
+ # For templates, if no specific policy description is provided, use template description as default
+ if ([string]::IsNullOrEmpty($Request.body.AdminDisplayName) -and -not [string]::IsNullOrEmpty($Request.body.Description)) {
+ $Request.body.AdminDisplayName = $Request.body.Description
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Using template description as default policy description" -Sev Debug
+ }
+
+ # Log what we're using for template name and description
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Template Name: '$($policyObject.TemplateName)', Description: '$($policyObject.TemplateDescription)'" -Sev Debug
+
+ # Copy specific properties we want to keep
+ $propertiesToKeep = @(
+ # Policy properties
+ "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice",
+ "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders",
+ "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls",
+ "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding",
+ # Rule properties
+ "RuleName", "Priority", "State", "Comments",
+ "SentTo", "SentToMemberOf", "RecipientDomainIs",
+ "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs"
+ )
+
+ # Copy each property if it exists
+ foreach ($prop in $propertiesToKeep) {
+ if ($null -ne $Request.body.$prop) {
+ $policyObject[$prop] = $Request.body.$prop
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Added property '$prop' with value '$($Request.body.$prop)'" -Sev Debug
+ }
+ }
+
+ # Convert to JSON
+ $JSON = $policyObject | ConvertTo-Json -Depth 10
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Final JSON: $JSON" -Sev Debug
+
+ # Save the template to Azure Table Storage
+ $Table = Get-CippTable -tablename 'templates'
+ $Table.Force = $true
+ Add-CIPPAzDataTableEntity @Table -Entity @{
+ JSON = "$JSON"
+ RowKey = "$GUID"
+ PartitionKey = 'SafeLinksTemplate'
+ }
+
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Created SafeLinks Policy Template '$($policyObject.TemplateName)' with GUID $GUID" -Sev Info
+ $body = [pscustomobject]@{'Results' = "Created SafeLinks Policy Template '$($policyObject.TemplateName)' with GUID $GUID" }
+ $StatusCode = [HttpStatusCode]::OK
+
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $body = [pscustomobject]@{'Results' = "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" }
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1
new file mode 100644
index 000000000000..8023f9b62bbb
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-CreateSafeLinksPolicyTemplate.ps1
@@ -0,0 +1,77 @@
+using namespace System.Net
+
+Function Invoke-CreateSafeLinksPolicyTemplate {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.SafeLinks.ReadWrite
+ .DESCRIPTION
+ This function creates a new Safe Links policy template from scratch.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug
+
+ try {
+ $GUID = (New-Guid).GUID
+
+ # Create a new ordered hashtable to store selected properties
+ $policyObject = [ordered]@{}
+
+ # Set name and comments first
+ $policyObject["TemplateName"] = $Request.body.TemplateName
+ $policyObject["TemplateDescription"] = $Request.body.TemplateDescription
+
+ # Copy specific properties we want to keep
+ $propertiesToKeep = @(
+ # Policy properties
+ "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice",
+ "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders",
+ "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls",
+ "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding",
+
+ # Rule properties
+ "RuleName", "Priority", "State", "Comments",
+ "SentTo", "SentToMemberOf", "RecipientDomainIs",
+ "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs"
+ )
+
+ # Copy each property if it exists
+ foreach ($prop in $propertiesToKeep) {
+ if ($null -ne $Request.body.$prop) {
+ $policyObject[$prop] = $Request.body.$prop
+ }
+ }
+
+ # Convert to JSON
+ $JSON = $policyObject | ConvertTo-Json -Depth 10
+
+ # Save the template to Azure Table Storage
+ $Table = Get-CippTable -tablename 'templates'
+ $Table.Force = $true
+ Add-CIPPAzDataTableEntity @Table -Entity @{
+ JSON = "$JSON"
+ RowKey = "$GUID"
+ PartitionKey = 'SafeLinksTemplate'
+ }
+
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Created SafeLinks Policy Template $($policyObject.TemplateName) with GUID $GUID" -Sev Info
+ $body = [pscustomobject]@{'Results' = "Created SafeLinks Policy Template $($policyObject.TemplateName) with GUID $GUID" }
+ $StatusCode = [HttpStatusCode]::OK
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $body = [pscustomobject]@{'Results' = "Failed to create SafeLinks policy template: $($ErrorMessage.NormalizedError)" }
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1
new file mode 100644
index 000000000000..c763a14a21e1
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicy.ps1
@@ -0,0 +1,217 @@
+using namespace System.Net
+
+function Invoke-EditSafeLinksPolicy {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.SpamFilter.ReadWrite
+ .DESCRIPTION
+ This function modifies an existing Safe Links policy and its associated rule.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
+ $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName
+ $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName
+
+ # Helper function to process array fields
+ function Process-ArrayField {
+ param (
+ [Parameter(Mandatory = $false)]
+ $Field
+ )
+
+ if ($null -eq $Field) { return @() }
+
+ # If already an array, process each item
+ if ($Field -is [array]) {
+ $result = [System.Collections.ArrayList]@()
+ foreach ($item in $Field) {
+ if ($item -is [string]) {
+ $result.Add($item) | Out-Null
+ }
+ elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) {
+ # Extract value from object
+ if ($null -ne $item.value) {
+ $result.Add($item.value) | Out-Null
+ }
+ elseif ($null -ne $item.userPrincipalName) {
+ $result.Add($item.userPrincipalName) | Out-Null
+ }
+ elseif ($null -ne $item.id) {
+ $result.Add($item.id) | Out-Null
+ }
+ else {
+ $result.Add($item.ToString()) | Out-Null
+ }
+ }
+ else {
+ $result.Add($item.ToString()) | Out-Null
+ }
+ }
+ return $result.ToArray()
+ }
+
+ # If it's a single object
+ if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) {
+ if ($null -ne $Field.value) { return @($Field.value) }
+ if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) }
+ if ($null -ne $Field.id) { return @($Field.id) }
+ }
+
+ # If it's a string, return as an array with one item
+ if ($Field -is [string]) {
+ return @($Field)
+ }
+
+ return @($Field)
+ }
+
+ # Extract policy parameters from body
+ $EnableSafeLinksForEmail = $Request.Body.EnableSafeLinksForEmail
+ $EnableSafeLinksForTeams = $Request.Body.EnableSafeLinksForTeams
+ $EnableSafeLinksForOffice = $Request.Body.EnableSafeLinksForOffice
+ $TrackClicks = $Request.Body.TrackClicks
+ $AllowClickThrough = $Request.Body.AllowClickThrough
+ $ScanUrls = $Request.Body.ScanUrls
+ $EnableForInternalSenders = $Request.Body.EnableForInternalSenders
+ $DeliverMessageAfterScan = $Request.Body.DeliverMessageAfterScan
+ $DisableUrlRewrite = $Request.Body.DisableUrlRewrite
+ $DoNotRewriteUrls = Process-ArrayField -Field $Request.Body.DoNotRewriteUrls
+ $AdminDisplayName = $Request.Body.AdminDisplayName
+ $CustomNotificationText = $Request.Body.CustomNotificationText
+ $EnableOrganizationBranding = $Request.Body.EnableOrganizationBranding
+
+ # Extract rule parameters from body
+ $Priority = $Request.Body.Priority
+ $Comments = $Request.Body.Comments
+ $State = $Request.Body.State
+
+ # Process recipient-related parameters
+ $SentTo = Process-ArrayField -Field $Request.Body.SentTo
+ $SentToMemberOf = Process-ArrayField -Field $Request.Body.SentToMemberOf
+ $RecipientDomainIs = Process-ArrayField -Field $Request.Body.RecipientDomainIs
+ $ExceptIfSentTo = Process-ArrayField -Field $Request.Body.ExceptIfSentTo
+ $ExceptIfSentToMemberOf = Process-ArrayField -Field $Request.Body.ExceptIfSentToMemberOf
+ $ExceptIfRecipientDomainIs = Process-ArrayField -Field $Request.Body.ExceptIfRecipientDomainIs
+
+ $Results = [System.Collections.ArrayList]@()
+ $hasPolicyParams = $false
+ $hasRuleParams = $false
+ $hasRuleOperation = $false
+ $ruleMessages = [System.Collections.ArrayList]@()
+
+ try {
+ # Check which types of updates we need to perform
+ # PART 1: Build and check policy parameters
+ $policyParams = @{
+ Identity = $PolicyName
+ }
+
+ # Only add parameters that are explicitly provided
+ if ($null -ne $EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $EnableSafeLinksForEmail); $hasPolicyParams = $true }
+ if ($null -ne $EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $EnableSafeLinksForTeams); $hasPolicyParams = $true }
+ if ($null -ne $EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $EnableSafeLinksForOffice); $hasPolicyParams = $true }
+ if ($null -ne $TrackClicks) { $policyParams.Add('TrackClicks', $TrackClicks); $hasPolicyParams = $true }
+ if ($null -ne $AllowClickThrough) { $policyParams.Add('AllowClickThrough', $AllowClickThrough); $hasPolicyParams = $true }
+ if ($null -ne $ScanUrls) { $policyParams.Add('ScanUrls', $ScanUrls); $hasPolicyParams = $true }
+ if ($null -ne $EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $EnableForInternalSenders); $hasPolicyParams = $true }
+ if ($null -ne $DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $DeliverMessageAfterScan); $hasPolicyParams = $true }
+ if ($null -ne $DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $DisableUrlRewrite); $hasPolicyParams = $true }
+ if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls); $hasPolicyParams = $true }
+ if ($null -ne $AdminDisplayName) { $policyParams.Add('AdminDisplayName', $AdminDisplayName); $hasPolicyParams = $true }
+ if ($null -ne $CustomNotificationText) { $policyParams.Add('CustomNotificationText', $CustomNotificationText); $hasPolicyParams = $true }
+ if ($null -ne $EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $EnableOrganizationBranding); $hasPolicyParams = $true }
+
+ # PART 2: Build and check rule parameters
+ $ruleParams = @{
+ Identity = $RuleName
+ }
+
+ # Add parameters that are explicitly provided
+ if ($null -ne $Comments) { $ruleParams.Add('Comments', $Comments); $hasRuleParams = $true }
+ if ($null -ne $Priority) { $ruleParams.Add('Priority', $Priority); $hasRuleParams = $true }
+ if ($SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo); $hasRuleParams = $true }
+ if ($SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf); $hasRuleParams = $true }
+ if ($RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs); $hasRuleParams = $true }
+ if ($ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo); $hasRuleParams = $true }
+ if ($ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf); $hasRuleParams = $true }
+ if ($ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs); $hasRuleParams = $true }
+
+ # Now perform only the necessary operations
+
+ # PART 1: Update policy if needed
+ if ($hasPolicyParams) {
+ $ExoPolicyRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Set-SafeLinksPolicy'
+ cmdParams = $policyParams
+ useSystemMailbox = $true
+ }
+
+ $null = New-ExoRequest @ExoPolicyRequestParam
+ $Results.Add("Successfully updated SafeLinks policy '$PolicyName'") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Updated SafeLinks policy '$PolicyName'" -Sev 'Info'
+ }
+
+ # PART 2: Update rule if needed
+ if ($hasRuleParams) {
+ $ExoRuleRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Set-SafeLinksRule'
+ cmdParams = $ruleParams
+ useSystemMailbox = $true
+ }
+
+ $null = New-ExoRequest @ExoRuleRequestParam
+ $hasRuleOperation = $true
+ $ruleMessages.Add("updated properties") | Out-Null
+ }
+
+ # Handle enable/disable if needed
+ if ($null -ne $State) {
+ $EnableCmdlet = $State ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule'
+ $EnableRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = $EnableCmdlet
+ cmdParams = @{
+ Identity = $RuleName
+ }
+ useSystemMailbox = $true
+ }
+
+ $null = New-ExoRequest @EnableRequestParam
+ $hasRuleOperation = $true
+ $State = $State ? "enabled" : "disabled"
+ $ruleMessages.Add($State) | Out-Null
+ }
+
+ # Add combined rule message if any rule operations were performed
+ if ($hasRuleOperation) {
+ $ruleOperations = $ruleMessages -join " and "
+ $Results.Add("Successfully $ruleOperations SafeLinks rule '$RuleName'") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "$ruleOperations SafeLinks rule '$RuleName'" -Sev 'Info'
+ }
+
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results.Add("Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed updating SafeLinks configuration '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Results }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1
new file mode 100644
index 000000000000..3e6ece129dc8
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-EditSafeLinksPolicyTemplate.ps1
@@ -0,0 +1,89 @@
+using namespace System.Net
+
+Function Invoke-EditSafeLinksPolicyTemplate {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.SafeLinks.ReadWrite
+ .DESCRIPTION
+ This function updates an existing Safe Links policy template.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev Debug
+
+ try {
+ $ID = $Request.Body.ID
+
+ if (-not $ID) {
+ throw "Template ID is required"
+ }
+
+ # Check if template exists
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$ID'"
+ $ExistingTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter
+
+ if (-not $ExistingTemplate) {
+ throw "Template with ID '$ID' not found"
+ }
+
+ # Create a new ordered hashtable to store selected properties
+ $policyObject = [ordered]@{}
+
+ # Set name and comments
+ $policyObject["TemplateName"] = $Request.body.TemplateName
+ $policyObject["TemplateDescription"] = $Request.body.TemplateDescription
+
+ # Copy specific properties we want to keep
+ $propertiesToKeep = @(
+ # Policy properties
+ "PolicyName", "EnableSafeLinksForEmail", "EnableSafeLinksForTeams", "EnableSafeLinksForOffice",
+ "TrackClicks", "AllowClickThrough", "ScanUrls", "EnableForInternalSenders",
+ "DeliverMessageAfterScan", "DisableUrlRewrite", "DoNotRewriteUrls",
+ "AdminDisplayName", "CustomNotificationText", "EnableOrganizationBranding",
+
+ # Rule properties
+ "RuleName", "Priority", "State", "Comments",
+ "SentTo", "SentToMemberOf", "RecipientDomainIs",
+ "ExceptIfSentTo", "ExceptIfSentToMemberOf", "ExceptIfRecipientDomainIs"
+ )
+
+ # Copy each property if it exists
+ foreach ($prop in $propertiesToKeep) {
+ if ($null -ne $Request.body.$prop) {
+ $policyObject[$prop] = $Request.body.$prop
+ }
+ }
+
+ # Convert to JSON
+ $JSON = $policyObject | ConvertTo-Json -Depth 10
+
+ # Update the template in Azure Table Storage
+ $Table.Force = $true
+ Add-CIPPAzDataTableEntity @Table -Entity @{
+ JSON = "$JSON"
+ RowKey = "$ID"
+ PartitionKey = 'SafeLinksTemplate'
+ }
+
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Updated SafeLinks Policy Template $($policyObject.TemplateName) with ID $ID" -Sev Info
+ $body = [pscustomobject]@{'Results' = "Updated SafeLinks Policy Template $($policyObject.TemplateName) with ID $ID" }
+ $StatusCode = [HttpStatusCode]::OK
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -Headers $Headers -API $APINAME -message "Failed to update SafeLinks policy template: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage
+ $body = [pscustomobject]@{'Results' = "Failed to update SafeLinks policy template: $($ErrorMessage.NormalizedError)" }
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $body
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1
new file mode 100644
index 000000000000..6bd622381129
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecDeleteSafeLinksPolicy.ps1
@@ -0,0 +1,94 @@
+using namespace System.Net
+function Invoke-ExecDeleteSafeLinksPolicy {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.SpamFilter.ReadWrite
+ .DESCRIPTION
+ This function deletes a Safe Links rule and its associated policy.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
+ $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName
+ $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName
+
+ $ResultMessages = [System.Collections.ArrayList]@()
+
+ try {
+ # Only try to delete the rule if a name was provided
+ if ($RuleName) {
+ try {
+ $ExoRequestRuleParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Remove-SafeLinksRule'
+ cmdParams = @{
+ Identity = $RuleName
+ Confirm = $false
+ }
+ useSystemMailbox = $true
+ }
+ $null = New-ExoRequest @ExoRequestRuleParam
+ $ResultMessages.Add("Successfully deleted SafeLinks rule '$RuleName'") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks rule '$RuleName'" -Sev 'Info'
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $ResultMessages.Add("Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning'
+ }
+ }
+ else {
+ $ResultMessages.Add("No rule name provided, skipping rule deletion") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule deletion" -Sev 'Info'
+ }
+
+ # Only try to delete the policy if a name was provided
+ if ($PolicyName) {
+ try {
+ $ExoRequestPolicyParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Remove-SafeLinksPolicy'
+ cmdParams = @{
+ Identity = $PolicyName
+ Confirm = $false
+ }
+ useSystemMailbox = $true
+ }
+ $null = New-ExoRequest @ExoRequestPolicyParam
+ $ResultMessages.Add("Successfully deleted SafeLinks policy '$PolicyName'") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully deleted SafeLinks policy '$PolicyName'" -Sev 'Info'
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $ResultMessages.Add("Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to delete SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning'
+ }
+ }
+ else {
+ $ResultMessages.Add("No policy name provided, skipping policy deletion") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy deletion" -Sev 'Info'
+ }
+
+ # Combine all result messages
+ $Result = $ResultMessages -join " | "
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "An unexpected error occurred: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1
new file mode 100644
index 000000000000..91945d8736ca
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ExecNewSafeLinksPolicy.ps1
@@ -0,0 +1,228 @@
+using namespace System.Net
+
+function Invoke-ExecNewSafeLinksPolicy {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.SpamFilter.ReadWrite
+ .DESCRIPTION
+ This function creates a new Safe Links policy and an associated rule.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
+
+ # Extract policy settings from body
+ $PolicyName = $Request.Body.PolicyName
+ $EnableSafeLinksForEmail = $Request.Body.EnableSafeLinksForEmail
+ $EnableSafeLinksForTeams = $Request.Body.EnableSafeLinksForTeams
+ $EnableSafeLinksForOffice = $Request.Body.EnableSafeLinksForOffice
+ $TrackClicks = $Request.Body.TrackClicks
+ $AllowClickThrough = $Request.Body.AllowClickThrough
+ $ScanUrls = $Request.Body.ScanUrls
+ $EnableForInternalSenders = $Request.Body.EnableForInternalSenders
+ $DeliverMessageAfterScan = $Request.Body.DeliverMessageAfterScan
+ $DisableUrlRewrite = $Request.Body.DisableUrlRewrite
+ $DoNotRewriteUrls = $Request.Body.DoNotRewriteUrls
+ $AdminDisplayName = $Request.Body.AdminDisplayName
+ $CustomNotificationText = $Request.Body.CustomNotificationText
+ $EnableOrganizationBranding = $Request.Body.EnableOrganizationBranding
+
+ # Extract rule settings from body
+ $Priority = $Request.Body.Priority
+ $Comments = $Request.Body.Comments
+ $State = $Request.Body.State
+ $RuleName = $Request.Body.RuleName
+
+ # Extract recipient fields and handle different input formats
+ $SentTo = $Request.Body.SentTo
+ $SentToMemberOf = $Request.Body.SentToMemberOf
+ $RecipientDomainIs = $Request.Body.RecipientDomainIs
+ $ExceptIfSentTo = $Request.Body.ExceptIfSentTo
+ $ExceptIfSentToMemberOf = $Request.Body.ExceptIfSentToMemberOf
+ $ExceptIfRecipientDomainIs = $Request.Body.ExceptIfRecipientDomainIs
+
+ function Test-PolicyExists {
+ param($TenantFilter, $PolicyName)
+ $ExistingPolicies = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true
+ return $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName }
+ }
+
+ function Test-RuleExists {
+ param($TenantFilter, $RuleName)
+ $ExistingRules = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true
+ return $ExistingRules | Where-Object { $_.Name -eq $RuleName }
+ }
+
+ # Helper function to process array fields
+ function Process-ArrayField {
+ param (
+ [Parameter(Mandatory = $false)]
+ $Field
+ )
+
+ if ($null -eq $Field) { return @() }
+
+ # If already an array, process each item
+ if ($Field -is [array]) {
+ $result = [System.Collections.ArrayList]@()
+ foreach ($item in $Field) {
+ if ($item -is [string]) {
+ $result.Add($item) | Out-Null
+ }
+ elseif ($item -is [hashtable] -or $item -is [PSCustomObject]) {
+ # Extract value from object
+ if ($null -ne $item.value) {
+ $result.Add($item.value) | Out-Null
+ }
+ elseif ($null -ne $item.userPrincipalName) {
+ $result.Add($item.userPrincipalName) | Out-Null
+ }
+ elseif ($null -ne $item.id) {
+ $result.Add($item.id) | Out-Null
+ }
+ else {
+ $result.Add($item.ToString()) | Out-Null
+ }
+ }
+ else {
+ $result.Add($item.ToString()) | Out-Null
+ }
+ }
+ return $result.ToArray()
+ }
+
+ # If it's a single object
+ if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) {
+ if ($null -ne $Field.value) { return @($Field.value) }
+ if ($null -ne $Field.userPrincipalName) { return @($Field.userPrincipalName) }
+ if ($null -ne $Field.id) { return @($Field.id) }
+ }
+
+ # If it's a string, return as an array with one item
+ if ($Field -is [string]) {
+ return @($Field)
+ }
+
+ return @($Field)
+ }
+
+ # Process all array fields
+ $SentTo = Process-ArrayField -Field $SentTo
+ $SentToMemberOf = Process-ArrayField -Field $SentToMemberOf
+ $RecipientDomainIs = Process-ArrayField -Field $RecipientDomainIs
+ $ExceptIfSentTo = Process-ArrayField -Field $ExceptIfSentTo
+ $ExceptIfSentToMemberOf = Process-ArrayField -Field $ExceptIfSentToMemberOf
+ $ExceptIfRecipientDomainIs = Process-ArrayField -Field $ExceptIfRecipientDomainIs
+ $DoNotRewriteUrls = Process-ArrayField -Field $DoNotRewriteUrls
+
+ try {
+ # Check if policy already exists
+ if (Test-PolicyExists -TenantFilter $TenantFilter -PolicyName $PolicyName) {
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Policy '$PolicyName' already exists" -Sev 'Warning'
+ return "Policy '$PolicyName' already exists in tenant $TenantFilter"
+ }
+
+ # Check if rule already exists
+ if (Test-RuleExists -TenantFilter $TenantFilter -RuleName $RuleName) {
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Rule '$RuleName' already exists" -Sev 'Warning'
+ return "Rule '$RuleName' already exists in tenant $TenantFilter"
+ }
+
+ # Build command parameters for policy
+ $policyParams = @{
+ Name = $PolicyName
+ }
+
+ # Only add parameters that are explicitly provided
+ if ($null -ne $EnableSafeLinksForEmail) { $policyParams.Add('EnableSafeLinksForEmail', $EnableSafeLinksForEmail) }
+ if ($null -ne $EnableSafeLinksForTeams) { $policyParams.Add('EnableSafeLinksForTeams', $EnableSafeLinksForTeams) }
+ if ($null -ne $EnableSafeLinksForOffice) { $policyParams.Add('EnableSafeLinksForOffice', $EnableSafeLinksForOffice) }
+ if ($null -ne $TrackClicks) { $policyParams.Add('TrackClicks', $TrackClicks) }
+ if ($null -ne $AllowClickThrough) { $policyParams.Add('AllowClickThrough', $AllowClickThrough) }
+ if ($null -ne $ScanUrls) { $policyParams.Add('ScanUrls', $ScanUrls) }
+ if ($null -ne $EnableForInternalSenders) { $policyParams.Add('EnableForInternalSenders', $EnableForInternalSenders) }
+ if ($null -ne $DeliverMessageAfterScan) { $policyParams.Add('DeliverMessageAfterScan', $DeliverMessageAfterScan) }
+ if ($null -ne $DisableUrlRewrite) { $policyParams.Add('DisableUrlRewrite', $DisableUrlRewrite) }
+ if ($null -ne $DoNotRewriteUrls -and $DoNotRewriteUrls.Count -gt 0) { $policyParams.Add('DoNotRewriteUrls', $DoNotRewriteUrls) }
+ if ($null -ne $AdminDisplayName) { $policyParams.Add('AdminDisplayName', $AdminDisplayName) }
+ if ($null -ne $CustomNotificationText) { $policyParams.Add('CustomNotificationText', $CustomNotificationText) }
+ if ($null -ne $EnableOrganizationBranding) { $policyParams.Add('EnableOrganizationBranding', $EnableOrganizationBranding) }
+
+ $ExoPolicyRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'New-SafeLinksPolicy'
+ cmdParams = $policyParams
+ useSystemMailbox = $true
+ }
+
+ $null = New-ExoRequest @ExoPolicyRequestParam
+ $PolicyResult = "Successfully created new SafeLinks policy '$PolicyName'"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $PolicyResult -Sev 'Info'
+
+ # Build command parameters for rule
+ $ruleParams = @{
+ Name = $RuleName
+ SafeLinksPolicy = $PolicyName
+ }
+
+ # Only add parameters that are explicitly provided
+ if ($null -ne $Priority) { $ruleParams.Add('Priority', $Priority) }
+ if ($null -ne $Comments) { $ruleParams.Add('Comments', $Comments) }
+ if ($null -ne $SentTo -and $SentTo.Count -gt 0) { $ruleParams.Add('SentTo', $SentTo) }
+ if ($null -ne $SentToMemberOf -and $SentToMemberOf.Count -gt 0) { $ruleParams.Add('SentToMemberOf', $SentToMemberOf) }
+ if ($null -ne $RecipientDomainIs -and $RecipientDomainIs.Count -gt 0) { $ruleParams.Add('RecipientDomainIs', $RecipientDomainIs) }
+ if ($null -ne $ExceptIfSentTo -and $ExceptIfSentTo.Count -gt 0) { $ruleParams.Add('ExceptIfSentTo', $ExceptIfSentTo) }
+ if ($null -ne $ExceptIfSentToMemberOf -and $ExceptIfSentToMemberOf.Count -gt 0) { $ruleParams.Add('ExceptIfSentToMemberOf', $ExceptIfSentToMemberOf) }
+ if ($null -ne $ExceptIfRecipientDomainIs -and $ExceptIfRecipientDomainIs.Count -gt 0) { $ruleParams.Add('ExceptIfRecipientDomainIs', $ExceptIfRecipientDomainIs) }
+
+ $ExoRuleRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'New-SafeLinksRule'
+ cmdParams = $ruleParams
+ useSystemMailbox = $true
+ }
+
+ $null = New-ExoRequest @ExoRuleRequestParam
+
+ # If State is specified, enable or disable the rule
+ if ($null -ne $State) {
+ $EnableCmdlet = $State ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule'
+ $EnableRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = $EnableCmdlet
+ cmdParams = @{
+ Identity = $RuleName
+ }
+ useSystemMailbox = $true
+ }
+
+ $null = New-ExoRequest @EnableRequestParam
+ }
+
+ $RuleResult = "Successfully created new SafeLinks rule '$RuleName'"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $RuleResult -Sev 'Info'
+
+ $Result = "Successfully created new SafeLinks policy '$PolicyName'and rule '$RuleName'"
+ $StatusCode = [HttpStatusCode]::OK
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed creating new SafeLinks policy '$PolicyName'and rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1
new file mode 100644
index 000000000000..a18d03c2aba1
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicy.ps1
@@ -0,0 +1,201 @@
+using namespace System.Net
+Function Invoke-ListSafeLinksPolicy {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Security.SafeLinksPolicy.Read
+ .DESCRIPTION
+ This function is used to list the Safe Links policies in the tenant, including unmatched rules and policies.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ $Tenantfilter = $request.Query.tenantfilter
+
+ try {
+ $Policies = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksPolicy' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type'
+ $Rules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-SafeLinksRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type'
+ $BuiltInRules = New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-EOPProtectionPolicyRule' | Select-Object -Property * -ExcludeProperty '*@odata.type' , '*@data.type'
+
+ # Track matched items to identify orphans
+ $MatchedRules = [System.Collections.Generic.HashSet[string]]::new()
+ $MatchedPolicies = [System.Collections.Generic.HashSet[string]]::new()
+ $MatchedBuiltInRules = [System.Collections.Generic.HashSet[string]]::new()
+ $Output = [System.Collections.Generic.List[PSCustomObject]]::new()
+
+ # First pass: Process policies with their associated rules
+ foreach ($policy in $Policies) {
+ $policyName = $policy.Name
+ $MatchedPolicies.Add($policyName) | Out-Null
+
+ # Find associated rule (single lookup per policy)
+ $associatedRule = $null
+ foreach ($rule in $Rules) {
+ if ($rule.SafeLinksPolicy -eq $policyName) {
+ $associatedRule = $rule
+ $MatchedRules.Add($rule.Name) | Out-Null
+ break
+ }
+ }
+
+ # Find matching built-in rule (single lookup per policy)
+ $matchingBuiltInRule = $null
+ foreach ($builtInRule in $BuiltInRules) {
+ if ($policyName -like "$($builtInRule.Name)*") {
+ $matchingBuiltInRule = $builtInRule
+ $MatchedBuiltInRules.Add($builtInRule.Name) | Out-Null
+ break
+ }
+ }
+
+ # Create output object for matched policy
+ $OutputItem = [PSCustomObject]@{
+ # Copy all original policy properties
+ Name = $policy.Name
+ AdminDisplayName = $policy.AdminDisplayName
+ EnableSafeLinksForEmail = $policy.EnableSafeLinksForEmail
+ EnableSafeLinksForTeams = $policy.EnableSafeLinksForTeams
+ EnableSafeLinksForOffice = $policy.EnableSafeLinksForOffice
+ TrackClicks = $policy.TrackClicks
+ AllowClickThrough = $policy.AllowClickThrough
+ ScanUrls = $policy.ScanUrls
+ EnableForInternalSenders = $policy.EnableForInternalSenders
+ DeliverMessageAfterScan = $policy.DeliverMessageAfterScan
+ DisableUrlRewrite = $policy.DisableUrlRewrite
+ DoNotRewriteUrls = $policy.DoNotRewriteUrls
+ CustomNotificationText = $policy.CustomNotificationText
+ EnableOrganizationBranding = $policy.EnableOrganizationBranding
+
+ # Calculated properties
+ PolicyName = $policyName
+ RuleName = $associatedRule.Name
+ Priority = if ($matchingBuiltInRule) { $matchingBuiltInRule.Priority } else { $associatedRule.Priority }
+ State = if ($matchingBuiltInRule) { $matchingBuiltInRule.State } else { $associatedRule.State }
+ SentTo = $associatedRule.SentTo
+ SentToMemberOf = $associatedRule.SentToMemberOf
+ RecipientDomainIs = $associatedRule.RecipientDomainIs
+ ExceptIfSentTo = $associatedRule.ExceptIfSentTo
+ ExceptIfSentToMemberOf = $associatedRule.ExceptIfSentToMemberOf
+ ExceptIfRecipientDomainIs = $associatedRule.ExceptIfRecipientDomainIs
+ Description = $policy.AdminDisplayName
+ IsBuiltIn = ($matchingBuiltInRule -ne $null)
+ IsValid = $policy.IsValid
+ ConfigurationStatus = if ($associatedRule) { "Complete" } else { "Policy Only (Missing Rule)" }
+ }
+ $Output.Add($OutputItem)
+ }
+
+ # Second pass: Add unmatched rules (orphaned rules without policies)
+ foreach ($rule in $Rules) {
+ if (-not $MatchedRules.Contains($rule.Name)) {
+ # This rule doesn't have a matching policy
+ $OutputItem = [PSCustomObject]@{
+ # Policy properties (null since no policy exists)
+ Name = $null
+ AdminDisplayName = $null
+ EnableSafeLinksForEmail = $null
+ EnableSafeLinksForTeams = $null
+ EnableSafeLinksForOffice = $null
+ TrackClicks = $null
+ AllowClickThrough = $null
+ ScanUrls = $null
+ EnableForInternalSenders = $null
+ DeliverMessageAfterScan = $null
+ DisableUrlRewrite = $null
+ DoNotRewriteUrls = $null
+ CustomNotificationText = $null
+ EnableOrganizationBranding = $null
+
+ # Rule properties
+ PolicyName = $rule.SafeLinksPolicy
+ RuleName = $rule.Name
+ Priority = $rule.Priority
+ State = $rule.State
+ SentTo = $rule.SentTo
+ SentToMemberOf = $rule.SentToMemberOf
+ RecipientDomainIs = $rule.RecipientDomainIs
+ ExceptIfSentTo = $rule.ExceptIfSentTo
+ ExceptIfSentToMemberOf = $rule.ExceptIfSentToMemberOf
+ ExceptIfRecipientDomainIs = $rule.ExceptIfRecipientDomainIs
+ Description = $rule.Comments
+ IsBuiltIn = $false
+ ConfigurationStatus = "Rule Only (Missing Policy: $($rule.SafeLinksPolicy))"
+ }
+ $Output.Add($OutputItem)
+ }
+ }
+
+ # Third pass: Add unmatched built-in rules
+ foreach ($builtInRule in $BuiltInRules) {
+ if (-not $MatchedBuiltInRules.Contains($builtInRule.Name)) {
+ # Check if this built-in rule might be SafeLinks related
+ if ($builtInRule.Name -like "*SafeLinks*" -or $builtInRule.Name -like "*Safe*Links*") {
+ $OutputItem = [PSCustomObject]@{
+ # Policy properties (null since no policy exists)
+ Name = $null
+ AdminDisplayName = $null
+ EnableSafeLinksForEmail = $null
+ EnableSafeLinksForTeams = $null
+ EnableSafeLinksForOffice = $null
+ TrackClicks = $null
+ AllowClickThrough = $null
+ ScanUrls = $null
+ EnableForInternalSenders = $null
+ DeliverMessageAfterScan = $null
+ DisableUrlRewrite = $null
+ DoNotRewriteUrls = $null
+ CustomNotificationText = $null
+ EnableOrganizationBranding = $null
+
+ # Built-in rule properties
+ PolicyName = $null
+ RuleName = $builtInRule.Name
+ Priority = $builtInRule.Priority
+ State = $builtInRule.State
+ SentTo = $builtInRule.SentTo
+ SentToMemberOf = $builtInRule.SentToMemberOf
+ RecipientDomainIs = $builtInRule.RecipientDomainIs
+ ExceptIfSentTo = $builtInRule.ExceptIfSentTo
+ ExceptIfSentToMemberOf = $builtInRule.ExceptIfSentToMemberOf
+ ExceptIfRecipientDomainIs = $builtInRule.ExceptIfRecipientDomainIs
+ Description = $builtInRule.Comments
+ IsBuiltIn = $true
+ ConfigurationStatus = "Built-In Rule Only (No Associated Policy)"
+ }
+ $Output.Add($OutputItem)
+ }
+ }
+ }
+
+ # Sort output by ConfigurationStatus and Name for better organization
+ $SortedOutput = $Output.ToArray() | Sort-Object ConfigurationStatus, Name, RuleName
+
+ # Generate summary statistics
+ $CompleteConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -eq "Complete" }).Count
+ $PolicyOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Policy Only*" }).Count
+ $RuleOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Rule Only*" }).Count
+ $BuiltInOnlyConfigs = ($SortedOutput | Where-Object { $_.ConfigurationStatus -like "*Built-In Rule Only*" }).Count
+
+ if ($PolicyOnlyConfigs -gt 0 -or $RuleOnlyConfigs -gt 0) {
+ Write-LogMessage -headers $Headers -API $APIName -message "Found $($PolicyOnlyConfigs + $RuleOnlyConfigs) orphaned SafeLinks configurations that may need attention" -Sev 'Warning'
+ }
+
+ $StatusCode = [HttpStatusCode]::OK
+ $FinalOutput = $SortedOutput
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -headers $Headers -API $APIName -message "Error retrieving Safe Links policies: $ErrorMessage" -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::Forbidden
+ $FinalOutput = $ErrorMessage
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $FinalOutput
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1
new file mode 100644
index 000000000000..c0b11a9c3ad9
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyDetails.ps1
@@ -0,0 +1,106 @@
+using namespace System.Net
+function Invoke-ListSafeLinksPolicyDetails {
+ <#
+ .FUNCTIONALITY
+ Entrypoint
+ .ROLE
+ Exchange.SpamFilter.Read
+ .DESCRIPTION
+ This function retrieves details for a specific Safe Links policy and rule.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
+ $PolicyName = $Request.Query.PolicyName ?? $Request.Body.PolicyName
+ $RuleName = $Request.Query.RuleName ?? $Request.Body.RuleName
+
+ $Result = @{}
+ $LogMessages = [System.Collections.ArrayList]@()
+
+ try {
+ # Get policy details if PolicyName is provided
+ if ($PolicyName) {
+ try {
+ $PolicyRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Get-SafeLinksPolicy'
+ cmdParams = @{
+ Identity = $PolicyName
+ }
+ useSystemMailbox = $true
+ }
+ $PolicyDetails = New-ExoRequest @PolicyRequestParam
+ $Result.Policy = $PolicyDetails
+ $Result.PolicyName = $PolicyDetails.Name
+ $LogMessages.Add("Successfully retrieved details for SafeLinks policy '$PolicyName'") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks policy '$PolicyName'" -Sev 'Info'
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $LogMessages.Add("Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks policy '$PolicyName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning'
+ $Result.PolicyError = "Failed to retrieve: $($ErrorMessage.NormalizedError)"
+ }
+ }
+ else {
+ $LogMessages.Add("No policy name provided, skipping policy retrieval") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No policy name provided, skipping policy retrieval" -Sev 'Info'
+ }
+
+ # Get rule details if RuleName is provided
+ if ($RuleName) {
+ try {
+ $RuleRequestParam = @{
+ tenantid = $TenantFilter
+ cmdlet = 'Get-SafeLinksRule'
+ cmdParams = @{
+ Identity = $RuleName
+ }
+ useSystemMailbox = $true
+ }
+ $RuleDetails = New-ExoRequest @RuleRequestParam
+ $Result.Rule = $RuleDetails
+ $Result.RuleName = $RuleDetails.Name
+ $LogMessages.Add("Successfully retrieved details for SafeLinks rule '$RuleName'") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Successfully retrieved details for SafeLinks rule '$RuleName'" -Sev 'Info'
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $LogMessages.Add("Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to retrieve details for SafeLinks rule '$RuleName'. Error: $($ErrorMessage.NormalizedError)" -Sev 'Warning'
+ $Result.RuleError = "Failed to retrieve: $($ErrorMessage.NormalizedError)"
+ }
+ }
+ else {
+ $LogMessages.Add("No rule name provided, skipping rule retrieval") | Out-Null
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "No rule name provided, skipping rule retrieval" -Sev 'Info'
+ }
+
+ # If no valid retrievals were performed, throw an error
+ if (-not ($Result.Policy -or $Result.Rule)) {
+ throw "No valid policy or rule details could be retrieved"
+ }
+
+ # Set success status
+ $StatusCode = [HttpStatusCode]::OK
+ $Result.Message = $LogMessages -join " | "
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Operation failed: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1
new file mode 100644
index 000000000000..26f19bceb065
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplateDetails.ps1
@@ -0,0 +1,57 @@
+using namespace System.Net
+Function Invoke-ListSafeLinksPolicyTemplateDetails {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.SafeLinks.Read
+ .DESCRIPTION
+ This function retrieves details for a specific Safe Links policy template.
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Get the template ID from query parameters
+ $ID = $Request.Query.ID ?? $Request.Body.ID
+
+ $Result = @{}
+
+ try {
+ if (-not $ID) {
+ throw "Template ID is required"
+ }
+
+ # Get the specific template from Azure Table Storage
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$ID'"
+ $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter
+
+ if (-not $Template) {
+ throw "Template with ID '$ID' not found"
+ }
+
+ # Parse the JSON data and add metadata
+ $TemplateData = $Template.JSON | ConvertFrom-Json
+ $TemplateData | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $Template.RowKey -Force
+
+ $Result = $TemplateData
+ $StatusCode = [HttpStatusCode]::OK
+ Write-LogMessage -headers $Headers -API $APIName -message "Successfully retrieved template details for ID '$ID'" -Sev 'Info'
+ }
+ catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to retrieve template details for ID '$ID'. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error'
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ }
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1
new file mode 100644
index 000000000000..5c4477985199
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-ListSafeLinksPolicyTemplates.ps1
@@ -0,0 +1,39 @@
+using namespace System.Net
+Function Invoke-ListSafeLinksPolicyTemplates {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.SafeLinks.Read
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ $Table = Get-CippTable -tablename 'templates'
+ $Templates = Get-ChildItem 'Config\*.SafeLinksTemplate.json' | ForEach-Object {
+ $Entity = @{
+ JSON = "$(Get-Content $_)"
+ RowKey = "$($_.name)"
+ PartitionKey = 'SafeLinksTemplate'
+ GUID = "$($_.name)"
+ }
+ Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force
+ }
+ #List policies
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'SafeLinksTemplate'"
+ $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) | ForEach-Object {
+ $GUID = $_.RowKey
+ $data = $_.JSON | ConvertFrom-Json
+ $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $GUID
+ $data
+ }
+ if ($Request.query.ID) { $Templates = $Templates | Where-Object -Property RowKey -EQ $Request.query.id }
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = [HttpStatusCode]::OK
+ Body = @($Templates)
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1
new file mode 100644
index 000000000000..676b72e4b17e
--- /dev/null
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Safe-Links-Policy/Invoke-RemoveSafeLinksPolicyTemplate.ps1
@@ -0,0 +1,35 @@
+using namespace System.Net
+
+Function Invoke-RemoveSafeLinksPolicyTemplate {
+ <#
+ .FUNCTIONALITY
+ Entrypoint,AnyTenant
+ .ROLE
+ Exchange.SafeLinks.ReadWrite
+ #>
+ [CmdletBinding()]
+ param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $User = $Request.Headers
+ Write-LogMessage -Headers $User -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $ID = $request.query.ID ?? $request.body.ID
+ try {
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$id'"
+ $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey
+ Remove-AzDataTableEntity -Force @Table -Entity $ClearRow
+ $Result = "Removed SafeLinks Policy Template with ID $ID."
+ Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to remove SafeLinks Policy template with ID $ID. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -Headers $User -API $APINAME -message $Result -Sev 'Error' -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::Forbidden
+ }
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = @{ Results = $Result }
+ })
+}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1
index e8f19acf0534..13eb06e766b1 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSite.ps1
@@ -14,21 +14,23 @@ Function Invoke-AddSite {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $SharePointObj = $Request.body
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Body.tenantFilter
+ $SharePointObj = $Request.Body
try {
- $SharePointSite = New-CIPPSharepointSite -SiteName $SharePointObj.siteName -SiteDescription $SharePointObj.siteDescription -SiteOwner $SharePointObj.siteOwner.value -TemplateName $SharePointObj.templateName.value -SiteDesign $SharePointObj.siteDesign.value -SensitivityLabel $SharePointObj.sensitivityLabel -TenantFilter $SharePointObj.tenantFilter
- $body = [pscustomobject]@{'Results' = $SharePointSite }
+ $Result = New-CIPPSharepointSite -Headers $Headers -SiteName $SharePointObj.siteName -SiteDescription $SharePointObj.siteDescription -SiteOwner $SharePointObj.siteOwner.value -TemplateName $SharePointObj.templateName.value -SiteDesign $SharePointObj.siteDesign.value -SensitivityLabel $SharePointObj.sensitivityLabel -TenantFilter $TenantFilter
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($userobj.tenantid) -message "Adding SharePoint Site failed. Error: $($_.Exception.Message)" -Sev 'Error'
- $body = [pscustomobject]@{'Results' = "Failed. Error message: $($_.Exception.Message)" }
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ $Result = "Failed to create SharePoint Site: $($_.Exception.Message)"
}
-
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Body
+ StatusCode = $StatusCode
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1
index 8ca292b2c52f..bbb63aebc399 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddSiteBulk.ps1
@@ -15,22 +15,20 @@ Function Invoke-AddSiteBulk {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $Results = [System.Collections.ArrayList]@()
+ $Results = [System.Collections.Generic.List[System.Object]]::new()
- foreach ($sharepointObj in $Request.Body.bulkSites) {
+ foreach ($sharePointObj in $Request.Body.bulkSites) {
try {
- $SharePointSite = New-CIPPSharepointSite -SiteName $SharePointObj.siteName -SiteDescription $SharePointObj.siteDescription -SiteOwner $SharePointObj.siteOwner -TemplateName $SharePointObj.templateName -SiteDesign $SharePointObj.siteDesign -SensitivityLabel $SharePointObj.sensitivityLabel -TenantFilter $Request.body.TenantFilter
- $Results.add($SharePointSite)
+ $SharePointSite = New-CIPPSharepointSite -Headers $Headers -SiteName $sharePointObj.siteName -SiteDescription $sharePointObj.siteDescription -SiteOwner $sharePointObj.siteOwner -TemplateName $sharePointObj.templateName -SiteDesign $sharePointObj.siteDesign -SensitivityLabel $sharePointObj.sensitivityLabel -TenantFilter $Request.body.tenantFilter
+ $Results.Add($SharePointSite)
} catch {
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($userobj.tenantid) -message "Adding SharePoint Site failed. Error: $($_.Exception.Message)" -Sev 'Error'
- $Results.add("Failed to create $($sharepointObj.siteName) Error message: $($_.Exception.Message)")
+ $Results.Add("Failed to create $($sharePointObj.siteName) Error message: $($_.Exception.Message)")
}
}
- $Body = [pscustomobject]@{'Results' = $Results }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
- Body = $Body
+ Body = @{'Results' = $Results }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1
index b022aee68d46..2e1835cba80e 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment.ps1
@@ -14,19 +14,26 @@ Function Invoke-ExecRemoveTeamsVoicePhoneNumberAssignment {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $tenantFilter = $Request.Body.TenantFilter
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Body.tenantFilter
+ $AssignedTo = $Request.Body.AssignedTo
+ $PhoneNumber = $Request.Body.PhoneNumber
+ $PhoneNumberType = $Request.Body.PhoneNumberType
+
try {
- $null = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Remove-CsPhoneNumberAssignment' -CmdParams @{Identity = $Request.Body.AssignedTo; PhoneNumber = $Request.Body.PhoneNumber; PhoneNumberType = $Request.Body.PhoneNumberType; ErrorAction = 'stop'}
- $Results = [pscustomobject]@{'Results' = "Successfully unassigned $($Request.Body.PhoneNumber) from $($Request.Body.AssignedTo)"}
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($TenantFilter) -message $($Results.Results) -Sev 'Info'
+ $null = New-TeamsRequest -TenantFilter $TenantFilter -Cmdlet 'Remove-CsPhoneNumberAssignment' -CmdParams @{Identity = $AssignedTo; PhoneNumber = $PhoneNumber; PhoneNumberType = $PhoneNumberType; ErrorAction = 'Stop' }
+ $Result = "Successfully unassigned $PhoneNumber from $AssignedTo"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info'
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- $Results = [pscustomobject]@{'Results' = $ErrorMessage}
- Write-LogMessage -headers $Request.Headers -API $APINAME -tenant $($TenantFilter) -message $($Results.Results) -Sev 'Error'
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to unassign $PhoneNumber from $AssignedTo. Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Results
+ StatusCode = $StatusCode
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1
index 0894c81fbdf1..3d8e9947ff77 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSetSharePointMember.ps1
@@ -13,28 +13,34 @@ Function Invoke-ExecSetSharePointMember {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
- Write-LogMessage -Headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
- $TenantFilter = $Request.body.tenantFilter
-
-
-
- if ($Request.body.SharePointType -eq 'Group') {
- $GroupId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=mail eq '$($Request.Body.GroupID)' or proxyAddresses/any(x:endsWith(x,'$($Request.Body.GroupID)'))&`$count=true" -ComplexFilter -tenantid $TenantFilter).id
- if ($Request.body.Add -eq $true) {
- $Results = Add-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $Request.Body.user.value -TenantFilter $TenantFilter -Headers $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Body.tenantFilter
+
+ try {
+ if ($Request.Body.SharePointType -eq 'Group') {
+ $GroupId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=mail eq '$($Request.Body.GroupID)' or proxyAddresses/any(x:endsWith(x,'$($Request.Body.GroupID)'))&`$count=true" -ComplexFilter -tenantid $TenantFilter).id
+ if ($Request.Body.Add -eq $true) {
+ $Results = Add-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $Request.Body.user.value -TenantFilter $TenantFilter -Headers $Headers
+ } else {
+ $UserID = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($Request.Body.user.value)" -tenantid $TenantFilter).id
+ $Results = Remove-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $UserID -TenantFilter $TenantFilter -Headers $Headers
+ }
} else {
- $UserID = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($Request.Body.user.value)" -tenantid $TenantFilter).id
- $Results = Remove-CIPPGroupMember -GroupType 'Team' -GroupID $GroupID -Member $UserID -TenantFilter $TenantFilter -Headers $Request.Headers
+ $StatusCode = [HttpStatusCode]::BadRequest
+ $Results = 'This type of SharePoint site is not supported.'
}
- } else {
- $Results = 'This type of SharePoint site is not supported.'
+ } catch {
+ $Results = $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
- $body = [pscustomobject]@{'Results' = $Results }
+
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $body
+ StatusCode = $StatusCode
+ Body = @{ 'Results' = $Results }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1
index e2c91f14a12e..2952dc7c645b 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ExecSharePointPerms.ps1
@@ -11,11 +11,11 @@ Function Invoke-ExecSharePointPerms {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- $tenantFilter = $Request.Body.tenantFilter
$Headers = $Request.Headers
-
Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev Debug
+ $TenantFilter = $Request.Body.tenantFilter
+
Write-Host '===================================='
Write-Host 'Request Body:'
Write-Host (ConvertTo-Json $Request.body -Depth 10)
@@ -31,7 +31,7 @@ Function Invoke-ExecSharePointPerms {
try {
- $State = Set-CIPPSharePointPerms -tenantFilter $tenantFilter `
+ $State = Set-CIPPSharePointPerms -tenantFilter $TenantFilter `
-UserId $UserId `
-OnedriveAccessUser $OnedriveAccessUser `
-Headers $Headers `
@@ -41,8 +41,8 @@ Function Invoke-ExecSharePointPerms {
$Result = "$State"
$StatusCode = [HttpStatusCode]::OK
} catch {
- $ErrorMessage = Get-CippException -Exception $_
- $Result = "Failed. $($ErrorMessage.NormalizedError)"
+ $ErrorMessage = $_.Exception.Message
+ $Result = "Failed. Error: $ErrorMessage"
$StatusCode = [HttpStatusCode]::BadRequest
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1
index ef0e1e5c690e..d486b5eec8cb 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointAdminUrl.ps1
@@ -19,9 +19,8 @@ function Invoke-ListSharepointAdminUrl {
if ($Tenant.SharepointAdminUrl) {
$AdminUrl = $Tenant.SharepointAdminUrl
} else {
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
- $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
- $Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $AdminUrl
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
+ $Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $SharePointInfo.AdminUrl
$Table = Get-CIPPTable -TableName 'Tenants'
Add-CIPPAzDataTableEntity @Table -Entity $Tenant -Force
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1
index d09419fcc476..9f9db254c6d1 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointQuota.ps1
@@ -15,30 +15,29 @@ Function Invoke-ListSharepointQuota {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
- if ($Request.Query.TenantFilter -eq 'AllTenants') {
+ if ($TenantFilter -eq 'AllTenants') {
$UsedStoragePercentage = 'Not Supported'
} else {
try {
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
-
- $sharepointToken = (Get-GraphToken -scope "https://$($tenantName)-admin.sharepoint.com/.default" -tenantid $TenantFilter)
- $sharepointToken.Add('accept', 'application/json')
- # Implement a try catch later to deal with sharepoint guest user settings
- $sharepointQuota = (Invoke-RestMethod -Method 'GET' -Headers $sharepointToken -Uri "https://$($tenantName)-admin.sharepoint.com/_api/StorageQuotas()?api-version=1.3.2" -ErrorAction Stop).value | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
+ $extraHeaders = @{
+ 'Accept' = 'application/json'
+ }
+ $SharePointQuota = (New-GraphGetRequest -extraHeaders $extraHeaders -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -uri "$($SharePointInfo.AdminUrl)/_api/StorageQuotas()?api-version=1.3.2") | Sort-Object -Property GeoUsedStorageMB -Descending | Select-Object -First 1
- if ($sharepointQuota) {
- $UsedStoragePercentage = [int](($sharepointQuota.GeoUsedStorageMB / $sharepointQuota.TenantStorageMB) * 100)
+ if ($SharePointQuota) {
+ $UsedStoragePercentage = [int](($SharePointQuota.GeoUsedStorageMB / $SharePointQuota.TenantStorageMB) * 100)
}
} catch {
$UsedStoragePercentage = 'Not available'
}
}
- $sharepointQuotaDetails = @{
- GeoUsedStorageMB = $sharepointQuota.GeoUsedStorageMB
- TenantStorageMB = $sharepointQuota.TenantStorageMB
+ $SharePointQuotaDetails = @{
+ GeoUsedStorageMB = $SharePointQuota.GeoUsedStorageMB
+ TenantStorageMB = $SharePointQuota.TenantStorageMB
Percentage = $UsedStoragePercentage
Dashboard = "$($UsedStoragePercentage) / 100"
}
@@ -48,7 +47,7 @@ Function Invoke-ListSharepointQuota {
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = $sharepointQuotaDetails
+ Body = $SharePointQuotaDetails
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1
index cb84b80a8b6e..fdf4db9675f2 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSharepointSettings.ps1
@@ -14,21 +14,17 @@ Function Invoke-ListSharepointSettings {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
+ # XXX - Seems to be an unused endpoint? -Bobby
# Interact with query parameters or the body of the request.
- $tenant = $Request.Query.TenantFilter
- $User = $Request.query.user
- $USERToGet = $Request.query.usertoGet
- $body = '{"isResharingByExternalUsersEnabled": "False"}'
- $Request = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -Type patch -Body $body -ContentType 'application/json'
+ $Tenant = $Request.Query.tenantFilter
+ $Request = New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings'
- Write-LogMessage -API 'Standards' -tenant $tenantFilter -message 'Disabled Password Expiration' -sev Info
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
- Body = @($GraphRequest)
+ Body = @($Request)
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1
index 2d23640c9fcd..84b64bbeb7ae 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1
@@ -10,6 +10,10 @@ Function Invoke-ListSites {
[CmdletBinding()]
param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -Headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
$TenantFilter = $Request.Query.TenantFilter
$Type = $request.query.Type
$UserUPN = $request.query.UserUPN
@@ -93,7 +97,7 @@ Function Invoke-ListSites {
try {
$Requests = (New-GraphBulkRequest -tenantid $TenantFilter -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) -asapp $true).body.value | Where-Object { $_.list.template -eq 'DocumentLibrary' }
} catch {
- Write-LogMessage -Message "Error getting auto map urls: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter -API 'ListSites' -LogData (Get-CippException -Exception $_)
+ Write-LogMessage -Headers $Headers -Message "Error getting auto map urls: $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter -API 'ListSites' -LogData (Get-CippException -Exception $_)
}
$GraphRequest = foreach ($Site in $GraphRequest) {
$ListId = ($Requests | Where-Object { $_.parentReference.siteId -like "*$($Site.siteId)*" }).id
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1
index 38637679c79c..4d6d55619c15 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeams.ps1
@@ -14,13 +14,10 @@ Function Invoke-ListTeams {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
-
-
# Interact with query parameters or the body of the request.
$TenantFilter = $Request.Query.TenantFilter
if ($request.query.type -eq 'List') {
- $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$select=id,displayname,description,visibility,mailNickname" -tenantid $TenantFilter | Sort-Object -Property displayName
+ $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')&`$select=id,displayName,description,visibility,mailNickname" -tenantid $TenantFilter | Sort-Object -Property displayName
}
$TeamID = $request.query.ID
Write-Host $TeamID
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1
index 342f8096d272..9323a71f30a1 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsActivity.ps1
@@ -10,12 +10,14 @@ Function Invoke-ListTeamsActivity {
[CmdletBinding()]
param($Request, $TriggerMetadata)
-
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
- $type = $request.query.Type
+ $TenantFilter = $Request.Query.tenantFilter
+ $type = $request.Query.Type
$GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/get$($type)Detail(period='D30')" -tenantid $TenantFilter | ConvertFrom-Csv | Select-Object @{ Name = 'UPN'; Expression = { $_.'User Principal Name' } },
@{ Name = 'LastActive'; Expression = { $_.'Last Activity Date' } },
@{ Name = 'TeamsChat'; Expression = { $_.'Team Chat Message Count' } },
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1
index 2bfd473f902c..a35801474473 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListTeamsVoice.ps1
@@ -1,6 +1,6 @@
using namespace System.Net
-Function Invoke-ListTeamsVoice {
+function Invoke-ListTeamsVoice {
<#
.FUNCTIONALITY
Entrypoint
@@ -24,7 +24,7 @@ Function Invoke-ListTeamsVoice {
Write-Host "Getting page $Skip"
$data = (New-TeamsAPIGetRequest -uri "https://api.interfaces.records.teams.microsoft.com/Skype.TelephoneNumberMgmt/Tenants/$($TenantId)/telephone-numbers?skip=$($Skip)&locale=en-US&top=999" -tenantid $TenantFilter).TelephoneNumbers | ForEach-Object {
Write-Host 'Reached the loop'
- $CompleteRequest = $_ | Select-Object *, @{Name = 'AssignedTo'; Expression = { $users | Where-Object -Property id -EQ $_.AssignedTo.id } }
+ $CompleteRequest = $_ | Select-Object *, @{Name = 'AssignedTo'; Expression = { $users | Where-Object -Property id -EQ $_.TargetId } }
if ($CompleteRequest.AcquisitionDate) {
$CompleteRequest.AcquisitionDate = $_.AcquisitionDate -split 'T' | Select-Object -First 1
} else {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1
index 8fcf39248cd2..72872dce00a6 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-AddAlert.ps1
@@ -12,30 +12,31 @@ Function Invoke-AddAlert {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $Tenants = $request.body.tenantFilter
- $Conditions = $request.body.conditions | ConvertTo-Json -Compress -Depth 10 | Out-String
+
+ # Interact with query parameters or the body of the request.
+ $Tenants = $Request.Body.tenantFilter
+ $Conditions = $Request.Body.conditions | ConvertTo-Json -Compress -Depth 10 | Out-String
$TenantsJson = $Tenants | ConvertTo-Json -Compress -Depth 10 | Out-String
- $excludedTenantsJson = $request.body.excludedTenants | ConvertTo-Json -Compress -Depth 10 | Out-String
- $Actions = $request.body.actions | ConvertTo-Json -Compress -Depth 10 | Out-String
- $RowKey = $Request.body.RowKey ? $Request.body.RowKey : (New-Guid).ToString()
+ $excludedTenantsJson = $Request.Body.excludedTenants | ConvertTo-Json -Compress -Depth 10 | Out-String
+ $Actions = $Request.Body.actions | ConvertTo-Json -Compress -Depth 10 | Out-String
+ $RowKey = $Request.Body.RowKey ? $Request.Body.RowKey : (New-Guid).ToString()
$CompleteObject = @{
Tenants = [string]$TenantsJson
excludedTenants = [string]$excludedTenantsJson
Conditions = [string]$Conditions
Actions = [string]$Actions
- type = $request.body.logbook.value
+ type = $Request.Body.logbook.value
RowKey = $RowKey
PartitionKey = 'Webhookv2'
}
- $WebhookTable = get-cipptable -TableName 'WebhookRules'
+ $WebhookTable = Get-CippTable -TableName 'WebhookRules'
Add-CIPPAzDataTableEntity @WebhookTable -Entity $CompleteObject -Force
$Results = "Added Audit Log Alert for $($Tenants.count) tenants. It may take up to four hours before Microsoft starts delivering these alerts."
- $body = [pscustomobject]@{'Results' = @($results) }
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = [HttpStatusCode]::OK
- Body = $body
+ Body = @{ 'Results' = @($Results) }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1
index 9df2ffaf6737..dc54f2259f53 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ExecAuditLogSearch.ps1
@@ -8,6 +8,10 @@ function Invoke-ExecAuditLogSearch {
[CmdletBinding()]
param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
$Query = $Request.Body
if (!$Query.TenantFilter) {
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1
index d51774c11c2e..f7ed737d6a55 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1
@@ -8,6 +8,10 @@ function Invoke-ListAuditLogTest {
#>
Param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
$AuditLogQuery = @{
TenantFilter = $Request.Query.TenantFilter
SearchId = $Request.Query.SearchId
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1
index 42c6bf3e8787..12abc8bd4e6a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListWebhookAlert.ps1
@@ -13,8 +13,10 @@ Function Invoke-ListWebhookAlert {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- $Table = get-cipptable -TableName 'SchedulerConfig'
- $WebhookRow = foreach ($Webhook in Get-CIPPAzDataTableEntity @Table | Where-Object -Property PartitionKey -EQ 'WebhookAlert') {
+
+ # Interact with query parameters or the body of the request.
+ $Table = Get-CippTable -TableName 'SchedulerConfig'
+ $WebhookRow = foreach ($Webhook in (Get-CIPPAzDataTableEntity @Table | Where-Object -Property PartitionKey -EQ 'WebhookAlert')) {
$Webhook.If = $Webhook.If | ConvertFrom-Json
$Webhook.execution = $Webhook.execution | ConvertFrom-Json
$Webhook
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1
index b2eaf9309790..d0828455b311 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1
@@ -8,12 +8,16 @@ function Invoke-PublicWebhooks {
#>
param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
Set-Location (Get-Item $PSScriptRoot).Parent.FullName
$WebhookTable = Get-CIPPTable -TableName webhookTable
$WebhookIncoming = Get-CIPPTable -TableName WebhookIncoming
$Webhooks = Get-CIPPAzDataTableEntity @WebhookTable
Write-Host 'Received request'
- $url = ($request.headers.'x-ms-original-url').split('/API') | Select-Object -First 1
+ $url = ($Headers.'x-ms-original-url').split('/API') | Select-Object -First 1
$CIPPURL = [string]$url
Write-Host $url
if ($Webhooks.Resource -eq 'M365AuditLogs') {
@@ -21,22 +25,22 @@ function Invoke-PublicWebhooks {
$body = 'This webhook is not authorized, its an old entry.'
$StatusCode = [HttpStatusCode]::Forbidden
}
- if ($Request.query.ValidationToken) {
+ if ($Request.Query.ValidationToken) {
Write-Host 'Validation token received - query ValidationToken'
- $body = $request.query.ValidationToken
+ $body = $Request.Query.ValidationToken
$StatusCode = [HttpStatusCode]::OK
- } elseif ($Request.body.validationCode) {
+ } elseif ($Request.Body.validationCode) {
Write-Host 'Validation token received - body validationCode'
- $body = $request.body.validationCode
+ $body = $Request.Body.validationCode
$StatusCode = [HttpStatusCode]::OK
- } elseif ($Request.query.validationCode) {
+ } elseif ($Request.Query.validationCode) {
Write-Host 'Validation token received - query validationCode'
- $body = $request.query.validationCode
+ $body = $Request.Query.validationCode
$StatusCode = [HttpStatusCode]::OK
} elseif ($Request.Query.CIPPID -in $Webhooks.RowKey) {
Write-Host 'Found matching CIPPID'
- $url = ($request.headers.'x-ms-original-url').split('/API') | Select-Object -First 1
- $Webhookinfo = $Webhooks | Where-Object -Property RowKey -EQ $Request.query.CIPPID
+ $url = ($Headers.'x-ms-original-url').split('/API') | Select-Object -First 1
+ $Webhookinfo = $Webhooks | Where-Object -Property RowKey -EQ $Request.Query.CIPPID
if ($Request.Query.Type -eq 'GraphSubscription') {
# Graph Subscriptions
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1
index 26c9b127959e..42e0b5d0ebf6 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApproval.ps1
@@ -18,11 +18,11 @@ Function Invoke-ExecAppApproval {
Write-Host "$($Request.query.ID)"
# Interact with query parameters or the body of the request.
- $applicationid = if ($request.query.applicationid) { $request.query.applicationid } else { $env:ApplicationID }
- $Results = get-tenants | ForEach-Object {
+ $ApplicationId = if ($Request.Query.ApplicationId) { $Request.Query.ApplicationId } else { $env:ApplicationID }
+ $Results = Get-Tenants | ForEach-Object {
[PSCustomObject]@{
defaultDomainName = $_.defaultDomainName
- link = "https://login.microsoftonline.com/$($_.customerId)/v2.0/adminconsent?client_id=$applicationid&scope=$applicationid/.default"
+ link = "https://login.microsoftonline.com/$($_.customerId)/v2.0/adminconsent?client_id=$ApplicationId&scope=$ApplicationId/.default"
}
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1
index f604328df1c1..9368600072bb 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppApprovalTemplate.ps1
@@ -13,7 +13,7 @@ function Invoke-ExecAppApprovalTemplate {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
$Table = Get-CIPPTable -TableName 'templates'
- $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json
+ $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json
$Action = $Request.Query.Action ?? $Request.Body.Action
@@ -58,12 +58,12 @@ function Invoke-ExecAppApprovalTemplate {
}
)
- Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template Saved: $($Request.Body.TemplateName)" -Sev 'Info'
+ Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template Saved: $($Request.Body.TemplateName)" -Sev 'Info'
} catch {
$Body = @{
'Results' = $_.Exception.Message
}
- Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template Save failed: $($_.Exception.Message)" -Sev 'Error'
+ Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template Save failed: $($_.Exception.Message)" -Sev 'Error'
}
}
'Delete' {
@@ -83,7 +83,7 @@ function Invoke-ExecAppApprovalTemplate {
$Body = @{
'Results' = "Successfully deleted template '$TemplateName'"
}
- Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template deleted: $TemplateName" -Sev 'Info'
+ Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template deleted: $TemplateName" -Sev 'Info'
} else {
$Body = @{
'Results' = 'No template found with the provided ID'
@@ -93,7 +93,7 @@ function Invoke-ExecAppApprovalTemplate {
$Body = @{
'Results' = "Failed to delete template: $($_.Exception.Message)"
}
- Write-LogMessage -headers $Request.Headers -API $APIName -message "App Deployment Template Delete failed: $($_.Exception.Message)" -Sev 'Error'
+ Write-LogMessage -headers $Headers -API $APIName -message "App Deployment Template Delete failed: $($_.Exception.Message)" -Sev 'Error'
}
}
'Get' {
@@ -102,7 +102,7 @@ function Invoke-ExecAppApprovalTemplate {
if ($Request.Query.TemplateId) {
$templateId = $Request.Query.TemplateId
$filter = "PartitionKey eq 'AppApprovalTemplate' and RowKey eq '$templateId'"
- Write-LogMessage -headers $Request.Headers -API $APIName -message "Retrieved specific template: $templateId" -Sev 'Info'
+ Write-LogMessage -headers $Headers -API $APIName -message "Retrieved specific template: $templateId" -Sev 'Info'
}
$Templates = Get-CIPPAzDataTableEntity @Table -Filter $filter
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1
index 87f8185a6628..4dcabae9c18a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAppPermissionTemplate.ps1
@@ -3,14 +3,18 @@ function Invoke-ExecAppPermissionTemplate {
.FUNCTIONALITY
Entrypoint,AnyTenant
.ROLE
- Tenant.Application.ReadWrite
+ Tenant.ApplicationTemplates.ReadWrite
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
$Table = Get-CIPPTable -TableName 'AppPermissions'
- $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Request.Headers.'x-ms-client-principal')) | ConvertFrom-Json
+ $User = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($Headers.'x-ms-client-principal')) | ConvertFrom-Json
$Action = $Request.Query.Action ?? $Request.Body.Action
@@ -34,7 +38,7 @@ function Invoke-ExecAppPermissionTemplate {
'TemplateId' = $RowKey
}
}
- Write-LogMessage -headers $Request.Headers -API 'ExecAppPermissionTemplate' -message "Permissions Saved for template: $($Request.Body.TemplateName)" -Sev 'Info' -LogData $Permissions
+ Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Permissions Saved for template: $($Request.Body.TemplateName)" -Sev 'Info' -LogData $Permissions
} catch {
Write-Information "Failed to save template: $($_.Exception.Message) - $($_.InvocationInfo.PositionMessage)"
$Body = @{
@@ -53,7 +57,7 @@ function Invoke-ExecAppPermissionTemplate {
$Body = @{
'Results' = "Successfully deleted template '$TemplateName'"
}
- Write-LogMessage -headers $Request.Headers -API 'ExecAppPermissionTemplate' -message "Permission template deleted: $TemplateName" -Sev 'Info'
+ Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Permission template deleted: $TemplateName" -Sev 'Info'
} else {
$Body = @{
'Results' = 'No Template ID provided for deletion'
@@ -71,7 +75,7 @@ function Invoke-ExecAppPermissionTemplate {
if ($Request.Query.TemplateId) {
$templateId = $Request.Query.TemplateId
$filter = "PartitionKey eq 'Templates' and RowKey eq '$templateId'"
- Write-LogMessage -headers $Request.Headers -API 'ExecAppPermissionTemplate' -message "Retrieved specific template: $templateId" -Sev 'Info'
+ Write-LogMessage -headers $Headers -API 'ExecAppPermissionTemplate' -message "Retrieved specific template: $templateId" -Sev 'Info'
}
$Body = Get-CIPPAzDataTableEntity @Table -Filter $filter | ForEach-Object {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1
index 45baf77621b4..743eb49291ad 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecAddSPN.ps1
@@ -15,18 +15,21 @@ Function Invoke-ExecAddSPN {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with query parameters or the body of the request.
- $Body = if ($Request.Query.Enable) { '{"accountEnabled":"true"}' } else { '{"accountEnabled":"false"}' }
try {
- $GraphRequest = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $env:TenantID -type POST -Body "{ `"appId`": `"2832473f-ec63-45fb-976f-5d45a7d4bb91`" }" -NoAuthCheck $true
- $Results = [pscustomobject]@{'Results' = "Successfully completed request. Add your GDAP migration permissions to your SAM application here: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($env:ApplicationID)/isMSAApp/ " }
+ $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/servicePrincipals' -tenantid $env:TenantID -type POST -Body "{ `"appId`": `"2832473f-ec63-45fb-976f-5d45a7d4bb91`" }" -NoAuthCheck $true
+ $Result = "Successfully completed request. Add your GDAP migration permissions to your SAM application here: https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($env:ApplicationID)/isMSAApp/ "
+ $StatusCode = [HttpStatusCode]::OK
} catch {
- $Results = [pscustomobject]@{'Results' = "Failed to add SPN. Please manually execute 'New-AzureADServicePrincipal -AppId 2832473f-ec63-45fb-976f-5d45a7d4bb91' The error was $($_.Exception.Message)" }
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to add SPN. The error was: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $env:TenantID -message $Result -Sev Error -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Results
+ StatusCode = $StatusCode
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1
index 4afdfb601df5..6abc2c461279 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1
@@ -9,23 +9,25 @@ Function Invoke-ExecOffboardTenant {
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)
+
$APIName = $Request.Params.CIPPEndpoint
- try {
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ try {
$TenantQuery = $Request.Body.TenantFilter.value ?? $Request.Body.TenantFilter
$Tenant = Get-Tenants -IncludeAll -TenantFilter $TenantQuery
$TenantId = $Tenant.customerId
$TenantFilter = $Tenant.defaultDomainName
- $results = [System.Collections.ArrayList]@()
- $errors = [System.Collections.ArrayList]@()
+ $Results = [System.Collections.Generic.List[object]]::new()
+ $Errors = [System.Collections.Generic.List[object]]::new()
if (!$Tenant) {
- $results.Add('Tenant has already been offboarded')
+ $Results.Add('Tenant has already been offboarded')
} elseif ($TenantId -eq $env:TenantID) {
- $errors.Add('You cannot offboard the CSP tenant')
+ $Errors.Add('You cannot offboard the CSP tenant')
} else {
if ($request.body.RemoveCSPGuestUsers -eq $true) {
# Delete guest users who's domains match the CSP tenants
@@ -33,9 +35,9 @@ Function Invoke-ExecOffboardTenant {
try {
$domains = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/domains?`$select=id" -tenantid $env:TenantID -NoAuthCheck:$true).id
$DomainFilter = ($Domains | ForEach-Object { "endswith(mail, '$_')" }) -join ' or '
- $CSPGuestUsers = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,mail&`$filter=userType eq 'Guest' and ($DomainFilter)&`$count=true" -tenantid $Tenantfilter -ComplexFilter)
+ $CSPGuestUsers = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/users?`$select=id,mail&`$filter=userType eq 'Guest' and ($DomainFilter)&`$count=true" -tenantid $TenantFilter -ComplexFilter)
} catch {
- $errors.Add("Failed to retrieve guest users: $($_.Exception.message)")
+ $Errors.Add("Failed to retrieve guest users: $($_.Exception.message)")
}
if ($CSPGuestUsers) {
@@ -90,29 +92,28 @@ Function Invoke-ExecOffboardTenant {
$patchContactBody = if (!($newPropertyContent)) { "{ `"$($property)`" : [] }" } else { [pscustomobject]@{ $property = $newPropertyContent } | ConvertTo-Json }
try {
- New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $Tenantfilter -ContentType 'application/json'
- $results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split('@')[1] }))")
+ New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $TenantFilter -ContentType 'application/json'
+ $Results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split('@')[1] }))")
Write-LogMessage -headers $Request.Headers -API $APIName -message "Contacts were removed from $($property)" -Sev 'Info' -tenant $TenantFilter
} catch {
- $errors.Add("Failed to update property $($property): $($_.Exception.message)")
+ $Errors.Add("Failed to update property $($property): $($_.Exception.message)")
}
} else {
$results.Add("No notification contacts found in $($property)")
}
}
- # Add logic for privacyProfile later - rvdwegen
+ # TODO Add logic for privacyProfile later - rvdwegen
}
$VendorApps = $Request.Body.vendorApplications
if ($VendorApps) {
$VendorApps | ForEach-Object {
try {
- $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.value)" -tenantid $Tenantfilter)
- $results.Add("Successfully removed app $($_.label)")
- Write-LogMessage -headers $Request.Headers -API $APIName -message "App $($_.label) was removed" -Sev 'Info' -tenant $TenantFilter
+ $null = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.value)" -tenantid $TenantFilter)
+ $Results.Add("Successfully removed app $($_.label)")
+ Write-LogMessage -headers $Headers -API $APIName -message "App $($_.label) was removed" -Sev 'Info' -tenant $TenantFilter
} catch {
- #$results.Add("Failed to removed app $($_.displayName)")
- $errors.Add("Failed to removed app $($_.label)")
+ $Errors.Add("Failed to removed app $($_.label)")
}
}
}
@@ -121,21 +122,21 @@ Function Invoke-ExecOffboardTenant {
if ($request.body.RemoveMultitenantCSPApps -eq $true) {
# Remove multi-tenant apps with the CSP tenant as origin
try {
- $multitenantCSPApps = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$count=true&`$select=displayName,appId,id,appOwnerOrganizationId&`$filter=appOwnerOrganizationId eq $($env:TenantID)" -tenantid $Tenantfilter -ComplexFilter)
- $sortedArray = $multitenantCSPApps | Sort-Object @{Expression = { if ($_.appId -eq $env:ApplicationID) { 1 } else { 0 } }; Ascending = $true }
+ $MultiTenantCSPApps = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/servicePrincipals?`$count=true&`$select=displayName,appId,id,appOwnerOrganizationId&`$filter=appOwnerOrganizationId eq $($env:TenantID)" -tenantid $TenantFilter -ComplexFilter)
+ $sortedArray = $MultiTenantCSPApps | Sort-Object @{Expression = { if ($_.appId -eq $env:ApplicationID) { 1 } else { 0 } }; Ascending = $true }
$sortedArray | ForEach-Object {
try {
- $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $Tenantfilter)
- $results.Add("Successfully removed app $($_.displayName)")
+ $null = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $TenantFilter)
+ $Results.Add("Successfully removed app $($_.displayName)")
Write-LogMessage -headers $Request.Headers -API $APIName -message "App $($_.displayName) was removed" -Sev 'Info' -tenant $TenantFilter
} catch {
- #$results.Add("Failed to removed app $($_.displayName)")
- $errors.Add("Failed to removed app $($_.displayName)")
+ #$Results.Add("Failed to removed app $($_.displayName)")
+ $Errors.Add("Failed to removed app $($_.displayName)")
}
}
} catch {
- #$results.Add("Failed to retrieve multitenant apps, no apps have been removed: $($_.Exception.message)")
- $errors.Add("Failed to retrieve multitenant CSP apps, no apps have been removed: $($_.Exception.message)")
+ #$Results.Add("Failed to retrieve multi-tenant apps, no apps have been removed: $($_.Exception.message)")
+ $Errors.Add("Failed to retrieve multi-tenant CSP apps, no apps have been removed: $($_.Exception.message)")
}
}
$ClearCache = $false
@@ -146,8 +147,8 @@ Function Invoke-ExecOffboardTenant {
$delegatedAdminRelationships = (New-GraphGETRequest -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships?`$filter=(status eq 'active') AND (customer/tenantId eq '$tenantid')" -tenantid $env:TenantID)
$delegatedAdminRelationships | ForEach-Object {
try {
- $terminate = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID)
- $results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter")
+ $null = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID)
+ $Results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter")
Write-LogMessage -headers $Request.Headers -API $APIName -message "GDAP Relationship $($_.displayName) has been terminated" -Sev 'Info' -tenant $TenantFilter
} catch {
@@ -158,20 +159,20 @@ Function Invoke-ExecOffboardTenant {
}
} catch {
$($_.Exception.message)
- #$results.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)")
- $errors.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)")
+ #$Results.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)")
+ $Errors.Add("Failed to retrieve GDAP relationships, no relationships have been terminated: $($_.Exception.message)")
}
}
if ($request.body.TerminateContract -eq $true) {
# Terminate contract relationship
try {
- $terminate = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID)
- $results.Add('Successfully terminated contract relationship')
- Write-LogMessage -headers $Request.Headers -API $APIName -message 'Contract relationship terminated' -Sev 'Info' -tenant $TenantFilter
+ $null = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID)
+ $Results.Add('Successfully terminated contract relationship')
+ Write-LogMessage -headers $Headers -API $APIName -message 'Contract relationship terminated' -Sev 'Info' -tenant $TenantFilter
} catch {
- #$results.Add("Failed to terminate contract relationship: $($_.Exception.message)")
- $errors.Add("Failed to terminate contract relationship: $($_.Exception.message)")
+ #$Results.Add("Failed to terminate contract relationship: $($_.Exception.message)")
+ $Errors.Add("Failed to terminate contract relationship: $($_.Exception.message)")
}
}
}
@@ -181,11 +182,11 @@ Function Invoke-ExecOffboardTenant {
$Results.Add('Tenant cache has been cleared')
}
- Write-LogMessage -headers $Request.Headers -API $APIName -message 'Offboarding completed' -Sev 'Info' -tenant $TenantFilter
+ Write-LogMessage -headers $Headers -API $APIName -message 'Offboarding completed' -Sev 'Info' -tenant $TenantFilter
$StatusCode = [HttpStatusCode]::OK
$body = [pscustomobject]@{
- 'Results' = @($results)
- 'Errors' = @($errors)
+ 'Results' = @($Results)
+ 'Errors' = @($Errors)
}
} catch {
$StatusCode = [HttpStatusCode]::OK
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1
index ad61daf3ddd2..fdf1b97e79ba 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1
@@ -9,8 +9,11 @@ function Invoke-ExecOnboardTenant {
#>
param($Request, $TriggerMetadata)
- $APIName = 'ExecOnboardTenant'
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
$Id = $Request.Body.id
if ($Id) {
try {
@@ -84,7 +87,7 @@ function Invoke-ExecOnboardTenant {
Batch = @($Item)
}
$InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
- Write-LogMessage -headers $Request.Headers -API $APINAME -message "Onboarding job $Id started" -Sev 'Info' -LogData @{ 'InstanceId' = $InstanceId }
+ Write-LogMessage -headers $Headers -API $APIName -message "Onboarding job $Id started" -Sev 'Info' -LogData @{ 'InstanceId' = $InstanceId }
}
$Steps = $TenantOnboarding.OnboardingSteps | ConvertFrom-Json
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1
index 406a029c49c4..bf38d341040c 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1
@@ -15,22 +15,29 @@ Function Invoke-ExecUpdateSecureScore {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
# Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Body.TenantFilter
+ $ControlName = $Request.Body.ControlName
$Body = @{
- comment = $request.body.reason
- state = $request.body.resolutionType.value
- vendorInformation = $request.body.vendorInformation
+ comment = $Request.Body.reason
+ state = $Request.Body.resolutionType.value
+ vendorInformation = $Request.Body.vendorInformation
}
try {
- $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Request.body.ControlName)" -tenantid $Request.body.TenantFilter -type PATCH -Body $($Body | ConvertTo-Json -Compress)
- $Results = [pscustomobject]@{'Results' = "Successfully set control to $($Body.state) " }
+ $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$ControlName" -tenantid $TenantFilter -type PATCH -Body (ConvertTo-Json -InputObject $Body -Compress)
+ $StatusCode = [HttpStatusCode]::OK
+ $Result = "Successfully set control $ControlName to $($Body.state)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info'
} catch {
- $Results = [pscustomobject]@{'Results' = "Failed to set Control to $($Body.state) $($_.Exception.Message)" }
+ $ErrorMessage = Get-CippException -Exception $_
+ $Result = "Failed to set control $ControlName to $($Body.state). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev Error -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $Results
+ StatusCode = $StatusCode
+ Body = @{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1
index 55f5fdd82b76..89472cd5821a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListAppConsentRequests.ps1
@@ -10,14 +10,15 @@ function Invoke-ListAppConsentRequests {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- $TenantFilter = $Request.Query.TenantFilter
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter
try {
- if ($Request.Query.TenantFilter -eq 'AllTenants') {
+ if ($TenantFilter -eq 'AllTenants') {
throw 'AllTenants is not yet supported'
- } else {
- $TenantFilter = $Request.Query.TenantFilter
}
$appConsentRequests = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/identityGovernance/appConsent/appConsentRequests' -tenantid $TenantFilter # Need the beta endpoint to get consentType
@@ -49,9 +50,10 @@ function Invoke-ListAppConsentRequests {
}
$StatusCode = [HttpStatusCode]::OK
} catch {
- $StatusCode = [HttpStatusCode]::OK
- Write-LogMessage -Headers $Headers -API $APIName -message 'app consent request list failed' -Sev 'Error' -tenant $TenantFilter
- $Results = @{ appDisplayName = "Error: $($_.Exception.Message)" }
+ $ErrorMessage = Get-CippException -Exception $_
+ $StatusCode = [HttpStatusCode]::InternalServerError
+ Write-LogMessage -Headers $Headers -API $APIName -message 'app consent request list failed' -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
+ $Results = @{ appDisplayName = "Error: $($ErrorMessage.NormalizedError)" }
}
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1
index ece424ca883d..cf11e6b09050 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListDomains.ps1
@@ -14,24 +14,20 @@ Function Invoke-ListDomains {
$Headers = $Request.Headers
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
-
-
-
# Interact with query parameters or the body of the request.
- $TenantFilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
try {
- $GraphRequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter | Select-Object id, isdefault, isinitial | Sort-Object isdefault -Descending
+ $Result = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter | Select-Object id, isdefault, isinitial | Sort-Object isdefault -Descending
$StatusCode = [HttpStatusCode]::OK
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- $StatusCode = [HttpStatusCode]::Forbidden
- $GraphRequest = $ErrorMessage
+ $Result = Get-NormalizedError -Message $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = @($GraphRequest)
+ Body = @($Result)
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1
index 843248a2d2ff..ce5bafeedcd3 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ListTenantOnboarding.ps1
@@ -10,7 +10,7 @@ function Invoke-ListTenantOnboarding {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
- Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
try {
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1
index 9e089ff3f77c..5c5d3c2839b3 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-SetAuthMethod.ps1
@@ -21,13 +21,13 @@ function Invoke-SetAuthMethod {
$Result = Set-CIPPAuthenticationPolicy -Tenant $TenantFilter -APIName $APIName -AuthenticationMethodId $AuthenticationMethodId -Enabled $State -Headers $Headers
$StatusCode = [HttpStatusCode]::OK
} catch {
- $Result = $_
- $StatusCode = [HttpStatusCode]::Forbidden
+ $Result = $_.Exception.Message
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
StatusCode = $StatusCode
- Body = [pscustomobject]@{'Results' = "$Result" }
+ Body = [pscustomobject]@{'Results' = $Result }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1
index 65a355bf337f..1234c4523d2d 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-AddTenant.ps1
@@ -9,7 +9,10 @@ function Invoke-AddTenant {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+
+ # Interact with query parameters or the body of the request.
$Action = $Request.Body.Action ?? $Request.Query.Action
$TenantName = $Request.Body.TenantName ?? $Request.Query.TenantName
$StatusCode = [HttpStatusCode]::OK
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1
index b0fa498d20ed..e18c9ba35954 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-EditTenant.ps1
@@ -12,9 +12,9 @@ function Invoke-EditTenant {
$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- Write-LogMessage -headers $Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
-
+ # Interact with query parameters or the body of the request.
$customerId = $Request.Body.customerId
$tenantAlias = $Request.Body.tenantAlias
$tenantGroups = $Request.Body.tenantGroups
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1
index 786a2e4cb673..95cf208f7224 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenantDetails.ps1
@@ -11,37 +11,35 @@ Function Invoke-ListTenantDetails {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
-
- $tenantfilter = $Request.Query.TenantFilter
+ # Interact with query parameters or the body of the request.
+ $TenantFilter = $Request.Query.tenantFilter
try {
- $org = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $tenantfilter | Select-Object displayName, id, city, country, countryLetterCode, street, state, postalCode,
+ $org = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/organization' -tenantid $TenantFilter | Select-Object displayName, id, city, country, countryLetterCode, street, state, postalCode,
@{ Name = 'businessPhones'; Expression = { $_.businessPhones -join ', ' } },
@{ Name = 'technicalNotificationMails'; Expression = { $_.technicalNotificationMails -join ', ' } },
tenantType, createdDateTime, onPremisesLastPasswordSyncDateTime, onPremisesLastSyncDateTime, onPremisesSyncEnabled, assignedPlans
- $customProperties = Get-TenantProperties -customerId $tenantfilter
+ $customProperties = Get-TenantProperties -customerId $TenantFilter
$org | Add-Member -MemberType NoteProperty -Name 'customProperties' -Value $customProperties
- $Groups = (Get-TenantGroups -TenantFilter $tenantfilter) ?? @()
+ $Groups = (Get-TenantGroups -TenantFilter $TenantFilter) ?? @()
$org | Add-Member -MemberType NoteProperty -Name 'Groups' -Value @($Groups)
+ $StatusCode = [HttpStatusCode]::OK
-
- # Respond with the successful output
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $org
- })
} catch {
- # Log the exception message
- Write-LogMessage -headers $Request.Headers -API $APINAME -message "Error: $($_.Exception.Message)" -Sev 'Error'
-
- # Respond with a 500 error and include the exception message in the response body
- Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::InternalServerError
- Body = Get-NormalizedError -message $_.Exception.Message
- })
+ $ErrorMessage = Get-CippException -Exception $_
+ $org = "Failed to retrieve tenant details: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $org -Sev 'Error' -LogData $ErrorMessage
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
+
+ # Associate values to output bindings by calling 'Push-OutputBinding'.
+ Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
+ StatusCode = $StatusCode
+ Body = $org
+ })
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1
index d91f6d5a2392..624e16be31c7 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1
@@ -11,8 +11,10 @@ function Invoke-ListTenants {
param($Request, $TriggerMetadata)
$APIName = $Request.Params.CIPPEndpoint
+ $Headers = $Request.Headers
+ Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
- Write-LogMessage -headers $Request.Headers -API $APINAME -message 'Accessed this API' -Sev 'Debug'
+ # Interact with query parameters or the body of the request.
$TenantAccess = Test-CIPPAccess -Request $Request -TenantList
Write-Host "Tenant Access: $TenantAccess"
@@ -67,7 +69,7 @@ function Invoke-ListTenants {
}
}
try {
- $tenantfilter = $Request.Query.TenantFilter
+ $TenantFilter = $Request.Query.tenantFilter
$Tenants = Get-Tenants -IncludeErrors -SkipDomains
if ($TenantAccess -notcontains 'AllTenants') {
$Tenants = $Tenants | Where-Object -Property customerId -In $TenantAccess
@@ -107,12 +109,12 @@ function Invoke-ListTenants {
}
} else {
- $body = $Tenants | Where-Object -Property defaultDomainName -EQ $Tenantfilter
+ $body = $Tenants | Where-Object -Property defaultDomainName -EQ $TenantFilter
}
- Write-LogMessage -headers $Request.Headers -tenant $Tenantfilter -API $APINAME -message 'Listed Tenant Details' -Sev 'Debug'
+ Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message 'Listed Tenant Details' -Sev 'Debug'
} catch {
- Write-LogMessage -headers $Request.Headers -tenant $Tenantfilter -API $APINAME -message "List Tenant failed. The error is: $($_.Exception.Message)" -Sev 'Error'
+ Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message "List Tenant failed. The error is: $($_.Exception.Message)" -Sev 'Error'
$body = [pscustomobject]@{
'Results' = "Failed to retrieve tenants: $($_.Exception.Message)"
defaultDomainName = ''
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1
index e8d3d5692a54..eb3324e2ab75 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-EditCAPolicy.ps1
@@ -18,16 +18,31 @@ Function Invoke-EditCAPolicy {
$TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter
$ID = $Request.Query.GUID ?? $Request.Body.GUID
$State = $Request.Query.State ?? $Request.Body.State
+ $DisplayName = $Request.Query.newDisplayName ?? $Request.Body.newDisplayName
try {
- $EditBody = "{`"state`": `"$($State)`"}"
- $Request = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta//identity/conditionalAccess/policies/$($ID)" -tenantid $TenantFilter -type PATCH -body $EditBody -asapp $true
- $Result = "Successfully set CA policy $($ID) to $($State)"
+ $properties = @{}
+
+ # Conditionally add properties
+ if ($State) {
+ $properties["state"] = $State
+ }
+
+ if ($DisplayName) {
+ $properties["displayName"] = $DisplayName
+ }
+
+ $Request = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta//identity/conditionalAccess/policies/$($ID)" -tenantid $TenantFilter -type PATCH -body ($properties | ConvertTo-Json) -asapp $true
+
+ $Result = "Successfully updated CA policy $($ID)"
+ if ($State) { $Result += " state to $($State)" }
+ if ($DisplayName) { $Result += " name to '$($DisplayName)'" }
+
Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Info'
$StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
- $Result = "Failed to set CA policy $($ID) to $($State): $($ErrorMessage.NormalizedError)"
+ $Result = "Failed to update CA policy $($ID): $($ErrorMessage.NormalizedError)"
Write-LogMessage -headers $Headers -API $APIName -tenant $($TenantFilter) -message $Result -Sev 'Error' -LogData $ErrorMessage
$StatusCode = [HttpStatusCode]::InternalServerError
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1
index 38653783e2e6..e98898f6a858 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecNamedLocation.ps1
@@ -15,26 +15,26 @@ function Invoke-ExecNamedLocation {
Write-LogMessage -headers $Headers -API $APIName -message 'Accessed this API' -Sev 'Debug'
+ # Interact with query parameters or the body of the request.
$TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter
- $NamedLocationId = $Request.Body.NamedLocationId ?? $Request.Query.NamedLocationId
- $change = $Request.Body.change ?? $Request.Query.change
- $content = $Request.Body.input ?? $Request.Query.input
+ $NamedLocationId = $Request.Body.namedLocationId ?? $Request.Query.namedLocationId
+ $Change = $Request.Body.change ?? $Request.Query.change
+ $Content = $Request.Body.input ?? $Request.Query.input
try {
- $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -change $change -content $content -Headers $Request.Headers
+ $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers
+ $StatusCode = [HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -headers $Request.Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
+ Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
$results = "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)"
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
-
- $body = [pscustomobject]@{'Results' = @($results) }
-
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
- Body = $body
+ StatusCode = $StatusCode
+ Body = @{'Results' = @($results) }
})
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1
index c32e70d81ea9..356f5443ece3 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1
@@ -34,7 +34,7 @@ Function Invoke-ListBPATemplates {
foreach ($Template in $Templates) {
$Template.JSON = $Template.JSON -replace '"parameters":', '"Parameters":'
}
- $Templates = $Templates.JSON | ConvertFrom-Json
+ $Templates = $Templates.JSON | ConvertFrom-Json | Sort-Object Name
} else {
$Templates = $Templates | ForEach-Object {
$TemplateJson = $_.JSON -replace '"parameters":', '"Parameters":'
@@ -45,7 +45,7 @@ Function Invoke-ListBPATemplates {
Name = $Template.Name
Style = $Template.Style
}
- }
+ } | Sort-Object Name
}
# Associate values to output bindings by calling 'Push-OutputBinding'.
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1
index 72455cfce276..21b8b79272c7 100644
--- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListStandardsCompare.ps1
@@ -50,6 +50,18 @@ function Invoke-ListStandardsCompare {
$FieldName = $Standard.RowKey
$FieldValue = $Standard.Value
$Tenant = $Standard.PartitionKey
+
+ # decode field names that are hex encoded (e.g. QuarantineTemplates)
+ if ($FieldName -match '^(standards\.QuarantineTemplate\.)(.+)$') {
+ $Prefix = $Matches[1]
+ $HexEncodedName = $Matches[2]
+ $Chars = [System.Collections.Generic.List[char]]::new()
+ for ($i = 0; $i -lt $HexEncodedName.Length; $i += 2) {
+ $Chars.Add([char][Convert]::ToInt32($HexEncodedName.Substring($i,2),16))
+ }
+ $FieldName = "$Prefix$(-join $Chars)"
+ }
+
if ($FieldValue -is [System.Boolean]) {
$FieldValue = [bool]$FieldValue
} elseif ($FieldValue -like '*{*') {
diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1
index dcf8fb9a523f..ff61308e7023 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListCSPsku.ps1
@@ -24,15 +24,17 @@ Function Invoke-ListCSPsku {
} else {
$GraphRequest = Get-SherwebCatalog -TenantFilter $TenantFilter
}
+ $StatusCode = [HttpStatusCode]::OK
} catch {
$GraphRequest = [PSCustomObject]@{
name = @(@{value = 'Error getting catalog' })
sku = $_.Exception.Message
}
+ $StatusCode = [HttpStatusCode]::InternalServerError
}
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::OK
+ StatusCode = $StatusCode
Body = @($GraphRequest)
}) -Clobber
diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1
index 15b91b83d47d..71e79f07a75a 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1
@@ -46,7 +46,7 @@ function Start-BPAOrchestrator {
PartitionKey = 'BPATemplate'
GUID = "$($_.name)"
}
- Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force
+ Add-CIPPAzDataTableEntity @BPATemplateTable -Entity $Entity -Force
}
$TemplateRows = Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter
}
diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1
deleted file mode 100644
index 65d1ea7c29e1..000000000000
--- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-CIPPGraphSubscriptionCleanupTimer.ps1
+++ /dev/null
@@ -1,19 +0,0 @@
-function Start-CIPPGraphSubscriptionCleanupTimer {
- <#
- .SYNOPSIS
- Remove CIPP Graph Subscriptions for all tenants except the partner tenant.
-
- .DESCRIPTION
- Remove CIPP Graph Subscriptions for all tenants except the partner tenant.
- #>
- [CmdletBinding(SupportsShouldProcess = $true)]
- param()
- try {
- $Tenants = Get-Tenants -IncludeAll | Where-Object { $_.customerId -ne $env:TenantID -and $_.Excluded -eq $false }
- $Tenants | ForEach-Object {
- if ($PSCmdlet.ShouldProcess($_.defaultDomainName, 'Remove-CIPPGraphSubscription')) {
- Remove-CIPPGraphSubscription -cleanup $true -TenantFilter $_.defaultDomainName
- }
- }
- } catch {}
-}
diff --git a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1
index 864e1017074d..177e67cdb378 100644
--- a/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1
+++ b/Modules/CIPPCore/Public/Entrypoints/Timer Functions/Start-TableCleanup.ps1
@@ -6,97 +6,69 @@ function Start-TableCleanup {
[CmdletBinding(SupportsShouldProcess = $true)]
param()
- $CleanupRules = @(
+ $Batch = @(
@{
+ FunctionName = 'TableCleanupTask'
+ Type = 'CleanupRule'
+ TableName = 'webhookTable'
DataTableProps = @{
- Context = (Get-CIPPTable -tablename 'webhookTable').Context
Property = @('PartitionKey', 'RowKey', 'ETag', 'Resource')
+ First = 1000
}
Where = "`$_.Resource -match '^Audit'"
}
@{
+ FunctionName = 'TableCleanupTask'
+ Type = 'CleanupRule'
+ TableName = 'AuditLogSearches'
DataTableProps = @{
- Context = (Get-CIPPTable -tablename 'AuditLogSearches').Context
Filter = "PartitionKey eq 'Search' and Timestamp lt datetime'$((Get-Date).AddHours(-12).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'"
First = 10000
Property = @('PartitionKey', 'RowKey', 'ETag')
}
}
@{
+ FunctionName = 'TableCleanupTask'
+ Type = 'CleanupRule'
+ TableName = 'CippFunctionStats'
DataTableProps = @{
- Context = (Get-CIPPTable -tablename 'CippFunctionStats').Context
Filter = "PartitionKey eq 'Durable' and Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'"
First = 10000
Property = @('PartitionKey', 'RowKey', 'ETag')
}
}
@{
+ FunctionName = 'TableCleanupTask'
+ Type = 'CleanupRule'
+ TableName = 'CippQueue'
DataTableProps = @{
- Context = (Get-CIPPTable -tablename 'CippQueue').Context
Filter = "PartitionKey eq 'CippQueue' and Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'"
First = 10000
Property = @('PartitionKey', 'RowKey', 'ETag')
}
}
@{
+ FunctionName = 'TableCleanupTask'
+ Type = 'CleanupRule'
+ TableName = 'CippQueueTasks'
DataTableProps = @{
- Context = (Get-CIPPTable -tablename 'CippQueueTasks').Context
Filter = "PartitionKey eq 'Task' and Timestamp lt datetime'$((Get-Date).AddDays(-7).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ'))'"
First = 10000
Property = @('PartitionKey', 'RowKey', 'ETag')
}
}
- )
-
- $DeleteTables = @(
- 'knownlocationdb'
- )
-
- if ($PSCmdlet.ShouldProcess('Start-TableCleanup', 'Starting Table Cleanup')) {
- foreach ($Table in $DeleteTables) {
- try {
- $Table = Get-CIPPTable -tablename $Table
- if ($Table) {
- Write-Information "Deleting table $($Table.Context.TableName)"
- try {
- Remove-AzDataTable -Context $Table.Context -Force
- } catch {
- #Write-LogMessage -API 'TableCleanup' -message "Failed to delete table $($Table.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_)
- }
- }
- } catch {
- Write-Information "Table $Table not found"
- }
+ @{
+ FunctionName = 'TableCleanupTask'
+ Type = 'DeleteTable'
+ Tables = @('knownlocationdb')
}
+ )
- Write-Information 'Starting table cleanup'
- foreach ($Rule in $CleanupRules) {
- if ($Rule.Where) {
- $Where = [scriptblock]::Create($Rule.Where)
- } else {
- $Where = { $true }
- }
- $DataTableProps = $Rule.DataTableProps
-
- $CleanupCompleted = $false
- do {
- $Entities = Get-AzDataTableEntity @DataTableProps | Where-Object $Where
- if ($Entities) {
- Write-Information "Removing $($Entities.Count) entities from $($Rule.DataTableProps.Context.TableName)"
- try {
- Remove-AzDataTableEntity -Context $DataTableProps.Context -Entity $Entities -Force
- if ($DataTableProps.First -and $Entities.Count -lt $DataTableProps.First) {
- $CleanupCompleted = $true
- }
- } catch {
- Write-LogMessage -API 'TableCleanup' -message "Failed to remove entities from $($DataTableProps.Context.TableName)" -sev Error -LogData (Get-CippException -Exception $_)
- $CleanupCompleted = $true
- }
- } else {
- $CleanupCompleted = $true
- }
- } while (!$CleanupCompleted)
- }
- Write-Information 'Table cleanup complete'
+ $InputObject = @{
+ Batch = @($Batch)
+ OrchestratorName = 'TableCleanup'
+ SkipLog = $true
}
+
+ Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
}
diff --git a/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1 b/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1
index 2f5e45739c69..c5b0af944f3c 100644
--- a/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1
+++ b/Modules/CIPPCore/Public/Get-CIPPOutOfOffice.ps1
@@ -1,14 +1,14 @@
function Get-CIPPOutOfOffice {
[CmdletBinding()]
param (
- $userid,
+ $UserID,
$TenantFilter,
$APIName = 'Get Out of Office',
$Headers
)
try {
- $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $userid } -Anchor $userid
+ $OutOfOffice = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $UserID } -Anchor $UserID
$Results = @{
AutoReplyState = $OutOfOffice.AutoReplyState
StartTime = $OutOfOffice.StartTime.ToString('yyyy-MM-dd HH:mm')
@@ -18,7 +18,9 @@ function Get-CIPPOutOfOffice {
} | ConvertTo-Json
return $Results
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- return "Could not retrieve out of office message for $($userid). Error: $ErrorMessage"
+ $ErrorMessage = Get-CippException -Exception $_
+ $Results = "Could not retrieve out of office message for $($UserID). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage
+ throw $Results
}
}
diff --git a/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 b/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1
index 0187ef3bda0f..28ecc040f452 100644
--- a/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1
+++ b/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1
@@ -2,23 +2,23 @@ function Get-CIPPPerUserMFA {
[CmdletBinding()]
param(
$TenantFilter,
- $userId,
+ $UserId,
$Headers,
$AllUsers = $false
)
try {
if ($AllUsers -eq $true) {
- $AllUsers = New-graphGetRequest -Uri "https://graph.microsoft.com/v1.0/users?`$top=999&`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $tenantfilter
+ $AllUsers = New-GraphGetRequest -Uri "https://graph.microsoft.com/v1.0/users?`$top=999&`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $TenantFilter
return $AllUsers
} else {
- $MFAState = New-graphGetRequest -Uri "https://graph.microsoft.com/v1.0/users/$($userId)?`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $tenantfilter
+ $MFAState = New-GraphGetRequest -Uri "https://graph.microsoft.com/v1.0/users/$($UserId)?`$select=UserPrincipalName,Id,perUserMfaState" -tenantid $TenantFilter
return [PSCustomObject]@{
PerUserMFAState = $MFAState.perUserMfaState
- UserPrincipalName = $userId
+ UserPrincipalName = $UserId
}
}
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- "Failed to get MFA State for $id : $ErrorMessage"
+ throw "Failed to get MFA State for $UserId : $ErrorMessage"
}
}
diff --git a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1
index fec489bc729d..0435eabdaf13 100644
--- a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1
+++ b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1
@@ -8,11 +8,13 @@ function Get-CIPPSPOTenant {
if (!$SharepointPrefix) {
# get sharepoint admin site
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
+ $tenantName = $SharePointInfo.TenantName
+ $AdminUrl = $SharePointInfo.AdminUrl
} else {
$tenantName = $SharepointPrefix
+ $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
}
- $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
# Query tenant settings
$XML = @'
@@ -21,7 +23,7 @@ function Get-CIPPSPOTenant {
$AdditionalHeaders = @{
'Accept' = 'application/json;odata=verbose'
}
- $Results = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders
+ $Results = New-GraphPostRequest -scope "$($AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders
$Results | Select-Object -Last 1 *, @{n = 'SharepointPrefix'; e = { $tenantName } }, @{n = 'TenantFilter'; e = { $TenantFilter } }
}
diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1
index ac1ad36e954b..c89d8fd2dd75 100644
--- a/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1
+++ b/Modules/CIPPCore/Public/GraphHelper/Get-GraphToken.ps1
@@ -21,7 +21,7 @@ function Get-GraphToken($tenantid, $scope, $AsApp, $AppID, $AppSecret, $refreshT
$TenantsTable = Get-CippTable -tablename 'Tenants'
$Filter = "PartitionKey eq 'Tenants' and delegatedPrivilegeStatus eq 'directTenant'"
$ClientType = Get-CIPPAzDataTableEntity @TenantsTable -Filter $Filter | Where-Object { $_.customerId -eq $tenantid -or $_.defaultDomainName -eq $tenantid }
- if ($clientType.delegatedPrivilegeStatus -eq 'directTenant') {
+ if ($tenantid -ne $env:TenantID -and $clientType.delegatedPrivilegeStatus -eq 'directTenant') {
Write-Host "Using direct tenant refresh token for $($clientType.customerId)"
$ClientRefreshToken = Get-Item -Path "env:\$($clientType.customerId)" -ErrorAction SilentlyContinue
$refreshToken = $ClientRefreshToken.Value
diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1
index 7833e7caa472..75b05c0fc184 100644
--- a/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1
+++ b/Modules/CIPPCore/Public/GraphHelper/Get-NormalizedError.ps1
@@ -3,6 +3,7 @@ function Get-NormalizedError {
.FUNCTIONALITY
Internal
#>
+ [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'CIPP does not use this function to catch errors')]
[CmdletBinding()]
param (
[string]$message
@@ -21,16 +22,16 @@ function Get-NormalizedError {
#We need to check if the message is in one of these fields, and if so, return it.
if ($JSONMsg.error.innererror.message) {
- Write-Host "innererror.message found: $($JSONMsg.error.innererror.message)"
+ Write-Information "innererror.message found: $($JSONMsg.error.innererror.message)"
$message = $JSONMsg.error.innererror.message
} elseif ($JSONMsg.error.message) {
- Write-Host "error.message found: $($JSONMsg.error.message)"
+ Write-Information "error.message found: $($JSONMsg.error.message)"
$message = $JSONMsg.error.message
} elseif ($JSONMsg.error.details.message) {
- Write-Host "error.details.message found: $($JSONMsg.error.details.message)"
+ Write-Information "error.details.message found: $($JSONMsg.error.details.message)"
$message = $JSONMsg.error.details.message
} elseif ($JSONMsg.error.innererror.internalException.message) {
- Write-Host "error.innererror.internalException.message found: $($JSONMsg.error.innererror.internalException.message)"
+ Write-Information "error.innererror.internalException.message found: $($JSONMsg.error.innererror.internalException.message)"
$message = $JSONMsg.error.innererror.internalException.message
}
@@ -42,11 +43,11 @@ function Get-NormalizedError {
'Response status code does not indicate success: 400 (Bad Request).' { 'Error 400 occured. There is an issue with the token configuration for this tenant. Please perform an access check' }
'*Microsoft.Skype.Sync.Pstn.Tnm.Common.Http.HttpResponseException*' { 'Could not connect to Teams Admin center - Tenant might be missing a Teams license' }
'*Provide valid credential.*' { 'Error 400: There is an issue with your Exchange Token configuration. Please perform an access check for this tenant' }
- '*This indicate that a subscription within the tenant has lapsed*' { 'There is subscription for this service available, Check licensing information.' }
+ '*This indicate that a subscription within the tenant has lapsed*' { 'There is no subscription for this service available, Check licensing information.' }
'*User was not found.*' { 'The relationship between this tenant and the partner has been dissolved from the tenant side.' }
'*AADSTS50020*' { 'AADSTS50020: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' }
'*AADSTS50177' { 'AADSTS50177: The user you have used for your Secure Application Model is a guest in this tenant, or your are using GDAP and have not added the user to the correct group. Please delete the guest user to gain access to this tenant' }
- '*invalid or malformed*' { 'The request is malformed. Have you finished the SAM Setup?' }
+ '*invalid or malformed*' { 'The request is malformed. Have you finished the Setup Wizard' }
'*Windows Store repository apps feature is not supported for this tenant*' { 'This tenant does not have WinGet support available' }
'*AADSTS650051*' { 'The application does not exist yet. Try again in 30 seconds.' }
'*AppLifecycle_2210*' { 'Failed to call Intune APIs: Does the tenant have a license available?' }
@@ -58,7 +59,7 @@ function Get-NormalizedError {
'*Authentication failed. MFA required*' { 'Authentication failed. MFA required' }
'*Your tenant is not licensed for this feature.*' { 'Required license not available for this tenant' }
'*AADSTS65001*' { 'We cannot access this tenant as consent has not been given, please try refreshing the CPV permissions in the application settings menu.' }
- '*AADSTS700082*' { 'The CIPP user access token has expired. Run the SAM Setup wizard to refresh your tokens.' }
+ '*AADSTS700082*' { 'The CIPP user access token has expired. Run the Setup Wizard to refresh your tokens.' }
'*Account is not provisioned.' { 'The account is not provisioned. You do not the correct M365 license to access this information..' }
'*AADSTS5000224*' { 'This resource is not available - Has this tenant been deleted?' }
'*AADSTS53003*' { 'Access has been blocked by Conditional Access policies. Please check the Conditional Access configuration documentation' }
@@ -66,7 +67,7 @@ function Get-NormalizedError {
'*AADSTS9002313*' { 'The credentials used to connect to the Graph API are not available, please retry. If this issue persists you may need to execute the SAM wizard.' }
'*One or more platform(s) is/are not configured for the customer. Please configure the platform before trying to purchase a SKU.*' { 'One or more platform(s) is/are not configured for the customer. Please configure the platform before trying to purchase a SKU.' }
"One or more added object references already exist for the following modified properties: 'members'." { 'This user is already a member of the selected group.' }
- Default { $message }
+ default { $message }
}
}
diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1
new file mode 100644
index 000000000000..43c7326489be
--- /dev/null
+++ b/Modules/CIPPCore/Public/GraphHelper/Get-SharePointAdminLink.ps1
@@ -0,0 +1,68 @@
+function Get-SharePointAdminLink {
+ <#
+ .FUNCTIONALITY
+ Internal
+ #>
+ [CmdletBinding()]
+ param ($Public, $TenantFilter)
+
+ if ($Public) {
+ # Do it through domain discovery, unreliable
+ try {
+ # Get tenant information using autodiscover
+ $body = @"
+
+
+
+ http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation
+ https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc
+
+ http://www.w3.org/2005/08/addressing/anonymous
+
+
+
+
+
+ $TenantFilter
+
+
+
+
+"@
+
+ # Create the headers
+ $AutoDiscoverHeaders = @{
+ 'Content-Type' = 'text/xml; charset=utf-8'
+ 'SOAPAction' = '"http://schemas.microsoft.com/exchange/2010/Autodiscover/Autodiscover/GetFederationInformation"'
+ 'User-Agent' = 'AutodiscoverClient'
+ }
+
+ # Invoke autodiscover
+ $Response = Invoke-RestMethod -UseBasicParsing -Method Post -Uri 'https://autodiscover-s.outlook.com/autodiscover/autodiscover.svc' -Body $body -Headers $AutoDiscoverHeaders
+
+ # Get the onmicrosoft.com domain from the response
+ $TenantDomains = $Response.Envelope.body.GetFederationInformationResponseMessage.response.Domains.Domain | Sort-Object
+ $OnMicrosoftDomains = $TenantDomains | Where-Object { $_ -like '*.onmicrosoft.com' }
+
+ if ($OnMicrosoftDomains.Count -eq 0) {
+ throw 'Could not find onmicrosoft.com domain through autodiscover'
+ } elseif ($OnMicrosoftDomains.Count -gt 1) {
+ throw "Multiple onmicrosoft.com domains found through autodiscover. Cannot determine the correct one: $($OnMicrosoftDomains -join ', ')"
+ } else {
+ $OnMicrosoftDomain = $OnMicrosoftDomains[0]
+ $tenantName = $OnMicrosoftDomain.Split('.')[0]
+ }
+ } catch {
+ throw "Failed to get SharePoint admin URL through autodiscover: $($_.Exception.Message)"
+ }
+ } else {
+ $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
+ }
+
+ # Return object with all needed properties
+ return [PSCustomObject]@{
+ AdminUrl = "https://$tenantName-admin.sharepoint.com"
+ TenantName = $tenantName
+ SharePointUrl = "https://$tenantName.sharepoint.com"
+ }
+}
diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1
index acdad77ac57a..c231eb916100 100644
--- a/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1
+++ b/Modules/CIPPCore/Public/GraphHelper/Get-Tenants.ps1
@@ -67,7 +67,7 @@ function Get-Tenants {
relationshipEnd = $Relationship.endDateTime
}
}
- $CurrentTenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and Excluded eq false"
+ $CurrentTenants = Get-CIPPAzDataTableEntity @TenantsTable -Filter "PartitionKey eq 'Tenants' and Excluded eq false and delegatedPrivilegeStatus ne 'directTenant'"
$CurrentTenants | Where-Object { $_.customerId -notin $GDAPList.customerId -and $_.customerId -ne $env:TenantID } | ForEach-Object {
Remove-AzDataTableEntity -Force @TenantsTable -Entity $_
}
diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1
index 9383f7bf4245..12bab410eeee 100644
--- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1
+++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1
@@ -15,7 +15,8 @@ function New-GraphGetRequest {
$Caller,
[switch]$ComplexFilter,
[switch]$CountOnly,
- [switch]$IncludeResponseHeaders
+ [switch]$IncludeResponseHeaders,
+ [hashtable]$extraHeaders
)
if ($NoAuthCheck -eq $false) {
@@ -35,7 +36,11 @@ function New-GraphGetRequest {
$headers['ConsistencyLevel'] = 'eventual'
}
$nextURL = $uri
-
+ if ($extraHeaders) {
+ foreach ($key in $extraHeaders.Keys) {
+ $headers[$key] = $extraHeaders[$key]
+ }
+ }
# Track consecutive Graph API failures
$TenantsTable = Get-CippTable -tablename Tenants
$Filter = "PartitionKey eq 'Tenants' and (defaultDomainName eq '{0}' or customerId eq '{0}')" -f $tenantid
diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1
index 73fa5b11845c..4a86eebf73c0 100644
--- a/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1
+++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphPOSTRequest.ps1
@@ -7,7 +7,7 @@ function New-GraphPOSTRequest ($uri, $tenantid, $body, $type, $scope, $AsApp, $N
if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) {
$headers = Get-GraphToken -tenantid $tenantid -scope $scope -AsApp $asapp -SkipCache $skipTokenCache
if ($AddedHeaders) {
- foreach ($header in $AddedHeaders.getenumerator()) {
+ foreach ($header in $AddedHeaders.GetEnumerator()) {
$headers.Add($header.Key, $header.Value)
}
}
diff --git a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1 b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1
index 75c4fb04cfa4..0e4d23694b98 100644
--- a/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1
+++ b/Modules/CIPPCore/Public/New-CIPPCAPolicy.ps1
@@ -134,6 +134,14 @@ function New-CIPPCAPolicy {
if ($location.countriesAndRegions) { $location.countriesAndRegions = @($location.countriesAndRegions) }
$Body = ConvertTo-Json -InputObject $Location
$GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -body $body -Type POST -tenantid $tenantfilter -asApp $true
+ $retryCount = 0
+ do {
+ Write-Host "Checking for location $($GraphRequest.id) attempt $retryCount. $TenantFilter"
+ $LocationRequest = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter -asApp $true | Where-Object -Property id -EQ $GraphRequest.id
+ Write-Host "LocationRequest: $($LocationRequest.id)"
+ Start-Sleep -Seconds 2
+ $retryCount++
+ } while ((!$LocationRequest -or !$LocationRequest.id) -and ($retryCount -lt 5))
Write-LogMessage -Headers $User -API $APINAME -message "Created new Named Location: $($location.displayName)" -Sev 'Info'
[pscustomobject]@{
id = $GraphRequest.id
@@ -236,8 +244,7 @@ function New-CIPPCAPolicy {
} else {
Write-Information 'Creating'
if ($JSONobj.GrantControls.authenticationStrength.policyType -or $JSONObj.$jsonobj.LocationInfo) {
- #quick fix for if the policy isn't available
- Start-Sleep 1
+ Start-Sleep 3
}
$null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter -type POST -body $RawJSON -asApp $true
Write-LogMessage -Headers $User -API $APINAME -tenant $($Tenant) -message "Added Conditional Access Policy $($JSONObj.Displayname)" -Sev 'Info'
diff --git a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1 b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1
index a7ce98992420..29764fc40e34 100644
--- a/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1
+++ b/Modules/CIPPCore/Public/New-CIPPOneDriveShortCut.ps1
@@ -2,21 +2,21 @@
function New-CIPPOneDriveShortCut {
[CmdletBinding()]
param (
- $username,
- $userid,
+ $Username,
+ $UserId,
$URL,
$TenantFilter,
$APIName = 'Create OneDrive shortcut',
$Headers
)
- Write-Host "Received $username and $userid. We're using $url and $TenantFilter"
+ Write-Host "Received $Username and $UserId. We're using $URL and $TenantFilter"
try {
- $SiteInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/' -tenantid $TenantFilter -asapp $true | Where-Object -Property weburl -EQ $url
+ $SiteInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/' -tenantid $TenantFilter -asapp $true | Where-Object -Property weburl -EQ $URL
$ListItemUniqueId = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/sites/$($siteInfo.id)/drive?`$select=SharepointIds" -tenantid $TenantFilter -asapp $true).SharePointIds
$body = [PSCustomObject]@{
name = 'Documents'
remoteItem = @{
- sharepointIds = @{
+ SharepointIds = @{
listId = $($ListItemUniqueId.listid)
listItemUniqueId = 'root'
siteId = $($ListItemUniqueId.siteId)
@@ -28,11 +28,12 @@ function New-CIPPOneDriveShortCut {
} | ConvertTo-Json -Depth 10
New-GraphPOSTRequest -method POST "https://graph.microsoft.com/beta/users/$username/drive/root/children" -body $body -tenantid $TenantFilter -asapp $true
Write-LogMessage -API $APIName -headers $Headers -message "Created OneDrive shortcut called $($SiteInfo.displayName) for $($username)" -Sev 'info'
- return "Created OneDrive Shortcut for $username called $($SiteInfo.displayName) "
+ return "Successfully created OneDrive Shortcut for $username called $($SiteInfo.displayName) "
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -headers $Headers -API $APIName -message "Could not add Onedrive shortcut to $username : $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
- return "Could not add Onedrive shortcut to $username : $($ErrorMessage.NormalizedError)"
+ $Result = "Could not add Onedrive shortcut to $username : $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage
+ throw $Result
}
}
diff --git a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1
index 9cf2c95c301f..cc5f7a3a1203 100644
--- a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1
+++ b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1
@@ -60,15 +60,15 @@ function New-CIPPSharepointSite {
[string]$Classification,
[Parameter(Mandatory = $true)]
- [string]$TenantFilter
- )
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
- $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
- $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]'
- $SiteUrl = "https://$tenantName.sharepoint.com/sites/$SitePath"
-
+ [string]$TenantFilter,
+ $APIName = 'Create SharePoint Site',
+ $Headers
+ )
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
+ $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]'
+ $SiteUrl = "https://$($SharePointInfo.TenantName).sharepoint.com/sites/$SitePath"
switch ($TemplateName) {
'Communication' {
@@ -139,14 +139,38 @@ function New-CIPPSharepointSite {
'accept' = 'application/json;odata.metadata=none'
'odata-version' = '4.0'
}
- $Results = New-GraphPostRequest -scope "$AdminUrl/.default" -uri "$AdminUrl/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders
+ $Results = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -uri "$($SharePointInfo.AdminUrl)/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders
}
- if ($Results.SiteStatus -eq '4') {
- return 'This site already exists. Please choose a different site name.'
- }
- if ($Results.SiteStatus -eq '2') {
- return "The site $($SiteName) was created successfully."
+ # Check the results. This response is weird. https://learn.microsoft.com/en-us/sharepoint/dev/apis/site-creation-rest
+ switch ($Results.SiteStatus) {
+ '0' {
+ $Result = "Failed to create new SharePoint site $SiteName with URL $SiteUrl. The site doesn't exist."
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error
+ throw $Results
+ }
+ '1' {
+ $Result = "Successfully created new SharePoint site $SiteName with URL $SiteUrl. The site is however currently being provisioned. Please wait for it to finish."
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info
+ return $Results
+ }
+ '2' {
+ $Result = "Successfully created new SharePoint site $SiteName with URL $SiteUrl"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info
+ return $Results
+ }
+ '3' {
+ $Result = "Failed to create new SharePoint site $SiteName with URL $SiteUrl. An error occurred while provisioning the site."
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error
+ throw $Results
+ }
+ '4' {
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Error
+ $Result = "Failed to create new SharePoint site $SiteName with URL $SiteUrl. The site already exists."
+ throw $Result
+ }
+ Default {}
}
+
}
diff --git a/Modules/CIPPCore/Public/New-CIPPTAP.ps1 b/Modules/CIPPCore/Public/New-CIPPTAP.ps1
index 1d934411dff1..120a81ff1264 100644
--- a/Modules/CIPPCore/Public/New-CIPPTAP.ps1
+++ b/Modules/CIPPCore/Public/New-CIPPTAP.ps1
@@ -1,28 +1,75 @@
function New-CIPPTAP {
[CmdletBinding()]
param (
- $userid,
+ $UserID,
$TenantFilter,
$APIName = 'Create TAP',
- $Headers
+ $Headers,
+ $LifetimeInMinutes,
+ [bool]$IsUsableOnce,
+ $StartDateTime
)
try {
- $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($userid)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body '{}' -verbose
- Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $userid" -Sev 'Info' -tenant $TenantFilter
+ # Build the request body based on provided parameters
+ $RequestBody = @{}
+
+ if ($LifetimeInMinutes) {
+ $RequestBody.lifetimeInMinutes = [int]$LifetimeInMinutes
+ }
+
+ if ($null -ne $IsUsableOnce) {
+ $RequestBody.isUsableOnce = $IsUsableOnce
+ }
+
+ if ($StartDateTime) {
+ # Convert Unix timestamp to DateTime if it's a number
+ if ($StartDateTime -match '^\d+$') {
+ $dateTime = [DateTimeOffset]::FromUnixTimeSeconds([int]$StartDateTime).DateTime
+ $RequestBody.startDateTime = Get-Date $dateTime -Format 'o'
+ } else {
+ # If it's already a date string, format it properly
+ $dateTime = Get-Date $StartDateTime
+ $RequestBody.startDateTime = Get-Date $dateTime -Format 'o'
+ }
+ }
+
+ # Convert request body to JSON
+ $BodyJson = if ($RequestBody) { $RequestBody | ConvertTo-Json } else { '{}' }
+ $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($UserID)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body $BodyJson -verbose
+
+ # Build log message parts based on actual response values
+ $logParts = [System.Collections.Generic.List[string]]::new()
+ $logParts.Add("Lifetime: $($GraphRequest.lifetimeInMinutes) minutes")
+
+ $logParts.Add($GraphRequest.isUsableOnce ? 'one-time use' : 'multi-use')
+
+ $logParts.Add($StartDateTime ? "starts at $(Get-Date $GraphRequest.startDateTime -Format 'yyyy-MM-dd HH:mm:ss') UTC" : 'starts immediately')
+
+ # Create parameter string for logging
+ $paramString = ' with ' + ($logParts -join ', ')
+
+ Write-LogMessage -headers $Headers -API $APIName -message "Created Temporary Access Password (TAP) for $UserID$paramString" -Sev 'Info' -tenant $TenantFilter
+
+ # Build result text with parameters
+ $resultText = "The TAP for $UserID is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes"
+ $resultText += $GraphRequest.isUsableOnce ? ' (one-time use only)' : ''
+ $resultText += $StartDateTime ? " starting at $(Get-Date $GraphRequest.startDateTime -Format 'yyyy-MM-dd HH:mm:ss') UTC" : ''
+
return @{
- resultText = "The TAP for $userid is $($GraphRequest.temporaryAccessPass) - This TAP is usable for the next $($GraphRequest.LifetimeInMinutes) minutes"
- userid = $userid
+ resultText = $resultText
+ userId = $UserID
copyField = $GraphRequest.temporaryAccessPass
temporaryAccessPass = $GraphRequest.temporaryAccessPass
lifetimeInMinutes = $GraphRequest.LifetimeInMinutes
startDateTime = $GraphRequest.startDateTime
+ isUsableOnce = $GraphRequest.isUsableOnce
state = 'success'
}
} catch {
$ErrorMessage = Get-CippException -Exception $_
- $Result = "Failed to create Temporary Access Password (TAP) for $($userid): $($ErrorMessage.NormalizedError)"
+ $Result = "Failed to create Temporary Access Password (TAP) for $($UserID): $($ErrorMessage.NormalizedError)"
Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
throw $Result
}
diff --git a/Modules/CIPPCore/Public/OrganizationManagementRoles.json b/Modules/CIPPCore/Public/OrganizationManagementRoles.json
new file mode 100644
index 000000000000..86b72bf503e3
--- /dev/null
+++ b/Modules/CIPPCore/Public/OrganizationManagementRoles.json
@@ -0,0 +1,51 @@
+[
+ "Audit Logs",
+ "Communication Compliance Admin",
+ "Communication Compliance Investigation",
+ "Compliance Admin",
+ "Data Loss Prevention",
+ "Distribution Groups",
+ "E-Mail Address Policies",
+ "Federated Sharing",
+ "Information Protection Admin",
+ "Information Protection Analyst",
+ "Information Protection Investigator",
+ "Information Protection Reader",
+ "Information Rights Management",
+ "Insider Risk Management Admin",
+ "Insider Risk Management Investigation",
+ "Journaling",
+ "Legal Hold",
+ "Mail Enabled Public Folders",
+ "Mail Recipient Creation",
+ "Mail Recipients",
+ "Mail Tips",
+ "Message Tracking",
+ "Migration",
+ "Move Mailboxes",
+ "Org Custom Apps",
+ "Org Marketplace Apps",
+ "Organization Client Access",
+ "Organization Configuration",
+ "Organization Transport Settings",
+ "PlacesBuildingManagement",
+ "PlacesDeskManagement",
+ "Privacy Management Admin",
+ "Privacy Management Investigation",
+ "Public Folders",
+ "Recipient Policies",
+ "Remote and Accepted Domains",
+ "Reset Password",
+ "Retention Management",
+ "Role Management",
+ "Security Admin",
+ "Security Group Creation and Membership",
+ "Security Reader",
+ "TenantPlacesManagement",
+ "Transport Hygiene",
+ "Transport Rules",
+ "User Options",
+ "View-Only Audit Logs",
+ "View-Only Configuration",
+ "View-Only Recipients"
+]
\ No newline at end of file
diff --git a/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1 b/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1
index 862b4b318610..c723ff2d77e7 100644
--- a/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1
+++ b/Modules/CIPPCore/Public/Remove-CIPPGroupMember.ps1
@@ -7,23 +7,23 @@ function Remove-CIPPGroupMember(
[string]$APIName = 'Remove Group Member'
) {
try {
- if ($member -like '*#EXT#*') { $member = [System.Web.HttpUtility]::UrlEncode($member) }
- # $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($member)" -tenantid $TenantFilter).id
- # $addmemberbody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }"
+ if ($Member -like '*#EXT#*') { $Member = [System.Web.HttpUtility]::UrlEncode($Member) }
+ # $MemberIDs = 'https://graph.microsoft.com/v1.0/directoryObjects/' + (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$($Member)" -tenantid $TenantFilter).id
+ # $AddMemberBody = "{ `"members@odata.bind`": $(ConvertTo-Json @($MemberIDs)) }"
if ($GroupType -eq 'Distribution list' -or $GroupType -eq 'Mail-Enabled Security') {
- $Params = @{ Identity = $GroupId; Member = $member; BypassSecurityGroupManagerCheck = $true }
- New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true
+ $Params = @{ Identity = $GroupId; Member = $Member; BypassSecurityGroupManagerCheck = $true }
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-DistributionGroupMember' -cmdParams $Params -UseSystemMailbox $true
} else {
- New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)/members/$($Member)/`$ref" -tenantid $TenantFilter -type DELETE -body '{}' -Verbose
+ $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupId)/members/$($Member)/`$ref" -tenantid $TenantFilter -type DELETE -body '{}' -Verbose
}
- $Message = "Successfully removed user $($Member) from $($GroupId)."
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Message -Sev 'Info'
- return $message
+ $Results = "Successfully removed user $($Member) from $($GroupId)."
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Info
+ return $Results
} catch {
$ErrorMessage = Get-CippException -Exception $_
- $message = "Failed to remove user $($Member) from $($GroupId): $($ErrorMessage.NormalizedError)"
- Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $message -Sev 'error' -LogData $ErrorMessage
- return $message
+ $Results = "Failed to remove user $($Member) from $($GroupId): $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Results -Sev Error -LogData $ErrorMessage
+ throw $Results
}
}
diff --git a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1 b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1
index 46514ac6dd14..46afa2f3d333 100644
--- a/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1
+++ b/Modules/CIPPCore/Public/Request-CIPPSPOPersonalSite.ps1
@@ -36,17 +36,17 @@ function Request-CIPPSPOPersonalSite {
"@
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
- $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
try {
- $Request = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml'
+ $Request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml'
if (!$Request.IsComplete) { throw }
Write-LogMessage -headers $Headers -API $APIName -message "Requested personal site for $($UserEmails -join ', ')" -Sev 'Info' -tenant $TenantFilter
- return "Requested personal site for $($UserEmails -join ', ')"
+ return "Successfully requested personal site for $($UserEmails -join ', ')"
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -headers $Headers -API $APIName -message "Could not request personal site for $($UserEmails -join ', '). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
- return "Could not request personal site for $($UserEmails -join ', '). Error: $($ErrorMessage.NormalizedError)"
+ $Result = "Failed to request personal site for $($UserEmails -join ', '). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
+ throw $Result
}
}
diff --git a/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1 b/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1
index a7f37e6e7854..e9d319008a2d 100644
--- a/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1
+++ b/Modules/CIPPCore/Public/Revoke-CIPPSessions.ps1
@@ -15,7 +15,9 @@ function Revoke-CIPPSessions {
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -headers $Headers -API $APIName -message "Failed to revoke sessions for $($username): $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
- return "Revoke Session Failed: $($ErrorMessage.NormalizedError)"
+ $Result = "Failed to revoke sessions for $($username). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
+ # TODO - needs to be changed to throw, but the rest of the functions using this cant handle anything but a return.
+ return $Result
}
}
diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json
index ff59fb4f7e32..12702b4c6beb 100644
--- a/Modules/CIPPCore/Public/SAMManifest.json
+++ b/Modules/CIPPCore/Public/SAMManifest.json
@@ -143,6 +143,10 @@
"id": "292d869f-3427-49a8-9dab-8c70152b74e9",
"type": "Role"
},
+ {
+ "id": "2cb92fee-97a3-4034-8702-24a6f5d0d1e9",
+ "type": "Role"
+ },
{
"id": "b6890674-9dd5-4e42-bb15-5af07f541ae1",
"type": "Role"
@@ -191,6 +195,10 @@
"id": "2a60023f-3219-47ad-baa4-40e17cd02a1d",
"type": "Role"
},
+ {
+ "id": "025d3225-3f02-4882-b4c0-cd5b541a4e80",
+ "type": "Role"
+ },
{
"id": "04c55753-2244-4c25-87fc-704ab82a4f69",
"type": "Role"
@@ -399,6 +407,10 @@
"id": "46ca0847-7e6b-426e-9775-ea810a948356",
"type": "Scope"
},
+ {
+ "id": "346c19ff-3fb2-4e81-87a0-bac9e33990c1",
+ "type": "Scope"
+ },
{
"id": "e67e6727-c080-415e-b521-e3f35d5248e9",
"type": "Scope"
@@ -631,4 +643,4 @@
]
}
]
-}
+}
\ No newline at end of file
diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1
index 478f1aa6e2b9..272c0d21aff8 100644
--- a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1
@@ -9,7 +9,7 @@ function Set-CIPPAssignedApplication {
$APIName = 'Assign Application',
$Headers
)
-
+ Write-Host "GroupName: $GroupName Intent: $Intent AppType: $AppType ApplicationId: $ApplicationId TenantFilter: $TenantFilter APIName: $APIName"
try {
$MobileAppAssignment = switch ($GroupName) {
'AllUsers' {
diff --git a/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1 b/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1
index 9564d818f6d5..705b6231045b 100644
--- a/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPCopyGroupMembers.ps1
@@ -31,7 +31,7 @@ function Set-CIPPCopyGroupMembers {
$CurrentMemberships = ($Results | Where-Object { $_.id -eq 'UserMembership' }).body.value
$CopyFromMemberships = ($Results | Where-Object { $_.id -eq 'CopyFromMembership' }).body.value
- Write-Information ($Results | ConvertTo-Json -Depth 10)
+ # Write-Information ($Results | ConvertTo-Json -Depth 10) # For debugging
$ODataBind = 'https://graph.microsoft.com/v1.0/directoryObjects/{0}' -f $User.id
$AddMemberBody = @{
@@ -40,12 +40,16 @@ function Set-CIPPCopyGroupMembers {
$Success = [System.Collections.Generic.List[object]]::new()
$Errors = [System.Collections.Generic.List[object]]::new()
- $Memberships = $CopyFromMemberships | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.group' -and $_.groupTypes -notcontains 'DynamicMembership' -and $_.onPremisesSyncEnabled -ne $true -and $_.visibility -ne 'Public' -and $CurrentMemberships.id -notcontains $_.id }
+ $Memberships = $CopyFromMemberships | Where-Object { $_.'@odata.type' -eq '#microsoft.graph.group' -and
+ $_.groupTypes -notcontains 'DynamicMembership' -and
+ $_.onPremisesSyncEnabled -ne $true -and
+ $_.visibility -ne 'Public' -and
+ $CurrentMemberships.id -notcontains $_.id }
$ScheduleExchangeGroupTask = $false
foreach ($MailGroup in $Memberships) {
try {
if ($PSCmdlet.ShouldProcess($MailGroup.displayName, "Add $UserId to group")) {
- if ($MailGroup.MailEnabled -and $Mailgroup.ResourceProvisioningOptions -notcontains 'Team' -and $MailGroup.groupTypes -notcontains 'Unified') {
+ if ($MailGroup.MailEnabled -and $MailGroup.ResourceProvisioningOptions -notcontains 'Team' -and $MailGroup.groupTypes -notcontains 'Unified') {
$Params = @{ Identity = $MailGroup.id; Member = $UserId; BypassSecurityGroupManagerCheck = $true }
try {
$null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Add-DistributionGroupMember' -cmdParams $params -UseSystemMailbox $true
diff --git a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1 b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1
index 3f23365c31be..888622741f86 100644
--- a/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPNamedLocation.ps1
@@ -3,45 +3,75 @@ function Set-CIPPNamedLocation {
param(
$NamedLocationId,
$TenantFilter,
- #$change should be one of 'addip','addlocation','removeip','removelocation'
- [ValidateSet('addip', 'addlocation', 'removeip', 'removelocation')]
- $change,
- $content,
+ #$Change should be one of 'addIp','addLocation','removeIp','removeLocation','rename','setTrusted','setUntrusted','delete'
+ [ValidateSet('addIp', 'addLocation', 'removeIp', 'removeLocation', 'rename', 'setTrusted', 'setUntrusted', 'delete')]
+ $Change,
+ $Content,
$APIName = 'Set Named Location',
$Headers
)
try {
- $NamedLocations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -Tenantid $tenantfilter
- switch ($change) {
- 'addip' {
- $NamedLocations.ipRanges = @($NamedLocations.ipRanges + @{ cidrAddress = $content; '@odata.type' = '#microsoft.graph.iPv4CidrRange' })
+ $NamedLocations = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -Tenantid $TenantFilter
+
+ switch ($Change) {
+ 'addIp' {
+ $NamedLocations.ipRanges = @($NamedLocations.ipRanges + @{ cidrAddress = $Content; '@odata.type' = '#microsoft.graph.iPv4CidrRange' })
+ $ActionDescription = "Adding IP $Content to named location"
+ }
+ 'addLocation' {
+ $NamedLocations.countriesAndRegions = $NamedLocations.countriesAndRegions + $Content
+ $ActionDescription = "Adding location $Content to named location"
+ }
+ 'removeIp' {
+ $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $Content)
+ $ActionDescription = "Removing IP $Content from named location"
+ }
+ 'removeLocation' {
+ $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $Content })
+ $ActionDescription = "Removing location $Content from named location"
}
- 'addlocation' {
- $NamedLocations.countriesAndRegions = $NamedLocations.countriesAndRegions + $content
+ 'rename' {
+ $NamedLocations.displayName = $Content
+ $ActionDescription = "Renaming named location to: $Content"
}
- 'removeip' {
- $NamedLocations.ipRanges = @($NamedLocations.ipRanges | Where-Object -Property cidrAddress -NE $content)
+ 'setTrusted' {
+ $NamedLocations.isTrusted = $true
+ $ActionDescription = 'Setting named location as trusted'
}
- 'removelocation' {
- $NamedLocations.countriesAndRegions = @($NamedLocations.countriesAndRegions | Where-Object { $_ -NE $content })
+ 'setUntrusted' {
+ $NamedLocations.isTrusted = $false
+ $ActionDescription = 'Setting named location as untrusted'
+ }
+ 'delete' {
+ $ActionDescription = 'Deleting named location'
}
}
- if ($PSCmdlet.ShouldProcess($GroupName, "Assigning Application $ApplicationId")) {
- #Remove unneeded propertie
- if ($change -like '*location*') {
- $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions'
+
+ if ($PSCmdlet.ShouldProcess($NamedLocations.displayName, $ActionDescription)) {
+ if ($Change -eq 'delete') {
+ $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type DELETE
+ $Result = "Deleted named location: $($NamedLocations.displayName)"
} else {
- $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted'
+ # PATCH operations - remove unneeded properties
+ if ($NamedLocations.'@odata.type' -eq '#microsoft.graph.countryNamedLocation') {
+ $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'countriesAndRegions', 'includeUnknownCountriesAndRegions'
+ } elseif ($NamedLocations.'@odata.type' -eq '#microsoft.graph.ipNamedLocation') {
+ $NamedLocations = $NamedLocations | Select-Object '@odata.type', 'displayName', 'ipRanges', 'isTrusted'
+ }
+
+ $JsonBody = ConvertTo-Json -InputObject $NamedLocations -Compress -Depth 10
+ $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type PATCH -body $JsonBody
+ $Result = "Edited named location: $($NamedLocations.displayName). Change: $Change$(if ($Content) { " with content $Content" })"
}
- $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations/$NamedLocationId" -tenantid $TenantFilter -type PATCH -body $($NamedLocations | ConvertTo-Json -Compress -Depth 10)
- Write-LogMessage -headers $Headers -API $APIName -message "Edited named location. Change: $change with content $($content)" -Sev 'Info' -tenant $TenantFilter
+ Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Info'
}
- return "Edited named location. Change: $change with content $($content)"
+ return $Result
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -headers $Headers -API $APIName -message "Failed to edit named location: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
- return "Failed to edit named location. Error: $($ErrorMessage.NormalizedError)"
+ $Result = "Failed to edit named location: $($NamedLocations.displayName). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message $Result -Sev 'Error' -LogData $ErrorMessage
+ throw $Result
}
}
diff --git a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1 b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1
index 6ee8d1ea49b1..ced3fad0a5af 100644
--- a/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPOutOfoffice.ps1
@@ -1,11 +1,14 @@
function Set-CIPPOutOfOffice {
[CmdletBinding()]
param (
- $userid,
+ [Parameter(Mandatory = $true)]
+ $UserID,
$InternalMessage,
$ExternalMessage,
$TenantFilter,
- $State,
+ [ValidateSet('Enabled', 'Disabled', 'Scheduled')]
+ [Parameter(Mandatory = $true)]
+ [string]$State,
$APIName = 'Set Out of Office',
$Headers,
$StartTime,
@@ -13,24 +16,40 @@ function Set-CIPPOutOfOffice {
)
try {
- if (-not $StartTime) {
- $StartTime = (Get-Date).ToString()
+
+ $CmdParams = @{
+ Identity = $UserID
+ AutoReplyState = $State
+ }
+
+ if ($PSBoundParameters.ContainsKey('InternalMessage')) {
+ $CmdParams.InternalMessage = $InternalMessage
}
- if (-not $EndTime) {
- $EndTime = (Get-Date $StartTime).AddDays(7)
+
+ if ($PSBoundParameters.ContainsKey('ExternalMessage')) {
+ $CmdParams.ExternalMessage = $ExternalMessage
}
- if ($State -ne 'Scheduled') {
- $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $userid; AutoReplyState = $State; InternalMessage = $InternalMessage; ExternalMessage = $ExternalMessage } -Anchor $userid
- Write-LogMessage -headers $Headers -API $APIName -message "Set Out-of-office for $($userid) to $State" -Sev 'Info' -tenant $TenantFilter
- return "Set Out-of-office for $($userid) to $State."
- } else {
- $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams @{Identity = $userid; AutoReplyState = $State; InternalMessage = $InternalMessage; ExternalMessage = $ExternalMessage; StartTime = $StartTime; EndTime = $EndTime } -Anchor $userid
- Write-LogMessage -headers $Headers -API $APIName -message "Scheduled Out-of-office for $($userid) between $StartTime and $EndTime" -Sev 'Info' -tenant $TenantFilter
- return "Scheduled Out-of-office for $($userid) between $($StartTime.toString()) and $($EndTime.toString())"
+
+ if ($State -eq 'Scheduled') {
+ # If starttime or endtime are not provided, default to enabling OOO for 7 days
+ $StartTime = $StartTime ? $StartTime : (Get-Date).ToString()
+ $EndTime = $EndTime ? $EndTime : (Get-Date $StartTime).AddDays(7)
+ $CmdParams.StartTime = $StartTime
+ $CmdParams.EndTime = $EndTime
}
+
+ $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Set-MailboxAutoReplyConfiguration' -cmdParams $CmdParams -Anchor $UserID
+
+ $Results = $State -eq 'Scheduled' ?
+ "Scheduled Out-of-office for $($UserID) between $($StartTime.toString()) and $($EndTime.toString())" :
+ "Set Out-of-office for $($UserID) to $State."
+
+ Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Info' -tenant $TenantFilter
+ return $Results
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -headers $Headers -API $APIName -message "Could not add OOO for $($userid). Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
- return "Could not add out of office message for $($userid). Error: $($ErrorMessage.NormalizedError)"
+ $Results = "Could not add OOO for $($UserID). Error: $($ErrorMessage.NormalizedError)"
+ Write-LogMessage -headers $Headers -API $APIName -message $Results -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage
+ throw $Results
}
}
diff --git a/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1 b/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1
index ff87694f31f0..69b780169c69 100644
--- a/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPResetPassword.ps1
@@ -29,14 +29,16 @@ function Set-CIPPResetPassword {
Write-LogMessage -headers $Headers -API $APIName -message "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn" -Sev 'Info' -tenant $TenantFilter
if ($UserDetails.onPremisesSyncEnabled -eq $true) {
- return [pscustomobject]@{ resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password. WARNING: This user is AD synced. Please confirm passthrough or writeback is enabled."
- copyField = $password
- state = 'warning'
+ return [pscustomobject]@{
+ resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password. WARNING: This user is AD synced. Please confirm passthrough or writeback is enabled."
+ copyField = $password
+ state = 'warning'
}
} else {
- return [pscustomobject]@{ resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password"
- copyField = $password
- state = 'success'
+ return [pscustomobject]@{
+ resultText = "Reset the password for $DisplayName, $($UserID). User must change password is set to $forceChangePasswordNextSignIn. The new password is $password"
+ copyField = $password
+ state = 'success'
}
}
} catch {
diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1
index 620936d895fb..c5c6db30d5da 100644
--- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1
@@ -44,12 +44,13 @@ function Set-CIPPSPOTenant {
process {
if (!$SharepointPrefix) {
# get sharepoint admin site
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
+ $AdminUrl = $SharePointInfo.AdminUrl
} else {
$tenantName = $SharepointPrefix
+ $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
}
$Identity = $Identity -replace "`n", '
'
- $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
$AllowedTypes = @('Boolean', 'String', 'Int32')
$SetProperty = [System.Collections.Generic.List[string]]::new()
$x = 114
diff --git a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1 b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1
index 66a87747b371..17bf2358d2de 100644
--- a/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1
+++ b/Modules/CIPPCore/Public/Set-CIPPSharePointPerms.ps1
@@ -20,8 +20,8 @@ function Set-CIPPSharePointPerms {
Write-Information 'No URL provided, getting URL from Graph'
$URL = (New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/users/$($UserId)/Drives" -asapp $true -tenantid $TenantFilter).WebUrl
}
- $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0]
- $AdminUrl = "https://$($tenantName)-admin.sharepoint.com"
+
+ $SharePointInfo = Get-SharePointAdminLink -Public $false -tenantFilter $TenantFilter
$XML = @"
@@ -39,7 +39,7 @@ function Set-CIPPSharePointPerms {
"@
- $request = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml'
+ $request = New-GraphPostRequest -scope "$($SharePointInfo.AdminUrl)/.default" -tenantid $TenantFilter -Uri "$($SharePointInfo.AdminUrl)/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml'
# Write-Host $($request)
if (!$request.ErrorInfo.ErrorMessage) {
$Message = "$($OnedriveAccessUser) has been $($RemovePermission ? 'removed from' : 'given') access to $URL"
diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1
index 31643c813caf..7eaf01a80d1e 100644
--- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1
+++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1
@@ -60,6 +60,14 @@ function Get-CIPPStandards {
$CurrentStandard = $Item.PSObject.Copy()
$CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force
+ if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) {
+ $reportAction = [pscustomobject]@{
+ label = 'Report'
+ value = 'Report'
+ }
+ $CurrentStandard.action = @($CurrentStandard.action) + $reportAction
+ }
+
$Actions = $CurrentStandard.action.value
if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') {
if (-not $ComputedStandards.Contains($StandardName)) {
@@ -75,6 +83,14 @@ function Get-CIPPStandards {
$CurrentStandard = $Value.PSObject.Copy()
$CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force
+ if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) {
+ $reportAction = [pscustomobject]@{
+ label = 'Report'
+ value = 'Report'
+ }
+ $CurrentStandard.action = @($CurrentStandard.action) + $reportAction
+ }
+
$Actions = $CurrentStandard.action.value
if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') {
if (-not $ComputedStandards.Contains($StandardName)) {
@@ -190,6 +206,14 @@ function Get-CIPPStandards {
$CurrentStandard = $Item.PSObject.Copy()
$CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force
+ if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) {
+ $reportAction = [pscustomobject]@{
+ label = 'Report'
+ value = 'Report'
+ }
+ $CurrentStandard.action = @($CurrentStandard.action) + $reportAction
+ }
+
$Actions = $CurrentStandard.action.value
if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') {
if (-not $ComputedStandards.Contains($StandardName)) {
@@ -204,6 +228,14 @@ function Get-CIPPStandards {
$CurrentStandard = $Value.PSObject.Copy()
$CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force
+ if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) {
+ $reportAction = [pscustomobject]@{
+ label = 'Report'
+ value = 'Report'
+ }
+ $CurrentStandard.action = @($CurrentStandard.action) + $reportAction
+ }
+
$Actions = $CurrentStandard.action.value
if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') {
if (-not $ComputedStandards.Contains($StandardName)) {
@@ -230,6 +262,14 @@ function Get-CIPPStandards {
$CurrentStandard = $Item.PSObject.Copy()
$CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force
+ if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) {
+ $reportAction = [pscustomobject]@{
+ label = 'Report'
+ value = 'Report'
+ }
+ $CurrentStandard.action = @($CurrentStandard.action) + $reportAction
+ }
+
# Filter actions only 'Remediate','warn','Report'
$Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'Report' }
if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') {
@@ -245,6 +285,14 @@ function Get-CIPPStandards {
$CurrentStandard = $Value.PSObject.Copy()
$CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force
+ if ($CurrentStandard.action.value -contains 'Remediate' -and -not ($CurrentStandard.action.value -contains 'Report')) {
+ $reportAction = [pscustomobject]@{
+ label = 'Report'
+ value = 'Report'
+ }
+ $CurrentStandard.action = @($CurrentStandard.action) + $reportAction
+ }
+
$Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'Report' }
if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') {
if (-not $ComputedStandards.Contains($StandardName)) {
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1
index 6a885ec4b92d..8e119023f392 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1
@@ -76,6 +76,8 @@ function Invoke-CIPPStandardAddDKIM {
'*.signature365.net'
'*.myteamsconnect.io'
'*.teams.dstny.com'
+ '*.msteams.8x8.com'
+ '*.ucconnect.co.uk'
)
$AllDomains = ($BatchResults | Where-Object { $_.DomainName }).DomainName | ForEach-Object {
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1
new file mode 100644
index 000000000000..85e5ba190ec8
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDMARCToMOERA.ps1
@@ -0,0 +1,142 @@
+function Invoke-CIPPStandardAddDMARCToMOERA {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) AddDMARCToMOERA
+ .SYNOPSIS
+ (Label) Enables DMARC on MOERA (onmicrosoft.com) domains
+ .DESCRIPTION
+ (Helptext) Note: requires 'Domain Name Administrator' GDAP role. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default value is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%
+ (DocsDescription) Note: requires 'Domain Name Administrator' GDAP role. Adds a DMARC record to MOERA (onmicrosoft.com) domains. This should be enabled even if the MOERA (onmicrosoft.com) domains is not used for sending. Enabling this prevents email spoofing. The default record is 'v=DMARC1; p=reject;' recommended because the domain is only used within M365 and reporting is not needed. Omitting pct tag default to 100%
+ .NOTES
+ CAT
+ Global Standards
+ TAG
+ "CIS"
+ "Security"
+ "PhishingProtection"
+ ADDEDCOMPONENT
+ {"type":"autoComplete","multiple":false,"creatable":true,"required":false,"placeholder":"v=DMARC1; p=reject; (recommended)","label":"Value","name":"standards.AddDMARCToMOERA.RecordValue","options":[{"label":"v=DMARC1; p=reject; (recommended)","value":"v=DMARC1; p=reject;"}]}
+ IMPACT
+ Low Impact
+ ADDEDDATE
+ 2025-06-16
+ POWERSHELLEQUIVALENT
+ Portal only
+ RECOMMENDEDBY
+ "CIS"
+ "Microsoft"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+ #$Rerun -Type Standard -Tenant $Tenant -API 'AddDMARCToMOERA' -Settings $Settings
+
+ $RecordModel = [PSCustomObject]@{
+ HostName = '_dmarc'
+ TtlValue = 3600
+ Type = 'TXT'
+ Value = $Settings.RecordValue.Value ?? "v=DMARC1; p=reject;"
+ }
+
+ # Get all fallback domains (onmicrosoft.com domains) and check if the DMARC record is set correctly
+ try {
+ $Domains = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri 'https://admin.microsoft.com/admin/api/Domains/List' | Where-Object -Property Name -like "*.onmicrosoft.com"
+
+ $CurrentInfo = $Domains | ForEach-Object {
+ # Get current DNS records that matches _dmarc hostname and TXT type
+ $CurrentRecords = New-GraphGetRequest -scope 'https://admin.microsoft.com/.default' -TenantID $Tenant -Uri "https://admin.microsoft.com/admin/api/Domains/Records?domainName=$($_.Name)" | Select-Object -ExpandProperty DnsRecords | Where-Object { $_.HostName -eq $RecordModel.HostName -and $_.Type -eq $RecordModel.Type }
+
+ if ($CurrentRecords.count -eq 0) {
+ #record not found, return a model with Match set to false
+ [PSCustomObject]@{
+ DomainName = $_.Name
+ Match = $false
+ CurrentRecord = $null
+ }
+ } else {
+ foreach ($CurrentRecord in $CurrentRecords) {
+ # Create variable matching the RecordModel used for comparison
+ $CurrentRecordModel = [PSCustomObject]@{
+ HostName = $CurrentRecord.HostName
+ TtlValue = $CurrentRecord.TtlValue
+ Type = $CurrentRecord.Type
+ Value = $CurrentRecord.Value
+ }
+
+ # Compare the current record with the expected record model
+ if (!(Compare-Object -ReferenceObject $RecordModel -DifferenceObject $CurrentRecordModel -Property HostName, TtlValue, Type, Value)) {
+ [PSCustomObject]@{
+ DomainName = $_.Name
+ Match = $true
+ CurrentRecord = $CurrentRecord
+ }
+ } else {
+ [PSCustomObject]@{
+ DomainName = $_.Name
+ Match = $false
+ CurrentRecord = $CurrentRecord
+ }
+ }
+ }
+ }
+ }
+ # Check if match is true and there is only one DMARC record for the domain
+ $StateIsCorrect = $false -notin $CurrentInfo.Match -and $CurrentInfo.Count -eq 1
+ } catch {
+ if ($_.Exception.Message -like '*403*') {
+ $Message = "AddDMARCToMOERA: Insufficient permissions. Please ensure the tenant GDAP relationship includes the 'Domain Name Administrator' role: $(Get-NormalizedError -message $_.Exception.message)"
+ }
+ else {
+ $Message = "Failed to get dns records for MOERA domains: $(Get-NormalizedError -message $_.Exception.message)"
+ }
+ Write-LogMessage -API 'Standards' -tenant $tenant -message $Message -sev Error
+ throw $Message
+ }
+
+ If ($Settings.remediate -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'DMARC record is already set for all MOERA (onmicrosoft.com) domains.' -sev Info
+ }
+ else {
+ # Loop through each domain and set the DMARC record, existing misconfigured records and duplicates will be deleted
+ foreach ($Domain in ($CurrentInfo | Sort-Object -Property DomainName -Unique)) {
+ try {
+ foreach ($Record in ($CurrentInfo | Where-Object -Property DomainName -eq $Domain.DomainName)) {
+ if ($Record.CurrentRecord) {
+ New-GraphPOSTRequest -tenantid $tenant -scope 'https://admin.microsoft.com/.default' -Uri "https://admin.microsoft.com/admin/api/Domains/Record?domainName=$($Domain.DomainName)" -Body ($Record.CurrentRecord | ConvertTo-Json -Compress) -AddedHeaders @{'x-http-method-override' = 'Delete'}
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Deleted incorrect DMARC record for domain $($Domain.DomainName)" -sev Info
+ }
+ New-GraphPOSTRequest -tenantid $tenant -scope 'https://admin.microsoft.com/.default' -type "PUT" -Uri "https://admin.microsoft.com/admin/api/Domains/Record?domainName=$($Domain.DomainName)" -Body (@{RecordModel = $RecordModel} | ConvertTo-Json -Compress)
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Set DMARC record for domain $($Domain.DomainName)" -sev Info
+ }
+ }
+ catch {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set DMARC record for domain $($Domain.DomainName): $(Get-NormalizedError -message $_.Exception.message)" -sev Error
+ }
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'DMARC record is already set for all MOERA (onmicrosoft.com) domains.' -sev Info
+ } else {
+ $UniqueDomains = ($CurrentInfo | Sort-Object -Property DomainName -Unique)
+ $NotSetDomains = @($UniqueDomains | ForEach-Object {if ($_.Match -eq $false -or ($CurrentInfo | Where-Object -Property DomainName -eq $_.DomainName).Count -eq 1) { $_.DomainName } })
+ $Message = "DMARC record is not set for $($NotSetDomains.count) of $($UniqueDomains.count) MOERA (onmicrosoft.com) domains."
+
+ Write-StandardsAlert -message $Message -object @{MissingDMARC = ($NotSetDomains -join ', ')} -tenant $tenant -standardName 'AddDMARCToMOERA' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "$Message. Missing for: $($NotSetDomains -join ', ')" -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ set-CIPPStandardsCompareField -FieldName 'standards.AddDMARCToMOERA' -FieldValue $StateIsCorrect -TenantFilter $Tenant
+ Add-CIPPBPAField -FieldName 'AddDMARCToMOERA' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1
index fb768688f7de..f4c51a1a7048 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1
@@ -69,15 +69,15 @@ function Invoke-CIPPStandardAppDeploy {
foreach ($AppId in $AppIds) {
if ($AppId -notin $AppExists.appId) {
- Write-Information "Adding $($AppId) to tenant $($Tenant)."
- $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Item.tenant -body "{ `"appId`": `"$($Item.appId)`" }"
- Write-LogMessage -message "Added $($Item.AppId) to tenant $($Item.Tenant)" -tenant $Item.Tenant -API 'Add Multitenant App' -sev Info
+ Write-Information "Adding $AppId to tenant $Tenant."
+ $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Tenant -body "{ `"appId`": `"$AppId`" }"
+ Write-LogMessage -message "Added $AppId to tenant $Tenant" -tenant $Tenant -API 'Add Multitenant App' -sev Info
}
}
foreach ($TemplateId in $TemplateIds) {
try {
- Add-CIPPApplicationPermission -TemplateId $TemplateId -Tenantfilter $Tenant
- Add-CIPPDelegatedPermission -TemplateId $TemplateId -Tenantfilter $Tenant
+ Add-CIPPApplicationPermission -TemplateId $TemplateId -TenantFilter $Tenant
+ Add-CIPPDelegatedPermission -TemplateId $TemplateId -TenantFilter $Tenant
Write-LogMessage -API 'Standards' -tenant $tenant -message "Added application(s) from template $($TemplateName) and updated it's permissions" -sev Info
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1
index cf616b421872..041628368288 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1
@@ -18,7 +18,7 @@ function Invoke-CIPPStandardAutopilotProfile {
ADDEDCOMPONENT
{"type":"textField","name":"standards.AutopilotProfile.DisplayName","label":"Profile Display Name"}
{"type":"textField","name":"standards.AutopilotProfile.Description","label":"Profile Description"}
- {"type":"textField","name":"standards.AutopilotProfile.DeviceNameTemplate","label":"Unique Device Name Template"}
+ {"type":"textField","name":"standards.AutopilotProfile.DeviceNameTemplate","label":"Unique Device Name Template","required":false}
{"type":"autoComplete","multiple":false,"creatable":false,"required":false,"name":"standards.AutopilotProfile.Languages","label":"Languages","api":{"url":"/languageList.json","labelField":"language","valueField":"tag"}}
{"type":"switch","name":"standards.AutopilotProfile.CollectHash","label":"Convert all targeted devices to Autopilot","defaultValue":true}
{"type":"switch","name":"standards.AutopilotProfile.AssignToAllDevices","label":"Assign to all devices","defaultValue":true}
@@ -43,42 +43,44 @@ function Invoke-CIPPStandardAutopilotProfile {
# Get the current configuration
try {
+ # Replace variables in displayname to prevent duplicates
+ $DisplayName = Get-CIPPTextReplacement -Text $Settings.DisplayName -TenantFilter $Tenant
+
$CurrentConfig = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotDeploymentProfiles' -tenantid $Tenant |
- Where-Object { $_.displayName -eq $Settings.DisplayName } |
- Select-Object -Property displayName, description, deviceNameTemplate, language, enableWhiteGlove, extractHardwareHash, outOfBoxExperienceSetting, preprovisioningAllowed
+ Where-Object { $_.displayName -eq $DisplayName } |
+ Select-Object -Property displayName, description, deviceNameTemplate, language, enableWhiteGlove, extractHardwareHash, outOfBoxExperienceSetting, preprovisioningAllowed
if ($Settings.NotLocalAdmin -eq $true) { $userType = 'Standard' } else { $userType = 'Administrator' }
if ($Settings.SelfDeployingMode -eq $true) { $DeploymentMode = 'shared' } else { $DeploymentMode = 'singleUser' }
- if ($Settings.AllowWhiteGlove -eq $true) {$Settings.HideChangeAccount = $true}
+ if ($Settings.AllowWhiteGlove -eq $true) { $Settings.HideChangeAccount = $true }
- $StateIsCorrect = ($CurrentConfig.displayName -eq $Settings.DisplayName) -and
- ($CurrentConfig.description -eq $Settings.Description) -and
- ($CurrentConfig.deviceNameTemplate -eq $Settings.DeviceNameTemplate) -and
- ([string]::IsNullOrWhiteSpace($CurrentConfig.language) -and [string]::IsNullOrWhiteSpace($Settings.Languages.value) -or $CurrentConfig.language -eq $Settings.Languages.value) -and
- ($CurrentConfig.enableWhiteGlove -eq $Settings.AllowWhiteGlove) -and
- ($CurrentConfig.extractHardwareHash -eq $Settings.CollectHash) -and
- ($CurrentConfig.outOfBoxExperienceSetting.deviceUsageType -eq $DeploymentMode) -and
- ($CurrentConfig.outOfBoxExperienceSetting.escapeLinkHidden -eq $Settings.HideChangeAccount) -and
- ($CurrentConfig.outOfBoxExperienceSetting.privacySettingsHidden -eq $Settings.HidePrivacy) -and
- ($CurrentConfig.outOfBoxExperienceSetting.eulaHidden -eq $Settings.HideTerms) -and
- ($CurrentConfig.outOfBoxExperienceSetting.userType -eq $userType) -and
- ($CurrentConfig.outOfBoxExperienceSetting.keyboardSelectionPageSkipped -eq $Settings.AutoKeyboard)
- }
- catch {
+ $StateIsCorrect = ($CurrentConfig.displayName -eq $DisplayName) -and
+ ($CurrentConfig.description -eq $Settings.Description) -and
+ ($CurrentConfig.deviceNameTemplate -eq $Settings.DeviceNameTemplate) -and
+ ([string]::IsNullOrWhiteSpace($CurrentConfig.language) -and [string]::IsNullOrWhiteSpace($Settings.Languages.value) -or $CurrentConfig.language -eq $Settings.Languages.value) -and
+ ($CurrentConfig.enableWhiteGlove -eq $Settings.AllowWhiteGlove) -and
+ ($CurrentConfig.extractHardwareHash -eq $Settings.CollectHash) -and
+ ($CurrentConfig.outOfBoxExperienceSetting.deviceUsageType -eq $DeploymentMode) -and
+ ($CurrentConfig.outOfBoxExperienceSetting.escapeLinkHidden -eq $Settings.HideChangeAccount) -and
+ ($CurrentConfig.outOfBoxExperienceSetting.privacySettingsHidden -eq $Settings.HidePrivacy) -and
+ ($CurrentConfig.outOfBoxExperienceSetting.eulaHidden -eq $Settings.HideTerms) -and
+ ($CurrentConfig.outOfBoxExperienceSetting.userType -eq $userType) -and
+ ($CurrentConfig.outOfBoxExperienceSetting.keyboardSelectionPageSkipped -eq $Settings.AutoKeyboard)
+ } catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to check Autopilot profile: $ErrorMessage" -sev Error
$StateIsCorrect = $false
}
# Remediate if the state is not correct
- If ($Settings.remediate -eq $true) {
+ if ($Settings.remediate -eq $true) {
if ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($Settings.DisplayName)' already exists" -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($DisplayName)' already exists" -sev Info
} else {
try {
$Parameters = @{
tenantFilter = $Tenant
- displayName = $Settings.DisplayName
+ displayName = $DisplayName
description = $Settings.Description
userType = $userType
DeploymentMode = $DeploymentMode
@@ -95,9 +97,9 @@ function Invoke-CIPPStandardAutopilotProfile {
Set-CIPPDefaultAPDeploymentProfile @Parameters
if ($null -eq $CurrentConfig) {
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created Autopilot profile '$($Settings.DisplayName)'" -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created Autopilot profile '$($DisplayName)'" -sev Info
} else {
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated Autopilot profile '$($Settings.DisplayName)'" -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated Autopilot profile '$($DisplayName)'" -sev Info
}
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
@@ -108,19 +110,19 @@ function Invoke-CIPPStandardAutopilotProfile {
}
# Report
- If ($Settings.report -eq $true) {
+ if ($Settings.report -eq $true) {
$FieldValue = $StateIsCorrect -eq $true ? $true : $CurrentConfig
Set-CIPPStandardsCompareField -FieldName 'standards.AutopilotProfile' -FieldValue $FieldValue -TenantFilter $Tenant
Add-CIPPBPAField -FieldName 'AutopilotProfile' -FieldValue [bool]$StateIsCorrect -StoreAs bool -Tenant $Tenant
}
# Alert
- If ($Settings.alert -eq $true) {
- If ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($Settings.DisplayName)' exists" -sev Info
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($DisplayName)' exists" -sev Info
} else {
- Write-StandardsAlert -message "Autopilot profile '$($Settings.DisplayName)' do not match expected configuration" -object $CurrentConfig -tenant $Tenant -standardName 'AutopilotProfile' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($Settings.DisplayName)' do not match expected configuration" -sev Info
+ Write-StandardsAlert -message "Autopilot profile '$($DisplayName)' do not match expected configuration" -object $CurrentConfig -tenant $Tenant -standardName 'AutopilotProfile' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Autopilot profile '$($DisplayName)' do not match expected configuration" -sev Info
}
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1
index 2bf8af08593e..3220da6c44f0 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardConditionalAccessTemplate.ps1
@@ -33,8 +33,6 @@ function Invoke-CIPPStandardConditionalAccessTemplate {
If ($Settings.remediate -eq $true) {
- $APINAME = 'Standards'
-
foreach ($Setting in $Settings) {
try {
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1
new file mode 100644
index 000000000000..9093ab528e51
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDefaultSharingLink.ps1
@@ -0,0 +1,73 @@
+function Invoke-CIPPStandardDefaultSharingLink {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) DefaultSharingLink
+ .SYNOPSIS
+ (Label) Set Default Sharing Link Settings
+ .DESCRIPTION
+ (Helptext) Sets the default sharing link type to Internal and permission to View in SharePoint and OneDrive.
+ (DocsDescription) Sets the default sharing link type to Internal and permission to View in SharePoint and OneDrive.
+ .NOTES
+ CAT
+ SharePoint Standards
+ TAG
+ ADDEDCOMPONENT
+ IMPACT
+ Medium Impact
+ ADDEDDATE
+ 2025-06-13
+ POWERSHELLEQUIVALENT
+ Set-SPOTenant -DefaultSharingLinkType Internal -DefaultLinkPermission View
+ RECOMMENDEDBY
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+
+ $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant |
+ Select-Object -Property DefaultSharingLinkType, DefaultLinkPermission
+
+ $StateIsCorrect = ($CurrentState.DefaultSharingLinkType -eq 2) -and ($CurrentState.DefaultLinkPermission -eq 1)
+
+ if ($Settings.remediate -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Default sharing link settings are already configured correctly' -Sev Info
+ } else {
+ $Properties = @{
+ DefaultSharingLinkType = 2 # Internal
+ DefaultLinkPermission = 1 # View
+ }
+
+ try {
+ Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully set default sharing link settings' -Sev Info
+ } catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set default sharing link settings. Error: $ErrorMessage" -Sev Error
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Default sharing link settings are configured correctly' -Sev Info
+ } else {
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Default sharing link settings are not configured correctly' -Sev Alert
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ Add-CIPPBPAField -FieldName 'DefaultSharingLink' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant
+ if ($StateIsCorrect) {
+ $FieldValue = $true
+ } else {
+ $FieldValue = $CurrentState
+ }
+ Set-CIPPStandardsCompareField -FieldName 'standards.DefaultSharingLink' -FieldValue $FieldValue -Tenant $Tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1
new file mode 100644
index 000000000000..26a3f61a0d12
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployContactTemplates.ps1
@@ -0,0 +1,374 @@
+function Invoke-CIPPStandardDeployContactTemplates {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) DeployContactTemplates
+ .SYNOPSIS
+ (Label) Deploy Mail Contact Template
+ .DESCRIPTION
+ (Helptext) Creates new mail contacts in Exchange Online across all selected tenants based on the selected templates. The contact will be visible in the Global Address List unless hidden.
+ (DocsDescription) This standard creates new mail contacts in Exchange Online based on the selected templates. Mail contacts are useful for adding external email addresses to your organization's address book. They can be used for distribution lists, shared mailboxes, and other collaboration scenarios.
+ .NOTES
+ CAT
+ Exchange Standards
+ TAG
+ ADDEDCOMPONENT
+ {"type":"autoComplete","multiple":true,"creatable":false,"label":"Select Mail Contact Templates","name":"standards.DeployContactTemplates.templateIds","api":{"url":"/api/ListContactTemplates","labelField":"name","valueField":"GUID","queryKey":"Contact Templates"}}
+ DISABLEDFEATURES
+ {"report":false,"warn":false,"remediate":false}
+ IMPACT
+ Low Impact
+ ADDEDDATE
+ 2025-05-31
+ POWERSHELLEQUIVALENT
+ New-MailContact
+ RECOMMENDEDBY
+ "CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+
+ $APIName = 'Standards'
+
+
+
+ # Helper function to get template by GUID
+ function Get-ContactTemplate($TemplateGUID) {
+ try {
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'ContactTemplate' and RowKey eq '$TemplateGUID'"
+ $StoredTemplate = Get-CIPPAzDataTableEntity @Table -Filter $Filter
+
+ if (-not $StoredTemplate) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "Contact template with GUID $TemplateGUID not found" -sev Error
+ return $null
+ }
+
+ return $StoredTemplate.JSON | ConvertFrom-Json
+ }
+ catch {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "Failed to retrieve template $TemplateGUID. Error: $($_.Exception.Message)" -sev Error
+ return $null
+ }
+ }
+
+
+
+ try {
+ # Extract control flags from Settings
+ $RemediateEnabled = [bool]$Settings.remediate
+ $AlertEnabled = [bool]$Settings.alert
+ $ReportEnabled = [bool]$Settings.report
+
+ # Get templateIds array
+ if (-not $Settings.templateIds -or $Settings.templateIds.Count -eq 0) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No template IDs found in settings" -sev Error
+ return "No template IDs found in settings"
+ }
+
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($Settings.templateIds.Count) template(s)" -sev Info
+
+ # Get the current contacts
+ $CurrentContacts = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailContact' -ErrorAction Stop
+
+ # Process each template in the templateIds array
+ $CompareList = foreach ($TemplateItem in $Settings.templateIds) {
+ try {
+ # Get the template GUID directly from the value property
+ $TemplateGUID = $TemplateItem.value
+
+ if ([string]::IsNullOrWhiteSpace($TemplateGUID)) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: TemplateGUID cannot be empty." -sev Error
+ continue
+ }
+
+ # Fetch the template from storage
+ $Template = Get-ContactTemplate -TemplateGUID $TemplateGUID
+ if (-not $Template) {
+ continue
+ }
+
+ # Input validation for required fields
+ if ([string]::IsNullOrWhiteSpace($Template.displayName)) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: DisplayName cannot be empty for template $TemplateGUID." -sev Error
+ continue
+ }
+
+ if ([string]::IsNullOrWhiteSpace($Template.email)) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: ExternalEmailAddress cannot be empty for template $TemplateGUID." -sev Error
+ continue
+ }
+
+ # Validate email address format
+ try {
+ $null = [System.Net.Mail.MailAddress]::new($Template.email)
+ }
+ catch {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Invalid email address format: $($Template.email)" -sev Error
+ continue
+ }
+
+ # Check if the contact already exists (using DisplayName as key)
+ $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName }
+
+ # If the contact exists, we'll overwrite it; if not, we'll create it
+ if ($ExistingContact) {
+ $StateIsCorrect = $false # Always update existing contacts to match template
+ $Action = "Update"
+ $Missing = $false
+ }
+ else {
+ # Contact doesn't exist, needs to be created
+ $StateIsCorrect = $false
+ $Action = "Create"
+ $Missing = $true
+ }
+
+ [PSCustomObject]@{
+ missing = $Missing
+ StateIsCorrect = $StateIsCorrect
+ Action = $Action
+ Template = $Template
+ TemplateGUID = $TemplateGUID
+ }
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $Message = "Failed to process template $TemplateGUID, Error: $ErrorMessage"
+ Write-LogMessage -API $APIName -tenant $tenant -message $Message -sev 'Error'
+ Return $Message
+ }
+ }
+
+ # Remediate each contact which needs to be created or updated
+ If ($RemediateEnabled) {
+ $ContactsToProcess = $CompareList | Where-Object { $_.StateIsCorrect -eq $false }
+
+ if ($ContactsToProcess.Count -gt 0) {
+ $ContactsToCreate = $ContactsToProcess | Where-Object { $_.Action -eq "Create" }
+ $ContactsToUpdate = $ContactsToProcess | Where-Object { $_.Action -eq "Update" }
+
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Processing $($ContactsToCreate.Count) new contacts, $($ContactsToUpdate.Count) existing contacts" -sev Info
+
+ # First pass: Create new mail contacts and update existing ones
+ $ProcessedContacts = [System.Collections.Generic.List[PSCustomObject]]::new()
+ $ProcessingFailures = 0
+
+ # Handle new contacts
+ foreach ($Contact in $ContactsToCreate) {
+ try {
+ $Template = $Contact.Template
+
+ # Parameters for creating new contact
+ $NewContactParams = @{
+ displayName = $Template.displayName
+ name = $Template.displayName
+ ExternalEmailAddress = $Template.email
+ }
+
+ # Add optional name fields if provided
+ if (![string]::IsNullOrWhiteSpace($Template.firstName)) {
+ $NewContactParams.FirstName = $Template.firstName
+ }
+ if (![string]::IsNullOrWhiteSpace($Template.lastName)) {
+ $NewContactParams.LastName = $Template.lastName
+ }
+
+ # Create the mail contact
+ $NewContact = New-ExoRequest -tenantid $Tenant -cmdlet 'New-MailContact' -cmdParams $NewContactParams -UseSystemMailbox $true
+
+ # Store contact info for second pass
+ $ProcessedContacts.Add([PSCustomObject]@{
+ Contact = $Contact
+ ContactObject = $NewContact
+ Template = $Template
+ IsNew = $true
+ })
+ }
+ catch {
+ $ProcessingFailures++
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create contact $($Template.displayName): $ErrorMessage" -sev 'Error'
+ }
+ }
+
+ # Handle existing contacts - update their basic properties
+ foreach ($Contact in $ContactsToUpdate) {
+ try {
+ $Template = $Contact.Template
+ $ExistingContact = $CurrentContacts | Where-Object { $_.DisplayName -eq $Template.displayName }
+
+ # Update MailContact properties (email address)
+ $UpdateMailContactParams = @{
+ Identity = $ExistingContact.Identity
+ ExternalEmailAddress = $Template.email
+ }
+
+ # Update the existing mail contact
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $UpdateMailContactParams -UseSystemMailbox $true
+
+ # Update Contact properties (names) if provided
+ $UpdateContactParams = @{
+ Identity = $ExistingContact.Identity
+ }
+ $ContactNeedsUpdate = $false
+
+ if (![string]::IsNullOrWhiteSpace($Template.firstName)) {
+ $UpdateContactParams.FirstName = $Template.firstName
+ $ContactNeedsUpdate = $true
+ }
+ if (![string]::IsNullOrWhiteSpace($Template.lastName)) {
+ $UpdateContactParams.LastName = $Template.lastName
+ $ContactNeedsUpdate = $true
+ }
+
+ # Only update Contact if we have name changes
+ if ($ContactNeedsUpdate) {
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $UpdateContactParams -UseSystemMailbox $true
+ }
+
+ # Store contact info for second pass
+ $ProcessedContacts.Add([PSCustomObject]@{
+ Contact = $Contact
+ ContactObject = $ExistingContact
+ Template = $Template
+ IsNew = $false
+ })
+ }
+ catch {
+ $ProcessingFailures++
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update contact $($Template.displayName): $ErrorMessage" -sev 'Error'
+ }
+ }
+
+ # Log processing summary
+ $ProcessedCount = $ProcessedContacts.Count
+ if ($ProcessedCount -gt 0) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Successfully processed $ProcessedCount contacts" -sev Info
+
+ # Wait for contacts to propagate before updating additional fields
+ Start-Sleep -Seconds 1
+
+ # Second pass: Update contacts with additional fields (only if needed)
+ $UpdateFailures = 0
+ $ContactsRequiringUpdates = 0
+
+ foreach ($ProcessedContactInfo in $ProcessedContacts) {
+ try {
+ $Template = $ProcessedContactInfo.Template
+ $ContactObject = $ProcessedContactInfo.ContactObject
+ $HasUpdates = $false
+
+ # Check if Set-Contact is needed
+ $ContactIdentity = if ($ProcessedContactInfo.IsNew) { $ContactObject.id } else { $ContactObject.Identity }
+ $SetContactParams = @{ Identity = $ContactIdentity }
+ $PropertyMap = @{
+ 'Company' = $Template.companyName
+ 'StateOrProvince' = $Template.state
+ 'Office' = $Template.streetAddress
+ 'Phone' = $Template.businessPhone
+ 'WebPage' = $Template.website
+ 'Title' = $Template.jobTitle
+ 'City' = $Template.city
+ 'PostalCode' = $Template.postalCode
+ 'CountryOrRegion' = $Template.country
+ 'MobilePhone' = $Template.mobilePhone
+ }
+
+ foreach ($Property in $PropertyMap.GetEnumerator()) {
+ if (![string]::IsNullOrWhiteSpace($Property.Value)) {
+ $SetContactParams[$Property.Key] = $Property.Value
+ $HasUpdates = $true
+ }
+ }
+
+ # Check if Set-MailContact is needed for additional properties
+ $MailContactParams = @{ Identity = $ContactIdentity }
+ $NeedsMailContactUpdate = $false
+
+ if ([bool]$Template.hidefromGAL) {
+ $MailContactParams.HiddenFromAddressListsEnabled = $true
+ $NeedsMailContactUpdate = $true
+ $HasUpdates = $true
+ }
+
+ if (![string]::IsNullOrWhiteSpace($Template.mailTip)) {
+ $MailContactParams.MailTip = $Template.mailTip
+ $NeedsMailContactUpdate = $true
+ $HasUpdates = $true
+ }
+
+ # Only increment and update if there are actual changes
+ if ($HasUpdates) {
+ $ContactsRequiringUpdates++
+
+ # Apply Set-Contact updates if needed
+ if ($SetContactParams.Count -gt 1) {
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-Contact' -cmdParams $SetContactParams -UseSystemMailbox $true
+ }
+
+ # Apply Set-MailContact updates if needed
+ if ($NeedsMailContactUpdate) {
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-MailContact' -cmdParams $MailContactParams -UseSystemMailbox $true
+ }
+ }
+ }
+ catch {
+ $UpdateFailures++
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API $APIName -tenant $tenant -message "Failed to update additional fields for contact $($Template.displayName): $ErrorMessage" -sev 'Error'
+ }
+ }
+
+ # Log update summary only if updates were needed
+ if ($ContactsRequiringUpdates -gt 0) {
+ $SuccessfulUpdates = $ContactsRequiringUpdates - $UpdateFailures
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: Updated additional fields for $SuccessfulUpdates of $ContactsRequiringUpdates contacts" -sev Info
+ }
+ }
+
+ # Final summary
+ if ($ProcessingFailures -gt 0) {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $ProcessingFailures contacts failed to process" -sev Error
+ }
+ }
+ }
+
+ if ($AlertEnabled) {
+ $MissingContacts = ($CompareList | Where-Object { $_.missing }).Count
+ $ExistingContacts = ($CompareList | Where-Object { -not $_.missing }).Count
+
+ if ($MissingContacts -gt 0 -or $ExistingContacts -gt 0) {
+ foreach ($Contact in $CompareList) {
+ if ($Contact.missing) {
+ $CurrentInfo = $Contact.Template | Select-Object -Property displayName, email, missing
+ Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) is missing." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate'
+ }
+ else {
+ $CurrentInfo = $CurrentContacts | Where-Object -Property DisplayName -eq $Contact.Template.displayName | Select-Object -Property DisplayName, ExternalEmailAddress, FirstName, LastName
+ Write-StandardsAlert -message "Mail contact $($Contact.Template.displayName) from template $($Contact.TemplateGUID) will be updated to match template." -object $CurrentInfo -tenant $Tenant -standardName 'DeployContactTemplate'
+ }
+ }
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: $MissingContacts missing, $ExistingContacts to update" -sev Info
+ } else {
+ Write-LogMessage -API $APIName -tenant $Tenant -message "DeployContactTemplate: No contacts need processing" -sev Info
+ }
+ }
+
+ if ($ReportEnabled) {
+ foreach ($Contact in $CompareList) {
+ Set-CIPPStandardsCompareField -FieldName "standards.DeployContactTemplate" -FieldValue $Contact.StateIsCorrect -TenantFilter $Tenant
+ }
+ }
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API $APIName -tenant $tenant -message "Failed to create or update mail contact(s) from templates, Error: $ErrorMessage" -sev 'Error'
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1
index 1802ac42847a..4f991c25be1f 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDeployMailContact.ps1
@@ -21,11 +21,15 @@ function Invoke-CIPPStandardDeployMailContact {
IMPACT
Low Impact
ADDEDDATE
- 2025-05-28
+ 2024-03-19
POWERSHELLEQUIVALENT
New-MailContact
RECOMMENDEDBY
"CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
#>
param($Tenant, $Settings)
@@ -100,4 +104,4 @@ function Invoke-CIPPStandardDeployMailContact {
Add-CIPPBPAField -FieldName 'DeployMailContact' -FieldValue $ReportData -StoreAs json -Tenant $Tenant
Set-CIPPStandardsCompareField -FieldName 'standards.DeployMailContact' -FieldValue $($ExistingContact ? $true : $ReportData) -Tenant $Tenant
}
-}
\ No newline at end of file
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1
index 2608c92914fc..74ca49628813 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableAdditionalStorageProviders.ps1
@@ -33,21 +33,21 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders {
param($Tenant, $Settings)
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableAdditionalStorageProviders'
- $AdditionalStorageProvidersState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{Identity = 'OwaMailboxPolicy-Default' }
+ $AdditionalStorageProvidersState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OwaMailboxPolicy' -cmdParams @{Identity = 'OwaMailboxPolicy-Default' } -Select 'Identity, AdditionalStorageProvidersAvailable'
if ($Settings.remediate -eq $true) {
try {
if ($AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable) {
- New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams @{ Identity = $AdditionalStorageProvidersState.Identity; AdditionalStorageProvidersAvailable = $false } -useSystemMailbox $true
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers have been disabled.' -sev Info
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OwaMailboxPolicy' -cmdParams @{ Identity = $AdditionalStorageProvidersState.Identity; AdditionalStorageProvidersAvailable = $false } -useSystemMailbox $true
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers has been disabled.' -sev Info
$AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable = $false
} else {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers are already disabled.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers are already disabled.' -sev Info
}
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable OWA additional storage providers. Error: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable OWA additional storage providers. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
@@ -55,16 +55,16 @@ function Invoke-CIPPStandardDisableAdditionalStorageProviders {
if ($Settings.alert -eq $true) {
if ($AdditionalStorageProvidersState.AdditionalStorageProvidersAvailable) {
$Object = $AdditionalStorageProvidersState | Select-Object -Property AdditionalStorageProvidersAvailable
- Write-StandardsAlert -message 'OWA additional storage providers are enabled' -object $Object -tenant $tenant -standardName 'DisableAdditionalStorageProviders' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers are enabled' -sev Info
+ Write-StandardsAlert -message 'OWA additional storage providers are enabled' -object $Object -tenant $Tenant -standardName 'DisableAdditionalStorageProviders' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers are enabled' -sev Info
} else {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'OWA additional storage providers are disabled' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'OWA additional storage providers are disabled' -sev Info
}
}
if ($Settings.report -eq $true) {
- $state = $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled ? $false : $true
- Set-CIPPStandardsCompareField -FieldName 'standards.DisableAdditionalStorageProviders' -FieldValue $state -TenantFilter $Tenant
- Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersEnabled' -FieldValue $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled -StoreAs bool -Tenant $tenant
+ $State = $AdditionalStorageProvidersState.AdditionalStorageProvidersEnabled ? $false : $true
+ Set-CIPPStandardsCompareField -FieldName 'standards.DisableAdditionalStorageProviders' -FieldValue $State -TenantFilter $Tenant
+ Add-CIPPBPAField -FieldName 'AdditionalStorageProvidersEnabled' -FieldValue $State -StoreAs bool -Tenant $Tenant
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1
new file mode 100644
index 000000000000..33b0ab22ed63
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableExchangeOnlinePowerShell.ps1
@@ -0,0 +1,95 @@
+function Invoke-CIPPStandardDisableExchangeOnlinePowerShell {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) DisableExchangeOnlinePowerShell
+ .SYNOPSIS
+ (Label) Disable Exchange Online PowerShell for non-admin users
+ .DESCRIPTION
+ (Helptext) Disables the ability for non-admin users to use Exchange Online PowerShell. Only administrators will be able to use PowerShell to connect to Exchange Online.
+ (DocsDescription) Disables the ability for non-admin users to use Exchange Online PowerShell. This helps prevent attackers from using PowerShell to run malicious commands, access file systems, registry, and distribute ransomware throughout networks. Only administrators will be able to use PowerShell to connect to Exchange Online, aligning with a least privileged access approach to security.
+ .NOTES
+ CAT
+ Exchange Standards
+ TAG
+ "CIS"
+ "PowerShell"
+ "Security"
+ ADDEDCOMPONENT
+ IMPACT
+ Medium Impact
+ ADDEDDATE
+ 2025-06-19
+ POWERSHELLEQUIVALENT
+ Get-User -ResultSize Unlimited -Filter 'RemotePowerShellEnabled -eq $true' | ForEach-Object { Set-User -Identity $_.Identity -RemotePowerShellEnabled $false }
+ RECOMMENDEDBY
+ "CIS"
+ "CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableExchangeOnlinePowerShell'
+
+ try {
+
+ $AdminUsers = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?$expand=principal' -tenantid $Tenant).principal.userPrincipalName
+ $UsersWithPowerShell = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-User' -Select 'userPrincipalName, identity, remotePowerShellEnabled' | Where-Object { $_.RemotePowerShellEnabled -eq $true -and $_.userPrincipalName -notin $AdminUsers }
+ $PowerShellEnabledCount = ($UsersWithPowerShell | Measure-Object).Count
+ $StateIsCorrect = $PowerShellEnabledCount -eq 0
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not check Exchange Online PowerShell status. $($ErrorMessage.NormalizedError)" -sev Error
+ $StateIsCorrect = $null
+ }
+
+ if ($Settings.remediate -eq $true) {
+ if ($PowerShellEnabledCount -gt 0) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Started disabling Exchange Online PowerShell for $PowerShellEnabledCount users." -sev Info
+
+ $Request = $UsersWithPowerShell | ForEach-Object {
+ @{
+ CmdletInput = @{
+ CmdletName = 'Set-User'
+ Parameters = @{Identity = $_.Identity; RemotePowerShellEnabled = $false }
+ }
+ }
+ }
+
+ $BatchResults = New-ExoBulkRequest -tenantid $tenant -cmdletArray @($Request)
+ $SuccessCount = 0
+ $BatchResults | ForEach-Object {
+ if ($_.error) {
+ $ErrorMessage = Get-NormalizedError -Message $_.error
+ Write-Host "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage"
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Exchange Online PowerShell for $($_.target). Error: $ErrorMessage" -sev Error
+ } else {
+ $SuccessCount++
+ }
+ }
+
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully disabled Exchange Online PowerShell for $SuccessCount out of $PowerShellEnabledCount users." -sev Info
+ } else {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'Exchange Online PowerShell is already disabled for all non-admin users' -sev Info
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'Exchange Online PowerShell is disabled for all non-admin users.' -sev Info
+ } else {
+ Write-StandardsAlert -message "Exchange Online PowerShell is enabled for $PowerShellEnabledCount users" -object @{UsersWithPowerShellEnabled = $PowerShellEnabledCount } -tenant $tenant -standardName 'DisableExchangeOnlinePowerShell' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Exchange Online PowerShell is enabled for $PowerShellEnabledCount users." -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ $state = $StateIsCorrect ?? @{UsersWithPowerShellEnabled = $PowerShellEnabledCount }
+ Set-CIPPStandardsCompareField -FieldName 'standards.DisableExchangeOnlinePowerShell' -FieldValue $state -TenantFilter $Tenant
+ Add-CIPPBPAField -FieldName 'ExchangeOnlinePowerShellDisabled' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1
index 26510a937daf..56ac2349159d 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableGuests.ps1
@@ -40,7 +40,7 @@ function Invoke-CIPPStandardDisableGuests {
Where-Object { $_.signInActivity.lastSuccessfulSignInDateTime -le $90Days }
$RecentlyReactivatedUsers = (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/auditLogs/directoryAudits?`$filter=activityDisplayName eq 'Enable account' and activityDateTime ge $AuditLookup" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant |
- ForEach-Object { $_.targetResources[0].id } | Select-Object -Unique)
+ ForEach-Object { $_.targetResources[0].id } | Select-Object -Unique)
$GraphRequest = $GraphRequest | Where-Object { -not ($RecentlyReactivatedUsers -contains $_.id) }
@@ -48,11 +48,11 @@ function Invoke-CIPPStandardDisableGuests {
if ($GraphRequest.Count -gt 0) {
foreach ($guest in $GraphRequest) {
try {
- New-GraphPostRequest -type Patch -tenantid $tenant -uri "https://graph.microsoft.com/beta/users/$($guest.id)" -body '{"accountEnabled":"false"}'
+ $null = New-GraphPostRequest -type Patch -tenantid $tenant -uri "https://graph.microsoft.com/beta/users/$($guest.id)" -body '{"accountEnabled":"false"}'
Write-LogMessage -API 'Standards' -tenant $tenant -message "Disabling guest $($guest.UserPrincipalName) ($($guest.id))" -sev Info
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable guest $($guest.UserPrincipalName) ($($guest.id)): $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
} else {
@@ -70,9 +70,9 @@ function Invoke-CIPPStandardDisableGuests {
}
}
if ($Settings.report -eq $true) {
- $filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled
- $state = $filtered ? $filtered : $true
- Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue $state -TenantFilter $Tenant
- Add-CIPPBPAField -FieldName 'DisableGuests' -FieldValue $filtered -StoreAs json -Tenant $tenant
+ $Filtered = $GraphRequest | Select-Object -Property UserPrincipalName, id, signInActivity, mail, userType, accountEnabled
+ $State = $Filtered ? $Filtered : $true
+ Set-CIPPStandardsCompareField -FieldName 'standards.DisableGuests' -FieldValue $State -TenantFilter $Tenant
+ Add-CIPPBPAField -FieldName 'DisableGuests' -FieldValue $Filtered -StoreAs json -Tenant $tenant
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1
new file mode 100644
index 000000000000..dca3fceecf40
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableResourceMailbox.ps1
@@ -0,0 +1,78 @@
+function Invoke-CIPPStandardDisableResourceMailbox {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) DisableResourceMailbox
+ .SYNOPSIS
+ (Label) Disable Unlicensed Resource Mailbox Entra accounts
+ .DESCRIPTION
+ (Helptext) Blocks login for all accounts that are marked as a resource mailbox and does not have a license assigned. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.
+ (DocsDescription) Resource mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for resource mailboxes. Accounts that are synced from on-premises AD are excluded, as account state is managed in the on-premises AD.
+ .NOTES
+ CAT
+ Exchange Standards
+ TAG
+ "CIS"
+ ADDEDCOMPONENT
+ IMPACT
+ Medium Impact
+ ADDEDDATE
+ 2025-06-01
+ POWERSHELLEQUIVALENT
+ Get-Mailbox & Update-MgUser
+ RECOMMENDEDBY
+ "CIS"
+ "CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableResourceMailbox'
+
+ # Get all users that are able to be
+ $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true and assignedLicenses/$count eq 0&$count=true' -Tenantid $Tenant -ComplexFilter |
+ Where-Object { $_.userType -eq 'Member' }
+ $ResourceMailboxList = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdParams @{ Filter = "RecipientTypeDetails -eq 'RoomMailbox' -or RecipientTypeDetails -eq 'EquipmentMailbox'" } -Select 'UserPrincipalName,DisplayName,RecipientTypeDetails,ExternalDirectoryObjectId' |
+ Where-Object { $_.ExternalDirectoryObjectId -in $UserList.id }
+
+ If ($Settings.remediate -eq $true) {
+ Write-Host 'Time to remediate'
+
+
+ if ($ResourceMailboxList) {
+ Write-Host "Resource Mailboxes to disable: $($ResourceMailboxList.Count)"
+ $ResourceMailboxList | ForEach-Object {
+ try {
+ New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ExternalDirectoryObjectId)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for $($_.RecipientTypeDetails), $($_.DisplayName), $($_.UserPrincipalName) disabled." -sev Info
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for $($_.RecipientTypeDetails), $($_.DisplayName), $($_.UserPrincipalName). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ }
+ }
+ } else {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are already disabled.' -sev Info
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+
+ if ($ResourceMailboxList) {
+ Write-StandardsAlert -message "Resource mailboxes with enabled accounts: $($ResourceMailboxList.Count)" -object $ResourceMailboxList -tenant $Tenant -standardName 'DisableResourceMailbox' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Resource mailboxes with enabled accounts: $($ResourceMailboxList.Count)" -sev Info
+ } else {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for resource mailboxes are disabled.' -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ # If there are no resource mailboxes, we set the state to true, so that the standard reports as compliant.
+ $State = $ResourceMailboxList ? $ResourceMailboxList : $true
+ Set-CIPPStandardsCompareField -FieldName 'standards.DisableResourceMailbox' -FieldValue $State -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'DisableResourceMailbox' -FieldValue $ResourceMailboxList -StoreAs json -Tenant $Tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1
index 62a20519f44c..535aeca021cc 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1
@@ -7,8 +7,8 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses {
.SYNOPSIS
(Label) Disable Self Service Licensing
.DESCRIPTION
- (Helptext) This standard disables all self service licenses and enables all exclusions
- (DocsDescription) This standard disables all self service licenses and enables all exclusions
+ (Helptext) Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions
+ (DocsDescription) Note: requires 'Billing Administrator' GDAP role. This standard disables all self service licenses and enables all exclusions
.NOTES
CAT
Entra (AAD) Standards
@@ -31,15 +31,17 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses {
param($Tenant, $Settings)
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSelfServiceLicenses'
- # disable for now - MS enforced role requirement
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error
- return
-
try {
$selfServiceItems = (New-GraphGETRequest -scope 'aeb86249-8ea3-49e2-900b-54cc8e308f85/.default' -uri 'https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products' -tenantid $Tenant).items
} catch {
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to retrieve self service products: $($_.Exception.Message)" -sev Error
- throw "Failed to retrieve self service products: $($_.Exception.Message)"
+ if ($_.Exception.Message -like '*403*') {
+ $Message = "Failed to retrieve self service products: Insufficient permissions. Please ensure the tenant GDAP relationship includes the 'Billing Administrator' role: $($_.Exception.Message)"
+ }
+ else {
+ $Message = "Failed to retrieve self service products: $($_.Exception.Message)"
+ }
+ Write-LogMessage -API 'Standards' -tenant $tenant -message $Message -sev Error
+ throw $Message
}
if ($settings.remediate) {
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1
index 66bc9dc3e30f..257229cb8aea 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSharedMailbox.ps1
@@ -5,7 +5,7 @@ function Invoke-CIPPStandardDisableSharedMailbox {
.COMPONENT
(APIName) DisableSharedMailbox
.SYNOPSIS
- (Label) Disable Shared Mailbox AAD accounts
+ (Label) Disable Shared Mailbox Entra accounts
.DESCRIPTION
(Helptext) Blocks login for all accounts that are marked as a shared mailbox. This is Microsoft best practice to prevent direct logons to shared mailboxes.
(DocsDescription) Shared mailboxes can be directly logged into if the password is reset, this presents a security risk as do all shared login credentials. Microsoft's recommendation is to disable the user account for shared mailboxes. It would be a good idea to review the sign-in reports to establish potential impact.
@@ -33,39 +33,39 @@ function Invoke-CIPPStandardDisableSharedMailbox {
param($Tenant, $Settings)
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'DisableSharedMailbox'
- $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true' -Tenantid $tenant -scope 'https://graph.microsoft.com/.default' -ComplexFilter
- $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -EQ 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName })
+ $UserList = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$filter=accountEnabled eq true and onPremisesSyncEnabled ne true&$count=true' -Tenantid $Tenant -ComplexFilter
+ $SharedMailboxList = (New-GraphGetRequest -uri "https://outlook.office365.com/adminapi/beta/$($Tenant)/Mailbox" -Tenantid $Tenant -scope ExchangeOnline | Where-Object { $_.RecipientTypeDetails -EQ 'SharedMailbox' -or $_.RecipientTypeDetails -eq 'SchedulingMailbox' -and $_.UserPrincipalName -in $UserList.UserPrincipalName })
If ($Settings.remediate -eq $true) {
if ($SharedMailboxList) {
$SharedMailboxList | ForEach-Object {
try {
- New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $tenant
- Write-LogMessage -API 'Standards' -tenant $tenant -message "AAD account for shared mailbox $($_.DisplayName) disabled." -sev Info
+ New-GraphPOSTRequest -uri "https://graph.microsoft.com/v1.0/users/$($_.ObjectKey)" -type PATCH -body '{"accountEnabled":"false"}' -tenantid $Tenant
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Entra account for shared mailbox $($_.DisplayName) disabled." -sev Info
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable AAD account for shared mailbox. Error: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Entra account for shared mailbox. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
} else {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'All AAD accounts for shared mailboxes are already disabled.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are already disabled.' -sev Info
}
}
if ($Settings.alert -eq $true) {
if ($SharedMailboxList) {
- Write-StandardsAlert -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -object $SharedMailboxList -tenant $tenant -standardName 'DisableSharedMailbox' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -sev Info
+ Write-StandardsAlert -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -object $SharedMailboxList -tenant $Tenant -standardName 'DisableSharedMailbox' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Shared mailboxes with enabled accounts: $($SharedMailboxList.Count)" -sev Info
} else {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'All AAD accounts for shared mailboxes are disabled.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'All Entra accounts for shared mailboxes are disabled.' -sev Info
}
}
if ($Settings.report -eq $true) {
- $state = $SharedMailboxList ? $SharedMailboxList : $true
- Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharedMailbox' -FieldValue $state -Tenant $tenant
- Add-CIPPBPAField -FieldName 'DisableSharedMailbox' -FieldValue $SharedMailboxList -StoreAs json -Tenant $tenant
+ $State = $SharedMailboxList ? $SharedMailboxList : $true
+ Set-CIPPStandardsCompareField -FieldName 'standards.DisableSharedMailbox' -FieldValue $State -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'DisableSharedMailbox' -FieldValue $SharedMailboxList -StoreAs json -Tenant $Tenant
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1
index 59487435b343..dcdab63d0073 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableTenantCreation.ps1
@@ -37,37 +37,37 @@ function Invoke-CIPPStandardDisableTenantCreation {
$StateIsCorrect = ($CurrentState.defaultUserRolePermissions.allowedToCreateTenants -eq $false)
If ($Settings.remediate -eq $true) {
+ Write-Host "Time to remediate DisableTenantCreation standard for tenant $Tenant"
if ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are already disabled from creating tenants.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are already disabled from creating tenants.' -sev Info
} else {
try {
$GraphRequest = @{
- tenantid = $tenant
- uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy'
- AsApp = $false
- Type = 'PATCH'
- ContentType = 'application/json'
- Body = '{"defaultUserRolePermissions":{"allowedToCreateTenants":false}}'
+ tenantid = $Tenant
+ uri = 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy'
+ Type = 'PATCH'
+ Body = '{"defaultUserRolePermissions":{"allowedToCreateTenants":false}}'
}
- New-GraphPostRequest @GraphRequest
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Disabled users from creating tenants.' -sev Info
+ New-GraphPOSTRequest @GraphRequest
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully disabled users from creating tenants.' -sev Info
} catch {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Failed to disable users from creating tenants' -sev 'Error' -LogData $_
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable users from creating tenants. Error: $($ErrorMessage.NormalizedError)" -sev 'Error' -LogData $ErrorMessage
}
}
}
if ($Settings.alert -eq $true) {
if ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are not allowed to create tenants.' -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are not allowed to create tenants.' -sev Info
} else {
- Write-StandardsAlert -message 'Users are allowed to create tenants' -object $CurrentState -tenant $tenant -standardName 'DisableTenantCreation' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Users are allowed to create tenants.' -sev Info
+ Write-StandardsAlert -message 'Users are allowed to create tenants' -object $CurrentState -tenant $Tenant -standardName 'DisableTenantCreation' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Users are allowed to create tenants.' -sev Info
}
}
if ($Settings.report -eq $true) {
Set-CIPPStandardsCompareField -FieldName 'standards.DisableTenantCreation' -FieldValue $StateIsCorrect -TenantFilter $Tenant
- Add-CIPPBPAField -FieldName 'DisableTenantCreation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $tenant
+ Add-CIPPBPAField -FieldName 'DisableTenantCreation' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1
new file mode 100644
index 000000000000..b3d00a681c95
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableNamePronunciation.ps1
@@ -0,0 +1,73 @@
+function Invoke-CIPPStandardEnableNamePronunciation {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) EnableNamePronunciation
+ .SYNOPSIS
+ (Label) Enable Name Pronunciation
+ .DESCRIPTION
+ (Helptext) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile.
+ (DocsDescription) Enables the Name Pronunciation feature for the tenant. This allows users to set their name pronunciation in their profile.
+ .NOTES
+ CAT
+ Global Standards
+ TAG
+ ADDEDCOMPONENT
+ IMPACT
+ Low Impact
+ ADDEDDATE
+ 2025-06-06
+ RECOMMENDEDBY
+ "CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param ($Tenant, $Settings)
+
+ $Uri = 'https://graph.microsoft.com/beta/admin/people/namePronunciation'
+ try {
+ $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Name Pronunciation. Error: $($ErrorMessage.NormalizedError)" -sev Error
+ Return
+ }
+ Write-Host $CurrentState
+
+ if ($Settings.remediate -eq $true) {
+ Write-Host 'Time to remediate'
+
+ if ($CurrentState.isEnabledInOrganization -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is already enabled.' -sev Info
+ } else {
+ $CurrentState.isEnabledInOrganization = $true
+ try {
+ $Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress
+ $null = New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH -AsApp $true
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled name pronunciation.' -sev Info
+ } catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable name pronunciation. Error: $ErrorMessage" -sev Error
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+
+ if ($CurrentState.isEnabledInOrganization -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is enabled.' -sev Info
+ } else {
+ Write-StandardsAlert -message 'Name Pronunciation is not enabled' -object $CurrentState -tenant $tenant -standardName 'EnableNamePronunciation' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $tenant -message 'Name Pronunciation is not enabled.' -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ Set-CIPPStandardsCompareField -FieldName 'standards.EnableNamePronunciation' -FieldValue $CurrentState.isEnabledInOrganization -Tenant $tenant
+ Add-CIPPBPAField -FieldName 'NamePronunciationEnabled' -FieldValue $CurrentState.isEnabledInOrganization -StoreAs bool -Tenant $tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1
index e3c27cd633ad..3e3a23a6ee9a 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnablePronouns.ps1
@@ -35,10 +35,9 @@ function Invoke-CIPPStandardEnablePronouns {
$CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant
} catch {
$ErrorMessage = Get-CippException -Exception $_
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get CurrentState for Pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
Return
}
- Write-Host $CurrentState
if ($Settings.remediate -eq $true) {
Write-Host 'Time to remediate'
@@ -49,11 +48,11 @@ function Invoke-CIPPStandardEnablePronouns {
$CurrentState.isEnabledInOrganization = $true
try {
$Body = ConvertTo-Json -InputObject $CurrentState -Depth 10 -Compress
- New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH
+ $null = New-GraphPostRequest -Uri $Uri -tenantid $Tenant -Body $Body -type PATCH -AsApp $true
Write-LogMessage -API 'Standards' -tenant $tenant -message 'Enabled pronouns.' -sev Info
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable pronouns. Error: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enable pronouns. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1
index 37910f14272d..d32d5008f175 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExConnector.ps1
@@ -8,7 +8,6 @@ function Invoke-CIPPStandardExConnector {
If ($Settings.remediate -eq $true) {
- $APINAME = 'Standards'
foreach ($Template in $Settings.TemplateList) {
try {
$Table = Get-CippTable -tablename 'templates'
@@ -20,10 +19,10 @@ function Invoke-CIPPStandardExConnector {
if ($Existing) {
$RequestParams | Add-Member -NotePropertyValue $Existing.Identity -NotePropertyName Identity -Force
$null = New-ExoRequest -tenantid $Tenant -cmdlet "Set-$($ConnectorType)connector" -cmdParams $RequestParams -useSystemMailbox $true
- Write-LogMessage -API $APINAME -tenant $Tenant -message "Updated transport rule for $($Tenant, $Settings)" -sev info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated transport rule for $($Tenant, $Settings)" -sev info
} else {
$null = New-ExoRequest -tenantid $Tenant -cmdlet "New-$($ConnectorType)connector" -cmdParams $RequestParams -useSystemMailbox $true
- Write-LogMessage -API $APINAME -tenant $Tenant -message "Created transport rule for $($Tenant, $Settings)" -sev info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created transport rule for $($Tenant, $Settings)" -sev info
}
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1
index 34cbf330fed0..bec921cca4cf 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardExcludedfileExt.ps1
@@ -69,6 +69,7 @@ function Invoke-CIPPStandardExcludedfileExt {
if ($Settings.alert -eq $true) {
if ($MissingExclusions) {
+ Write-StandardsAlert -message 'Exclude File Extensions from Syncing missing some extensions.' -object $MissingExclusions -tenant $Tenant -standardName 'ExcludedfileExt' -standardId $Settings.standardId
Write-LogMessage -API 'Standards' -tenant $tenant -message "Excluded synced files does not contain $($MissingExclusions -join ',')" -sev Alert
} else {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Excluded synced files contains $($Settings.ext)" -sev Info
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1
new file mode 100644
index 000000000000..795216b19e74
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardFormsPhishingProtection.ps1
@@ -0,0 +1,86 @@
+function Invoke-CIPPStandardFormsPhishingProtection {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) FormsPhishingProtection
+ .SYNOPSIS
+ (Label) Enable internal phishing protection for Forms
+ .DESCRIPTION
+ (Helptext) Enables internal phishing protection for Microsoft Forms to help prevent malicious forms from being created and shared within the organization. This feature scans forms created by internal users for potential phishing content and suspicious patterns.
+ (DocsDescription) Enables internal phishing protection for Microsoft Forms by setting the isInOrgFormsPhishingScanEnabled property to true. This security feature helps protect organizations from internal phishing attacks through Microsoft Forms by automatically scanning forms created by internal users for potential malicious content, suspicious links, and phishing patterns. When enabled, Forms will analyze form content and block or flag potentially dangerous forms before they can be shared within the organization.
+ .NOTES
+ CAT
+ Global Standards
+ TAG
+ "CIS"
+ "Security"
+ "PhishingProtection"
+ ADDEDCOMPONENT
+ IMPACT
+ Low Impact
+ ADDEDDATE
+ 2025-06-06
+ POWERSHELLEQUIVALENT
+ Graph API
+ RECOMMENDEDBY
+ "CIS"
+ "CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param ($Tenant, $Settings)
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'FormsPhishingProtection'
+
+ $Uri = 'https://graph.microsoft.com/beta/admin/forms/settings'
+
+ try {
+ $CurrentState = (New-GraphGetRequest -Uri $Uri -tenantid $Tenant).isInOrgFormsPhishingScanEnabled
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current Forms settings. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ Return
+ }
+
+ if ($Settings.remediate -eq $true) {
+ Write-Host 'Time to remediate Forms phishing protection'
+
+ # Check if phishing protection is already enabled
+ if ($CurrentState -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is already enabled.' -sev Info
+ } else {
+ # Enable Forms phishing protection
+ try {
+ $Body = @{
+ isInOrgFormsPhishingScanEnabled = $true
+ } | ConvertTo-Json -Depth 10 -Compress
+
+ $null = New-GraphPostRequest -Uri $Uri -Body $Body -TenantID $Tenant -Type PATCH
+
+ # Refresh the current state after enabling
+ $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully enabled Forms internal phishing protection.' -sev Info
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to enable Forms internal phishing protection. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($CurrentState -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is enabled.' -sev Info
+ } else {
+ Write-StandardsAlert -message 'Forms internal phishing protection is not enabled' -object $CurrentState -tenant $Tenant -standardName 'FormsPhishingProtection' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Forms internal phishing protection is not enabled.' -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ Set-CIPPStandardsCompareField -FieldName 'standards.FormsPhishingProtection' -FieldValue $CurrentState -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'FormsPhishingProtection' -FieldValue $CurrentState -StoreAs bool -Tenant $Tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1
index 1796526de3ef..9f93f49538c5 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardGlobalQuarantineNotifications.ps1
@@ -31,7 +31,7 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications {
param ($Tenant, $Settings)
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'GlobalQuarantineNotifications'
- $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' }
+ $CurrentState = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-QuarantinePolicy' -cmdParams @{ QuarantinePolicyType = 'GlobalQuarantinePolicy' } | Select-Object -ExcludeProperty '*data.type'
# This might take the cake on ugly hacky stuff i've done,
# but i just cant understand why the API returns the values it does and not a timespan like the equivalent powershell command does
@@ -51,8 +51,8 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications {
try {
$WantedState = [timespan]$NotificationInterval
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "GlobalQuarantineNotifications: Invalid NotificationInterval parameter set. Error: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "GlobalQuarantineNotifications: Invalid NotificationInterval parameter set. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
return
}
@@ -70,8 +70,8 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications {
}
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set Global Quarantine Notifications to $WantedState" -sev Info
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Global Quarantine Notifications to $WantedState. Error: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Global Quarantine Notifications to $WantedState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
}
@@ -81,14 +81,13 @@ function Invoke-CIPPStandardGlobalQuarantineNotifications {
if ($CurrentState.EndUserSpamNotificationFrequency -eq $WantedState) {
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Global Quarantine Notifications are set to the desired value of $WantedState" -sev Info
} else {
- $Object = $CurrentState | Select-Object -Property * -ExcludeProperty '*@odata.type'
- Write-StandardsAlert -message "Global Quarantine Notifications are not set to the desired value of $WantedState" -object $Object -tenant $Tenant -standardName 'GlobalQuarantineNotifications' -standardId $Settings.standardId
+ Write-StandardsAlert -message "Global Quarantine Notifications are not set to the desired value of $WantedState" -object $CurrentState -tenant $Tenant -standardName 'GlobalQuarantineNotifications' -standardId $Settings.standardId
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Global Quarantine Notifications are not set to the desired value of $WantedState" -sev Info
}
}
if ($Settings.report -eq $true) {
- $notificationInterval = @{ NotificationInterval = "$(($CurrentState.EndUserSpamNotificationFrequency).totalHours) hours" }
+ $notificationInterval = @{ NotificationInterval = "$(($CurrentState.EndUserSpamNotificationFrequency).TotalHours) hours" }
$ReportState = $CurrentState.EndUserSpamNotificationFrequency -eq $WantedState ? $true : $notificationInterval
Set-CIPPStandardsCompareField -FieldName 'standards.GlobalQuarantineNotifications' -FieldValue $ReportState -Tenant $Tenant
Add-CIPPBPAField -FieldName 'GlobalQuarantineNotificationsSet' -FieldValue [string]$CurrentState.EndUserSpamNotificationFrequency -StoreAs string -Tenant $Tenant
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1
index fca1c64d74ed..6f57d05f687c 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailboxRecipientLimits.ps1
@@ -23,6 +23,10 @@ function Invoke-CIPPStandardMailboxRecipientLimits {
Set-Mailbox -RecipientLimits
RECOMMENDEDBY
"CIPP"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
#>
param($Tenant, $Settings)
@@ -57,57 +61,69 @@ function Invoke-CIPPStandardMailboxRecipientLimits {
$Mailboxes = New-ExoBulkRequest -tenantid $Tenant -cmdletArray $Requests
- # Process mailboxes and categorize them based on their plan limits
- $MailboxResults = $Mailboxes | ForEach-Object {
- $Mailbox = $_
- $Plan = $MailboxPlanLookup[$Mailbox.MailboxPlanId]
-
- if ($Plan) {
- $PlanMaxRecipients = $Plan.MaxRecipientsPerMessage
-
- # If mailbox has "Unlimited" set but has a plan, use the plan's limit as the current limit
- $CurrentLimit = if ($Mailbox.RecipientLimits -eq 'Unlimited') {
- $PlanMaxRecipients
- }
- else {
- $Mailbox.RecipientLimits
+ # Skip processing entirely if no mailboxes returned - most performant approach
+ $MailboxResults = @()
+ $MailboxesToUpdate = @()
+ $MailboxesWithPlanIssues = @()
+
+ if ($null -ne $Mailboxes -and @($Mailboxes).Count -gt 0) {
+ # Process mailboxes and categorize them based on their plan limits
+ $MailboxResults = @($Mailboxes) | ForEach-Object {
+ $Mailbox = $_
+
+ # Safe hashtable lookup - check if MailboxPlanId exists and is not null
+ $Plan = $null
+ if ($Mailbox.MailboxPlanId -and $MailboxPlanLookup.ContainsKey($Mailbox.MailboxPlanId)) {
+ $Plan = $MailboxPlanLookup[$Mailbox.MailboxPlanId]
}
-
- if ($Settings.RecipientLimit -gt $PlanMaxRecipients) {
- [PSCustomObject]@{
- Type = 'PlanIssue'
- Mailbox = $Mailbox
- CurrentLimit = $CurrentLimit
- PlanLimit = $PlanMaxRecipients
- PlanName = $Plan.DisplayName
+
+ if ($Plan) {
+ $PlanMaxRecipients = $Plan.MaxRecipientsPerMessage
+
+ # If mailbox has "Unlimited" set but has a plan, use the plan's limit as the current limit
+ $CurrentLimit = if ($Mailbox.RecipientLimits -eq 'Unlimited') {
+ $PlanMaxRecipients
+ }
+ else {
+ $Mailbox.RecipientLimits
+ }
+
+ if ($Settings.RecipientLimit -gt $PlanMaxRecipients) {
+ [PSCustomObject]@{
+ Type = 'PlanIssue'
+ Mailbox = $Mailbox
+ CurrentLimit = $CurrentLimit
+ PlanLimit = $PlanMaxRecipients
+ PlanName = $Plan.DisplayName
+ }
+ }
+ elseif ($CurrentLimit -ne $Settings.RecipientLimit) {
+ [PSCustomObject]@{
+ Type = 'ToUpdate'
+ Mailbox = $Mailbox
+ }
}
}
- elseif ($CurrentLimit -ne $Settings.RecipientLimit) {
+ elseif ($Mailbox.RecipientLimits -ne $Settings.RecipientLimit) {
[PSCustomObject]@{
Type = 'ToUpdate'
Mailbox = $Mailbox
}
}
}
- elseif ($Mailbox.RecipientLimits -ne $Settings.RecipientLimit) {
+
+ # Separate mailboxes into their respective categories only if we have results
+ $MailboxesToUpdate = $MailboxResults | Where-Object { $_.Type -eq 'ToUpdate' } | Select-Object -ExpandProperty Mailbox
+ $MailboxesWithPlanIssues = $MailboxResults | Where-Object { $_.Type -eq 'PlanIssue' } | ForEach-Object {
[PSCustomObject]@{
- Type = 'ToUpdate'
- Mailbox = $Mailbox
+ Identity = $_.Mailbox.Identity
+ CurrentLimit = $_.CurrentLimit
+ PlanLimit = $_.PlanLimit
+ PlanName = $_.PlanName
}
}
}
- # Separate mailboxes into their respective categories
- $MailboxesToUpdate = $MailboxResults | Where-Object { $_.Type -eq 'ToUpdate' } | Select-Object -ExpandProperty Mailbox
- $MailboxesWithPlanIssues = $MailboxResults | Where-Object { $_.Type -eq 'PlanIssue' } | ForEach-Object {
- [PSCustomObject]@{
- Identity = $_.Mailbox.Identity
- CurrentLimit = $_.CurrentLimit
- PlanLimit = $_.PlanLimit
- PlanName = $_.PlanName
- }
- }
-
# Remediation
if ($Settings.remediate -eq $true) {
if ($MailboxesWithPlanIssues.Count -gt 0) {
@@ -119,6 +135,20 @@ function Invoke-CIPPStandardMailboxRecipientLimits {
if ($MailboxesToUpdate.Count -gt 0) {
try {
+ # Create detailed log data for audit trail
+ $MailboxChanges = $MailboxesToUpdate | ForEach-Object {
+ $CurrentLimit = if ($_.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $_.RecipientLimits }
+ @{
+ Identity = $_.Identity
+ DisplayName = $_.DisplayName
+ PrimarySmtpAddress = $_.PrimarySmtpAddress
+ CurrentLimit = $CurrentLimit
+ NewLimit = $Settings.RecipientLimit
+ }
+ }
+
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updating recipient limits to $($Settings.RecipientLimit) for $($MailboxesToUpdate.Count) mailboxes" -sev Info -LogData $MailboxChanges
+
# Create batch requests for mailbox updates
$UpdateRequests = $MailboxesToUpdate | ForEach-Object {
@{
@@ -134,7 +164,7 @@ function Invoke-CIPPStandardMailboxRecipientLimits {
# Execute batch update
$null = New-ExoBulkRequest -tenantid $Tenant -cmdletArray $UpdateRequests
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set recipient limits to $($Settings.RecipientLimit) for $($MailboxesToUpdate.Count) mailboxes" -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied recipient limits to $($MailboxesToUpdate.Count) mailboxes" -sev Info
}
catch {
$ErrorMessage = Get-CippException -Exception $_
@@ -152,12 +182,60 @@ function Invoke-CIPPStandardMailboxRecipientLimits {
Write-LogMessage -API 'Standards' -tenant $Tenant -message "All mailboxes have the correct recipient limit of $($Settings.RecipientLimit)" -sev Info
}
else {
- $AlertMessage = "Found $($MailboxesToUpdate.Count) mailboxes with incorrect recipient limits"
+ # Create structured alert data
+ $AlertData = @{
+ RequestedLimit = $Settings.RecipientLimit
+ MailboxesToUpdate = @()
+ MailboxesWithPlanIssues = @()
+ }
+
+ # Use Generic List for efficient object collection
+ $AlertObjects = [System.Collections.Generic.List[Object]]::new()
+
+ # Add mailboxes that need updating
+ if ($MailboxesToUpdate.Count -gt 0) {
+ $AlertData.MailboxesToUpdate = $MailboxesToUpdate | ForEach-Object {
+ $CurrentLimit = if ($_.RecipientLimits -eq 'Unlimited') { 'Unlimited' } else { $_.RecipientLimits }
+ @{
+ Identity = $_.Identity
+ DisplayName = $_.DisplayName
+ PrimarySmtpAddress = $_.PrimarySmtpAddress
+ CurrentLimit = $CurrentLimit
+ RequiredLimit = $Settings.RecipientLimit
+ }
+ }
+ # Add to alert objects list efficiently
+ foreach ($Mailbox in $MailboxesToUpdate) {
+ $AlertObjects.Add($Mailbox)
+ }
+ }
+
+ # Add mailboxes with plan issues
if ($MailboxesWithPlanIssues.Count -gt 0) {
- $AlertMessage += " and $($MailboxesWithPlanIssues.Count) mailboxes where the requested limit exceeds their mailbox plan limit"
+ $AlertData.MailboxesWithPlanIssues = $MailboxesWithPlanIssues | ForEach-Object {
+ @{
+ Identity = $_.Identity
+ CurrentLimit = $_.CurrentLimit
+ PlanLimit = $_.PlanLimit
+ PlanName = $_.PlanName
+ RequestedLimit = $Settings.RecipientLimit
+ }
+ }
+ # Add to alert objects list efficiently
+ foreach ($Mailbox in $MailboxesWithPlanIssues) {
+ $AlertObjects.Add($Mailbox)
+ }
+ }
+
+ # Build alert message efficiently
+ $AlertMessage = if ($MailboxesWithPlanIssues.Count -gt 0) {
+ "Found $($MailboxesToUpdate.Count) mailboxes with incorrect recipient limits and $($MailboxesWithPlanIssues.Count) mailboxes where the requested limit exceeds their mailbox plan limit"
+ } else {
+ "Found $($MailboxesToUpdate.Count) mailboxes with incorrect recipient limits"
}
- Write-StandardsAlert -message $AlertMessage -object ($MailboxesToUpdate + $MailboxesWithPlanIssues) -tenant $Tenant -standardName 'MailboxRecipientLimits' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info
+
+ Write-StandardsAlert -message $AlertMessage -object $AlertObjects.ToArray() -tenant $Tenant -standardName 'MailboxRecipientLimits' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info -LogData $AlertData
}
}
@@ -177,4 +255,4 @@ function Invoke-CIPPStandardMailboxRecipientLimits {
}
Set-CIPPStandardsCompareField -FieldName 'standards.MailboxRecipientLimits' -FieldValue $FieldValue -Tenant $Tenant
}
-}
\ No newline at end of file
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1
index 2322f74afe7f..a52afe311852 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPWcompanionAppAllowedState.ps1
@@ -14,7 +14,7 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState {
Entra (AAD) Standards
TAG
ADDEDCOMPONENT
- {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.PWcompanionAppAllowedState.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]}
+ {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.PWcompanionAppAllowedState.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"},{"label":"Microsoft managed","value":"default"}]}
IMPACT
Low Impact
ADDEDDATE
@@ -30,32 +30,31 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState {
param($Tenant, $Settings)
- $authenticatorFeaturesState = (New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator')
- $authState = if ($authenticatorFeaturesState.featureSettings.companionAppAllowedState.state -eq 'enabled') { $true } else { $false }
-
-
+ $AuthenticatorFeaturesState = (New-GraphGetRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator')
# Get state value using null-coalescing operator
- $state = $Settings.state.value ? $Settings.state.value : $settings.state
- $authState = if ($authenticatorFeaturesState.featureSettings.companionAppAllowedState.state -eq $state) { $true } else { $false }
+ $CurrentState = $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState.state
+ $WantedState = $Settings.state.value ? $Settings.state.value : $settings.state
+ $AuthStateCorrect = if ($CurrentState -eq $WantedState) { $true } else { $false }
# Input validation
- if (([string]::IsNullOrWhiteSpace($state) -or $state -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) {
+ if (([string]::IsNullOrWhiteSpace($WantedState) -or $WantedState -eq 'Select a value') -and ($Settings.remediate -eq $true -or $Settings.alert -eq $true)) {
Write-LogMessage -API 'Standards' -tenant $Tenant -message 'PWcompanionAppAllowedState: Invalid state parameter set' -sev Error
Return
}
If ($Settings.remediate -eq $true) {
+ Write-Host "Remediating PWcompanionAppAllowedState for tenant $Tenant to $WantedState"
- if ($authenticatorFeaturesState.featureSettings.companionAppAllowedState.state -eq $state) {
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is already set to the desired state of $state." -sev Info
+ if ($AuthStateCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is already set to the desired state of $WantedState." -sev Info
} else {
try {
# Remove number matching from featureSettings because this is now Microsoft enforced and shipping it returns an error
- $authenticatorFeaturesState.featureSettings.PSObject.Properties.Remove('numberMatchingRequiredState')
+ $AuthenticatorFeaturesState.featureSettings.PSObject.Properties.Remove('numberMatchingRequiredState')
# Define feature body
$featureBody = @{
- state = $state
+ state = $WantedState
includeTarget = [PSCustomObject]@{
targetType = 'group'
id = 'all_users'
@@ -65,33 +64,33 @@ function Invoke-CIPPStandardPWcompanionAppAllowedState {
id = '00000000-0000-0000-0000-000000000000'
}
}
- $authenticatorFeaturesState.featureSettings.companionAppAllowedState = $featureBody
- $body = ConvertTo-Json -Depth 3 -Compress -InputObject $authenticatorFeaturesState
+ $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState = $featureBody
+ $body = ConvertTo-Json -Depth 3 -Compress -InputObject $AuthenticatorFeaturesState
$null = (New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' -Type patch -Body $body -ContentType 'application/json')
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set companionAppAllowedState to $state." -sev Info
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set companionAppAllowedState to $WantedState." -sev Info
} catch {
$ErrorMessage = Get-CippExceptionMessage -Exception $_
- Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set companionAppAllowedState to $state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set companionAppAllowedState to $WantedState. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
}
}
if ($Settings.alert -eq $true) {
- if ($authState) {
- Write-LogMessage -API 'Standards' -tenant $Tenant -message 'companionAppAllowedState is enabled.' -sev Info
+ if ($AuthStateCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is set to $WantedState." -sev Info
} else {
- Write-StandardsAlert -message 'companionAppAllowedState is not enabled' -object $authenticatorFeaturesState -tenant $Tenant -standardName 'PWcompanionAppAllowedState' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $Tenant -message 'companionAppAllowedState is not enabled.' -sev Info
+ Write-StandardsAlert -message "companionAppAllowedState is not set to $WantedState. Current state is $CurrentState." -object $AuthenticatorFeaturesState -tenant $Tenant -standardName 'PWcompanionAppAllowedState' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "companionAppAllowedState is not set to $WantedState. Current state is $CurrentState." -sev Info
}
}
if ($Settings.report -eq $true) {
- Add-CIPPBPAField -FieldName 'companionAppAllowedState' -FieldValue $authState -StoreAs bool -Tenant $Tenant
- if ($authState) {
+ Add-CIPPBPAField -FieldName 'companionAppAllowedState' -FieldValue $AuthStateCorrect -StoreAs bool -Tenant $Tenant
+ if ($AuthStateCorrect -eq $true) {
$FieldValue = $true
} else {
- $FieldValue = $authenticatorFeaturesState.featureSettings.companionAppAllowedState
+ $FieldValue = $AuthenticatorFeaturesState.featureSettings.companionAppAllowedState
}
Set-CIPPStandardsCompareField -FieldName 'standards.PWcompanionAppAllowedState' -FieldValue $FieldValue -Tenant $Tenant
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1
index bd3e581bbd97..a801f3a32244 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPasswordExpireDisabled.ps1
@@ -34,7 +34,8 @@ function Invoke-CIPPStandardPasswordExpireDisabled {
param($Tenant, $Settings)
$GraphRequest = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/domains' -tenantid $Tenant
- $DomainsWithoutPassExpire = $GraphRequest | Where-Object -Property passwordValidityPeriodInDays -NE '2147483647'
+ $DomainsWithoutPassExpire = $GraphRequest |
+ Where-Object { $_.isVerified -eq $true -and $_.passwordValidityPeriodInDays -ne 2147483647 }
if ($Settings.remediate -eq $true) {
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1
index 088cfe4f5ffa..782d847ec8a1 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineRequestAlert.ps1
@@ -78,7 +78,9 @@ function Invoke-CIPPStandardQuarantineRequestAlert {
if ($StateIsCorrect -eq $true) {
Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Quarantine Request Alert is enabled' -sev Info
} else {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Quarantine Request Alert is disabled' -sev Info
+ $Message = 'Quarantine Request Alert is not enabled.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'QuarantineRequestAlerts' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -sev Info
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1
index 566754a66bdc..4f76c8f4378c 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardQuarantineTemplate.ps1
@@ -52,6 +52,9 @@ function Invoke-CIPPStandardQuarantineTemplate {
try {
# Create hashtable with desired Quarantine Setting
$EndUserQuarantinePermissions = @{
+ # ViewHeader and Download are set to false because the value 0 or 1 does nothing per Microsoft documentation
+ PermissionToViewHeader = $false
+ PermissionToDownload = $false
PermissionToBlockSender = $Policy.PermissionToBlockSender
PermissionToDelete = $Policy.PermissionToDelete
PermissionToPreview = $Policy.PermissionToPreview
@@ -64,7 +67,7 @@ function Invoke-CIPPStandardQuarantineTemplate {
if ($Policy.displayName.value -in $CurrentPolicies.Name) {
#Get the current policy and convert EndUserQuarantinePermissions from string to hashtable for compare
$ExistingPolicy = $CurrentPolicies | Where-Object -Property Name -eq $Policy.displayName.value
- $ExistingPolicyEndUserQuarantinePermissions = Convert-QuarantinePermissionsValue @EndUserQuarantinePermissions -ErrorAction Stop
+ $ExistingPolicyEndUserQuarantinePermissions = Convert-QuarantinePermissionsValue -InputObject $ExistingPolicy.EndUserQuarantinePermissions -ErrorAction Stop
#Compare the current policy
$StateIsCorrect = ($ExistingPolicy.Name -eq $Policy.displayName.value) -and
@@ -180,9 +183,10 @@ function Invoke-CIPPStandardQuarantineTemplate {
}
if ($true -in $Settings.report) {
- # This could do with an improvement. But will work for now or else reporting could be disabled for now
foreach ($Policy in $CompareList | Where-Object -Property report -EQ $true) {
- Set-CIPPStandardsCompareField -FieldName "standards.QuarantineTemplate" -FieldValue $Policy.StateIsCorrect -TenantFilter $Tenant
+ # Convert displayName to hex to avoid invalid characters "/, \, #, ?" which are not allowed in RowKey, but "\, #, ?" can be used in quarantine displayName
+ $HexName = -join ($Policy.displayName.ToCharArray() | ForEach-Object { '{0:X2}' -f [int][char]$_ })
+ Set-CIPPStandardsCompareField -FieldName "standards.QuarantineTemplate.$HexName" -FieldValue $Policy.StateIsCorrect -TenantFilter $Tenant
}
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1
new file mode 100644
index 000000000000..d523b3d83174
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardRestrictThirdPartyStorageServices.ps1
@@ -0,0 +1,93 @@
+function Invoke-CIPPStandardRestrictThirdPartyStorageServices {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) RestrictThirdPartyStorageServices
+ .SYNOPSIS
+ (Label) Restrict third-party storage services in Microsoft 365 on the web
+ .DESCRIPTION
+ (Helptext) Restricts third-party storage services in Microsoft 365 on the web by managing the Microsoft 365 on the web service principal. This disables integrations with services like Dropbox, Google Drive, Box, and other third-party storage providers.
+ (DocsDescription) Third-party storage can be enabled for users in Microsoft 365, allowing them to store and share documents using services such as Dropbox, alongside OneDrive and team sites. This standard ensures Microsoft 365 on the web third-party storage services are restricted by creating and disabling the Microsoft 365 on the web service principal (appId: c1f33bc0-bdb4-4248-ba9b-096807ddb43e). By using external storage services an organization may increase the risk of data breaches and unauthorized access to confidential information. Additionally, third-party services may not adhere to the same security standards as the organization, making it difficult to maintain data privacy and security. Impact is highly dependent upon current practices - if users do not use other storage providers, then minimal impact is likely. However, if users regularly utilize providers outside of the tenant this will affect their ability to continue to do so.
+ .NOTES
+ CAT
+ Global Standards
+ TAG
+ "CIS"
+ ADDEDCOMPONENT
+ IMPACT
+ Medium Impact
+ ADDEDDATE
+ 2025-06-06
+ POWERSHELLEQUIVALENT
+ New-MgServicePrincipal and Update-MgServicePrincipal
+ RECOMMENDEDBY
+ "CIS"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param ($Tenant, $Settings)
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'RestrictThirdPartyStorageServices'
+
+ $AppId = 'c1f33bc0-bdb4-4248-ba9b-096807ddb43e'
+ $Uri = "https://graph.microsoft.com/beta/servicePrincipals?`$filter=appId eq '$AppId'"
+
+ try {
+ $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant | Select-Object displayName, accountEnabled, appId
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current state for Microsoft 365 on the web service principal. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ Return
+ }
+
+ if ($Settings.remediate -eq $true) {
+ Write-Host 'Time to remediate third-party storage services restriction'
+
+ # Check if service principal is already disabled
+ if ($CurrentState.accountEnabled -eq $false) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are already restricted (service principal is disabled).' -sev Info
+ } else {
+ # Disable the service principal to restrict third-party storage services
+ try {
+ $DisableBody = @{
+ accountEnabled = $false
+ } | ConvertTo-Json -Depth 10 -Compress
+
+ # Normal /servicePrincipal/AppId does not find the service principal, so gotta use the Upsert method. Also handles if the service principal does not exist nicely.
+ # https://learn.microsoft.com/en-us/graph/api/serviceprincipal-upsert?view=graph-rest-beta&tabs=http
+ $UpdateUri = "https://graph.microsoft.com/beta/servicePrincipals(appId='$AppId')"
+ $null = New-GraphPostRequest -Uri $UpdateUri -Body $DisableBody -TenantID $Tenant -Type PATCH
+
+ # Refresh the current state after disabling
+ $CurrentState = New-GraphGetRequest -Uri $Uri -tenantid $Tenant | Select-Object displayName, accountEnabled, appId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Successfully restricted third-party storage services in Microsoft 365 on the web.' -sev Info
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to restrict third-party storage services. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($CurrentState.accountEnabled -eq $false) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are restricted (service principal is disabled).' -sev Info
+ } else {
+ Write-StandardsAlert -message 'Third-party storage services are not restricted in Microsoft 365 on the web' -object $CurrentState -tenant $Tenant -standardName 'RestrictThirdPartyStorageServices' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Third-party storage services are not restricted.' -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ if ($null -eq $CurrentState.accountEnabled -or $CurrentState.accountEnabled -eq $true) {
+ Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -FieldValue $false -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue $false -StoreAs bool -Tenant $Tenant
+ } else {
+ $CorrectState = $CurrentState.accountEnabled -eq $false ? $true : $false
+ Set-CIPPStandardsCompareField -FieldName 'standards.RestrictThirdPartyStorageServices' -FieldValue $CorrectState -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'ThirdPartyStorageServicesRestricted' -FieldValue $CorrectState -StoreAs bool -Tenant $Tenant
+ }
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1
index 51d24c66deff..016f4697e400 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPAzureB2B.ps1
@@ -58,7 +58,9 @@ function Invoke-CIPPStandardSPAzureB2B {
if ($StateIsCorrect -eq $true) {
Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Azure B2B is enabled' -Sev Info
} else {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Azure B2B is not enabled' -Sev Alert
+ $Message = 'SharePoint Azure B2B is not enabled.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPAzureB2B' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1
index b6d529cfeac5..e91952957966 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDirectSharing.ps1
@@ -39,7 +39,7 @@ function Invoke-CIPPStandardSPDirectSharing {
if ($Settings.remediate -eq $true) {
if ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Sharing Restriction is already enabled' -Sev Info
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Default Direct Sharing is already enabled' -Sev Info
} else {
$Properties = @{
DefaultSharingLinkType = 1
@@ -47,19 +47,21 @@ function Invoke-CIPPStandardSPDirectSharing {
try {
Get-CIPPSPOTenant -TenantFilter $Tenant | Set-CIPPSPOTenant -Properties $Properties
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully set the SharePoint Sharing Restriction to Direct' -Sev Info
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Successfully set the SharePoint Default Direct Sharing to Direct' -Sev Info
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set the SharePoint Sharing Restriction to Direct. Error: $ErrorMessage" -Sev Error
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Failed to set the SharePoint Default Direct Sharing to Direct. Error: $ErrorMessage" -Sev Error
}
}
}
if ($Settings.alert -eq $true) {
if ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Sharing Restriction is enabled' -Sev Info
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Direct Sharing is enabled' -Sev Info
} else {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Sharing Restriction is not enabled' -Sev Alert
+ $Message = 'SharePoint Default Direct Sharing is not enabled.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPDirectSharing' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1
index 3093949eceab..57a4f292367c 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisableLegacyWorkflows.ps1
@@ -57,9 +57,11 @@ function Invoke-CIPPStandardSPDisableLegacyWorkflows {
if ($Settings.alert -eq $true) {
if ($StateIsCorrect -eq $true) {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Workflows are disabled' -Sev Info
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'SharePoint Legacy Workflows are disabled' -Sev Info
} else {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Legacy Workflows are enabled' -Sev Info
+ $Message = 'SharePoint Legacy Workflows is not disabled.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPDisableLegacyWorkflows' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Info
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1
index 60278d1c2e11..c61ec77268fb 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPDisallowInfectedFiles.ps1
@@ -59,7 +59,9 @@ function Invoke-CIPPStandardSPDisallowInfectedFiles {
if ($StateIsCorrect -eq $true) {
Write-LogMessage -API 'Standards' -tenant $tenant -Message 'Downloading SharePoint infected files are disallowed.' -Sev Info
} else {
- Write-LogMessage -API 'Standards' -tenant $tenant -Message 'Downloading SharePoint infected files are allowed.' -Sev Alert
+ $Message = 'Downloading SharePoint infected files is not set to the desired value.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPDisallowInfectedFiles' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $tenant -Message $Message -Sev Alert
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1
index 7c0ed3fb7d4d..c26879a60c2b 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPEmailAttestation.ps1
@@ -66,7 +66,9 @@ function Invoke-CIPPStandardSPEmailAttestation {
if ($StateIsCorrect -eq $true) {
Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Re-authentication with verification code is restricted.' -Sev Info
} else {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'Re-authentication with verification code is not restricted.' -Sev Alert
+ $Message = 'Re-authentication with verification code is not set to the desired value.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPEmailAttestation' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1
index 7fc1b7fd1c14..7db6ed6a53e1 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSPExternalUserExpiration.ps1
@@ -61,7 +61,9 @@ function Invoke-CIPPStandardSPExternalUserExpiration {
if ($StateIsCorrect -eq $true) {
Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'External User Expiration is enabled' -Sev Info
} else {
- Write-LogMessage -API 'Standards' -Tenant $Tenant -Message 'External User Expiration is not enabled' -Sev Alert
+ $Message = 'External User Expiration is not set to the desired value.'
+ Write-StandardsAlert -message $Message -object $CurrentState -tenant $Tenant -standardName 'SPExternalUserExpiration' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -Tenant $Tenant -Message $Message -Sev Alert
}
}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1
new file mode 100644
index 000000000000..00d71e371b2b
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSafeLinksTemplatePolicy.ps1
@@ -0,0 +1,487 @@
+function Invoke-CIPPStandardSafeLinksTemplatePolicy {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) SafeLinksTemplatePolicy
+ .SYNOPSIS
+ (Label) SafeLinks Policy Template
+ .DESCRIPTION
+ (Helptext) Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.
+ (DocsDescription) Deploy and manage SafeLinks policy templates to protect against malicious URLs in emails and Office documents.
+ .NOTES
+ CAT
+ Templates
+ MULTIPLE
+ False
+ DISABLEDFEATURES
+ {"report":false,"warn":false,"remediate":false}
+ IMPACT
+ Medium Impact
+ ADDEDDATE
+ 2025-04-29
+ ADDEDCOMPONENT
+ {"type":"autoComplete","multiple":true,"creatable":false,"name":"standards.SafeLinksTemplatePolicy.TemplateIds","label":"Select SafeLinks Policy Templates","api":{"url":"/api/ListSafeLinksPolicyTemplates","labelField":"TemplateName","valueField":"GUID","queryKey":"ListSafeLinksPolicyTemplates"}}
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with settings: $($Settings | ConvertTo-Json -Compress)" -sev Debug
+
+ # Verify tenant has necessary license
+ if (-not (Test-MDOLicense -Tenant $Tenant -Settings $Settings)) {
+ return
+ }
+
+ # Normalize template list property
+ $TemplateList = Get-NormalizedTemplateList -Settings $Settings
+ if (-not $TemplateList) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "No templates selected for SafeLinks policy deployment" -sev Error
+ return
+ }
+
+ # Handle different modes
+ switch ($true) {
+ ($Settings.remediate -eq $true) {
+ Invoke-SafeLinksRemediation -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings
+ }
+ ($Settings.alert -eq $true) {
+ Invoke-SafeLinksAlert -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings
+ }
+ ($Settings.report -eq $true) {
+ Invoke-SafeLinksReport -Tenant $Tenant -TemplateList $TemplateList -Settings $Settings
+ }
+ }
+}
+
+function Test-MDOLicense {
+ param($Tenant, $Settings)
+
+ $ServicePlans = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/subscribedSkus?$select=servicePlans' -tenantid $Tenant
+ $ServicePlans = $ServicePlans.servicePlans.servicePlanName
+ $MDOLicensed = $ServicePlans -contains 'ATP_ENTERPRISE'
+
+ if (-not $MDOLicensed) {
+ $Message = 'Tenant does not have Microsoft Defender for Office 365 license'
+
+ if ($Settings.remediate -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks templates: $Message" -sev Error
+ }
+
+ if ($Settings.alert -eq $true) {
+ Write-StandardsAlert -message "SafeLinks templates could not be applied: $Message" -object $MDOLicensed -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks templates could not be applied: $Message" -sev Info
+ }
+
+ if ($Settings.report -eq $true) {
+ Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $false -StoreAs bool -Tenant $Tenant
+ Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $false -Tenant $Tenant
+ }
+
+ return $false
+ }
+
+ return $true
+}
+
+function Get-NormalizedTemplateList {
+ param($Settings)
+
+ if ($Settings.'standards.SafeLinksTemplatePolicy.TemplateIds') {
+ return $Settings.'standards.SafeLinksTemplatePolicy.TemplateIds'
+ }
+ elseif ($Settings.TemplateIds) {
+ return $Settings.TemplateIds
+ }
+
+ return $null
+}
+
+function Get-SafeLinksTemplateFromStorage {
+ param($TemplateId)
+
+ $Table = Get-CippTable -tablename 'templates'
+ $Filter = "PartitionKey eq 'SafeLinksTemplate' and RowKey eq '$TemplateId'"
+ $Template = Get-CIPPAzDataTableEntity @Table -Filter $Filter
+
+ if (-not $Template) {
+ throw "Template with ID $TemplateId not found"
+ }
+
+ return $Template.JSON | ConvertFrom-Json -ErrorAction Stop
+}
+
+function ConvertTo-SafeArray {
+ param($Field)
+
+ if ($null -eq $Field) { return @() }
+
+ $ResultList = [System.Collections.Generic.List[string]]::new()
+
+ if ($Field -is [array]) {
+ foreach ($item in $Field) {
+ if ($item -is [string]) {
+ $ResultList.Add($item)
+ }
+ elseif ($item.value) {
+ $ResultList.Add($item.value)
+ }
+ elseif ($item.userPrincipalName) {
+ $ResultList.Add($item.userPrincipalName)
+ }
+ elseif ($item.id) {
+ $ResultList.Add($item.id)
+ }
+ else {
+ $ResultList.Add($item.ToString())
+ }
+ }
+ return $ResultList.ToArray()
+ }
+
+ if ($Field -is [hashtable] -or $Field -is [PSCustomObject]) {
+ if ($Field.value) {
+ $ResultList.Add($Field.value)
+ return $ResultList.ToArray()
+ }
+ if ($Field.userPrincipalName) {
+ $ResultList.Add($Field.userPrincipalName)
+ return $ResultList.ToArray()
+ }
+ if ($Field.id) {
+ $ResultList.Add($Field.id)
+ return $ResultList.ToArray()
+ }
+ }
+
+ if ($Field -is [string]) {
+ $ResultList.Add($Field)
+ return $ResultList.ToArray()
+ }
+
+ $ResultList.Add($Field.ToString())
+ return $ResultList.ToArray()
+}
+
+function Get-ExistingSafeLinksObjects {
+ param($Tenant, $PolicyName, $RuleName)
+
+ $PolicyExists = $null
+ $RuleExists = $null
+
+ try {
+ $ExistingPolicies = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksPolicy' -useSystemMailbox $true
+ $PolicyExists = $ExistingPolicies | Where-Object { $_.Name -eq $PolicyName }
+ }
+ catch {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing policies: $($_.Exception.Message)" -sev Warning
+ }
+
+ try {
+ $ExistingRules = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-SafeLinksRule' -useSystemMailbox $true
+ $RuleExists = $ExistingRules | Where-Object { $_.Name -eq $RuleName }
+ }
+ catch {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to retrieve existing rules: $($_.Exception.Message)" -sev Warning
+ }
+
+ return @{
+ PolicyExists = $PolicyExists
+ RuleExists = $RuleExists
+ }
+}
+
+function New-SafeLinksPolicyParameters {
+ param($Template)
+
+ $PolicyMappings = @{
+ 'EnableSafeLinksForEmail' = 'EnableSafeLinksForEmail'
+ 'EnableSafeLinksForTeams' = 'EnableSafeLinksForTeams'
+ 'EnableSafeLinksForOffice' = 'EnableSafeLinksForOffice'
+ 'TrackClicks' = 'TrackClicks'
+ 'AllowClickThrough' = 'AllowClickThrough'
+ 'ScanUrls' = 'ScanUrls'
+ 'EnableForInternalSenders' = 'EnableForInternalSenders'
+ 'DeliverMessageAfterScan' = 'DeliverMessageAfterScan'
+ 'DisableUrlRewrite' = 'DisableUrlRewrite'
+ 'AdminDisplayName' = 'AdminDisplayName'
+ 'CustomNotificationText' = 'CustomNotificationText'
+ 'EnableOrganizationBranding' = 'EnableOrganizationBranding'
+ }
+
+ $PolicyParams = @{}
+
+ foreach ($templateKey in $PolicyMappings.Keys) {
+ if ($null -ne $Template.$templateKey) {
+ $PolicyParams[$PolicyMappings[$templateKey]] = $Template.$templateKey
+ }
+ }
+
+ $DoNotRewriteUrls = ConvertTo-SafeArray -Field $Template.DoNotRewriteUrls
+ if ($DoNotRewriteUrls.Count -gt 0) {
+ $PolicyParams['DoNotRewriteUrls'] = $DoNotRewriteUrls
+ }
+
+ return $PolicyParams
+}
+
+function New-SafeLinksRuleParameters {
+ param($Template)
+
+ $RuleParams = @{}
+
+ # Basic rule parameters
+ if ($null -ne $Template.Priority) { $RuleParams['Priority'] = $Template.Priority }
+ if ($null -ne $Template.Description) { $RuleParams['Comments'] = $Template.Description }
+ if ($null -ne $Template.TemplateDescription) { $RuleParams['Comments'] = $Template.TemplateDescription }
+
+ # Array-based rule parameters
+ $ArrayMappings = @{
+ 'SentTo' = ConvertTo-SafeArray -Field $Template.SentTo
+ 'SentToMemberOf' = ConvertTo-SafeArray -Field $Template.SentToMemberOf
+ 'RecipientDomainIs' = ConvertTo-SafeArray -Field $Template.RecipientDomainIs
+ 'ExceptIfSentTo' = ConvertTo-SafeArray -Field $Template.ExceptIfSentTo
+ 'ExceptIfSentToMemberOf' = ConvertTo-SafeArray -Field $Template.ExceptIfSentToMemberOf
+ 'ExceptIfRecipientDomainIs' = ConvertTo-SafeArray -Field $Template.ExceptIfRecipientDomainIs
+ }
+
+ foreach ($paramName in $ArrayMappings.Keys) {
+ if ($ArrayMappings[$paramName].Count -gt 0) {
+ $RuleParams[$paramName] = $ArrayMappings[$paramName]
+ }
+ }
+
+ return $RuleParams
+}
+
+function Set-SafeLinksRuleState {
+ param($Tenant, $RuleName, $State)
+
+ if ($null -eq $State) { return }
+
+ $IsEnabled = switch ($State) {
+ "Enabled" { $true }
+ "Disabled" { $false }
+ $true { $true }
+ $false { $false }
+ default { $null }
+ }
+
+ if ($null -ne $IsEnabled) {
+ $Cmdlet = $IsEnabled ? 'Enable-SafeLinksRule' : 'Disable-SafeLinksRule'
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet $Cmdlet -cmdParams @{ Identity = $RuleName } -useSystemMailbox $true
+ return $IsEnabled ? "enabled" : "disabled"
+ }
+
+ return $null
+}
+
+function Invoke-SafeLinksRemediation {
+ param($Tenant, $TemplateList, $Settings)
+
+ $OverallSuccess = $true
+ $TemplateResults = @{}
+
+ foreach ($TemplateItem in $TemplateList) {
+ $TemplateId = $TemplateItem.value
+
+ try {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Processing SafeLinks template with ID: $TemplateId" -sev Info
+
+ # Get template from storage
+ $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId
+
+ $PolicyName = $Template.PolicyName ?? $Template.Name
+ $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule"
+
+ # Check existing objects
+ $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName
+
+ $ActionsTaken = [System.Collections.Generic.List[string]]::new()
+
+ # Process Policy
+ $PolicyParams = New-SafeLinksPolicyParameters -Template $Template
+
+ if ($ExistingObjects.PolicyExists) {
+ # Update existing policy to keep it in line
+ $PolicyParams['Identity'] = $PolicyName
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true
+ $ActionsTaken.Add("Updated SafeLinks policy '$PolicyName'")
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks policy '$PolicyName'" -sev Info
+ }
+ else {
+ # Create new policy
+ $PolicyParams['Name'] = $PolicyName
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksPolicy' -cmdParams $PolicyParams -useSystemMailbox $true
+ $ActionsTaken.Add("Created new SafeLinks policy '$PolicyName'")
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks policy '$PolicyName'" -sev Info
+ }
+
+ # Process Rule
+ $RuleParams = New-SafeLinksRuleParameters -Template $Template
+
+ if ($ExistingObjects.RuleExists) {
+ # Update existing rule to keep it in line
+ $RuleParams['Identity'] = $RuleName
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true
+ $ActionsTaken.Add("Updated SafeLinks rule '$RuleName'")
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Updated SafeLinks rule '$RuleName'" -sev Info
+ }
+ else {
+ # Create new rule
+ $RuleParams['Name'] = $RuleName
+ $RuleParams['SafeLinksPolicy'] = $PolicyName
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'New-SafeLinksRule' -cmdParams $RuleParams -useSystemMailbox $true
+ $ActionsTaken.Add("Created new SafeLinks rule '$RuleName'")
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created new SafeLinks rule '$RuleName'" -sev Info
+ }
+
+ # Set rule state
+ $StateResult = Set-SafeLinksRuleState -Tenant $Tenant -RuleName $RuleName -State $Template.State
+ if ($StateResult) {
+ $ActionsTaken.Add("Rule $StateResult")
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "SafeLinks rule '$RuleName' $StateResult" -sev Info
+ }
+
+ $TemplateResults[$TemplateId] = @{
+ Success = $true
+ ActionsTaken = $ActionsTaken.ToArray()
+ TemplateName = $Template.TemplateName ?? $Template.Name
+ PolicyName = $PolicyName
+ RuleName = $RuleName
+ }
+
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied SafeLinks template '$($Template.TemplateName ?? $Template.Name)'" -sev Info
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $TemplateResults[$TemplateId] = @{
+ Success = $false
+ Message = $ErrorMessage
+ TemplateName = $Template.TemplateName ?? $Template.Name ?? "Unknown"
+ }
+ $OverallSuccess = $false
+
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to apply SafeLinks template ID $TemplateId : $ErrorMessage" -sev Error
+ }
+ }
+
+ # Report overall results
+ if ($OverallSuccess) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully applied all SafeLinks templates" -sev Info
+ }
+ else {
+ $SuccessCount = ($TemplateResults.Values | Where-Object { $_.Success -eq $true }).Count
+ $TotalCount = $TemplateList.Count
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Applied $SuccessCount out of $TotalCount SafeLinks templates" -sev Info
+ }
+}
+
+function Invoke-SafeLinksAlert {
+ param($Tenant, $TemplateList, $Settings)
+
+ $AllTemplatesApplied = $true
+ $AlertMessages = [System.Collections.Generic.List[string]]::new()
+
+ foreach ($TemplateItem in $TemplateList) {
+ $TemplateId = $TemplateItem.value
+
+ try {
+ $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId
+ $PolicyName = $Template.PolicyName ?? $Template.Name
+ $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule"
+
+ $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName
+
+ if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) {
+ $AllTemplatesApplied = $false
+ $Status = "SafeLinks template '$($Template.TemplateName ?? $Template.Name)' is not applied"
+
+ if (-not $ExistingObjects.PolicyExists) {
+ $Status = "$Status - policy '$PolicyName' does not exist"
+ }
+
+ if (-not $ExistingObjects.RuleExists) {
+ $Status = "$Status - rule '$RuleName' does not exist"
+ }
+
+ $AlertMessages.Add($Status)
+ }
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $AlertMessages.Add("Failed to check template with ID $TemplateId : $ErrorMessage")
+ $AllTemplatesApplied = $false
+ }
+ }
+
+ if ($AllTemplatesApplied) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "All SafeLinks templates are correctly applied" -sev Info
+ }
+ else {
+ $AlertMessage = "One or more SafeLinks templates are not correctly applied: " + ($AlertMessages.ToArray() -join " | ")
+ Write-StandardsAlert -message $AlertMessage -object @{
+ Templates = $TemplateList
+ Issues = $AlertMessages.ToArray()
+ } -tenant $Tenant -standardName 'SafeLinksTemplatePolicy' -standardId $Settings.standardId
+
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message $AlertMessage -sev Info
+ }
+}
+
+function Invoke-SafeLinksReport {
+ param($Tenant, $TemplateList, $Settings)
+
+ $AllTemplatesApplied = $true
+ $ReportResults = @{}
+
+ foreach ($TemplateItem in $TemplateList) {
+ $TemplateId = $TemplateItem.value
+
+ try {
+ $Template = Get-SafeLinksTemplateFromStorage -TemplateId $TemplateId
+ $PolicyName = $Template.PolicyName ?? $Template.Name
+ $RuleName = $Template.RuleName ?? "$($PolicyName)_Rule"
+
+ $ExistingObjects = Get-ExistingSafeLinksObjects -Tenant $Tenant -PolicyName $PolicyName -RuleName $RuleName
+
+ $ReportResults[$TemplateId] = @{
+ Success = ($ExistingObjects.PolicyExists -and $ExistingObjects.RuleExists)
+ TemplateName = $Template.TemplateName ?? $Template.Name
+ PolicyName = $PolicyName
+ RuleName = $RuleName
+ PolicyExists = [bool]$ExistingObjects.PolicyExists
+ RuleExists = [bool]$ExistingObjects.RuleExists
+ }
+
+ if (-not $ExistingObjects.PolicyExists -or -not $ExistingObjects.RuleExists) {
+ $AllTemplatesApplied = $false
+ }
+ }
+ catch {
+ $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
+ $ReportResults[$TemplateId] = @{
+ Success = $false
+ Message = $ErrorMessage
+ }
+ $AllTemplatesApplied = $false
+ }
+ }
+
+ Add-CIPPBPAField -FieldName 'SafeLinksTemplatePolicy' -FieldValue $AllTemplatesApplied -StoreAs bool -Tenant $Tenant
+
+ if ($AllTemplatesApplied) {
+ Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue $true -Tenant $Tenant
+ }
+ else {
+ Set-CIPPStandardsCompareField -FieldName 'standards.SafeLinksTemplatePolicy' -FieldValue @{
+ TemplateResults = $ReportResults
+ ProcessedTemplates = $TemplateList.Count
+ SuccessfulTemplates = ($ReportResults.Values | Where-Object { $_.Success -eq $true }).Count
+ } -Tenant $Tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1
index 344f83bc00bc..e9e3daa4a36b 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSendReceiveLimitTenant.ps1
@@ -37,20 +37,28 @@ function Invoke-CIPPStandardSendReceiveLimitTenant {
return
}
- # Input validation
if ([Int32]$Settings.ReceiveLimit -lt 1 -or [Int32]$Settings.ReceiveLimit -gt 150) {
Write-LogMessage -API 'Standards' -tenant $tenant -message 'SendReceiveLimitTenant: Invalid ReceiveLimit parameter set' -sev Error
return
}
-
$AllMailBoxPlans = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-MailboxPlan' | Select-Object DisplayName, MaxSendSize, MaxReceiveSize, GUID
- $MaxSendSize = [int64]"$($Settings.SendLimit)MB"
- $MaxReceiveSize = [int64]"$($Settings.ReceiveLimit)MB"
+ $MaxSendSize = $Settings.SendLimit * 1MB
+ $MaxReceiveSize = $Settings.ReceiveLimit * 1MB
$NotSetCorrectly = foreach ($MailboxPlan in $AllMailBoxPlans) {
- $PlanMaxSendSize = [int64]($MailboxPlan.MaxSendSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '')
- $PlanMaxReceiveSize = [int64]($MailboxPlan.MaxReceiveSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '')
+ if ($MailboxPlan.MaxSendSize -eq 'Unlimited') {
+ $PlanMaxSendSize = [int64]::MaxValue
+ } else {
+ $PlanMaxSendSize = [int64]($MailboxPlan.MaxSendSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '')
+ }
+
+ if ($MailboxPlan.MaxReceiveSize -eq 'Unlimited') {
+ $PlanMaxReceiveSize = [int64]::MaxValue
+ } else {
+ $PlanMaxReceiveSize = [int64]($MailboxPlan.MaxReceiveSize -replace '.*\(([\d,]+).*', '$1' -replace ',', '')
+ }
+
if ($PlanMaxSendSize -ne $MaxSendSize -or $PlanMaxReceiveSize -ne $MaxReceiveSize) {
$MailboxPlan
}
@@ -76,7 +84,6 @@ function Invoke-CIPPStandardSendReceiveLimitTenant {
}
if ($Settings.alert -eq $true) {
-
if ($NotSetCorrectly.Count -eq 0) {
Write-LogMessage -API 'Standards' -tenant $tenant -message "The tenant send($($Settings.SendLimit)MB) and receive($($Settings.ReceiveLimit)MB) limits are set correctly" -sev Info
} else {
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1
index bbf74d0e09a4..c650c95ef858 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardSpamFilterPolicy.ps1
@@ -75,43 +75,48 @@ function Invoke-CIPPStandardSpamFilterPolicy {
$MarkAsSpamWebBugsInHtml = if ($Settings.MarkAsSpamWebBugsInHtml) { 'On' } else { 'Off' }
$MarkAsSpamSensitiveWordList = if ($Settings.MarkAsSpamSensitiveWordList) { 'On' } else { 'Off' }
- $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and
- ($CurrentState.SpamAction -eq $SpamAction) -and
- ($CurrentState.SpamQuarantineTag -eq $SpamQuarantineTag) -and
- ($CurrentState.HighConfidenceSpamAction -eq $HighConfidenceSpamAction) -and
- ($CurrentState.HighConfidenceSpamQuarantineTag -eq $HighConfidenceSpamQuarantineTag) -and
- ($CurrentState.BulkSpamAction -eq $BulkSpamAction) -and
- ($CurrentState.BulkQuarantineTag -eq $BulkQuarantineTag) -and
- ($CurrentState.PhishSpamAction -eq $PhishSpamAction) -and
- ($CurrentState.PhishQuarantineTag -eq $PhishQuarantineTag) -and
- ($CurrentState.HighConfidencePhishAction -eq 'Quarantine') -and
- ($CurrentState.HighConfidencePhishQuarantineTag -eq $HighConfidencePhishQuarantineTag) -and
- ($CurrentState.BulkThreshold -eq [int]$Settings.BulkThreshold) -and
- ($CurrentState.QuarantineRetentionPeriod -eq 30) -and
- ($CurrentState.IncreaseScoreWithImageLinks -eq $IncreaseScoreWithImageLinks) -and
- ($CurrentState.IncreaseScoreWithNumericIps -eq 'On') -and
- ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'On') -and
- ($CurrentState.IncreaseScoreWithBizOrInfoUrls -eq $IncreaseScoreWithBizOrInfoUrls) -and
- ($CurrentState.MarkAsSpamEmptyMessages -eq 'On') -and
- ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'On') -and
- ($CurrentState.MarkAsSpamFramesInHtml -eq $MarkAsSpamFramesInHtml) -and
- ($CurrentState.MarkAsSpamObjectTagsInHtml -eq $MarkAsSpamObjectTagsInHtml) -and
- ($CurrentState.MarkAsSpamEmbedTagsInHtml -eq $MarkAsSpamEmbedTagsInHtml) -and
- ($CurrentState.MarkAsSpamFormTagsInHtml -eq $MarkAsSpamFormTagsInHtml) -and
- ($CurrentState.MarkAsSpamWebBugsInHtml -eq $MarkAsSpamWebBugsInHtml) -and
- ($CurrentState.MarkAsSpamSensitiveWordList -eq $MarkAsSpamSensitiveWordList) -and
- ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'On') -and
- ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'On') -and
- ($CurrentState.MarkAsSpamNdrBackscatter -eq 'On') -and
- ($CurrentState.MarkAsSpamBulkMail -eq 'On') -and
- ($CurrentState.InlineSafetyTipsEnabled -eq $true) -and
- ($CurrentState.PhishZapEnabled -eq $true) -and
- ($CurrentState.SpamZapEnabled -eq $true) -and
- ($CurrentState.EnableLanguageBlockList -eq $Settings.EnableLanguageBlockList) -and
- ((-not $CurrentState.LanguageBlockList -and -not $Settings.LanguageBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.LanguageBlockList -DifferenceObject $Settings.LanguageBlockList.value))) -and
- ($CurrentState.EnableRegionBlockList -eq $Settings.EnableRegionBlockList) -and
- ((-not $CurrentState.RegionBlockList -and -not $Settings.RegionBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.RegionBlockList -DifferenceObject $Settings.RegionBlockList.value))) -and
- (!(Compare-Object -ReferenceObject $CurrentState.AllowedSenderDomains -DifferenceObject ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains)))
+ try {
+ $StateIsCorrect = ($CurrentState.Name -eq $PolicyName) -and
+ ($CurrentState.SpamAction -eq $SpamAction) -and
+ ($CurrentState.SpamQuarantineTag -eq $SpamQuarantineTag) -and
+ ($CurrentState.HighConfidenceSpamAction -eq $HighConfidenceSpamAction) -and
+ ($CurrentState.HighConfidenceSpamQuarantineTag -eq $HighConfidenceSpamQuarantineTag) -and
+ ($CurrentState.BulkSpamAction -eq $BulkSpamAction) -and
+ ($CurrentState.BulkQuarantineTag -eq $BulkQuarantineTag) -and
+ ($CurrentState.PhishSpamAction -eq $PhishSpamAction) -and
+ ($CurrentState.PhishQuarantineTag -eq $PhishQuarantineTag) -and
+ ($CurrentState.HighConfidencePhishAction -eq 'Quarantine') -and
+ ($CurrentState.HighConfidencePhishQuarantineTag -eq $HighConfidencePhishQuarantineTag) -and
+ ($CurrentState.BulkThreshold -eq [int]$Settings.BulkThreshold) -and
+ ($CurrentState.QuarantineRetentionPeriod -eq 30) -and
+ ($CurrentState.IncreaseScoreWithImageLinks -eq $IncreaseScoreWithImageLinks) -and
+ ($CurrentState.IncreaseScoreWithNumericIps -eq 'On') -and
+ ($CurrentState.IncreaseScoreWithRedirectToOtherPort -eq 'On') -and
+ ($CurrentState.IncreaseScoreWithBizOrInfoUrls -eq $IncreaseScoreWithBizOrInfoUrls) -and
+ ($CurrentState.MarkAsSpamEmptyMessages -eq 'On') -and
+ ($CurrentState.MarkAsSpamJavaScriptInHtml -eq 'On') -and
+ ($CurrentState.MarkAsSpamFramesInHtml -eq $MarkAsSpamFramesInHtml) -and
+ ($CurrentState.MarkAsSpamObjectTagsInHtml -eq $MarkAsSpamObjectTagsInHtml) -and
+ ($CurrentState.MarkAsSpamEmbedTagsInHtml -eq $MarkAsSpamEmbedTagsInHtml) -and
+ ($CurrentState.MarkAsSpamFormTagsInHtml -eq $MarkAsSpamFormTagsInHtml) -and
+ ($CurrentState.MarkAsSpamWebBugsInHtml -eq $MarkAsSpamWebBugsInHtml) -and
+ ($CurrentState.MarkAsSpamSensitiveWordList -eq $MarkAsSpamSensitiveWordList) -and
+ ($CurrentState.MarkAsSpamSpfRecordHardFail -eq 'On') -and
+ ($CurrentState.MarkAsSpamFromAddressAuthFail -eq 'On') -and
+ ($CurrentState.MarkAsSpamNdrBackscatter -eq 'On') -and
+ ($CurrentState.MarkAsSpamBulkMail -eq 'On') -and
+ ($CurrentState.InlineSafetyTipsEnabled -eq $true) -and
+ ($CurrentState.PhishZapEnabled -eq $true) -and
+ ($CurrentState.SpamZapEnabled -eq $true) -and
+ ($CurrentState.EnableLanguageBlockList -eq $Settings.EnableLanguageBlockList) -and
+ ((-not $CurrentState.LanguageBlockList -and -not $Settings.LanguageBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.LanguageBlockList -DifferenceObject $Settings.LanguageBlockList.value))) -and
+ ($CurrentState.EnableRegionBlockList -eq $Settings.EnableRegionBlockList) -and
+ ((-not $CurrentState.RegionBlockList -and -not $Settings.RegionBlockList.value) -or (!(Compare-Object -ReferenceObject $CurrentState.RegionBlockList -DifferenceObject $Settings.RegionBlockList.value))) -and
+ (!(Compare-Object -ReferenceObject $CurrentState.AllowedSenderDomains -DifferenceObject ($Settings.AllowedSenderDomains.value ?? $Settings.AllowedSenderDomains)))
+ }
+ catch {
+ $StateIsCorrect = $false
+ }
$AcceptedDomains = New-ExoRequest -TenantId $Tenant -cmdlet 'Get-AcceptedDomain'
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1
new file mode 100644
index 000000000000..c7938c496d19
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTeamsMeetingVerification.ps1
@@ -0,0 +1,77 @@
+function Invoke-CIPPStandardTeamsMeetingVerification {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) TeamsMeetingVerification
+ .SYNOPSIS
+ (Label) Meeting Verification (ReCaptcha) for Teams
+ .DESCRIPTION
+ (Helptext) Configures the CAPTCHA verification for external users joining Teams meetings. This helps prevent unauthorized AI notetakers and bots from joining meetings.
+ (DocsDescription) Configures the CAPTCHA verification for external users joining Teams meetings. This helps prevent unauthorized AI notetakers and bots from joining meetings. When enabled, external users from untrusted organizations or anonymous users will need to complete a CAPTCHA verification before joining meetings.
+ .NOTES
+ CAT
+ Teams Standards
+ TAG
+ ADDEDCOMPONENT
+ {"type":"autoComplete","required":true,"multiple":false,"creatable":false,"name":"standards.TeamsMeetingVerification.CaptchaVerificationForMeetingJoin","label":"CAPTCHA verification for meeting join","options":[{"label":"Not Required","value":"NotRequired"},{"label":"Anonymous Users and Untrusted Organizations","value":"AnonymousUsersAndUntrustedOrganizations"}]}
+ IMPACT
+ Low Impact
+ ADDEDDATE
+ 2025-06-14
+ POWERSHELLEQUIVALENT
+ Set-CsTeamsMeetingPolicy -Identity Global -CaptchaVerificationForMeetingJoin AnonymousUsersAndUntrustedOrganizations
+ RECOMMENDEDBY
+ "Microsoft"
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ .LINK
+ https://learn.microsoft.com/en-us/microsoftteams/join-verification-check
+ #>
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TeamsMeetingVerification'
+
+ param($Tenant, $Settings)
+ $CurrentState = New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Get-CsTeamsMeetingPolicy' -CmdParams @{Identity = 'Global' } | Select-Object CaptchaVerificationForMeetingJoin
+ $CaptchaVerificationForMeetingJoin = $Settings.CaptchaVerificationForMeetingJoin.value ?? $Settings.CaptchaVerificationForMeetingJoin
+ $StateIsCorrect = ($CurrentState.CaptchaVerificationForMeetingJoin -eq $CaptchaVerificationForMeetingJoin)
+
+ if ($Settings.remediate -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy already set.' -sev Info
+ } else {
+ $cmdParams = @{
+ Identity = 'Global'
+ CaptchaVerificationForMeetingJoin = $CaptchaVerificationForMeetingJoin
+ }
+
+ try {
+ New-TeamsRequest -TenantFilter $Tenant -Cmdlet 'Set-CsTeamsMeetingPolicy' -CmdParams $cmdParams
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Updated Teams Meeting Verification Policy' -sev Info
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set Teams Meeting Verification Policy. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy is set correctly.' -sev Info
+ } else {
+ Write-StandardsAlert -message 'Teams Meeting Verification Policy is not set correctly.' -object $CurrentState -tenant $Tenant -standardName 'TeamsMeetingVerification' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Teams Meeting Verification Policy is not set correctly.' -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ if ($StateIsCorrect) {
+ $FieldValue = $true
+ } else {
+ $FieldValue = $CurrentState
+ }
+ Set-CIPPStandardsCompareField -FieldName 'standards.TeamsMeetingVerification' -FieldValue $FieldValue -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'TeamsMeetingVerification' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1
index b867b66a54de..79e154dbe8e0 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTransportRuleTemplate.ps1
@@ -51,7 +51,7 @@ function Invoke-CIPPStandardTransportRuleTemplate {
Write-LogMessage -API 'Standards' -tenant $tenant -message "Successfully created transport rule for $tenant" -sev 'Info'
}
- Write-LogMessage -API $APINAME -tenant $Tenant -message "Created transport rule for $($tenantFilter)" -sev 'Debug'
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Created transport rule for $($tenantFilter)" -sev 'Debug'
} catch {
$ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
Write-LogMessage -API 'Standards' -tenant $tenant -message "Could not create transport rule for $($tenantFilter): $ErrorMessage" -sev 'Error'
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1
new file mode 100644
index 000000000000..13cbb9562d82
--- /dev/null
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardTwoClickEmailProtection.ps1
@@ -0,0 +1,84 @@
+function Invoke-CIPPStandardTwoClickEmailProtection {
+ <#
+ .FUNCTIONALITY
+ Internal
+ .COMPONENT
+ (APIName) TwoClickEmailProtection
+ .SYNOPSIS
+ (Label) Set two-click confirmation for encrypted emails in New Outlook
+ .DESCRIPTION
+ (Helptext) Configures the two-click confirmation requirement for viewing encrypted/protected emails in OWA and new Outlook. When enabled, users must click "View message" before accessing protected content, providing an additional layer of privacy protection.
+ (DocsDescription) Configures the TwoClickMailPreviewEnabled setting in Exchange Online organization configuration. This security feature requires users to click "View message" before accessing encrypted or protected emails in Outlook on the web (OWA) and new Outlook for Windows. This provides additional privacy protection by preventing protected content from automatically displaying, giving users time to ensure their screen is not visible to others before viewing sensitive content. The feature helps protect against shoulder surfing and accidental disclosure of confidential information.
+ .NOTES
+ CAT
+ Exchange Standards
+ TAG
+ ADDEDCOMPONENT
+ {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select value","name":"standards.TwoClickEmailProtection.state","options":[{"label":"Enabled","value":"enabled"},{"label":"Disabled","value":"disabled"}]}
+ IMPACT
+ Low Impact
+ ADDEDDATE
+ 2025-06-13
+ POWERSHELLEQUIVALENT
+ Set-OrganizationConfig -TwoClickMailPreviewEnabled \$true \| \$false
+ RECOMMENDEDBY
+ UPDATECOMMENTBLOCK
+ Run the Tools\Update-StandardsComments.ps1 script to update this comment block
+ .LINK
+ https://docs.cipp.app/user-documentation/tenant/standards/list-standards
+ #>
+
+ param($Tenant, $Settings)
+ ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'TwoClickEmailProtection'
+
+ # Get state value using null-coalescing operator
+ $State = $Settings.state.value ?? $Settings.state
+
+ # Input validation
+ if ([string]::IsNullOrWhiteSpace($State)) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message 'TwoClickEmailProtection: Invalid state parameter set' -sev Error
+ Return
+ }
+
+ try {
+ $CurrentState = (New-ExoRequest -tenantid $Tenant -cmdlet 'Get-OrganizationConfig').TwoClickMailPreviewEnabled
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Could not get current two-click email protection state. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ Return
+ }
+
+ $WantedState = $State -eq 'enabled' ? $true : $false
+ $StateIsCorrect = $CurrentState -eq $WantedState ? $true : $false
+
+ if ($Settings.remediate -eq $true) {
+ Write-Host 'Time to remediate two-click email protection'
+
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is already set to $State." -sev Info
+ } else {
+ try {
+ $null = New-ExoRequest -tenantid $Tenant -cmdlet 'Set-OrganizationConfig' -cmdParams @{ TwoClickMailPreviewEnabled = $WantedState } -useSystemMailbox $true
+ $StateIsCorrect = -not $StateIsCorrect # Toggle the state to reflect the change
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set two-click email protection to $State." -sev Info
+ } catch {
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set two-click email protection to $State. Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
+ }
+ }
+ }
+
+ if ($Settings.alert -eq $true) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is correctly set to $State." -sev Info
+ } else {
+ Write-StandardsAlert -message "Two-click email protection is not correctly set to $State, but instead $($CurrentState ? 'enabled' : 'disabled')" -object @{TwoClickMailPreviewEnabled = $CurrentState } -tenant $Tenant -standardName 'TwoClickEmailProtection' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Two-click email protection is not correctly set to $State, but instead $($CurrentState ? 'enabled' : 'disabled')" -sev Info
+ }
+ }
+
+ if ($Settings.report -eq $true) {
+ Set-CIPPStandardsCompareField -FieldName 'standards.TwoClickEmailProtection' -FieldValue $StateIsCorrect -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'TwoClickEmailProtection' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant
+ }
+}
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1
index 8d78372a969d..e2b65e9c9bba 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardUserPreferredLanguage.ps1
@@ -14,7 +14,7 @@ function Invoke-CIPPStandardUserPreferredLanguage {
Entra (AAD) Standards
TAG
ADDEDCOMPONENT
- {"type":"autoComplete","multiple":false,"creatable":false,"name":"standards.UserPreferredLanguage.preferredLanguage","label":"Preferred Language","api":{"url":"/languageList.json","labelField":"language","valueField":"tag"}}
+ {"type":"autoComplete","multiple":false,"creatable":false,"name":"standards.UserPreferredLanguage.preferredLanguage","label":"Preferred Language","api":{"url":"/languageList.json","labelField":"tag","valueField":"tag"}}
IMPACT
High Impact
ADDEDDATE
diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1
index a84306c4a549..57fe9a533670 100644
--- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1
+++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardunmanagedSync.ps1
@@ -5,22 +5,24 @@ function Invoke-CIPPStandardunmanagedSync {
.COMPONENT
(APIName) unmanagedSync
.SYNOPSIS
- (Label) Only allow users to sync OneDrive from AAD joined devices
+ (Label) Restrict access to SharePoint and OneDrive from unmanaged devices
.DESCRIPTION
- (Helptext) The unmanaged Sync standard has been temporarily disabled and does nothing.
- (DocsDescription) The unmanaged Sync standard has been temporarily disabled and does nothing.
+ (Helptext) Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect.
+ (DocsDescription) Entra P1 required. Block or limit access to SharePoint and OneDrive content from unmanaged devices (those not hybrid AD joined or compliant in Intune). These controls rely on Microsoft Entra Conditional Access policies and can take up to 24 hours to take effect. 0 = Allow Access, 1 = Allow limited, web-only access, 2 = Block access. All information about this can be found in Microsofts documentation [here.](https://learn.microsoft.com/en-us/sharepoint/control-access-from-unmanaged-devices)
.NOTES
CAT
SharePoint Standards
TAG
ADDEDCOMPONENT
+ {"type":"autoComplete","multiple":false,"creatable":false,"name":"standards.unmanagedSync.state","label":"State","options":[{"label":"Allow limited, web-only access","value":"1"},{"label":"Block access","value":"2"}],"required":false}
IMPACT
High Impact
ADDEDDATE
- 2022-06-15
+ 2025-06-13
POWERSHELLEQUIVALENT
- Update-MgAdminSharePointSetting
+ Set-SPOTenant -ConditionalAccessPolicy AllowFullAccess \| AllowLimitedAccess \| BlockAccess
RECOMMENDEDBY
+ "CIS"
UPDATECOMMENTBLOCK
Run the Tools\Update-StandardsComments.ps1 script to update this comment block
.LINK
@@ -30,36 +32,41 @@ function Invoke-CIPPStandardunmanagedSync {
param($Tenant, $Settings)
##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'unmanagedSync'
- $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -tenantid $Tenant -AsApp $true
+ $CurrentState = Get-CIPPSPOTenant -TenantFilter $Tenant | Select-Object _ObjectIdentity_, TenantFilter, ConditionalAccessPolicy
+
+ $WantedState = [int]($Settings.state.value ?? 2) # Default to 2 (Block Access) if not set, for pre v8.0.3 standard compatibility
+ $Label = $Settings.state.label ?? 'Block Access' # Default label if not set, for pre v8.0.3 standard compatibility
+ $StateIsCorrect = ($CurrentState.ConditionalAccessPolicy -eq $WantedState)
If ($Settings.remediate -eq $true) {
- if ($CurrentInfo.isUnmanagedSyncAppForTenantRestricted -eq $false) {
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sync for unmanaged devices is already correctly set to: $Label" -sev Info
+ } else {
try {
- #$body = '{"isUnmanagedSyncAppForTenantRestricted": true}'
- #$null = New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/admin/sharepoint/settings' -AsApp $true -Type patch -Body $body -ContentType 'application/json'
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'The unmanaged Sync standard has been temporarily disabled.' -sev Info
+ $CurrentState | Set-CIPPSPOTenant -Properties @{ConditionalAccessPolicy = $WantedState }
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Successfully set the unmanaged Sync state to: $Label" -sev Info
} catch {
- $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message
- Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to disable Sync for unmanaged devices: $ErrorMessage" -sev Error
+ $ErrorMessage = Get-CippException -Exception $_
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to disable Sync for unmanaged devices: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
}
- } else {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Sync for unmanaged devices is already disabled' -sev Info
}
}
if ($Settings.alert -eq $true) {
- if ($CurrentInfo.isUnmanagedSyncAppForTenantRestricted -eq $true) {
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Sync for unmanaged devices is disabled' -sev Info
+ if ($StateIsCorrect -eq $true) {
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sync for unmanaged devices is correctly set to: $Label" -sev Info
} else {
- Write-StandardsAlert -message 'Sync for unmanaged devices is not disabled' -object $CurrentInfo -tenant $tenant -standardName 'unmanagedSync' -standardId $Settings.standardId
- Write-LogMessage -API 'Standards' -tenant $tenant -message 'Sync for unmanaged devices is not disabled' -sev Info
+ Write-StandardsAlert -message "Sync for unmanaged devices is not correctly set to $Label, but instead $($CurrentState.ConditionalAccessPolicy)" -object $CurrentState -tenant $Tenant -standardName 'unmanagedSync' -standardId $Settings.standardId
+ Write-LogMessage -API 'Standards' -tenant $Tenant -message "Sync for unmanaged devices is not correctly set to $Label, but instead $($CurrentState.ConditionalAccessPolicy)" -sev Info
}
}
if ($Settings.report -eq $true) {
- Set-CIPPStandardsCompareField -FieldName 'standards.unmanagedSync' -FieldValue $CurrentInfo.isUnmanagedSyncAppForTenantRestricted -Tenant $tenant
- Add-CIPPBPAField -FieldName 'unmanagedSync' -FieldValue $CurrentInfo.isUnmanagedSyncAppForTenantRestricted -StoreAs bool -Tenant $tenant
+
+ $State = $StateIsCorrect ? $true : $CurrentState.ConditionalAccessPolicy
+ Set-CIPPStandardsCompareField -FieldName 'standards.unmanagedSync' -FieldValue $State -Tenant $Tenant
+ Add-CIPPBPAField -FieldName 'unmanagedSync' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant
}
}
diff --git a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1 b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1
index a54a22d5cc2e..bae5f70793b0 100644
--- a/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1
+++ b/Modules/CIPPCore/Public/Test-CIPPAccessTenant.ps1
@@ -48,14 +48,16 @@ function Test-CIPPAccessTenant {
$ExchangeStatus = $false
$Results = [PSCustomObject]@{
- TenantName = $Tenant.defaultDomainName
- GraphStatus = $false
- GraphTest = ''
- ExchangeStatus = $false
- ExchangeTest = ''
- GDAPRoles = ''
- MissingRoles = ''
- LastRun = (Get-Date).ToUniversalTime()
+ TenantName = $Tenant.defaultDomainName
+ GraphStatus = $false
+ GraphTest = ''
+ ExchangeStatus = $false
+ ExchangeTest = ''
+ GDAPRoles = ''
+ MissingRoles = ''
+ OrgManagementRoles = @()
+ OrgManagementRolesMissing = @()
+ LastRun = (Get-Date).ToUniversalTime()
}
$AddedText = ''
@@ -103,8 +105,32 @@ function Test-CIPPAccessTenant {
try {
$null = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-OrganizationConfig' -ErrorAction Stop
- $ExchangeStatus = $true
- $ExchangeTest = 'Successfully connected to Exchange'
+
+ $OrgManagementRoles = New-ExoRequest -tenantid $Tenant.customerId -cmdlet 'Get-ManagementRoleAssignment' -cmdParams @{ RoleAssignee = 'Organization Management'; Delegating = $false } | Select-Object -Property Role, Guid
+ Write-Information "Found $($OrgManagementRoles.Count) Organization Management roles in Exchange"
+ $Results.OrgManagementRoles = $OrgManagementRoles
+
+ $RoleDefinitions = New-GraphGetRequest -tenantid $Tenant.customerId -uri 'https://graph.microsoft.com/beta/roleManagement/exchange/roleDefinitions'
+ Write-Information "Found $($RoleDefinitions.Count) Exchange role definitions"
+
+ $BasePath = Get-Module -Name 'CIPPCore' | Select-Object -ExpandProperty ModuleBase
+ $AllOrgManagementRoles = Get-Content -Path "$BasePath\Public\OrganizationManagementRoles.json" -ErrorAction Stop | ConvertFrom-Json
+ Write-Information "Loaded all Organization Management roles from $BasePath\Public\OrganizationManagementRoles.json"
+
+ $AvailableRoles = $RoleDefinitions | Where-Object -Property displayName -In $AllOrgManagementRoles | Select-Object -Property displayName, id, description
+ Write-Information "Found $($AvailableRoles.Count) available Organization Management roles in Exchange"
+ $MissingOrgMgmtRoles = $AvailableRoles | Where-Object { $OrgManagementRoles.Role -notcontains $_.displayName }
+ if (($MissingOrgMgmtRoles | Measure-Object).Count -gt 0) {
+ $Results.OrgManagementRolesMissing = $MissingOrgMgmtRoles
+ Write-Warning "Found $($MissingRoles.Count) missing Organization Management roles in Exchange"
+ $ExchangeStatus = $false
+ $ExchangeTest = 'Connected to Exchange but missing permissions in Organization Management. This may impact the ability to manage Exchange features'
+ Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message 'Tenant access check for Exchange failed: Missing Organization Management roles' -Sev 'Warning' -LogData $MissingOrgMgmtRoles
+ } else {
+ Write-Warning 'All available Organization Management roles are present in Exchange'
+ $ExchangeStatus = $true
+ $ExchangeTest = 'Successfully connected to Exchange'
+ }
} catch {
$ErrorMessage = Get-CippException -Exception $_
$ReportedError = ($_.ErrorDetails | ConvertFrom-Json -ErrorAction SilentlyContinue)
@@ -113,6 +139,7 @@ function Test-CIPPAccessTenant {
$ExchangeTest = "Failed to connect to Exchange: $($ErrorMessage.NormalizedError)"
Write-LogMessage -headers $Headers -API $APINAME -tenant $tenant.defaultDomainName -message "Tenant access check for Exchange failed: $($ErrorMessage.NormalizedError) " -Sev 'Error' -LogData $ErrorMessage
+ Write-Warning "Failed to connect to Exchange: $($_.Exception.Message)"
}
if ($GraphStatus -and $ExchangeStatus) {
diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1
index 76def2143679..546ca1cebb7a 100644
--- a/Modules/CippEntrypoints/CippEntrypoints.psm1
+++ b/Modules/CippEntrypoints/CippEntrypoints.psm1
@@ -61,7 +61,7 @@ function Receive-CippHttpTrigger {
} catch {
Write-Warning "Exception occurred on HTTP trigger ($FunctionName): $($_.Exception.Message)"
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
- StatusCode = [HttpStatusCode]::Forbidden
+ StatusCode = [HttpStatusCode]::InternalServerError
Body = $_.Exception.Message
})
}
diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1
index 1603da37c897..245ff267103c 100644
--- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1
+++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1
@@ -1816,7 +1816,7 @@ function Invoke-NinjaOneTenantSync {
Set-Location $CIPPRoot
try {
- $StandardsDefinitions = Get-Content 'config/standards.json' | ConvertFrom-Json -Depth 100
+ $StandardsDefinitions = Invoke-RestMethod -Uri 'https://raw.githubusercontent.com/KelvinTegelaar/CIPP/refs/heads/main/src/data/standards.json'
$AppliedStandards = Get-CIPPStandards -TenantFilter $Customer.defaultDomainName
$Templates = Get-CIPPTable 'templates'
$StandardTemplates = Get-CIPPAzDataTableEntity @Templates | Where-Object { $_.PartitionKey -eq 'StandardsTemplateV2' }
diff --git a/cspell.json b/cspell.json
index a46f912f562a..e4ec9aadb5ed 100644
--- a/cspell.json
+++ b/cspell.json
@@ -1,70 +1,103 @@
{
- "version": "0.2",
- "ignorePaths": [],
- "dictionaryDefinitions": [],
- "dictionaries": [],
- "words": [
- "ADMS",
- "AITM",
- "Autotask",
- "Bluetrait",
- "CIPP",
- "CIPP-API",
- "Connectwise",
- "Datto",
- "Entra",
- "GDAP",
- "Intune",
- "OBEE",
- "Passwordless",
- "PSTN",
- "Sherweb",
- "SSPR",
- "Terrl",
- "TNEF",
- "winmail",
- "Yubikey"
- ],
- "ignoreWords": [
- "tenantid",
- "APINAME",
- "CIPPBPA",
- "CIPPCA",
- "CIPPSPO",
- "CIPPAPI",
- "Addins",
- "Helptext",
- "ADDEDCOMPONENT",
- "ADDEDDATE",
- "POWERSHELLEQUIVALENT",
- "RECOMMENDEDBY",
- "UPDATECOMMENTBLOCK",
- "DISABLEDFEATURES",
- "pscustomobject",
- "microsoftonline",
- "mdo_safeattachments",
- "mdo_highconfidencespamaction",
- "mdo_highconfidencephishaction",
- "mdo_phisspamacation",
- "mdo_spam_notifications_only_for_admins",
- "mdo_antiphishingpolicies",
- "mdo_phishthresholdlevel",
- "mdo_autoforwardingmode",
- "mdo_blockmailforward",
- "mdo_zapspam",
- "mdo_zapphish",
- "mdo_zapmalware",
- "mdo_safedocuments",
- "mdo_commonattachmentsfilter",
- "mdo_safeattachmentpolicy",
- "mdo_safelinksforemail",
- "mdo_safelinksforOfficeApps",
- "exo_storageproviderrestricted",
- "exo_individualsharing",
- "exo_outlookaddins",
- "exo_mailboxaudit",
- "exo_mailtipsenabled",
- "mip_search_auditlog"
- ],
- "import": []
+ "version": "0.2",
+ "ignorePaths": [],
+ "dictionaryDefinitions": [],
+ "dictionaries": [],
+ "words": [
+ "ADMS",
+ "AITM",
+ "Autotask",
+ "Bluetrait",
+ "cipp",
+ "CIPP",
+ "CIPP-API",
+ "Connectwise",
+ "CPIM",
+ "Datto",
+ "DMARC",
+ "endswith",
+ "entra",
+ "Entra",
+ "exploitability",
+ "gdap",
+ "GDAP",
+ "IMAP",
+ "Intune",
+ "locationcipp",
+ "MAPI",
+ "Multitenant",
+ "OBEE",
+ "passwordless",
+ "Passwordless",
+ "PSTN",
+ "rvdwegen",
+ "sharepoint",
+ "SharePoint",
+ "Sherweb",
+ "Signup",
+ "SSPR",
+ "Standardcal",
+ "Terrl",
+ "TNEF",
+ "weburl",
+ "winmail",
+ "Yubikey"
+ ],
+ "ignoreWords": [
+ "ACOM",
+ "Sharepoint",
+ "tenantid",
+ "jnlp",
+ "wsfed",
+ "Imap",
+ "APINAME",
+ "CIPPAPI",
+ "WCSS",
+ "cacheusers",
+ "CIPPBPA",
+ "CIPPCA",
+ "MCAPI",
+ "skuid",
+ "BPOS",
+ "EPMID",
+ "CIPPSPO",
+ "CIPPTAP",
+ "donotchange",
+ "Addins",
+ "Helptext",
+ "ADDEDCOMPONENT",
+ "ADDEDDATE",
+ "POWERSHELLEQUIVALENT",
+ "RECOMMENDEDBY",
+ "UPDATECOMMENTBLOCK",
+ "DISABLEDFEATURES",
+ "pscustomobject",
+ "microsoftonline",
+ "mdo_safeattachments",
+ "mdo_highconfidencespamaction",
+ "mdo_highconfidencephishaction",
+ "mdo_phisspamacation",
+ "mdo_spam_notifications_only_for_admins",
+ "mdo_antiphishingpolicies",
+ "mdo_phishthresholdlevel",
+ "mdo_autoforwardingmode",
+ "mdo_blockmailforward",
+ "mdo_zapspam",
+ "mdo_zapphish",
+ "mdo_zapmalware",
+ "mdo_safedocuments",
+ "mdo_commonattachmentsfilter",
+ "mdo_safeattachmentpolicy",
+ "mdo_safelinksforemail",
+ "mdo_safelinksforOfficeApps",
+ "exo_storageproviderrestricted",
+ "exo_individualsharing",
+ "exo_outlookaddins",
+ "exo_mailboxaudit",
+ "exo_mailtipsenabled",
+ "mip_search_auditlog",
+ "onmicrosoft",
+ "MOERA"
+ ],
+ "import": []
}
diff --git a/openapi.json b/openapi.json
index 7fb2383fa0cc..ffb4023947fe 100644
--- a/openapi.json
+++ b/openapi.json
@@ -2850,7 +2850,7 @@
"schema": {
"type": "string"
},
- "name": "keepCopy",
+ "name": "KeepCopy",
"in": "body"
}
],
@@ -3557,6 +3557,89 @@
}
}
},
+ "/ListContactTemplates": {
+ "get": {
+ "description": "List Contact Templates",
+ "summary": "List Contact Templates",
+ "tags": ["GET"],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/RemoveContactTemplates": {
+ "get": {
+ "description": "Remove Contact Template",
+ "summary": "Remove Contact Template",
+ "tags": ["GET"],
+ "parameters": [
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/AddContactTemplates": {
+ "post": {
+ "description": "Add Contact Template",
+ "summary": "Add Contact Template",
+ "tags": ["POST"],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
"/ListMailboxRules": {
"get": {
"description": "ListMailboxRules",
@@ -3746,7 +3829,7 @@
"schema": {
"type": "string"
},
- "name": "keepCopy",
+ "name": "KeepCopy",
"in": "body"
},
{
@@ -8898,6 +8981,680 @@
}
}
},
+ "/ExecDeleteSafeLinksPolicy": {
+ "get": {
+ "description": "ExecDeleteSafeLinksPolicy",
+ "summary": "ExecDeleteSafeLinksPolicy",
+ "tags": [
+ "GET"
+ ],
+ "parameters": [
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "tenantFilter",
+ "in": "query"
+ },
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "RuleName",
+ "in": "query"
+ },
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "PolicyName",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/EditSafeLinksPolicy": {
+ "post": {
+ "description": "EditSafeLinksPolicy",
+ "summary": "EditSafeLinksPolicy",
+ "tags": [
+ "POST"
+ ],
+ "parameters": [
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "tenantFilter",
+ "in": "query"
+ },
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "PolicyName",
+ "in": "query"
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "Name": {
+ "type": "string"
+ },
+ "EnableSafeLinksForEmail": {
+ "type": "boolean"
+ },
+ "EnableSafeLinksForTeams": {
+ "type": "boolean"
+ },
+ "EnableSafeLinksForOffice": {
+ "type": "boolean"
+ },
+ "TrackClicks": {
+ "type": "boolean"
+ },
+ "AllowClickThrough": {
+ "type": "boolean"
+ },
+ "ScanUrls": {
+ "type": "boolean"
+ },
+ "EnableForInternalSenders": {
+ "type": "boolean"
+ },
+ "DeliverMessageAfterScan": {
+ "type": "boolean"
+ },
+ "DisableUrlRewrite": {
+ "type": "boolean"
+ },
+ "DoNotRewriteUrls": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "AdminDisplayName": {
+ "type": "string"
+ },
+ "CustomNotificationText": {
+ "type": "string"
+ },
+ "EnableOrganizationBranding": {
+ "type": "boolean"
+ },
+ "Priority": {
+ "type": "integer"
+ },
+ "Comments": {
+ "type": "string"
+ },
+ "Enabled": {
+ "type": "boolean"
+ },
+ "SentTo": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExceptIfSentTo": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "SentToMemberOf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExceptIfSentToMemberOf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "RecipientDomainIs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExceptIfRecipientDomainIs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "Results": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/ListSafeLinksPolicyDetails": {
+ "get": {
+ "description": "ListSafeLinksPolicyDetails",
+ "summary": "ListSafeLinksPolicyDetails",
+ "tags": [
+ "GET"
+ ],
+ "parameters": [
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "tenantFilter",
+ "in": "query"
+ },
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "PolicyName",
+ "in": "query"
+ },
+ {
+ "required": false,
+ "schema": {
+ "type": "string"
+ },
+ "name": "RuleName",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "Results": {
+ "type": "object",
+ "properties": {
+ "Policy": {
+ "type": "object",
+ "properties": {
+ "Name": {
+ "type": "string"
+ },
+ "EnableSafeLinksForEmail": {
+ "type": "boolean"
+ },
+ "EnableSafeLinksForTeams": {
+ "type": "boolean"
+ },
+ "EnableSafeLinksForOffice": {
+ "type": "boolean"
+ },
+ "TrackClicks": {
+ "type": "boolean"
+ },
+ "AllowClickThrough": {
+ "type": "boolean"
+ },
+ "ScanUrls": {
+ "type": "boolean"
+ },
+ "EnableForInternalSenders": {
+ "type": "boolean"
+ },
+ "DeliverMessageAfterScan": {
+ "type": "boolean"
+ },
+ "DisableUrlRewrite": {
+ "type": "boolean"
+ },
+ "DoNotRewriteUrls": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "AdminDisplayName": {
+ "type": "string"
+ },
+ "CustomNotificationText": {
+ "type": "string"
+ },
+ "EnableOrganizationBranding": {
+ "type": "boolean"
+ }
+ }
+ },
+ "Rule": {
+ "type": "object",
+ "properties": {
+ "Name": {
+ "type": "string"
+ },
+ "Priority": {
+ "type": "integer"
+ },
+ "Comments": {
+ "type": "string"
+ },
+ "State": {
+ "type": "string"
+ },
+ "SentTo": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExceptIfSentTo": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "SentToMemberOf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExceptIfSentToMemberOf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "RecipientDomainIs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "ExceptIfRecipientDomainIs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "Message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/ExecNewSafeLinksPolicy": {
+ "post": {
+ "description": "Create a new SafeLinks policy and associated rule",
+ "summary": "Create SafeLinks Policy and Rule Configuration",
+ "tags": [
+ "POST"
+ ],
+ "parameters": [
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "tenantFilter",
+ "in": "query"
+ },
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "Name",
+ "in": "query"
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "EnableSafeLinksForEmail": {
+ "type": "boolean",
+ "description": "Enable Safe Links protection for email messages"
+ },
+ "EnableSafeLinksForTeams": {
+ "type": "boolean",
+ "description": "Enable Safe Links protection for Teams messages"
+ },
+ "EnableSafeLinksForOffice": {
+ "type": "boolean",
+ "description": "Enable Safe Links protection for Office applications"
+ },
+ "TrackClicks": {
+ "type": "boolean",
+ "description": "Track user clicks related to Safe Links protection"
+ },
+ "AllowClickThrough": {
+ "type": "boolean",
+ "description": "Allow users to click through to the original URL"
+ },
+ "ScanUrls": {
+ "type": "boolean",
+ "description": "Enable real-time scanning of URLs"
+ },
+ "EnableForInternalSenders": {
+ "type": "boolean",
+ "description": "Enable Safe Links for messages sent between internal senders"
+ },
+ "DeliverMessageAfterScan": {
+ "type": "boolean",
+ "description": "Wait until URL scanning is complete before delivering the message"
+ },
+ "DisableUrlRewrite": {
+ "type": "boolean",
+ "description": "Disable the rewriting of URLs in messages"
+ },
+ "DoNotRewriteUrls": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of URLs that will not be rewritten by Safe Links"
+ },
+ "AdminDisplayName": {
+ "type": "string",
+ "description": "Display name for the policy in the admin interface"
+ },
+ "CustomNotificationText": {
+ "type": "string",
+ "description": "Custom text for the notification when a link is blocked"
+ },
+ "EnableOrganizationBranding": {
+ "type": "boolean",
+ "description": "Enable organization branding on warning pages"
+ },
+ "Priority": {
+ "type": "integer",
+ "description": "Priority of the rule (lower numbers = higher priority)"
+ },
+ "Comments": {
+ "type": "string",
+ "description": "Administrative comments for the rule"
+ },
+ "Enabled": {
+ "type": "boolean",
+ "description": "Whether the rule is enabled or disabled"
+ },
+ "SentTo": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Apply the rule if the recipient is any of these email addresses"
+ },
+ "SentToMemberOf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Apply the rule if the recipient is a member of any of these groups"
+ },
+ "RecipientDomainIs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Apply the rule if the recipient's domain matches any of these domains"
+ },
+ "ExceptIfSentTo": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Do not apply the rule if the recipient is any of these email addresses"
+ },
+ "ExceptIfSentToMemberOf": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Do not apply the rule if the recipient is a member of any of these groups"
+ },
+ "ExceptIfRecipientDomainIs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Do not apply the rule if the recipient's domain matches any of these domains"
+ }
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "Results": {
+ "type": "string",
+ "description": "Result message from the operation"
+ }
+ }
+ }
+ }
+ },
+ "description": "Successfully created SafeLinks policy and rule"
+ },
+ "400": {
+ "description": "Bad request - missing required parameters",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "Results": {
+ "type": "string",
+ "description": "Error message"
+ }
+ }
+ }
+ }
+ }
+ },
+ "500": {
+ "description": "Internal server error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {
+ "Results": {
+ "type": "string",
+ "description": "Error message"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ListSafeLinksPolicyTemplates": {
+ "get": {
+ "description": "List SafeLinks Policy Templates",
+ "summary": "List SafeLinks Policy Templates",
+ "tags": [
+ "GET"
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/RemoveSafeLinksPolicyTemplate": {
+ "get": {
+ "description": "Remove SafeLinks Policy Template",
+ "summary": "Remove SafeLinks Policy Template",
+ "tags": [
+ "GET"
+ ],
+ "parameters": [
+ {
+ "required": true,
+ "schema": {
+ "type": "string"
+ },
+ "name": "id",
+ "in": "query"
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/AddSafeLinksPolicyTemplate": {
+ "post": {
+ "description": "Add SafeLinks Policy Template",
+ "summary": "Add SafeLinks Policy Template",
+ "tags": [
+ "POST"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
+ "/AddSafeLinksPolicyFromTemplate": {
+ "post": {
+ "description": "Deploy SafeLinks Policy From Template",
+ "summary": "Deploy SafeLinks Policy From Template",
+ "tags": [
+ "POST"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "properties": {}
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {},
+ "type": "object"
+ }
+ }
+ },
+ "description": "Successful operation"
+ }
+ }
+ }
+ },
"openapi": "3.1.0",
"servers": [
{
diff --git a/profile.ps1 b/profile.ps1
index cfcf29575171..a763fe8b693e 100644
--- a/profile.ps1
+++ b/profile.ps1
@@ -13,7 +13,7 @@
# Remove this if you are not planning on using MSI or Azure PowerShell.
# Import modules
-@('CIPPCore', 'CippExtensions', 'Az.KeyVault', 'Az.Accounts') | ForEach-Object {
+@('CIPPCore', 'CippExtensions', 'Az.KeyVault', 'Az.Accounts', 'AzBobbyTables') | ForEach-Object {
try {
$Module = $_
Import-Module -Name $_ -ErrorAction Stop
diff --git a/version_latest.txt b/version_latest.txt
index 215aacb45236..8104cabd36fb 100644
--- a/version_latest.txt
+++ b/version_latest.txt
@@ -1 +1 @@
-8.0.3
+8.1.0