Author Archives: admin

Logon Script and PowerShell ADAssist Tool

In one of my first posts, I wrote about a monthly rolling log file which contains information about users’ activity; most importantly when and where each user logs on and logs off his or her computer. In addition, I’ve encouraged you to implement this or similar script in your organization to help you tackle tracking Logon / Logoff activity in your AD environment, which is somewhat a cumbersome process to do without the scripts.

PowerShell ADAssist Tool

Here I will show you an application I use to harness stats data produced by logon/logoff scripts created with PowerShell; my goal was to provide additional tools for smooth and efficient daily administration tasks.

Honestly, almost ten years ago I created an HTA application (see picture 01) with the exact same objective but never finalized the process of rendering all those VB scripts’ functions into PowerShell scripts.

Picture 01: my HTA application created in late 2008.

Instead, in this post I’ve decided to use an idea presented on TechNet script gallery under name “LazyWinAdmin”. The result is “PowerShell ADAssist” as shown in the picture 02.

Picture 02: newly created ADAssist tool

ADAssist is a PowerShell script that generates Windows Forms (WinForms) and provides the following features:

Computer component (as shown in picture 03):

  • Verifies the connectivity and general information for selected computer
  • Provides information regarding computer’s operating system
  • Displays network configuration, update GPOs, list settings and ports
  • Queries and display services, process …

Picture 03: computer section of the ADAssist tool

User component (as shown in picture 04):

  • Displays basic user info
  • Provides list of the computer(s) used by selected user
  • Shows logon activity for selected user and selected log file
  • Launches a tool to display, manage and edit user’s attributes and membership in AD

Picture 04: user section of the ADAssist tool

It is important to note that this tool does not need stats data produced by logon/logoff scripts. You could simply type a computer’s name and click on one of the buttons displayed on the application’s tabs (General, Operating System, Network and Services-Processes). But, the option to harness logon/logoff stats data makes it an efficient Active Directory Management tool, with ability to search and manage your users and computers accounts; it is especially useful in an environment where computers exist with multiple users’ accounts and provides the following:

  • consistent and accurate view of the computers being used by the selected user
  • general information about logon sessions associated with users/computers
  • display status of computers and makes data associated with users’ profiles easily accessible

How you install/configure ADAssist?

No installation required; just download the compressed file from the download/application section of this site and extract it anywhere you want (in my example it is extracted inside C:\PSScript\AD-Assist folder).

Picture 05: Location of ADAssist application

Note the XML file named “ADassistConfigFile.xml” which is an integral part of this app. As shown in picture 06. ADAssist.ps1 script reads XML document and stores the elements’ content into the script’s variables.

Some nodes/elements (like ‘<PcOU>’) could be empty, but some, like user element ‘<UserOU>’ must have value – Distinguished Name of an OU in Active Directory that contains users’ accounts.

Picture 06: ADAssist XML configuration file

If the user element ‘<UserOU>’ in XML file is empty, upon launch of the ADAssist.ps1 script, the select OU windows form will show up to let you choose an organizational unit containing users’ accounts (see the picture 07). The selected OU’s DistinguishedName will be saved in ‘<UserOU>’ element of the ADassistConfigFile.xml file. To complete this step, once you’ve selected an OU, please click ‘OK’ button to close the form, and then on ADAssist form please click ‘Refresh User List’ button to populate drop-down users’ list.

Picture 07: this OU windows form lets you choose an OU containing users’ accounts.

Please click ’Select Log File’ button as shown in picture 08 to finalize configuration settings. In the ‘Select Logon File’ dialog box, either type the path to a file or click ‘Browse’ button to pick a folder holding a file with logon stats data and then by clicking ‘Select’ button, save the selection to the XML configuration file.

Picture 08: Select Logon file

I always use the most recent log file, actually the one being produced by logon script!!! When required to do some auditing related tasks, I use the other log files previously created by logon script.

Certainly, you can always open the ADassistConfigFile.xml file with Notepad and populate its elements with values that correspond to your Active Directory and network environment.

With these configuration settings completed, you enable ADAssist to harness stats data produced by logon/logoff scripts. The script will search the specified Log File for the selected User Name from the drop-down list and produce a list of computers used by a selected user for the period of time encompassed by a logon script.

