Snapshot.psm1

#region Get-ActiveDirectorySnapshot
Function Get-ActiveDirectorySnapshot
{
    <#
    .Synopsis
       Get the Active Directory snapshots.
    .DESCRIPTION
       Get the Active Directory snapshots on a domain controller.
    .EXAMPLE
        PS C:\> Get-ActiveDirectorySnapshot
 
        Identity Date SetID
        -------- ---- -----
        {B22E56F9-C4C5-49C2-A4EF-67A40DE21132} 7/9/2016 9:08:54 μμ {34987893-9D63-4955-968B-3000002789F5}
        {1DF443D2-9F27-41C4-9E32-950F557E9826} 7/9/2016 8:39:35 μμ {6D3802A3-BD22-4732-AE27-5FEC25EE1E01}
    #>


    [cmdletBinding()]

    Param
    (
        # The identity of the snapshot
        [Parameter(Mandatory=$false, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]
        [string]$Identity,

        # The Active Directory Domain Controller
        [Parameter(Mandatory=$false, 
                   Position=1)]
        [string[]]$Server = $env:COMPUTERNAME
    )

    Begin {}

    Process
    {
        # Get the snapshots
        foreach($s in $Server)
        {
            $snapshots = Get-WmiObject Win32_ShadowCopy -ComputerName $Server |
                            Select-Object @{Name="Identity"; Expression={$_.Id}}, `
                                          @{Name="Date"; Expression={$_.ConvertToDateTime($_.InstallDate)}}, `
                                          @{Name="SetID"; Expression={$_.SetID}}, `
                                          @{Name="Mounted"; Expression={$_.ExposedLocally}}, `
                                          @{Name="MountPoint"; Expression={$_.ExposedName}}, `
                                          @{Name="Server"; Expression={$s}} |
                                Sort-Object -Property Install_Date -Descending

            # Return particular snapshot (if requested)
            if($Identity)
            {
                $snapshots |
                    Where-Object {$_.Identity -eq $Identity}
            }
            else
            {
                $snapshots
            }
        }
    }

    End {}
}
#endregion

#region New-ActiveDirectorySnapshot
Function New-ActiveDirectorySnapshot
{
    <#
    .Synopsis
       Create an Active Directory snapshot.
    .DESCRIPTION
       Create an Active Directory snapshot.
    .EXAMPLE
        PS C:\> New-ActiveDirectorySnapshot
 
        Identity Date SetID Server
        -------- ---- ----- ------
        {809B678A-7733-4011-ACD5-6... 17/6/2017 12:31:54 πμ {D69CF349-6EFD-4717-AB58-1... DC1
    #>


    [cmdletBinding(SupportsShouldProcess=$true,
                   ConfirmImpact='Medium')]

    Param
    (
        # The Active Directory Domain Controller
        [Parameter(Mandatory=$false, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]        
        [string[]]$Server = $env:COMPUTERNAME
    )

    Begin {}

    Process
    {
        # Select the Domain Controller
        if(-Not $Server)
        {
            $Server = $env:COMPUTERNAME
        }

        foreach($s in $Server)
        {
            if($PSCmdlet.ShouldProcess($s, "Create Active Directory snapshot"))
            {
                $id = Invoke-Command -ComputerName $s `
                           -ScriptBlock {
                                $pinfo = New-Object System.Diagnostics.ProcessStartInfo
                                $pinfo.FileName = "ntdsutil"
                                $pinfo.RedirectStandardError = $true
                                $pinfo.RedirectStandardOutput = $true
                                $pinfo.RedirectStandardInput = $true
                                $pinfo.UseShellExecute = $false
                                $pinfo.CreateNoWindow = $true
    
                                $p = New-Object System.Diagnostics.Process
                                $p.StartInfo = $pinfo
                                $p.Start() | Out-Null

                                $p.StandardInput.WriteLine("Activate Instance NTDS")
                                $p.StandardInput.WriteLine("snapshot")
                                $p.StandardInput.WriteLine("create")
                                $p.StandardInput.WriteLine("quit")
                                $p.StandardInput.WriteLine("quit")
                                $p.WaitForExit()
    
                                $stdout = $p.StandardOutput.ReadToEnd()
                                $stderr = $p.StandardError.ReadToEnd()
            
                                if($p.ExitCode -ne 0)
                                {
                                    Write-Error $stderr
                                    return $null
                                }
                                else
                                {
                                    [string]$data = $stdout
                                    $startIndex = $data.IndexOf("Snapshot set") + 13 
                                    $stopIndex = $data.IndexOf("generated") - 1
                                    $data = $data.Substring($startIndex, $stopIndex - $startIndex)
                                    $data
                                }
                            }
                if($id -ne $null)
                {
                    Get-ActiveDirectorySnapshot -Server $s |
                        Where-Object {$_.SetID -eq $id}
                
                }
                else
                {
                    Write-Error "Failed to create Active Directory snapshot on server $s"
                }
            }
        }
    }

    End {}
}
#endregion

