Modules/M365DSCIntuneUtil.psm1

function ConvertFrom-IntunePolicyAssignment
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable[]])]
    param (
        [Parameter(Mandatory = $true)]
        [Array]
        $Assignments,

        [Parameter()]
        [System.Boolean]
        $IncludeDeviceFilter = $true
    )

    if ($null -eq $Script:IntuneAssignmentFilters)
    {
        $Script:IntuneAssignmentFilters = Get-MgBetaDeviceManagementAssignmentFilter -All -ErrorAction SilentlyContinue | ForEach-Object {
            @{
                FilterId    = $_.Id
                DisplayName = $_.DisplayName
            }
        }
    }

    $assignmentResult = @()
    foreach ($assignment in $Assignments)
    {
        $hashAssignment = [ordered]@{}
        if ($null -ne $assignment.Target.'@odata.type')
        {
            $dataType = $assignment.Target.'@odata.type'
        }
        else
        {
            $dataType = $assignment.Target.'@odata.type'
        }

        if ($null -ne $assignment.Target.groupId)
        {
            $groupId = $assignment.Target.groupId
        }
        else
        {
            $groupId = $assignment.Target.groupId
        }

        if ($null -ne $assignment.Target.collectionId)
        {
            $collectionId = $assignment.Target.collectionId
        }
        else
        {
            $collectionId = $assignment.Target.collectionId
        }

        $hashAssignment.Add('dataType', $dataType)
        if (-not [string]::IsNullOrEmpty($groupId))
        {
            $hashAssignment.Add('groupId', $groupId)

            $group = Get-MgGroup -GroupId ($groupId) -ErrorAction SilentlyContinue
            if ($null -ne $group)
            {
                $groupDisplayName = $group.DisplayName
            }
        }
        if ($dataType -eq '#microsoft.graph.allLicensedUsersAssignmentTarget')
        {
            $groupDisplayName = 'All users'
        }
        if ($dataType -eq '#microsoft.graph.allDevicesAssignmentTarget')
        {
            $groupDisplayName = 'All devices'
        }
        if ($null -ne $groupDisplayName)
        {
            $hashAssignment.Add('groupDisplayName', $groupDisplayName)
        }
        if (-not [string]::IsNullOrEmpty($collectionId))
        {
            $hashAssignment.Add('collectionId', $collectionId)
        }
        if ($IncludeDeviceFilter)
        {
            if ($null -ne $assignment.Target.DeviceAndAppManagementAssignmentFilterType)
            {
                $hashAssignment.Add('deviceAndAppManagementAssignmentFilterType', $assignment.Target.DeviceAndAppManagementAssignmentFilterType.ToString())
            }
            if ($null -ne $assignment.Target.DeviceAndAppManagementAssignmentFilterId)
            {
                $filterId = $assignment.Target.DeviceAndAppManagementAssignmentFilterId
                $hashAssignment.Add('deviceAndAppManagementAssignmentFilterId', $filterId)
                $hashAssignment.Add('deviceAndAppManagementAssignmentFilterDisplayName', (($Script:IntuneAssignmentFilters | Where-Object -FilterScript { $_.FilterId -eq $filterId }).DisplayName))
            }
        }

        $assignmentResult += $hashAssignment
    }

    return ,$assignmentResult
}

function ConvertTo-IntunePolicyAssignment
{
    [CmdletBinding()]
    [OutputType([Hashtable[]])]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $Assignments,

        [Parameter()]
        [System.Boolean]
        $IncludeDeviceFilter = $true
    )

    if ($null -eq $Script:IntuneAssignmentFilters)
    {
        $Script:IntuneAssignmentFilters = Get-MgBetaDeviceManagementAssignmentFilter -All -ErrorAction SilentlyContinue | ForEach-Object {
            @{
                FilterId    = $_.Id
                DisplayName = $_.DisplayName
            }
        }
    }

    if ($null -eq $Assignments)
    {
        return ,@()
    }

    $assignmentResult = @()
    foreach ($assignment in $Assignments)
    {
        $target = @{
            '@odata.type' = $assignment.dataType
        }
        if ($IncludeDeviceFilter)
        {
            if ($null -ne $assignment.DeviceAndAppManagementAssignmentFilterType -and $assignment.DeviceAndAppManagementAssignmentFilterType -ne 'none')
            {
                $filter = $Script:IntuneAssignmentFilters | Where-Object -FilterScript { $_.FilterId -eq $assignment.DeviceAndAppManagementAssignmentFilterId }
                if ($null -eq $filter)
                {
                    $filter = $Script:IntuneAssignmentFilters | Where-Object -FilterScript { $_.DisplayName -eq $assignment.DeviceAndAppManagementAssignmentFilterDisplayName }
                    if ($null -eq $filter)
                    {
                        Write-Warning -Message "Assignment filter with DisplayName {$($assignment.DeviceAndAppManagementAssignmentFilterDisplayName)} not found in the directory. Please update your DSC resource extract with the correct filterId or filterDisplayName."
                    }
                }

                if ($null -ne $filter)
                {
                    $target.Add('deviceAndAppManagementAssignmentFilterType', $assignment.DeviceAndAppManagementAssignmentFilterType)
                    $target.Add('deviceAndAppManagementAssignmentFilterId', $filter.FilterId)
                }
            }
        }
        if ($assignment.dataType -like '*CollectionAssignmentTarget')
        {
            $target.Add('collectionId', $assignment.collectionId)
        }
        elseif ($assignment.dataType -like '*GroupAssignmentTarget')
        {
            $group = $null
            if (-not [System.String]::IsNullOrEmpty($assignment.groupId))
            {
                $group = Get-MgGroup -GroupId ($assignment.groupId) -ErrorAction SilentlyContinue
            }
            if ($null -eq $group -and -not [System.String]::IsNullOrEmpty($assignment.groupDisplayName))
            {
                $escapedName = $assignment.groupDisplayName -replace "'", "''"
                [array]$group = Get-MgGroup -Filter "DisplayName eq '$escapedName'" -All -ErrorAction SilentlyContinue
                if ($null -eq $group -or $group.Count -eq 0)
                {
                    Write-Warning "Skipping assignment: groupDisplayName '{$($assignment.groupDisplayName)}' not found."
                    $target = $null
                    $group = $null
                }
                elseif ($group.Count -gt 1)
                {
                    Write-Warning "Skipping assignment: groupDisplayName '{$($assignment.groupDisplayName)}' is not unique."
                    $target = $null
                    $group = $null
                }
            }
            # If group found, add its ID
            if ($null -ne $group)
            {
                $target.Add('groupId', $group.Id)
            }
            elseif ($null -eq $group -and [System.String]::IsNullOrEmpty($assignment.groupDisplayName))
            {
                Write-Warning 'Skipping assignment: missing both groupId and groupDisplayName.'
                $target = $null
            }
        }

        if ($null -ne $target)
        {
            $assignmentResult += @{ target = $target }
        }
    }

    return ,$assignmentResult
}

