Public/Entra/Group/Get-DynamicGroup.ps1
|
<#
.SYNOPSIS Retrieves dynamic groups from Microsoft 365, Exchange Online, and Entra ID with attribute analysis. .DESCRIPTION The Get-DynamicGroup function retrieves all dynamic groups from Microsoft 365 environments, including Exchange Online Dynamic Distribution Groups and Entra ID Dynamic Security/M365 Groups. It analyzes membership rules to extract attributes and provides security warnings for attributes in the "Personal-Information" property set that users can modify themselves. .PARAMETER GroupId When specified, retrieves dynamic groups by their unique GroupId. If not specified, retrieves all dynamic groups (both Exchange Online and Entra ID). .PARAMETER EntraIDOnly When specified, retrieves only Entra ID Dynamic Groups (Security and M365 groups). Requires an active Microsoft Graph connection (use Connect-MgGraph). .PARAMETER ExchangeOnlineOnly When specified, retrieves only Exchange Online Dynamic Distribution Groups. Requires an active Exchange Online session (use Connect-ExchangeOnline). .EXAMPLE Get-DynamicGroup Retrieves all dynamic groups from both Exchange Online and Entra ID. .EXAMPLE Get-DynamicGroup -ExchangeOnlineOnly Retrieves only Exchange Online Dynamic Distribution Groups. .EXAMPLE Get-DynamicGroup -EntraIDOnly Retrieves only Entra ID Dynamic Groups. .OUTPUTS System.Collections.Generic.List[Object] .NOTES OUTPUT PROPERTIES Returns a collection of custom objects with the following properties: - Name: Display name of the group - Type: Type of dynamic group (Exchange Dynamic Distribution Group, M365 Dynamic Group, Entra ID Dynamic Security Group) - Email: Primary email address of the group - Filter: The membership rule or LDAP filter used for dynamic membership - UserAttributes: Pipe-separated list of user attributes referenced in the membership rule - GroupAttributes: Pipe-separated list of group attributes referenced in the membership rule (Entra ID only) - DeviceAttributes: Pipe-separated list of device attributes referenced in the membership rule (Entra ID only) - Warning: Security warning if any attribute is in the "Personal-Information" property set Security Considerations: This function identifies attributes in the "Personal-Information" property set that users can modify themselves, potentially allowing unauthorized group membership. Review warnings carefully. More information on: https://itpro-tips.com/property-set-personal-information-and-active-directory-security-and-governance/ .LINK https://ps365.clidsys.com/docs/commands/Get-DynamicGroup #> # Active Directory to Entra ID Attribute Mapping Reference: # ========================================================= # Attribut AD Attribut Entra ID # assistant assistant # c country # facsimileTelephoneNumber faxNumber # homePhone homePhone # homePostalAddress homePostalAddress # info notes # ipPhone ipPhone # l city # mobile mobile # otherFacsimileTelephoneNumber otherFaxNumbers # otherHomePhone otherHomePhones # otherIpPhone otherIpPhones # otherMobile otherMobiles # otherPager otherPagers # otherTelephone otherPhones # pager pager # personalTitle personalTitle # physicalDeliveryOfficeName officeLocation # postalAddress streetAddress # postalCode postalCode # postOfficeBox postOfficeBox # st state # street streetAddress # streetAddress streetAddress # telephoneNumber telephoneNumber # thumbnailPhoto thumbnailPhoto # userCertificate userCertificate # msDS-cloudExtensionAttribute1 xx # msDS-cloudExtensionAttribute2 xx # msDS-cloudExtensionAttribute3 xx # msDS-cloudExtensionAttribute4 xx # msDS-cloudExtensionAttribute5 xx # msDS-cloudExtensionAttribute6 xx # msDS-cloudExtensionAttribute7 xx # msDS-cloudExtensionAttribute8 xx # msDS-cloudExtensionAttribute9 xx # msDS-cloudExtensionAttribute10 xx # msDS-cloudExtensionAttribute11 xx # msDS-cloudExtensionAttribute12 xx # msDS-cloudExtensionAttribute13 xx # msDS-cloudExtensionAttribute14 xx # msDS-cloudExtensionAttribute15 xx # msDS-ExternalDirectoryObjectId externalDirectoryObjectId function Get-DynamicGroup { [CmdletBinding()] param( [Parameter(Mandatory = $false)] [String]$GroupId, [Parameter(Mandatory = $false)] [switch]$ExchangeOnlineOnly, [Parameter(Mandatory = $false)] [switch]$EntraIDOnly, [Parameter(Mandatory = $false)] [switch]$ExportToExcel ) $propertySetsAttribute = @( 'assistant' 'country' 'faxNumber' 'homePhone' 'homePostalAddress' 'notes' 'ipPhone' 'city' 'mobile' 'MobilePhone' 'otherFaxNumbers' 'otherHomePhones' 'otherIpPhones' 'otherMobiles' 'otherPagers' 'otherPhones' 'pager' 'personalTitle' 'officeLocation' 'streetAddress' 'postalCode' 'postOfficeBox' 'state' 'streetAddress' 'streetAddress' 'telephoneNumber' 'thumbnailPhoto' 'userCertificate' ) # Initialize an array list for better performance [System.Collections.Generic.List[Object]]$dynGroupArray = @() # Check Exchange Online for Dynamic Distribution Groups if (-not $EntraIDOnly) { Write-Verbose 'Retrieving Exchange Online Dynamic Distribution Groups...' # Retrieve dynamic distribution groups if ([string]::IsNullOrWhitespace($GroupId) -eq $false) { try { $exchangeGroups = @(Get-DynamicDistributionGroup -Identity $GroupId -ErrorAction Stop) } catch { Write-Error "Error retrieving Exchange Online group with GroupId $GroupId. $($_.Exception.Message)" return } } else { try { $exchangeGroups = Get-DynamicDistributionGroup -ErrorAction Stop Write-Verbose "Found $($exchangeGroups.Count) Exchange Online Dynamic Distribution Groups" } catch { Write-Error "Error retrieving Exchange Online groups: $($_.Exception.Message)" $object = [PSCustomObject][ordered]@{ GroupId = 'Error' Name = 'Error' Type = 'Exchange Dynamic Distribution Group' MembershipRule = 'Error retrieving groups. Ensure you are connected to Exchange Online.' MembershipRuleProcessingState = 'Error' UserAttributes = 'N/A' GroupAttributes = 'N/A' DeviceAttributes = 'N/A' MemberOf = 'N/A' DisplayName = 'Error' Description = 'Error' Mail = 'Error' MailEnabled = 'Error' MailNickname = 'Error' SecurityEnabled = 'Error' GroupTypes = 'Error' CreatedDateTime = 'Error' RenewedDateTime = 'Error' OnPremisesSyncEnabled = 'Error' SecurityIdentifier = 'Error' Classification = 'Error' Visibility = 'Error' } $dynGroupArray.Add($object) } } foreach ($group in $exchangeGroups) { Write-Verbose "Processing Exchange group: $($group.Name)" try { $memberOf = Get-DistributionGroup -Identity $group.Identity | Select-Object -ExpandProperty MemberOfGroup -ErrorAction SilentlyContinue } catch { $memberOf = $null } $object = [PSCustomObject][ordered]@{ GroupId = $group.Guid Name = $group.Name Type = 'Exchange Dynamic Distribution Group' MembershipRule = (($group.LdapRecipientFilter.Replace("`r`n", '').Replace("`n", '').Replace("`r", '') -replace '\s+', ' ') -replace '(\))(\w)', '$1 $2' -replace '(\w)(\()', '$1 $2' -replace '\(\s+', '(').Trim() MembershipRuleProcessingState = $null UserAttributes = (($group.LdapRecipientFilter | Select-String -Pattern '\(([a-zA-Z][a-zA-Z0-9]*)' -AllMatches).Matches | Select-Object -ExpandProperty Value -Unique).Replace('(', '') -join ' | ' GroupAttributes = $null DeviceAttributes = $null MemberOf = $memberOf -join '|' Members = (Get-DynamicDistributionGroupMember -Identity $group.Identity).Count DisplayName = $group.DisplayName Description = $group.Notes Mail = $group.PrimarySmtpAddress MailEnabled = $true MailNickname = $group.Alias SecurityEnabled = $false GroupTypes = 'DynamicDistribution' CreatedDateTime = $group.WhenCreated RenewedDateTime = $null OnPremisesSyncEnabled = $group.IsDirSynced SecurityIdentifier = $null Classification = $null Visibility = if ($group.HiddenFromAddressListsEnabled) { 'Private' } else { 'Public' } } $dynGroupArray.Add($object) } } # Check Entra ID for Dynamic Groups if (-not $ExchangeOnlineOnly) { Write-Verbose 'Retrieving Entra ID Dynamic Groups...' if ([string]::IsNullOrWhitespace($GroupId) -eq $false) { try { $entraGroups = @(Get-MgGroup -GroupId $GroupId -ErrorAction Stop) if ($entraGroups.groupTypes -notcontains 'DynamicMembership') { Write-Error "The specified GroupId $GroupId is not a dynamic group." return } } catch { Write-Error "Error retrieving Entra ID group with GroupId $GroupId. $($_.Exception.Message)" return } } else { try { $entraGroups = Get-MgGroup -All -Filter "groupTypes/any(c:c eq 'DynamicMembership')" -ErrorAction Stop Write-Verbose "Found $($entraGroups.Count) Entra ID Dynamic Groups" } catch { Write-Error "Error retrieving Entra ID groups: $($_.Exception.Message)" $object = [PSCustomObject][ordered]@{ GroupId = 'Error' Name = 'Error' Type = 'Entra ID Dynamic Group' MembershipRule = 'Error retrieving groups. Ensure you are connected to Microsoft Graph.' MembershipRuleProcessingState = 'Error' UserAttributes = 'N/A' GroupAttributes = 'N/A' DeviceAttributes = 'N/A' MemberOf = 'N/A' Members = 'N/A' DisplayName = 'Error' Description = 'Error' Mail = 'Error' MailEnabled = 'Error' MailNickname = 'Error' SecurityEnabled = 'Error' GroupTypes = 'Error' CreatedDateTime = 'Error' RenewedDateTime = 'Error' OnPremisesSyncEnabled = 'Error' SecurityIdentifier = 'Error' Classification = 'Error' Visibility = 'Error' } $dynGroupArray.Add($object) } } foreach ($group in $entraGroups) { Write-Verbose "Processing Entra ID group: $($group.DisplayName)" try { $memberOf = Get-MgGroupMemberOf -GroupId $group.Id -ErrorAction SilentlyContinue $memberOf = ($memberOf | Where-Object { $_.AdditionalProperties.'@odata.type' -eq '#microsoft.graph.group' } | ForEach-Object { $_.AdditionalProperties.displayName }) -join '|' } catch { $memberOf = $null } $object = [PSCustomObject][ordered]@{ GroupId = $group.Id Name = $group.DisplayName Type = if ($group.groupTypes -contains 'Unified') { 'M365 Dynamic Group' } else { 'Entra ID Dynamic Security Group' } # MembershipRule can be multiline, so we need to clean it up # Remove line breaks and extra spaces MembershipRule = (($group.MembershipRule.Replace("`r`n", '').Replace("`n", '').Replace("`r", '') -replace '\s+', ' ') -replace '(\))(\w)', '$1 $2' -replace '(\w)(\()', '$1 $2' -replace '\(\s+', '(').Trim() MembershipRuleProcessingState = $group.MembershipRuleProcessingState UserAttributes = (($group.MembershipRule | Select-String -Pattern 'user\.([a-zA-Z][a-zA-Z0-9]*)' -AllMatches).Matches | Select-Object -ExpandProperty Value | ForEach-Object { $_.Replace('user.', '') } | Sort-Object -Unique) -join '| ' GroupAttributes = (($group.MembershipRule | Select-String -Pattern 'group\.([a-zA-Z][a-zA-Z0-9]*)' -AllMatches).Matches | Select-Object -ExpandProperty Value | ForEach-Object { $_.Replace('group.', '') } | Sort-Object -Unique) -join '| ' DeviceAttributes = (($group.MembershipRule | Select-String -Pattern 'device\.([a-zA-Z][a-zA-Z0-9]*)' -AllMatches).Matches | Select-Object -ExpandProperty Value | ForEach-Object { $_.Replace('device.', '') } | Sort-Object -Unique) -join '| ' MemberOf = $memberOf -join '|' Members = (Get-MgGroupMember -GroupId $group.Id -All).Count DisplayName = $group.DisplayName Description = $group.Description Mail = $group.Mail MailEnabled = $group.MailEnabled MailNickname = $group.MailNickname SecurityEnabled = $group.SecurityEnabled GroupTypes = ($group.GroupTypes -join ', ') CreatedDateTime = $group.CreatedDateTime RenewedDateTime = $group.RenewedDateTime OnPremisesSyncEnabled = $group.OnPremisesSyncEnabled SecurityIdentifier = $group.SecurityIdentifier Classification = $group.Classification Visibility = $group.Visibility } $dynGroupArray.Add($object) } } # foreach attribute, check if it's in the `Personal-Information` property set attribute # if it's, add a warning to the object Write-Verbose "Analyzing security attributes for $($dynGroupArray.Count) groups..." foreach ($group in $dynGroupArray) { $group | Add-Member -MemberType NoteProperty -Name Warning -Value $null foreach ($attribute in $group.UserAttributes.Split('|')) { if ($propertySetsAttribute -contains $attribute) { $group.Warning = "'$attribute' is in the `Personal-Information` property set, the user can modify it and add himself to the group. See https://itpro-tips.com/property-set-personal-information-and-active-directory-security-and-governance/" } } } if ($ExportToExcel.IsPresent) { Write-Verbose 'Preparing Excel export...' $now = Get-Date -Format 'yyyy-MM-dd_HHmmss' $excelFilePath = "$($env:userprofile)\$now-DynamicGroups.xlsx" Write-Verbose "Excel file path: $excelFilePath" Write-Host -ForegroundColor Cyan "Exporting dynamic groups to Excel file: $excelFilePath" $dynGroupArray | Export-Excel -Path $excelFilePath -AutoSize -AutoFilter -WorksheetName 'DynamicGroups' Write-Host -ForegroundColor Green 'Export completed successfully!' } else { Write-Verbose "Returning $($dynGroupArray.Count) dynamic groups" return $dynGroupArray } } |