With ADAssist you can use any log file in your environment, produced by powershell, VB script or a bat file; it is important that the log file has UserName and ComputerName combination.

For example, here is the monthly rolling Log File with name 2012-11.txt as presented in one of my posts; it is not a proper .csv file, but it has separate data fields delimited with a comma, and it does not have a header with a list of column names in the file:

LogDate, LogTime, UserName, ComputerName, Action [NOTE: column names do not exist]

2012-11-12, 11:28 AM, Alex, Halifax-01, Login

2012-11-12, 11:31 AM, Alex, Dartmouth-02, Login

2012-11-12, 11:32 AM, Alex, Halifax-01, Logoff

2012-11-12, 3:30 PM, David, Bedford-03, Logoff

2012-11-12, 3:33 PM, John, Dartmouth-04, Login

Another example could be a Log File named ‘logoninfo.txt’ which is a proper tab separated CSV file and looks as follows:

PCName                      UserName       Date

Bldg05-OC-L0208     Smith.A           2017:04:28:15:28:15

Bldg04-OA-L0081     Parker.F           2017:04:28:15:30:21

Bldg01-03-W0984      Miller.W          2017:04:28:15:30:24

Bldg05-OC-WP160    Trump.D          2017:04:28:15:32:49

To use different Log Files, you need to edit just a few lines of code in ADAssist (lines 230-238 to be specific). In the following paragraphs you can observe how script uses two cmdlets ‘Get-Content’ and ‘Import-CSV’ to  read and parse the above presented text/CSV files.

PowerShell comma delimited log file example (NOTE: this option is integrated in ADAssist):


$obj = New-Object -TypeName PSObject

Get-Content -Path $Script:logFile |

ForEach-Object {

$obj| Add-Member -Force -MemberType Noteproperty -Name "LogDate" -Value $_.Split(",")[0]

$obj| Add-Member -Force -MemberType Noteproperty -Name "LogTime" -Value $_.Split(",")[1]

$obj| Add-Member -Force -MemberType Noteproperty -Name "UserName" -Value $_.Split(",")[2]

$obj| Add-Member -Force -MemberType Noteproperty -Name "PCName" -Value $_.Split(",")[3]

$obj| Add-Member -Force -MemberType Noteproperty -Name "Action" -Value $_.Split(",")[4]

$obj| Where-Object {$_.UserName.Trim() -eq $DropDown.SelectedItem.SamAccountName}

} | Select-Object -Property PCName -Unique | 

VB Script tab separated CSV log file example:


$obj = New-Object -TypeName PSObject

Import-Csv -Delimiter "`t" -Path $Script:logFile -Header "PCName","UserName","Date"| 

Select -Property PCName, UserName, Date |

ForEach-Object {

$obj| Add-Member -Force -MemberType Noteproperty -Name "PCName" -Value $_.PCName

$obj| Add-Member -Force -MemberType Noteproperty -Name "UserName" -Value $_.UserName

$obj| Add-Member -Force -MemberType Noteproperty -Name "Date" -Value $_.Date

$obj| Where-Object {$_.UserName.Trim() -eq $DropDown.SelectedItem.SamAccountName}

} | Select-Object -Property PCName -Unique |

 How ADAssist works?

As previously stated, ADAssist app could be used just by typing a computer name in the ‘Optional – Type Computer Name’ text box and a click on one of the buttons displayed on the tool’s tabs (General, Operating System, Network and Services-Processes). The retrieved information is displayed in the RichTextbox where you have an option to copy it to the clip board or clear the content.

It is important to note that the typed-in computer’s name has precedence over a computer’s name selected in the grid view!

Most likely this tool will be used as shown in the picture 09.

Picture 09: usual ADAssist workflow

First, you will select a user name from drop-down list, then click on the ‘Run-Log File’ button to search the log file and display all computers used by a selected user for the period of time encompassed by logon stats data. If selected user has logged in and out of his/her system, the grid view will display all the computers recorded in the log file, along with their current network status and additional logon session information about other users.

The next step is to select a computer from the grid view and click on one of the buttons laid out on four tabs.

If you have clients located in different OU, but you have the same logon script, you would just click on ‘Select OU’ button and finalize the process of selecting a different OU in your Active Directory. To replace current list of users in the drop-down list with the ones in newly selected OU, please click the ‘Refresh User List’ button.

