Reporting on BitLocker recovery keys and associated devices

Today’s article will be an odd one, as its primary goal is to address some requests from the Q&A platform. In particular, the question about getting a list of all BitLocker recovery keys, posted back in June and subsequent requests on the same topic. The original query is easy enough to address – it’s a single cmdlet when using the Graph SDK for PowerShell:

Connect-MgGraph -Scopes BitLockerKey.Read.All
Get-MgInformationProtectionBitlockerRecoveryKey -All

Things get a little more complicated if you want to include the actual key values within the output, as you will have to iterate over each key (the value is only shown when specifically requesting the “key” property and only when doing a request against a specific key, i.e. not returned on the LIST method). Still, it’s easy enough to do:

Get-MgInformationProtectionBitlockerRecoveryKey -All | select Id,CreatedDateTime,DeviceId,@{n="Key";e={(Get-MgInformationProtectionBitlockerRecoveryKey -BitlockerRecoveryKeyId $_.Id -Property key).key}},VolumeType

As it usually happens with such questions though, there is always the next ask, and the next one after that. Over the past few months, multiple people have requested additional bits of information to be included in the oneliner above, such as the device name, or its owner. Others suggested a device-centric view instead, highlighting all the devices with (or without) a BitLocker recovery key. So let’s see how this all can be incorporated in a simple script.

Including the device name is easy, as we already have the device identifier within the output. What’s not as easy as it should have been is the way the Graph SDK handles things, but this is a recurring pattern in all our script samples, sadly. This time, the Graph SDK, in its all autogenerated wisdom, is exposing the –DeviceId parameter for the Get-MgDevice cmdlet, while at the same time only accepting the “Id” value for it. In other words, you end up with the following behavior:

Get-MgDevice -DeviceId 05ab7c00-ea9d-4c1b-8dc2-ef539bf2a27b
Get-MgDevice_Get: Resource '05ab7c00-ea9d-4c1b-8dc2-ef539bf2a27b' does not exist or one of its queried reference-property objects are not present.

Get-MgDevice -Filter "deviceId eq '05ab7c00-ea9d-4c1b-8dc2-ef539bf2a27b'"

Get MgDeviceAbsolutely marvelous, award-worthy even! Anyway, at the end of the day once you work around the oddities of the module, the task at hand is easy enough. Don’t forget that you might need additional permissions in order to run the Get-MgDevice cmdlet, namely Device.Read.All. Same goes for the next task at hand, namely getting the device owner – you only need to make sure you have sufficient permissions to run the Get-MgDeviceRegisteredOwner cmdlet (User.ReadBasic.All being the minimum required one):

Get-MgDeviceRegisteredOwner -DeviceId 126e6c29-de6f-4622-9795-07ad9ff2e613

At this point, we are already hitting three different cmdlets per each key, so we might as well start thinking about some optimizations. For example, instead of using a separate call for getting the device owner, we can leverage the $expand operator to include the registeredOwners properties in our “get device” request. It’s not exactly an efficient method, as it always returns the full set of properties for the user object… but it’s better than running multiple queries. Maybe we’ll live to see proper support for $expand=registeredOwners($select=userPrincipalName) and similar, who knows 🙂

Anyway, another optimization we can do is to replace the call to Get-MgDevice cmdlet per each key with a single call that fetches all the devices, including their registered owners. This in turn makes it that much easier to address the final request – providing a report of all devices with/without BitLocker recovery key. And to avoid further queries, we can also include any relevant device property, such as the device’s compliance status, or even last activity.

Without further ado, you can download the BitLocker report script from my GitHub repository. The following parameters are supported by the script, with the relevant permissions listed next to them (BitLockerKey.Read.All is always needed, regardless of any parameters used!):

  • IncludeDeviceInfo  – optional, use it to retrieve device details. Requires Device.Read.All permissions.
  • IncludeDeviceOwner – optional, use it to retrieve device owner’s UPN. Requires User.ReadBasic.All permissions.
  • DeviceReport – optional, switches the output formatting to device-centric report. Requires all the aforementioned permissions.

