Convenient way to get Intune Remediation Scripts assignments and info
|

Convenient way to get Intune Remediation Scripts assignments and info

Reading Time: 7 minutes

So I was trying to find the Intune remediation scripts assignments to perform a cleanup and wanted to avoid the manual check of every single script — going into each one, checking its assignments, filters, and schedules one by one. If you have ever been there, you know how tedious that gets, especially when you have dozens of scripts deployed across different groups.

That is exactly why I created a simple PowerShell script to do this automatically.

Instead of clicking through the Intune portal endlessly, this script pulls all your Intune Remediation Scripts (deviceHealthScripts) and their full assignment details in one shot — outputs it to the console, a grid view, and a CSV you can actually work with.

What Are Intune Remediation Scripts?

If you are not fully familiar, Remediation Scripts are pairs of PowerShell scripts in Microsoft Intune — a detection script and a remediation script — used to proactively find and fix issues on managed Windows devices.

They live under:
Intune > Devices > Scripts and remediations > Remediation

Over time, environments tend to accumulate a lot of these — some assigned, some not, some scheduled daily, some hourly — and keeping track of all of it manually is a nightmare.

What Does This Script Do?

The script connects to Microsoft Graph (beta endpoint) and for each remediation script in your tenant it collects:

  • Script name, ID, description, publisher, and version
  • Created and last modified date
  • Assignment target type (specific group, all devices, all users, etc.)
  • Group name and ID (resolved automatically from the group ID)
  • Assignment filter type and filter ID
  • Run schedule details (daily, hourly, weekly — with interval, time, and UTC info)

Everything gets neatly assembled into a report that you can:

  • View directly in the console
  • Open in Out-GridView for quick filtering
  • Export to a CSV file for documentation or cleanup work
Convenient way to get Intune Remediation Scripts assignments and info

Requirements

Before running the script, make sure you have:

  • PowerShell 5.1+ or PowerShell 7+
  • Microsoft Graph PowerShell SDK installed:
Install-Module Microsoft.Graph #-Scope CurrentUser
  • The following delegated Graph permissions (you will be prompted to consent on first run):
    • DeviceManagementConfiguration.Read.All
    • DeviceManagementManagedDevices.Read.All
    • Group.Read.All

The Script

At the top of the script you will find a settings block — adjust these to fit your needs:

$GridView = $true   # Set to $true to open Out-GridView
$CSV      = $true    # Set to $true to export CSV
$CsvPath  = "C:\TEMP\Intune_RemediationScripts_Assignments_Report.csv"

Here is the full script (you can also find it in my GitHub page here):

#-----------------------------------------
# Default settings (edit as needed)
#-----------------------------------------
$GridView = $true
$CSV = $true
$CsvPath = "C:\TEMP\Intune_RemediationScripts_Assignments_Report.csv"

#-----------------------------------------
# Script purpose
#-----------------------------------------
# This script retrieves all Intune Remediation scripts assignments (deviceHealthScripts) and their data:
# - Assignment target type (group/all devices/all users/etc.)
# - Group name and ID (when applicable)
# - Assignment filter details
# - Schedule type and frequency/timing summary
#
# Output can be viewed in console, Out-GridView, and/or CSV.

Write-Host "=====================================================" -ForegroundColor Cyan
Write-Host " Intune Remediation Scripts Assignment Report" -ForegroundColor Cyan
Write-Host "=====================================================" -ForegroundColor Cyan
Write-Host "[$(Get-Date -Format 'u')] Starting script..." -ForegroundColor Yellow

#-----------------------------------------
# Connect to Microsoft Graph
#-----------------------------------------
Write-Host "[$(Get-Date -Format 'u')] Checking Microsoft Graph connection..." -ForegroundColor Yellow
try {
    if (-not (Get-MgContext)) {
        Write-Host "[$(Get-Date -Format 'u')] No active Graph session found. Connecting..." -ForegroundColor Yellow
        Connect-MgGraph -Scopes @(
            "DeviceManagementConfiguration.Read.All",
            "DeviceManagementManagedDevices.Read.All",
            "Group.Read.All"
        ) | Out-Null
        Write-Host "[$(Get-Date -Format 'u')] Connected to Microsoft Graph." -ForegroundColor Green
    }
    else {
        $ctx = Get-MgContext
        Write-Host "[$(Get-Date -Format 'u')] Already connected to Graph as $($ctx.Account)." -ForegroundColor Green
    }
}
catch {
    Write-Error "Failed to connect to Microsoft Graph. $_"
    return
}

