diff --git a/Import-AllFunctions.ps1 b/Import-AllFunctions.ps1 new file mode 100644 index 0000000..52c4bbb --- /dev/null +++ b/Import-AllFunctions.ps1 @@ -0,0 +1,17 @@ +# Dot-source all functions in IntuneOperator. +# Usage: . ./Import-AllFunctions.ps1 + +$publicFolders = Get-ChildItem -Path "$PSScriptRoot/src/functions/public" -Directory +$privateFolder = Join-Path $PSScriptRoot 'src/functions/private' + +# Dot-source all private functions. +Get-ChildItem -Path $privateFolder -Filter '*.ps1' | ForEach-Object { + . $_.FullName +} + +# Dot-source all public functions, including nested folders. +foreach ($folder in $publicFolders) { + Get-ChildItem -Path $folder.FullName -Recurse -Filter '*.ps1' | ForEach-Object { + . $_.FullName + } +} diff --git a/src/functions/public/Remediation/Get-IntuneRemediationDeviceStatus.ps1 b/src/functions/public/Remediation/Get-IntuneRemediationDeviceStatus.ps1 index 7318dd1..d81aa3c 100644 --- a/src/functions/public/Remediation/Get-IntuneRemediationDeviceStatus.ps1 +++ b/src/functions/public/Remediation/Get-IntuneRemediationDeviceStatus.ps1 @@ -22,6 +22,12 @@ The ID (GUID) of the device health script (remediation) to query. Parameter set: ById. + .PARAMETER ConvertPreRemediationOutput + Parses PreRemediationOutput as JSON and adds a PreRemediationData property when valid JSON is present. + + .PARAMETER ConvertPostRemediationOutput + Parses PostRemediationOutput as JSON and adds a PostRemediationData property when valid JSON is present. + .EXAMPLE Connect-MgGraph -Scopes "DeviceManagementConfiguration.Read.All" Get-IntuneRemediationDeviceStatus -Name "BitLocker*" @@ -38,6 +44,18 @@ Pipes remediations that have devices with issues into this cmdlet to get device-level detail. + .EXAMPLE + Get-IntuneRemediationDeviceStatus -Name 'Win | Device | All | Detection | Secure Boot Status' -ConvertPreRemediationOutput | + Where-Object { $null -ne $_.PreRemediationData } + + Parses valid JSON found in PreRemediationOutput and exposes it as PreRemediationData. + + .EXAMPLE + Get-IntuneRemediationDeviceStatus -Name 'Win | Device | All | Detection | Secure Boot Status' -ConvertPreRemediationOutput -ConvertPostRemediationOutput | + Select-Object DeviceName, DetectionState, RemediationState, PreRemediationData, PostRemediationData + + Parses valid JSON from both remediation output fields and selects the parsed data alongside device state. + .INPUTS System.String (Name or Id via pipeline by property name) @@ -53,6 +71,8 @@ - RemediationState (string) : Outcome of the last remediation script run - PreRemediationOutput (string) : stdout captured before remediation ran - PostRemediationOutput (string) : stdout captured after remediation ran + - PreRemediationData (object/null) : Parsed JSON from PreRemediationOutput when -ConvertPreRemediationOutput is used + - PostRemediationData (object/null): Parsed JSON from PostRemediationOutput when -ConvertPostRemediationOutput is used - DetectionOutput (string) : Detection-only script stdout - PreRemediationError (string) : stderr captured before remediation ran - RemediationError (string) : stderr from the remediation script @@ -85,7 +105,13 @@ )] [ValidatePattern('^[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}$')] [Alias('RemediationId')] - [string]$Id + [string]$Id, + + [Parameter(HelpMessage = 'Parse pre-remediation output as JSON and add PreRemediationData')] + [switch]$ConvertPreRemediationOutput, + + [Parameter(HelpMessage = 'Parse post-remediation output as JSON and add PostRemediationData')] + [switch]$ConvertPostRemediationOutput ) begin { @@ -229,8 +255,12 @@ } } - # Cast Graph fields to predictable output types for downstream filtering/export. - [PSCustomObject]@{ + # Capture the raw script output once so optional JSON parsing stays local. + $preRemediationOutput = [string]$state.preRemediationDetectionScriptOutput + $postRemediationOutput = [string]$state.postRemediationDetectionScriptOutput + + # Build the output object in-order and extend it only when JSON parsing is requested. + $outputObject = [ordered]@{ RemediationName = $remediationName RemediationId = $remediationId DeviceId = $deviceId @@ -239,13 +269,38 @@ LastStateUpdate = $lastUpdate DetectionState = [string]$state.detectionState RemediationState = [string]$state.remediationState - PreRemediationOutput = [string]$state.preRemediationDetectionScriptOutput - PostRemediationOutput = [string]$state.postRemediationDetectionScriptOutput + PreRemediationOutput = $preRemediationOutput + PostRemediationOutput = $postRemediationOutput DetectionOutput = [string]$state.detectionScriptOutput PreRemediationError = [string]$state.preRemediationDetectionScriptError RemediationError = [string]$state.remediationScriptError DetectionError = [string]$state.detectionScriptError } + + if ($ConvertPreRemediationOutput.IsPresent) { + $outputObject.PreRemediationData = $null + if (-not [string]::IsNullOrWhiteSpace($preRemediationOutput) -and $preRemediationOutput.Trim().StartsWith('{')) { + try { + $outputObject.PreRemediationData = $preRemediationOutput | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Verbose -Message "Failed to parse pre-remediation JSON for device '$deviceName'." + } + } + } + + if ($ConvertPostRemediationOutput.IsPresent) { + $outputObject.PostRemediationData = $null + if (-not [string]::IsNullOrWhiteSpace($postRemediationOutput) -and $postRemediationOutput.Trim().StartsWith('{')) { + try { + $outputObject.PostRemediationData = $postRemediationOutput | ConvertFrom-Json -ErrorAction Stop + } catch { + Write-Verbose -Message "Failed to parse post-remediation JSON for device '$deviceName'." + } + } + } + + # Cast Graph fields to predictable output types for downstream filtering/export. + [PSCustomObject]$outputObject } } } # Process diff --git a/tests/public/Remediation/Get-IntuneRemediationDeviceStatus.Tests.ps1 b/tests/public/Remediation/Get-IntuneRemediationDeviceStatus.Tests.ps1 index a43a28d..2a8f4e0 100644 --- a/tests/public/Remediation/Get-IntuneRemediationDeviceStatus.Tests.ps1 +++ b/tests/public/Remediation/Get-IntuneRemediationDeviceStatus.Tests.ps1 @@ -71,6 +71,120 @@ Describe 'Get-IntuneRemediationDeviceStatus' { $result[0].LastStateUpdate | Should -BeOfType [datetime] } + It 'Should add parsed remediation data when JSON output switches are used' { + # Arrange + $mockScriptId = '11f5d1d7-2d2b-4d8c-9f0a-0d2a3d1e2f3a' + $mockScriptName = 'Secure Boot remediation' + + $mockListResponse = [PSCustomObject]@{ + value = @( + [PSCustomObject]@{ id = $mockScriptId; displayName = $mockScriptName } + ) + } + + $mockRunStatesResponse = [PSCustomObject]@{ + value = @( + [PSCustomObject]@{ + id = 'state-json' + detectionState = 'success' + remediationState = 'success' + lastStateUpdateDateTime = '2026-03-12T08:00:00Z' + preRemediationDetectionScriptOutput = '{"secureBoot":false,"status":"off"}' + postRemediationDetectionScriptOutput = '{"secureBoot":true,"status":"on"}' + detectionScriptOutput = $null + preRemediationDetectionScriptError = $null + remediationScriptError = $null + detectionScriptError = $null + managedDevice = [PSCustomObject]@{ + id = 'dev-json' + deviceName = 'PC-JSON' + userPrincipalName = 'json@contoso.com' + } + } + ) + } + + Mock -CommandName 'Invoke-GraphGet' -MockWith { + param([string]$Uri) + if ($Uri -match 'deviceHealthScripts\?\$select=id,displayName$') { + return $mockListResponse + } + if ($Uri -match '/deviceHealthScripts/.+/deviceRunStates') { + return $mockRunStatesResponse + } + throw "Unexpected URI: $Uri" + } + + # Act + $result = @( + Get-IntuneRemediationDeviceStatus -Name $mockScriptName -ConvertPreRemediationOutput -ConvertPostRemediationOutput + ) + + # Assert + $result.Count | Should -Be 1 + $result[0].PreRemediationData | Should -Not -BeNullOrEmpty + $result[0].PreRemediationData.secureBoot | Should -BeFalse + $result[0].PostRemediationData | Should -Not -BeNullOrEmpty + $result[0].PostRemediationData.secureBoot | Should -BeTrue + } + + It 'Should leave parsed remediation data null when JSON parsing fails' { + # Arrange + $mockScriptId = '12f5d1d7-2d2b-4d8c-9f0a-0d2a3d1e2f3a' + $mockScriptName = 'Broken JSON remediation' + + $mockListResponse = [PSCustomObject]@{ + value = @( + [PSCustomObject]@{ id = $mockScriptId; displayName = $mockScriptName } + ) + } + + $mockRunStatesResponse = [PSCustomObject]@{ + value = @( + [PSCustomObject]@{ + id = 'state-bad-json' + detectionState = 'fail' + remediationState = 'skipped' + lastStateUpdateDateTime = '2026-03-12T08:00:00Z' + preRemediationDetectionScriptOutput = '{"secureBoot":' + postRemediationDetectionScriptOutput = 'plain text' + detectionScriptOutput = $null + preRemediationDetectionScriptError = $null + remediationScriptError = $null + detectionScriptError = $null + managedDevice = [PSCustomObject]@{ + id = 'dev-bad-json' + deviceName = 'PC-BAD-JSON' + userPrincipalName = 'broken@contoso.com' + } + } + ) + } + + Mock -CommandName 'Invoke-GraphGet' -MockWith { + param([string]$Uri) + if ($Uri -match 'deviceHealthScripts\?\$select=id,displayName$') { + return $mockListResponse + } + if ($Uri -match '/deviceHealthScripts/.+/deviceRunStates') { + return $mockRunStatesResponse + } + throw "Unexpected URI: $Uri" + } + + # Act + $result = @( + Get-IntuneRemediationDeviceStatus -Name $mockScriptName -ConvertPreRemediationOutput -ConvertPostRemediationOutput -Verbose:$false + ) + + # Assert + $result.Count | Should -Be 1 + $result[0].PSObject.Properties.Name | Should -Contain 'PreRemediationData' + $result[0].PSObject.Properties.Name | Should -Contain 'PostRemediationData' + $result[0].PreRemediationData | Should -Be $null + $result[0].PostRemediationData | Should -Be $null + } + It 'Should return multiple rows when multiple devices have run states' { # Arrange $mockScriptId = 'a1f5d1d7-2d2b-4d8c-9f0a-0d2a3d1e2f3a'