Public/New-ApplicationDeploymentGroup.ps1

function New-ApplicationDeploymentGroup {
    <#
    .SYNOPSIS
        Generates, and optionally creates, the Entra ID security groups used to deploy an application with Intune.

    .DESCRIPTION
        The New-ApplicationDeploymentGroup function builds four group names for each application:
          Intune-AG-<Name>-Available, Intune-AG-<Name>-Required, Intune-App-<Name>-Test and
          Intune-App-<Name>-Phase1
        where <Name> is the application name in title case without spaces.

        By default the group list is returned. With -CreateGroups the groups are created in Entra ID
        (existing groups are skipped), optionally added to an administrative unit and given members.
        With -CreateFile the list is also exported to Application-Groups.csv in Destination.

        Creating groups needs the Microsoft.Entra module (Get-EntraGroup, New-EntraGroup,
        Add-EntraGroupMember) and, for -AdminUnitId, Microsoft.Graph.Identity.DirectoryManagement.
        Connect first with Connect-Entra or Connect-MgGraph.

        The alias New-ApplicationDeploymentGroups is kept for compatibility with earlier versions.

    .PARAMETER ApplicationName
        The name(s) of the application(s) for which to create deployment groups. Multiple names can be provided.

    .PARAMETER CreateGroups
        Create the groups in Entra ID.

    .PARAMETER CreateFile
        Export the group list to Application-Groups.csv in Destination.

    .PARAMETER Destination
        The folder for Application-Groups.csv when using -CreateFile. Must be an existing folder.

    .PARAMETER AdminUnitId
        The ID of the Entra ID administrative unit that new groups are added to.

    .PARAMETER AvailableMembers
        Object IDs of the members to add to the Available groups.

    .PARAMETER RequiredMembers
        Object IDs of the members to add to the Required groups.

    .PARAMETER TestMembers
        Object IDs of the members to add to the Test groups.

    .PARAMETER Phase1Members
        Object IDs of the members to add to the Phase1 groups.

    .OUTPUTS
        System.Management.Automation.PSCustomObject
        One object per group (Name, GroupName, GroupDescription) when -CreateGroups is not used.

    .EXAMPLE
        New-ApplicationDeploymentGroup -ApplicationName "Microsoft 365 Apps"

        Returns the four group names for Microsoft 365 Apps, for example Intune-AG-Microsoft365Apps-Available.

    .EXAMPLE
        New-ApplicationDeploymentGroup -ApplicationName "Microsoft 365 Apps" -CreateGroups -TestMembers '00000000-0000-0000-0000-000000000001'

        Creates the security groups in Entra ID and adds one member to the Test group.

    .EXAMPLE
        New-ApplicationDeploymentGroup -ApplicationName "Adobe Reader", "Google Chrome" -CreateFile -Destination "C:\Output"

        Returns the group names for Adobe Reader and Google Chrome and writes them to C:\Output\Application-Groups.csv.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$ApplicationName,

        [switch]$CreateGroups,

        [Parameter(ParameterSetName = "file")]
        [switch]$CreateFile,

        [Parameter(ParameterSetName = "file")]
        [ValidateScript({
                if (-not (Test-Path -Path $_ -PathType Container)) {
                    throw "The path $_ does not exist."
                }
                return $true
            })]
        [string]$Destination,

        [Parameter(Mandatory = $false)]
        [string]$AdminUnitId,

        [Parameter(Mandatory = $false)]
        [string[]]$AvailableMembers,

        [Parameter(Mandatory = $false)]
        [string[]]$RequiredMembers,

        [Parameter(Mandatory = $false)]
        [string[]]$TestMembers,

        [Parameter(Mandatory = $false)]
        [string[]]$Phase1Members
    )
    begin {
        $TelemetryArgs = @{
            ModuleName    = $MyInvocation.MyCommand.Module.Name
            ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
            CommandName   = $MyInvocation.MyCommand.Name
            ExecutionID   = [guid]::NewGuid().ToString()
        }
        Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
        $TelemetryFailed = $false
        try {
            # Capitalise each word in the application name and remove the spaces
            $textInfo = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo
            $FormattedNames = $ApplicationName | ForEach-Object { ($_.Split(' ') | ForEach-Object { $textInfo.ToTitleCase($_.ToLower()) }) -join '' }
            if ($CreateFile -and [string]::IsNullOrEmpty($Destination)) {
                throw 'Destination is required when CreateFile is used.'
            }
            if ($CreateGroups -and -not (Get-Command -Name 'New-EntraGroup' -ErrorAction SilentlyContinue)) {
                throw 'CreateGroups needs the Microsoft.Entra module (New-EntraGroup). Install it with Install-Module Microsoft.Entra and connect with Connect-Entra.'
            }
        }
        catch {
            $TelemetryFailed = $true
            Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            throw
        }
    }
    process {
        try {
            # Generate the security group names
            $GroupList = [System.Collections.Generic.List[object]]::new()
            $NameTemplates = @(
                "Intune-AG-{{ApplicationName}}-Available",
                "Intune-AG-{{ApplicationName}}-Required",
                "Intune-App-{{ApplicationName}}-Test",
                "Intune-App-{{ApplicationName}}-Phase1"
            )
            foreach ($NameTemplate in $NameTemplates) {
                foreach ($Application in $FormattedNames) {
                    $GroupList.Add([PSCustomObject]@{
                            Name             = $Application
                            GroupName        = $NameTemplate.Replace('{{ApplicationName}}', $Application)
                            GroupDescription = "Assignment group for the $Application application."
                        })
                }
            }
            $MembersBySuffix = @{
                'Available' = $AvailableMembers
                'Required'  = $RequiredMembers
                'Test'      = $TestMembers
                'Phase1'    = $Phase1Members
            }
            if ($CreateGroups) {
                foreach ($Group in $GroupList) {
                    $GroupName = $Group.GroupName
                    # Test if the group exists first
                    try {
                        $GroupExists = Get-EntraGroup -Filter "displayName eq '$($GroupName.Replace("'", "''"))'" -ErrorAction Stop
                    }
                    catch {
                        Write-Verbose "Could not look up group '$GroupName': $($_.Exception.Message)"
                        $GroupExists = $null
                    }
                    if ($GroupExists) {
                        Write-Output "Group $GroupName already exists in Entra, skipping."
                        continue
                    }
                    if (-not $PSCmdlet.ShouldProcess($GroupName, 'Create Entra ID security group')) {
                        continue
                    }
                    Write-Output "Creating group $GroupName in Entra"
                    try {
                        $newGroup = New-EntraGroup -DisplayName $GroupName -MailEnabled $false -SecurityEnabled $true -MailNickname $GroupName -Description $Group.GroupDescription -ErrorAction Stop
                    }
                    catch {
                        Write-Error "Failed to create group $GroupName. Error: $_"
                        continue
                    }
                    # Assign to the administrative unit if specified
                    if ($AdminUnitId) {
                        try {
                            Add-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $AdminUnitId -DirectoryObjectId $newGroup.Id -ErrorAction Stop
                            Write-Output "Assigned group $($newGroup.Id) to Admin Unit $AdminUnitId"
                        }
                        catch {
                            Write-Warning "Failed to assign group $($newGroup.Id) to Admin Unit ${AdminUnitId}: $_"
                        }
                    }
                    # Add group members based on the group type (the last part of the group name)
                    $Suffix = $GroupName.Split('-')[-1]
                    foreach ($member in @($MembersBySuffix[$Suffix] | Where-Object { $_ })) {
                        try {
                            Add-EntraGroupMember -GroupId $newGroup.Id -MemberId $member -ErrorAction Stop
                            Write-Output "Added member $member to group $GroupName"
                        }
                        catch {
                            Write-Warning "Failed to add member $member to group ${GroupName}: $_"
                        }
                    }
                }
            }
            if ($CreateFile) {
                $CsvPath = Join-Path -Path $Destination -ChildPath 'Application-Groups.csv'
                if ($PSCmdlet.ShouldProcess($CsvPath, 'Export group list')) {
                    $GroupList | Export-Csv -Path $CsvPath -NoTypeInformation -Force
                }
            }
            if (-not $CreateGroups) {
                $GroupList
            }
        }
        catch {
            if (-not $TelemetryFailed) {
                $TelemetryFailed = $true
                Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
            }
            throw
        }
    }
    end {
        if (-not $TelemetryFailed) {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
    }
}