Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Import-AllFunctions.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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*"
Expand All @@ -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)

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
114 changes: 114 additions & 0 deletions tests/public/Remediation/Get-IntuneRemediationDeviceStatus.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading