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.
        A group whose existence cannot be checked (for example because the lookup fails) is not
        created. Every failure for a group (lookup, creation, administrative unit, member) is written
        as a non-terminating error and the other groups are still processed.
        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
        Without -CreateGroups: one object per group with Name, GroupName and GroupDescription.
        With -CreateGroups: one object per created or existing group with Name, GroupName,
        GroupDescription, Id and Status ('Created' or 'Exists'). Groups that failed are not returned.

    .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
        # The last per-group error; reported to telemetry as a failure at the end
        $GroupError = $null
        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
                    # A failed lookup is not the same as "not found": do not create a possible duplicate
                    try {
                        $ExistingGroup = @(Get-EntraGroup -Filter "displayName eq '$($GroupName.Replace("'", "''"))'" -ErrorAction Stop | Where-Object { $_ })
                    }
                    catch {
                        $GroupError = $_
                        Write-Error -Message "Could not check whether the group '$GroupName' exists, so it was not created: $($_.Exception.Message)" -Exception $_.Exception -TargetObject $GroupName
                        continue
                    }
                    if ($ExistingGroup.Count -gt 0) {
                        Write-Verbose "Group $GroupName already exists in Entra ID, skipping."
                        [PSCustomObject]@{
                            Name             = $Group.Name
                            GroupName        = $GroupName
                            GroupDescription = $Group.GroupDescription
                            Id               = $ExistingGroup[0].Id
                            Status           = 'Exists'
                        }
                        continue
                    }
                    if (-not $PSCmdlet.ShouldProcess($GroupName, 'Create Entra ID security group')) {
                        continue
                    }
                    Write-Verbose "Creating group $GroupName in Entra ID."
                    try {
                        $newGroup = New-EntraGroup -DisplayName $GroupName -MailEnabled $false -SecurityEnabled $true -MailNickname $GroupName -Description $Group.GroupDescription -ErrorAction Stop
                    }
                    catch {
                        $GroupError = $_
                        Write-Error -Message "Failed to create group ${GroupName}: $($_.Exception.Message)" -Exception $_.Exception -TargetObject $GroupName
                        continue
                    }
                    # Assign to the administrative unit if specified
                    if ($AdminUnitId) {
                        try {
                            Add-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $AdminUnitId -DirectoryObjectId $newGroup.Id -ErrorAction Stop
                            Write-Verbose "Assigned group $($newGroup.Id) to administrative unit $AdminUnitId."
                        }
                        catch {
                            $GroupError = $_
                            Write-Error -Message "Failed to assign group $GroupName ($($newGroup.Id)) to administrative unit ${AdminUnitId}: $($_.Exception.Message)" -Exception $_.Exception -TargetObject $GroupName
                        }
                    }
                    # 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-Verbose "Added member $member to group $GroupName."
                        }
                        catch {
                            $GroupError = $_
                            Write-Error -Message "Failed to add member $member to group ${GroupName}: $($_.Exception.Message)" -Exception $_.Exception -TargetObject $GroupName
                        }
                    }
                    [PSCustomObject]@{
                        Name             = $Group.Name
                        GroupName        = $GroupName
                        GroupDescription = $Group.GroupDescription
                        Id               = $newGroup.Id
                        Status           = 'Created'
                    }
                }
            }
            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) {
            if ($GroupError) {
                Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $GroupError
            }
            else {
                Invoke-TelemetryCollection @TelemetryArgs -Stage End
            }
        }
    }
}