#region Remove-ActiveDirectorySnapshot
Function Remove-ActiveDirectorySnapshot
{
    <#
    .Synopsis
       Short description
    .DESCRIPTION
       Long description
    .EXAMPLE
       Example of how to use this cmdlet
    .EXAMPLE
       Another example of how to use this cmdlet
    #>


    [CmdletBinding(SupportsShouldProcess=$true, 
                  ConfirmImpact='High')]

    Param
    (
        # The identity of the snapshot to remove.
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]
        [string[]]$Identity,

        # The Active Directory Domain Controller.
        [Parameter(Mandatory=$false, 
                   Position=1)]
        [string]$Server = $env:COMPUTERNAME
    )

    Begin {}

    Process
    {
        foreach($i in $Identity)
        {
            if($PSCmdlet.ShouldProcess($i, "Remove Active Directory snapshot"))
            {
                $snapshot = Get-WmiObject Win32_ShadowCopy -ComputerName $Server |
                    Where-Object {$_.Id -eq $Identity}

                if($snapshot)
                {
                    $snapshot.Delete()
                }
                else
                {
                    Write-Error "Could not find snaphot!"
                }
            }
        }
    }
    
    End
    {
    }                
}
#endregion

#region Mount-ActiveDirectorySnapshot
function Mount-ActiveDirectorySnapshot
{
    <#
    .Synopsis
       Mount an Active Directory snapshot.
    .DESCRIPTION
       Mount an Active Directory snapshot and use dsamain.exe to load it.
    .EXAMPLE
        PS C:\> Get-ActiveDirectorySnapshot
 
        Identity : {CAE74093-6422-42D5-AB1B-988C418E1560}
        Date : 17/6/2017 11:42:37 πμ
        SetID : {7772C6CE-F375-49B9-BC28-16F965B4E55C}
        Mounted : False
        MountPoint :
        Server : DC1
 
        PS C:\> Mount-ActiveDirectorySnapshot -Identity "{CAE74093-6422-42D5-AB1B-988C418E1560}" -Port 33389
 
        Mount the Active Directory snapshot with id {CAE74093-6422-42D5-AB1B-988C418E1560} on port 33389
    .EXAMPLE
        PS C:\> Get-ActiveDirectorySnapshot | Sort-Object -Property Date -Descending | Select-Object -First 1 | Mount-ActiveDirectorySnapshot -Port 33389 -Verbose
        VERBOSE: Performing the operation "Mount Active Directory snapshot" on target "{CAE74093-6422-42D5-AB1B-988C418E1560}".
        VERBOSE: Loading database: C:\$SNAP_201706171142_VOLUMEC$\Windows\NTDS\ntds.dit
        VERBOSE: Mounting database on port 33389
        VERBOSE: In order to dismount the database, close dsamain and then run "Dismount-ActiveDirectorySnapshot
 
        Mount the latest Active Directory snapshot.
    #>


    [CmdletBinding(SupportsShouldProcess=$true, 
                  ConfirmImpact='Medium')]
    Param
    (
        # The identity of the snapshot to mount
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]
        [ValidateNotNull()]
        [string]
        $Identity,

        # The port to use in order to query the mounted database
        [Parameter(Mandatory=$true, 
                   Position=1)]
        [ValidateNotNull()]
        [ValidateRange(1025,65535)]
        [int]
        $Port
        
    )

    Begin {}

    Process
    {
        if($PSCmdlet.ShouldProcess($Identity, "Mount Active Directory snapshot"))
        {
            # Mount the snapshot
            $pinfo = New-Object System.Diagnostics.ProcessStartInfo
            $pinfo.FileName = "ntdsutil"
            $pinfo.RedirectStandardError = $true
            $pinfo.RedirectStandardOutput = $true
            $pinfo.RedirectStandardInput = $true
            $pinfo.UseShellExecute = $false
            $pinfo.CreateNoWindow = $true
    
            $p = New-Object System.Diagnostics.Process
            $p.StartInfo = $pinfo
            $p.Start() | Out-Null

            $p.StandardInput.WriteLine("Activate Instance NTDS")
            $p.StandardInput.WriteLine("snapshot")
            $p.StandardOutput.DiscardBufferedData()
            $p.StandardInput.WriteLine("mount $Identity")
            $p.StandardInput.WriteLine("quit")
            $p.StandardInput.WriteLine("quit")
            $p.WaitForExit()


            $stdout = $p.StandardOutput.ReadToEnd()
            $stderr = $p.StandardError.ReadToEnd()
            
            if($p.ExitCode -ne 0)
            {
                Write-Error $stderr
                return $null
            }
            else
            {
                # Get the snapshot mount directory
                $mountPoint = (Get-ActiveDirectorySnapshot -Identity $Identity).MountPoint

                # Add the path to the database file
                $mountPoint += (Get-Item -Path "HKLM:\SYSTEM\CurrentcontrolSet\Services\NTDS\Parameters").GetValue("DSA Database File").SubString(3)
            }

            if($mountPoint)
            { 
                Write-Verbose "Loading database: $mountPoint"

                # Create a background job and start dsamain
                Write-Verbose ("Mounting database on port $Port")
                
                # Start DSAMAIN
                $DSAMAIN = Start-Process -FilePath dsamain.exe -ArgumentList "-dbpath $mountPoint -ldapport $Port" -PassThru                
                
                Write-Verbose ('In order to dismount the database, close dsamain and then run "Dismount-ActiveDirectorySnapshot')
            }
            else
            {
                Write-Error "Could not mount the database."
            }
        }
    }

    End {}
}
#endregion