function ConvertFrom-IntuneMobileAppAssignment
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable[]])]
    param (
        [Parameter(Mandatory = $true)]
        [Array]
        $Assignments,

        [Parameter()]
        [System.Boolean]
        $IncludeDeviceFilter = $true
    )

    if ($null -eq $Script:IntuneAssignmentFilters)
    {
        $Script:IntuneAssignmentFilters = Get-MgBetaDeviceManagementAssignmentFilter -All -ErrorAction SilentlyContinue | ForEach-Object {
            @{
                FilterId    = $_.Id
                DisplayName = $_.DisplayName
            }
        }
    }

    $assignmentResult = @()
    foreach ($assignment in $Assignments)
    {
        $hashAssignment = @{}
        if ($null -ne $assignment.Target.'@odata.type')
        {
            $dataType = $assignment.Target.'@odata.type'
        }
        else
        {
            $dataType = $assignment.Target.'@odata.type'
        }

        if ($null -ne $assignment.Target.groupId)
        {
            $groupId = $assignment.Target.groupId
        }
        else
        {
            $groupId = $assignment.Target.groupId
        }

        $hashAssignment.Add('dataType', $dataType)
        if (-not [string]::IsNullOrEmpty($groupId))
        {
            $hashAssignment.Add('groupId', $groupId)

            $group = Get-MgGroup -GroupId ($groupId) -ErrorAction SilentlyContinue
            if ($null -ne $group)
            {
                $groupDisplayName = $group.DisplayName
            }
        }

        if ($dataType -eq '#microsoft.graph.allLicensedUsersAssignmentTarget')
        {
            $groupDisplayName = 'All users'
        }
        if ($dataType -eq '#microsoft.graph.allDevicesAssignmentTarget')
        {
            $groupDisplayName = 'All devices'
        }
        if ($null -ne $groupDisplayName)
        {
            $hashAssignment.Add('groupDisplayName', $groupDisplayName)
        }

        $hashAssignment.Add('intent', $assignment.intent.ToString())

        if ($IncludeDeviceFilter)
        {
            if ($null -ne $assignment.Target.DeviceAndAppManagementAssignmentFilterType)
            {
                $hashAssignment.Add('deviceAndAppManagementAssignmentFilterType', $assignment.Target.DeviceAndAppManagementAssignmentFilterType.ToString())
            }
            if ($null -ne $assignment.Target.DeviceAndAppManagementAssignmentFilterId)
            {
                $filterId = $assignment.Target.DeviceAndAppManagementAssignmentFilterId
                $hashAssignment.Add('deviceAndAppManagementAssignmentFilterId', $filterId)
                $hashAssignment.Add('deviceAndAppManagementAssignmentFilterDisplayName', (($Script:IntuneAssignmentFilters | Where-Object -FilterScript { $_.FilterId -eq $filterId }).DisplayName))
            }
        }

        if ($null -ne $assignment.settings -and $assignment.settings.Count -gt 0)
        {
            $settings = (Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $assignment.settings)
            $hashAssignment.Add('assignmentSettings', $settings)
        }

        $assignmentResult += $hashAssignment
    }

    return ,$assignmentResult
}

