Public/Import-IntuneDeviceFilter.ps1

function Import-IntuneDeviceFilter {
    <#
    .SYNOPSIS
        Creates device filters for Intune from templates
    .DESCRIPTION
        Reads JSON templates from Templates/Filters and creates device filters via Graph API.
        Filters can be used to target or exclude devices from policy assignments.
    .PARAMETER TemplatePath
        Path to the filter template directory (defaults to Templates/Filters)
    .PARAMETER RemoveExisting
        If specified, removes existing filters created by this kit instead of creating new ones
    .PARAMETER Platform
        Filter templates by platform. Valid values: Windows, macOS, iOS, Android, All.
        Defaults to 'All' which imports all filter templates regardless of platform.
        Note: Linux device filters are not currently supported by Intune.
    .EXAMPLE
        Import-IntuneDeviceFilter
    .EXAMPLE
        Import-IntuneDeviceFilter -TemplatePath ./MyFilters
    .EXAMPLE
        Import-IntuneDeviceFilter -Platform Windows,macOS
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [string]$TemplatePath,

        [Parameter()]
        [ValidateSet('Windows', 'macOS', 'iOS', 'Android', 'All')]
        [string[]]$Platform = @('All'),

        [Parameter()]
        [switch]$RemoveExisting
    )

    if (-not $TemplatePath) {
        $TemplatePath = Join-Path -Path $script:TemplatesPath -ChildPath "Filters"
    }

    if (-not (Test-Path -Path $TemplatePath)) {
        Write-Warning "Filter template directory not found: $TemplatePath"
        return @()
    }

    $templateFiles = Get-FilteredTemplates -Path $TemplatePath -Platform $Platform -FilterMode 'Prefix' -Recurse -ResourceType "filter template"

    if (-not $templateFiles -or $templateFiles.Count -eq 0) {
        Write-Warning "No filter templates found in: $TemplatePath"
        return @()
    }

    $results = @()

    # Prefetch existing filters with pagination (OData filter on displayName not supported for this endpoint)
    # Store full filter objects so we can check descriptions later
    $existingFilters = @{}
    try {
        $listUri = "beta/deviceManagement/assignmentFilters?`$select=id,displayName,description"
        do {
            $existingFiltersResponse = Invoke-MgGraphRequest -Method GET -Uri $listUri -ErrorAction Stop
            foreach ($existingFilter in $existingFiltersResponse.value) {
                if (-not $existingFilters.ContainsKey($existingFilter.displayName)) {
                    $existingFilters[$existingFilter.displayName] = @{
                        Id          = $existingFilter.id
                        Description = $existingFilter.description
                    }
                }
            }
            $listUri = $existingFiltersResponse.'@odata.nextLink'
        } while ($listUri)
    } catch {
        Write-Warning "Could not retrieve existing filters: $_"
        $existingFilters = @{}
    }

    # Build a simple name->id lookup for backwards compatibility in the import section
    $existingFilterNames = @{}
    foreach ($key in $existingFilters.Keys) {
        $existingFilterNames[$key] = $existingFilters[$key].Id
    }

    # Remove existing filters if requested
    # SAFETY: Only delete filters that have "Imported by Intune Hydration Kit" in description
    if ($RemoveExisting) {
        foreach ($filterName in $existingFilters.Keys) {
            $filterInfo = $existingFilters[$filterName]

            # Safety check: Only delete if created by this kit (has hydration marker in description)
            if (-not (Test-HydrationKitObject -Description $filterInfo.Description -ObjectName $filterName)) {
                Write-Verbose "Skipping '$filterName' - not created by Intune Hydration Kit"
                continue
            }

            if ($PSCmdlet.ShouldProcess($filterName, "Delete device filter")) {
                $maxRetries = 3
                $retryCount = 0
                $success = $false

                while (-not $success -and $retryCount -lt $maxRetries) {
                    try {
                        Invoke-MgGraphRequest -Method DELETE -Uri "beta/deviceManagement/assignmentFilters/$($filterInfo.Id)" -ErrorAction Stop
                        Write-HydrationLog -Message " Deleted: $filterName" -Level Info
                        $results += New-HydrationResult -Name $filterName -Type 'DeviceFilter' -Action 'Deleted' -Status 'Success'
                        $success = $true
                    } catch {
                        $statusCode = $_.Exception.Response.StatusCode.value__

                        # Only retry on server errors (500+), not client errors (400-499)
                        if ($statusCode -ge 500 -and $retryCount -lt ($maxRetries - 1)) {
                            $retryCount++
                            $waitSeconds = [math]::Pow(2, $retryCount)
                            Write-HydrationLog -Message " Retry $retryCount/$maxRetries for '$filterName' after ${waitSeconds}s (HTTP $statusCode)" -Level Info
                            Start-Sleep -Seconds $waitSeconds
                        } else {
                            $errMessage = Get-GraphErrorMessage -ErrorRecord $_
                            Write-HydrationLog -Message " [!] Failed: $filterName - $errMessage" -Level Warning
                            $results += New-HydrationResult -Name $filterName -Type 'DeviceFilter' -Action 'Failed' -Status "Delete failed: $errMessage"
                            break
                        }
                    }
                }
            } else {
                Write-HydrationLog -Message " WouldDelete: $filterName" -Level Info
                $results += New-HydrationResult -Name $filterName -Type 'DeviceFilter' -Action 'WouldDelete' -Status 'DryRun'
            }
        }

        return $results
    }

    # Process each template file
    foreach ($templateFile in $templateFiles) {
        try {
            $template = Get-Content -Path $templateFile.FullName -Raw -Encoding utf8 | ConvertFrom-Json

            # Each template file contains a "filters" array
            if (-not $template.filters) {
                Write-Warning "Template missing 'filters' array: $($templateFile.FullName)"
                $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'DeviceFilter' -Action 'Failed' -Status "Missing 'filters' array"
                continue
            }

            foreach ($filter in $template.filters) {
                # Validate required properties
                if (-not $filter.displayName) {
                    Write-Warning "Filter missing displayName in: $($templateFile.FullName)"
                    continue
                }
                if (-not $filter.platform) {
                    Write-Warning "Filter '$($filter.displayName)' missing platform in: $($templateFile.FullName)"
                    continue
                }
                if (-not $filter.rule) {
                    Write-Warning "Filter '$($filter.displayName)' missing rule in: $($templateFile.FullName)"
                    continue
                }

                try {
                    # Check if filter already exists using pre-fetched list
                    if ($existingFilterNames.ContainsKey($filter.displayName)) {
                        Write-HydrationLog -Message " Skipped: $($filter.displayName)" -Level Info
                        $results += New-HydrationResult -Name $filter.displayName -Id $existingFilterNames[$filter.displayName] -Platform $filter.platform -Type 'DeviceFilter' -Action 'Skipped' -Status 'Already exists'
                        continue
                    }

                    if ($PSCmdlet.ShouldProcess($filter.displayName, "Create device filter")) {
                        # Build description with hydration kit marker
                        $description = if ($filter.description) {
                            "$($filter.description) - Imported by Intune Hydration Kit"
                        } else {
                            "Imported by Intune Hydration Kit"
                        }

                        $filterBody = @{
                            displayName   = $filter.displayName
                            description   = $description
                            platform      = $filter.platform
                            rule          = $filter.rule
                            roleScopeTags = @("0")
                        }

                        $maxRetries = 3
                        $retryCount = 0
                        $success = $false

                        while (-not $success -and $retryCount -lt $maxRetries) {
                            try {
                                $newFilter = Invoke-MgGraphRequest -Method POST -Uri "beta/deviceManagement/assignmentFilters" -Body $filterBody -ErrorAction Stop
                                Write-HydrationLog -Message " Created: $($filter.displayName)" -Level Info
                                $results += New-HydrationResult -Name $filter.displayName -Id $newFilter.id -Platform $filter.platform -Type 'DeviceFilter' -Action 'Created' -Status 'Success'
                                $success = $true
                            } catch {
                                $statusCode = $_.Exception.Response.StatusCode.value__

                                # Only retry on server errors (500+), not client errors (400-499)
                                if ($statusCode -ge 500 -and $retryCount -lt ($maxRetries - 1)) {
                                    $retryCount++
                                    $waitSeconds = [math]::Pow(2, $retryCount)
                                    Write-HydrationLog -Message " Retry $retryCount/$maxRetries for '$($filter.displayName)' after ${waitSeconds}s (HTTP $statusCode)" -Level Info
                                    Start-Sleep -Seconds $waitSeconds
                                } else {
                                    $errMessage = Get-GraphErrorMessage -ErrorRecord $_
                                    Write-HydrationLog -Message " [!] Failed: $($filter.displayName) - $errMessage" -Level Warning
                                    $results += New-HydrationResult -Name $filter.displayName -Platform $filter.platform -Type 'DeviceFilter' -Action 'Failed' -Status $errMessage
                                    break
                                }
                            }
                        }
                    } else {
                        Write-HydrationLog -Message " WouldCreate: $($filter.displayName)" -Level Info
                        $results += New-HydrationResult -Name $filter.displayName -Platform $filter.platform -Type 'DeviceFilter' -Action 'WouldCreate' -Status 'DryRun'
                    }
                } catch {
                    $errMessage = Get-GraphErrorMessage -ErrorRecord $_
                    Write-HydrationLog -Message " Failed: $($filter.displayName) - $errMessage" -Level Warning
                    $results += New-HydrationResult -Name $filter.displayName -Platform $filter.platform -Type 'DeviceFilter' -Action 'Failed' -Status $errMessage
                }
            }
        } catch {
            $errMessage = Get-GraphErrorMessage -ErrorRecord $_
            Write-HydrationLog -Message " Failed to parse: $($templateFile.Name) - $errMessage" -Level Warning
            $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'DeviceFilter' -Action 'Failed' -Status "Parse error: $errMessage"
        }
    }

    return $results
}