#Module name: O365Undo #Author: Jos Lieben (OGD) #Date: 08-05-2017 #Script help: www.lieben.nu #Purpose: reverses all renames/changes/moves/copies a user has done on Sharepoint Online / Onedrive For Business / Office 365 Groups after a given (infection) date #Requirements: #Sharepoint Client components: https://www.microsoft.com/en-us/download/details.aspx?id=42038 #Unified Audit log has to be enabled before infection date: https://support.office.com/en-us/article/Search-the-audit-log-in-the-Office-365-Security-Compliance-Center-0d4d0f35-390b-4518-800e-0c7ec95e946c #If unified audit log was not enabled, you can only use V1 of this script to restore previous file versions #Powershell 4 #.NET 4.5 #run “Set-Executionpolicy Unrestricted” in an elevated powershell window #Windows 7+ or Windows Server 2008+ # #INSTRUCTIONS: #1. disable the infected user's account #2. wait 15-30 minutes (so the Universal Audit Log is up to date, it lags a little behind usually) #3. run this script ######## #CHANGELOG #V0.1: restore previous versions of all files in a given document library #V0.2: roll back file renames and file copies based on the universal audit log for a specific user anywhere in the tenant #V0.3: roll back file edits/uploads to the version before infection if such a version is available #V0.4: compatible with files in Office 365 Groups #V0.5: compatible with latest change in logged data in UAL for Personal Sites Param( [Parameter(Mandatory=$true)][String]$affectedUserLogin, #login of the user that was affected by a CryptoLocker [Parameter(Mandatory=$true)][DateTime]$infectionDateTime, #datetime of the moment the user was infected and from which changes should be rolled back, eg: 12-15-2016 13:00:00 [Parameter(Mandatory=$true)][String]$login, #login of the user OR of an admin with permissions on the user's files [Parameter(Mandatory=$true)][String]$password #password of the above account ) $logfile = ($env:APPDATA + "\O365Undo.log") #Logfile to log to $scriptName = "O365Undo" $scriptVersion = "v0.5" [DateTime]$infectionDateTime = $infectionDateTime.ToUniversalTime() function log{ param ( [String]$text, [Switch]$fout, [Switch]$append ) if($append){$text = "$text, $($Error[0])"} ac $logfile $text if($fout){ Write-Host $text -ForegroundColor Red }else{ Write-Host $text -ForegroundColor Green } } function endScript{ Read-Host 'Script has finished, press Enter to exit...' | Out-Null Exit } log -text "-----$(Get-Date) $scriptName $scriptVersion - $($env:USERNAME) on $($env:COMPUTERNAME) starting-----" #Load sharepoint libraries try{ [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client") | Out-Null [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime") | Out-Null [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.UserProfiles") | Out-Null log -text "Sharepoint Client Libraries: OK" } catch { $OUTPUT= [System.Windows.Forms.MessageBox]::Show("Please install the Sharepoint 2013 Client Components from https://www.microsoft.com/en-us/download/details.aspx?id=42038", "$scriptName $scriptVersion" , 0) log -text "Failed to load Sharepoint Extensions, please install the Sharepoint 2013 Client Components from https://www.microsoft.com/en-us/download/details.aspx?id=42038" -fout -append endScript } #connect to ExchangeOnline try{ $secpasswd = ConvertTo-SecureString $password -AsPlainText -Force $Credentials = New-Object System.Management.Automation.PSCredential ($login, $secpasswd) $Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credentials -Authentication Basic -AllowRedirection $res = Import-PSSession $Session -AllowClobber -DisableNameChecking -WarningAction SilentlyContinue log -text "Connected to Exchange Online, retrieving Unified Audit Logs..." }catch{ log -text "Failed to connect to Exchange Online" -fout -append endScript } #retrieve full unified log for renames done by this user [DateTime]$currentDate = Get-Date $loggedReversibles = $Null $session = "O365AntiCryptoLocker$currentDate" $operations = @("FileRenamed","FileMoved","FileModified","FileRestored","FileUploaded") $resultCount = -1 while($loggedReversibles.Count -lt $resultCount -or $loggedReversibles -eq $Null){ try{ $entries = $Null $entries = Search-UnifiedAuditLog -EndDate $currentDate -StartDate $infectionDateTime -UserIds $affectedUserLogin -SessionId $session -SessionCommand ReturnNextPreviewPage -Operations $operations if($entries){ $loggedReversibles += $entries $resultCount = $loggedReversibles[0].ResultCount }else{ break } }catch{ log -text "There was a problem with the Unified Audit Log" -fout -append break } } #check if we've found any if($loggedReversibles){ log -text "Found $($loggedReversibles[0].ResultCount) entries to process" $x = Read-Host "Press any key to continue" }else{ log -text "No entries found in the Unified Audit Log, cannot continue" -fout endScript } #close Exchange connection Get-PSSession | Remove-PSSession #SPO connect function function connectToSPOSite{ param( [Parameter(Mandatory=$true)] [String]$siteURL ) if($client -and $client.Url.Replace("/","") -eq $siteURL.Replace("/","")){ return $True } #Connect to Site $script:client = New-Object Microsoft.SharePoint.Client.ClientContext($siteURL) #build Credential Object $secpasswd = ConvertTo-SecureString $password -AsPlainText -Force $script:client.Credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($login,$secpasswd) #Connect try{ $script:client.Load($script:client.Web) $script:client.ExecuteQuery() return $True }catch{ return $False } } function reverse_RenameOrCopy { Param( [Parameter(Mandatory=$true)] [String]$oldName, [Parameter(Mandatory=$true)] [String]$oldUrl, [Parameter(Mandatory=$true)] [String]$newName, [Parameter(Mandatory=$true)] [String]$newUrl, [Parameter(Mandatory=$true)] [String]$siteUrl ) if((connectToSPOSite -siteURL $siteUrl) -eq $True){ #process rename try{ if($newUrl.startsWith("sites") -or $newUrl.startsWith("personal")){ $fileUrl = "/$($newUrl)" $newFileUrl = "/$($oldUrl)" #fileUrl is already correct }else{ $fileUrl = Join-Path -Path $client.Web.ServerRelativeUrl -ChildPath $newUrl $fileUrl = Join-Path -Path $fileUrl -ChildPath $newName $fileUrl = $fileUrl.Replace("\","/") $newFileUrl = Join-Path -Path $client.Web.ServerRelativeUrl -ChildPath $oldUrl $newFileUrl = Join-Path -Path $newFileUrl -ChildPath $oldName $newFileUrl = $newFileUrl.Replace("\","/") } $file = $client.Web.GetFileByServerRelativeUrl($fileUrl) $client.Load($file) $client.ExecuteQuery() }catch{ return $False } try{ $file.MoveTo($newFileUrl,[Microsoft.SharePoint.Client.MoveOperations]::Overwrite) $client.ExecuteQuery() return $True }catch{ return $False } }else{ log -text "Failed to connect to $siteUrl" -fout -append return $False } } function restore_PreviousVersion{ Param( [Parameter(Mandatory=$true)]$fileUrl, [Parameter(Mandatory=$true)]$fileName, [Parameter(Mandatory=$true)]$siteUrl, [Parameter(Mandatory=$true)]$infectionDateTime ) if((connectToSPOSite -siteURL $siteUrl) -eq $True){ #process restore try{ if($fileUrl.startsWith("sites") -or $fileUrl.startsWith("personal")){ $fileUrl = "/$($fileUrl)" #fileUrl is already correct }else{ $fileUrl = Join-Path -Path $client.Web.ServerRelativeUrl -ChildPath $fileUrl $fileUrl = Join-Path -Path $fileUrl -ChildPath $fileName } $fileUrl = $fileUrl.Replace("\","/") $file = $client.Web.GetFileByServerRelativeUrl($fileUrl) $client.Load($file) $client.ExecuteQuery() #skip folders, return $False and log error if($file.FileSystemObjectType -eq "folder"){ Throw "WARNING, skipping $($file["FileRef"]) because it is a directory" } #load previous versions of the file $fileVersions = $file.Versions $client.Load($fileVersions) $client.ExecuteQuery() #process previous versions if($fileVersions.Count -gt 0){ #loop over versions to get the latest [DateTime]$latestFileVersionDateTime = "01-01-1900 01:01:01" [String]$latestFileVersionLabel = "" foreach($fileVersion in $fileVersions){ $fileCreationTime = $fileVersion.Created.ToUniversalTime() if($fileCreationTime -gt $latestFileVersionDateTime -and $fileVersion.IsCurrentVersion -eq $False -and $fileCreationTime -lt $infectionDateTime){ $latestFileVersionDateTime = $fileCreationTime $latestFileVersionLabel = $fileVersion.VersionLabel } } #restore latest version available if($latestFileVersionLabel){ try{ $fileVersions.RestoreByLabel($latestFileVersionLabel) $client.ExecuteQuery() log -text "Restored $latestFileVersionLabel ($latestFileVersionDateTime UTC) of $($file["FileRef"])" Return $True }catch{ Throw "Failed to restore $latestFileVersionLabel ($latestFileVersionDateTime UTC) of $($file["FileRef"]) $($Error[0])" } }else{ Throw "No usable previous version found from before infection time" } }else{ Throw "No previous versions available, it is likely unaffected" } return $True }catch{ return $False } }else{ log -text "Failed to connect to $siteUrl" -fout -append return $False } } #loop over all entries we've found and call their respective function in reverse chronological order to undo compatible entry types $changedItemsList = @() foreach($reversible in $loggedReversibles){ $auditData = $reversible.AuditData | ConvertFrom-Json $res = $Null #per type operation roepen we een bijbehorende functie aan if($reversible.Operations -eq "FileRenamed" -or $reversible.Operations -eq "FileCopied"){ $res = reverse_RenameOrCopy -oldName $auditData.SourceFileName -oldUrl $auditData.SourceRelativeUrl -newName $auditData.DestinationFileName -newUrl $auditData.DestinationRelativeUrl -siteUrl $auditData.SiteUrl if($res){ log -text "Rename or Copy of $($auditData.DestinationFileName) to $($auditData.SourceFileName) succeeded" }else{ log -text "Rename or Copy of $($auditData.DestinationFileName) to $($auditData.SourceFileName) failed" -fout -append } } #add to unique/touched items list if this item is not yet on it, replace if it is $i = 0 $add = $True foreach($changedItem in $changedItemsList){ $changedItemAuditData = $changedItem.AuditData | ConvertFrom-Json if($changedItemAuditData.ListItemUniqueId -eq $auditData.ListItemUniqueId){ $changedItemsList[$i] = $reversible $add = $False } $i++ } if($add){ $changedItemsList += $reversible } } #We're done reversing things we know we can reverse, now we'll restore previous versions of all files that were touched according to the log foreach($changedItem in $changedItemsList){ $auditData = $changedItem.AuditData | ConvertFrom-Json $res = $Null $res = restore_PreviousVersion -fileUrl $auditData.SourceRelativeUrl -fileName $auditData.SourceFileName -siteUrl $auditData.SiteUrl -infectionDateTime $infectionDateTime if(!$res){ log -text "Restore of $($auditData.ObjectId) failed" -fout -append } } log -text "Finished processing Universal Audit Log Entries" endScript