function ConvertTo-IntuneMobileAppAssignment
{
    [CmdletBinding()]
    [OutputType([Hashtable[]])]
    param (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $Assignments,

        [Parameter()]
        [System.Boolean]
        $IncludeDeviceFilter = $true
    )

    if ($null -eq $Script:IntuneAssignmentFilters)
    {
        $Script:IntuneAssignmentFilters = Get-MgBetaDeviceManagementAssignmentFilter -All -ErrorAction SilentlyContinue | ForEach-Object {
            @{
                FilterId    = $_.Id
                DisplayName = $_.DisplayName
            }
        }
    }

    if ($null -eq $Assignments)
    {
        return ,@()
    }

    $assignmentResult = @()
    foreach ($assignment in $Assignments)
    {
        $formattedAssignment = @{}
        $target = @{
            '@odata.type' = $assignment.dataType
        }

        # Handle Device Filters
        if ($IncludeDeviceFilter)
        {
            if ($null -ne $assignment.DeviceAndAppManagementAssignmentFilterType -and
                $assignment.DeviceAndAppManagementAssignmentFilterType -ne 'none')
            {
                $filter = $Script:IntuneAssignmentFilters | Where-Object {
                    $_.FilterId -eq $assignment.DeviceAndAppManagementAssignmentFilterId
                }

                if ($null -eq $filter)
                {
                    $filter = $Script:IntuneAssignmentFilters | Where-Object {
                        $_.DisplayName -eq $assignment.DeviceAndAppManagementAssignmentFilterDisplayName
                    }
                }

                if ($null -ne $filter)
                {
                    $target.Add('deviceAndAppManagementAssignmentFilterType', $assignment.DeviceAndAppManagementAssignmentFilterType)
                    $target.Add('deviceAndAppManagementAssignmentFilterId', $filter.FilterId)
                }
                else
                {
                    Write-Warning "Assignment filter with DisplayName {$($assignment.DeviceAndAppManagementAssignmentFilterDisplayName)} not found."
                }
            }
        }

        # Add intent (required for app assignments)
        $formattedAssignment.Add('intent', $assignment.intent)

        if ($assignment.dataType -like '*groupAssignmentTarget')
        {
            $group = $null
            if (-not [System.String]::IsNullOrEmpty($assignment.groupId))
            {
                $group = Get-MgGroup -GroupId $assignment.groupId -ErrorAction SilentlyContinue
            }
            # If groupId lookup failed, try by display name
            if ($null -eq $group -and -not [System.String]::IsNullOrEmpty($assignment.groupDisplayName))
            {
                $escapedName = $assignment.groupDisplayName -replace "'", "''"
                [array]$group = Get-MgGroup -Filter "DisplayName eq '$escapedName'" -All -ErrorAction SilentlyContinue
                if ($null -eq $group -or $group.Count -eq 0)
                {
                    Write-Warning "Skipping assignment: groupDisplayName '{$($assignment.groupDisplayName)}' not found."
                    $target = $null
                    $group = $null
                }
                elseif ($group.Count -gt 1)
                {
                    Write-Warning "Skipping assignment: groupDisplayName '{$($assignment.groupDisplayName)}' is not unique."
                    $target = $null
                    $group = $null
                }
            }
            # If group found, add its ID
            if ($null -ne $group)
            {
                $target.Add('groupId', $group.Id)
            }
            elseif ($null -eq $group -and [System.String]::IsNullOrEmpty($assignment.groupDisplayName))
            {
                Write-Warning 'Skipping assignment: missing both groupId and groupDisplayName.'
                $target = $null
            }
        }
        # Add target if valid
        if ($null -ne $target)
        {
            $formattedAssignment.Add('target', $target)

            # Add assignment settings if present
            if ($null -ne $assignment.assignmentSettings)
            {
                $settings = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $assignment.assignmentSettings
                $formattedAssignment.Add('settings', $settings)
                $formattedAssignment.settings.Add('@odata.type', $formattedAssignment.settings.odataType)
                $formattedAssignment.settings.Remove('odataType') | Out-Null
            }
            $assignmentResult += $formattedAssignment
        }
    }
    return ,$assignmentResult
}

function Update-DeviceConfigurationPolicyAssignment
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $DeviceConfigurationPolicyId,

        [Parameter()]
        [Array]
        $Targets,

        [Parameter()]
        [System.String]
        $Repository = 'deviceManagement/configurationPolicies',

        [Parameter()]
        [ValidateSet('v1.0', 'beta')]
        [System.String]
        $APIVersion = 'beta',

        [Parameter()]
        [System.String]
        $RootIdentifier = 'assignments'
    )

    try
    {
        $deviceManagementPolicyAssignments = @()
        $Uri = "/$APIVersion/$Repository/$DeviceConfigurationPolicyId/assign"

        foreach ($target in $targets)
        {
            $targetAssignment = @{}
            $formattedTarget = @{'@odata.type' = $target.dataType }
            if ($null -ne $target.runRemediationScript)
            {
                $targetAssignment.Add('runRemediationScript', $target.runRemediationScript)
            }
            if ($null -ne $target.runSchedule)
            {
                $targetAssignment.Add('runSchedule', $target.runSchedule)
            }
            if ($target.target -is [hashtable])
            {
                $target = $target.target
            }
            if (-not $formattedTarget.'@odata.type' -and $target.'@odata.type')
            {
                $formattedTarget.'@odata.type' = $target.'@odata.type'
            }
            if ($target.groupId)
            {
                $group = Get-MgGroup -GroupId ($target.groupId) -ErrorAction SilentlyContinue
                if ($null -eq $group)
                {
                    if ($target.groupDisplayName)
                    {
                        [array]$group = Get-MgGroup -Filter "DisplayName eq '$($target.groupDisplayName -replace "'", "''")'" -All -ErrorAction SilentlyContinue
                        if ($null -eq $group -or $group.Count -eq 0)
                        {
                            $message = "Skipping assignment for the group with DisplayName {$($target.groupDisplayName)} as it could not be found in the directory.`r`n"
                            $message += 'Please update your DSC resource extract with the correct groupId or groupDisplayName.'
                            Write-Warning -Message $message
                            continue
                        }
                        if ($group -and $group.Count -gt 1)
                        {
                            $message = "Skipping assignment for the group with DisplayName {$($target.groupDisplayName)} as it is not unique in the directory.`r`n"
                            $message += 'Please update your DSC resource extract with the correct groupId or a unique group DisplayName.'
                            Write-Warning -Message $message
                            continue
                        }
                    }
                    else
                    {
                        $message = "Skipping assignment for the group with Id {$($target.groupId)} as it could not be found in the directory.`r`n"
                        $message += 'Please update your DSC resource extract with the correct groupId or a unique group DisplayName.'
                        Write-Warning -Message $message
                        continue
                    }
                }
                #Skipping assignment if group not found from either groupId or groupDisplayName
                if ($null -ne $group)
                {
                    $formattedTarget.Add('groupId', $group.Id)
                }
            }
            if ($target.collectionId)
            {
                $formattedTarget.Add('collectionId', $target.collectionId)
            }
            if ($target.deviceAndAppManagementAssignmentFilterType)
            {
                $formattedTarget.Add('deviceAndAppManagementAssignmentFilterType', $target.deviceAndAppManagementAssignmentFilterType)
            }
            if ($target.deviceAndAppManagementAssignmentFilterId)
            {
                $formattedTarget.Add('deviceAndAppManagementAssignmentFilterId', $target.deviceAndAppManagementAssignmentFilterId)
            }
            $targetAssignment.Add('target', $formattedTarget)
            $deviceManagementPolicyAssignments += $targetAssignment
        }

        $body = @{$RootIdentifier = $deviceManagementPolicyAssignments } | ConvertTo-Json -Depth 20
        Write-Verbose -Message $body

        Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $body -ErrorAction Stop
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error updating data:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source) `
            -TenantId $TenantId `
            -Credential $Credential

        return $null
    }
}