In addition to the basic information displayed for the selected user account, you could search and obtain user’s logon activity by clicking on ‘User Logon Info’ button – see picture 10. Just type the path to a log file or click ‘Browse’ button to pick a folder holding a file with logon stats data and then click ‘Run’ button to obtain user’s logon activity information.

Picture 10: unfiltered logon activity from one log file

And if you want to view additional user’s attributes or to administer selected user account, you can click on ‘Display User’ button and launch a new Windows form as shown in picture 11.

Picture 11: Administer selected user account

The next post will elaborate more on administration of user accounts in Active Directory as shown in the picture above and the current status of this ‘AD – User Properties’ form is ‘work in progress’. I have more features on the road-map and I would welcome any feature ideas / suggestions.


 

PowerShell – Create USB Recovery Drive

This is the third post in my series about Windows Recovery Environment (Windows RE). Here, I will suggest to you to make a recovery drive and subsequently create a backup ISO image of your bootable USB recovery drive, mainly if you purchase a new computer and it is running smoothly.

The Recovery Drive creates a FAT32 formatted USB that boots in both MBR and UEFI with SecureBoot. It helps you to troubleshoot and fix problems with your PC, even if it won’t start, because the (USB) media contains a bootable copy of Windows RE, and therefore gives you access to all troubleshooting and recovery tools. No product key is required when using the Recovery USB Drive to reinstall Windows 10.

In Windows 10 OS, you can use a command-line tool named RecoverDrive.exe to create a system recovery USB drive. To make a USB recovery drive in Windows 10, please run PowerShell as Admin, and do the following:

  1. Type ‘RecoveryDrive.exe’ on the command line.
  2. In a new window that will appear; please select ‘Back up system files to the recovery drive’.
  3. Select Next.
  4. Choose the USB drive you want to use and click next.
  5. Select Create and let the process complete.
  6. Select Finish

Most guides out there will suggest to you that once you create the USB recovery drive, you will not be able to use it for anything else; that you should remove the USB recovery drive from your computer, label or mark it as such and keep it somewhere safe.

But there is an additional option, described in this post that enables you to use PowerShell script and create a backup ISO image of your bootable USB recovery drive, and whenever you need it again, just restore the backup image to the same or another USB stick.

To use this option, please do not remove (just created) USB recovery drive from your computer as soon as you finished step 6 listed above; close the ‘RecoveryDrive.exe’ window only, and run the following PowerShell script which will produce an ISO File:

Example: CreateOrMount-WinREisoFile.ps1 -Path C:\WinRe\WinRE.iso -Title “WinRE” -Force

The following two pictures display the process of creating a backup ISO image of your bootable USB recovery drive.

Picture 01: closing the RecoverDrive window and changing the current directory to the one containing my PowerShell script.

Picture 02: running a PS script to create a backup ISO image of a USB recovery drive.

Here is the PowerShell script:


<# Name: CreateOrMount-WinREisoFile.ps1 
Author: Alex Dujakovic 
Date: June 2017 
Description: Creates ISO File from USB WinRE Recovery Drive 
Mounts ISO File and creates USB WinRE Drive 
Examples: Mounts ISO File and creates USB WinRE Drive 
CreateOrMount-WinREisoFile.ps1 -ImagePath C:\Test\Win10.iso -USBDriveLetter Z -label 'WinRE' 
Creates ISO File from USB WinRE Recovery Drive 
CreateOrMount-WinREisoFile.ps1 -Path C:\Test\WinRE.iso -Title "WinRE" -Force 
CreateOrMount-WinREisoFile.ps1 -Path C:\Test\WinRE.iso -Force Tested on Windows 10 only 
#> 
            
[CmdletBinding(SupportsShouldProcess=$True,DefaultParametersetName="MountISO")]

param( 
[Parameter(ParameterSetName="MountISO", Mandatory=$True, Position=0)] 
[string]$ImagePath, 

[Parameter(ParameterSetName="MountISO", Position=1)] 
[string]$USBDriveLetter,

[Parameter(ParameterSetName="MountISO", Position=2)] 
[string]$label = "WinRE", 

[Parameter(ParameterSetName="CreateISO", Mandatory=$True, Position=0)] 
[string]$Path,
[Parameter(ParameterSetName="CreateISO", Position=1)] 
[string]$Title = "WinRE", 
[Parameter(ParameterSetName="CreateISO", Position=2)]  
[switch]$Force
)
# -------------------------|Functions|------------------------------------

