Inspecting Deleted Objects before Restore
Accidental deletions can happen in Active Directory in many ways. An administrator can end up deleting a single user object unintentionally or fat finger an entire tree of OUs. A rogue script could end up deleting multiple objects at multiple locations in the AD hierarchy. In Active Directory, when deleted objects are moved to the deleted objects container, they lose their hierarchy (because the DN is mangled to ensure uniqueness).
Once accidental deletions happen it's important to analyze the impact carefully, before devising a restore strategy. Things which are required to analyze the accidental deletion are
- What got deleted?
- When did the deletion happen?
- Where did the deletion happen? (Which Domain Controller)
Once this analysis phase is done, and the objects to restore are identified … you may choose your favorite restore method.
The traditional recovery consist of hoping that you have a recent system state backup and performing an auth restore of the deleted objects … If you're on Windows Server 2008 R2 forest functional level … the Recycle Bin feature of Windows Server 2008 R2 AD has made restoring a single object very simple. Once the new feature is turned on, all attributes are now preserved on an object, when it's deleted - and once the object is restored using the Restore-ADObject cmdlet - the object is restored in its entirety. So, if a User is accidently deleted and restored using Restore-ADObject - all forward links and backlinks to that user object are restored too - Group Memberships, Manager, ManagedObjects, DirectReports etc. along with all the non-linked attributes.
This blog post focuses on the analysis phase: Identifying what to restore.
The need is to find ways in which you can analyze the accidental deletion disaster. I will take the following three categorization approaches:
- If an accidental deletion deletes multiple objects, usually, all of those objects are deleted within a short span of time. It may be an amateur consultant ending up deleting ad objects while installing his application, or a rogue script running and deleting objects here and there, or an admin fat fingering a tree of OUs or even a malicious action - most disastrous deletions happen in a span of seconds/minutes.
- If an accidental deletion deletes multiple objects, usually, all of those objects are deleted on the same domain controller.
- AD data is hierarchical, GUI is hierarchical - non-single object accidental deletions also usually delete a hierarchy.
In the absence of a good GUI for inspecting the recycle bin contents, here's a script that can inspect the contents of your deleted objects container, and give you info on the 3 categorizations above.
The script reads the deleted objects container, to construct a hierarchy of all deleted containers (organizational units and containers). Further it reads the replication metadata of these containers to ascertain when and where the object was deleted.
See in the figure below. 5 trees of OUs have been deleted. 4 of those trees were under the domain NC and one was under the users container. For each deleted OU, the script shows accurate when deleted (not the value of whenChanged attribute) and where the deletion happened.
I can now Pipeline the output of this command to Where-Object and filter out the view per criteria of my choosing - per where deleted and/or when deleted
I can also sort the output of the command on WhenDeleted - it does mess up the tree view though …
Tree Restoration
Restoring deleted trees isn't as easy as single object restores. When objects are deleted they are moved to a flat hierarchy in the Deleted Objects Container. The RDN of the object is mangled with its GUID and so a deleted tree loses its hierarchy. The Restore-ADTree script makes it easier to restore an entire tree. You can pass to the script the DN or the GUID of the root of the tree and it would restore all objects under that root recursively. You can use Restore-ADTree on deleted OUs if *all* the deleted children of that OU (presently residing in the deleted objects container) should be restored.
When used with the -LeafObjectInfo parameter, this script (Get-ADDeletedContainers) also shows info about the subordinates of each container. The 'subordinates' field shows the number of deleted objects which are immediate subordinates of the deleted OU, and the maximum gap of time between the when deleted time of the subordinates. So, if an fresh OU had 5 user objects, one was deleted 3 days back (a valid deletion) and today the OU and 4 of its children got deleted accidently - the subordinates field of the OU would show 5 (3.00:00:00).
LeafObjectsInfo option will take longer to execute. This option will help you analyze whether or not to use the Restore-ADTree script on deleted OUs. If the subordinate time of an OU is more than a few minutes - you should analyze if all the deleted subordinates are candidates for restore or not.
So, before using Restore-ADObject or Restore-ADTree or even if you are planning an auth restore (if recycle bin feature isn't available) - you can use this script to better view and analyze the contents of your deleted objects container better.
Usage: Map an AD PowerShell drive to your AD or cd into the default AD: drive and use the script per the following samples. RecycleBin feature needn't be turned on for this script to work
- PS CONTOSO:\> Get-ADDeletedContainers.ps1 | ft DisplayName,WhenDeleted,WhereDeleted
- PS CONTOSO:\> Get-ADDeletedContainers.ps1 | ft DisplayName,ObjectClass,ObjectGUID,WhenDeleted,WhereDeleted
- PS CONTOSO:\> Get-ADDeletedContainers.ps1 -LeafObjectsInfo | ft DisplayName,Subordinates
- PS CONTOSO:\> Get-ADDeletedContainers.ps1 | where {$_.WhenDeleted -gt [DateTime]::Parse("5/25/2009 9:40:00 AM")}|ft DisplayName,WhenDeleted,WhereDeleted
- PS CONTOSO:\> Get-ADDeletedContainers.ps1 | where {$_.WhereDeleted -like "CONTOSO-DC2*"}|ft DisplayName,WhenDeleted,WhereDeleted
- PS CONTOSO:\> Get-ADDeletedContainers.ps1 | where {$_.WhenDeleted -gt [DateTime]::Parse("5/25/2009") -and $_.WhereDeleted -like "CONTOSO-DC2*"}|ft DisplayName,WhenDeleted,WhereDeleted
ps: Protect your OUs against accidental deletion to avoid future accidental deletions. To learn how - search for the phrase "Protect an Organizational Unit from Accidental Deletion"
enjoy!
Dushyant Gill
Program Manager, Directory Services
Get-ADDeletedContainers ps1.txt
Comments
Anonymous
June 07, 2009
PingBack from http://greenteafatburner.info/story.php?id=2530Anonymous
July 13, 2009
Great! Maybe bundle as cmdlets with 2008-R2 by default as part of the AD module in PowerShell?Anonymous
April 21, 2011
Hmmm...you state you should run it from within the AD drive, but your examples state it as otherwise.If I have the said script saved under C:PS , how would I go about running it from within the AD drive ?Anonymous
June 20, 2011
I recommend downloading NetWrix Active Directory Change Reporter with Active Directory Object Restore Wizard. The tool will streamline the process of identifying what got deleted, when the deletion happened, and where it happened. The tool will send automated reports that tell you exactly who deleted what, when and where, and will allow you to very easily roll back those changes without even rebooting. The solution goes beyond standard tombstone capabilities, allowing you to revert modified and incorrectly added objects. Download AD Change Reporter at www.netwrix.com/active_directory_change_reporting_freeware.htmlThanks,Stephen SchimmelProduct ManagerNetWrix Corporationwww.netwrix.comAnonymous
June 21, 2013
This script throws the following error in several of the environments I have tried in:PS C:Temp> .Get-ADDeletedContainers.ps1 -LeafObjectsInfo | ft DisplayName,SubordinatesCannot convert argument "1", with value: "", for "Replace" to type "System.Char": "Cannot convert value "" to type "System.Char". Error: "String must be exactly one character long.""At C:TempGet-ADDeletedContainers.ps1:25 char:53 $xmlObj = [xml]("<root>"+$xmlMetadataString.Replace <<<< ([char]0,"")+"</root>") + CategoryInfo : NotSpecified: (:) [], MethodException + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgumentCannot convert argument "1", with value: "", for "Replace" to type "System.Char": "Cannot convert value "" to type "System.Char". Error: "String must be exactly one character long.""At C:TempGet-ADDeletedContainers.ps1:25 char:53 $xmlObj = [xml]("<root>"+$xmlMetadataString.Replace <<<< ([char]0,"")+"</root>") + CategoryInfo : NotSpecified: (:) [], MethodException + FullyQualifiedErrorId : MethodArgumentConversionInvalidCastArgumentCannot find an overload for "op_Subtraction" and the argument count: "2".At C:TempGet-ADDeletedContainers.ps1:192 char:82 $maxGapBetweenWhenDeletedOfDeletedImmidiateSubordinates = $latestWhenDeleted - <<<< $oldestWhenDeleted + CategoryInfo : NotSpecified: (:) [], MethodException + FullyQualifiedErrorId : MethodCountCouldNotFindBest<snip>DisplayName : CN=Policies,CN=System,DC=Reskit,DC=comName :ObjectClass :ObjectGUID :WhenDeleted :WhereDeleted :Subordinates :DisplayName : +-{260AF85C-0595-46DA-83FE-C03C9D562B02}Name : {260AF85C-0595-46DA-83FE-C03C9D562B02}ObjectClass : groupPolicyContainerObjectGUID : 1e40326a-4751-44f9-a16a-5b15e033b646WhenDeleted :WhereDeleted :Subordinates :DisplayName : ¦ +-MachineName : MachineObjectClass : containerObjectGUID : c9c42486-7791-4456-88c4-90eaf74493b5WhenDeleted :WhereDeleted :Subordinates :DisplayName : ¦ +-UserName : UserObjectClass : containerObjectGUID : 51df14e0-de62-45f7-8ec0-9a73a0a428d6WhenDeleted :WhereDeleted :Subordinates :DisplayName : +-{3BBE9E82-C898-4399-B061-33AD7F2713F2}Name : {3BBE9E82-C898-4399-B061-33AD7F2713F2}ObjectClass : groupPolicyContainerObjectGUID : 93b45db6-713d-4fc9-baca-cb86f948a41f<snip>Anonymous
June 21, 2013
The comment has been removedAnonymous
January 23, 2014
Thanks for sharing. Cool stuffAnonymous
March 27, 2014
i received this messages when i ran the scriptPS C:> .Get-ADDeletedContainers.ps1 -LeafObjectsInfo | ft DisplayName,SubordinatesMissing ')' in method call.At C:Get-ADDeletedContainers.ps1:25 char:65 $xmlObj = [xml]("<root>"+$xmlMetadataString.Replace([char]0,"" <<<< "")+"</root>") + CategoryInfo : ParserError: (CloseParenToken:TokenId) [], ParseException + FullyQualifiedErrorId : MissingEndParenthesisInMethodCallAnonymous
May 07, 2014
dears i can't get the script working please provide clear steps if applicable