#-----------------------------------------
# Helper: Resolve AAD group display name safely
#-----------------------------------------
function Get-GroupDisplayNameSafe {
    param(
        [Parameter(Mandatory = $true)][string]$GroupId
    )
    try {
        $group = Get-MgGroup -GroupId $GroupId -ErrorAction Stop
        return $group.DisplayName
    }
    catch {
        return "Unknown group ($GroupId)"
    }
}

#-----------------------------------------
# Helper: Convert assignment runSchedule into readable text
#-----------------------------------------
function Get-ScheduleSummary {
    param(
        [Parameter(Mandatory = $false)]$RunSchedule
    )

    if (-not $RunSchedule) { return "Not specified" }

    $odataType = $RunSchedule.'@odata.type'

    switch -Regex ($odataType) {
        "deviceHealthScriptDailySchedule" {
            $time = if ($RunSchedule.time) { $RunSchedule.time } else { "N/A" }
            $interval = if ($RunSchedule.interval) { $RunSchedule.interval } else { "1" }
            $useUtc = if ($null -ne $RunSchedule.useUtc) { $RunSchedule.useUtc } else { "N/A" }
            return "Daily | Every $interval day(s) | Time: $time | UTC: $useUtc"
        }
        "deviceHealthScriptHourlySchedule" {
            $interval = if ($RunSchedule.interval) { $RunSchedule.interval } else { "1" }
            $useUtc = if ($null -ne $RunSchedule.useUtc) { $RunSchedule.useUtc } else { "N/A" }
            return "Hourly | Every $interval hour(s) | UTC: $useUtc"
        }
        "deviceHealthScriptWeeklySchedule" {
            $interval = if ($RunSchedule.interval) { $RunSchedule.interval } else { "1" }
            $time = if ($RunSchedule.time) { $RunSchedule.time } else { "N/A" }
            $day = if ($RunSchedule.dayOfWeek) { $RunSchedule.dayOfWeek } else { "N/A" }
            $useUtc = if ($null -ne $RunSchedule.useUtc) { $RunSchedule.useUtc } else { "N/A" }
            return "Weekly | Every $interval week(s) | Day: $day | Time: $time | UTC: $useUtc"
        }
        default {
            return "Other schedule type ($odataType): $($RunSchedule | ConvertTo-Json -Compress -Depth 10)"
        }
    }
}

#-----------------------------------------
# Main: Retrieve remediation scripts
#-----------------------------------------
$baseUrl = "https://graph.microsoft.com/beta/deviceManagement/deviceHealthScripts"
Write-Host "[$(Get-Date -Format 'u')] Retrieving remediation scripts from Intune..." -ForegroundColor Yellow

try {
    $scriptsResponse = Invoke-MgGraphRequest -Uri $baseUrl -Method GET
}
catch {
    Write-Error "Failed to retrieve remediation scripts. $_"
    return
}

if (-not $scriptsResponse.value) {
    Write-Host "[$(Get-Date -Format 'u')] No remediation scripts found." -ForegroundColor Yellow
    return
}

$totalScripts = $scriptsResponse.value.Count
Write-Host "[$(Get-Date -Format 'u')] Found $totalScripts remediation script(s)." -ForegroundColor Green

$report = New-Object System.Collections.Generic.List[object]
$scriptCounter = 0