Function New-IsoFile  
{  
  <# .Synopsis Creates a new .iso file .Description The New-IsoFile cmdlet creates a new .iso file containing content from chosen folders .Example New-IsoFile "c:\tools","c:Downloads\utils" This command creates a .iso file in $env:temp folder (default location) that contains c:\tools and c:\downloads\utils folders. The folders themselves are included at the root of the .iso image. .Example New-IsoFile -FromClipboard -Verbose Before running this command, select and copy (Ctrl-C) files/folders in Explorer first. .Example dir c:\WinPE | New-IsoFile -Path c:\temp\WinPE.iso -BootFile "${env:ProgramFiles(x86)}\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\Oscdimg\efisys.bin" -Media DVDPLUSR -Title "WinPE" This command creates a bootable .iso file containing the content from c:\WinPE folder, but the folder itself isn't included. Boot file etfsboot.com can be found in Windows ADK. Refer to IMAPI_MEDIA_PHYSICAL_TYPE enumeration for possible media types: http://msdn.microsoft.com/en-us/library/windows/desktop/aa366217(v=vs.85).aspx .Notes NAME: New-IsoFile AUTHOR: Chris Wu LASTEDIT: 03/23/2016 14:46:50 #>  
  
  [CmdletBinding(DefaultParameterSetName='Source')]Param( 
    [parameter(Position=1,Mandatory=$true,ValueFromPipeline=$true, ParameterSetName='Source')]$Source,  
    [parameter(Position=2)][string]$Path = "$env:temp\$((Get-Date).ToString('yyyyMMdd-HHmmss.ffff')).iso",  
    [ValidateScript({Test-Path -LiteralPath $_ -PathType Leaf})][string]$BootFile = $null, 
    [ValidateSet('CDR','CDRW','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','BDR','BDRE')][string] $Media = 'DVDPLUSRW_DUALLAYER', 
    [string]$Title = (Get-Date).ToString("yyyyMMdd-HHmmss.ffff"),  
    [switch]$Force, 
    [parameter(ParameterSetName='Clipboard')][switch]$FromClipboard 
  ) 
 
  Begin {  
    ($cp = new-object System.CodeDom.Compiler.CompilerParameters).CompilerOptions = '/unsafe' 
    if (!('ISOFile' -as [type])) {  
      Add-Type -CompilerParameters $cp -TypeDefinition @' 
public class ISOFile  
{ 
  public unsafe static void Create(string Path, object Stream, int BlockSize, int TotalBlocks)  
  {  
    int bytes = 0;  
    byte[] buf = new byte[BlockSize];  
    var ptr = (System.IntPtr)(&bytes);  
    var o = System.IO.File.OpenWrite(Path);  
    var i = Stream as System.Runtime.InteropServices.ComTypes.IStream;  
  
    if (o != null) { 
      while (TotalBlocks-- > 0) {  
        i.Read(buf, BlockSize, ptr); o.Write(buf, 0, bytes);  
      }  
      o.Flush(); o.Close();  
    } 
  } 
}  
'@  
    } 
  
$i = 0

    if ($BootFile) { 
      if('BDR','BDRE' -contains $Media) { Write-Warning "Bootable image doesn't seem to work with media type $Media" } 
      ($Stream = New-Object -ComObject ADODB.Stream -Property @{Type=1}).Open()  # adFileTypeBinary 
      $Stream.LoadFromFile((Get-Item -LiteralPath $BootFile).Fullname) 
      ($Boot = New-Object -ComObject IMAPI2FS.BootOptions).AssignBootImage($Stream) 
    } 
    $MediaType = @('UNKNOWN','CDROM','CDR','CDRW','DVDROM','DVDRAM','DVDPLUSR','DVDPLUSRW','DVDPLUSR_DUALLAYER','DVDDASHR','DVDDASHRW','DVDDASHR_DUALLAYER','DISK','DVDPLUSRW_DUALLAYER','HDDVDROM','HDDVDR','HDDVDRAM','BDROM','BDR','BDRE') 
    ($Image = New-Object -com IMAPI2FS.MsftFileSystemImage -Property @{VolumeName=$Title}).ChooseImageDefaultsForMediaType($MediaType.IndexOf($Media)) 
  
    if (!($Target = New-Item -Path $Path -ItemType File -Force:$Force -ErrorAction SilentlyContinue)) { Write-Error -Message "Cannot create file $Path. Use -Force parameter to overwrite if the target file already exists."; break } 
  }  
 
  Process { 
    $i++
    $Activity = "Creating ISO File"
    $CurFolderText = '"Item: $($i.ToString().PadLeft($Script:IsoItemsCollection.Count.ToString().Length)) of $($Script:IsoItemsCollection.Count) | $($item.FullName)"'
    $CurFolderBlock = [ScriptBlock]::Create($CurFolderText)
   
    if($FromClipboard) { 
      if($PSVersionTable.PSVersion.Major -lt 5) { Write-Error -Message 'The -FromClipboard parameter is only supported on PowerShell v5 or higher'; break } 
      $Source = Get-Clipboard -Format FileDropList 
    } 

    foreach($item in $Source) { 
    Write-Host " " 
    Write-Host "       Processing:$item"    
      if($item -isnot [System.IO.FileInfo] -and $item -isnot [System.IO.DirectoryInfo]) { 
        $item = Get-Item -LiteralPath $item 
      } 
  if($item){     
  try { 

    $CurFolderpPercent = $i / $Script:IsoItemsCollection.Count * 100
    $Task = "Adding item files into ISO container"
    Write-Progress -Id 1 -Activity $Activity -Status (& $CurFolderBlock) -CurrentOperation $Task -PercentComplete $CurFolderpPercent 

    $Image.Root.AddTree($item. FullName, $true) 
  }
  catch { Write-Error -Message ($_.Exception.Message.Trim() + ' Try a different media type.') } 
  } 
  } 
  } # End of Process
 
  End {  
    if ($Boot) { $Image.BootImageOptions=$Boot }  
    $Result = $Image.CreateResultImage()  
    [ISOFile]::Create($Target.FullName,$Result.ImageStream,$Result.BlockSize,$Result.TotalBlocks) 
    $Target 
    Write-Host "Done! Script is finished executing." -ForegroundColor Green
  } 
}

