How to use the Invoke-HoldRemovalAction cmdlet to remove legacy and orphaned holds in Microsoft 365

Even if you are vaguely familiar with the set of compliance features within the Microsoft 365 suite, you are probably aware that Microsoft deprecated the In-Place eDiscovery holds functionality a while back, in favor of the suite-wide retention policies and tags. Regardless of the extended deprecation period, it looks like many customers failed to replace the old-style in-place eDiscovery holds, and now that the feature is officially deprecated, they face challenges removing such holds. To address such issues, Microsoft is introducing the Invoke-HoldRemovalAction cmdlet. Let’s take a look at the cmdlet and its use.

First things first, the cmdlet is only available as part of the Security and Compliance Center cmdlets, so make sure you first establish a session via the Connect-IPPSSession cmdlet from the Exchange Online V3 PowerShell module. Of course, you will also need permissions in order to run the cmdlet, but as the official documentation just links to the generic “permissions” article instead of answering this question, and due to the very limited support for RBAC-related cmdlets within the SCC module, we cannot give you a comprehensive set of roles that include the cmdlet. As per the Message center post, the Compliance administrator role group should be sufficient, so let’s assume that the Hold role contained within it should do the trick for any custom role assignments.

The Invoke-HoldRemovalAction cmdlet supports the following parameters:

  • Action – defines the operation you want to complete. Supported values include:
    • GetHolds – return a (sanitized) list of holds assigned to the mailbox or site in question.
    • RemoveHold – remove a hold from the specified mailbox or site.
    • GetHoldRemovals – return a list of hold removals performed previously via the cmdlet.
  • HoldId – the identity (GUID) of the hold you want to remove.
  • ExchangeLocation – the identity of the mailbox you want to act upon. Accepts any valid Exchange identifier.
  • SharePointLocation – the URL of the site you want to act upon.

The first thing you probably want to do is to get the ID of the hold(s) in question. There are several methods you can use to achieve that, including the one introduced via the –Action parameter of the cmdlet. For example, the following will list the set of holds currently affecting the “sharedtest” mailbox:

Invoke-HoldRemovalAction -ExchangeLocation sharedtest -Action GetHolds
98E9BABD09A04bcf8455A58C2AA74182

Here’s probably a good time to mention the types of holds supported by the cmdlet. Good old litigation holds are supported, as evident by the example above, which returns the GUID of the “standard” litigation hold policy. Yes, litigation holds also have a corresponding policy, as you can see by the following “hack”, which exposes the full Deserialized.Microsoft.Exchange.Data.Directory.Recipient.ADUser object:

Get-MailboxPermission sharedtest -ReadFromDomainController | select -Last 1 | select LitigationHoldEnabled,InPlaceHoldsRaw

LitigationHoldEnabled InPlaceHoldsRaw
--------------------- ---------------
True {98E9BABD09A04bcf8455A58C2AA74182}

A variation of the policy ID you might also run into is 98E9BABD09A04bcf8455A58C2AA74182Unlimit, which signals a litigation hold with no end duration. And for the sake of completeness, here’s how things look in “traditional” AD environment:

msexchuserholdpolicies=msExchUserHoldPolicies: 98E9BABD09A04bcf8455A58C2AA74182, 98E9BABD09A04bcf8455A58C2AA74182Unlimit

But I digress. Another type of hold supported by the Invoke-HoldRemovalAction cmdlet is of course the old In-place eDiscovery hold. Here’s an example of how this looks like:

Invoke-HoldRemovalAction -Action GetHolds -ExchangeLocation huku
UniH1796042d-88ce-4274-ab79-5bcb9bb0d2dc

New-style (standard) eDiscovery holds will also be reflected. However, the output of the Invoke-HoldRemovalAction cmdlet does not seem to include any Retention policies, even though you can readily see those reflected in the InPlaceHolds property, either on the mailbox or organizational level. Which in turn leads us to believe that such holds are not currently supported by the cmdlet, right?