#region Dismount-ActiveDirectorySnapshot
function Dismount-ActiveDirectorySnapshot
{
    <#
    .Synopsis
       Dismount an Active Directory snapshot.
    .DESCRIPTION
       Dismount and Active Directory snapshot.
    .EXAMPLE
        PS C:\> Dismount-ActiveDirectorySnapshot -Identity "{CAE74093-6422-42D5-AB1B-988C418E1560}" -Verbose
        VERBOSE: Performing the operation "Dismount Active Directory snapshot" on target "{CAE74093-6422-42D5-AB1B-988C418E1560}".
 
        Dismount the Active Directory snapshot with id {CAE74093-6422-42D5-AB1B-988C418E1560}
    .EXAMPLE
        PS C:\> Get-ActiveDirectorySnapshot | ? mounted -eq $true | Dismount-ActiveDirectorySnapshot -Verbose
        VERBOSE: Performing the operation "Dismount Active Directory snapshot" on target "{CAE74093-6422-42D5-AB1B-988C418E1560}".
     
        Dismount all currently mounted Active Directory snapshots.
    #>


    [CmdletBinding(SupportsShouldProcess=$true, 
                  ConfirmImpact='Medium')]
    Param
    (
        # The identity of the snapshot to mount
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]
        [ValidateNotNull()]
        [string]
        $Identity
    )

    Begin {}

    Process
    {
        if($PSCmdlet.ShouldProcess($Identity, "Dismount Active Directory snapshot"))
        {
            # Dismount the snapshot
            $pinfo = New-Object System.Diagnostics.ProcessStartInfo
            $pinfo.FileName = "ntdsutil"
            $pinfo.RedirectStandardError = $true
            $pinfo.RedirectStandardOutput = $true
            $pinfo.RedirectStandardInput = $true
            $pinfo.UseShellExecute = $false
            $pinfo.CreateNoWindow = $true
    
            $p = New-Object System.Diagnostics.Process
            $p.StartInfo = $pinfo
            $p.Start() | Out-Null

            $p.StandardInput.WriteLine("Activate Instance NTDS")
            $p.StandardInput.WriteLine("snapshot")
            $p.StandardOutput.DiscardBufferedData()
            $p.StandardInput.WriteLine("unmount $Identity")
            $p.StandardInput.WriteLine("quit")
            $p.StandardInput.WriteLine("quit")
            $p.WaitForExit()

            $stdout = $p.StandardOutput.ReadToEnd()
            $stderr = $p.StandardError.ReadToEnd()
            
            if($p.ExitCode -ne 0)
            {
                Write-Error $stderr
                return $null
            }
            else
            {
            }
        }
    }

    End {}
}
#endregion

