diff --git a/CIPPTimers.json b/CIPPTimers.json index 3532f6fafe42..4bbf8518afc6 100644 --- a/CIPPTimers.json +++ b/CIPPTimers.json @@ -105,6 +105,14 @@ "Priority": 5, "RunOnProcessor": true }, + { + "Id": "0967c860-3a57-4860-8f33-e5136eae7b4e", + "Command": "Start-TenantDynamicGroupOrchestrator", + "Description": "Orchestrator to update dynamic tenant groups", + "Cron": "0 0 */4 * * *", + "Priority": 6, + "RunOnProcessor": true + }, { "Id": "4ca242d0-8dc8-4256-b0ed-186599f4233f", "Command": "Start-UpdateTokensTimer", @@ -206,4 +214,4 @@ "RunOnProcessor": true, "IsSystem": true } -] \ No newline at end of file +] diff --git a/Modules/AzBobbyTables/3.3.1/AzBobbyTables.PS.dll b/Modules/AzBobbyTables/3.3.1/AzBobbyTables.PS.dll deleted file mode 100644 index 7d9b64a3818b..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/AzBobbyTables.PS.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/AzBobbyTables.PS.pdb b/Modules/AzBobbyTables/3.3.1/AzBobbyTables.PS.pdb deleted file mode 100644 index cb1c42199263..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/AzBobbyTables.PS.pdb and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/AzBobbyTables.Core.dll b/Modules/AzBobbyTables/3.3.1/dependencies/AzBobbyTables.Core.dll deleted file mode 100644 index ed1ae67e4b44..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/AzBobbyTables.Core.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/AzBobbyTables.Core.pdb b/Modules/AzBobbyTables/3.3.1/dependencies/AzBobbyTables.Core.pdb deleted file mode 100644 index 00c402d71f90..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/AzBobbyTables.Core.pdb and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/Azure.Core.dll b/Modules/AzBobbyTables/3.3.1/dependencies/Azure.Core.dll deleted file mode 100644 index fb5b1ba52966..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/Azure.Core.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/Azure.Data.Tables.dll b/Modules/AzBobbyTables/3.3.1/dependencies/Azure.Data.Tables.dll deleted file mode 100644 index 9527e455da9d..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/Azure.Data.Tables.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.Bcl.AsyncInterfaces.dll b/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.Bcl.AsyncInterfaces.dll deleted file mode 100644 index 39fd1311f266..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.Bcl.AsyncInterfaces.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.VisualStudio.Threading.dll b/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.VisualStudio.Threading.dll deleted file mode 100644 index 62814912b2cf..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.VisualStudio.Threading.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Buffers.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Buffers.dll deleted file mode 100644 index c0970c078522..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Buffers.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.ClientModel.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.ClientModel.dll deleted file mode 100644 index 1363faf66562..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.ClientModel.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Linq.Async.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Linq.Async.dll deleted file mode 100644 index a328d9d3087e..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Linq.Async.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Memory.Data.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Memory.Data.dll deleted file mode 100644 index 6f2a3e0ad07f..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Memory.Data.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Memory.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Memory.dll deleted file mode 100644 index 1e6aef802063..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Memory.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Numerics.Vectors.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Numerics.Vectors.dll deleted file mode 100644 index 10205772c39d..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Numerics.Vectors.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Runtime.CompilerServices.Unsafe.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Runtime.CompilerServices.Unsafe.dll deleted file mode 100644 index 491a80a97880..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Runtime.CompilerServices.Unsafe.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Text.Json.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Text.Json.dll deleted file mode 100644 index 5417430f23c0..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Text.Json.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Threading.Tasks.Extensions.dll b/Modules/AzBobbyTables/3.3.1/dependencies/System.Threading.Tasks.Extensions.dll deleted file mode 100644 index dfab23478ab4..000000000000 Binary files a/Modules/AzBobbyTables/3.3.1/dependencies/System.Threading.Tasks.Extensions.dll and /dev/null differ diff --git a/Modules/AzBobbyTables/3.4.0/AzBobbyTables.PS.dll b/Modules/AzBobbyTables/3.4.0/AzBobbyTables.PS.dll new file mode 100644 index 000000000000..e70586e52e2c Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/AzBobbyTables.PS.dll differ diff --git a/Modules/AzBobbyTables/3.3.1/AzBobbyTables.psd1 b/Modules/AzBobbyTables/3.4.0/AzBobbyTables.psd1 similarity index 87% rename from Modules/AzBobbyTables/3.3.1/AzBobbyTables.psd1 rename to Modules/AzBobbyTables/3.4.0/AzBobbyTables.psd1 index 0b1f9cc6a082..14215e7c8d1d 100644 --- a/Modules/AzBobbyTables/3.3.1/AzBobbyTables.psd1 +++ b/Modules/AzBobbyTables/3.4.0/AzBobbyTables.psd1 @@ -4,7 +4,7 @@ RootModule = 'AzBobbyTables.PS.dll' # Version number of this module. -ModuleVersion = '3.3.1' +ModuleVersion = '3.4.0' # Supported PSEditions CompatiblePSEditions = @('Core') @@ -69,6 +69,7 @@ CmdletsToExport = @( 'Clear-AzDataTable' 'Get-AzDataTable' 'Get-AzDataTableEntity' + 'Get-AzDataTableSupportedEntityType' 'Remove-AzDataTableEntity' 'Update-AzDataTableEntity' 'New-AzDataTableContext' @@ -109,11 +110,19 @@ PrivateData = @{ # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '## [3.3.1] - 2024-10-19 + ReleaseNotes = '## [3.4.0] - 2025-07-03 ### Added -- Added `-OperationType` parameter to `Add-AzDataTableEntity` and `Update-AzDataTableEntity` to support merge or replace operations [#81](https://github.com/PalmEmanuel/AzBobbyTables/pull/81) +- Added SortedList as valid type for -Entity parameter [#52](https://github.com/PalmEmanuel/AzBobbyTables/issues/52) +- New command `Get-AzDataTableSupportedEntityType` to get the supported data types for the module when using `-Entity` parameter + +### Changed + +- Dependency version bumps +- Rewrote core module logic to add a converter system which allows for flexible entity types +- Updated gitversion config for build and release +- Improved module tests for the new type converter system ' diff --git a/Modules/AzBobbyTables/3.3.1/CHANGELOG.md b/Modules/AzBobbyTables/3.4.0/CHANGELOG.md similarity index 62% rename from Modules/AzBobbyTables/3.3.1/CHANGELOG.md rename to Modules/AzBobbyTables/3.4.0/CHANGELOG.md index 8b9681a286b1..3a3ec108efc8 100644 --- a/Modules/AzBobbyTables/3.3.1/CHANGELOG.md +++ b/Modules/AzBobbyTables/3.4.0/CHANGELOG.md @@ -6,7 +6,27 @@ The format is based on and uses the types of changes according to [Keep a Change ### Added -- Added `-OperationType` parameter to `Add-AzDataTableEntity` and `Update-AzDataTableEntity` to support merge or replace operations [#81](https://github.com/PalmEmanuel/AzBobbyTables/pull/81) +- Added SortedList as valid type for -Entity parameter [#52](https://github.com/PalmEmanuel/AzBobbyTables/issues/52) +- New command `Get-AzDataTableSupportedEntityType` to get the supported data types for the module when using `-Entity` parameter + +### Changed + +- Dependency version bumps +- Rewrote core module logic to add a converter system which allows for flexible entity types +- Updated gitversion config for build and release +- Improved module tests for the new type converter system + +## [3.3.2] - 2025-02-26 + +### Fixed + +- Fixed bug where validation for Partition- and RowKey was not checking case sensitivity [#68](https://github.com/PalmEmanuel/AzBobbyTables/pull/81) + +## [3.3.1] - 2024-10-19 + +### Added + +- Added `-OperationType` parameter to `Add-AzDataTableEntity` and `Update-AzDataTableEntity` to support merge or replace operations [#81](https://github.com/PalmEmanuel/AzBobbyTables/pull/81) ## [3.3.0] - 2024-10-18 @@ -49,7 +69,11 @@ The format is based on and uses the types of changes according to [Keep a Change ## 3.1.1 - 2023-05-03 -[Unreleased]: https://github.com/PalmEmanuel/AzBobbyTables/compare/v3.3.0...HEAD +[Unreleased]: https://github.com/PalmEmanuel/AzBobbyTables/compare/v3.3.2...HEAD + +[3.3.2]: https://github.com/PalmEmanuel/AzBobbyTables/compare/v3.3.1...v3.3.2 + +[3.3.1]: https://github.com/PalmEmanuel/AzBobbyTables/compare/v3.3.0...v3.3.1 [3.3.0]: https://github.com/PalmEmanuel/AzBobbyTables/compare/v3.2.1...v3.3.0 diff --git a/Modules/AzBobbyTables/3.3.1/LICENSE b/Modules/AzBobbyTables/3.4.0/LICENSE similarity index 100% rename from Modules/AzBobbyTables/3.3.1/LICENSE rename to Modules/AzBobbyTables/3.4.0/LICENSE diff --git a/Modules/AzBobbyTables/3.3.1/PSGetModuleInfo.xml b/Modules/AzBobbyTables/3.4.0/PSGetModuleInfo.xml similarity index 57% rename from Modules/AzBobbyTables/3.3.1/PSGetModuleInfo.xml rename to Modules/AzBobbyTables/3.4.0/PSGetModuleInfo.xml index 2559d56021b4..64fa6faf50b2 100644 --- a/Modules/AzBobbyTables/3.3.1/PSGetModuleInfo.xml +++ b/Modules/AzBobbyTables/3.4.0/PSGetModuleInfo.xml @@ -1,143 +1,145 @@ - - - - Microsoft.PowerShell.Commands.PSRepositoryItemInfo - System.Management.Automation.PSCustomObject - System.Object - - - AzBobbyTables - 3.3.1 - Module - A module for handling Azure Table Storage operations by wrapping the Azure Data Tables SDK. - Emanuel Palm - PalmEmanuel - (c) Emanuel Palm. All rights reserved. -
2024-10-19T09:03:27-04:00
- - - https://github.com/PalmEmanuel/AzBobbyTables/blob/main/LICENSE - https://github.com/PalmEmanuel/AzBobbyTables - - - - System.Object[] - System.Array - System.Object - - - azure - storage - table - cosmos - cosmosdb - data - PSModule - PSEdition_Core - - - - - System.Collections.Hashtable - System.Object - - - - RoleCapability - - - - - - - Workflow - - - - Function - - - - Command - - - - Add-AzDataTableEntity - Clear-AzDataTable - Get-AzDataTable - Get-AzDataTableEntity - Remove-AzDataTableEntity - Update-AzDataTableEntity - New-AzDataTableContext - Remove-AzDataTable - New-AzDataTable - - - - - DscResource - - - - Cmdlet - - - - Add-AzDataTableEntity - Clear-AzDataTable - Get-AzDataTable - Get-AzDataTableEntity - Remove-AzDataTableEntity - Update-AzDataTableEntity - New-AzDataTableContext - Remove-AzDataTable - New-AzDataTable - - - - - - - ## [3.3.1] - 2024-10-19_x000A__x000A_### Added_x000A__x000A_- Added `-OperationType` parameter to `Add-AzDataTableEntity` and `Update-AzDataTableEntity` to support merge or replace operations [#81](https://github.com/PalmEmanuel/AzBobbyTables/pull/81) - - - - - https://www.powershellgallery.com/api/v2 - PSGallery - NuGet - - - System.Management.Automation.PSCustomObject - System.Object - - - (c) Emanuel Palm. All rights reserved. - A module for handling Azure Table Storage operations by wrapping the Azure Data Tables SDK. - False - ## [3.3.1] - 2024-10-19_x000A__x000A_### Added_x000A__x000A_- Added `-OperationType` parameter to `Add-AzDataTableEntity` and `Update-AzDataTableEntity` to support merge or replace operations [#81](https://github.com/PalmEmanuel/AzBobbyTables/pull/81) - True - True - 16 - 20865 - 1478233 - 10/19/2024 9:03:27 AM -04:00 - 10/19/2024 9:03:27 AM -04:00 - 10/19/2024 4:20:00 PM -04:00 - azure storage table cosmos cosmosdb data PSModule PSEdition_Core PSCmdlet_Add-AzDataTableEntity PSCommand_Add-AzDataTableEntity PSCmdlet_Clear-AzDataTable PSCommand_Clear-AzDataTable PSCmdlet_Get-AzDataTable PSCommand_Get-AzDataTable PSCmdlet_Get-AzDataTableEntity PSCommand_Get-AzDataTableEntity PSCmdlet_Remove-AzDataTableEntity PSCommand_Remove-AzDataTableEntity PSCmdlet_Update-AzDataTableEntity PSCommand_Update-AzDataTableEntity PSCmdlet_New-AzDataTableContext PSCommand_New-AzDataTableContext PSCmdlet_Remove-AzDataTable PSCommand_Remove-AzDataTable PSCmdlet_New-AzDataTable PSCommand_New-AzDataTable PSIncludes_Cmdlet - False - 2024-10-19T16:20:00Z - 3.3.1 - Emanuel Palm - false - Module - AzBobbyTables.nuspec|dependencies\Azure.Data.Tables.dll|dependencies\Microsoft.Bcl.AsyncInterfaces.dll|dependencies\System.Memory.Data.dll|AzBobbyTables.PS.dll|dependencies\System.Text.Encodings.Web.dll|dependencies\Azure.Core.dll|dependencies\AzBobbyTables.Core.dll|LICENSE|dependencies\System.Security.Principal.Windows.dll|dependencies\Microsoft.VisualStudio.Validation.dll|dependencies\System.Runtime.CompilerServices.Unsafe.dll|CHANGELOG.md|dependencies\AzBobbyTables.Core.pdb|dependencies\Microsoft.VisualStudio.Threading.dll|dependencies\System.Text.Json.dll|AzBobbyTables.PS.pdb|dependencies\System.ClientModel.dll|dependencies\System.Linq.Async.dll|dependencies\System.Threading.Tasks.Extensions.dll|AzBobbyTables.psd1|dependencies\Microsoft.Win32.Registry.dll|dependencies\System.Numerics.Vectors.dll|dependencies\System.Buffers.dll|en-US\AzBobbyTables.PS.dll-Help.xml|dependencies\System.Memory.dll|dependencies\System.Diagnostics.DiagnosticSource.dll|dependencies\System.Security.AccessControl.dll - eead4f42-5080-4f83-8901-340c529a5a11 - 7.0 - pipe.how - - - C:\GitHub\CIPP Workspace\CIPP-API\Modules\AzBobbyTables\3.3.1 -
-
-
+ + + + Microsoft.PowerShell.Commands.PSRepositoryItemInfo + System.Management.Automation.PSCustomObject + System.Object + + + AzBobbyTables + 3.4.0 + Module + A module for handling Azure Table Storage operations by wrapping the Azure Data Tables SDK. + Emanuel Palm + PalmEmanuel + (c) Emanuel Palm. All rights reserved. +
2025-07-03T17:55:54-04:00
+ + + https://github.com/PalmEmanuel/AzBobbyTables/blob/main/LICENSE + https://github.com/PalmEmanuel/AzBobbyTables + + + + System.Object[] + System.Array + System.Object + + + azure + storage + table + cosmos + cosmosdb + data + PSModule + PSEdition_Core + + + + + System.Collections.Hashtable + System.Object + + + + Function + + + + + + + Command + + + + Add-AzDataTableEntity + Clear-AzDataTable + Get-AzDataTable + Get-AzDataTableEntity + Get-AzDataTableSupportedEntityType + Remove-AzDataTableEntity + Update-AzDataTableEntity + New-AzDataTableContext + Remove-AzDataTable + New-AzDataTable + + + + + DscResource + + + + Workflow + + + + RoleCapability + + + + Cmdlet + + + + Add-AzDataTableEntity + Clear-AzDataTable + Get-AzDataTable + Get-AzDataTableEntity + Get-AzDataTableSupportedEntityType + Remove-AzDataTableEntity + Update-AzDataTableEntity + New-AzDataTableContext + Remove-AzDataTable + New-AzDataTable + + + + + + + ## [3.4.0] - 2025-07-03_x000A__x000A_### Added_x000A__x000A_- Added SortedList as valid type for -Entity parameter [#52](https://github.com/PalmEmanuel/AzBobbyTables/issues/52)_x000A_- New command `Get-AzDataTableSupportedEntityType` to get the supported data types for the module when using `-Entity` parameter_x000A__x000A_### Changed_x000A__x000A_- Dependency version bumps_x000A_- Rewrote core module logic to add a converter system which allows for flexible entity types_x000A_- Updated gitversion config for build and release_x000A_- Improved module tests for the new type converter system + + + + + https://www.powershellgallery.com/api/v2 + PSGallery + NuGet + + + System.Management.Automation.PSCustomObject + System.Object + + + (c) Emanuel Palm. All rights reserved. + A module for handling Azure Table Storage operations by wrapping the Azure Data Tables SDK. + False + ## [3.4.0] - 2025-07-03_x000A__x000A_### Added_x000A__x000A_- Added SortedList as valid type for -Entity parameter [#52](https://github.com/PalmEmanuel/AzBobbyTables/issues/52)_x000A_- New command `Get-AzDataTableSupportedEntityType` to get the supported data types for the module when using `-Entity` parameter_x000A__x000A_### Changed_x000A__x000A_- Dependency version bumps_x000A_- Rewrote core module logic to add a converter system which allows for flexible entity types_x000A_- Updated gitversion config for build and release_x000A_- Improved module tests for the new type converter system + True + True + 3355 + 58167 + 1560682 + 7/3/2025 5:55:54 PM -04:00 + 7/3/2025 5:55:54 PM -04:00 + 10/18/2025 1:10:00 AM -04:00 + azure storage table cosmos cosmosdb data PSModule PSEdition_Core PSCmdlet_Add-AzDataTableEntity PSCommand_Add-AzDataTableEntity PSCmdlet_Clear-AzDataTable PSCommand_Clear-AzDataTable PSCmdlet_Get-AzDataTable PSCommand_Get-AzDataTable PSCmdlet_Get-AzDataTableEntity PSCommand_Get-AzDataTableEntity PSCmdlet_Get-AzDataTableSupportedEntityType PSCommand_Get-AzDataTableSupportedEntityType PSCmdlet_Remove-AzDataTableEntity PSCommand_Remove-AzDataTableEntity PSCmdlet_Update-AzDataTableEntity PSCommand_Update-AzDataTableEntity PSCmdlet_New-AzDataTableContext PSCommand_New-AzDataTableContext PSCmdlet_Remove-AzDataTable PSCommand_Remove-AzDataTable PSCmdlet_New-AzDataTable PSCommand_New-AzDataTable PSIncludes_Cmdlet + False + 2025-10-18T01:10:00Z + 3.4.0 + Emanuel Palm + false + Module + AzBobbyTables.nuspec|AzBobbyTables.psd1|dependencies\System.Numerics.Vectors.dll|dependencies\System.Buffers.dll|dependencies\System.Security.AccessControl.dll|LICENSE|dependencies\Azure.Core.dll|dependencies\AzBobbyTables.Core.dll|dependencies\Azure.Data.Tables.dll|AzBobbyTables.PS.dll|dependencies\Microsoft.VisualStudio.Threading.dll|dependencies\Microsoft.VisualStudio.Validation.dll|dependencies\System.Text.Encodings.Web.dll|dependencies\System.Diagnostics.DiagnosticSource.dll|dependencies\System.Memory.dll|dependencies\System.ClientModel.dll|CHANGELOG.md|dependencies\System.Text.Json.dll|dependencies\System.Memory.Data.dll|dependencies\System.Threading.Tasks.Extensions.dll|en-US\AzBobbyTables.PS.dll-Help.xml|dependencies\Microsoft.Bcl.AsyncInterfaces.dll|dependencies\System.Linq.Async.dll|dependencies\Microsoft.Win32.Registry.dll|dependencies\System.Runtime.CompilerServices.Unsafe.dll|dependencies\System.Security.Principal.Windows.dll + eead4f42-5080-4f83-8901-340c529a5a11 + 7.0 + pipe.how + + + C:\GitHub\CIPP Workspace\CIPP-API\Modules\AzBobbyTables\3.4.0 +
+
+
diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/AzBobbyTables.Core.dll b/Modules/AzBobbyTables/3.4.0/dependencies/AzBobbyTables.Core.dll new file mode 100644 index 000000000000..01c63c259aad Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/AzBobbyTables.Core.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/Azure.Core.dll b/Modules/AzBobbyTables/3.4.0/dependencies/Azure.Core.dll new file mode 100644 index 000000000000..390182c78ad8 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/Azure.Core.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/Azure.Data.Tables.dll b/Modules/AzBobbyTables/3.4.0/dependencies/Azure.Data.Tables.dll new file mode 100644 index 000000000000..e1ca8eec36f2 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/Azure.Data.Tables.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.Bcl.AsyncInterfaces.dll b/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.Bcl.AsyncInterfaces.dll new file mode 100644 index 000000000000..ec2a3b9cfd57 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.Bcl.AsyncInterfaces.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.VisualStudio.Threading.dll b/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.VisualStudio.Threading.dll new file mode 100644 index 000000000000..eb30047dc714 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.VisualStudio.Threading.dll differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.VisualStudio.Validation.dll b/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.VisualStudio.Validation.dll similarity index 100% rename from Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.VisualStudio.Validation.dll rename to Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.VisualStudio.Validation.dll diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.Win32.Registry.dll b/Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.Win32.Registry.dll similarity index 100% rename from Modules/AzBobbyTables/3.3.1/dependencies/Microsoft.Win32.Registry.dll rename to Modules/AzBobbyTables/3.4.0/dependencies/Microsoft.Win32.Registry.dll diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Buffers.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Buffers.dll new file mode 100644 index 000000000000..85e0f73c836b Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Buffers.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.ClientModel.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.ClientModel.dll new file mode 100644 index 000000000000..0cb4427b10db Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.ClientModel.dll differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Diagnostics.DiagnosticSource.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Diagnostics.DiagnosticSource.dll similarity index 100% rename from Modules/AzBobbyTables/3.3.1/dependencies/System.Diagnostics.DiagnosticSource.dll rename to Modules/AzBobbyTables/3.4.0/dependencies/System.Diagnostics.DiagnosticSource.dll diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Linq.Async.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Linq.Async.dll new file mode 100644 index 000000000000..3f589cf3e9d2 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Linq.Async.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Memory.Data.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Memory.Data.dll new file mode 100644 index 000000000000..ed4f7b399813 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Memory.Data.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Memory.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Memory.dll new file mode 100644 index 000000000000..a28ae5e0dd2c Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Memory.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Numerics.Vectors.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Numerics.Vectors.dll new file mode 100644 index 000000000000..f7e31b7df037 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Numerics.Vectors.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Runtime.CompilerServices.Unsafe.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Runtime.CompilerServices.Unsafe.dll new file mode 100644 index 000000000000..4faa7050d984 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Runtime.CompilerServices.Unsafe.dll differ diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Security.AccessControl.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Security.AccessControl.dll similarity index 100% rename from Modules/AzBobbyTables/3.3.1/dependencies/System.Security.AccessControl.dll rename to Modules/AzBobbyTables/3.4.0/dependencies/System.Security.AccessControl.dll diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Security.Principal.Windows.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Security.Principal.Windows.dll similarity index 100% rename from Modules/AzBobbyTables/3.3.1/dependencies/System.Security.Principal.Windows.dll rename to Modules/AzBobbyTables/3.4.0/dependencies/System.Security.Principal.Windows.dll diff --git a/Modules/AzBobbyTables/3.3.1/dependencies/System.Text.Encodings.Web.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Text.Encodings.Web.dll similarity index 100% rename from Modules/AzBobbyTables/3.3.1/dependencies/System.Text.Encodings.Web.dll rename to Modules/AzBobbyTables/3.4.0/dependencies/System.Text.Encodings.Web.dll diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Text.Json.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Text.Json.dll new file mode 100644 index 000000000000..c1df9f92f2ca Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Text.Json.dll differ diff --git a/Modules/AzBobbyTables/3.4.0/dependencies/System.Threading.Tasks.Extensions.dll b/Modules/AzBobbyTables/3.4.0/dependencies/System.Threading.Tasks.Extensions.dll new file mode 100644 index 000000000000..2f496fbbd4e3 Binary files /dev/null and b/Modules/AzBobbyTables/3.4.0/dependencies/System.Threading.Tasks.Extensions.dll differ diff --git a/Modules/AzBobbyTables/3.3.1/en-US/AzBobbyTables.PS.dll-Help.xml b/Modules/AzBobbyTables/3.4.0/en-US/AzBobbyTables.PS.dll-Help.xml similarity index 97% rename from Modules/AzBobbyTables/3.3.1/en-US/AzBobbyTables.PS.dll-Help.xml rename to Modules/AzBobbyTables/3.4.0/en-US/AzBobbyTables.PS.dll-Help.xml index 0261a5fa7941..691c321d29b9 100644 --- a/Modules/AzBobbyTables/3.3.1/en-US/AzBobbyTables.PS.dll-Help.xml +++ b/Modules/AzBobbyTables/3.4.0/en-US/AzBobbyTables.PS.dll-Help.xml @@ -711,6 +711,60 @@ PS C:\> $UserEntities = Get-AzDataTableEntity -Property 'FirstName','Age' -Co + + + Get-AzDataTableSupportedEntityType + Get + AzDataTableSupportedEntityType + + Gets the list of data types supported as input entities by the module for commands with the `-Entity` parameter. + + + + The cmdlet retrieves a list of data types that are supported as input entities by the module for commands with the `-Entity` parameter, such as Hashtable, PSObject and SortedList. + + + + Get-AzDataTableSupportedEntityType + + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + PS C:\> Get-AzDataTableSupportedEntityType + + This command retrieves and displays the list of supported entity types that can be used with the module. + + + + + New-AzDataTable diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 866cb12d44fe..60448c409b93 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -1,4 +1,8 @@ function Add-CIPPAzDataTableEntity { + <# + .FUNCTIONALITY + Internal + #> [CmdletBinding(DefaultParameterSetName = 'OperationType')] param( $Context, @@ -13,6 +17,16 @@ function Add-CIPPAzDataTableEntity { [string]$OperationType = 'Add' ) + # Validate input parameters + if ($null -eq $Context) { + throw 'Context parameter cannot be null' + } + + if ($null -eq $Entity) { + Write-Warning 'Entity parameter is null - nothing to process' + return + } + $Parameters = @{ Context = $Context CreateTableIfNotExists = $CreateTableIfNotExists @@ -28,16 +42,55 @@ function Add-CIPPAzDataTableEntity { foreach ($SingleEnt in @($Entity)) { try { + # Skip null entities + if ($null -eq $SingleEnt) { + Write-Warning 'Skipping null entity' + continue + } + if ($null -eq $SingleEnt.PartitionKey -or $null -eq $SingleEnt.RowKey) { throw 'PartitionKey or RowKey is null' } + # Ensure entity is not empty + if ($SingleEnt -is [hashtable] -and $SingleEnt.Count -eq 0) { + Write-Warning 'Skipping empty hashtable entity' + continue + } elseif ($SingleEnt -is [PSCustomObject] -and ($SingleEnt.PSObject.Properties | Measure-Object).Count -eq 0) { + Write-Warning 'Skipping empty PSCustomObject entity' + continue + } + + # Additional validation for AzBobbyTables compatibility + try { + # Ensure all property values are not null for string properties + if ($SingleEnt -is [hashtable]) { + foreach ($key in @($SingleEnt.Keys)) { + if ($null -eq $SingleEnt[$key]) { + $SingleEnt.Remove($key) + } + } + } elseif ($SingleEnt -is [PSCustomObject]) { + $propsToRemove = [system.Collections.Generic.List[string]]::new() + foreach ($prop in $SingleEnt.PSObject.Properties) { + if ($null -eq $prop.Value) { + $propsToRemove.Add($prop.Name) + } + } + foreach ($propName in $propsToRemove) { + $SingleEnt.PSObject.Properties.Remove($propName) + } + } + } catch { + Write-Warning "Error during entity validation: $($_.Exception.Message)" + } + Add-AzDataTableEntity @Parameters -Entity $SingleEnt -ErrorAction Stop } catch [System.Exception] { if ($_.Exception.ErrorCode -in @('PropertyValueTooLarge', 'EntityTooLarge', 'RequestBodyTooLarge')) { try { - Write-Host 'Entity is too large. Splitting entity into multiple parts.' + Write-Information 'Entity is too large. Splitting entity into multiple parts.' $largePropertyNames = [System.Collections.Generic.List[string]]::new() $entitySize = 0 @@ -173,11 +226,13 @@ function Add-CIPPAzDataTableEntity { } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-Warning 'AzBobbyTables Error' - Write-Information ($SingleEnt | ConvertTo-Json) throw "Error processing entity: $ErrorMessage Linenumber: $($_.InvocationInfo.ScriptLineNumber)" } } else { + Write-Information ($_.Exception | ConvertTo-Json) Write-Information "THE ERROR IS $($_.Exception.message). The size of the entity is $entitySize." + Write-Information "Parameters are: $($Parameters | ConvertTo-Json -Compress)" + Write-Information $_.InvocationInfo.PositionMessage throw $_ } } diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index d2838016027c..93ffb9ec6540 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -56,7 +56,7 @@ function Add-CIPPScheduledTask { $propertiesToCheck = @('Webhook', 'Email', 'PSA') $PostExecutionObject = ($propertiesToCheck | Where-Object { $task.PostExecution.$_ -eq $true }) - $PostExecution = $PostExecutionObject ? ($PostExecutionObject -join ',') : ($Task.PostExecution.value -join ',') + $PostExecution = $PostExecutionObject ? @($PostExecutionObject -join ',') : ($Task.PostExecution.value -join ',') $Parameters = [System.Collections.Hashtable]@{} foreach ($Key in $task.Parameters.PSObject.Properties.Name) { $Param = $task.Parameters.$Key @@ -122,6 +122,10 @@ function Add-CIPPScheduledTask { $task.Recurrence.value } + if ($task.PSObject.Properties.Name -notcontains 'ScheduledTime') { + $task | Add-Member -MemberType NoteProperty -Name 'ScheduledTime' -Value 0 -Force + } + if ($DesiredStartTime) { try { # Parse the epoch time @@ -184,7 +188,10 @@ function Add-CIPPScheduledTask { AdditionalProperties = [string]$AdditionalProperties Hidden = [bool]$Hidden Results = 'Planned' + AlertComment = [string]$task.AlertComment } + + # Always store DesiredStartTime if provided if ($DesiredStartTime) { $entity['DesiredStartTime'] = [string]$DesiredStartTime @@ -204,6 +211,39 @@ function Add-CIPPScheduledTask { # Not a JSON object, ignore } } + + if ($task.Trigger) { + $entity.Trigger = [string]($task.Trigger | ConvertTo-Json -Compress) + $TriggerType = $task.Trigger.Type.value ?? $task.Trigger.Type + if ($TriggerType -eq 'DeltaQuery') { + $Parameters = @{} + if ($task.Trigger.WatchedAttributes -and ($task.Trigger.WatchedAttributes | Measure-Object).Count -gt 0) { + $Parameters.'$select' = $task.Trigger.WatchedAttributes | ForEach-Object { $_.value ?? $_ } -join ',' + } + if ($task.Trigger.ResourceFilter) { + $Parameters.'$filter' = "id eq '" + $task.Trigger.ResourceFilter | ForEach-Object { $_.value ?? $_ } -join "' or id eq '" + } + $Resource = $task.Trigger.DeltaResource.value ?? $task.Trigger.DeltaResource + + if ($entity.TenantGroup) { + $tenantFilter = $entity.TenantGroup | ConvertFrom-Json + } + $DeltaQuery = @{ + TenantFilter = $tenantFilter + Resource = $Resource + Parameters = $Parameters + PartitionKey = $RowKey + } + + try { + $null = New-GraphDeltaQuery @DeltaQuery + Write-Information "Created delta query for resource $($Resource)" + } catch { + Write-Warning "Failed to create delta query for resource $($Resource): $($_.Exception.Message)" + } + } + } + if ($SyncType) { $entity.SyncType = $SyncType } @@ -211,15 +251,17 @@ function Add-CIPPScheduledTask { Add-CIPPAzDataTableEntity @Table -Entity $entity -Force } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - return "Could not add task: $ErrorMessage" + Write-Information $_.InvocationInfo.PositionMessage + Write-Information ($entity | ConvertTo-Json) + return "Error - Could not add task: $ErrorMessage" } Write-LogMessage -headers $Headers -API 'ScheduledTask' -message "Added task $($entity.Name) with ID $($entity.RowKey)" -Sev 'Info' -Tenant $tenantFilter return "Successfully added task: $($entity.Name)" } } catch { Write-Warning "Failed to add scheduled task: $($_.Exception.Message)" - Write-Information $_.InvocationInfo.PositionMessage $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - throw "Could not add task: $ErrorMessage" + #Write-Information ($Task | ConvertTo-Json) + throw "Error - Could not add task: $ErrorMessage" } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 index 3326efd47f3a..a9816aa5aef6 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertEntraConnectSyncStatus.ps1 @@ -20,10 +20,18 @@ function Get-CIPPAlertEntraConnectSyncStatus { $LastPasswordSync = $ConnectSyncStatus.onPremisesLastPasswordSyncDateTime $SyncDateTime = $ConnectSyncStatus.onPremisesLastSyncDateTime # Get the older of the two sync times - $LastSync = if ($SyncDateTime -lt $LastPasswordSync) { $SyncDateTime } else { $LastPasswordSync } + $LastSync = if ($SyncDateTime -lt $LastPasswordSync) { $SyncDateTime; $Cause = 'DirectorySync' } else { $LastPasswordSync; $Cause = 'PasswordSync' } if ($LastSync -lt (Get-Date).AddHours(-$Hours).ToUniversalTime()) { - $AlertData = "Entra Connect Sync for $($TenantFilter) has not run for over $Hours hours. Last sync was at $($LastSync.ToString('o'))" + + $AlertData = @{ + Message = "Entra Connect $Cause for $($TenantFilter) has not run for over $Hours hours. Last sync was at $($LastSync.ToString('o'))" + LastSync = $LastSync + Cause = $Cause + LastPasswordSync = $LastPasswordSync + LastDirectorySync = $SyncDateTime + Tenant = $TenantFilter + } Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 new file mode 100644 index 000000000000..138ba2870631 --- /dev/null +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 @@ -0,0 +1,45 @@ +function Get-CIPPAlertMXRecordChanged { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + $TenantFilter, + [Alias('input')] + $InputValue + ) + + try { + $DomainData = Get-CIPPDomainAnalyser -TenantFilter $TenantFilter + $CacheTable = Get-CippTable -tablename 'CacheMxRecords' + $PreviousResults = Get-CIPPAzDataTableEntity @CacheTable -Filter "PartitionKey eq '$TenantFilter'" + + $ChangedDomains = foreach ($Domain in $DomainData) { + $PreviousDomain = $PreviousResults | Where-Object { $_.Domain -eq $Domain.Domain } + if ($PreviousDomain -and $PreviousDomain.ActualMXRecords -ne $Domain.ActualMXRecords) { + "$($Domain.Domain): MX records changed from [$($PreviousDomain.ActualMXRecords -join ', ')] to [$($Domain.ActualMXRecords -join ', ')]" + } + } + + if ($ChangedDomains) { + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains + } + + # Update cache with current data + foreach ($Domain in $DomainData) { + $CacheEntity = @{ + PartitionKey = $TenantFilter + RowKey = $Domain.Domain + Domain = $Domain.Domain + ActualMXRecords = $Domain.ActualMXRecords + LastRefresh = $Domain.LastRefresh + MailProvider = $Domain.MailProvider + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $CacheEntity -Force + } + } catch { + Write-LogMessage -message "Failed to check MX record changes: $($_.Exception.Message)" -API 'MX Record Alert' -tenant $TenantFilter -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Alerts/Get-CippAlertBreachAlert.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CippAlertBreachAlert.ps1 index cd599c6b51d0..ce6f5a05c030 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CippAlertBreachAlert.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CippAlertBreachAlert.ps1 @@ -12,7 +12,7 @@ function Get-CippAlertBreachAlert { ) try { $Search = New-BreachTenantSearch -TenantFilter $TenantFilter - Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $Search + Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $Search -PartitionKey BreachAlert } catch { Write-AlertMessage -tenant $($TenantFilter) -message "Could not get New Breaches for $($TenantFilter): $(Get-NormalizedError -message $_.Exception.message)" } diff --git a/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 b/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 index a48bf575c136..a52728c6d5d7 100644 --- a/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Get-CippAllowedPermissions.ps1 @@ -187,5 +187,5 @@ function Get-CippAllowedPermissions { } # Return sorted unique permissions - return ($AllowedPermissions | Sort-Object -Unique) + return ($AllowedPermissions | Where-Object { $_ -notmatch 'None$' } | Sort-Object -Unique) } diff --git a/Modules/CIPPCore/Public/DeltaQueries/Get-DeltaQueryResults.ps1 b/Modules/CIPPCore/Public/DeltaQueries/Get-DeltaQueryResults.ps1 new file mode 100644 index 000000000000..0b76e0f51165 --- /dev/null +++ b/Modules/CIPPCore/Public/DeltaQueries/Get-DeltaQueryResults.ps1 @@ -0,0 +1,30 @@ +function Get-DeltaQueryResults { + <# + .SYNOPSIS + Retrieves results for Delta Queries + .DESCRIPTION + This helper function modifies the results from Delta Query triggers based on specified properties. + .PARAMETER Data + The data containing Delta Query results. Use %triggerdata% from the scheduler to pass in the data. + .PARAMETER Properties + A comma-separated list of properties to include in the output. If not specified, all properties are returned. + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $Data, + [string]$Properties, + [string]$TenantFilter, + $Headers + ) + + $Properties = $Properties -split ',' | ForEach-Object { $_.Trim() } + if (!$Properties -or $Properties.Count -eq 0) { + Write-Information 'No specific properties requested, returning all data.' + Write-Information ($Data | ConvertTo-Json -Depth 10) + return $Data + } + + $Data = $Data | Select-Object -Property $Properties +} diff --git a/Modules/CIPPCore/Public/DeltaQueries/Get-DeltaQueryUrl.ps1 b/Modules/CIPPCore/Public/DeltaQueries/Get-DeltaQueryUrl.ps1 new file mode 100644 index 000000000000..84acde036209 --- /dev/null +++ b/Modules/CIPPCore/Public/DeltaQueries/Get-DeltaQueryUrl.ps1 @@ -0,0 +1,29 @@ +function Get-DeltaQueryUrl { + <# + .SYNOPSIS + Retrieves the URL for Delta Queries + .DESCRIPTION + This helper function constructs the URL for Delta Query requests based on the resource and parameters. + .PARAMETER TenantFilter + The tenant to filter the query on. + .PARAMETER PartitionKey + The partition key for the delta query. + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $TenantFilter, + [Parameter(Mandatory = $true)] + $PartitionKey + ) + + $Table = Get-CIPPTable -TableName 'DeltaQueries' + $DeltaQueryEntity = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$PartitionKey' and RowKey eq '$TenantFilter'" + + if ($DeltaQueryEntity) { + return $DeltaQueryEntity.DeltaUrl + } else { + throw "Delta Query not found for Tenant '$TenantFilter' and PartitionKey '$PartitionKey'." + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/DeltaQueries/New-GraphDeltaQuery.ps1 b/Modules/CIPPCore/Public/DeltaQueries/New-GraphDeltaQuery.ps1 index 9ec6c2bbdd0c..3ff6452739af 100644 --- a/Modules/CIPPCore/Public/DeltaQueries/New-GraphDeltaQuery.ps1 +++ b/Modules/CIPPCore/Public/DeltaQueries/New-GraphDeltaQuery.ps1 @@ -4,61 +4,167 @@ function New-GraphDeltaQuery { Creates a new Graph Delta Query. .DESCRIPTION This function creates a new Graph Delta Query to track changes in a specified resource. + Always returns the full response including the deltaLink for future incremental queries. .PARAMETER Resource The resource to track changes for (e.g., 'users', 'groups'). .PARAMETER TenantFilter The tenant to filter the query on. + .PARAMETER Parameters + Additional query parameters (e.g., $select, $filter, $top). + .PARAMETER DeltaUrl + Use this parameter to continue a delta query with a specific delta or next link. + .FUNCTIONALITY + Internal #> [CmdletBinding(DefaultParameterSetName = 'NewDeltaQuery')] param( [Parameter(Mandatory = $true, ParameterSetName = 'NewDeltaQuery')] [Parameter(Mandatory = $true, ParameterSetName = 'DeltaUrl')] - [string]$TenantFilter, + $TenantFilter, [Parameter(ParameterSetName = 'NewDeltaQuery', Mandatory = $true)] - [ValidateSet('users', 'groups', 'contacts', 'devices', 'applications', 'servicePrincipals', 'directoryObjects', 'administrativeUnits')] + [ValidateSet('users', 'groups', 'contacts', 'orgContact', 'devices', 'applications', 'servicePrincipals', 'directoryObjects', 'directoryRole', 'administrativeUnits', 'oAuth2PermissionGrant')] [string]$Resource, [Parameter(ParameterSetName = 'NewDeltaQuery', Mandatory = $false)] [hashtable]$Parameters = @{}, [Parameter(ParameterSetName = 'DeltaUrl', Mandatory = $true)] - [string]$DeltaUrl + [string]$DeltaUrl, + + [Parameter(Mandatory = $false, ParameterSetName = 'NewDeltaQuery')] + [Parameter(Mandatory = $true, ParameterSetName = 'DeltaUrl')] + [string]$PartitionKey ) - try { - if ($DeltaUrl) { - $GraphQuery = [System.UriBuilder]$DeltaUrl + if ($TenantFilter -eq 'AllTenants' -or $TenantFilter.type -eq 'Group') { + Write-Information 'Creating delta query for all tenants or tenant group.' + if ($TenantFilter.type -eq 'group') { + $Tenants = Expand-CIPPTenantGroups -TenantFilter $TenantFilter } else { - $GraphQuery = [System.UriBuilder]('https://graph.microsoft.com/beta/{0}/delta' -f $Resource) - $QueryParams = @{ - '$deltaToken' = 'latest' + $Tenants = Get-Tenants -IncludeErrors + } + + if (!$PartitionKey) { + $ParamJson = $Parameters | ConvertTo-Json -Depth 5 -Compress + $PartitionKey = Get-StringHash -String ($Resource + $ParamJson) + } + # Prepare batch processing for all tenants + $TenantBatch = $Tenants | ForEach-Object { + [PSCustomObject]@{ + FunctionName = 'GraphDeltaQuery' + TenantFilter = $_.defaultDomainName ?? $_.value + Resource = $Resource + Parameters = $Parameters + PartitionKey = $PartitionKey } - $ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) + } - foreach ($key in $QueryParams.Keys) { - if ($QueryParams[$key]) { - $ParamCollection.Add($key, $QueryParams[$key]) - } + $InputObject = @{ + Batch = @($TenantBatch) + OrchestratorName = 'ProcessDeltaQueries' + SkipLog = $true + } + Write-Information "Starting delta query orchestration for $($Tenants.Count) tenants." + Write-Information "Orchestration Input: $($InputObject | ConvertTo-Json -Compress -Depth 5)" + $Orchestration = Start-NewOrchestration -FunctionName CIPPOrchestrator -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + + } else { + $Table = Get-CIPPTable -TableName 'DeltaQueries' + + if ($Parameters -and $Resource) { + $ParamJson = $Parameters | ConvertTo-Json -Depth 5 + $ResourceHash = Get-StringHash -String ($Resource + $ParamJson) + + $DeltaQuery = @{ + PartitionKey = $PartitionKey ?? $ResourceHash + RowKey = $TenantFilter + Resource = $Resource + Parameters = [string]($Parameters | ConvertTo-Json -Depth 5 -Compress) + DeltaUrl = $DeltaUrl } + } elseif ($PartitionKey) { + $DeltaQuery = Get-AzDataTableEntity @Table -Filter "PartitionKey eq '$PartitionKey' and RowKey eq '$TenantFilter'" + } - foreach ($Item in ($Parameters.GetEnumerator() | Sort-Object -CaseSensitive -Property Key)) { - if ($Item.Value -is [System.Boolean]) { - $Item.Value = $Item.Value.ToString().ToLower() + try { + if ($DeltaUrl) { + $GraphQuery = [System.UriBuilder]$DeltaUrl + } else { + $GraphQuery = [System.UriBuilder]('https://graph.microsoft.com/beta/{0}/delta' -f $Resource) + $QueryParams = @{ + '$deltaToken' = 'latest' } - if ($Item.Value) { - $ParamCollection.Add($Item.Key, $Item.Value) + $ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) + + foreach ($key in $QueryParams.Keys) { + if ($QueryParams[$key]) { + $ParamCollection.Add($key, $QueryParams[$key]) + } } + + foreach ($Item in ($Parameters.GetEnumerator() | Sort-Object -CaseSensitive -Property Key)) { + if ($Item.Value -is [System.Boolean]) { + $Item.Value = $Item.Value.ToString().ToLower() + } + if ($Item.Value) { + $ParamCollection.Add($Item.Key, $Item.Value) + } + } + $GraphQuery.Query = $ParamCollection.ToString() } - $GraphQuery.Query = $ParamCollection.ToString() - } - #Write-Information "Creating Delta Query for $Resource with parameters: $($GraphQuery.Query)" - $response = New-GraphGetRequest -tenantid $TenantFilter -uri $GraphQuery.ToString() -ReturnRawResponse - Write-Information "Delta Query created successfully for $Resource. Response: $($response | ConvertTo-Json -Depth 5)" - return $response.Content - } catch { - Write-Error "Failed to create Delta Query: $(Get-NormalizedError -Message $_.Exception.message)" - Write-Warning $_.InvocationInfo.PositionMessage + $allResults = [System.Collections.ArrayList]::new() + $nextUrl = $GraphQuery.ToString() + $deltaLink = $null + + $DeltaError = $false + do { + try { + $response = New-GraphGetRequest -tenantid $TenantFilter -uri $nextUrl -ReturnRawResponse -extraHeaders @{ Prefer = 'return=minimal' } -ErrorAction Stop + if ($response.Content) { + $content = $response.Content + if ($content -is [string]) { + $content = $content | ConvertFrom-Json + } + + # Add results from this page + if ($content.value) { + $allResults.AddRange($content.value) + } + + # Check for next page or delta link + $nextUrl = $content.'@odata.nextLink' + $deltaLink = $content.'@odata.deltaLink' + } + } catch { + Write-Error "Error during Graph Delta Query request for tenant '$TenantFilter': $(Get-NormalizedError -Message $_.Exception.message)" + $DeltaError = $true + } + } while ($nextUrl -and -not $deltaLink -and -not $DeltaError) + + if ($DeltaError) { + throw "Delta Query failed for tenant '$TenantFilter'." + } + $DeltaQuery.RowKey = $TenantFilter + $DeltaQuery.DeltaUrl = $deltaLink + + # Return results with delta link for future queries + $result = @{ + value = $allResults.ToArray() + '@odata.deltaLink' = $deltaLink + PartitionKey = $PartitionKey + } + # Save link to table + Add-CIPPAzDataTableEntity @Table -Entity $DeltaQuery -Force + + Write-Information "Delta Query created for $($DeltaQuery.Resource). Total items: $($allResults.Count)" + + # Always return full response with deltaLink + return $result + } catch { + Write-Error "Failed to create Delta Query: $(Get-NormalizedError -Message $_.Exception.message)" + Write-Warning $_.InvocationInfo.PositionMessage + } } } diff --git a/Modules/CIPPCore/Public/DeltaQueries/Push-GraphDeltaQuery.ps1 b/Modules/CIPPCore/Public/DeltaQueries/Push-GraphDeltaQuery.ps1 new file mode 100644 index 000000000000..67e503543e7f --- /dev/null +++ b/Modules/CIPPCore/Public/DeltaQueries/Push-GraphDeltaQuery.ps1 @@ -0,0 +1,19 @@ +function Push-GraphDeltaQuery { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param ( + $Item + ) + + $Item = $Item | Select-Object -ExcludeProperty FunctionName | ConvertTo-Json -Depth 5 | ConvertFrom-Json -AsHashtable + try { + New-GraphDeltaQuery @Item + } catch { + Write-Error "Failed to create Delta Query: $(Get-NormalizedError -Message $_.Exception.message)" + Write-Warning $_.InvocationInfo.PositionMessage + } + +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 b/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 new file mode 100644 index 000000000000..a510320b160a --- /dev/null +++ b/Modules/CIPPCore/Public/DeltaQueries/Test-DeltaQueryConditions.ps1 @@ -0,0 +1,173 @@ +function Test-DeltaQueryConditions { + <# + .SYNOPSIS + Tests if the conditions for a Delta Query trigger are met. + .DESCRIPTION + This function evaluates whether the specified conditions for a Delta Query trigger are satisfied based on the provided data. + .PARAMETER Query + The result of the delta query to evaluate. + .PARAMETER Trigger + The trigger configuration containing conditions to test against. + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + $Query, + [Parameter(Mandatory = $true)] + $Trigger, + [Parameter(Mandatory = $true)] + $TenantFilter, + [Parameter(Mandatory = $true)] + $LastTrigger + ) + + $ConditionsMet = $false + $MatchedData = @() + + $Data = $Query.value + $EventType = $Trigger.EventType.value ?? $Trigger.EventType + + # Filter data based on delta query change type according to Microsoft Graph specification + switch ($EventType) { + 'deleted' { + Write-Information "data to process for deleted: $($Data | ConvertTo-Json -Depth 5)" + # Removed instances are represented by their id and an @removed object + $Data = $Data | Where-Object { + $_.PSObject.Properties.Name -contains '@removed' -and $_.'@removed'.reason -eq 'changed' + } + + # For directory objects, fetch full details of deleted items + Write-Information 'Fetching full details for deleted directory objects.' + + $Requests = foreach ($item in $Data) { + [PSCustomObject]@{ + 'id' = $item.id + 'url' = "directory/deletedItems/$($item.id)" + 'method' = 'GET' + } + } + try { + $DeletedItems = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter + if ($DeletedItems.status -eq 200) { + Write-Information 'Retrieved full details for deleted items.' + Write-Information "Deleted items response: $($DeletedItems | ConvertTo-Json -Depth 5)" + $EnrichedData = [System.Collections.Generic.List[object]]::new() + foreach ($Row in $Data) { + $fullItem = ($DeletedItems | Where-Object { $_.id -eq $Row.id -and $_.status -eq 200 }).body + if ($fullItem) { + $EnrichedData.Add($fullItem) + } else { + $EnrichedData.Add($Row) + } + } + $Data = $EnrichedData + } + } catch { + Write-Warning "Failed to retrieve full details for deleted items: $($_.Exception.Message)" + } + + Write-Information "Found $($Data.Count) deleted items." + } + 'created' { + # Newly created instances use standard representation without @removed + # These will have their full standard representation, not minimal response + $Data = $Data | Where-Object { $_.createdDateTime -ge $LastTrigger } + Write-Information "Found $($Data.Count) created items." + } + 'updated' { + # Updated instances have at least updated properties but no @removed object + $Data = $Data | Where-Object { + $_.PSObject.Properties.Name -notcontains '@removed' -and + (!$_.createdDateTime -or $_.createdDateTime -lt $LastTrigger) + } + Write-Information "Found $($Data.Count) updated items." + } + } + + # Check if we have any data after event type filtering + if (($Data | Measure-Object).Count -eq 0) { + Write-Information "No data matches the event type filter '$EventType'. Conditions not met." + return @{ + ConditionsMet = $false + MatchedData = @() + TotalItems = ($Query.value | Measure-Object).Count + FilteredItems = 0 + MatchedItems = 0 + EventTypeFilter = $EventType + ChangeTypeSummary = @() + } + } + + if ($Trigger.UseConditions -eq $true -and $Trigger.Conditions) { + try { + # Parse conditions from JSON (similar to audit log processing) + $conditions = $Trigger.Conditions | ConvertFrom-Json | Where-Object { $_.Input.value -ne '' -and $_.Input.value -ne $null } + + if ($conditions) { + # Initialize collections for condition strings + $conditionStrings = [System.Collections.Generic.List[string]]::new() + $CIPPClause = [System.Collections.Generic.List[string]]::new() + + foreach ($condition in $conditions) { + # Handle array vs single values + $value = if ($condition.Input.value -is [array]) { + $arrayAsString = $condition.Input.value | ForEach-Object { + "'$_'" + } + "@($($arrayAsString -join ', '))" + } else { + "'$($condition.Input.value)'" + } + + # Build PowerShell condition string + $conditionStrings.Add("`$(`$_.$($condition.Property.label)) -$($condition.Operator.value) $value") + $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $value") + } + + # Join all conditions with AND + $finalCondition = $conditionStrings -join ' -AND ' + + Write-Information "Testing delta query conditions: $finalCondition" + Write-Information "Human readable: $($CIPPClause -join ' and ')" + + # Apply conditions to filter the data using a script block instead of Invoke-Expression + $scriptBlock = [scriptblock]::Create("param(`$_) $finalCondition") + $MatchedData = $Data | Where-Object $scriptBlock + } else { + Write-Information 'No valid conditions found in trigger configuration.' + $MatchedData = $Data + } + } catch { + Write-Warning "Error processing delta query conditions: $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + $MatchedData = @() + } + } else { + # No conditions specified, consider all data as matching + $MatchedData = $Data + } + + # Determine if conditions are met based on final matched data count + $ConditionsMet = ($MatchedData | Measure-Object).Count -gt 0 + + # Return results with matched data and change type summary + $changeTypeSummary = $MatchedData | Group-Object CIPPChangeType | ForEach-Object { + @{ + ChangeType = $_.Name + Count = $_.Count + } + } + + return @{ + ConditionsMet = $ConditionsMet + MatchedData = $MatchedData + TotalItems = ($Query.value | Measure-Object).Count + FilteredItems = ($Data | Measure-Object).Count + MatchedItems = ($MatchedData | Measure-Object).Count + EventTypeFilter = $EventType + ChangeTypeSummary = $changeTypeSummary + } + +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 index 9abeee5a440e..5aae136f1ba4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Applications/Push-UploadApplication.ps1 @@ -64,7 +64,7 @@ function Push-UploadApplication { foreach ($tenant in $tenants) { try { - $ApplicationList = (New-GraphGetRequest -Uri $baseuri -tenantid $tenant) | Where-Object { $_.DisplayName -eq $ChocoApp.Applicationname } + $ApplicationList = New-GraphGetRequest -Uri $baseuri -tenantid $tenant | Where-Object { $_.DisplayName -eq $ChocoApp.Applicationname -and ($_.'@odata.type' -eq '#microsoft.graph.win32LobApp' -or $_.'@odata.type' -eq '#microsoft.graph.winGetApp') } if ($ApplicationList.displayname.count -ge 1) { Write-LogMessage -api 'AppUpload' -tenant $tenant -message "$($ChocoApp.Applicationname) exists. Skipping this application" -Sev 'Info' continue diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index cbc9cc65fc8f..111af213af0e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -18,6 +18,54 @@ function Push-ExecScheduledCommand { # We don't need to expand groups here as that's handled in the orchestrator $TenantInfo = Get-Tenants -TenantFilter $Tenant + if ($task.Trigger) { + # Extract trigger data from the task and process + $Trigger = if (Test-Json -Json $task.Trigger) { $task.Trigger | ConvertFrom-Json } else { $task.Trigger } + $TriggerType = $Trigger.Type.value ?? $Trigger.Type + if ($TriggerType -eq 'DeltaQuery') { + $IsTriggerTask = $true + $DeltaUrl = Get-DeltaQueryUrl -TenantFilter $Tenant -PartitionKey $task.RowKey + $DeltaQuery = @{ + DeltaUrl = $DeltaUrl + TenantFilter = $Tenant + PartitionKey = $task.RowKey + } + $Query = New-GraphDeltaQuery @DeltaQuery + + $secondsToAdd = switch -Regex ($task.Recurrence) { + '(\d+)m$' { [int64]$matches[1] * 60 } + '(\d+)h$' { [int64]$matches[1] * 3600 } + '(\d+)d$' { [int64]$matches[1] * 86400 } + default { 0 } + } + + $Minutes = [int]($secondsToAdd / 60) + + $DeltaQueryConditions = @{ + Query = $Query + Trigger = $Trigger + TenantFilter = $Tenant + LastTrigger = [datetime]::UtcNow.AddMinutes(-$Minutes) + } + $DeltaResults = Test-DeltaQueryConditions @DeltaQueryConditions + + if (-not $DeltaResults.ConditionsMet) { + Write-Information "Delta query conditions not met for tenant $Tenant. Skipping execution." + # update interval + $nextRunUnixTime = [int64]$task.ScheduledTime + [int64]$secondsToAdd + $null = Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + TaskState = 'Planned' + ScheduledTime = [string]$nextRunUnixTime + } + return + } + } + } else { + $IsTriggerTask = $false + } + $null = Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey RowKey = $task.RowKey @@ -54,18 +102,72 @@ function Push-ExecScheduledCommand { Write-Information "Failed to remove parameters: $($_.Exception.Message)" } - Write-Information "Started Task: $($Item.Command) for tenant: $Tenant" - try { - + if ($IsTriggerTask -eq $true -and $Trigger.ExecutePerResource -ne $true) { + # iterate through paramters looking for %variables% and replace them with matched data from the delta query + # examples would be %id% to be the id of the result + # if %triggerdata% is found, pass the entire matched data object try { - Write-Information "Starting task: $($Item.Command) with parameters: $($commandParameters | ConvertTo-Json)" - $results = & $Item.Command @commandParameters + foreach ($key in $commandParameters.Keys) { + if ($commandParameters[$key] -is [string]) { + if ($commandParameters[$key] -match '^%(.*)%$') { + $variableName = $matches[1] + if ($variableName -eq 'triggerdata') { + Write-Information "Replacing parameter $key with full matched data object." + $commandParameters[$key] = $DeltaResults.MatchedData + } else { + # Replace with array of matched property values + Write-Information "Replacing parameter $key with matched data property '$variableName'." + $commandParameters[$key] = $DeltaResults.MatchedData | Select-Object -ExpandProperty $variableName + } + } + } + } } catch { - $results = "Task Failed: $($_.Exception.Message)" - $State = 'Failed' + Write-Information "Failed to process trigger data parameters: $($_.Exception.Message)" + } + } elseif ($IsTriggerTask -eq $true -and $Trigger.ExecutePerResource -eq $true) { + Write-Information 'This is a trigger task with ExecutePerResource set to true. Iterating through matched data to execute command per resource.' + $results = foreach ($dataItem in $DeltaResults.MatchedData) { + $individualCommandParameters = $commandParameters.Clone() + try { + foreach ($key in $individualCommandParameters.Keys) { + if ($individualCommandParameters[$key] -is [string]) { + if ($individualCommandParameters[$key] -match '^%(.*)%$') { + if ($matches[1] -eq 'triggerdata') { + Write-Information "Replacing parameter $key with full matched data object for individual execution." + $individualCommandParameters[$key] = $dataItem + } else { + $variableName = $matches[1] + Write-Information "Replacing parameter $key with matched data property '$variableName' for individual execution." + $individualCommandParameters[$key] = $dataItem.$variableName + } + } + } + } + } catch { + Write-Information "Failed to process trigger data parameters for individual execution: $($_.Exception.Message)" + } + try { + Write-Information "Executing command $($Item.Command) for individual matched data item with parameters: $($individualCommandParameters | ConvertTo-Json -Depth 10)" + & $Item.Command @individualCommandParameters + Write-Information "Results for individual execution: $($results | ConvertTo-Json -Depth 10)" + } catch { + Write-Information "Failed to execute command for individual matched data item: $($_.Exception.Message)" + } } + } - Write-Information 'Ran the command. Processing results' + try { + if (-not $Trigger.ExecutePerResource) { + try { + Write-Information "Starting task: $($Item.Command) for tenant: $Tenant with parameters: $($commandParameters | ConvertTo-Json)" + $results = & $Item.Command @commandParameters + } catch { + $results = "Task Failed: $($_.Exception.Message)" + $State = 'Failed' + } + Write-Information 'Ran the command. Processing results' + } Write-Information "Results: $($results | ConvertTo-Json -Depth 10)" if ($item.command -like 'Get-CIPPAlert*') { Write-Information 'This is an alert task. Processing results as alerts.' @@ -144,9 +246,15 @@ function Push-ExecScheduledCommand { Write-Information 'Sending task results to target. Updating the task state.' if ($Results) { - $TableDesign = '' + $TableDesign = '' $FinalResults = if ($results -is [array] -and $results[0] -is [string]) { $Results | ConvertTo-Html -Fragment -Property @{ l = 'Text'; e = { $_ } } } else { $Results | ConvertTo-Html -Fragment } - $HTML = $FinalResults -replace '', "This alert is for tenant $Tenant.

$TableDesign
" | Out-String + $HTML = $FinalResults -replace '
', "This alert is for tenant $Tenant.

$TableDesign
" | Out-String + + # Add alert comment if available + if ($task.AlertComment) { + $HTML += "

Alert Information

$($task.AlertComment)

" + } + $title = "$TaskType - $Tenant - $($task.Name)" Write-Information 'Scheduler: Sending the results to the target.' Write-Information "The content of results is: $Results" @@ -155,10 +263,11 @@ function Push-ExecScheduledCommand { '*email*' { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $Tenant } '*webhook*' { $Webhook = [PSCustomObject]@{ - 'tenantId' = $TenantInfo.customerId - 'Tenant' = $Tenant - 'TaskInfo' = $Item.TaskInfo - 'Results' = $Results + 'tenantId' = $TenantInfo.customerId + 'Tenant' = $Tenant + 'TaskInfo' = $Item.TaskInfo + 'Results' = $Results + 'AlertComment' = $task.AlertComment } Send-CIPPAlert -Type 'webhook' -Title $title -TenantFilter $Tenant -JSONContent $($Webhook | ConvertTo-Json -Depth 20) } @@ -167,7 +276,7 @@ function Push-ExecScheduledCommand { Write-Information 'Sent the results to the target. Updating the task state.' try { - if ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence)) { + if ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence) -or $Trigger.ExecutionMode.value -eq 'once') { Write-Information 'Recurrence empty or 0. Task is not recurring. Setting task state to completed.' Update-AzDataTableEntity -Force @Table -Entity @{ PartitionKey = $task.PartitionKey diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListTenantAllowBlockListAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListTenantAllowBlockListAllTenants.ps1 new file mode 100644 index 000000000000..e249acc59c92 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListTenantAllowBlockListAllTenants.ps1 @@ -0,0 +1,46 @@ +function Push-ListTenantAllowBlockListAllTenants { + <# + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding()] + param($Item) + + $Tenant = Get-Tenants -TenantFilter $Item.customerId + $domainName = $Tenant.defaultDomainName + $Table = Get-CIPPTable -TableName 'cacheTenantAllowBlockList' + $ListTypes = 'Sender', 'Url', 'FileHash', 'IP' + + try { + foreach ($ListType in $ListTypes) { + $Entries = New-ExoRequest -tenantid $domainName -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ ListType = $ListType } + foreach ($Entry in $Entries) { + $CleanEntry = $Entry | Select-Object -ExcludeProperty *'@data.type'*, *'(DateTime])'* + $CleanEntry | Add-Member -MemberType NoteProperty -Name Tenant -Value $domainName -Force + $CleanEntry | Add-Member -MemberType NoteProperty -Name ListType -Value $ListType -Force + $Entity = @{ + Entry = [string]($CleanEntry | ConvertTo-Json -Depth 10 -Compress) + RowKey = [string](New-Guid).Guid + PartitionKey = 'TenantAllowBlockList' + Tenant = [string]$domainName + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + } + } + } catch { + $ErrorEntry = [pscustomobject]@{ + Tenant = $domainName + ListType = 'Error' + Identity = 'Error' + DisplayName = "Could not retrieve tenant allow/block list: $($_.Exception.Message)" + Timestamp = (Get-Date).ToString('s') + } + $Entity = @{ + Entry = [string]($ErrorEntry | ConvertTo-Json -Depth 10 -Compress) + RowKey = [string](New-Guid).Guid + PartitionKey = 'TenantAllowBlockList' + Tenant = [string]$domainName + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force | Out-Null + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tenant Groups/Push-UpdateDynamicTenantGroup.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tenant Groups/Push-UpdateDynamicTenantGroup.ps1 new file mode 100644 index 000000000000..4a3af5c2a6bf --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Tenant Groups/Push-UpdateDynamicTenantGroup.ps1 @@ -0,0 +1,14 @@ +function Push-UpdateDynamicTenantGroup { + <# + .SYNOPSIS + Push an update to a Dynamic Tenant Group + .FUNCTIONALITY + Entrypoint + #> + + [CmdletBinding()] + param ($Item) + + Write-Information "Pushing update to Dynamic Tenant Group: $($Item.Name) (ID: $($Item.Id))" + Update-CIPPDynamicTenantGroups -GroupId $Item.Id +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecPartnerWebhook.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecPartnerWebhook.ps1 index 6a7a86b59a2c..21febc29a539 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecPartnerWebhook.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecPartnerWebhook.ps1 @@ -5,7 +5,7 @@ function Invoke-ExecPartnerWebhook { .ROLE CIPP.AppSettings.ReadWrite #> - Param($Request, $TriggerMetadata) + param($Request, $TriggerMetadata) switch ($Request.Query.Action) { 'ListEventTypes' { $Uri = 'https://api.partnercenter.microsoft.com/webhooks/v1/registration/events' @@ -72,7 +72,7 @@ function Invoke-ExecPartnerWebhook { } } - return @{ + return [HttpResponseContext]@{ StatusCode = [System.Net.HttpStatusCode]::OK Body = $Body } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetCippAlerts.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetCippAlerts.ps1 index 7fe97ed91c8d..6a50e28c547d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetCippAlerts.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-GetCippAlerts.ps1 @@ -53,7 +53,7 @@ function Invoke-GetCippAlerts { type = 'error' }) } - if ((!$env:WEBSITE_RUN_FROM_PACKAGE -or [string]::IsNullOrEmpty($env:WEBSITE_RUN_FROM_PACKAGE)) -and $env:AzureWebJobsStorage -ne 'UseDevelopmentStorage=true' -and $env:NonLocalHostAzurite -ne 'true') { + if (!(![string]::IsNullOrEmpty($env:WEBSITE_RUN_FROM_PACKAGE) -or ![string]::IsNullOrEmpty($env:DEPLOYMENT_STORAGE_CONNECTION_STRING)) -and $env:AzureWebJobsStorage -ne 'UseDevelopmentStorage=true' -and $env:NonLocalHostAzurite -ne 'true') { $Alerts.Add( @{ title = 'Function App in Write Mode' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 index f4eeda9925b4..c8e5114e8dbc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ListApiTest.ps1 @@ -7,8 +7,20 @@ function Invoke-ListApiTest { #> [CmdletBinding()] param($Request, $TriggerMetadata) + + $Response = @{} + $Response.Request = $Request + if ($env:DEBUG_ENV_VARS -eq 'true') { + $BlockedKeys = @('ApplicationSecret', 'RefreshToken', 'AzureWebJobsStorage', 'DEPLOYMENT_STORAGE_CONNECTION_STRING') + $EnvironmentVariables = [PSCustomObject]@{} + Get-ChildItem env: | Where-Object { $BlockedKeys -notcontains $_.Name } | ForEach-Object { + $EnvironmentVariables | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value + } + $Response.EnvironmentVariables = $EnvironmentVariables + } + return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = ($Request | ConvertTo-Json -Depth 5) + Body = $Response }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 index 99f3647bb342..4516ff5491c4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-AddScheduledItem.ps1 @@ -29,7 +29,14 @@ function Invoke-AddScheduledItem { $Result = "Error scheduling task: $($_.Exception.Message)" } } else { - $Result = Add-CIPPScheduledTask -Task $Request.Body -Headers $Request.Headers -hidden $hidden -DisallowDuplicateName $Request.Query.DisallowDuplicateName -DesiredStartTime $Request.Body.DesiredStartTime + $ScheduledTask = @{ + Task = $Request.Body + Headers = $Request.Headers + hidden = $hidden + DisallowDuplicateName = $Request.Query.DisallowDuplicateName + DesiredStartTime = $Request.Body.DesiredStartTime + } + $Result = Add-CIPPScheduledTask @ScheduledTask Write-LogMessage -headers $Request.Headers -API $APINAME -message $Result -Sev 'Info' } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 index ae0399aa8af8..be60e04ec9b2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItemDetails.ps1 @@ -23,7 +23,7 @@ function Invoke-ListScheduledItemDetails { # Retrieve the task information $TaskTable = Get-CIPPTable -TableName 'ScheduledTasks' - $Task = Get-CIPPAzDataTableEntity @TaskTable -Filter "RowKey eq '$RowKey' and PartitionKey eq 'ScheduledTask'" | Select-Object RowKey, Name, TaskState, Command, Parameters, Recurrence, ExecutedTime, ScheduledTime, PostExecution, Tenant, TenantGroup, Hidden, Results, Timestamp + $Task = Get-CIPPAzDataTableEntity @TaskTable -Filter "RowKey eq '$RowKey' and PartitionKey eq 'ScheduledTask'" | Select-Object RowKey, Name, TaskState, Command, Parameters, Recurrence, ExecutedTime, ScheduledTime, PostExecution, Tenant, TenantGroup, Hidden, Results, Timestamp, Trigger if (-not $Task) { return ([HttpResponseContext]@{ @@ -81,6 +81,18 @@ function Invoke-ListScheduledItemDetails { $Task.Tenant = $TenantForDisplay } + if ($Task.Trigger) { + try { + $TriggerObject = $Task.Trigger | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($TriggerObject) { + $Task | Add-Member -NotePropertyName Trigger -NotePropertyValue $TriggerObject -Force + } + } catch { + Write-Warning "Failed to parse trigger information for task $($Task.RowKey): $($_.Exception.Message)" + # Fall back to keeping original trigger value + } + } + # Get the results if available $ResultsTable = Get-CIPPTable -TableName 'ScheduledTaskResults' $ResultsFilter = "PartitionKey eq '$RowKey'" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 index d20d240529e3..91829dddb4b4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 @@ -101,6 +101,17 @@ function Invoke-ListScheduledItems { type = 'Tenant' } } + if ($Task.Trigger) { + try { + $TriggerObject = $Task.Trigger | ConvertFrom-Json -ErrorAction SilentlyContinue + if ($TriggerObject) { + $Task | Add-Member -NotePropertyName Trigger -NotePropertyValue $TriggerObject -Force + } + } catch { + Write-Warning "Failed to parse trigger information for task $($Task.RowKey): $($_.Exception.Message)" + # Fall back to keeping original trigger value + } + } $Task } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 new file mode 100644 index 000000000000..bd2a682429ac --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCreateDefaultGroups.ps1 @@ -0,0 +1,66 @@ +function Invoke-ExecCreateDefaultGroups { + <# + .SYNOPSIS + Create default tenant groups + .DESCRIPTION + This function creates a set of default tenant groups that are commonly used + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Groups.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + try { + $Table = Get-CippTable -tablename 'TenantGroups' + $Results = [System.Collections.Generic.List[object]]::new() + $ExistingGroups = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'TenantGroup' and Type eq 'dynamic'" + $DefaultGroups = '[{"PartitionKey":"TenantGroup","RowKey":"369d985e-0fba-48f9-844f-9f793b10a12c","Description":"This group does not have a license for intune, nor a license for Entra ID Premium","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Not Intune and Entra Premium Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"4dbca08b-7dc5-4e0f-bc25-14a90c8e0941","Description":"This group has atleast one Business Premium License available","Description@type":null,"DynamicRules":"[{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium\",\"value\":\"SPB\"}]},{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium (no Teams)\",\"value\":\"Microsoft_365_ Business_ Premium_(no Teams)\"}]},{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium Donation\",\"value\":\"Microsoft_365_Business_Premium_Donation_(Non_Profit_Pricing)\"}]},{\"property\":\"availableLicense\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft 365 Business Premium EEA (no Teams)\",\"value\":\"Office_365_w\/o_Teams_Bundle_Business_Premium\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"or","RuleLogic@type":null,"Name":"Business Premium License available","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"703c0e69-84a8-4dcf-a1c2-4986d2ccc850","Description":"This group does have a license for Entra Premium but does not have a license for Intune","Description@type":null,"DynamicRules":"[{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\",\"id\":\"41781fb2-bc02-4b7c-bd55-b576c07bb09d\"}]},{\"property\":\"availableServicePlan\",\"operator\":\"notIn\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\",\"id\":\"c1ec4a95-1f05-45b3-a911-aa3fa01094f5\"}]}]","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra Premium Capable, Not Intune Capable","Name@type":null},{"PartitionKey":"TenantGroup","RowKey":"c1dadbc0-f0b4-448c-a2e6-e1938ba102e0","Description":"This group has Intune and Entra ID Premium available","Description@type":null,"DynamicRules":"{\"property\":\"availableServicePlan\",\"operator\":\"in\",\"value\":[{\"label\":\"Microsoft Intune\",\"value\":\"INTUNE_A\"},{\"label\":\"Microsoft Entra ID P1\",\"value\":\"AAD_PREMIUM\"}]}","DynamicRules@type":null,"GroupType":"dynamic","GroupType@type":null,"RuleLogic":"and","RuleLogic@type":null,"Name":"Entra ID Premium and Intune Capable","Name@type":null}]' | ConvertFrom-Json + + + foreach ($Group in $DefaultGroups) { + # Check if group with same name already exists + $ExistingGroup = $ExistingGroups | Where-Object -Property Name -EQ $group.Name + if ($ExistingGroup) { + $Results.Add(@{ + resultText = "Group '$($Group.Name)' already exists, skipping" + state = 'warning' + }) + continue + } + $GroupEntity = @{ + PartitionKey = 'TenantGroup' + RowKey = $group.RowKey + Name = $Group.Name + Description = $Group.Description + GroupType = $Group.GroupType + DynamicRules = $Group.DynamicRules + RuleLogic = $Group.RuleLogic + } + Add-CIPPAzDataTableEntity @Table -Entity $GroupEntity -Force + + $Results.Add(@{ + resultText = "Created default group: '$($Group.Name)'" + state = 'success' + }) + + Write-LogMessage -API 'TenantGroups' -message "Created default tenant group: $($Group.Name)" -sev Info + } + + $Body = @{ Results = $Results } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + }) + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'TenantGroups' -message "Failed to create default groups: $ErrorMessage" -sev Error + $Body = @{ Results = "Failed to create default groups: $ErrorMessage" } + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = $Body + }) + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunTenantGroupRule.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunTenantGroupRule.ps1 new file mode 100644 index 000000000000..31aaec5c4d31 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRunTenantGroupRule.ps1 @@ -0,0 +1,40 @@ +function Invoke-ExecRunTenantGroupRule { + <# + .SYNOPSIS + Execute tenant group dynamic rules immediately + .DESCRIPTION + This function executes dynamic tenant group rules for immediate membership updates + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Tenant.Groups.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $GroupId = $Request.Body.groupId ?? $Request.Query.groupId + + try { + $GroupTable = Get-CippTable -tablename 'TenantGroups' + $Group = Get-CIPPAzDataTableEntity @GroupTable -Filter "PartitionKey eq 'TenantGroup' and RowKey eq '$GroupId'" + + if (-not $Group) { $Body = @{ Results = 'Group not found' } } + + $null = Start-TenantDynamicGroupOrchestrator -GroupId $GroupId + + $Body = @{ Results = "Dynamic rules executed successfully for group '$($Group.Name)'. Processing will continue in the background. Check the logbook for details." } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + }) + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'TenantGroups' -message "Failed to execute tenant group rules: $ErrorMessage" -sev Error + $Body = @{ Results = "Failed to execute dynamic rules: $ErrorMessage" } + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = $Body + }) + } +} 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 b99383a63b14..335c5b80c1fa 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 @@ -19,6 +19,9 @@ function Invoke-ExecTenantGroup { $groupName = $Request.Body.groupName $groupDescription = $Request.Body.groupDescription $members = $Request.Body.members + $groupType = $Request.Body.groupType ?? 'static' + $dynamicRules = $Request.Body.dynamicRules + $ruleLogic = $Request.Body.ruleLogic ?? 'and' switch ($Action) { 'AddEdit' { @@ -32,6 +35,13 @@ function Invoke-ExecTenantGroup { if ($groupDescription) { $GroupEntity.Description = $groupDescription } + $GroupEntity.GroupType = $groupType + if ($groupType -eq 'dynamic' -and $dynamicRules) { + $GroupEntity.DynamicRules = "$($dynamicRules | ConvertTo-Json -depth 100 -Compress)" + $GroupEntity | Add-Member -NotePropertyName 'RuleLogic' -NotePropertyValue $ruleLogic -Force + } else { + $GroupEntity | Add-Member -NotePropertyName 'RuleLogic' -NotePropertyValue $null -Force + } Add-CIPPAzDataTableEntity @Table -Entity $GroupEntity -Force } else { $GroupEntity = @{ @@ -39,39 +49,51 @@ function Invoke-ExecTenantGroup { RowKey = $groupId Name = $groupName Description = $groupDescription + GroupType = $groupType + } + if ($groupType -eq 'dynamic' -and $dynamicRules) { + $GroupEntity.DynamicRules = "$($dynamicRules | ConvertTo-Json -depth 100 -Compress)" + $GroupEntity.RuleLogic = $ruleLogic } Add-CIPPAzDataTableEntity @Table -Entity $GroupEntity -Force } - $CurrentMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "PartitionKey eq 'Member' and GroupId eq '$groupId'" - + # Handle members based on group type $Adds = [System.Collections.Generic.List[string]]::new() $Removes = [System.Collections.Generic.List[string]]::new() - # Add members - foreach ($member in $members) { - if ($CurrentMembers) { - $CurrentMember = $CurrentMembers | Where-Object { $_.customerId -eq $member.value } - if ($CurrentMember) { - continue + + if ($groupType -eq 'static') { + # Static group - manage members manually + $CurrentMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "PartitionKey eq 'Member' and GroupId eq '$groupId'" + + # Add members + foreach ($member in $members) { + if ($CurrentMembers) { + $CurrentMember = $CurrentMembers | Where-Object { $_.customerId -eq $member.value } + if ($CurrentMember) { + continue + } } + $MemberEntity = @{ + PartitionKey = 'Member' + 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) } - $MemberEntity = @{ - PartitionKey = 'Member' - 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) - } - if ($CurrentMembers) { - foreach ($CurrentMember in $CurrentMembers) { - if ($members.value -notcontains $CurrentMember.customerId) { - Remove-AzDataTableEntity @MembersTable -Entity $CurrentMember -Force - $Removes.Add('Removed member {0}' -f $CurrentMember.customerId) + if ($CurrentMembers) { + foreach ($CurrentMember in $CurrentMembers) { + if ($members.value -notcontains $CurrentMember.customerId) { + Remove-AzDataTableEntity @MembersTable -Entity $CurrentMember -Force + $Removes.Add('Removed member {0}' -f $CurrentMember.customerId) + } } } + } elseif ($groupType -eq 'dynamic') { + $Adds.Add('Dynamic group updated. Rules will be processed on next scheduled run.') } $Results.Add(@{ resultText = "Group '$groupName' saved successfully" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-AddTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-AddTenantAllowBlockList.ps1 index 65bdadcceb22..1d8e0d727b05 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-AddTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-AddTenantAllowBlockList.ps1 @@ -1,4 +1,4 @@ -Function Invoke-AddTenantAllowBlockList { +function Invoke-AddTenantAllowBlockList { <# .FUNCTIONALITY Entrypoint @@ -9,14 +9,24 @@ Function Invoke-AddTenantAllowBlockList { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + $BlockListObject = $Request.Body - if ($Request.Body.tenantId -eq 'AllTenants') { $Tenants = (Get-Tenants).defaultDomainName } else { $Tenants = @($Request.body.tenantId) } + $TenantID = $Request.Body.tenantID.value ?? $Request.Body.tenantID + + if ($TenantID -eq 'AllTenants') { + $Tenants = (Get-Tenants).defaultDomainName + } elseif ($TenantID -is [array]) { + $Tenants = $TenantID + } else { + $Tenants = @($TenantID) + } $Results = [System.Collections.Generic.List[string]]::new() $Entries = @() if ($BlockListObject.entries -is [array]) { $Entries = $BlockListObject.entries } else { - $Entries = @($BlockListObject.entries -split "[,;]" | Where-Object { $_ -ne "" } | ForEach-Object { $_.Trim() }) + $Entries = @($BlockListObject.entries -split '[,;]' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.Trim() }) } foreach ($Tenant in $Tenants) { try { @@ -38,19 +48,20 @@ Function Invoke-AddTenantAllowBlockList { } New-ExoRequest @ExoRequest - - $results.add("Successfully added $($BlockListObject.Entries) as type $($BlockListObject.ListType) to the $($BlockListObject.listMethod) list for $tenant") - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $Tenant -message $result -Sev 'Info' + $Result = "Successfully added $($BlockListObject.Entries) as type $($BlockListObject.ListType) to the $($BlockListObject.listMethod) list for $tenant" + $Results.Add($Result) + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message $Result -Sev 'Info' } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $results.add("Failed to create blocklist. Error: $ErrorMessage") - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $Tenant -message $result -Sev 'Error' + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to create blocklist. Error: $($ErrorMessage.NormalizedError)" + $Results.Add($Result) + Write-LogMessage -headers $Headers -API $APIName -tenant $Tenant -message $Result -Sev 'Error' -LogData $ErrorMessage } } return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @{ - 'Results' = $results + 'Results' = $Results 'Request' = $ExoRequest } }) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 index 00b6f123e7df..54333e58ff47 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneTemplates.ps1 @@ -39,6 +39,7 @@ function Invoke-ListIntuneTemplates { $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $JSONData.Type -Force $data | Add-Member -NotePropertyName 'GUID' -NotePropertyValue $_.RowKey -Force $data | Add-Member -NotePropertyName 'package' -NotePropertyValue $_.Package -Force + $data | Add-Member -NotePropertyName 'isSynced' -NotePropertyValue (![string]::IsNullOrEmpty($_.SHA)) $data } catch { @@ -84,7 +85,7 @@ function Invoke-ListIntuneTemplates { return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = ($Templates | ConvertTo-Json -Depth 100) + Body = ConvertTo-Json -Depth 100 -InputObject @($Templates) }) } 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 8fee00823f75..56325421b363 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 @@ -14,16 +14,22 @@ function Invoke-AddGroupTemplate { throw 'You must enter a displayname' } - # Normalize group type to match New-CIPPGroup expectations (handle both camelCase and lowercase) + # Normalize group type to match New-CIPPGroup expectations + # Handle values from ListGroups calculatedGroupType and frontend form values $groupType = switch -wildcard ($Request.Body.groupType.ToLower()) { - '*dynamicdistribution*' { 'dynamicDistribution'; break } # Check this first before *dynamic* and *distribution* + # Values from ListGroups calculatedGroupType + '*mail-enabled security*' { 'security'; break } + '*microsoft 365*' { 'm365'; break } + '*distribution list*' { 'distribution'; break } + # Dynamic groups don't have a specific calculatedGroupType - they're detected by membershipRule + # Frontend form values (camelCase and lowercase) + '*dynamicdistribution*' { 'dynamicDistribution'; break } '*dynamic*' { 'dynamic'; break } '*azurerole*' { 'azureRole'; break } '*unified*' { 'm365'; break } - '*microsoft*' { 'm365'; break } '*m365*' { 'm365'; break } '*generic*' { 'generic'; break } - '*security*' { 'security'; break } + '*security*' { 'generic'; break } '*distribution*' { 'distribution'; break } '*mail*' { 'distribution'; break } default { $Request.Body.groupType } @@ -31,18 +37,19 @@ function Invoke-AddGroupTemplate { # Override to dynamic if membership rules are provided (for backward compatibility) # but only if it's not already a dynamicDistribution group - if ($Request.body.membershipRules -and $groupType -notin @('dynamicDistribution')) { + if ($Request.body.membershipRules -and ![string]::IsNullOrEmpty($Request.Body.membershipRules) -and $Request.Body.membershipRules -ne 'membershipRule' -and $groupType -notin @('dynamicDistribution')) { $groupType = 'dynamic' } # Normalize field names to handle different casing from various forms $displayName = $Request.Body.displayName ?? $Request.Body.Displayname ?? $Request.Body.displayname $description = $Request.Body.description ?? $Request.Body.Description + $MembershipRules = ([string]::IsNullOrEmpty($Request.Body.membershipRules) -or $Request.Body.membershipRules -eq 'membershipRule') ? $null : $Request.Body.membershipRules $object = [PSCustomObject]@{ displayName = $displayName description = $description groupType = $groupType - membershipRules = $Request.Body.membershipRules + membershipRules = $MembershipRules allowExternal = $Request.Body.allowExternal username = $Request.Body.username # Can contain variables like @%tenantfilter% GUID = $GUID 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 a98aae5fecd9..a94233ce1e8c 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 @@ -371,6 +371,20 @@ function Invoke-EditGroup { } } + # Only process visibility if it was explicitly sent for Microsoft 365 groups + if ($GroupType -eq 'Microsoft 365' -and -not [string]::IsNullOrWhiteSpace($UserObj.visibility)) { + try { + $VisibilityValue = $UserObj.visibility + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/groups/$($GroupID)" -type PATCH -tenantid $TenantId -body (@{'visibility' = $VisibilityValue } | ConvertTo-Json) + + $Results.Add("Set group visibility to $VisibilityValue for $($GroupName).") + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Warning "Error in visibility: $($ErrorMessage.NormalizedError) - $($_.InvocationInfo.ScriptLineNumber)" + $Results.Add("Failed to set group visibility for $($GroupName): $($ErrorMessage.NormalizedError)") + } + } + # Only process sendCopies if it was explicitly sent if ($null -ne $UserObj.sendCopies) { try { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 index e5dfa9108b16..f2f642edc5d8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupTemplates.ps1 @@ -16,7 +16,12 @@ function Invoke-ListGroupTemplates { $data = $_.JSON | ConvertFrom-Json # Normalize groupType to camelCase for consistent frontend handling + # Handle both stored normalized values and legacy values $normalizedGroupType = switch -Wildcard ($data.groupType.ToLower()) { + # Already normalized values (most common) + 'dynamicdistribution' { 'dynamicDistribution'; break } + 'azurerole' { 'azureRole'; break } + # Legacy values that might exist in stored templates '*dynamicdistribution*' { 'dynamicDistribution'; break } '*dynamic*' { 'dynamic'; break } '*azurerole*' { 'azureRole'; break } 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 471d4951051f..890ab08c3748 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 @@ -6,280 +6,162 @@ function Invoke-ExecJITAdmin { Identity.Role.ReadWrite .DESCRIPTION - Just-in-time admin management API endpoint. This function can list JIT admins, create users, add roles, remove roles, delete, or disable a user. + Just-in-time admin management API endpoint. This function can create users, add roles, remove roles, delete, or disable a user. #> [CmdletBinding()] param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint - $User = $Request.Headers + $Headers = $Request.Headers $TenantFilter = $Request.Body.tenantFilter.value ? $Request.Body.tenantFilter.value : $Request.Body.tenantFilter - Write-LogMessage -Headers $User -API $APIName -message 'Accessed this API' -Sev 'Debug' - if ($Request.Query.Action -eq 'List') { - # TODO: The list functionality should be moved to a separate function. ListJITAdmin or similar. - $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 - if ($Request.Query.TenantFilter -ne 'AllTenants') { - # Single tenant logic - $Query = @{ - TenantFilter = $Request.Query.TenantFilter - Endpoint = 'users' - Parameters = @{ - '$count' = 'true' - '$select' = "id,accountEnabled,displayName,userPrincipalName,$($Schema.id)" - '$filter' = "$($Schema.id)/jitAdminEnabled eq true or $($Schema.id)/jitAdminEnabled eq false" - } - } - $Users = Get-GraphRequestList @Query | Where-Object { $_.id } - $BulkRequests = $Users | ForEach-Object { @( - @{ - id = $_.id - method = 'GET' - url = "users/$($_.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName" - } - ) - } - $RoleResults = New-GraphBulkRequest -tenantid $Request.Query.TenantFilter -Requests @($BulkRequests) - #Write-Information ($RoleResults | ConvertTo-Json -Depth 10 ) - $Results = $Users | ForEach-Object { - $MemberOf = ($RoleResults | Where-Object -Property id -EQ $_.id).body.value | Select-Object displayName, id - [PSCustomObject]@{ - id = $_.id - displayName = $_.displayName - userPrincipalName = $_.userPrincipalName - accountEnabled = $_.accountEnabled - jitAdminEnabled = $_.($Schema.id).jitAdminEnabled - jitAdminExpiration = $_.($Schema.id).jitAdminExpiration - jitAdminReason = $_.($Schema.id).jitAdminReason - memberOf = $MemberOf - } - } - #Write-Information ($Results | ConvertTo-Json -Depth 10) - $Body = @{ - Results = @($Results) - Metadata = @{ - Parameters = $Query.Parameters - } - } - } else { - # AllTenants logic - $Results = [System.Collections.Generic.List[object]]::new() - $Metadata = @{} - $Table = Get-CIPPTable -TableName CacheJITAdmin - $PartitionKey = 'JITAdminUser' - $Filter = "PartitionKey eq '$PartitionKey'" - $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-60) + 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 + } - $QueueReference = '{0}-{1}' -f $Request.Query.TenantFilter, $PartitionKey # $TenantFilter is 'AllTenants' - Write-Information "QueueReference: $QueueReference" - $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + $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 ($RunningQueue) { - $Metadata = [PSCustomObject]@{ - QueueMessage = 'Still loading JIT Admin data for all tenants. Please check back in a few more minutes.' - QueueId = $RunningQueue.RowKey - } - } elseif (!$Rows -and !$RunningQueue) { - $TenantList = Get-Tenants -IncludeErrors - $Queue = New-CippQueueEntry -Name 'JIT Admin List - All Tenants' -Link '/identity/administration/jit-admin?tenantFilter=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count + if ($Request.Body.userAction -eq 'create') { + $Domain = $Request.Body.Domain.value ? $Request.Body.Domain.value : $Request.Body.Domain + $Username = "$($Request.Body.Username)@$($Domain)" + Write-Information "Creating JIT Admin user: $($Request.Body.username)" - $Metadata = [PSCustomObject]@{ - QueueMessage = 'Loading JIT Admin data for all tenants. Please check back in a few minutes.' - QueueId = $Queue.RowKey - } - $InputObject = [PSCustomObject]@{ - OrchestratorName = 'JITAdminOrchestrator' - QueueFunction = @{ - FunctionName = 'GetTenants' - QueueId = $Queue.RowKey - TenantParams = @{ - IncludeErrors = $true - } - DurableName = 'ExecJITAdminListAllTenants' - } - SkipLog = $true - } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - } else { - $Metadata = [PSCustomObject]@{ - QueueId = $RunningQueue.RowKey ?? $null - } - # There is data in the cache, so we will use that - Write-Information "Found $($Rows.Count) rows in the cache" - foreach ($row in $Rows) { - $UserObject = $row.JITAdminUser | ConvertFrom-Json - $Results.Add( - [PSCustomObject]@{ - Tenant = $row.Tenant - id = $UserObject.id - displayName = $UserObject.displayName - userPrincipalName = $UserObject.userPrincipalName - accountEnabled = $UserObject.accountEnabled - jitAdminEnabled = $UserObject.jitAdminEnabled - jitAdminExpiration = $UserObject.jitAdminExpiration - jitAdminReason = $UserObject.jitAdminReason - memberOf = $UserObject.memberOf - } - ) - } - } - $Body = @{ - Results = @($Results) - Metadata = $Metadata + $JITAdmin = @{ + User = @{ + 'FirstName' = $Request.Body.FirstName + 'LastName' = $Request.Body.LastName + 'UserPrincipalName' = $Username } + Expiration = $Expiration + Reason = $Request.Body.reason + Action = 'Create' + TenantFilter = $TenantFilter } - } else { - - 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 + $CreateResult = Set-CIPPUserJITAdmin @JITAdmin + Write-LogMessage -Headers $Headers -API $APIName -tenant $TenantFilter -message "Created JIT Admin user: $Username. Reason: $($Request.Body.reason). Roles: $($Request.Body.adminRoles.label -join ', ')" -Sev 'Info' -LogData $JITAdmin + $Results.Add("Created User: $Username") + if (!$Request.Body.UseTAP) { + $Results.Add("Password: $($CreateResult.password)") } + $Results.Add("JIT Admin Expires: $($Expiration)") + Start-Sleep -Seconds 1 + } - $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') { - $Domain = $Request.Body.Domain.value ? $Request.Body.Domain.value : $Request.Body.Domain - $Username = "$($Request.Body.Username)@$($Domain)" - Write-Information "Creating JIT Admin user: $($Request.Body.username)" - - $JITAdmin = @{ - User = @{ - 'FirstName' = $Request.Body.FirstName - 'LastName' = $Request.Body.LastName - 'UserPrincipalName' = $Username + #Region TAP creation + if ($Request.Body.UseTAP) { + try { + if ($Start -gt (Get-Date)) { + $TapParams = @{ + startDateTime = [System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate).DateTime } - Expiration = $Expiration - Reason = $Request.Body.reason - Action = 'Create' - TenantFilter = $TenantFilter - } - $CreateResult = Set-CIPPUserJITAdmin @JITAdmin - Write-LogMessage -Headers $User -API $APIName -tenant $TenantFilter -message "Created JIT Admin user: $Username. Reason: $($Request.Body.reason). Roles: $($Request.Body.adminRoles.label -join ', ')" -Sev 'Info' -LogData $JITAdmin - $Results.Add("Created User: $Username") - if (!$Request.Body.UseTAP) { - $Results.Add("Password: $($CreateResult.password)") + $TapBody = ConvertTo-Json -Depth 5 -InputObject $TapParams + } else { + $TapBody = '{}' } - $Results.Add("JIT Admin Expires: $($Expiration)") - Start-Sleep -Seconds 1 - } - - #Region TAP creation - if ($Request.Body.UseTAP) { - try { - if ($Start -gt (Get-Date)) { - $TapParams = @{ - startDateTime = [System.DateTimeOffset]::FromUnixTimeSeconds($Request.Body.StartDate).DateTime - } - $TapBody = ConvertTo-Json -Depth 5 -InputObject $TapParams - } else { - $TapBody = '{}' - } - # Write-Information "https://graph.microsoft.com/beta/users/$Username/authentication/temporaryAccessPassMethods" - # Retry creating the TAP up to 10 times, since it can fail due to the user not being fully created yet. Sometimes it takes 2 reties, sometimes it takes 8+. Very annoying. -Bobby - $Retries = 0 - $MAX_TAP_RETRIES = 10 - do { - try { - $TapRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($Username)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body $TapBody - } catch { - Start-Sleep -Seconds 2 - Write-Information "ERROR: Run $Retries of $MAX_TAP_RETRIES : Failed to create TAP, retrying" - # Write-Information ( ConvertTo-Json -Depth 5 -InputObject (Get-CippException -Exception $_)) - } - $Retries++ - } while ( $null -eq $TapRequest.temporaryAccessPass -and $Retries -le $MAX_TAP_RETRIES ) - - $TempPass = $TapRequest.temporaryAccessPass - $PasswordExpiration = $TapRequest.LifetimeInMinutes - - $PasswordLink = New-PwPushLink -Payload $TempPass - $Password = $PasswordLink ? $PasswordLink : $TempPass - - $Results.Add("Temporary Access Pass: $Password") - $Results.Add("This TAP is usable starting at $($TapRequest.startDateTime) UTC for the next $PasswordExpiration minutes") - } catch { - $Results.Add('Failed to create TAP, if this is not yet enabled, use the Standards to push the settings to the tenant.') - Write-Information (Get-CippException -Exception $_ | ConvertTo-Json -Depth 5) - if ($Password) { - $Results.Add("Password: $Password") + # Write-Information "https://graph.microsoft.com/beta/users/$Username/authentication/temporaryAccessPassMethods" + # Retry creating the TAP up to 10 times, since it can fail due to the user not being fully created yet. Sometimes it takes 2 reties, sometimes it takes 8+. Very annoying. -Bobby + $Retries = 0 + $MAX_TAP_RETRIES = 10 + do { + try { + $TapRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/users/$($Username)/authentication/temporaryAccessPassMethods" -tenantid $TenantFilter -type POST -body $TapBody + } catch { + Start-Sleep -Seconds 2 + Write-Information "ERROR: Run $Retries of $MAX_TAP_RETRIES : Failed to create TAP, retrying" + # Write-Information ( ConvertTo-Json -Depth 5 -InputObject (Get-CippException -Exception $_)) } + $Retries++ + } while ( $null -eq $TapRequest.temporaryAccessPass -and $Retries -le $MAX_TAP_RETRIES ) + + $TempPass = $TapRequest.temporaryAccessPass + $PasswordExpiration = $TapRequest.LifetimeInMinutes + + $PasswordLink = New-PwPushLink -Payload $TempPass + $Password = $PasswordLink ? $PasswordLink : $TempPass + + $Results.Add("Temporary Access Pass: $Password") + $Results.Add("This TAP is usable starting at $($TapRequest.startDateTime) UTC for the next $PasswordExpiration minutes") + } catch { + $Results.Add('Failed to create TAP, if this is not yet enabled, use the Standards to push the settings to the tenant.') + Write-Information (Get-CippException -Exception $_ | ConvertTo-Json -Depth 5) + if ($Password) { + $Results.Add("Password: $Password") } } - #EndRegion TAP creation + } + #EndRegion TAP creation - $Parameters = @{ - TenantFilter = $TenantFilter - User = @{ - 'UserPrincipalName' = $Username - } - Roles = $Request.Body.AdminRoles.value - Action = 'AddRoles' - Reason = $Request.Body.Reason - Expiration = $Expiration - } - if ($Start -gt (Get-Date)) { - $TaskBody = @{ - TenantFilter = $TenantFilter - Name = "JIT Admin (enable): $Username" - Command = @{ - value = 'Set-CIPPUserJITAdmin' - label = 'Set-CIPPUserJITAdmin' - } - Parameters = [pscustomobject]$Parameters - ScheduledTime = $Request.Body.StartDate - PostExecution = @{ - Webhook = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'webhook') - Email = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'email') - PSA = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'PSA') - } - } - Add-CIPPScheduledTask -Task $TaskBody -hidden $false - if ($Request.Body.userAction -ne 'create') { - Set-CIPPUserJITAdminProperties -TenantFilter $TenantFilter -UserId $Request.Body.existingUser.value -Expiration $Expiration -Reason $Request.Body.Reason - } - $Results.Add("Scheduling JIT Admin enable task for $Username") - Write-LogMessage -Headers $User -API $APIName -message "Scheduling JIT Admin for existing user: $Username. Reason: $($Request.Body.reason). Roles: $($Request.Body.adminRoles.label -join ', ') " -tenant $TenantFilter -Sev 'Info' - } else { - $Results.Add("Executing JIT Admin enable task for $Username") - Set-CIPPUserJITAdmin @Parameters - Write-LogMessage -Headers $User -API $APIName -message "Executing JIT Admin for existing user: $Username. Reason: $($Request.Body.reason). Roles: $($Request.Body.adminRoles.label -join ', ') " -tenant $TenantFilter -Sev 'Info' + $Parameters = @{ + TenantFilter = $TenantFilter + User = @{ + 'UserPrincipalName' = $Username } - - $DisableTaskBody = [pscustomobject]@{ + Roles = $Request.Body.AdminRoles.value + Action = 'AddRoles' + Reason = $Request.Body.Reason + Expiration = $Expiration + } + if ($Start -gt (Get-Date)) { + $TaskBody = @{ TenantFilter = $TenantFilter - Name = "JIT Admin ($($Request.Body.ExpireAction.value)): $Username" + Name = "JIT Admin (enable): $Username" Command = @{ value = 'Set-CIPPUserJITAdmin' label = 'Set-CIPPUserJITAdmin' } - Parameters = [pscustomobject]@{ - TenantFilter = $TenantFilter - User = @{ - 'UserPrincipalName' = $Username - } - Roles = $Request.Body.AdminRoles.value - Reason = $Request.Body.Reason - Action = $Request.Body.ExpireAction.value - } + Parameters = [pscustomobject]$Parameters + ScheduledTime = $Request.Body.StartDate PostExecution = @{ Webhook = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'webhook') Email = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'email') PSA = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'PSA') } - ScheduledTime = $Request.Body.EndDate } - $null = Add-CIPPScheduledTask -Task $DisableTaskBody -hidden $false - $Results.Add("Scheduling JIT Admin $($Request.Body.ExpireAction.value) task for $Username") - $Body = @{ - Results = @($Results) + Add-CIPPScheduledTask -Task $TaskBody -hidden $false + if ($Request.Body.userAction -ne 'create') { + Set-CIPPUserJITAdminProperties -TenantFilter $TenantFilter -UserId $Request.Body.existingUser.value -Expiration $Expiration -Reason $Request.Body.Reason } + $Results.Add("Scheduling JIT Admin enable task for $Username") + Write-LogMessage -Headers $Headers -API $APIName -message "Scheduling JIT Admin for existing user: $Username. Reason: $($Request.Body.reason). Roles: $($Request.Body.adminRoles.label -join ', ') " -tenant $TenantFilter -Sev 'Info' + } else { + $Results.Add("Executing JIT Admin enable task for $Username") + Set-CIPPUserJITAdmin @Parameters + Write-LogMessage -Headers $Headers -API $APIName -message "Executing JIT Admin for existing user: $Username. Reason: $($Request.Body.reason). Roles: $($Request.Body.adminRoles.label -join ', ') " -tenant $TenantFilter -Sev 'Info' } + $DisableTaskBody = [pscustomobject]@{ + TenantFilter = $TenantFilter + Name = "JIT Admin ($($Request.Body.ExpireAction.value)): $Username" + Command = @{ + value = 'Set-CIPPUserJITAdmin' + label = 'Set-CIPPUserJITAdmin' + } + Parameters = [pscustomobject]@{ + TenantFilter = $TenantFilter + User = @{ + 'UserPrincipalName' = $Username + } + Roles = $Request.Body.AdminRoles.value + Reason = $Request.Body.Reason + Action = $Request.Body.ExpireAction.value + } + PostExecution = @{ + Webhook = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'webhook') + Email = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'email') + PSA = [bool]($Request.Body.PostExecution | Where-Object -Property value -EQ 'PSA') + } + ScheduledTime = $Request.Body.EndDate + } + $null = Add-CIPPScheduledTask -Task $DisableTaskBody -hidden $false + $Results.Add("Scheduling JIT Admin $($Request.Body.ExpireAction.value) task for $Username") + # 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. return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $Body + Body = @{'Results' = @($Results) } }) + } 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 f0b502bc7faa..17636856babf 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 @@ -1,4 +1,4 @@ -Function Invoke-ExecRestoreDeleted { +function Invoke-ExecRestoreDeleted { <# .FUNCTIONALITY Entrypoint @@ -19,7 +19,7 @@ Function Invoke-ExecRestoreDeleted { $DisplayName = $Request.Body.displayName try { - $null = New-GraphPostRequest -uri "https://graph.microsoft.com/v1.0/directory/deletedItems/$($RequestID)/restore" -tenantid $TenantFilter -type POST -body '{}' -Verbose + $null = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/directory/deletedItems/$($RequestID)/restore" -tenantid $TenantFilter -type POST -body '{}' -Verbose $Result = "Successfully restored deleted item with ID: '$($RequestID)'" if ($UserPrincipalName) { $Result += " User Principal Name: '$($UserPrincipalName)'" } if ($DisplayName) { $Result += " Display Name: '$($DisplayName)'" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 index 367083be7789..72fed7ce5282 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListDeletedItems.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListDeletedItems { +function Invoke-ListDeletedItems { <# .FUNCTIONALITY Entrypoint @@ -8,12 +8,29 @@ Function Invoke-ListDeletedItems { [CmdletBinding()] param($Request, $TriggerMetadata) $TenantFilter = $Request.Query.tenantFilter - # Interact with query parameters or the body of the request. - $Types = 'Application', 'User', 'Group' + + $Types = @('administrativeUnit', 'application', 'externalUserProfile', 'pendingExternalUserProfile', 'user', 'group', 'servicePrincipal', 'certificateBasedAuthPki', 'certificateAuthorityDetail') + $Requests = foreach ($Type in $Types) { + [PSCustomObject]@{ + id = $Type + url = "directory/deletedItems/microsoft.graph.$($Type)" + method = 'GET' + } + } + + $BulkResults = New-GraphBulkRequest -Requests @($Requests) -tenantid $TenantFilter + $GraphRequest = foreach ($Type in $Types) { - (New-GraphGetRequest -uri "https://graph.microsoft.com/beta/directory/deletedItems/microsoft.graph.$($Type)" -tenantid $TenantFilter) | - Where-Object -Property '@odata.context' -NotLike '*graph.microsoft.com*' | - Select-Object *, @{ Name = 'TargetType'; Expression = { $Type } } + # pretty format the type name + $FormattedType = (Get-Culture).TextInfo.ToTitleCase(($Type -creplace '([A-Z])', ' $1').Trim()) + + $Result = $BulkResults | Where-Object { $_.id -eq $Type } + if ($Result.status -eq 200) { + $Result.body.value | ForEach-Object { + $_ | Add-Member -NotePropertyName 'TargetType' -NotePropertyValue $FormattedType + $_ + } + } } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 new file mode 100644 index 000000000000..9c4ca15bc5d6 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 @@ -0,0 +1,127 @@ +function Invoke-ListJITAdmin { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Identity.Role.Read + + .DESCRIPTION + List Just-in-time admin users for a tenant or all tenants. + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + + $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 + $TenantFilter = $Request.Query.TenantFilter + + if ($TenantFilter -ne 'AllTenants') { + # Single tenant logic + $Query = @{ + TenantFilter = $TenantFilter + Endpoint = 'users' + Parameters = @{ + '$count' = 'true' + '$select' = "id,accountEnabled,displayName,userPrincipalName,$($Schema.id)" + '$filter' = "$($Schema.id)/jitAdminEnabled eq true or $($Schema.id)/jitAdminEnabled eq false" + } + } + $Users = Get-GraphRequestList @Query | Where-Object { $_.id } + $BulkRequests = $Users | ForEach-Object { @( + @{ + id = $_.id + method = 'GET' + url = "users/$($_.id)/memberOf/microsoft.graph.directoryRole/?`$select=id,displayName" + } + ) + } + $RoleResults = New-GraphBulkRequest -tenantid $TenantFilter -Requests @($BulkRequests) + # Write-Information ($RoleResults | ConvertTo-Json -Depth 10 ) + $Results = $Users | ForEach-Object { + $MemberOf = ($RoleResults | Where-Object -Property id -EQ $_.id).body.value | Select-Object displayName, id + [PSCustomObject]@{ + id = $_.id + displayName = $_.displayName + userPrincipalName = $_.userPrincipalName + accountEnabled = $_.accountEnabled + jitAdminEnabled = $_.($Schema.id).jitAdminEnabled + jitAdminExpiration = $_.($Schema.id).jitAdminExpiration + jitAdminReason = $_.($Schema.id).jitAdminReason + memberOf = $MemberOf + } + } + + # Write-Information ($Results | ConvertTo-Json -Depth 10) + $Metadata = [PSCustomObject]@{Parameters = $Query.Parameters } + } else { + # AllTenants logic + $Results = [System.Collections.Generic.List[object]]::new() + $Metadata = @{} + $Table = Get-CIPPTable -TableName CacheJITAdmin + $PartitionKey = 'JITAdminUser' + $Filter = "PartitionKey eq '$PartitionKey'" + $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-60) + + $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey # $TenantFilter is 'AllTenants' + Write-Information "QueueReference: $QueueReference" + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + + if ($RunningQueue) { + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Still loading JIT Admin data for all tenants. Please check back in a few more minutes.' + QueueId = $RunningQueue.RowKey + } + } elseif (!$Rows -and !$RunningQueue) { + $TenantList = Get-Tenants -IncludeErrors + $Queue = New-CippQueueEntry -Name 'JIT Admin List - All Tenants' -Link '/identity/administration/jit-admin?tenantFilter=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count + + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Loading JIT Admin data for all tenants. Please check back in a few minutes.' + QueueId = $Queue.RowKey + } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'JITAdminOrchestrator' + QueueFunction = @{ + FunctionName = 'GetTenants' + QueueId = $Queue.RowKey + TenantParams = @{ + IncludeErrors = $true + } + DurableName = 'ExecJITAdminListAllTenants' + } + SkipLog = $true + } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + # There is data in the cache, so we will use that + Write-Information "Found $($Rows.Count) rows in the cache" + foreach ($row in $Rows) { + $UserObject = $row.JITAdminUser | ConvertFrom-Json + $Results.Add( + [PSCustomObject]@{ + Tenant = $row.Tenant + id = $UserObject.id + displayName = $UserObject.displayName + userPrincipalName = $UserObject.userPrincipalName + accountEnabled = $UserObject.accountEnabled + jitAdminEnabled = $UserObject.jitAdminEnabled + jitAdminExpiration = $UserObject.jitAdminExpiration + jitAdminReason = $UserObject.jitAdminReason + memberOf = $UserObject.memberOf + } + ) + } + } + } + + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ + Results = @($Results) + Metadata = $Metadata + } + }) +} 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 4fea822f9bc6..df3cea078a04 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 @@ -20,6 +20,9 @@ function Invoke-ListUserSettings { $UserSettings = $UserSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue } catch { Write-Warning "Failed to convert UserSettings JSON: $($_.Exception.Message)" + } + + if (!$UserSettings) { $UserSettings = [pscustomobject]@{ direction = 'ltr' paletteMode = 'light' @@ -36,8 +39,7 @@ function Invoke-ListUserSettings { try { $UserSpecificSettings = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'UserSettings' and RowKey eq '$Username'" $UserSpecificSettings = $UserSpecificSettings.JSON | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue - } - catch { + } catch { Write-Warning "Failed to convert UserSpecificSettings JSON: $($_.Exception.Message)" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 index fcc01e55c49d..1bcc38c208a6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/New-CippCoreRequest.ps1 @@ -50,10 +50,17 @@ function New-CippCoreRequest { return ([HttpResponseContext]($HttpResponse | Select-Object -First 1)) } else { # If no valid response context found, create a default success response - return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $Response - }) + if ($Response.PSObject.Properties.Name -contains 'StatusCode' -and $Response.PSObject.Properties.Name -contains 'Body') { + return ([HttpResponseContext]@{ + StatusCode = $Response.StatusCode + Body = $Response.Body + }) + } else { + return ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Response + }) + } } } } catch { 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 91092b9c5db3..baa4e97d3fd0 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 @@ -23,6 +23,7 @@ function Invoke-ListSharepointAdminUrl { $Tenant | Add-Member -MemberType NoteProperty -Name SharepointAdminUrl -Value $SharePointInfo.AdminUrl $Table = Get-CIPPTable -TableName 'Tenants' Add-CIPPAzDataTableEntity @Table -Entity $Tenant -Force + $AdminUrl = $SharePointInfo.AdminUrl } if ($Request.Query.ReturnUrl) { 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 66636e28359a..a683b153c508 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 @@ -22,6 +22,7 @@ Function Invoke-AddAlert { type = $Request.Body.logbook.value RowKey = $RowKey PartitionKey = 'Webhookv2' + AlertComment = [string]$Request.Body.AlertComment } $WebhookTable = Get-CippTable -TableName 'WebhookRules' Add-CIPPAzDataTableEntity @WebhookTable -Entity $CompleteObject -Force diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 index 7b772b04c6c8..529486e9f868 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 @@ -32,6 +32,7 @@ function Invoke-ListAlertsQueue { RowKey = $Task.RowKey PartitionKey = $Task.PartitionKey RepeatsEvery = 'When received' + AlertComment = $Task.AlertComment RawAlert = @{ Conditions = @($Conditions) Actions = @($($Task.Actions | ConvertFrom-Json -Depth 10 -ErrorAction SilentlyContinue)) @@ -39,7 +40,7 @@ function Invoke-ListAlertsQueue { type = $Task.type RowKey = $Task.RowKey PartitionKey = $Task.PartitionKey - + AlertComment = $Task.AlertComment } } @@ -101,6 +102,7 @@ function Invoke-ListAlertsQueue { LogType = 'Scripted' EventType = 'Scheduled Task' RepeatsEvery = $Task.Recurrence + AlertComment = $Task.AlertComment RawAlert = $Task } 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 30a28debf9f9..95bfae0262f2 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 @@ -18,6 +18,7 @@ function Invoke-ExecNamedLocation { $NamedLocationId = $Request.Body.namedLocationId ?? $Request.Query.namedLocationId $Change = $Request.Body.change ?? $Request.Query.change $Content = $Request.Body.input ?? $Request.Query.input + if ($content.value) { $content = $content.value } try { $results = Set-CIPPNamedLocation -NamedLocationId $NamedLocationId -TenantFilter $TenantFilter -Change $Change -Content $Content -Headers $Headers diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPInvite.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPInvite.ps1 index baf649e6c680..2e5a051df76a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPInvite.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/GDAP/Invoke-ExecGDAPInvite.ps1 @@ -94,10 +94,9 @@ function Invoke-ExecGDAPInvite { 'OnboardingUrl' = $OnboardingUrl 'RoleMappings' = [string](@($RoleMappings) | ConvertTo-Json -Depth 10 -Compress) 'Technician' = [string]$Technician + 'Reference' = if ($Reference) { [string]$Reference } else { $null } } - if ($Reference) { $InviteEntity['Reference'] = [string]$Reference } - Add-CIPPAzDataTableEntity @Table -Entity $InviteEntity $Message = 'GDAP relationship invite created. Log in as a Global Admin in the new tenant to approve the invite.' @@ -133,10 +132,9 @@ function Invoke-ExecGDAPInvite { 'PartitionKey' = 'invite' 'RowKey' = $InviteId 'Technician' = $Technician + 'Reference' = if ($Reference) { $Reference } else { $null } } - if ($Reference) { $InviteEntity['Reference'] = $Reference } - Add-CIPPAzDataTableEntity @Table -Entity $InviteEntity -OperationType 'UpsertMerge' $Message = 'Invite updated' } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-AddStandardsTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-AddStandardsTemplate.ps1 index 9ad8c334794a..49e1dcf5485b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-AddStandardsTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-AddStandardsTemplate.ps1 @@ -10,7 +10,9 @@ function Invoke-AddStandardsTemplate { $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers - + if ($Request.Body.tenantFilter -eq 'tenantFilter') { + throw 'Invalid Tenant Selection. A standard must be assigned to at least 1 tenant.' + } $GUID = $Request.body.GUID ? $request.body.GUID : (New-Guid).GUID #updatedBy = $request.headers.'x-ms-client-principal' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 index 00146fd6e8bb..b600a6d876ed 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecUpdateDriftDeviation.ps1 @@ -42,11 +42,19 @@ function Invoke-ExecUpdateDriftDeviation { if ($Deviation.status -eq 'DeniedRemediate') { $Setting = $Deviation.standardName -replace 'standards.', '' $StandardTemplate = Get-CIPPTenantAlignment -TenantFilter $TenantFilter | Where-Object -Property standardType -EQ 'drift' - $StandardTemplate = $StandardTemplate.standardSettings.$Setting - - $StandardTemplate.standards.$Setting | Add-Member -MemberType NoteProperty -Name 'remediate' -Value $true -Force - $StandardTemplate.standards.$Setting | Add-Member -MemberType NoteProperty -Name 'report' -Value $true -Force - + if ($Setting -like '*IntuneTemplate*') { + $Setting = 'IntuneTemplate' + $TemplateId = $Deviation.standardName.split('.') | Select-Object -Last 1 + $StandardTemplate = $StandardTemplate.standardSettings.IntuneTemplate | Where-Object { $_.TemplateList.value -eq $TemplateId } + $StandardTemplate | Add-Member -MemberType NoteProperty -Name 'remediate' -Value $true -Force + $StandardTemplate | Add-Member -MemberType NoteProperty -Name 'report' -Value $true -Force + $Settings = $StandardTemplate + } else { + $StandardTemplate = $StandardTemplate.standardSettings.$Setting + $StandardTemplate.standards.$Setting | Add-Member -MemberType NoteProperty -Name 'remediate' -Value $true -Force + $StandardTemplate.standards.$Setting | Add-Member -MemberType NoteProperty -Name 'report' -Value $true -Force + $Settings = $StandardTemplate.standards.$Setting + } $TaskBody = @{ TenantFilter = $TenantFilter Name = "One Off Drift Remediation: $Setting - $TenantFilter" @@ -57,7 +65,7 @@ function Invoke-ExecUpdateDriftDeviation { Parameters = [pscustomobject]@{ Tenant = $TenantFilter - Settings = $StandardTemplate.standards.$Setting + Settings = $Settings } ScheduledTime = '0' PostExecution = @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListGitHubReleaseNotes.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListGitHubReleaseNotes.ps1 new file mode 100644 index 000000000000..fd6852d65e3e --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ListGitHubReleaseNotes.ps1 @@ -0,0 +1,70 @@ +function Invoke-ListGitHubReleaseNotes { + <# + .SYNOPSIS + Retrieves release notes for a GitHub repository. + .DESCRIPTION + Returns release metadata for the provided repository and semantic version. Hotfix + versions (e.g. v8.5.2) map back to the base release tag (v8.5.0). + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $Owner = $Request.Query.Owner + $Repository = $Request.Query.Repository + + if (-not $Owner) { + throw 'Owner parameter is required to retrieve release notes.' + } + + if (-not $Repository) { + throw 'Repository parameter is required to retrieve release notes.' + } + + $ReleasePath = "repos/$Owner/$Repository/releases?per_page=50" + + $Table = Get-CIPPTable -TableName cacheGitHubReleaseNotes + $PartitionKey = 'GitHubReleaseNotes' + $Filter = "PartitionKey eq '$PartitionKey'" + $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddHours(-24) + + try { + if ($Rows) { + $Releases = ConvertFrom-Json -InputObject $Rows.GitHubReleases -Depth 10 + } else { + $Releases = Invoke-GitHubApiRequest -Path $ReleasePath + $Releases = $Releases | ForEach-Object { + [ordered]@{ + name = $_.name + body = $_.body + releaseTag = $_.tag_name + htmlUrl = $_.html_url + publishedAt = $_.published_at + draft = [bool]$_.draft + prerelease = [bool]$_.prerelease + commitish = $_.target_commitish + } + } + + $Results = @{ + GitHubReleases = [string](ConvertTo-Json -Depth 10 -InputObject $Releases) + RowKey = [string]'GitHubReleaseNotes' + PartitionKey = $PartitionKey + } + Add-CIPPAzDataTableEntity @Table -Entity $Results -Force | Out-Null + } + + } catch { + $ErrorMessage = "Failed to retrieve release information: $($_)" + throw $ErrorMessage + } + + if (-not $Releases) { + return $IsListRequest ? @() : (throw "No releases returned for $Owner/$Repository") + } + + return $Releases +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 index 6593573d8c6a..f76d58c42139 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListLogs.ps1 @@ -58,6 +58,7 @@ function Invoke-ListLogs { $PartitionKey = $Request.Query.DateFilter $username = $Request.Query.User ?? '*' $TenantFilter = $Request.Query.Tenant + $ApiFilter = $Request.Query.API $StartDate = $Request.Query.StartDate ?? $Request.Query.DateFilter $EndDate = $Request.Query.EndDate ?? $Request.Query.DateFilter @@ -87,7 +88,8 @@ function Invoke-ListLogs { $Rows = Get-AzDataTableEntity @Table -Filter $Filter | Where-Object { $_.Severity -in $LogLevel -and $_.Username -like $username -and - ($TenantFilter -eq $null -or $TenantFilter -eq 'AllTenants' -or $_.Tenant -like "*$TenantFilter*" -or $_.TenantID -eq $TenantFilter) + ($TenantFilter -eq $null -or $TenantFilter -eq 'AllTenants' -or $_.Tenant -like "*$TenantFilter*" -or $_.TenantID -eq $TenantFilter) -and + ($ApiFilter -eq $null -or $_.API -match "$ApiFilter") } if ($AllowedTenants -notcontains 'AllTenants') { @@ -122,8 +124,8 @@ function Invoke-ListLogs { } return [HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = @($ReturnedLog | Sort-Object -Property DateTime -Descending) - } + StatusCode = [HttpStatusCode]::OK + Body = @($ReturnedLog | Sort-Object -Property DateTime -Descending) + } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 index 2794a61a0d94..dd03ae0af352 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListTenantAllowBlockList { +function Invoke-ListTenantAllowBlockList { <# .FUNCTIONALITY Entrypoint @@ -11,21 +11,74 @@ Function Invoke-ListTenantAllowBlockList { $TenantFilter = $Request.Query.tenantFilter $ListTypes = 'Sender', 'Url', 'FileHash', 'IP' try { - $Results = $ListTypes | ForEach-Object -Parallel { - Import-Module CIPPCore - $TempResults = New-ExoRequest -tenantid $using:TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ListType = $_ } - $TempResults | Add-Member -MemberType NoteProperty -Name ListType -Value $_ - $TempResults | Select-Object -ExcludeProperty *'@data.type'*, *'(DateTime])'* - } -ThrottleLimit 5 - + if ($TenantFilter -ne 'AllTenants') { + $Results = $ListTypes | ForEach-Object -Parallel { + Import-Module CIPPCore + $TempResults = New-ExoRequest -tenantid $using:TenantFilter -cmdlet 'Get-TenantAllowBlockListItems' -cmdParams @{ ListType = $_ } + $TempResults | Add-Member -MemberType NoteProperty -Name ListType -Value $_ -Force + $TempResults | Add-Member -MemberType NoteProperty -Name Tenant -Value $using:TenantFilter -Force + $TempResults | Select-Object -ExcludeProperty *'@data.type'*, *'(DateTime])'* + } -ThrottleLimit 5 + $Metadata = [PSCustomObject]@{} + } else { + $Table = Get-CIPPTable -TableName 'cacheTenantAllowBlockList' + $PartitionKey = 'TenantAllowBlockList' + $Filter = "PartitionKey eq '$PartitionKey'" + $Rows = Get-CIPPAzDataTableEntity @Table -filter $Filter | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-60) + $QueueReference = '{0}-{1}' -f $TenantFilter, $PartitionKey + $RunningQueue = Invoke-ListCippQueue -Reference $QueueReference | Where-Object { $_.Status -notmatch 'Completed' -and $_.Status -notmatch 'Failed' } + if ($RunningQueue) { + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Still loading data for all tenants. Please check back in a few more minutes' + QueueId = $RunningQueue.RowKey + } + $Results = @() + } elseif (!$Rows -and !$RunningQueue) { + $TenantList = Get-Tenants -IncludeErrors + $Queue = New-CippQueueEntry -Name 'Tenant Allow/Block List - All Tenants' -Link '/tenant/administration/allow-block-list?customerId=AllTenants' -Reference $QueueReference -TotalTasks ($TenantList | Measure-Object).Count + $Metadata = [PSCustomObject]@{ + QueueMessage = 'Loading data for all tenants. Please check back in a few minutes' + QueueId = $Queue.RowKey + } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'TenantAllowBlockListOrchestrator' + QueueFunction = @{ + FunctionName = 'GetTenants' + QueueId = $Queue.RowKey + TenantParams = @{ + IncludeErrors = $true + } + DurableName = 'ListTenantAllowBlockListAllTenants' + } + SkipLog = $true + } + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + $Results = @() + } else { + $Metadata = [PSCustomObject]@{ + QueueId = $RunningQueue.RowKey ?? $null + } + $Results = foreach ($Row in $Rows) { + $Row.Entry | ConvertFrom-Json + } + } + } $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $StatusCode = [HttpStatusCode]::Forbidden $Results = $ErrorMessage } - return [HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @($Results) + + if (!$Body) { + $Body = [PSCustomObject]@{ + Results = @($Results) + Metadata = $Metadata } + } + + return [HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $Body + } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 index 944592099a5e..7bbba7946425 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-AuditLogOrchestrator { <# .SYNOPSIS Start the Audit Log Polling Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 index 17318704ebe9..3971a7d3e728 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogProcessingOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-AuditLogProcessingOrchestrator { <# .SYNOPSIS Start the Audit Log Processing Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 index ce3cb766f8fb..f21b4592acd8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-AuditLogSearchCreation.ps1 @@ -2,6 +2,9 @@ function Start-AuditLogSearchCreation { <# .SYNOPSIS Start the Audit Log Searches + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DriftStandardsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DriftStandardsOrchestrator.ps1 index 7aad617a0c71..9a3b0203f852 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DriftStandardsOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DriftStandardsOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-DriftStandardsOrchestrator { <# .SYNOPSIS Start the Drift Standards Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 index c7463ec28e08..3d5d8ff1ea00 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-SchedulerOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-SchedulerOrchestrator { <# .SYNOPSIS Start the Scheduler Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-StandardsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-StandardsOrchestrator.ps1 index 76e689ef4952..fe261d84f695 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-StandardsOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-StandardsOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-StandardsOrchestrator { <# .SYNOPSIS Start the Standards Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 new file mode 100644 index 000000000000..49d1e6d6e12d --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-TenantDynamicGroupOrchestrator.ps1 @@ -0,0 +1,40 @@ +function Start-TenantDynamicGroupOrchestrator { + <# + .SYNOPSIS + Start the Tenant Dynamic Group Orchestrator + + .FUNCTIONALITY + Entrypoint + #> + [CmdletBinding(SupportsShouldProcess = $true)] + param( + [string]$GroupId = 'All' + ) + + try { + Write-Information 'Updating Dynamic Tenant Groups' + $TenantGroups = @{ + Dynamic = $true + } + $TenantGroups = Get-TenantGroups @TenantGroups + if ($GroupId -ne 'All') { + $TenantGroups = $TenantGroups | Where-Object { $_.Id -eq $GroupId } + } + + if ($TenantGroups.Count -gt 0) { + Write-Information "Found $($TenantGroups.Count) dynamic tenant groups" + $Queue = New-CippQueueEntry -Name 'Dynamic Tenant Groups' -TotalTasks $TenantGroups.Count + $TenantBatch = $TenantGroups | Select-Object Name, Id, @{n = 'FunctionName'; exp = { 'UpdateDynamicTenantGroup' } }, @{n = 'QueueId'; exp = { $Queue.RowKey } } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'UpdateDynamicTenantGroups' + Batch = @($TenantBatch) + SkipLog = $true + } + if ($PSCmdlet.ShouldProcess('Start-TenantDynamicGroupOrchestrator', 'Starting Tenant Dynamic Group Orchestrator')) { + Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + } + } else { + Write-Information 'No tenants require permissions update' + } + } catch {} +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 index 10caa27d3383..323c6d437219 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UpdatePermissionsOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-UpdatePermissionsOrchestrator { <# .SYNOPSIS Start the Update Permissions Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index 87404edca459..d635141637dc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -2,6 +2,9 @@ function Start-UserTasksOrchestrator { <# .SYNOPSIS Start the User Tasks Orchestrator + + .FUNCTIONALITY + Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] param() diff --git a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 index 93c6bce9f6ce..af5a29646f5c 100644 --- a/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPTextReplacement.ps1 @@ -62,19 +62,22 @@ function Get-CIPPTextReplacement { $Vars[$Var.RowKey] = $Var.Value } } - # Tenant Specific Variables - $ReplaceMap = Get-CIPPAzDataTableEntity @ReplaceTable -Filter "PartitionKey eq '$CustomerId'" - # If no results found by customerId, try by defaultDomainName - if (!$ReplaceMap) { - $ReplaceMap = Get-CIPPAzDataTableEntity @ReplaceTable -Filter "PartitionKey eq '$($Tenant.defaultDomainName)'" - } - if ($ReplaceMap) { - foreach ($Var in $ReplaceMap) { - if ($EscapeForJson.IsPresent) { - # Escape quotes for JSON if not already escaped - $Var.Value = $Var.Value -replace '(? + [CmdletBinding(DefaultParameterSetName = 'Standard')] param ( [Parameter( ParameterSetName = 'Skip', Mandatory = $True )] [switch]$SkipList, diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 index 503daa4c3e90..0b961c534385 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 @@ -3,6 +3,7 @@ function New-GraphBulkRequest { .FUNCTIONALITY Internal #> + [CmdletBinding()] param( $tenantid, $NoAuthCheck, diff --git a/Modules/CIPPCore/Public/GraphHelper/Write-AlertTrace.ps1 b/Modules/CIPPCore/Public/GraphHelper/Write-AlertTrace.ps1 index abe5236b8295..3b19ead2c588 100644 --- a/Modules/CIPPCore/Public/GraphHelper/Write-AlertTrace.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/Write-AlertTrace.ps1 @@ -6,10 +6,11 @@ function Write-AlertTrace { param( $cmdletName, $data, - $tenantFilter + $tenantFilter, + [string]$PartitionKey = (Get-Date -UFormat '%Y%m%d').ToString(), + [string]$AlertComment = $null ) $Table = Get-CIPPTable -tablename AlertLastRun - $PartitionKey = (Get-Date -UFormat '%Y%m%d').ToString() #Get current row and compare the $logData object. If it's the same, don't write it. $Row = Get-CIPPAzDataTableEntity @table -Filter "RowKey eq '$($tenantFilter)-$($cmdletName)' and PartitionKey eq '$PartitionKey'" try { @@ -23,6 +24,7 @@ function Write-AlertTrace { 'CmdletName' = "$cmdletName" 'Tenant' = "$tenantFilter" 'LogData' = [string]$LogData + 'AlertComment' = [string]$AlertComment } $Table.Entity = $TableRow Add-CIPPAzDataTableEntity @Table -Force | Out-Null @@ -36,6 +38,7 @@ function Write-AlertTrace { 'CmdletName' = "$cmdletName" 'Tenant' = "$tenantFilter" 'LogData' = [string]$LogData + 'AlertComment' = [string]$AlertComment } $Table.Entity = $TableRow Add-CIPPAzDataTableEntity @Table -Force | Out-Null diff --git a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 index a0bb456c602b..1a0836dba0c2 100644 --- a/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAlertTemplate.ps1 @@ -9,7 +9,8 @@ function New-CIPPAlertTemplate { $ActionResults, $CIPPURL, $Tenant, - $AuditLogLink + $AuditLogLink, + $AlertComment ) $Appname = '[{"Application Name":"ACOM Azure Website","Application IDs":"23523755-3a2b-41ca-9315-f81f3f566a95"},{"Application Name":"AEM-DualAuth","Application IDs":"69893ee3-dd10-4b1c-832d-4870354be3d8"},{"Application Name":"ASM Campaign Servicing","Application IDs":"0cb7b9ec-5336-483b-bc31-b15b5788de71"},{"Application Name":"Azure Advanced Threat Protection","Application IDs":"7b7531ad-5926-4f2d-8a1d-38495ad33e17"},{"Application Name":"Azure Data Lake","Application IDs":"e9f49c6b-5ce5-44c8-925d-015017e9f7ad"},{"Application Name":"Azure Lab Services Portal","Application IDs":"835b2a73-6e10-4aa5-a979-21dfda45231c"},{"Application Name":"Azure Portal","Application IDs":"c44b4083-3bb0-49c1-b47d-974e53cbdf3c"},{"Application Name":"AzureSupportCenter","Application IDs":"37182072-3c9c-4f6a-a4b3-b3f91cacffce"},{"Application Name":"Bing","Application IDs":"9ea1ad79-fdb6-4f9a-8bc3-2b70f96e34c7"},{"Application Name":"CPIM Service","Application IDs":"bb2a2e3a-c5e7-4f0a-88e0-8e01fd3fc1f4"},{"Application Name":"CRM Power BI Integration","Application IDs":"e64aa8bc-8eb4-40e2-898b-cf261a25954f"},{"Application Name":"Dataverse","Application IDs":"00000007-0000-0000-c000-000000000000"},{"Application Name":"Enterprise Roaming and Backup","Application IDs":"60c8bde5-3167-4f92-8fdb-059f6176dc0f"},{"Application Name":"IAM Supportability","Application IDs":"a57aca87-cbc0-4f3c-8b9e-dc095fdc8978"},{"Application Name":"IrisSelectionFrontDoor","Application IDs":"16aeb910-ce68-41d1-9ac3-9e1673ac9575"},{"Application Name":"MCAPI Authorization Prod","Application IDs":"d73f4b35-55c9-48c7-8b10-651f6f2acb2e"},{"Application Name":"Media Analysis and Transformation Service","Application IDs":"944f0bd1-117b-4b1c-af26-804ed95e767e
0cd196ee-71bf-4fd6-a57c-b491ffd4fb1e"},{"Application Name":"Microsoft 365 Support Service","Application IDs":"ee272b19-4411-433f-8f28-5c13cb6fd407"},{"Application Name":"Microsoft App Access Panel","Application IDs":"0000000c-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Approval Management","Application IDs":"65d91a3d-ab74-42e6-8a2f-0add61688c74
38049638-cc2c-4cde-abe4-4479d721ed44"},{"Application Name":"Microsoft Authentication Broker","Application IDs":"29d9ed98-a469-4536-ade2-f981bc1d605e"},{"Application Name":"Microsoft Azure CLI","Application IDs":"04b07795-8ddb-461a-bbee-02f9e1bf7b46"},{"Application Name":"Microsoft Azure PowerShell","Application IDs":"1950a258-227b-4e31-a9cf-717495945fc2"},{"Application Name":"Microsoft Bing Search","Application IDs":"cf36b471-5b44-428c-9ce7-313bf84528de"},{"Application Name":"Microsoft Bing Search for Microsoft Edge","Application IDs":"2d7f3606-b07d-41d1-b9d2-0d0c9296a6e8"},{"Application Name":"Microsoft Bing Default Search Engine","Application IDs":"1786c5ed-9644-47b2-8aa0-7201292175b6"},{"Application Name":"Microsoft Defender for Cloud Apps","Application IDs":"3090ab82-f1c1-4cdf-af2c-5d7a6f3e2cc7"},{"Application Name":"Microsoft Docs","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Dynamics ERP","Application IDs":"00000015-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Edge Insider Addons Prod","Application IDs":"6253bca8-faf2-4587-8f2f-b056d80998a7"},{"Application Name":"Microsoft Exchange Online Protection","Application IDs":"00000007-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Forms","Application IDs":"c9a559d2-7aab-4f13-a6ed-e7e9c52aec87"},{"Application Name":"Microsoft Graph","Application IDs":"00000003-0000-0000-c000-000000000000"},{"Application Name":"Microsoft Intune Web Company Portal","Application IDs":"74bcdadc-2fdc-4bb3-8459-76d06952a0e9"},{"Application Name":"Microsoft Intune Windows Agent","Application IDs":"fc0f3af4-6835-4174-b806-f7db311fd2f3"},{"Application Name":"Microsoft Learn","Application IDs":"18fbca16-2224-45f6-85b0-f7bf2b39b3f3"},{"Application Name":"Microsoft Office","Application IDs":"d3590ed6-52b3-4102-aeff-aad2292ab01c"},{"Application Name":"Microsoft Office 365 Portal","Application IDs":"00000006-0000-0ff1-ce00-000000000000"},{"Application Name":"Microsoft Office Web Apps Service","Application IDs":"67e3df25-268a-4324-a550-0de1c7f97287"},{"Application Name":"Microsoft Online Syndication Partner Portal","Application IDs":"d176f6e7-38e5-40c9-8a78-3998aab820e7"},{"Application Name":"Microsoft password reset service","Application IDs":"93625bc8-bfe2-437a-97e0-3d0060024faa"},{"Application Name":"Microsoft Power BI","Application IDs":"871c010f-5e61-4fb1-83ac-98610a7e9110"},{"Application Name":"Microsoft Storefronts","Application IDs":"28b567f6-162c-4f54-99a0-6887f387bbcc"},{"Application Name":"Microsoft Stream Portal","Application IDs":"cf53fce8-def6-4aeb-8d30-b158e7b1cf83"},{"Application Name":"Microsoft Substrate Management","Application IDs":"98db8bd6-0cc0-4e67-9de5-f187f1cd1b41"},{"Application Name":"Microsoft Support","Application IDs":"fdf9885b-dd37-42bf-82e5-c3129ef5a302"},{"Application Name":"Microsoft Teams","Application IDs":"1fec8e78-bce4-4aaf-ab1b-5451cc387264"},{"Application Name":"Microsoft Teams Services","Application IDs":"cc15fd57-2c6c-4117-a88c-83b1d56b4bbe"},{"Application Name":"Microsoft Teams Web Client","Application IDs":"5e3ce6c0-2b1f-4285-8d4b-75ee78787346"},{"Application Name":"Microsoft Whiteboard Services","Application IDs":"95de633a-083e-42f5-b444-a4295d8e9314"},{"Application Name":"O365 Suite UX","Application IDs":"4345a7b9-9a63-4910-a426-35363201d503"},{"Application Name":"Office 365 Exchange Online","Application IDs":"00000002-0000-0ff1-ce00-000000000000"},{"Application Name":"Office 365 Management","Application IDs":"00b41c95-dab0-4487-9791-b9d2c32c80f2"},{"Application Name":"Office 365 Search Service","Application IDs":"66a88757-258c-4c72-893c-3e8bed4d6899"},{"Application Name":"Office 365 SharePoint Online","Application IDs":"00000003-0000-0ff1-ce00-000000000000"},{"Application Name":"Office Delve","Application IDs":"94c63fef-13a3-47bc-8074-75af8c65887a"},{"Application Name":"Office Online Add-in SSO","Application IDs":"93d53678-613d-4013-afc1-62e9e444a0a5"},{"Application Name":"Office Online Client AAD- Augmentation Loop","Application IDs":"2abdc806-e091-4495-9b10-b04d93c3f040"},{"Application Name":"Office Online Client AAD- Loki","Application IDs":"b23dd4db-9142-4734-867f-3577f640ad0c"},{"Application Name":"Office Online Client AAD- Maker","Application IDs":"17d5e35f-655b-4fb0-8ae6-86356e9a49f5"},{"Application Name":"Office Online Client MSA- Loki","Application IDs":"b6e69c34-5f1f-4c34-8cdf-7fea120b8670"},{"Application Name":"Office Online Core SSO","Application IDs":"243c63a3-247d-41c5-9d83-7788c43f1c43"},{"Application Name":"Office Online Search","Application IDs":"a9b49b65-0a12-430b-9540-c80b3332c127"},{"Application Name":"Office.com","Application IDs":"4b233688-031c-404b-9a80-a4f3f2351f90"},{"Application Name":"Office365 Shell WCSS-Client","Application IDs":"89bee1f7-5e6e-4d8a-9f3d-ecd601259da7"},{"Application Name":"OfficeClientService","Application IDs":"0f698dd4-f011-4d23-a33e-b36416dcb1e6"},{"Application Name":"OfficeHome","Application IDs":"4765445b-32c6-49b0-83e6-1d93765276ca"},{"Application Name":"OfficeShredderWacClient","Application IDs":"4d5c2d63-cf83-4365-853c-925fd1a64357"},{"Application Name":"OMSOctopiPROD","Application IDs":"62256cef-54c0-4cb4-bcac-4c67989bdc40"},{"Application Name":"OneDrive SyncEngine","Application IDs":"ab9b8c07-8f02-4f72-87fa-80105867a763"},{"Application Name":"OneNote","Application IDs":"2d4d3d8e-2be3-4bef-9f87-7875a61c29de"},{"Application Name":"Outlook Mobile","Application IDs":"27922004-5251-4030-b22d-91ecd9a37ea4"},{"Application Name":"Partner Customer Delegated Admin Offline Processor","Application IDs":"a3475900-ccec-4a69-98f5-a65cd5dc5306"},{"Application Name":"Password Breach Authenticator","Application IDs":"bdd48c81-3a58-4ea9-849c-ebea7f6b6360"},{"Application Name":"Power BI Service","Application IDs":"00000009-0000-0000-c000-000000000000"},{"Application Name":"SharedWithMe","Application IDs":"ffcb16e8-f789-467c-8ce9-f826a080d987"},{"Application Name":"SharePoint Online Web Client Extensibility","Application IDs":"08e18876-6177-487e-b8b5-cf950c1e598c"},{"Application Name":"Signup","Application IDs":"b4bddae8-ab25-483e-8670-df09b9f1d0ea"},{"Application Name":"Skype for Business Online","Application IDs":"00000004-0000-0ff1-ce00-000000000000"},{"Application Name":"Sway","Application IDs":"905fcf26-4eb7-48a0-9ff0-8dcc7194b5ba"},{"Application Name":"Universal Store Native Client","Application IDs":"268761a2-03f3-40df-8a8b-c3db24145b6b"},{"Application Name":"Vortex [wsfed enabled]","Application IDs":"5572c4c0-d078-44ce-b81c-6cbf8d3ed39e"},{"Application Name":"Windows Azure Active Directory","Application IDs":"00000002-0000-0000-c000-000000000000"},{"Application Name":"Windows Azure Service Management API","Application IDs":"797f4846-ba00-4fd7-ba43-dac1f8f63013"},{"Application Name":"WindowsDefenderATP Portal","Application IDs":"a3b79187-70b2-4139-83f9-6016c58cd27b"},{"Application Name":"Windows Search","Application IDs":"26a7ee05-5602-4d76-a7ba-eae8b7b67941"},{"Application Name":"Windows Spotlight","Application IDs":"1b3c667f-cde3-4090-b60b-3d2abd0117f0"},{"Application Name":"Windows Store for Business","Application IDs":"45a330b1-b1ec-4cc1-9161-9f03992aa49f"},{"Application Name":"Yammer","Application IDs":"00000005-0000-0ff1-ce00-000000000000"},{"Application Name":"Yammer Web","Application IDs":"c1c74fed-04c9-4704-80dc-9f79a2e515cb"},{"Application Name":"Yammer Web Embed","Application IDs":"e1ef36fd-b883-4dbf-97f0-9ece4b576fc6"}]' | ConvertFrom-Json | Where-Object -Property 'Application IDs' -EQ $data.applicationId $HTMLTemplate = Get-Content 'TemplateEmail.html' -Raw | Out-String @@ -56,6 +57,12 @@ function New-CIPPAlertTemplate { $DataHTML = ($Data | Select-Object * -ExcludeProperty Etag, PartitionKey, TimeStamp | ConvertTo-Html | Out-String).Replace('
', '
') $IntroText = "

You've configured CIPP to send you alerts based on the logbook. The following alerts match your configured rules

$dataHTML" + + # Add alert comment if provided + if ($AlertComment) { + $IntroText = "$IntroText

Alert Information

$AlertComment

" + } + $ButtonUrl = "$CIPPURL/cipp/logs" $ButtonText = 'Check logbook information' } @@ -280,10 +287,11 @@ function New-CIPPAlertTemplate { } } return [pscustomobject]@{ - title = $Title - buttonurl = $ButtonUrl - buttontext = $ButtonText - auditlog = $AuditLogLink + title = $Title + buttonurl = $ButtonUrl + buttontext = $ButtonText + auditlog = $AuditLogLink + alertcomment = $AlertComment } } } diff --git a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 index d8b09e412d33..60c83b040d93 100644 --- a/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPTemplateRun.ps1 @@ -41,7 +41,7 @@ function New-CIPPTemplateRun { } foreach ($File in $Files) { if ($File.name -eq 'MigrationTable' -or $file.name -eq 'ALLOWED COUNTRIES') { continue } - $ExistingTemplate = $ExistingTemplates | Where-Object { (![string]::IsNullOrEmpty($_.displayName) -and (Get-SanitizedFilename -filename $_.displayName) -eq $File.name) -or (![string]::IsNullOrEmpty($_.templateName) -and (Get-SanitizedFilename -filename $_.templateName) -eq $File.name ) } | Select-Object -First 1 + $ExistingTemplate = $ExistingTemplates | Where-Object { (![string]::IsNullOrEmpty($_.displayName) -and (Get-SanitizedFilename -filename $_.displayName) -eq $File.name) -or (![string]::IsNullOrEmpty($_.templateName) -and (Get-SanitizedFilename -filename $_.templateName) -eq $File.name ) -and ![string]::IsNullOrEmpty($_.SHA) } | Select-Object -First 1 $UpdateNeeded = $false if ($ExistingTemplate -and $ExistingTemplate.SHA -ne $File.sha) { diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json index d4abc5200b3d..52f29c119433 100644 --- a/Modules/CIPPCore/Public/PermissionsTranslator.json +++ b/Modules/CIPPCore/Public/PermissionsTranslator.json @@ -5328,12 +5328,12 @@ "value": "AllSites.FullControl" }, { - "description": "Allows to read the LAPs passwords.", - "displayName": "Manage LAPs passwords", + "description": "Allows to read the LAPS passwords.", + "displayName": "Manage LAPS passwords", "id": "280b3b69-0437-44b1-bc20-3b2fca1ee3e9", "Origin": "Delegated", - "userConsentDescription": "Allows to read the LAPs passwords.", - "userConsentDisplayName": "Manage LAPs passwords", + "userConsentDescription": "Allows to read the LAPS passwords.", + "userConsentDisplayName": "Manage LAPS passwords", "value": "DeviceLocalCredential.Read.All" }, { diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index 2c972715f672..7606d4d0606e 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -25,7 +25,19 @@ function Send-CIPPAlert { $Recipients = if ($AltEmail) { [pscustomobject]@{EmailAddress = @{Address = $AltEmail } } } else { - $Config.email.split($(if ($Config.email -like '*,*') { ',' } else { ';' })).trim() | ForEach-Object { if ($_ -like '*@*') { [pscustomobject]@{EmailAddress = @{Address = $_ } } } } + $Config.email.split($(if ($Config.email -like '*,*') { ',' } else { ';' })).trim() | ForEach-Object { + if ($_ -like '*@*') { + ($Alias, $Domain) = $_ -split '@' + if ($Alias -match '%') { + # Allow for text replacement in alias portion of email address + $Alias = Get-CIPPTextReplacement -Text $Alias -Tenant $TenantFilter + $Recipient = "$Alias@$Domain" + } else { + $Recipient = $_ + } + [pscustomobject]@{EmailAddress = @{Address = $Recipient } } + } + } } $PowerShellBody = [PSCustomObject]@{ message = @{ @@ -43,9 +55,14 @@ function Send-CIPPAlert { if ($PSCmdlet.ShouldProcess($($Recipients.EmailAddress.Address -join ', '), 'Sending email')) { $null = New-GraphPostRequest -uri 'https://graph.microsoft.com/v1.0/me/sendMail' -tenantid $env:TenantID -NoAuthCheck $true -type POST -body ($JSONBody) } + + $LogData = @{ + Recipients = $Recipients + } + Write-LogMessage -API 'Webhook Alerts' -message "Sent an email alert: $Title" -tenant $TenantFilter -sev info -LogData $LogData + return "Sent an email alert: $Title" } - Write-LogMessage -API 'Webhook Alerts' -message "Sent an email alert: $Title" -tenant $TenantFilter -sev info - return "Sent an email alert: $Title" + } catch { $ErrorMessage = Get-CippException -Exception $_ Write-Information "Could not send webhook alert to email: $($ErrorMessage.NormalizedError)" diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 index c64d3eb1b2f3..a59431315bf2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAutopilotProfile.ps1 @@ -59,7 +59,7 @@ function Invoke-CIPPStandardAutopilotProfile { if ($Settings.NotLocalAdmin -eq $true) { $userType = 'Standard' } else { $userType = 'Administrator' } if ($Settings.SelfDeployingMode -eq $true) { $DeploymentMode = 'shared' - $Setings.AllowWhiteGlove = $false + $Settings.AllowWhiteGlove = $false } else { $DeploymentMode = 'singleUser' } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 new file mode 100644 index 000000000000..1588392ebbca --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardBitLockerKeysForOwnedDevice.ps1 @@ -0,0 +1,101 @@ +function Invoke-CIPPStandardBitLockerKeysForOwnedDevice { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) BitLockerKeysForOwnedDevice + .SYNOPSIS + (Label) Restrict users from recovering BitLocker keys for owned devices + .DESCRIPTION + (Helptext) Controls whether standard users can recover BitLocker keys for devices they own via Microsoft 365 portals. + (DocsDescription) Updates the default user role setting that governs access to BitLocker recovery keys for owned devices. This allows administrators to either permit self-service recovery or require helpdesk involvement through Microsoft Entra authorization policies. + .NOTES + CAT + Entra (AAD) Standards + TAG + "NIST CSF 2.0 (PR.AA-05)" + EXECUTIVETEXT + Ensures administrators retain control over BitLocker recovery secrets when required, while still allowing flexibility to enable self-service recovery when business needs demand it. + ADDEDCOMPONENT + {"type":"autoComplete","multiple":false,"creatable":false,"label":"Select state","name":"standards.BitLockerKeysForOwnedDevice.state","options":[{"label":"Restrict","value":"restrict"},{"label":"Allow","value":"allow"}]} + IMPACT + Medium Impact + ADDEDDATE + 2025-10-12 + POWERSHELLEQUIVALENT + Update-MgBetaPolicyAuthorizationPolicy + 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) + ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'BitLockerKeysForOwnedDevice' + + $StateValue = $Settings.state.value ?? $Settings.state + if ([string]::IsNullOrWhiteSpace($StateValue)) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'BitLockerKeysForOwnedDevice: Invalid state parameter set.' -sev Error + return + } + + switch ($StateValue.ToLowerInvariant()) { + 'restrict' { $DesiredValue = $false; $DesiredLabel = 'restricted'; break } + 'allow' { $DesiredValue = $true; $DesiredLabel = 'allowed'; break } + default { + Write-LogMessage -API 'Standards' -tenant $tenant -message "BitLockerKeysForOwnedDevice: Unsupported state value '$StateValue'." -sev Error + return + } + } + + try { + $CurrentState = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the BitLockerKeysForOwnedDevice state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + $CurrentValue = [bool]$CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice + $StateIsCorrect = ($CurrentValue -eq $DesiredValue) + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Users are already $DesiredLabel from recovering BitLocker keys for their owned devices." -sev Info + } else { + try { + $BodyObject = @{ defaultUserRolePermissions = @{ allowedToReadBitLockerKeysForOwnedDevice = $DesiredValue } } + $BodyJson = $BodyObject | ConvertTo-Json -Depth 4 -Compress + $null = New-GraphPOSTRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/authorizationPolicy/authorizationPolicy' -Type patch -Body $BodyJson + $ActionMessage = if ($DesiredValue) { 'Allowed users to recover BitLocker keys for their owned devices.' } else { 'Restricted users from recovering BitLocker keys for their owned devices.' } + Write-LogMessage -API 'Standards' -tenant $tenant -message $ActionMessage -sev Info + + + # Update current state variables to reflect the change immediately if running remediate and report/alert together + $CurrentState.defaultUserRolePermissions.allowedToReadBitLockerKeysForOwnedDevice = $DesiredValue + $CurrentValue = $DesiredValue + $StateIsCorrect = $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to $StateValue users to recover BitLocker keys for their owned devices: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Users are $DesiredLabel to recover BitLocker keys for their owned devices as configured." -sev Info + } else { + $CurrentLabel = if ($CurrentValue) { 'allowed' } else { 'restricted' } + $AlertMessage = "Users are $CurrentLabel to recover BitLocker keys for their owned devices but should be $DesiredLabel." + Write-StandardsAlert -message $AlertMessage -object $CurrentState -tenant $tenant -standardName 'BitLockerKeysForOwnedDevice' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $tenant -message $AlertMessage -sev Info + } + } + + if ($Settings.report -eq $true) { + Set-CIPPStandardsCompareField -FieldName 'standards.BitLockerKeysForOwnedDevice' -FieldValue $StateIsCorrect -Tenant $tenant + Add-CIPPBPAField -FieldName 'BitLockerKeysForOwnedDevice' -FieldValue $CurrentValue -StoreAs bool -Tenant $tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 index 1549723f1dba..2ed285caec7f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRequireMFA.ps1 @@ -29,36 +29,30 @@ function Invoke-CIPPStandardintuneRequireMFA { #> param($Tenant, $Settings) - $TestResult = Test-CIPPStandardLicense -StandardName 'intuneRequireMFA' -TenantFilter $Tenant -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1') ##$Rerun -Type Standard -Tenant $Tenant -Settings $Settings 'intuneRequireMFA' - if ($TestResult -eq $false) { - Write-Host "We're exiting as the correct license is not present for this standard." - return $true - } #we're done. - try { $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant - } - catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneRequireMFA state for $Tenant. Error: $ErrorMessage" -Sev Error + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneRequireMFA state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage return } - If ($Settings.remediate -eq $true) { + if ($Settings.remediate -eq $true) { if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Require to use MFA when joining/registering Entra Devices is already enabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Require to use MFA when joining/registering Entra Devices is already enabled.' -sev Info } else { try { $NewSetting = $PreviousSetting $NewSetting.multiFactorAuthConfiguration = 'required' $NewBody = ConvertTo-Json -Compress -InputObject $NewSetting -Depth 10 - New-GraphPostRequest -tenantid $tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody -ContentType 'application/json' - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Set required to use MFA when joining/registering Entra Devices' -sev Info + New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Set required to use MFA when joining/registering Entra Devices' -sev Info + $PreviousSetting.multiFactorAuthConfiguration = 'required' } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set require to use MFA when joining/registering Entra Devices: $ErrorMessage" -sev Error + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set require to use MFA when joining/registering Entra Devices: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage } } } @@ -66,16 +60,16 @@ function Invoke-CIPPStandardintuneRequireMFA { if ($Settings.alert -eq $true) { if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') { - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Require to use MFA when joining/registering Entra Devices is enabled.' -sev Info + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Require to use MFA when joining/registering Entra Devices is enabled.' -sev Info } else { - Write-StandardsAlert -message 'Require to use MFA when joining/registering Entra Devices is not enabled' -object $PreviousSetting -tenant $tenant -standardName 'intuneRequireMFA' -standardId $Settings.standardId - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Require to use MFA when joining/registering Entra Devices is not enabled.' -sev Info + Write-StandardsAlert -message 'Require to use MFA when joining/registering Entra Devices is not enabled' -object $PreviousSetting -tenant $Tenant -standardName 'intuneRequireMFA' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Require to use MFA when joining/registering Entra Devices is not enabled.' -sev Info } } if ($Settings.report -eq $true) { $RequireMFA = if ($PreviousSetting.multiFactorAuthConfiguration -eq 'required') { $true } else { $false } - Set-CIPPStandardsCompareField -FieldName 'standards.intuneRequireMFA' -FieldValue $RequireMFA -Tenant $tenant - Add-CIPPBPAField -FieldName 'intuneRequireMFA' -FieldValue $RequireMFA -StoreAs bool -Tenant $tenant + Set-CIPPStandardsCompareField -FieldName 'standards.intuneRequireMFA' -FieldValue $RequireMFA -Tenant $Tenant + Add-CIPPBPAField -FieldName 'intuneRequireMFA' -FieldValue $RequireMFA -StoreAs bool -Tenant $Tenant } } diff --git a/Modules/CIPPCore/Public/Functions/Expand-CIPPTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Functions/Expand-CIPPTenantGroups.ps1 rename to Modules/CIPPCore/Public/TenantGroups/Expand-CIPPTenantGroups.ps1 diff --git a/Modules/CIPPCore/Public/Functions/Get-TenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 similarity index 80% rename from Modules/CIPPCore/Public/Functions/Get-TenantGroups.ps1 rename to Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 index 761e1462d9cb..bb53c0e9e0b3 100644 --- a/Modules/CIPPCore/Public/Functions/Get-TenantGroups.ps1 +++ b/Modules/CIPPCore/Public/TenantGroups/Get-TenantGroups.ps1 @@ -11,8 +11,9 @@ function Get-TenantGroups { #> [CmdletBinding()] param( - $GroupId, - $TenantFilter + [string]$GroupId, + [string]$TenantFilter, + [switch]$Dynamic ) $GroupTable = Get-CippTable -tablename 'TenantGroups' @@ -30,6 +31,12 @@ function Get-TenantGroups { } $Tenants = Get-Tenants @TenantParams + if ($Dynamic.IsPresent) { + $GroupTable.Filter = "PartitionKey eq 'TenantGroup' and GroupType eq 'dynamic'" + } else { + $GroupTable.Filter = "PartitionKey eq 'TenantGroup'" + } + if ($GroupId) { $Groups = Get-CIPPAzDataTableEntity @GroupTable -Filter "RowKey eq '$GroupId'" $AllMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "GroupId eq '$GroupId'" @@ -77,10 +84,13 @@ function Get-TenantGroups { $SortedMembers = @() } $Results.Add([PSCustomObject]@{ - Id = $Group.RowKey - Name = $Group.Name - Description = $Group.Description - Members = @($SortedMembers) + Id = $Group.RowKey + Name = $Group.Name + Description = $Group.Description + GroupType = $Group.GroupType ?? 'static' + RuleLogic = $Group.RuleLogic ?? 'and' + DynamicRules = $Group.DynamicRules ? @($Group.DynamicRules | ConvertFrom-Json) : @() + Members = @($SortedMembers) }) } return $Results | Sort-Object Name diff --git a/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 new file mode 100644 index 000000000000..8738313f79e1 --- /dev/null +++ b/Modules/CIPPCore/Public/TenantGroups/Update-CIPPDynamicTenantGroups.ps1 @@ -0,0 +1,190 @@ +function Update-CIPPDynamicTenantGroups { + <# + .SYNOPSIS + Update dynamic tenant groups based on their rules + .DESCRIPTION + This function processes dynamic tenant group rules and updates membership accordingly + .PARAMETER GroupId + The specific group ID to update. If not provided, all dynamic groups will be updated + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [string]$GroupId + ) + + try { + $GroupTable = Get-CippTable -tablename 'TenantGroups' + $MembersTable = Get-CippTable -tablename 'TenantGroupMembers' + $LicenseCacheTable = Get-CippTable -tablename 'cachetenantskus' + + $Skus = Get-CIPPAzDataTableEntity @LicenseCacheTable -Filter "PartitionKey eq 'sku' and Timestamp ge datetime'$( (Get-Date).ToUniversalTime().AddHours(-8).ToString('yyyy-MM-ddTHH:mm:ssZ') )'" + + $SkuHashtable = @{} + foreach ($Sku in $Skus) { + if ($Sku.JSON -and (Test-Json -Json $Sku.JSON -ErrorAction SilentlyContinue)) { + $SkuHashtable[$Sku.RowKey] = $Sku.JSON | ConvertFrom-Json + } + } + + if ($GroupId) { + $DynamicGroups = Get-CIPPAzDataTableEntity @GroupTable -Filter "PartitionKey eq 'TenantGroup' and RowKey eq '$GroupId'" + } else { + $DynamicGroups = Get-CIPPAzDataTableEntity @GroupTable -Filter "PartitionKey eq 'TenantGroup' and GroupType eq 'dynamic'" + } + + if (-not $DynamicGroups) { + Write-LogMessage -API 'TenantGroups' -message 'No dynamic groups found to process' -sev Info + return @{ MembersAdded = 0; MembersRemoved = 0; GroupsProcessed = 0 } + } + + $AllTenants = Get-Tenants -IncludeErrors + $TotalMembersAdded = 0 + $TotalMembersRemoved = 0 + $GroupsProcessed = 0 + + foreach ($Group in $DynamicGroups) { + try { + Write-LogMessage -API 'TenantGroups' -message "Processing dynamic group: $($Group.Name)" -sev Info + $Rules = @($Group.DynamicRules | ConvertFrom-Json) + # Build a single Where-Object string for AND logic + $WhereConditions = foreach ($Rule in $Rules) { + $Property = $Rule.property + $Operator = $Rule.operator + $Value = $Rule.value + + switch ($Property) { + 'delegatedAccessStatus' { + "`$_.delegatedPrivilegeStatus -$Operator '$($Value.value)'" + } + 'availableLicense' { + if ($Operator -in @('in', 'notin')) { + $arrayValues = if ($Value -is [array]) { $Value.guid } else { @($Value.guid) } + $arrayAsString = $arrayValues | ForEach-Object { "'$_'" } + if ($Operator -eq 'in') { + "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0" + } else { + "(`$_.skuId | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0" + } + } else { + "`$_.skuId -$Operator '$($Value.guid)'" + } + } + 'availableServicePlan' { + if ($Operator -in @('in', 'notin')) { + $arrayValues = if ($Value -is [array]) { $Value.value } else { @($Value.value) } + $arrayAsString = $arrayValues | ForEach-Object { "'$_'" } + if ($Operator -eq 'in') { + # Keep tenants with ANY of the provided plans + "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -gt 0" + } else { + # Exclude tenants with ANY of the provided plans + "(`$_.servicePlans | Where-Object { `$_ -in @($($arrayAsString -join ', ')) }).Count -eq 0" + } + } else { + "`$_.servicePlans -$Operator '$($Value.value)'" + } + } + default { + Write-LogMessage -API 'TenantGroups' -message "Unknown property type: $Property" -sev Warning + $null + } + } + + } + if (!$WhereConditions) { + throw 'Generating the conditions failed. The conditions seem to be empty.' + } + $TenantObj = $AllTenants | ForEach-Object { + if ($Rules.property -contains 'availableLicense') { + if ($SkuHashtable.ContainsKey($_.customerId)) { + Write-Information "Using cached licenses for tenant $($_.defaultDomainName)" + $LicenseInfo = $SkuHashtable[$_.customerId] + } else { + Write-Information "Fetching licenses for tenant $($_.defaultDomainName)" + $LicenseInfo = New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/subscribedSkus' -TenantId $_.defaultDomainName + # Cache the result + $CacheEntity = @{ + PartitionKey = 'sku' + RowKey = [string]$_.customerId + JSON = [string]($LicenseInfo | ConvertTo-Json -Depth 5 -Compress) + } + Add-CIPPAzDataTableEntity @LicenseCacheTable -Entity $CacheEntity -Force + } + } + $SKUId = $LicenseInfo.SKUId ?? @() + $ServicePlans = (Get-CIPPTenantCapabilities -TenantFilter $_.defaultDomainName).psobject.properties.name + [pscustomobject]@{ + customerId = $_.customerId + defaultDomainName = $_.defaultDomainName + displayName = $_.displayName + skuId = $SKUId + servicePlans = $ServicePlans + delegatedPrivilegeStatus = $_.delegatedPrivilegeStatus + } + } + # Combine all conditions with the specified logic (AND or OR) + $LogicOperator = if ($Group.RuleLogic -eq 'or') { ' -or ' } else { ' -and ' } + $WhereString = $WhereConditions -join $LogicOperator + Write-Information "Evaluating tenants with condition: $WhereString" + + $ScriptBlock = [ScriptBlock]::Create($WhereString) + $MatchingTenants = $TenantObj | Where-Object $ScriptBlock + + Write-Information "Found $($MatchingTenants.Count) matching tenants for group '$($Group.Name)'" + + $CurrentMembers = Get-CIPPAzDataTableEntity @MembersTable -Filter "PartitionKey eq 'Member' and GroupId eq '$($Group.RowKey)'" + $CurrentMemberIds = $CurrentMembers.customerId + $NewMemberIds = $MatchingTenants.customerId + + $ToAdd = $NewMemberIds | Where-Object { $_ -notin $CurrentMemberIds } + $ToRemove = $CurrentMemberIds | Where-Object { $_ -notin $NewMemberIds } + + foreach ($TenantId in $ToAdd) { + $TenantInfo = $AllTenants | Where-Object { $_.customerId -eq $TenantId } + $MemberEntity = @{ + PartitionKey = 'Member' + RowKey = '{0}-{1}' -f $Group.RowKey, $TenantId + GroupId = $Group.RowKey + customerId = "$TenantId" + } + Add-CIPPAzDataTableEntity @MembersTable -Entity $MemberEntity -Force + Write-LogMessage -API 'TenantGroups' -message "Added tenant '$($TenantInfo.displayName)' to dynamic group '$($Group.Name)'" -sev Info + $TotalMembersAdded++ + } + + foreach ($TenantId in $ToRemove) { + $TenantInfo = $AllTenants | Where-Object { $_.customerId -eq $TenantId } + $MemberToRemove = $CurrentMembers | Where-Object { $_.customerId -eq $TenantId } + if ($MemberToRemove) { + Remove-AzDataTableEntity @MembersTable -Entity $MemberToRemove -Force + Write-LogMessage -API 'TenantGroups' -message "Removed tenant '$($TenantInfo.displayName)' from dynamic group '$($Group.Name)'" -sev Info + $TotalMembersRemoved++ + } + } + + $GroupsProcessed++ + Write-LogMessage -API 'TenantGroups' -message "Group '$($Group.Name)' updated: +$($ToAdd.Count) members, -$($ToRemove.Count) members" -sev Info + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'TenantGroups' -message "Failed to process group '$($Group.Name)': $ErrorMessage" -sev Error + } + } + + Write-LogMessage -API 'TenantGroups' -message "Dynamic tenant group update completed. Groups processed: $GroupsProcessed, Members added: $TotalMembersAdded, Members removed: $TotalMembersRemoved" -sev Info + + return @{ + MembersAdded = $TotalMembersAdded + MembersRemoved = $TotalMembersRemoved + GroupsProcessed = $GroupsProcessed + } + + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'TenantGroups' -message "Failed to update dynamic tenant groups: $ErrorMessage" -sev Error + throw + } +} + diff --git a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index b7361fe4b2ea..0d7f1aa20e1d 100644 --- a/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -79,7 +79,7 @@ function Invoke-CippWebhookProcessing { # Save audit log entry to table $LocationInfo = $Data.CIPPLocationInfo | ConvertFrom-Json -ErrorAction SilentlyContinue $AuditRecord = $Data.AuditRecord | ConvertFrom-Json -ErrorAction SilentlyContinue - $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL + $GenerateJSON = New-CIPPAlertTemplate -format 'json' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -AlertComment $WebhookRule.AlertComment $JsonContent = @{ Title = $GenerateJSON.Title ActionUrl = $GenerateJSON.ButtonUrl @@ -102,7 +102,7 @@ function Invoke-CippWebhookProcessing { $LogId = Send-CIPPAlert @CIPPAlert $AuditLogLink = '{0}/tenant/administration/audit-logs/log?logId={1}&tenantFilter={2}' -f $CIPPURL, $LogId, $Tenant.defaultDomainName - $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink + $GenerateEmail = New-CIPPAlertTemplate -format 'html' -data $Data -ActionResults $ActionResults -CIPPURL $CIPPURL -Tenant $Tenant.defaultDomainName -AuditLogLink $AuditLogLink -AlertComment $WebhookRule.AlertComment Write-Host 'Going to create the content' foreach ($action in $ActionList ) { diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index 04fcdfb76c9b..f6d5b73a7ebf 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -117,8 +117,6 @@ function Test-CIPPAuditLogRules { $CacheWebhooksTable = Get-CippTable -TableName 'CacheWebhooks' $ExtendedPropertiesIgnoreList = @( - 'OAuth2:Authorize' - 'OAuth2:Token' 'SAS:EndAuth' 'SAS:ProcessAuth' 'deviceAuth:ReprocessTls' diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 71c92337da9b..963614aa0aad 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -144,6 +144,16 @@ function Receive-CippHttpTrigger { $Response.Body = $Response.Body | ConvertTo-Json -Depth 20 -Compress } Push-OutputBinding -Name Response -Value ([HttpResponseContext]$Response) + } else { + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ + error = @{ + code = 'InternalServerError' + message = 'An error occurred processing the request' + } + } + }) } } return diff --git a/host.json b/host.json index e0b8b636c71e..cb92d98a0fcc 100644 --- a/host.json +++ b/host.json @@ -14,8 +14,8 @@ "maxConcurrentOrchestratorFunctions": 2, "tracing": { "distributedTracingEnabled": false, - "version": "V2" + "version": "None" } } } -} \ No newline at end of file +} diff --git a/version_latest.txt b/version_latest.txt index 85e2cd530929..acd405b1d62e 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -8.5.2 +8.6.0