foreach ($script in $scriptsResponse.value) {
    $scriptCounter++
    $scriptId = $script.id
    $scriptName = $script.displayName
    $scriptDescription = $script.description

    Write-Host "[$(Get-Date -Format 'u')] [$scriptCounter/$totalScripts] Checking script: '$scriptName'" -ForegroundColor Cyan

    $scriptAssignmentsUrl = "$baseUrl/$scriptId/assignments"

    try {
        $assignmentsResponse = Invoke-MgGraphRequest -Uri $scriptAssignmentsUrl -Method GET
        $assignments = $assignmentsResponse.value
    }
    catch {
        Write-Warning "Could not get assignments for script '$scriptName' ($scriptId)."
        $assignments = @()
    }

    if (-not $assignments -or $assignments.Count -eq 0) {
        Write-Host "[$(Get-Date -Format 'u')]    -> No assignments found." -ForegroundColor DarkYellow

        $report.Add([PSCustomObject]@{
            ScriptName           = $scriptName
            ScriptId             = $scriptId
            Description          = $scriptDescription
            Publisher            = $script.publisher
            Version              = $script.version
            CreatedDateTime      = $script.createdDateTime
            LastModifiedDateTime = $script.lastModifiedDateTime
            AssignmentId         = ""
            AssignmentTargetType = "Unassigned"
            AssignmentGroupId    = ""
            AssignmentGroupName  = ""
            AssignmentIntent     = ""
            AssignmentFilterType = ""
            AssignmentFilterId   = ""
            ScheduleType         = ""
            ScheduleSummary      = "Not assigned"
        })
        continue
    }

    Write-Host "[$(Get-Date -Format 'u')]    -> Found $($assignments.Count) assignment(s)." -ForegroundColor Green

    foreach ($a in $assignments) {
        $target = $a.target
        $targetType = $target.'@odata.type'

        $groupId = ""
        $groupName = ""
        $intent = ""
        $filterType = ""
        $filterId = ""

        if ($target.deviceAndAppManagementAssignmentFilterType) {
            $filterType = $target.deviceAndAppManagementAssignmentFilterType
        }
        if ($target.deviceAndAppManagementAssignmentFilterId) {
            $filterId = $target.deviceAndAppManagementAssignmentFilterId
        }

        if ($targetType -match "groupAssignmentTarget") {
            $groupId = $target.groupId
            if ($groupId) { $groupName = Get-GroupDisplayNameSafe -GroupId $groupId }
        }

        if ($target.intent) { $intent = $target.intent }

        $scheduleType = ""
        $scheduleSummary = "Not specified"

        if ($a.runSchedule) {
            $scheduleType = $a.runSchedule.'@odata.type'
            $scheduleSummary = Get-ScheduleSummary -RunSchedule $a.runSchedule
        }

        $report.Add([PSCustomObject]@{
            ScriptName           = $scriptName
            ScriptId             = $scriptId
            Description          = $scriptDescription
            Publisher            = $script.publisher
            Version              = $script.version
            CreatedDateTime      = $script.createdDateTime
            LastModifiedDateTime = $script.lastModifiedDateTime
            AssignmentId         = $a.id
            AssignmentTargetType = $targetType
            AssignmentGroupId    = $groupId
            AssignmentGroupName  = $groupName
            AssignmentIntent     = $intent
            AssignmentFilterType = $filterType
            AssignmentFilterId   = $filterId
            ScheduleType         = $scheduleType
            ScheduleSummary      = $scheduleSummary
        })
    }
}

#-----------------------------------------
# Output section
#-----------------------------------------
Write-Host "[$(Get-Date -Format 'u')] Preparing final report output..." -ForegroundColor Yellow

$reportSorted = $report | Sort-Object ScriptName, AssignmentGroupName

$reportSorted | Format-Table -AutoSize

Write-Host "[$(Get-Date -Format 'u')] Total output rows: $($reportSorted.Count)" -ForegroundColor Green

if ($GridView) {
    Write-Host "[$(Get-Date -Format 'u')] Opening Out-GridView..." -ForegroundColor Yellow
    $reportSorted | Out-GridView -Title "Intune Remediation Scripts Assignments"
}