#region Compare-ActiveDirectorySnapshotObject
function Compare-ActiveDirectorySnapshotObject
{
    <#
    .Synopsis
       Short description
    .DESCRIPTION
       Long description
    .EXAMPLE
        PS C:\> $guid = (Get-ADUser cpolydorou).ObjectGUID
        PS C:\> Compare-ActiveDirectorySnapshotObject -ObjectGUID $guid -ProductionServer localhost -SnapshotServer localhost:33389 -Attributes SamAccountName,GivenName,SN
 
        Attribute ProductionValue SnapshotValue Status
        --------- --------------- ------------- ------
        DistinguishedName CN=Christos Polydorou,OU=U... CN=Christos Polydorou,OU=U... Equal
        GivenName Christos Christos Equal
        Name Christos Polydorou Christos Polydorou Equal
        ObjectClass user user Equal
        ObjectGUID 241a6ed9-832e-458b-ab77-c8... 241a6ed9-832e-458b-ab77-c8... Equal
        SamAccountName cpolydorou cpolydorou Equal
        SN Polydorou 2 Polydorou Different
                 
        Compare the values of SamAccountName, GivenName and SN for the user cpolydorou using the object's ObjectGUID
    #>


    [CmdletBinding()]

    Param
    (
        # The identity of the object to compare
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $ObjectGUID,

        # The production Domain Controller
        [Parameter(Mandatory=$true, 
                   Position=1)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ProductionServer,

        # The snapshot Domain Controller
        [Parameter(Mandatory=$true, 
                   Position=2)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SnapshotServer,

        # The attributes to load
        [Parameter(Mandatory=$false, 
                   Position=3)]
        [string[]]
        $Attributes = "*"

    )

    Begin
    {
        # Load the ActiveDirectory module
        try
        {
            Import-Module ActiveDirectory -ErrorAction Stop
        }
        catch
        {
            Throw "Could not load the ActiveDirectory module."
        }
    }
    Process
    {
        foreach($o in $ObjectGUID)
        {
            # Get the object from the production DC
            $productionObject = Get-ADObject -Filter {objectguid -eq $o} -Properties $Attributes -Server $ProductionServer

            # Check if the object exists
            if( -not $productionObject)
            {
                Write-Error "Could not find an object with GUID $o in production server."
            }

            # Get the object from the snapshot
            $snapshotObject = Get-ADObject -Filter {objectguid -eq $o} -Properties $Attributes -Server $SnapshotServer

            # Check if the object exists
            if(-not $snapshotObject)
            {
                Write-Error "Could not find an object with GUID $o in snapshot server."
            }

            Compare-ActiveDirectoryObject -ReferenceObject $productionObject -DifferenceObject $snapshotObject |
                Select-Object -Property @{Name = "Attribute"; Expression = {$_.PropertyName}}, `
                                        @{Name = "ProductionValue"; Expression = {$_.ReferenceValue}}, `
                                        @{Name = "SnapshotValue"; Expression = {$_.DifferenceValue}}, `
                                        Status
        }        
    }
    End {}
}
#endregion

#region Restore-ActiveDirectoryAttribute
function Restore-ActiveDirectoryAttribute
{
    <#
    .Synopsis
       Restore Active Directory attributes.
    .DESCRIPTION
       Restore Active Directory object attributes using an Active Directory snapshot
    .EXAMPLE
        PS C:\Windows\system32> $guid = (Get-ADUser cpolydorou).ObjectGUID
 
        PS C:\Windows\system32> Restore-ActiveDirectoryAttribute -ObjectGUID $guid -ProductionServer localhost -SnapshotServer localhost:33389 -Attribute sn,givenname -Verbose
        VERBOSE: Performing the operation "Restore attribute sn" on target "CN=Christos Polydorou,OU=Users,OU=LAB,DC=LAB,DC=local".
        VERBOSE: Performing the operation "Restore attribute givenname" on target "CN=Christos Polydorou,OU=Users,OU=LAB,DC=LAB,DC=local".
 
        Restore the sn and givenname attributes for user "cpolydorou"
    #>


    [CmdletBinding(SupportsShouldProcess=$true, 
                  PositionalBinding=$false,
                  ConfirmImpact='High')]
    Param
    (
        # The ObjectGUID of the Active Directory object
        [Parameter(Mandatory=$true, 
                   ValueFromPipeline=$true,
                   ValueFromPipelineByPropertyName=$true, 
                   ValueFromRemainingArguments=$false, 
                   Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ObjectGUID,

        # The attribute to restore
        [Parameter(Mandatory=$true, 
                   Position=1)]
        [ValidateNotNullOrEmpty()]
        [string[]]
        $Attribute,

        # The production server
        [Parameter(Mandatory=$true, 
                   Position=2)]
        [ValidateNotNullOrEmpty()]
        [string]
        $ProductionServer,

        # The snapshot server
        [Parameter(Mandatory=$true, 
                   Position=3)]
        [ValidateNotNullOrEmpty()]
        [string]
        $SnapshotServer
    )

    Begin
    {
        # Import the ActiveDirectory module
        try
        {
            Import-Module ActiveDirectory -ErrorAction Stop -Verbose:$false
        }
        catch
        {
            Throw "Could not load the ActiveDirectory module."
        }
    }

    Process
    {
        foreach($o in $ObjectGUID)
        {
            # Get the object from the production DC
            $productionObject = Get-ADObject -Filter {objectguid -eq $o} -Properties $Attribute -Server $ProductionServer

            # Check if the object exists
            if( -not $productionObject)
            {
                Write-Error "Could not find an object with GUID $o in production server."
                continue
            }

            # Get the object from the snapshot
            $snapshotObject = Get-ADObject -Filter {objectguid -eq $o} -Properties $Attribute -Server $SnapshotServer

            # Check if the object exists
            if(-not $snapshotObject)
            {
                Write-Error "Could not find an object with GUID $o in snapshot server."
                continue
            }

            # Restore the attributes
            foreach($a in $Attribute)
            {
                # TODO: Check if the attribute exists
                if($productionObject.PropertyNames -notcontains $a)
                {
                    Write-Error "Attribute $a could not be found."
                }

                # Set the value on the production
                if ($pscmdlet.ShouldProcess($productionObject.DistinguishedName, "Restore attribute $a"))
                {
                    # Get the value from the snapshot object
                    $value  = $snapshotObject | % $a

                    # Form the hashtable
                    $table = @{ "$a" = "$value"}
                    Set-ADObject -Identity $o `
                                 -Server $ProductionServer `
                                 -Confirm:$false `
                                 -replace $table                    
                }
            }
        }
    }

    End {}
}
#endregion

#region Exports
Export-ModuleMember -Function Get-ActiveDirectorySnapshot
Export-ModuleMember -Function New-ActiveDirectorySnapshot
Export-ModuleMember -Function Remove-ActiveDirectorySnapshot
Export-ModuleMember -Function Mount-ActiveDirectorySnapshot
Export-ModuleMember -Function Dismount-ActiveDirectorySnapshot
Export-ModuleMember -Function Compare-ActiveDirectorySnapshotObject
Export-ModuleMember -Function Restore-ActiveDirectoryAttribute
#endregion