Function Mount-ISOFile{

    Param(
    [Parameter(Mandatory=$True)]
    [string]$ImagePath,
    [Parameter(Mandatory=$True)]
    [string]$USBDriveLetter,
    [string]$label = "WinRE"
    )

Begin{
    
    Function Get-PBRFile($Drive){
    
    $partitionStyle = (Get-Content -Path "$($Drive)\sources\`$PBR_Diskpart.txt"|
    Select-String -Pattern "convert" |
    ForEach-Object{$_.ToString().Replace("convert " ,$null)})
    return $partitionStyle.Trim()
    }

    Function do_CopyFiles($diskLetter, $folder){

    $FOF_CREATEPROGRESSDLG = "&H0&"
    $objShell = New-Object -ComObject "Shell.Application"
    $objFolder = $objShell.NameSpace($diskLetter)
 
    if(Test-Path -Path $folder){
	$folder = $folder + "\*.*"
        $objFolder.CopyHere($folder, $FOF_CREATEPROGRESSDLG)
    }  
    else{
        [System.Windows.Forms.MessageBox]::Show("Folder does not exist!")
    }
}

# Get USB drive and ISO file size - validate
$Disk = Get-Disk|Where-Object {$_.BusType -eq 'USB'} 
$USBDriveSize = ($Disk | Where-Object Number -eq ($Disk.Number) | Select -ExpandProperty Size)
$ISOFileSize = (Get-ChildItem $ImagePath).Length

If($ISOFileSize -gt $USBDriveSize)
{
Write-Warning "There is not enough free space on USB Drive!!!"
Write-Host "USB Drive capacity: $(($USBDriveSize/1GB).ToString("0.00 GB"))."
Write-Host "ISO File size: $(($ISOFileSize/1GB).ToString("0.00 GB"))."
Write-Warning "A USB drive with a $(($ISOFileSize/1GB).ToString("0.00 GB")) capacity is needed."
Break
}

#Mount ISO File
If(-not(Get-DiskImage -ImagePath $ImagePath | Get-Volume))
    {
        $ISOFileMounted = $true
    Try{  
        Write-Host "Mounting ISO File, please wait ..." -ForegroundColor Green      
        Mount-DiskImage -ImagePath $ImagePath -ErrorAction Stop | Out-Null
    }
    Catch{
        Write-Host "Error: $_.Exception.GetType().FullName; Error: $_.Description" -ForegroundColor Yellow
        Break    
    }
}
Else
   {
     Write-Host "ISO File already mounted" -ForegroundColor Green
}# End of If/Else

$ISODrive = ((Get-DiskImage -ImagePath $ImagePath | Get-Volume).DriveLetter) + ":"

Write-Host "ISO File mounted to $ISODrive" -ForegroundColor Green

$partitionStyle = (Get-PBRFile -Drive $ISODrive)

} # End of Begin
Process{

 $Disk = Get-Disk|Where-Object {$_.BusType -eq 'USB'} 

 Get-Disk -Number ($Disk.Number)| Get-Partition | Remove-Partition -Confirm:$False -ErrorAction SilentlyContinue

 Clear-Disk -Number $Disk.Number -RemoveData -RemoveOEM -Confirm:$false -ErrorAction SilentlyContinue

 Switch($partitionStyle){

    "MBR" {Initialize-Disk -Number $Disk.Number -PartitionStyle MBR -ErrorAction SilentlyContinue}
    "GPT" {Initialize-Disk -Number $Disk.Number -PartitionStyle GPT -ErrorAction SilentlyContinue}

 }
 
 $partition = New-Partition -DiskNumber $Disk.Number -DriveLetter $USBDriveLetter -UseMaximumSize -IsActive

 Format-Volume -Partition $partition -FileSystem FAT32 -NewFileSystemLabel "$($label)"

 Write-Host "USB drive formated, preparing to copy files, please wait ..." -ForegroundColor Green

 $USBDriveLetter = $USBDriveLetter  + ":"

 do_CopyFiles -diskLetter $USBDriveLetter -folder $ISODrive

} # End of Process
End{
     If($ISOFileMounted)
        {
            Write-Host "Dismounting image ..." -ForegroundColor Green
            Dismount-DiskImage -ImagePath $ImagePath
        }
        Write-Host "Recovery USB Drive creation complete!" -ForegroundColor Green
    }
} 
# ---------------------------|End Of Functions|----------------------------------

