diff --git a/.gitignore b/.gitignore index 1003755..98f6218 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store MDE_OffboardDevices.parameters.json -MDE/MDE_OffboardDevices.parameters.json \ No newline at end of file +MDE/MDE_OffboardDevices.parameters.json +Entra/Entra-PrivilegedRoleMemberships.csv \ No newline at end of file diff --git a/Entra/Export-PrivilegedRolesMembership.ps1 b/Entra/Export-PrivilegedRolesMembership.ps1 new file mode 100644 index 0000000..c1475de --- /dev/null +++ b/Entra/Export-PrivilegedRolesMembership.ps1 @@ -0,0 +1,251 @@ +<# +.SYNOPSIS + Export all active memberships in privileged Microsoft Entra roles, + including expansion of group-assigned roles to transitive group members. + +.NOTES + - Uses Microsoft Graph PowerShell only (no Microsoft.Graph.Beta module import) + - Uses Invoke-MgGraphRequest against the beta endpoint only for role definition discovery + - Exports CSV to .\Entra-PrivilegedRoleMemberships.csv + +.REQUIREMENTS + Install-Module Microsoft.Graph -Scope CurrentUser + +.PERMISSIONS + Suggested delegated scopes: + RoleManagement.Read.Directory + Directory.Read.All + GroupMember.Read.All +#> + +param( + [string]$CsvPath = ".\Entra-PrivilegedRoleMemberships.csv" +) + +$ErrorActionPreference = "Stop" + +function Get-ObjectType { + param([object]$Object) + + if ($null -eq $Object.AdditionalProperties.'@odata.type') { + return $Object.GetType().Name + } + + return ($Object.AdditionalProperties.'@odata.type' -replace '^#microsoft.graph\.', '') +} + +function Get-DisplayNameSafe { + param([object]$Object) + + if ($Object.PSObject.Properties.Name -contains 'DisplayName' -and $Object.DisplayName) { + return $Object.DisplayName + } + + if ($Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey('displayName')) { + return $Object.AdditionalProperties['displayName'] + } + + return $null +} + +function Get-UpnSafe { + param([object]$Object) + + if ($Object.PSObject.Properties.Name -contains 'UserPrincipalName' -and $Object.UserPrincipalName) { + return $Object.UserPrincipalName + } + + if ($Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey('userPrincipalName')) { + return $Object.AdditionalProperties['userPrincipalName'] + } + + return $null +} + +function Get-MailSafe { + param([object]$Object) + + if ($Object.PSObject.Properties.Name -contains 'Mail' -and $Object.Mail) { + return $Object.Mail + } + + if ($Object.AdditionalProperties -and $Object.AdditionalProperties.ContainsKey('mail')) { + return $Object.AdditionalProperties['mail'] + } + + return $null +} + +function Invoke-GraphGetAllPages { + param( + [Parameter(Mandatory)] + [string]$Uri + ) + + $items = @() + $nextLink = $Uri + + while ($nextLink) { + $response = Invoke-MgGraphRequest -Method GET -Uri $nextLink -OutputType PSObject + + if ($response.value) { + $items += $response.value + } + else { + $items += $response + break + } + + $nextLink = $response.'@odata.nextLink' + } + + return $items +} + +Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan + +Import-Module Microsoft.Graph.Authentication +Import-Module Microsoft.Graph.Identity.Governance +Import-Module Microsoft.Graph.DirectoryObjects +Import-Module Microsoft.Graph.Groups + +Connect-MgGraph -Scopes @( + "RoleManagement.Read.Directory", + "Directory.Read.All", + "GroupMember.Read.All" +) -NoWelcome + +Write-Host "Getting privileged role definitions from Graph beta endpoint..." -ForegroundColor Cyan + +$roleDefsUri = "https://graph.microsoft.com/beta/roleManagement/directory/roleDefinitions?`$filter=isPrivileged eq true" +$privilegedRoles = Invoke-GraphGetAllPages -Uri $roleDefsUri + +if (-not $privilegedRoles) { + Write-Warning "No privileged roles were returned." + return +} + +Write-Host "Getting active role assignments..." -ForegroundColor Cyan +$assignments = Get-MgRoleManagementDirectoryRoleAssignment -All + +$privilegedRoleIds = @($privilegedRoles | ForEach-Object { $_.id }) +$privilegedAssignments = $assignments | Where-Object { $_.RoleDefinitionId -in $privilegedRoleIds } + +if (-not $privilegedAssignments) { + Write-Warning "No active assignments found for privileged roles." + return +} + +$roleMap = @{} +foreach ($role in $privilegedRoles) { + $roleMap[$role.id] = $role.displayName +} + +$principalCache = @{} +$groupMemberCache = @{} +$results = New-Object System.Collections.Generic.List[object] + +foreach ($assignment in $privilegedAssignments) { + $roleName = $roleMap[$assignment.RoleDefinitionId] + $principalId = $assignment.PrincipalId + $directoryScopeId = $assignment.DirectoryScopeId + + if (-not $principalCache.ContainsKey($principalId)) { + try { + $principalCache[$principalId] = Get-MgDirectoryObjectById -Ids $principalId + } + catch { + Write-Warning "Failed to resolve principal $principalId" + continue + } + } + + $principal = $principalCache[$principalId] + $principalType = Get-ObjectType -Object $principal + $principalName = Get-DisplayNameSafe -Object $principal + + switch ($principalType.ToLower()) { + "group" { + if (-not $groupMemberCache.ContainsKey($principalId)) { + try { + $groupMemberCache[$principalId] = Get-MgGroupTransitiveMember -GroupId $principalId -All + } + catch { + Write-Warning "Failed to expand group members for group $principalName ($principalId)" + $groupMemberCache[$principalId] = @() + } + } + + $members = $groupMemberCache[$principalId] + + if (-not $members -or $members.Count -eq 0) { + $results.Add([pscustomobject]@{ + RoleName = $roleName + AssignmentType = "GroupAssignment" + AssignmentPrincipalType = $principalType + AssignmentPrincipal = $principalName + AssignmentPrincipalId = $principalId + ExpandedMemberType = $null + ExpandedMemberName = $null + ExpandedMemberUPN = $null + ExpandedMemberMail = $null + ExpandedMemberId = $null + DirectoryScopeId = $directoryScopeId + Notes = "Group assigned, but no transitive members returned" + }) + } + else { + foreach ($member in $members) { + $memberType = Get-ObjectType -Object $member + $memberName = Get-DisplayNameSafe -Object $member + $memberUpn = Get-UpnSafe -Object $member + $memberMail = Get-MailSafe -Object $member + + $results.Add([pscustomobject]@{ + RoleName = $roleName + AssignmentType = "GroupAssignmentExpanded" + AssignmentPrincipalType = $principalType + AssignmentPrincipal = $principalName + AssignmentPrincipalId = $principalId + ExpandedMemberType = $memberType + ExpandedMemberName = $memberName + ExpandedMemberUPN = $memberUpn + ExpandedMemberMail = $memberMail + ExpandedMemberId = $member.Id + DirectoryScopeId = $directoryScopeId + Notes = "Expanded from group assignment" + }) + } + } + } + + default { + $results.Add([pscustomobject]@{ + RoleName = $roleName + AssignmentType = "DirectAssignment" + AssignmentPrincipalType = $principalType + AssignmentPrincipal = $principalName + AssignmentPrincipalId = $principalId + ExpandedMemberType = $principalType + ExpandedMemberName = $principalName + ExpandedMemberUPN = Get-UpnSafe -Object $principal + ExpandedMemberMail = Get-MailSafe -Object $principal + ExpandedMemberId = $principalId + DirectoryScopeId = $directoryScopeId + Notes = "Direct role assignment" + }) + } + } +} + +$results | + Sort-Object RoleName, AssignmentType, ExpandedMemberName | + Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8 + +Write-Host "" +Write-Host "Exported to: $CsvPath" -ForegroundColor Green +Write-Host "" + +$results | + Sort-Object RoleName, ExpandedMemberName | + Format-Table RoleName, AssignmentType, AssignmentPrincipal, ExpandedMemberName, ExpandedMemberUPN, ExpandedMemberType -AutoSize \ No newline at end of file