function Update-DeviceAppManagementPolicyAssignment
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AppManagementPolicyId,

        [Parameter()]
        [Array]
        $Assignments,

        [Parameter()]
        [System.String]
        $Repository = 'deviceAppManagement/mobileApps',

        [Parameter()]
        [ValidateSet('v1.0', 'beta')]
        [System.String]
        $APIVersion = 'beta',

        [Parameter()]
        [System.String]
        $RootIdentifier = 'mobileAppAssignments'
    )

    try
    {
        $appManagementPolicyAssignments = @()
        $Uri = "/$APIVersion/$Repository/$AppManagementPolicyId/assign"

        foreach ($assignment in $Assignments)
        {
            $formattedAssignment = @{
                '@odata.type' = '#microsoft.graph.mobileAppAssignment'
                intent        = $assignment.intent
            }
            if ($assignment.settings)
            {
                $formattedAssignment.Add('settings', $assignment.settings)
            }

            if ($assignment.target -is [hashtable])
            {
                $target = $assignment.target
            }

            $formattedTarget = @{'@odata.type' = $target.dataType }
            if (-not $formattedTarget.'@odata.type' -and $target.'@odata.type')
            {
                $formattedTarget.'@odata.type' = $target.'@odata.type'
            }
            if ($target.groupId)
            {
                $group = Get-MgGroup -GroupId ($target.groupId) -ErrorAction SilentlyContinue
                if ($null -eq $group)
                {
                    if ($target.groupDisplayName)
                    {
                        [array]$group = Get-MgGroup -Filter "DisplayName eq '$($target.groupDisplayName -replace "'", "''")'" -All -ErrorAction SilentlyContinue
                        if ($null -eq $group -or $group.Count -eq 0)
                        {
                            $message = "Skipping assignment for the group with DisplayName {$($target.groupDisplayName)} as it could not be found in the directory.`r`n"
                            $message += 'Please update your DSC resource extract with the correct groupId or groupDisplayName.'
                            Write-Warning -Message $message
                            continue
                        }
                        if ($group -and $group.Count -gt 1)
                        {
                            $message = "Skipping assignment for the group with DisplayName {$($target.groupDisplayName)} as it is not unique in the directory.`r`n"
                            $message += 'Please update your DSC resource extract with the correct groupId or a unique group DisplayName.'
                            Write-Warning -Message $message
                            continue
                        }
                    }
                    else
                    {
                        $message = "Skipping assignment for the group with Id {$($target.groupId)} as it could not be found in the directory.`r`n"
                        $message += 'Please update your DSC resource extract with the correct groupId or a unique group DisplayName.'
                        Write-Warning -Message $message
                        continue
                    }
                }
                #Skipping assignment if group not found from either groupId or groupDisplayName
                if ($null -ne $group)
                {
                    $formattedTarget.Add('groupId', $group.Id)
                }
            }
            if ($target.deviceAndAppManagementAssignmentFilterType)
            {
                $formattedTarget.Add('deviceAndAppManagementAssignmentFilterType', $target.deviceAndAppManagementAssignmentFilterType)
            }
            if ($target.deviceAndAppManagementAssignmentFilterId)
            {
                $formattedTarget.Add('deviceAndAppManagementAssignmentFilterId', $target.deviceAndAppManagementAssignmentFilterId)
            }
            $formattedAssignment.Add('target', $formattedTarget)
            $appManagementPolicyAssignments += $formattedAssignment
        }

        $body = @{ $RootIdentifier = $appManagementPolicyAssignments } | ConvertTo-Json -Depth 20
        Write-Verbose -Message $body

        Invoke-MgGraphRequest -Method POST -Uri $Uri -Body $body -ErrorAction Stop
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error updating data:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source) `
            -TenantId $TenantId `
            -Credential $Credential

        return $null
    }
}