Well that might not be entirely true, as label policies do appear. Another interesting fact is that the cmdlet seems to reflect on disabled policies as well. For example, if we examine the output of the GetHolds action for my personal ODFB drive, we see the following:

Invoke-HoldRemovalAction -Action GetHolds -SharePointLocation https://michev-my.sharepoint.com/personal/vasil_michev_info
20ff98d9-6bfc-4313-a020-d4e116625dc3

To find out what the policy does, we pass the ID to the Get-RetentionCompliancePolicy cmdlet:

Get-RetentionCompliancePolicy 20ff98d9-6bfc-4313-a020-d4e116625dc3

Name Workload Enabled Mode
---- -------- ------- ----
RetentionPolicyTest Exchange, SharePoint, OneDriveForBusiness, Skype, ModernGroup False Enforce

In fact, this is a retention policy that publishes an auto-apply retention tag. But since it is currently in a disabled state, I’m a bit puzzled as to why the cmdlet returns it in the output. We will see what happens when we try to use the cmdlet to remove it later on 🙂

 

OK, now we know how to retrieve the hold ID, so let’s see how the removal process works. We take the example with “HuKu” mailbox above, which does have an old-style In-place eDiscovery policy still stamped on it. To remove it, we use the following:

Invoke-HoldRemovalAction -ExchangeLocation huku -HoldId UniH1796042d-88ce-4274-ab79-5bcb9bb0d2dc -Action RemoveHold

Hold policy with Id '1796042d-88ce-4274-ab79-5bcb9bb0d2dc' is found. Please remove the mailbox/site directly from the policy or use -Force to remove the hold.

At this point, the Invoke-HoldRemovalAction cmdlet performs a check against the compliance policy store. If a matching (existing) policy is found, the cmdlet will throw an error and inform you that you should remove the mailbox from the policy scope via the standard tools/cmdlets. Alternatively, you can use the –Force parameter to “convince” the cmdlet to proceed with the removal:

Invoke-HoldRemovalAction -ExchangeLocation huku -HoldId UniH1796042d-88ce-4274-ab79-5bcb9bb0d2dc -Action RemoveHold -Force
WARNING: Hold 'UniH1796042d-88ce-4274-ab79-5bcb9bb0d2dc' was removed. It may take up to 240 minutes to take effect.

And voila! If you run the GetHolds action against the same mailbox now, you will no longer see the corresponding hold returned. Same if you check the value of the InPlaceHolds property via the Get-Mailbox cmdlet. Mission accomplished!

 

Now that we have at least one “removal” action performed via the cmdlet, we can also check the workings of the GetHoldRemovals action, which serves as sort of an audit log for the cmdlet execution. Do note that only “destructive” actions are logged, so you will not see any entries corresponding to the GetHolds action, nor any entries that errored out while trying to remove a hold. Another thing to note is that if no hold removal actions have been performed thus far, the cmdlet will return an error. Something Microsoft should probably address.

Invoke-HoldRemovalAction -Action GetHoldRemovals

TenantId : 923712ba-352a-4eda-bece-09d0684d0cfb
Identity : dc613f0c-2179-4ed7-99bb-428db9ed7800
Action : RemoveHold
User : vasil@michev.info
HoldId : UniH1796042d-88ce-4274-ab79-5bcb9bb0d2dc
ExchangeLocation : huku
SharePointLocation :
CreatedTime : 16/10/23 08:08:03
Sequence : 20231016080803.9486840Z

 

Of course, testing how the cmdlet behaves with other holds types is certainly an interesting topic to explore, so let’s see how it goes. We start by trying to remove a good old litigation hold:

Invoke-HoldRemovalAction -ExchangeLocation sharedtest -Action GetHolds
98E9BABD09A04bcf8455A58C2AA74182

