Public/Import-IntuneCompliancePolicy.ps1

function Import-IntuneCompliancePolicy {
    <#
    .SYNOPSIS
        Imports device compliance policies from templates
    .DESCRIPTION
        Reads JSON templates from Templates/Compliance and creates compliance policies via Graph.
    .PARAMETER TemplatePath
        Path to the compliance template directory (defaults to Templates/Compliance)
    .EXAMPLE
        Import-IntuneCompliancePolicy
    #>

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

        [Parameter()]
        [switch]$RemoveExisting
    )

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

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

    $templateFiles = Get-HydrationTemplates -Path $TemplatePath -Recurse -ResourceType "compliance template"

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

    # Prefetch existing compliance policies (paged) from both classic and linux endpoints
    # Store full policy objects so we can check descriptions later
    $existingPolicies = @{}
    $endpointsToList = @(
        "beta/deviceManagement/deviceCompliancePolicies",
        "beta/deviceManagement/compliancePolicies"
    )
    foreach ($listUriStart in $endpointsToList) {
        $listUri = $listUriStart
        try {
            do {
                $existingResponse = Invoke-MgGraphRequest -Method GET -Uri $listUri -ErrorAction Stop
                foreach ($policy in $existingResponse.value) {
                    $policyName = if ($policy.displayName) { $policy.displayName } elseif ($policy.name) { $policy.name } else { $null }
                    if ($policyName -and -not $existingPolicies.ContainsKey($policyName)) {
                        $existingPolicies[$policyName] = @{
                            Id = $policy.id
                            Description = $policy.description
                            Endpoint = $listUriStart
                        }
                    }
                }
                $listUri = $existingResponse.'@odata.nextLink'
            } while ($listUri)
        }
        catch {
            continue
        }
    }

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

    $results = @()

    # Remove existing policies if requested
    # SAFETY: Only delete policies that have "Imported by Intune-Hydration-Kit" in description
    if ($RemoveExisting) {
        foreach ($policyName in $existingPolicies.Keys) {
            $policyInfo = $existingPolicies[$policyName]

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

            # Determine endpoint based on where we found the policy
            $deleteEndpoint = "$($policyInfo.Endpoint)/$($policyInfo.Id)"

            if ($PSCmdlet.ShouldProcess($policyName, "Delete compliance policy")) {
                try {
                    Invoke-MgGraphRequest -Method DELETE -Uri $deleteEndpoint -ErrorAction Stop
                    Write-HydrationLog -Message " Deleted: $policyName" -Level Info
                    $results += New-HydrationResult -Name $policyName -Type 'CompliancePolicy' -Action 'Deleted' -Status 'Success'
                }
                catch {
                    $errMessage = Get-GraphErrorMessage -ErrorRecord $_
                    Write-HydrationLog -Message " Failed: $policyName - $errMessage" -Level Warning
                    $results += New-HydrationResult -Name $policyName -Type 'CompliancePolicy' -Action 'Failed' -Status "Delete failed: $errMessage"
                }
            }
            else {
                Write-HydrationLog -Message " WouldDelete: $policyName" -Level Info
                $results += New-HydrationResult -Name $policyName -Type 'CompliancePolicy' -Action 'WouldDelete' -Status 'DryRun'
            }
        }

        return $results
    }

    foreach ($templateFile in $templateFiles) {
        try {
            $template = Get-Content -Path $templateFile.FullName -Raw -Encoding utf8 | ConvertFrom-Json
            $displayName = $template.displayName
            if (-not $displayName) {
                Write-Warning "Template missing displayName: $($templateFile.FullName)"
                $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Failed' -Status 'Missing displayName'
                continue
            }

            # Choose endpoint: Linux uses compliancePolicies, others use deviceCompliancePolicies
            $isLinuxCompliance = $template.platforms -eq 'linux' -and $template.technologies -eq 'linuxMdm'
            $endpoint = if ($isLinuxCompliance) {
                "beta/deviceManagement/compliancePolicies"
            } else {
                "beta/deviceManagement/deviceCompliancePolicies"
            }

            # For Linux, also consider 'name' when matching
            $lookupNames = @($displayName)
            if ($isLinuxCompliance -and $template.name) {
                $lookupNames += $template.name
            }

            $alreadyExists = $false
            foreach ($ln in $lookupNames) {
                if ($existingByName.ContainsKey($ln)) {
                    $alreadyExists = $true
                    break
                }
            }

            if ($alreadyExists) {
                Write-HydrationLog -Message " Skipped: $displayName" -Level Info
                $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Skipped' -Status 'Already exists'
                continue
            }

            $importBody = Copy-DeepObject -InputObject $template
            Remove-ReadOnlyGraphProperties -InputObject $importBody

            # Add hydration kit tag to description
            $existingDesc = if ($importBody.description) { $importBody.description } else { "" }
            $importBody.description = if ($existingDesc) { "$existingDesc - Imported by Intune-Hydration-Kit" } else { "Imported by Intune-Hydration-Kit" }

            # Linux endpoint expects 'name' instead of displayName; ensure it's present
            if ($isLinuxCompliance) {
                if (-not $importBody.name) {
                    $importBody | Add-Member -MemberType NoteProperty -Name name -Value $displayName -Force
                }
                # Some exports include displayName; keep it but ensure name is set
            }

            # Handle custom compliance policies with deviceCompliancePolicyScript
            # Uses the same approach as create-custom-compliance-policy.ps1
            if ($importBody.deviceCompliancePolicyScript) {
                $scriptDefinition = $template.deviceCompliancePolicyScriptDefinition
                $scriptDisplayName = if ($scriptDefinition.displayName) { $scriptDefinition.displayName } else { "$displayName Script" }

                # Step 1: Check if compliance script already exists or create it
                $scriptId = $null
                try {
                    $existingScripts = Invoke-MgGraphRequest -Method GET -Uri "beta/deviceManagement/deviceComplianceScripts" -ErrorAction Stop
                    $existingScript = $existingScripts.value | Where-Object { $_.displayName -eq $scriptDisplayName }

                    if ($existingScript) {
                        $scriptId = $existingScript.id
                    }
                    elseif ($scriptDefinition -and $scriptDefinition.detectionScriptContentBase64) {
                        # Create the compliance script
                        $scriptBody = @{
                            description = if ($scriptDefinition.description) { $scriptDefinition.description } else { "" }
                            detectionScriptContent = $scriptDefinition.detectionScriptContentBase64
                            displayName = $scriptDisplayName
                            enforceSignatureCheck = [bool]$scriptDefinition.enforceSignatureCheck
                            publisher = if ($scriptDefinition.publisher) { $scriptDefinition.publisher } else { "Publisher" }
                            runAs32Bit = [bool]$scriptDefinition.runAs32Bit
                            runAsAccount = if ($scriptDefinition.runAsAccount) { $scriptDefinition.runAsAccount } else { "system" }
                        }

                        $newScript = Invoke-MgGraphRequest -Method POST -Uri "beta/deviceManagement/deviceComplianceScripts" -Body ($scriptBody | ConvertTo-Json -Depth 10) -ContentType "application/json" -ErrorAction Stop
                        $scriptId = $newScript.id
                    }
                    else {
                        Write-Warning "Skipping compliance policy '$displayName' - no script definition found with detectionScriptContentBase64"
                        $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Failed' -Status 'Missing detectionScriptContentBase64 in deviceCompliancePolicyScriptDefinition'
                        continue
                    }
                }
                catch {
                    Write-Warning "Failed to create/find compliance script for '$displayName': $($_.Exception.Message)"
                    $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Failed' -Status "Script error: $($_.Exception.Message)"
                    continue
                }

                # Step 2: Convert rules to base64
                $rulesSource = $scriptDefinition.rules
                if (-not $rulesSource) {
                    Write-Warning "Skipping compliance policy '$displayName' - no rules found in deviceCompliancePolicyScriptDefinition"
                    $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Failed' -Status 'Missing rules in deviceCompliancePolicyScriptDefinition'
                    continue
                }

                $rulesJson = $rulesSource | ConvertTo-Json -Depth 100 -Compress
                $rulesBytes = [System.Text.Encoding]::UTF8.GetBytes($rulesJson)
                $rulesBase64 = [System.Convert]::ToBase64String($rulesBytes)

                # Step 3: Update the policy body with resolved values
                $importBody.deviceCompliancePolicyScript = @{
                    deviceComplianceScriptId = $scriptId
                    rulesContent = $rulesBase64
                }
            }

            # Remove internal helper definition before sending
            if ($importBody.PSObject.Properties['deviceCompliancePolicyScriptDefinition']) {
                $null = $importBody.PSObject.Properties.Remove('deviceCompliancePolicyScriptDefinition')
            }

            if ($PSCmdlet.ShouldProcess($displayName, "Create compliance policy")) {
                $null = Invoke-MgGraphRequest -Method POST -Uri $endpoint -Body ($importBody | ConvertTo-Json -Depth 100) -ContentType 'application/json' -ErrorAction Stop
                Write-HydrationLog -Message " Created: $displayName" -Level Info
                $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Created' -Status 'Success'
            }
            else {
                Write-HydrationLog -Message " WouldCreate: $displayName" -Level Info
                $results += New-HydrationResult -Name $displayName -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'WouldCreate' -Status 'DryRun'
            }
        }
        catch {
            $errMessage = Get-GraphErrorMessage -ErrorRecord $_
            Write-HydrationLog -Message " Failed: $($templateFile.Name) - $errMessage" -Level Warning
            $results += New-HydrationResult -Name $templateFile.Name -Path $templateFile.FullName -Type 'CompliancePolicy' -Action 'Failed' -Status $errMessage
        }
    }

    return $results
}