function Update-DeviceAppManagementAppCategory
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        $App,

        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [Array]
        $Categories,

        [Parameter()]
        [switch]
        $Compare
    )

    if ($Compare)
    {
        [array]$referenceObject = if ($null -ne $App.Categories.DisplayName)
        {
            $App.Categories.DisplayName
        }
        else
        {
            , @()
        }
        [array]$differenceObject = if ($null -ne $Categories.DisplayName)
        {
            $Categories.DisplayName
        }
        else
        {
            , @()
        }
        $delta = Compare-Object -ReferenceObject $referenceObject -DifferenceObject $differenceObject -PassThru
        foreach ($diff in $delta)
        {
            if ($diff.SideIndicator -eq '=>')
            {
                $category = $Categories | Where-Object { $_.DisplayName -eq $diff }
                if ($category.Id)
                {
                    $currentCategory = Get-MgBetaDeviceAppManagementMobileAppCategory -MobileAppCategoryId $category.Id -ErrorAction SilentlyContinue
                }

                if ($null -eq $currentCategory)
                {
                    $currentCategory = Get-MgBetaDeviceAppManagementMobileAppCategory -Filter "DisplayName eq '$($category.DisplayName -replace "'", "''")'"
                }

                if ($null -eq $currentCategory -or $currentCategory.Count -eq 0)
                {
                    throw "Mobile App Category with DisplayName $($category.DisplayName) not found."
                }

                Invoke-MgGraphRequest -Uri "/beta/deviceAppManagement/mobileApps/$($App.Id)/categories/`$ref" -Method 'POST' -Body @{
                    '@odata.id' = "$((Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl)beta/deviceAppManagement/mobileAppCategories/$($currentCategory.Id)"
                }
            }
            else
            {
                $category = $App.Categories | Where-Object { $_.DisplayName -eq $diff }
                Invoke-MgGraphRequest -Uri "/beta/deviceAppManagement/mobileApps/$($App.Id)/categories/$($category.Id)/`$ref" -Method 'DELETE'
            }
        }
    }
    else
    {
        foreach ($category in $Categories)
        {
            if ($category.Id)
            {
                $currentCategory = Get-MgBetaDeviceAppManagementMobileAppCategory -MobileAppCategoryId $category.Id -ErrorAction SilentlyContinue
            }

            if ($null -eq $currentCategory)
            {
                $currentCategory = Get-MgBetaDeviceAppManagementMobileAppCategory -Filter "DisplayName eq '$($category.DisplayName -replace "'", "''")'"
            }

            if ($null -eq $currentCategory -or $currentCategory.Count -eq 0)
            {
                throw "Mobile App Category with DisplayName $($category.DisplayName) not found."
            }

            Invoke-MgGraphRequest -Uri "$((Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl)beta/deviceAppManagement/mobileApps/$($App.Id)/categories/`$ref" -Method 'POST' -Body @{
                '@odata.id' = "$((Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl)beta/deviceAppManagement/mobileAppCategories/$($currentCategory.Id)"
            }
        }
    }
}

function Get-M365DSCIntuneDeviceConfigurationSettings
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = 'true')]
        [System.Collections.Hashtable]
        $Properties,

        [Parameter()]
        [System.String]
        $TemplateId
    )

    $templateCategoryId = (Get-MgBetaDeviceManagementTemplateCategory -DeviceManagementTemplateId $TemplateId).Id
    $templateSettings = Get-MgBetaDeviceManagementTemplateCategoryRecommendedSetting `
        -DeviceManagementTemplateId $TemplateId `
        -DeviceManagementTemplateSettingCategoryId $templateCategoryId

    $results = @()
    foreach ($setting in $templateSettings)
    {
        $result = @{}
        $settingType = $setting.'@odata.type'
        $settingValue = $null
        $currentValueKey = $Properties.keys | Where-Object -FilterScript { $setting.DefinitionId -like "*$_" }
        if ($null -ne $currentValueKey)
        {
            $settingValue = $Properties.$currentValueKey
        }

        $requiresValueJson = $false
        switch ($settingType)
        {
            {
                ( $_ -eq '#microsoft.graph.deviceManagementStringSettingInstance' ) -or
                ( $_ -eq '#microsoft.graph.deviceManagementBooleanSettingInstance' )
            }
            {
                if ([String]::IsNullOrEmpty($settingValue))
                {
                    $settingValue = $setting.ValueJson | ConvertFrom-Json
                }
            }
            '#microsoft.graph.deviceManagementCollectionSettingInstance'
            {
                $requiresValueJson = $true
                if ($null -eq $settingValue)
                {
                    $settingValue = ConvertTo-Json -InputObject @()
                }
                else
                {
                    $settingValue = ConvertTo-Json -InputObject ([Array]$settingValue)
                }
            }
            default
            {
                if ($null -eq $settingValue)
                {
                    $settingValue = $setting.ValueJson | ConvertFrom-Json
                }
            }
        }

        $result.Add('@odata.type', $settingType)
        $result.Add('Id', $setting.Id)
        $result.Add('definitionId', $setting.DefinitionId)

        if ($requiresValueJson)
        {
            $result.Add('valueJson', $settingValue)
        }
        else
        {
            $result.Add('value', $settingValue)
        }

        $results += $result
    }

    return $results
}

