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 ($existingFilter.displayName) {
                    $isTagged = Test-HydrationKitObject -Description $existingFilter.description
                    if (-not $existingFilters.ContainsKey($existingFilter.displayName)) {
                        $existingFilters[$existingFilter.displayName] = @{
                            Id          = $existingFilter.id
                            Description = $existingFilter.description
                            IsTagged    = $isTagged
                        }
                    } elseif ($isTagged -and -not $existingFilters[$existingFilter.displayName].IsTagged) {
                        $existingFilters[$existingFilter.displayName] = @{
                            Id          = $existingFilter.id
                            Description = $existingFilter.description
                            IsTagged    = $true
                        }
                    }
                }
            }
            $listUri = $existingFiltersResponse.'@odata.nextLink'
        } while ($listUri)
    } catch {
        Write-Warning "Could not retrieve existing filters: $_"
        $existingFilters = @{}
    }

    # Remove existing filters if requested
    # SAFETY: Only delete filters that have "Imported by Intune Hydration Kit" in description
    if ($RemoveExisting) {
        # Load template names to scope deletes to only filters this kit would create
        $knownTemplateNames = Get-TemplateDisplayNames -Path $TemplatePath -ArrayProperty 'filters' -Recurse

        # Collect filters to delete (only those with hydration marker AND matching a template name)
        $filtersToDelete = @()
        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
            }

            $escapedPrefix = [regex]::Escape($script:ImportPrefix)
            $nameForLookup = $filterName -replace "^$escapedPrefix", ''
            if (-not ($knownTemplateNames.Contains($filterName) -or $knownTemplateNames.Contains($nameForLookup))) {
                Write-Verbose "Skipping '$filterName' - not in this kit's templates (may be from another tool)"
                continue
            }

            $filtersToDelete += @{
                Name = $filterName
                Id   = $filterInfo.Id
            }
        }

        if ($filtersToDelete.Count -eq 0) {
            Write-Verbose "No device filters found to delete"
            return $results
        }

        # Handle WhatIf/Confirm mode
        if (-not $PSCmdlet.ShouldProcess("$($filtersToDelete.Count) device filter(s)", "Delete")) {
            if ($WhatIfPreference) {
                foreach ($filter in $filtersToDelete) {
                    Write-HydrationLog -Message " WouldDelete: $($filter.Name)" -Level Info
                    $results += New-HydrationResult -Name $filter.Name -Type 'DeviceFilter' -Action 'WouldDelete' -Status 'DryRun'
                }
            }
            return $results
        }

        # Batch delete filters using centralized helper
        $results += Invoke-GraphBatchOperation -Items $filtersToDelete -Operation 'DELETE' -BaseUrl '/deviceManagement/assignmentFilters' -ResultType 'DeviceFilter'

        return $results
    }

    # Collect all filters from templates
    $filtersToCreate = @()
    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
                }

                # Compute canonical prefixed name
                $prefixedName = "$($script:ImportPrefix)$($filter.displayName)"

                # Dual lookup: check both prefixed and unprefixed (legacy) names
                $existingName = $null
                $existingEntry = $null
                if ($existingFilters.ContainsKey($prefixedName) -and $existingFilters[$prefixedName].IsTagged) {
                    $existingName = $prefixedName
                    $existingEntry = $existingFilters[$prefixedName]
                } elseif ($existingFilters.ContainsKey($filter.displayName) -and $existingFilters[$filter.displayName].IsTagged) {
                    $existingName = $filter.displayName
                    $existingEntry = $existingFilters[$filter.displayName]
                }

                if ($existingEntry) {
                    Write-HydrationLog -Message " Skipped: $existingName" -Level Info
                    $results += New-HydrationResult -Name $existingName -Id $existingEntry.Id -Platform $filter.platform -Type 'DeviceFilter' -Action 'Skipped' -Status 'Already exists'
                    continue
                }

                # Add to list of filters to create
                $filtersToCreate += $filter
            }
        } 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"
        }
    }

    # Handle WhatIf/Confirm mode for creation
    if (-not $PSCmdlet.ShouldProcess("$($filtersToCreate.Count) device filter(s)", "Create")) {
        if ($WhatIfPreference) {
            foreach ($filter in $filtersToCreate) {
                $prefixedName = if ($filter.displayName.StartsWith($script:ImportPrefix)) { $filter.displayName } else { "$($script:ImportPrefix)$($filter.displayName)" }
                Write-HydrationLog -Message " WouldCreate: $prefixedName" -Level Info
                $results += New-HydrationResult -Name $prefixedName -Platform $filter.platform -Type 'DeviceFilter' -Action 'WouldCreate' -Status 'DryRun'
            }
        }
        return $results
    }

    # Batch create filters using centralized helper
    if ($filtersToCreate.Count -gt 0) {
        $batchItems = @()
        foreach ($filter in $filtersToCreate) {
            $prefixedName = if ($filter.displayName.StartsWith($script:ImportPrefix)) { $filter.displayName } else { "$($script:ImportPrefix)$($filter.displayName)" }
            $filterBody = @{
                displayName   = $prefixedName
                description   = New-HydrationDescription -ExistingText $filter.description
                platform      = $filter.platform
                rule          = $filter.rule
                roleScopeTags = @("0")
            }
            $batchItems += @{
                Name     = $prefixedName
                Platform = $filter.platform
                BodyJson = ($filterBody | ConvertTo-Json -Depth 10 -Compress)
            }
        }
        $results += Invoke-GraphBatchOperation -Items $batchItems -Operation 'POST' -BaseUrl '/deviceManagement/assignmentFilters' -ResultType 'DeviceFilter'
    }

    return $results
}