Invoke-HoldRemovalAction -ExchangeLocation sharedtest -Action RemoveHold -HoldId 98E9BABD09A04bcf8455A58C2AA74182
WARNING: Hold '98E9BABD09A04bcf8455A58C2AA74182' was removed. It may take up to 240 minutes to take effect.

BAM! Rerunning the Invoke-HoldRemovalAction cmdlet against the same mailbox no longer returns the entry corresponding to the litigation hold, however checking the mailbox properties still show LitigationHoldEnabled set to True, even after waiting the prescribed 240 minutes. It did however reflect on the value of the InPlaceHoldsRaw property:

Get-MailboxPermission sharedtest -ReadFromDomainController | select -Last 1 | select LitigationHoldEnabled,InPlaceHoldsRaw

LitigationHoldEnabled InPlaceHoldsRaw
--------------------- ---------------
True {}

Next, lets see if we can remove a different type of hold, one enforced by a retention policy. If you remember from the examples above, such holds are not returned in the output of the GetHolds action, but lets see if we can remove them?

Invoke-HoldRemovalAction -ExchangeLocation gosho -Action GetHolds
UniH427b45e8-9abe-4973-a93a-b4299e5b78ec

Invoke-HoldRemovalAction -ExchangeLocation gosho -Action RemoveHold -HoldId ba508eee305343ba9a86e2c238af6344
PrintResultAndCheckForNextPage : Hold ba508eee305343ba9a86e2c238af6344 could not be found for gosho.

Get-RetentionCompliancePolicy ba508eee305343ba9a86e2c238af6344

Name Workload Enabled Mode
---- -------- ------- ----
TestRP Exchange, SharePoint, OneDriveForBusiness, Skype, ModernGroup True Enforce

Unfortunately, the cmdlet does not seem to support such scenarios, which was to be expected – a retention policy object readily exists and you can use the “standard” tools to remove the mailbox from it, if needed.

Lastly, lets also check what happens in the scenario of the retention policy that publishes an auto-apply label policy, the one with GUID of 20ff98d9-6bfc-4313-a020-d4e116625dc3:

Invoke-HoldRemovalAction -Action GetHolds -SharePointLocation https://michev-my.sharepoint.com/personal/pesho_michev_info
4afd33fd-2c3e-4ebd-b2d7-13995446520a
20ff98d9-6bfc-4313-a020-d4e116625dc3

Invoke-HoldRemovalAction -Action RemoveHold -HoldId 20ff98d9-6bfc-4313-a020-d4e116625dc3 -SharePointLocation https://michev-my.sharepoint.com/personal/pesho_michev_info
WARNING: Hold '20ff98d9-6bfc-4313-a020-d4e116625dc3' was removed. It may take up to 240 minutes to take effect.

So the removal was a success. This is probably also the expected behavior, since the policy GUID was returned in the output of the GetHolds action, unlike the scenario with a “standard” retention policy. Still, the disabled state of the policy should have played a role here, but alas, that’s the current behavior. Interestingly, looking at the DistributionDetail of the corresponding property shows no exclusions added to it, and the OneDriveLocation property still has a value of All… so unclear if the removal actually did work. If it did, it works by removing the corresponding hold ID out of the compliance hold storage, but does not seem to update any related objects and properties (including LitigationHoldEnabled in the case of mailboxes and the retention compliance policy object itself).

And that’s pretty much all there is to the cmdlet. It is a small, but useful addition to the compliance arsenal, which should help in certain situations. Some clarifications on the supported types of holds should help, as well as detailing the intended behavior when removing others. I’ll make sure yo update the article should I find more info.

UPDATE: After waiting for few days, I believe I can safely confirm that removing Litigation holds does not work. Not only the property value remains unchanged, but trying to delete an item (with Single item recovery disabled) results in preserving a copy within the Purges folder, as you’d except for any mailbox put on hold.

1 thought on “How to use the Invoke-HoldRemovalAction cmdlet to remove legacy and orphaned holds in Microsoft 365

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.