function Get-OmaSettingPlainTextValue
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param(
        [Parameter(Mandatory = $true)]
        [System.String]
        $SecretReferenceValueId,

        [Parameter()]
        [ValidateSet('v1.0', 'beta')]
        [System.String]
        $APIVersion = 'beta'
    )

    try
    {
        <#
            e.g. PolicyId for SecretReferenceValueId '35ea58ec-2a79-471d-8eea-7e28e6cd2722_bdf6c690-05fb-4d02-835d-5a7406c35d58_abe32712-2255-445f-a35e-0c6f143d82ca'
            is 'bdf6c690-05fb-4d02-835d-5a7406c35d58'
        #>

        $SplitSecretReferenceValueId = $SecretReferenceValueId.Split('_')
        if ($SplitSecretReferenceValueId.Count -eq 3)
        {
            $PolicyId = $SplitSecretReferenceValueId[1]
        }
        else
        {
            return $null
        }
    }
    catch
    {
        return $null
    }

    $Repository = 'deviceManagement/deviceConfigurations'
    $Uri = "/{0}/{1}/{2}/getOmaSettingPlainTextValue(secretReferenceValueId='{3}')" -f $APIVersion, $Repository, $PolicyId, $SecretReferenceValueId

    try
    {
        $Result = Invoke-MgGraphRequest -Method GET -Uri $Uri -ErrorAction Stop
    }
    catch
    {
        $Message = 'Error decrypting OmaSetting with SecretReferenceValueId {0}:' -f $SecretReferenceValueId
        New-M365DSCLogEntry -Message $Message `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source) `
            -TenantId $TenantId `
            -Credential $Credential

        return $null
    }

    if (![String]::IsNullOrEmpty($Result.Value))
    {
        return $Result.Value
    }
    else
    {
        return $null
    }
}

function Get-IntuneSettingCatalogPolicySetting
{
    [CmdletBinding()]
    [OutputType([System.Object[]])]
    param (
        [Parameter(Mandatory = $true)]
        [System.Collections.Hashtable]
        $DSCParams,

        [Parameter(
            Mandatory = 'true',
            ParameterSetName = 'Start'
        )]
        [System.String]
        $TemplateId,

        [Parameter(
            Mandatory = 'true',
            ParameterSetName = 'DeviceAndUserSettings'
        )]
        [System.Array]
        $SettingTemplates,

        [Parameter(ParameterSetName = 'Start')]
        [switch]
        $ContainsDeviceAndUserSettings
    )

    $DSCParams.Remove('Identity') | Out-Null
    $DSCParams.Remove('DisplayName') | Out-Null
    $DSCParams.Remove('Description') | Out-Null

    if ($PSCmdlet.ParameterSetName -eq 'Start')
    {
        # Prepare setting definitions mapping
        $SettingTemplates = Get-MgBetaDeviceManagementConfigurationPolicyTemplateSettingTemplate `
            -DeviceManagementConfigurationPolicyTemplateId $TemplateId `
            -ExpandProperty 'SettingDefinitions' `
            -All
    }

    Initialize-M365DSCDllLoader -ErrorAction Stop

    return ,[Microsoft365DSC.Intune.SettingCatalogPolicySettingBuilder]::Build(
        [System.Collections.Generic.List[object]]@($SettingTemplates),
        $DSCParams,
        $ContainsDeviceAndUserSettings.IsPresent)
}

function Export-IntuneSettingCatalogPolicySettings
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param(
        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Start'
        )]
        $Settings,

        [Parameter(
            Mandatory = $true
        )]
        [System.Collections.Hashtable]$ReturnHashtable,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Setting'
        )]
        $SettingInstance,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Setting'
        )]
        $SettingDefinitions,

        [Parameter(
            Mandatory = $true,
            ParameterSetName = 'Setting'
        )]
        [Parameter(
            ParameterSetName = 'Start'
        )]
        [System.Array]
        $AllSettingDefinitions,

        [Parameter(
            ParameterSetName = 'Setting'
        )]
        [switch]$IsRoot,

        [Parameter(
            ParameterSetName = 'Start'
        )]
        [switch]$ContainsDeviceAndUserSettings
    )

    Initialize-M365DSCDllLoader -ErrorAction Stop

    return [Microsoft365DSC.Intune.SettingCatalogPolicyExporter]::Export($Settings, $ReturnHashtable, $AllSettingDefinitions, $ContainsDeviceAndUserSettings)
}

function Update-IntuneDeviceConfigurationPolicy
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = 'true')]
        [System.String]
        $DeviceConfigurationPolicyId,

        [Parameter()]
        [System.String]
        $Name,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter()]
        [System.String]
        $Platforms,

        [Parameter()]
        [System.String]
        $Technologies,

        [Parameter()]
        [System.String]
        $TemplateReferenceId,

        [Parameter()]
        [AllowNull()]
        [System.String]
        $CreationSource,

        [Parameter()]
        [Array]
        $Settings,

        [Parameter()]
        [System.String[]]
        $RoleScopeTagIds
    )

    try
    {
        $Uri = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "beta/deviceManagement/configurationPolicies/$DeviceConfigurationPolicyId"

        $policy = @{
            'name'            = $Name
            'description'     = $Description
            'platforms'       = $Platforms
            'technologies'    = $Technologies
            'settings'        = $Settings
            'roleScopeTagIds' = $RoleScopeTagIds
        }

        if ($PSBoundParameters.ContainsKey('TemplateReferenceId'))
        {
            $policy.Add('templateReference', @{ 'templateId' = $TemplateReferenceId })
        }

        if ($PSBoundParameters.ContainsKey('CreationSource') -and -not [System.String]::IsNullOrEmpty($CreationSource))
        {
            $policy.Add('creationSource', $CreationSource)
        }

        $body = $policy | ConvertTo-Json -Depth 20
        Write-Verbose -Message "Updating policy with:`r`n$body"
        Invoke-MgGraphRequest -Method PUT -Uri $Uri -Body $body -ErrorAction Stop
    }
    catch
    {
        New-M365DSCLogEntry -Message 'Error updating data:' `
            -Exception $_ `
            -Source $($MyInvocation.MyCommand.Source) `
            -TenantId $TenantId `
            -Credential $Credential

        throw
    }
}

function Get-ComplexFunctionsFromFilterQuery
{
    [CmdletBinding()]
    [OutputType([System.Array])]
    param (
        [string]$FilterQuery
    )

    $complexFunctionsRegex = "startswith\((.*?),\s*'(.*?)'\)|endswith\((.*?),\s*'(.*?)'\)|contains\((.*?),\s*'(.*?)'\)"
    [array]$complexFunctions = [regex]::Matches($FilterQuery, $complexFunctionsRegex) | ForEach-Object {
        $_.Value
    }

    return $complexFunctions
}

function Remove-ComplexFunctionsFromFilterQuery
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param (
        [string]$FilterQuery
    )

    $complexFunctionsRegex = "startswith\((.*?),\s*'(.*?)'\)|endswith\((.*?),\s*'(.*?)'\)|contains\((.*?),\s*'(.*?)'\)"
    $basicFilterQuery = [regex]::Replace($FilterQuery, $complexFunctionsRegex, '').Trim()
    $basicFilterQuery = $basicFilterQuery -replace '^and\s', '' -replace '\sand$', '' -replace '\sand\s+', ' and ' -replace '\sor\s+', ' or '

    return $basicFilterQuery
}

function Find-GraphDataUsingComplexFunctions
{
    [CmdletBinding()]
    [OutputType([System.Array])]
    param (
        [array]$Policies,
        [array]$ComplexFunctions
    )

    foreach ($function in $ComplexFunctions)
    {
        if ($function -match "startswith\((.*?),\s*'(.*?)'")
        {
            $property = $matches[1]
            $value = $matches[2]
            $Policies = $Policies | Where-Object { $_.$property -like "$value*" }
        }
        elseif ($function -match "endswith\((.*?),\s*'(.*?)'")
        {
            $property = $matches[1]
            $value = $matches[2]
            $Policies = $Policies | Where-Object { $_.$property -like "*$value" }
        }
        elseif ($function -match "contains\((.*?),\s*'(.*?)'")
        {
            $property = $matches[1]
            $value = $matches[2]
            $Policies = $Policies | Where-Object { $_.$property -like "*$value*" }
        }
    }

    return $Policies
}

function Invoke-M365DSCIntuneMobileAppInitialUpload
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AppId,

        [Parameter(Mandatory = $true)]
        [System.String]
        $OdataType,

        [Parameter(Mandatory = $true)]
        [System.String]
        $FileExtension
    )

    $OdataType = $OdataType.Replace('#', '')
    $contentVersionsUri = "beta/deviceAppManagement/mobileApps/$($AppId)/$OdataType/contentVersions"
    $contentVersion = Invoke-MgGraphRequest -Method POST -Uri $contentVersionsUri -Body @{}

    $manifest = $null
    $size = 1
    $sizeEncrypted = 64
    $base64File = '+drh1SKfuLjdp37gfv8EuWqOTt06m0TirqJJ0xQvrd5sm6NkiYBY8vBkFM+9ZwHRskO83NEfsLPtTzLB9FFsKA=='
    $encryptionKey = 'yqjlzT5KYpwU0wkr5eJGGukMB0Ar8iGqYX3B0lJJnKk='
    $fileDigest = 'ypeBEsobvcr6wjGzmiPcTaeG7/gUfE5yuYB3ha/uSLs='
    $initializationVector = 'bJujZImAWPLwZBTPvWcB0Q=='
    $mac = '+drh1SKfuLjdp37gfv8EuWqOTt06m0TirqJJ0xQvrd4='
    $macKey = 'mGfhTn/0AB3fftWzENQcoU34xghAfvVq23PoiBD81tM='
    switch ($OdataType)
    {
        'microsoft.graph.androidLobApp'
        {
            $size = 13168
            $sizeEncrypted = 13232
            $manifest = $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('<?xml version="1.0" encoding="utf-8"?><AndroidManifestProperties xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><Package>a.a</Package><PackageVersionCode>1</PackageVersionCode><PackageVersionName>1.0</PackageVersionName><ApplicationName>Sample.apk</ApplicationName><MinSdkVersion>24</MinSdkVersion><AWTVersion></AWTVersion></AndroidManifestProperties>')))
            $base64File = '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'
            $encryptionKey = '3uV+NpJnmT5vaufsl9xalZJqwpFmACdOT1OQH9LBJNQ='
            $fileDigest = 'EVykpzyjw9oNRwY4NR/JJNC7rjgSauvcOcuQ/hjHQu0='
            $initializationVector = 'd/kzcmL9UjjMlPxQdPIA6g=='
            $mac = 'l+rn68BUBB1/+Od1k1JnMcqcbzrbf7vhcy+zofWz6YY='
            $macKey = 'kb7oEqtP0SJGiy6jrkj0rUggcCmkcb3AkFkXTYovnLc='
        }
        'microsoft.graph.windowsMobileMSI'
        {
            $manifest = $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('<MobileMsiData MsiExecutionContext="User" MsiRequiresReboot="false" MsiUpgradeCode="{00000000-0000-0000-0000-000000000000}" MsiIsMachineInstall="false" MsiIsUserInstall="true" MsiRequiresLogon="true" MsiIncludesServices="false" MsiContainsSystemRegistryKeys="false" MsiContainsSystemFolders="false"></MobileMsiData>')))
        }
        'microsoft.graph.windowsUniversalAppx'
        {
            $manifest = $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("Sample.$($FileExtension)".ToUpper())))
        }
    }

    $fileUri = "beta/deviceAppManagement/mobileApps/$($AppId)/$OdataType/contentVersions/$($contentVersion.id)/files"
    if ($OdataType -eq 'microsoft.graph.windowsUniversalAppx')
    {
        # Manifest is required for Windows Universal Appx
        $manifest = $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("Sample.$($FileExtension)".ToUpper())))
    }
    elseif ($OdataType -eq 'microsoft.graph.windowsMobileMSI')
    {
        # Manifest is required for Windows Mobile MSI
        $manifest = $([System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes('<MobileMsiData MsiExecutionContext="User" MsiRequiresReboot="false" MsiUpgradeCode="{00000000-0000-0000-0000-000000000000}" MsiIsMachineInstall="false" MsiIsUserInstall="true" MsiRequiresLogon="true" MsiIncludesServices="false" MsiContainsSystemRegistryKeys="false" MsiContainsSystemFolders="false"></MobileMsiData>')))
    }
    $file = Invoke-MgGraphRequest -Method POST `
        -Uri $fileUri `
        -Body @{
            '@odata.type' = '#microsoft.graph.mobileAppContentFile'
            name          = "Sample.$($FileExtension)"
            size          = $size
            sizeEncrypted = $sizeEncrypted
            isDependency  = $false
            manifest      = $manifest
        }

    $file = Wait-ForFileProcessing -AppId $AppId -OdataType $OdataType -FileId $file.id -ContentVersionId $contentVersion.id -UploadStatePrefix 'AzureStorageUriRequest'

    # Upload the encrypted Sample file to Azure Storage
    $success = $false
    $breakCounter = 0
    do
    {
        try
        {
            Write-Verbose "Uploading file to Azure Storage: $($file.azureStorageUri)"
            $sasUri = $file.azureStorageUri
            $uri = "$($sasUri)&comp=block&blockid=0001"
            $iso = [System.Text.Encoding]::GetEncoding('iso-8859-1')
            $body = [System.Convert]::FromBase64String($base64File)
            $encodedBody = $iso.GetString($body)
            Invoke-WebRequest -Uri $uri -Method PUT -Body $encodedBody -Headers @{
                'content-type' = 'text/plain; charset=iso-8859-1'
                'x-ms-blob-type' = 'BlockBlob'
            } -ErrorAction Stop -UseBasicParsing | Out-Null
            Write-Verbose 'File uploaded successfully to Azure Storage.'
            $success = $true
        }
        catch
        {
            Write-Warning -Message "Failed to upload file to Azure Storage: $($_.Exception.Message)"
            Start-Sleep -Seconds 2
        }
    } while ($success -eq $false -and $breakCounter -lt 5)

    # Finalize the upload to Azure Storage
    $uri = "$($sasUri)&comp=blocklist"
    $xml = '<?xml version="1.0" encoding="utf-8"?><BlockList><Latest>0001</Latest></BlockList>'
    Invoke-RestMethod -Uri $uri -Method PUT -Body $xml

    # Commit the file and update the app
    $jsonCommit = @{
        fileEncryptionInfo = @{
            fileDigestAlgorithm  = 'SHA256'
            encryptionKey        = $encryptionKey
            initializationVector = $initializationVector
            fileDigest           = $fileDigest
            mac                  = $mac
            profileIdentifier    = 'ProfileVersion1'
            macKey               = $macKey
        }
    }
    $commitUri = "beta/deviceAppManagement/mobileApps/$AppId/$OdataType/contentVersions/$($contentVersion.id)/files/$($file.id)/commit"
    Invoke-MgGraphRequest -Method POST -Uri $commitUri -Body $($jsonCommit | ConvertTo-Json -Depth 10)

    Wait-ForFileProcessing -AppId $AppId -OdataType $OdataType -FileId $file.id -ContentVersionId $contentVersion.id -UploadStatePrefix 'CommitFile'

    # Update the app with the committed content version
    Invoke-MgGraphRequest -Method PATCH -Uri "beta/deviceAppManagement/mobileApps/$AppId" -Body @{
        '@odata.type'           = "#$OdataType"
        committedContentVersion = '1'
    }
}

