#Undelete-ADObject.ps1, based upon Undelete-User.ps1 v 3 #Active Directory object restore with menu. #Default restore path is Users container for users. #Alan Kaplan dot VA dot gov, alan at akaplan dot com #10-27-15 v3 fixed computer samname, added bail if object exists #6-22-16 v4 rewrote search and match for names. #8-25-17 v4.5 major rewrite #requires -version 3 #requires -module ActiveDirectory $strVer = "4.5" cls Add-Type -assemblyname Microsoft.visualBasic $ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path #### Optional Edits #### #Default paths $logfile = "$env:userProfile\Desktop\"+'RecoveredAccounts_'+$(Get-Date).ToString("yyyyMMdd_HHmm")+'.xls' #$logfile = "$scriptpath\"+'RecoveredAccounts_'+$(Get-Date).ToString("yyyyMMdd_HHmm")+'.xls' $btest = $False $Error.Clear() #Add assembly for VB message and input boxes Add-Type -assemblyname Microsoft.VisualBasic Function Bail () { "Done." if ($form1){ [System.Environment]::Exit(0) | out-null }ELSE{ Exit } } Function Close-Form{ #close form $form1.Close() } function GenerateForm { ######################################################################## # Code Generated By: SAPIEN Technologies PrimalForms (Community Edition) v1.0.10.0 # Generated On: 6/12/2015 4:20 PM # Generated By: VHASBYKaplaA ######################################################################## #region Import the Assemblies [reflection.assembly]::loadwithpartialname("System.Windows.Forms") | Out-Null [reflection.assembly]::loadwithpartialname("System.Drawing") | Out-Null #endregion #region Generated Form Objects $form1 = New-Object System.Windows.Forms.Form $CancelBtn = New-Object System.Windows.Forms.Button $OkayBtn = New-Object System.Windows.Forms.Button $label1 = New-Object System.Windows.Forms.Label $checkedListBox1 = New-Object System.Windows.Forms.CheckedListBox $InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState #endregion Generated Form Objects #---------------------------------------------- # Event Script Blocks #---------------------------------------------- $CancelBtn_OnClick= { write-warning "Cancelled" Bail } $handler_checkedListBox1_ItemCheck= { if ($_.NewValue -eq 'Checked') { $Count = $checkedlistbox1.Items.Count for ($index = 0; $index -lt $Count; ++$index) { if($_.Index -ne $index){ $checkedlistbox1.SetItemChecked($index, $false); } } } } $OkayBtn_OnClick= { if ($checkedListBox1.CheckedIndices.count -eq 0){ write-warning "Nothing selected, cancelling" Bail }ELSE{ [string]$script:objectType = $checkedListBox1.CheckedItems Close-Form } } $OnLoadForm_StateCorrection= {#Correct the initial state of the form to prevent the .Net maximized form issue $form1.WindowState = $InitialFormWindowState } #---------------------------------------------- #region Generated Form Code $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 236 $System_Drawing_Size.Width = 275 $form1.ClientSize = $System_Drawing_Size $form1.DataBindings.DefaultDataSourceUpdateMode = 0 $form1.Name = "form1" $form1.StartPosition = 1 $form1.Text = "Version $strVer. Choose Account Type" $form1.add_Load($handler_form1_Load) $CancelBtn.BackColor = [System.Drawing.Color]::FromArgb(255,139,0,0) $CancelBtn.DataBindings.DefaultDataSourceUpdateMode = 0 $CancelBtn.ForeColor = [System.Drawing.Color]::FromArgb(255,255,255,255) $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 148 $System_Drawing_Point.Y = 194 $CancelBtn.Location = $System_Drawing_Point $CancelBtn.Name = "CancelBtn" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 30 $System_Drawing_Size.Width = 60 $CancelBtn.Size = $System_Drawing_Size $CancelBtn.TabIndex = 3 $CancelBtn.Text = "Cancel" $CancelBtn.UseVisualStyleBackColor = $False $CancelBtn.add_Click($CancelBtn_OnClick) $form1.Controls.Add($CancelBtn) $OkayBtn.BackColor = [System.Drawing.Color]::FromArgb(255,0,192,0) $OkayBtn.DataBindings.DefaultDataSourceUpdateMode = 0 $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 66 $System_Drawing_Point.Y = 194 $OkayBtn.Location = $System_Drawing_Point $OkayBtn.Name = "OkayBtn" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 30 $System_Drawing_Size.Width = 60 $OkayBtn.Size = $System_Drawing_Size $OkayBtn.TabIndex = 2 $OkayBtn.Text = "Okay" $OkayBtn.UseVisualStyleBackColor = $False $OkayBtn.add_Click($OkayBtn_OnClick) $form1.Controls.Add($OkayBtn) $label1.DataBindings.DefaultDataSourceUpdateMode = 0 $label1.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",13,0,3,1) $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 7 $System_Drawing_Point.Y = 26 $label1.Location = $System_Drawing_Point $label1.Name = "label1" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 31 $System_Drawing_Size.Width = 260 $label1.Size = $System_Drawing_Size $label1.TabIndex = 1 $label1.Text = "Select Object Type to Restore" $label1.TextAlign = 32 $label1.add_Click($handler_label1_Click) $form1.Controls.Add($label1) $checkedListBox1.CheckOnClick = $True $checkedListBox1.DataBindings.DefaultDataSourceUpdateMode = 0 $checkedListBox1.FormattingEnabled = $True $checkedListBox1.Items.Add("user")|Out-Null $checkedListBox1.Items.Add("computer")|Out-Null $checkedListBox1.Items.Add("group")|Out-Null $checkedListBox1.Items.Add("printQueue")|Out-Null $checkedListBox1.Items.Add("contact")|Out-Null $System_Drawing_Point = New-Object System.Drawing.Point $System_Drawing_Point.X = 60 $System_Drawing_Point.Y = 60 $checkedListBox1.Location = $System_Drawing_Point $checkedListBox1.Name = "checkedListBox1" $System_Drawing_Size = New-Object System.Drawing.Size $System_Drawing_Size.Height = 109 $System_Drawing_Size.Width = 154 $checkedListBox1.Size = $System_Drawing_Size $checkedListBox1.TabIndex = 0 $checkedListBox1.add_ItemCheck($handler_checkedListBox1_ItemCheck) $form1.Controls.Add($checkedListBox1) #endregion Generated Form Code #Save the initial state of the form $InitialFormWindowState = $form1.WindowState #Init the OnLoad event to correct the initial state of the form $form1.add_Load($OnLoadForm_StateCorrection) #Show the Form $form1.ShowDialog()| Out-Null } #End Function $deletedObjs = $DeletedObj = $null #Get current domain for this computer $myDomain = $((get-addomain -current LocalComputer).dnsroot).toString() #message for Welcome menu $msg = "This script provides a graphical interface to the AD Recycle Bin in Server 2008 and later. "+` "You must already have enabled the Recycle Bin and have sufficent domain rights for this to work."+` "`fAlthough you can choose domains, note that it is faster and more reliable to run the script from "+` "a computer in the same domain as the accounts." #want line above without line break $msg= $msg.Replace("`n",$null) $msg +="`n`n1) Commit Changes to AD`n2) Run in Test Mode`n0) Quit" [int]$iMenuChoice= [Microsoft.VisualBasic.Interaction]::InputBox($msg,"Undelete-ADObject v $strver Welcome",2) Switch( $iMenuChoice ){ 0 {Bail} 1 {$bTest = $False ; Break} 2 {$bTest = $True ;Break} } #Get search domain $strdomain = [Microsoft.VisualBasic.Interaction]::InputBox("Search for deleted objects in what AD domain", "Domain Name", $myDomain) if ($strdomain.Length -eq 0){Bail} $title= "Log File" $logfile = [Microsoft.VisualBasic.Interaction]::InputBox("Enter path to tab delimited log. Valid extensions are `'.xls`' and `'.txt`'", $title, $logfile) if ($logfile.length -eq 0){Exit} #Call the Function GenerateForm #If you are searching another domain, use a global catalog server #This may help with timeout issues for external domains #If not, run from a computer in the domain from which you are recovering deleted accounts. if ($myDomain -inotlike $strdomain ) { #Get GC $DomainContext = new-object System.directoryServices.ActiveDirectory.DirectoryContext("Domain",$strDomain) $ADSIDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($DomainContext) $strServer = $ADSIDomain.PdcRoleOwner.Name #add GC Port to first GC in array $strServer = "$strServer`:3268" }Else { $strServer = $env:LogonServer.Replace("\\","") + ".$myDomain" } #Default is search for allnames $strSamName = "*" $msg = "You may limit the search to objects by NT style SamAccountName (if known), or return all deleted objects. You can use all or just the beginning of the SamAccount Name. Do not include wildcards. Choosing `"No`" returns all deleted $objectType objects. Do you want to search?" $retval = [System.Windows.Forms.MessageBox]::Show($msg,"Search by Name", "YesNoCancel” , "Question” , "Button2") switch ($retval) { Cancel {Bail} Yes { $bSearch = $true #prompt for name $myName= ((Get-ADUser -server $env:USERDNSDOMAIN -identity $env:USERNAME).SamAccountName).tostring().ToLower() $msg = "The search is not case senstive. Do not include domain, for example: $myName. `n`nSearch for this SAMAccountName:" $strSamName = [Microsoft.VisualBasic.Interaction]::InputBox($msg,"SAMAccountName") } Default { $bSearch = $False } } #Find the maximum number of days the forest permits for deleted objects $rootforestdomain = (Get-ADRootDSE).RootDomainNamingContext $MaxAge =Get-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,CN=Configuration,$rootforestdomain"` -Partition "CN=Configuration,$rootforestdomain" -Properties msDS-DeletedObjectLifetime | select -ExpandProperty msDS-DeletedObjectLifetime #Ask go back how far? $msg = "The domain `"$strDomain`" keeps deleted objects for $maxage days. Limiting the days speeds your search. `nLook for objects going back how many days?" [int]$searchDays= [Microsoft.VisualBasic.Interaction]::InputBox($msg,"Search Past Number of Days",$MaxAge) if ($searchDays -eq 0){Bail } #subtract seach days to get starting date $changedDate = (Get-Date).AddDays(-$searchDays) #get object representing $strdomain -- which really is a string $oDomain = Get-ADDomain -Identity $strDomain Write-Progress "Getting AD location for deleted items in $strDomain" $SearchBase = $oDomain.DeletedObjectsContainer write-Progress "Searching for deleted $objectType objects in $strDomain going back $searchdays days. (Users without sufficent rights may get no results from this search`)" $Error.Clear() $filter = "(objectclass -eq `'$objectType`')" if ($searchDays -ne $MaxAge) { $filter += " -and (modified -ge `'$ChangedDate`')" } if ($bSearch) { if ($objectType -eq 'computer') {$strSamName+='*'} $filter += " -and (SamAccountName -like '$StrSamName')" } #splat $DeletedQueryParams =@{ SearchBase=$Searchbase Server=$strServer Filter = $filter resultpagesize=500 includeDeletedObjects=$True properties="Description,SamAccountName,userprincipalname,DistinguishedName,WhenChanged" -Split(",") } try { Write-verbose "Running AD Query" $deletedObjs = Get-ADObject @DeletedQueryParams -erroraction Stop Write-Progress "Query complete, formatting." } Catch { Cls if (($Error[0].Exception.HResult -eq -2146233087) -and ($oDomain.DNSRoot -notmatch $myDomain)){ $errorMessage = $errorMessage + "Please try to rerun in "+ [string]$($oDomain).DNSRoot }Else{ $errorMessage = $Error[0].exception.message } Write-warning $Errormessage bail } cls Write-verbose "Selecting for out-gridview" #Use select with expressions to show nicer labels $deletedObjs = $deletedObjs | Where {$_.ObjectClass -eq $objectType} | Select @{Name="Name";Expression={$_.name.Split("`n")[0]}},` Description, @{Name="NT Account";Expression={$_."SamAccountName"}}, UserPrincipalName,` @{Name="Date Deleted";Expression={$_."WhenChanged"}}, DistinguishedName | Sort-Object -Property Name #Bail after notification if zero objects returned if ($deletedObjs.count -eq 0){ $msg= "No $ObjectType found to undelete in $strDomain going back $searchdays days with filter `"SamAccountName like $strSamName`"." $retval = [System.Windows.Forms.MessageBox]::Show($msg,"No $ObjectType Objects Found", "ok” , "Exclamation” , "Button1") Bail } $msg = "Select Account(s) to Restore, and click [OK]" if ($bTest -eq $true) {$msg = "[Test Mode - Restore is Simulated] $msg"} Write-Progress "Account information colllected" -completed #Send result list to Out-Gridview. PassThru allows selection to pass through to restore $aRestore = $deletedObjs| Out-GridView -Title $msg -Passthru @(foreach ($DeletedObj in $aRestore){ $Msg = 'Success' #Test if already exits if ($DeletedObj.'NT Account'){ $SamAccountName = $(($DeletedObj).'NT Account').ToString() } ELSE { $strSamName = '' } $Retval = Get-ADObject -Server $strServer -Filter {(SamAccountName -eq $SamAccountName) -and (objectclass -eq $objectType) } if ($retval -ne $null){ $msg = "An object with SamAccountName `"$SamAccountName`" already exists! Skipping restore" #Write-Warning $msg }ELSE{ try { $Restored = Restore-ADObject -Server $strDomain -identity $DeletedObj.DistinguishedName -whatif:$bTest -PassThru -erroraction Stop }Catch{ $msg = $error[0].exception.message } } if ($bTest) { $NewDN ='' $msg = 'Test only' } ELSE { $newDN = $Restored.DistinguishedName } $deletedObj | add-member -NotePropertyName "RestoredDN" -NotePropertyValue $NewDN -PassThru | add-member -NotePropertyName "Result" -NotePropertyValue $msg -PassThru }) | Export-Csv -NoTypeInformation $logfile Write-host "Done. Log file is $logfile"