if ($CSV) {
    $csvFolder = Split-Path -Path $CsvPath -Parent
    if (-not (Test-Path -Path $csvFolder)) {
        Write-Host "[$(Get-Date -Format 'u')] CSV folder does not exist. Creating: $csvFolder" -ForegroundColor Yellow
        New-Item -Path $csvFolder -ItemType Directory -Force | Out-Null
    }

    Write-Host "[$(Get-Date -Format 'u')] Exporting CSV to: $CsvPath" -ForegroundColor Yellow
    $reportSorted | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
    Write-Host "[$(Get-Date -Format 'u')] CSV export complete." -ForegroundColor Green
}

Write-Host "[$(Get-Date -Format 'u')] Script finished successfully." -ForegroundColor Cyan

Breaking Down the Key Parts

1. Settings Block

At the very top you control the output behaviour. Toggle $GridView and $CSV on or off, and set your own export path. No need to touch anything else for basic use.

2. Graph Connection

The script checks if you already have an active Graph session before prompting for login. If you are already connected from a previous session in the same PowerShell window, it reuses that connection and tells you which account is active — no unnecessary re-authentication.

3. Group Name Resolution

Assignments in Graph often return just a Group ID. The helper function Get-GroupDisplayNameSafe resolves that ID to a human-readable display name automatically. If the group cannot be found (deleted groups, permission issues), it falls back gracefully with "Unknown group (ID)" rather than throwing an error.

4. Schedule Summary

The Get-ScheduleSummary function translates the raw Graph schedule object into a clean readable string like:

Daily | Every 1 day(s) | Time: 08:00:00 | UTC: True
Weekly | Every 1 week(s) | Day: monday | Time: 07:00:00 | UTC: False

Much easier to read in a spreadsheet than raw JSON.

5. Unassigned Scripts Are Included

Scripts with no assignments are still included in the report with AssignmentTargetType = "Unassigned". This is the most useful part for cleanup — you can instantly spot orphaned scripts that are deployed to nobody and can be safely removed.

Sample CSV Output Columns

ColumnDescription
ScriptNameDisplay name of the remediation script
ScriptIdUnique GUID of the script
DescriptionScript description from Intune
PublisherPublisher field
VersionScript version
CreatedDateTimeWhen the script was created
LastModifiedDateTimeLast modification timestamp
AssignmentTargetTypegroupAssignmentTarget, allDevicesAssignmentTarget, etc.
AssignmentGroupNameResolved AAD group display name
AssignmentGroupIdAAD group GUID
AssignmentFilterTypeInclude / Exclude / None
AssignmentFilterIdGUID of the assignment filter
ScheduleTypeRaw schedule odata type
ScheduleSummaryHuman-readable schedule summary

Tips for Using the Report

For cleanup: Filter the CSV on AssignmentTargetType = "Unassigned" — those are your candidates for deletion or review.

For auditing: Sort by ScriptName and check whether groups, filters, and schedules are consistent and intentional across your environment.

For documentation: The CSV gives you a ready-made inventory of all your remediation scripts and their full deployment scope — handy for change management records or onboarding new colleagues.

Frequently Asked Questions

Does this script make any changes to Intune?
No. It is fully read-only. It only calls GET endpoints on Microsoft Graph — nothing is modified, created, or deleted.

Why does it use the beta endpoint?
The deviceHealthScripts resource with full assignment and schedule details is only available on the Graph beta API. The v1.0 endpoint does not expose all the scheduling properties needed for this report.

What if I have more than 100 scripts?
The current version fetches the first page of results. If you have a large tenant with more than 100 remediation scripts, you may want to add pagination support using the @odata.nextLink property returned in the Graph response.

Can I run this without admin rights?
You need at least read permissions in Intune and the ability to read group names in Azure AD. A Global Reader or Intune Service Administrator role will work fine.

Wrapping Up

Intune Remediation Scripts are powerful, but managing them at scale without tooling is genuinely painful. This script turns a tedious manual audit into a one-click report — saving you time and giving you a clear picture of what is deployed, to whom, and on what schedule.

If you found this useful, feel free to share it or drop a comment below. And if you extend it — for example adding pagination or filtering by publisher — I would love to hear what you built on top of it.

Happy automating!

References and Documentation

Other Interesting Posts

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *