While certainly interesting in nature, the recent Midnight Blizzard breach is just the same old story – unprotected account, unsecured environment, a lot of neglect and failure to adhere to the best practices and Microsoft’s own security guidance. If anything, it is another reminder of just how important covering your basics is. Which is exactly what we will do in this article, with an updated version of the script(s) to generate a report of all Entra ID directory role assignments within you organization, including the ones managed via Privileged Identity Management.
Last time we did this exercise, we updated our Azure AD PowerShell based script to use the Microsoft Graph SDK for PowerShell, and also provided a “raw” Graph API version. Since then not much has changed, apart from the GA of the Privileged Access Groups feature, now known as PIM for Groups, which the scripts now address, to an extent. In addition, the scripts now try to do a better job of handling activated eligible roles, as prompted by a recent comment. Related to those changes, the requirements for running the script have changed a bit.
As before, he have the Graph SDK version of the script, which runs in the context of a user, and the “raw” Graph API version, which uses application permissions and is more suitable for automation. Both versions have a hard requirement of having the Directory.Read.All scope, which is used to fetch the list of directory role assignments and provide information about the corresponding assignee. Depending on the set of optional parameters used, the permission requirements might change, as follows:
- IncludePIMEligibleAssignments – use this switch to include PIM eligible directory role assignments in the output. Requires the RoleManagement.Read.Directory or equivalent scope. Default value is $false.
- IncludePAGAssignments – use this switch to “expand” the list of members of any group-assigned roles. Requires the PrivilegedEligibilitySchedule.Read.AzureADGroup or equivalent scope. Default value is $false.
A note on permission handling. The SDK version of the script will check whether the token contains any of the required scopes via the Get-MgContext cmdlet, and will break processing if it doesn’t like what it sees. You might end up in a situation where you have granted “broader” scopes, which technically allow you to get all the required data, yet the script will complain. Should that happen, make sure to edit the corresponding checks (lines 16-20 and 31-32). In contrast, the Graph API version doesn’t bother with such checks, as we didn’t want to introduce additional helper functions to decode the token. So in scenarios where the Graph API version runs into an issue with permissions, it will simply spill out a warning and continue (or break, if you are missing the Directory.Read.All one).
If you are going to run the “raw” API version of the script, make sure to fill in the “authentication” variables on lines 17-20. Do make sure to provide the id of an application with sufficient permissions, as detailed above. And of course, never ever store client secrets in script files – the example used in the script is just that, an example, not to be used in any production environment. Unless you want to make some headlines yourself 🙂
Without further ado, here are the links to download the Graph API based script and its Graph SDK variant. Review the set of requirements and then run the scripts by leveraging one of the examples below (all examples use the Graph SDK version, replace the script name with AADRolesInventory-Graph.ps1 for the Graph API one):
#Generate a report of all (non-PIM) directory role assignments .\AADRolesInventory-MG.ps1 #Include PIM eligible assignments .\AADRolesInventory-MG.ps1 -IncludePIMEligibleAssignments #Also include members of any PAGs .\AADRolesInventory-MG.ps1 -IncludePIMEligibleAssignments -IncludePAGAssignments #Use -Verbose to display additional information as the script progresses, use -OutVariable to store the output if not exporting to CSV .\AADRolesInventory-MG.ps1 -Verbose -OutVariable out
By default, the script will generate a CSV file to store the output and save it within the working directory. The screenshot below illustrates how the report will look like. Note that we use a different format for the Principal value depending on its type, for user principals this will be the UPN, whereas for all other types, we expose the GUID.
If any PIM eligible role has been activated, the AssignmentType value will be updated to reflect that, avoiding the need to list multiple instances of the same role. The corresponding AssignmentStartDate and AssignmentStartDate values will also be updated to reflect the start and end timestamps, respectively. Now, for a group-based role assignment things are a bit more complicated, as it can have multiple users with active assignments at the same time. Instead of listing each separately, the script will try to “concatenate” the relevant info. For such scenarios, the GroupEligibleAssignmentActivatedFor column will be populated with a semicolon-separated list of group members that have activated the role. AssignmentStartDate and AssignmentEndDate values will represent the latest activation timestamp and the farthest deactivation one, respectively.
The chosen format is hardly a perfect solution, but it helps avoid listing the same role multiple times. And there is really no way to address both user (1:1) and group (1:many) assignments in a CSV-friendly format. The same goes for membership of PAGs. Currently, the script uses the ActiveGroupMembers and EligibleGroupMembers columns to present a concatenated list of members. If an eligible member has activated his membership to the group though, he will appear in both columns. While we can certainly remove him from the EligibleGroupMembers column, this can create confusion – after all there is still an eligible membership assignment for said user. The alternative would be to introduce yet another set of columns… or list every individual entry on a separate line, which brings back the “duplication” issue. Oh well, do let us know what format you prefer for the output 🙂
None of the roles showcased in the output above have been scoped down, and thus apply to the entire directory. If a scoped role assignment is found, the relevant AU will be listed within the AssignedRoleScope field. In addition, no custom roles are shown either, but they will be included in the report (with a False value for the IsBuiltIn column). Making sure that the script accounts for all such corner cases is the reason why we use a mix of the “standard” directory role methods and their PIM counterparts, as neither set gives us the full picture on their own. To be more specific, PIM “ignores” some of the now deprecated roles (for example the “AdHoc License Administrator” one) or just “hides” them in the UI (“Directory Synchronization Accounts”).
In summary, we presented an updated version of the directory role assignments report, now featuring support for Privileged Authentication Groups (or PIM for Groups), and with “improved” output. Our desire to avoid the introduction of “helper” functions might have resulted in some questionable code, but the end result meets our needs. As usual, treat the script samples as “proof of concept” and feel free to make any changes as you see fit. If you like or dislike something, leave a comment or a PR!
Before closing the article, it’s worth re-iterating that the directory roles are only part of the full picture. Nowadays, Microsoft 365 admins must stay on top of not just role assignments, but also service principals and application registrations, including their credentials. And do not forget that workloads within M365 have their own set of permissions and RBAC controls, which you should also keep an eye on.
Hi, I am running your script but it does not export. I run as suggested the script with verbose and outvariable out. It returns Output exported to C:\Scripts\2024-10-29_12-02-51_AzureADRoleInventory.csv but there is nothing there. Run the script with windows powerhsell and powershell 7+: same results.
Erm, looks like I uploaded a version of the script that has the export bits commented out. Thanks for pointing that out.
You can use the $out variable for the export:
Or just remove the comment out of line 148 (225 for the Graph API version).
when using cloud shell, there is still very old nuisance the sort alias is not recognized. updated – $report | sort-object PrincipalDisplayName.
Uh, my coding “style” is not Mac/Linux friendly, I’ll try to improve that going forward. Thanks.
hello,
thank you for this article, however from my side when i run the script i get this error? do you have any idea how to resolve this?
command : .\AADRolesInventory-MG.ps1 -IncludePIMEligibleAssignments -IncludePAGAssignments
error :
Get-MgGroupTransitiveMember: Unable to load the file or assembly ‘Microsoft.Graph.Authentication, Version=2.13.1.0, Culture=neutral,
PublicKeyToken=31bf3856ad364e35’ or one of its dependencies. The specified file cannot be found.
thank you
Make sure you have the latest version of the Graph SDK for PowerShell installed. Also, I’ve only tested the script on PowerShell 7+, in case you are running it on Windows PowerShell.