The script will try to detect whether it runs with the necessary permissions, but the check is not smart enough to detect scenarios where you have “broader” scopes already added, i.e. Directory.Read.All. Feel free to update the relevant code as you see fit (lines 40-56).

Here are some examples on how to run the script. By default, when you run it without any parameters, only minimal output will be returned (including the BitLocker key value). To include device details, use the –IncludeDeviceInfo switch. To also include device owner’s UPN, use the –IncludeDeviceOwner switch (which automatically toggles –IncludeDeviceInfo as well). And should you want to dump (mostly) all device details and produce a device-centric report, use the –DeviceReport switch (toggles all other switches too):

#Get list of BitLocker recovery keys and their values
.\GraphSDK_Bitlocker_report.ps1

#Include data about the device
.\GraphSDK_Bitlocker_report.ps1 -IncludeDeviceInfo

#Also include data about the device's owner
.\GraphSDK_Bitlocker_report.ps1 -IncludeDeviceInfo -IncludeDeviceOwner

#Generate a report of all devices, with their associated BitLocker recovery keys
.\GraphSDK_Bitlocker_report.ps1 -DeviceReport

#You can also use -Verbose and -OutVariable as needed
.\GraphSDK_Bitlocker_report.ps1 -DeviceReport -Verbose -OutVariable out

Few words about the output. By default, output will be written to a CSV file within the working directory. Depending on which parameters you used, the set of fields within the CSV file will greatly vary. If interested in adding additional properties to the output, insert them in between lines 113-120. For the device-centric report, all fields are already collected, but we hide some of them in the output. If you want to adjust that, edit the list of excluded properties on line 132.

BitLockerReport

Using the output above, it’s very easy to filter out any devices without a BitLocker recovery key, so all should be happy. But as always, feel free to update the script and its output format to best suit your own needs.

P.S. Another Graph quirk I forgot to mention – using the $filter operator against the /devices endpoint will always return null values for the managementType property… which is another reason why we get the list of all devices instead of doing things one by one.

6 thoughts on “Reporting on BitLocker recovery keys and associated devices

  1. Maksim Nikiforov says:

    Al worked perfectly until end of March, just remembered that day, now getting this:

    PS /Users/mwg/Downloads/Powershell> .\GraphSDK_Bitlocker_report.ps1 -IncludeDeviceInfo -IncludeDeviceOwner -Verbose
    VERBOSE: Connecting to Graph API…
    VERBOSE: Retrieving device details…
    VERBOSE: Retrieving device owner…
    VERBOSE: Retrieved 5263 devices
    VERBOSE: Retrieving BitLocker keys…
    VERBOSE: Retrieving BitLocker Recovery keys…
    Get-MgInformationProtectionBitlockerRecoveryKey_Get: /Users/mwg/Downloads/Powershell/GraphSDK_Bitlocker_report.ps1:92
    Line |
    92 | $RecoveryKey = Get-MgInformationProtectionBitlockerRecoveryKey -B …
    | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | The request was canceled due to the configured HttpClient.Timeout of 300 seconds elapsing.
    PS /Users/mwg/Downloads/Powershell>

    Reply
    1. Vasil Michev says:

      Still works fine here, it seems. Then again, I don’t have 5k devices to test it against… maybe try running it in batches? On line 91, replace $Keys with $Keys[0..999] to cover the first 1000 keys only, see how it behaves.

      Reply
  2. Luca says:

    It doesn’t work for me. It stucks after run it for minutes and minutes.

    Reply
      1. Maksim Nikiforov says:

        Hi Vasil,
        Many many thanks for your work and help for us! Really appreciate it!
        Please find my new comment above with output listing.
        {We can meet and a cup of coffee in Sofia 😉 }

        Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.