function Wait-ForFileProcessing
{
    [CmdletBinding()]
    [OutputType([System.Object])]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AppId,

        [Parameter(Mandatory = $true)]
        [System.String]
        $OdataType,

        [Parameter(Mandatory = $true)]
        [System.String]
        $FileId,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ContentVersionId,

        [Parameter(Mandatory = $true)]
        [System.String]
        $UploadStatePrefix
    )

    $fileUri = "beta/deviceAppManagement/mobileApps/$($AppId)/$OdataType/contentVersions/$ContentVersionId/files/$($FileId)"

    Write-Verbose "Waiting for file processing to complete for AppId: $AppId, OdataType: $OdataType, FileId: $FileId, ContentVersionId: $ContentVersionId"
    $file = Invoke-MgGraphRequest -Method GET -Uri $fileUri

    while ($file.uploadState -ne "$($UploadStatePrefix)Success")
    {
        if ($file.uploadState -like '*Failed')
        {
            throw "File upload failed with state: $($file.uploadState). Please check the file and try again."
        }

        Start-Sleep -Seconds 1
        Write-Verbose "Current upload state: $($file.uploadState). Waiting for processing to complete..."
        $file = Invoke-MgGraphRequest -Method GET -Uri $fileUri
    }

    $file
}

Export-ModuleMember -Function @(
    'Compare-M365DSCIntunePolicyAssignment',
    'ConvertFrom-IntuneMobileAppAssignment',
    'ConvertFrom-IntunePolicyAssignment',
    'ConvertTo-IntuneMobileAppAssignment',
    'ConvertTo-IntunePolicyAssignment',
    'Export-IntuneSettingCatalogPolicySettings',
    'Find-GraphDataUsingComplexFunctions',
    'Get-ComplexFunctionsFromFilterQuery',
    'Get-IntuneSettingCatalogPolicySetting',
    'Get-M365DSCIntuneDeviceConfigurationSettings',
    'Get-OmaSettingPlainTextValue',
    'Invoke-M365DSCIntuneMobileAppInitialUpload',
    'Remove-ComplexFunctionsFromFilterQuery',
    'Update-DeviceAppManagementAppCategory',
    'Update-DeviceAppManagementPolicyAssignment',
    'Update-DeviceConfigurationPolicyAssignment',
    'Update-IntuneDeviceConfigurationPolicy',
    'Wait-ForFileProcessing'
)