switch ($PsCmdlet.ParameterSetName){ 
"MountISO"{ 
    Mount-ISOFile -ImagePath $ImagePath -USBDriveLetter $USBDriveLetter -label $label 
 } 
"CreateISO"{
    $Disk= Get-Disk | Where-Object { $_.BusType –eq ‘USB’ }
    $DriveLetter = "$(($Disk | Get-Partition).DriveLetter):\"
    Write-Host "Collecting info about USB Recovery Drive, please wait ..." -ForegroundColor Green
    $Script:IsoItemsCollection = (Get-ChildItem $DriveLetter)
    Write-Host  "Creating ISO File - Adding Folders/Files to ISO File" -ForegroundColor Green
    Dir $DriveLetter | New-IsoFile -Path $Path -Title $Title -Force
 } 
}

And whenever you need USB Recovery drive, just restore the backup image to the same or another USB stick. Run the following PowerShell command:

Example: CreateOrMount-WinREisoFile.ps1 -ImagePath C:\WinRe\WinRE.iso –USBDriveLetter Z -label “WinRE”

Picture 03: recreating USB Recovery drive form an ISO file.

Just as an extra note, the process of making USB recovery drive creates additional files in your computer’s C:\Windows\System32\Recovery folder. These files ($PBR_Diskpart.txt, $PBR_ResetConfig.xml and ReAgent.xml) are present on USB media as well and my Powershell script reads one of them to configure a proper partition style for a bootable USB stick.

Picture 04: additional files created in the computer’s C:\Windows\System32\Recovery folder.

At the end, it is important to remember that a Recovery Drive is BIT specific, and if you create a Recovery Drive in a 64-bit version of Windows 10, you can’t use that drive to boot up and repair a 32-bit version of Windows 10.

The PowerShell script described here is available in download section, under Powershell.