Private/New-ExcludeGroup.ps1

function New-ExcludeGroup {
    <#
    .SYNOPSIS
    Create or get an Entra ID security group for policy exclusions
     
    .DESCRIPTION
    Creates a security group in Entra ID if it doesn't exist, or returns the existing group
    #>

    
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$GroupName
    )
    
    try {
        # Check if group already exists
        $existingGroup = $null
        try {
            $existingGroup = Get-MgGroup -Filter "DisplayName eq '$GroupName'" -Top 1 -ErrorAction SilentlyContinue
        }
        catch {
            # Use REST API fallback
            $invokeCmd = Get-Command Invoke-MgGraphRequest -ErrorAction SilentlyContinue
            if ($invokeCmd) {
                $filter = [System.Web.HttpUtility]::UrlEncode("displayName eq '$GroupName'")
                $response = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/groups?`$filter=$filter&`$top=1" -ErrorAction Stop
                if ($response.value -and $response.value.Count -gt 0) {
                    $existingGroup = $response.value[0]
                }
            }
        }
        
        if ($existingGroup) {
            $groupId = if ($existingGroup.Id) { $existingGroup.Id } else { $existingGroup.id }
            Write-Host " Group already exists: $GroupName (ID: $groupId)" -ForegroundColor Gray
            return $existingGroup
        }
        
        # Create new security group
        Write-Host " Creating group: $GroupName" -ForegroundColor Yellow
        $newGroup = $null
        
        try {
            # Try cmdlet first
            $newGroup = New-MgGroup -DisplayName $GroupName `
                -MailNickName ($GroupName.Replace(' ', '_').Replace('-', '_')) `
                -MailEnabled:$False `
                -SecurityEnabled `
                -IsAssignableToRole:$False `
                -ErrorAction Stop
        }
        catch {
            # Use REST API fallback (handles assembly conflicts and permission errors)
            $invokeCmd = Get-Command Invoke-MgGraphRequest -ErrorAction SilentlyContinue
            if ($invokeCmd) {
                $body = @{
                    displayName = $GroupName
                    mailNickname = ($GroupName.Replace(' ', '_').Replace('-', '_'))
                    mailEnabled = $false
                    securityEnabled = $true
                    groupTypes = @()
                } | ConvertTo-Json
                
                try {
                    $newGroup = Invoke-MgGraphRequest -Method POST `
                        -Uri "https://graph.microsoft.com/v1.0/groups" `
                        -Body $body `
                        -ContentType "application/json" `
                        -ErrorAction Stop
                }
                catch {
                    # Try to parse error response as JSON, but handle non-JSON errors gracefully
                    $errorResponse = $null
                    $errorMessage = $_.ErrorDetails.Message
                    
                    if ($errorMessage) {
                        # Check if error message looks like JSON (starts with { or [)
                        if ($errorMessage.Trim().StartsWith('{') -or $errorMessage.Trim().StartsWith('[')) {
                            try {
                                $errorResponse = $errorMessage | ConvertFrom-Json -ErrorAction Stop
                            }
                            catch {
                                # Not valid JSON, ignore
                            }
                        }
                        
                        # Check for permission errors in plain text too
                        if ($errorMessage -match "403" -or $errorMessage -match "Authorization_RequestDenied" -or $errorMessage -match "Insufficient") {
                            throw "Insufficient permissions. App Registration needs 'Group.ReadWrite.All' or 'Group.Create' permission."
                        }
                    }
                    
                    # If we successfully parsed JSON and it's an authorization error
                    if ($errorResponse -and $errorResponse.error -and $errorResponse.error.code -eq "Authorization_RequestDenied") {
                        throw "Insufficient permissions. App Registration needs 'Group.ReadWrite.All' or 'Group.Create' permission."
                    }
                    
                    # Re-throw original error if not a permission issue
                    throw $_
                }
            }
            else {
                throw "Cannot create group: $_"
            }
        }
        
        $groupId = if ($newGroup.Id) { $newGroup.Id } else { $newGroup.id }
        Write-Host " Group created: $GroupName (ID: $groupId)" -ForegroundColor Green
        return $newGroup
    }
    catch {
        $errorMsg = $_.Exception.Message
        $fullError = $_.ToString()
        
        # Check for JSON parsing errors (these are usually permission errors in disguise)
        if ($errorMsg -match "Conversion from JSON failed" -or $errorMsg -match "Unexpected character") {
            Write-Warning "Failed to create group $GroupName : Likely insufficient permissions (JSON parsing error indicates API permission issue)"
            throw "Insufficient permissions to create groups. Please add 'Group.ReadWrite.All' or 'Group.Create' permission to your App Registration."
        }
        # Check for permission errors
        elseif ($errorMsg -match "403" -or $errorMsg -match "Authorization_RequestDenied" -or $errorMsg -match "Insufficient" -or $fullError -match "403" -or $fullError -match "Authorization_RequestDenied") {
            Write-Warning "Failed to create group $GroupName : Insufficient permissions. The App Registration needs 'Group.ReadWrite.All' or 'Group.Create' permission."
            throw "Insufficient permissions to create groups. Please add 'Group.ReadWrite.All' or 'Group.Create' permission to your App Registration."
        }
        else {
            Write-Warning "Failed to create group $GroupName : $errorMsg"
            throw $_
        }
    }
}