Installing all Microsoft Visual C++ Runtime modules

On my blog post Latest Supported Visual C++ Downloads you a link to the Microsoft site Latest Supported Visual C++ Downloads. Once downloaded, how to install all these updates?

I made a script that installs all the Microsoft C++ Runtime modules. On x86 only the x86 modules are installed. On x64, both x86 and x64 modules are installed.

The list has been populated on June, 14th, 2014. So the list of C++ Runtime modules might – and will be – change.

The folder list should be as follows:

Folder PATH listing for volume OS
Volume serial number is 000000C0 0475:818B
C:.
|   install_all_VisualCPlusPlusRuntimes_v101.vbs
|   
+---01.Microsoft Visual C++ 2005 SP1 Redistributable Package
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---02.Microsoft Visual C++ 2005 SP1 Redistributable Package ATL Security Update
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---03.Microsoft Visual C++ 2005 SP1 Redistributable Package MFC Security Update
|       vcredist_x64.EXE
|       vcredist_x86.EXE
|       
+---04.Microsoft Visual C++ 2008 SP1 Redistributable Package
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---05.Microsoft Visual C++ 2008 SP1 Redistributable Package MFC Security Update
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---06.Microsoft Visual C++ 2010 SP1 Redistributable Package
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---07.Microsoft Visual C++ 2010 SP1 Redistributable Package MFC Security Update
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---08.Microsoft Visual C++ 2012 Update 3
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---09.Microsoft Visual C++ 2013 Redistributable Package
|       vcredist_x64.exe
|       vcredist_x86.exe
|       
+---10.Microsoft Visual C++ 2015 Redistributable Update 3
|       vcredist_x64.exe
|       vcredist_x86.exe
\---11.Microsoft Visual C++ Redistributable for Visual Studio 2017
        vcredist_x64.exe
        vcredist_x86.exe

The only thing you have to do is to download all the redistributable packages:

The code:

' ================================================================================================
' Install all Microsoft C++ Runtimes
' Created by Willem-Jan Vroom
' Version history:
'
' 0.0.1
'    Initial version
'
' 1.0.0
'    Final version
'
' 1.0.1
'    Added support for C++ Redistributable 2015
'
' 2.0.0
'    Addes support for Microsoft Visual C++ Redistributable for Visual Studio 2017
' ================================================================================================

' ------------------------------------------------------------------------------------------------
' Declare the most variables. 
' ------------------------------------------------------------------------------------------------

  Option Explicit

  Dim objShell            : set objShell              = WScript.CreateObject("WScript.Shell")
  Dim objLogFileFSO       : Set objLogFileFSO         = CreateObject("Scripting.FileSystemObject")
  Dim objFSO              : Set objFSO                = CreateObject("Scripting.FileSystemObject")
  Dim objProcessEnv       : Set objProcessEnv         = objShell.Environment("PROCESS")
  Dim objWMIService       : Set objWMIService         = GetObject("winmgmts:\\.\root\cimv2")
  Dim fn_objWMIService    : Set fn_objWMIService      = GetObject("winmgmts:\\.\root\cimv2")
  Dim objReg              : Set objReg                = GetObject("winmgmts:\\.\root\default:StdRegProv")
  Dim objLogFile
  Dim CurrentDir          : CurrentDir                = Left(Wscript.ScriptFullname, InstrRev(Wscript.ScriptFullname, "\"))
                            CurrentDir                = Left(CurrentDir,len(CurrentDir)-1)
  Dim strcomputerName     : strcomputerName           = objProcessEnv("COMPUTERNAME")
  Dim strLogLocation      : strLogLocation            = "C:\WINDOWS\system32\Logfiles"
  Dim strOutputFile       : strOutputFile             = strLogLocation & "\" & strcomputerName & "_Installation_C++Runtimes_" & Replace(FormatDateTime(Now(), 2),"/","-") & ".log"
  Dim strArchitecture     : strArchitecture           = ""
  Dim strCurrentUser      : strCurrentUser            = ""
  Dim strCurrentUserSID   : strCurrentUserSID         = "" 
  Dim strOS               : strOS                     = ""
  Dim strCommand          : strCommand                = ""
  Dim strLine             : strLine                   = ""
  Dim strQuery            : strQuery                  = ""
  Dim strCommonDesktop    : strCommonDesktop          = ""
  Dim strArray            : strArray                  = ""  
  Dim strAppDataFolder    : strAppDataFolder          = ""
  Dim strProcess          : strProcess                = ""
  Dim strValue            : strValue                  = ""
  Dim strKeyPath          : strKeyPath                = ""
  Dim strValueName        : strValueName              = ""
  Dim strLogFile
  Dim dwValue             : dwValue                   = 0
  Dim arrFolderUnsorted   : Redim arrFolderUnsorted(0)

  Dim arrArguments
  Dim arrLanguage
  Dim valCounter
  Dim valResult 
  Dim valReturnCode

  Dim colProcess, objProcess 
  Dim valOSBuildNumber
  Dim objSubfolder
  Dim colItems, objItem, Subfolder
  Dim colSoftware, objSoftware
  Dim arrValues, strHolder

  dim objFolders, objFolder, arrFolders, arrVersions, objVersion


  Const ForWriting           = 2
  Const ForReading           = 1
  Const ForAppending         = 8
  Const OverWriteFiles       = True
  const HKEY_CURRENT_USER    = &H80000001
  const HKEY_LOCAL_MACHINE   = &H80000002
  Const HKEY_USERS           = &H80000003 

' ------------------------------------------------------------------------------------------------
' Create the log location (if not exists)
' Open the logfile.  
' ------------------------------------------------------------------------------------------------

  CreateFolderStructure(strLogLocation)
  OpenLogFile()
  WriteToLog "- ACTION: script started."
  WriteToLog " "

' ------------------------------------------------------------------------------------------------
' Detect the current OS.  
' ------------------------------------------------------------------------------------------------

  Set colItems = objWMIService.ExecQuery("Select Caption,BuildNumber from Win32_OperatingSystem")
  For Each objItem in colItems
      strOS            = objItem.Caption
      valOSBuildNumber = objItem.BuildNumber
  next

' ------------------------------------------------------------------------------------------------
' Detect the current processor architecture.  
' ------------------------------------------------------------------------------------------------

  if objFSO.FolderExists(objShell.ExpandEnvironmentStrings("%windir%") & "\SysWOW64\Config") then  
     strArchitecture = "x64"
     arrVersions=Array("x86","x64")
       else
     strArchitecture = "x86"
     arrVersions=Array("x86")
  end if


  WriteToLog("########## Details regarding operating system                        ##########")
  WriteToLog("Found Operating System:       " & strOS)
  WriteToLog("Found architecture:           " & strArchitecture)
  WriteToLog("########## End details regarding operating system                    ##########")
  WriteToLog(" ")
  WriteToLog("########## Installation                                              ##########")

' ------------------------------------------------------------------------------------------------
' Sets SEE_MASK_NOZONECHECKS to 1.  
' That will avoid a screen that asks for permission to run an application from
' a share or mapped drive. 
' ------------------------------------------------------------------------------------------------

  objProcessEnv("SEE_MASK_NOZONECHECKS") = 1
  
  ' ------------------------------------------------------------------------------------------------
  ' Start the installation:
  '  1. Loop throu all the subfolders, sorted.
  '     Source: 
  '     http://blogs.technet.com/b/heyscriptingguy/archive/2004/11/22/how-can-i-list-the-members-of-a-group-in-alphabetical-order.aspx
  '  2. Depending on the foldername the correct parameters are passed to the vcredit_xx.exe
  '     Logging enabled.
  ' ------------------------------------------------------------------------------------------------ 

  ' 1: Get all the subfolder of the parent folder.

  valCounter  = 0
  set objFolders = objFSO.GetFolder(CurrentDir)
  Set arrFolders = objFolders.SubFolders
  For Each objFolder in arrFolders
      arrFolderUnsorted(valCounter) = objFolder.Name
      Redim Preserve arrFolderUnsorted(valCounter+1)
      valCounter = valCounter + 1
  Next   

  Redim Preserve arrFolderUnsorted(valCounter-1)
  Dim j

  ' 2: Sort them:

  For valCounter = (UBound(arrFolderUnsorted) - 1) to 0 Step -1
    For j= 0 to valCounter
        If UCase(arrFolderUnsorted(j)) > UCase(arrFolderUnsorted(j+1)) Then
            strHolder = arrFolderUnsorted(j+1)
            arrFolderUnsorted(j+1) = arrFolderUnsorted(j)
            arrFolderUnsorted(j) = strHolder
        End If
    Next
  Next

  ' 3: Install

  For valCounter = 0 to ubound(arrFolderUnsorted) ' I know: the folders are sorted now....

      if instr(arrFolderUnsorted(valCounter),"2005") > 0 Then InstallCPlusPlus2005Runtimes
      if instr(arrFolderUnsorted(valCounter),"2008") > 0 Then InstallCPlusPlus2008Runtimes
      if instr(arrFolderUnsorted(valCounter),"2010") > 0 Then InstallCPlusPlus2010Runtimes
      if instr(arrFolderUnsorted(valCounter),"2012") > 0 Then InstallCPlusPlus2012Runtimes
      if instr(arrFolderUnsorted(valCounter),"2013") > 0 Then InstallCPlusPlus2013OrLater
      if instr(arrFolderUnsorted(valCounter),"2015") > 0 Then InstallCPlusPlus2013OrLater
      if instr(arrFolderUnsorted(valCounter),"2017") > 0 Then InstallCPlusPlus2013OrLater

  next

  WriteToLog("########## End Installation                                          ##########")
  WriteToLog("########## Summary of installed C++ Redistributables                 ##########")
  WriteToLog(" ")

  Set colSoftware = objWMIService.ExecQuery("Select * from Win32_Product Where Name LIKE '%Visual C++%' AND (Name LIKE '%Redistributable%' OR Name LIKE '%Runtime%')")
  For Each objSoftware in colSoftware
      WriteToLog("Name:     " & objSoftware.Name)
      WriteToLog("Version:  " & objSoftware.Version)
      WriteToLog(" ")
  Next


  WriteToLog(" ")
  WriteToLog("########## End summary of installed C++ Redistributables             ##########")
  WriteToLog(" ")

  CloseLogFile()
  wscript.quit 0

Sub OpenLogFile() 

' ------------------------------------------------------------------------------------------------
' Subroutine: OpenLogFile() 
' The name of the logfile is mentinoed in the variabele strOutputFile.
' ------------------------------------------------------------------------------------------------

  If objLogFileFSO.FileExists(strOutputFile) Then
     Set objLogFile = objLogFileFSO.OpenTextFile(strOutputFile, ForWriting)
         Else
     Set objLogFile = objLogFileFSO.CreateTextFile(strOutputFile)
  End If

End Sub

Sub CloseLogFile()

' ------------------------------------------------------------------------------------------------
' Subroutine: CloseLogFile()
' Close the log file.
' ------------------------------------------------------------------------------------------------

  WriteToLog "- ACTION: script ended."
  objLogFile.Close
  Set objLogfileFSO = Nothing

End Sub

Function WriteToLog(sLogMessage)

' ------------------------------------------------------------------------------------------------
' Function: WriteToLog(sLogMessage)
' Writes an entry 'sLogMessage' in the logfile.
' ------------------------------------------------------------------------------------------------

  if instr(sLogMessage, "- ACTION: ") = 0 then sLogMessage = "          " & sLogMessage
  objLogFile.WriteLine("Time: " & now & "  " & sLogMessage)

End Function

Sub CreateFolderStructure(strFolderNameToBeCreated)

' ------------------------------------------------------------------------------------------------
' Subroutine: CreateFolderStructure(strFolderNameToBeCreated)
' Creates the map as mentioned in strFolderNameToBeCreated.
' ------------------------------------------------------------------------------------------------

  Dim arrFoldersTMP : arrFoldersTMP = split (strFolderNameToBeCreated,"\")
  Dim strFolder  : strFolder  = ""
  Dim objFolderTMP
 
  For Each objFolderTMP in arrFoldersTMP
      strFolder = strFolder & objFolderTMP
      If NOT objFSO.FolderExists(strFolder) Then
             objFSO.CreateFolder(strFolder)
      end If
      strFolder = strFolder & "\"
  Next
 
End Sub

Function fnKillProcess(strProcessName)

' ------------------------------------------------------------------------------------------------
' Function: fnKillProcess(strProcessName)
' Terminates the given processname.
' ------------------------------------------------------------------------------------------------

  Set colProcess = fn_objWMIService.ExecQuery ("Select * From Win32_Process")
  For Each objProcess In colProcess
    If Instr(LCase(objProcess.Name),LCase(strProcessName)) > 0 Then
       objShell.Run "TASKKILL /F /T /IM " & objProcess.Name, 0, False
       objProcess.Terminate()
       WriteToLog("Terminating application: " & objProcess.Name)
    End If
  Next

End Function

Sub InstallCPlusPlus2005Runtimes
' ------------------------------------------------------------------------------------------------
' Subroutine: InstallCPlusPlus2005Runtimes
' Installs the C++ 2005 Runtime modules.
' ------------------------------------------------------------------------------------------------

  for Each objVersion in arrVersions
      if instr(arrFolderUnsorted(valCounter),"MFC") > 0 Then
         strLogFile = right(arrFolderUnsorted(valCounter),len(arrFolderUnsorted(valCounter))-3) & " (" & objVersion & ").log" 
         strCommand = chr(34) & CurrentDir & "\" & arrFolderUnsorted(valCounter) & "\vcredist_" & objVersion & ".exe" & chr(34) & " /t:c:\temp /q"
         strCommand = strCommand & ":a /c:" & chr(34) & "msiexec /i vcredist.msi /qb! /l*v "
         strCommand = strCommand & chr(34) & chr(34) & strLogLocation & "\" & strLogFile & chr(34) & chr(34) & chr(34)
         WriteToLog("Running command: " & strCommand)
         valResult = objShell.Run(strCommand, 6, True)
         WriteToLog("Result: " & valResult)
           else
         strCommand = chr(34) & CurrentDir & "\" & arrFolderUnsorted(valCounter) & "\vcredist_" & objVersion & ".exe" & chr(34) & " /q"
         WriteToLog("Running command: " & strCommand)
         valResult = objShell.Run(strCommand, 6, True)
         WriteToLog("Result: " & valResult)
     end if
  next

End sub

Sub InstallCPlusPlus2008Runtimes
' ------------------------------------------------------------------------------------------------
' Subroutine: InstallCPlusPlus2008Runtimes
' Installs the C++ 2008 Runtime modules.
' ------------------------------------------------------------------------------------------------

  for Each objVersion in arrVersions
      strLogFile = right(arrFolderUnsorted(valCounter),len(arrFolderUnsorted(valCounter))-3) & " (" & objVersion & ").log" 
      strCommand = chr(34) & CurrentDir & "\" & arrFolderUnsorted(valCounter) & "\vcredist_" & objVersion & ".exe" & chr(34) & " /q /l "
      strCommand = strCommand & chr(34) & strLogLocation & "\" & strLogFile & chr(34)
      WriteToLog("Running command: " & strCommand)
      valResult = objShell.Run(strCommand, 6, True)
      WriteToLog("Result: " & valResult)
  next
  
End sub

Sub InstallCPlusPlus2010Runtimes
' ------------------------------------------------------------------------------------------------
' Subroutine: InstallCPlusPlus2010Runtimes
' Installs the C++ 2010 Runtime modules.
' ------------------------------------------------------------------------------------------------

  for Each objVersion in arrVersions
      strLogFile = right(arrFolderUnsorted(valCounter),len(arrFolderUnsorted(valCounter))-3) & " (" & objVersion & ").log" 
      strCommand = chr(34) & CurrentDir & "\" & arrFolderUnsorted(valCounter) & "\vcredist_" & objVersion & ".exe" & chr(34) & " /passive /norestart /log "
      strCommand = strCommand & chr(34) & strLogLocation & "\" & strLogFile & chr(34)
      WriteToLog("Running command: " & strCommand)
      valResult = objShell.Run(strCommand, 6, True)
      WriteToLog("Result: " & valResult)
  next

End sub

Sub InstallCPlusPlus2012Runtimes
' ------------------------------------------------------------------------------------------------
' Subroutine: InstallCPlusPlus2012Runtimes
' Installs the C++ 2012 Runtime modules.
' ------------------------------------------------------------------------------------------------

  for Each objVersion in arrVersions
      strLogFile = right(arrFolderUnsorted(valCounter),len(arrFolderUnsorted(valCounter))-3) & " (" & objVersion & ").log" 
      strCommand = chr(34) & CurrentDir & "\" & arrFolderUnsorted(valCounter) & "\vcredist_" & objVersion & ".exe" & chr(34) & " /install /passive /norestart /log "
      strCommand = strCommand & chr(34) & strLogLocation & "\" & strLogFile & chr(34)
      WriteToLog("Running command: " & strCommand)
      valResult = objShell.Run(strCommand, 6, True)
      WriteToLog("Result: " & valResult)
  next

End sub

Sub InstallCPlusPlus2013OrLater
' ------------------------------------------------------------------------------------------------
' Subroutine: InstallCPlusPlus2013OrLater
' Installs the C++ 2013 Runtime modules.
' ------------------------------------------------------------------------------------------------

  for Each objVersion in arrVersions
      strLogFile = right(arrFolderUnsorted(valCounter),len(arrFolderUnsorted(valCounter))-3) & " (" & objVersion & ").log" 
      strCommand = chr(34) & CurrentDir & "\" & arrFolderUnsorted(valCounter) & "\vcredist_" & objVersion & ".exe" & chr(34) & " /install /passive /norestart /log "
      strCommand = strCommand & chr(34) & strLogLocation & "\" & strLogFile & chr(34)
      WriteToLog("Running command: " & strCommand)
      valResult = objShell.Run(strCommand, 6, True)
      WriteToLog("Result: " & valResult)
  next

End sub          

C++ Detection Rule

C++ Detection Rule




Error 80073D01 during deployment MSIX application

The deployment of a MSIX application may fail in case roaming user profiles are used. This can be solved easily with a policy adjustment.

The issue is there if this policy setting has been enabled:

Delete cached copies of roaming profiles enabled

Delete cached copies of roaming profiles enabled

If used then enable the use of Allow deployment operations in special folders:

Allow deployment operations in special folders

Allow deployment operations in special folders

The explanation:




Deploy a MSIX application with SCCM and test it on a client

As per SCCM 2012 build 1806 it is possible to deploy MSIX packages. In this article, I will describe all the steps that are needed to deploy a MSIX package on a Windows 10 v1809 client.

In the article Package and install a MSIX package Paint.Net 4.1.5 was packaged as a MSIX package. In Create a MSIX modification package a modification package was created to add some addins. In this article both applications will be used.

Before you can install a MSIX package on a client, you have to:

  1. Deploy the certificate
  2. Enable sideloading apps

This can be done via policies:

Certficate in Trusted Root

Certficate in Trusted Root

Sideload apps

Sideload apps

Before the Paint.Net Addin package can be deployed, it is mandatory to have the dependency application deployed first.

DescriptionPicture
PaintNet has been distributed to all the distribution points as it is a dependency application (SCCM wise).
Paint.Net deployment type.
Paint.Net has been distributed to all distribution points.

After implementing the dependency application the addin application can be deployed:

DescriptionPicture
Create a new application in SCCM. Click [Browse] to select the msix file.
Browse to the msix plugin file.
Click [Next]
Click [Next]
Check the details and click [Next]
Confirm the settings and click [Next]
Click [Close]
Go to the properties of the deployment type.
Go to the Dependencies tab and click [Add]
Fill in the dependency group name and click [Add]
Find the PaintNet application and click [Ok]
Click [Ok]
Click [Apply]
Click [Ok]
Changing the icon that shows up in Software Center.

Properties of the application.
Go to the tab Application Catalog and click [Browse]
Find the correct icon to show in Software Center.
Click [Apply]
Click [Ok]
Deploy the application.
Select the collection and click [Next]
Select the distributiion points and click [Next]
Click [Next]
Click [Next]
Click [Next]
Click [Next]
Click [Next]
Click [Finish]

And last but not least the deployment on the client:

DescriptionPicture
Find the application in Software Center.
Double click on the application.
Click [Install]
Close software center after a successful installation.
Start the application from the Start Menu
The addins are visible.
And the addins are used.




Create a MSIX modification package

In my article package and install a MSIX package I described the steps to install a single MSIX package. In this article I will create a nodification package. Paint.Net will be modified with a package that installs some addins.

The packaging order:

  1. Revert the packaging machine to a clean state.
  2. Install the main application and reboot afterwards to avoid pending reboots.
  3. Capture the installation of the dependency application (or modification package).
  4. Cleanup
  5. Test

Install the main application and capture the installation of the dependency application.

DescriptionPicture
Revert the VM to a clean state and install the main application. Reboot after the installation to avoid pending reboots.
Start the MSIX packaging tool and select modification package.
Enter the VM details.
Enter package information
Disable the services that you do not need.
Click [Next]
Start the addon package installation.
Click [Next] when done.
You can't start the application as it does not have any shortcuts.
Save the 'raw' version.
Click [Create]
Click [Close]
Enter the package details, including certificate details.
Perform the cleanup.
Save the cleaned up version.
Click [Close]

Testing the dependency application with PowerShell.

If you use PowerShell to import the certificate you can skip the steps to import the certificate manually.

DescriptionPicture
Start the certificate manager to import a certificate.
Browse to pfx file,
Enter the password.
Add the certificate to the Trusted Root Certification Authorities.
Click [Finish]
Click [Ok]
Start Settings -> Updates and Security -> For Developpers.
Enable sideload apps,
Click [Close]
You cannot install the update package first. It will throw up an error.
Install the main application first, followed by the update package.
And the addons are used (1-2)
And the addons are used (2-2)
Alternatively, you can install both the main and update package in one PowerShell command.
And a progress bar.




Package and install a MSIX package

MSIX is the new packaging format to create new packages. It is aviable on Windows 10 v1809 and later. According to Microsoft packaging has never been easier.

In this tutorial I will create a Paint.Net 4.1.5 package on a dedicated Hyper-V Virtual Machine. The package will be deployed with PowerShell.

The order:

    1. Prepare the packaging client
      • At least Windows 10 v1809
      • Participate in the Windows Insider Program

        Windows Insider Program

        Windows Insider Program

      • Install the MSIX Packaging Tool from the Windows Store

        MSIX Packaging Tool

        MSIX Packaging Tool

    2. Create a self signed certificate to sign the MSIX package
      You can do that with PowerShell

      $CurrentDir     = Split-Path -parent $MyInvocation.MyCommand.Definition
      $CertName       = $CurrentDir + "\Test.pfx"
      $PassWord       = "P@ssw0rd"
      $HashedPassWord = ConvertTo-SecureString -String $PassWord -Force -AsPlainText
      $Org            = "CN=Contoso1"
      $FriendlyName   = "Test certificate"
      $ExpDate        = "01-01-2030 00:00:00"
      
      New-SelfSignedCertificate -Type Custom -Subject $Org -KeyUsage DigitalSignature -FriendlyName $FriendlyName -NotAfter $ExpDate -CertStoreLocation "Cert:\LocalMachine\My"
      
      $TP = Get-ChildItem cert: -Recurse |Where-Object{$_.FriendlyName -match $FriendlyName} |Select-Object -ExpandProperty Thumbprint
      
      Export-PfxCertificate -Cert Cert:\LocalMachine\My\$TP -FilePath $CertName -Password $HashedPassWord
      Remove-Item Cert:\LocalMachine\My\$TP 
      

    3. Prepare the packaging virtual machine
      You can choose from:

    4. Capture the Paint.Net 4.1.5 installation
    5. Revert the VM to a clean state
    6. Install the Paint.Net package with PowerShell

Capture the installation

DescriptionPicture
Start the MSIX packaging tool and click on Create your app package
Browse to the installer...
... and select the installer.
Click [Next]
Select the VM you want to use to perform the installation on.
Enter the login details.
Wait while the VM is prepared.
Enter the package details. Once done, click [Next]
Please wait while the package driver is installed on the VM. It can take a long time.
Disable the services you do not need.
Click [Next]
Start the installation.
Click [Next]
Disable the automatic update function.
Click [Next]
Once installed, do not start the application.
Click [Next]
Start the application from here.
The changes are still captured.
The application is started.
Once started, close it.
Click [Next]
Click [Move on]
Save the 'raw' version in the correct folder. So you always have a raw version before performing the clean up.
Click [Create]
Click [Close]
Now we are starting the cleanup and assigning the certificate to it.
Open the package
Open the 'raw' package.
Open the package.
Specify the certificate (pfx file).
Enter the certificate details.
Perform the cleanup.
And we are sure that we want to delete an entry.
Go to the package files...
... and delete all unneeded files.
Save the cleaned up package
Close.

Install the MSIX package

Revert the machine to a clean state.

Before you can install the MSIX package you have to install the certificate that you created earlier.

$CurrentDir     = Split-Path -parent $MyInvocation.MyCommand.Definition
$CertName       = $CurrentDir + "\Test.pfx"
$PassWord       = "P@ssw0rd"
$HashedPassWord = ConvertTo-SecureString -String $PassWord -Force -AsPlainText

Import-PfxCertificate -FilePath $CertName -CertStoreLocation Cert:\LocalMachine\Root -Password $HashedPassWord

In the certificate manager:

If you use PowerShell to import the certificate you can skip the steps to import the certificate manually.

DescriptionPicture
Start the certificate manager to import a certificate.
Browse to pfx file,
Enter the password.
Add the certificate to the Trusted Root Certification Authorities.
Click [Finish]
Click [Ok]
Start Settings -> Updates and Security -> For Developpers.
Enable sideload apps,
Click [Close]
Install the application with powershell.
The progress bar....
Start the application via the Start Menu
The application has been started successfully

You can download the ZIP file with the certificates here.




Performance issues on HP laptop after increasing memory

New internal memory

New internal memory

I have a HP 4740s laptop with 8Gb of RAM. According to HP the maximum memory size is 8Gb, but according to the motherboard specs, there could be 32Gb of memory in it.

So I tried to extend the memory from 8Gb to 16Gb. After doing that, starting up the computer took ages. When returning to 8Gb the performance was back to normal.

It seems that I was not the only one with this issue. I found the solution on YouTube:

In short:

  1. Make sure that the computer has 8Gb of internal memory
  2. Start the computer, start msconfig, select the tab Boot, click Advanced options and change the Maxium memory to 8192.
  3. Reboot, and make sure that the computer has a good performance
  4. Shutdown the computer, and extend the memeory to 16Gb
  5. Start the computer, start msconfig, select the tab Boot, click Advanced options and change the Maxium memory to 16384.
  6. Reboot

Of course all the credits are for mecomex.




Install all the MSI installation files in a folder

All the files in a folder

All the files in a folder

If you want to install a bunch of MSI files you put them in a folder and install them with a batch file. There is a downsize: you must modify the batch file for each situation. With this PowerShell script you can install all the MSI files in the folder, including applying transform and patch files. You can add your own properties in a csv settings file.

There are some parameters:

  • Install: Use this swith to specify an installation.
  • Uninstall: Use this swith to specify an uninstall.
  • MSIPath: Specify the location where the MSI files are located.
  • Loglocation: Specify the logfile location.

The default log location is C:\Windows\system32\LogFiles.

You can add a transform file.

You can add a transform file.

You can specify a settings file for each application.

You can specify a settings file for each application.

The default settings file is applied to all installations in the folder.

The default settings file is applied to all installations in the folder.

You can also use SCCM to install all the MSI files in the folder. Create an application with one deployment type. You can add all the product codes from each MSI file to identify a successful installation.

SCCM: The 'Application Catalog' tab.

SCCM: The ‘Application Catalog’ tab.

SCCM: Program

SCCM: Program

The Install line is:

The Uninstall line is:

SCCM: Detection rule

SCCM: Detection rule

Installation on the client.

Installation on the client.

Uninstall on the client.

Uninstall on the client.

A demonstration of this script can be found on my YouTube channel: the script and AppV Repository or view it below:

The script:

<#

.SYNOPSIS
    Installs all MSI's in a folder. By default, it is the folder where the script is located, but you can specify another location.
 
.DESCRIPTION
    Installs all MSI's in a folder. By default, it is the folder where the script is located, but you can specify another location.
    If there is a MST that starts with the MSI name, then the MSI / MST combination will be installed.   
    If there is a MSP that starts with the MSI name, then the MSI / MSP combination will be installed. 

    Valid combinations:
    -> MSI file: Application_1.20.msi
    -> MST file: Application_1.20_transform.mst
    -> MSP file: Application_1.20_Update_to_1.40.msp
    -> CSV file: Application_1.20.csv

    Invalid combinations:
    -> MSI file: Application_1.20.msi
    -> MST file: Application_1.20.msi.mst     (.msi should be removed)
    -> MST file: Application_patch.msp        (not the full msi file name)
 
.EXAMPLE
     Installs all the MSIs that are in the folder where the script is located.
     ."\install_all_msi (v10).ps1"

.EXAMPLE
     Installs all the MSIs that are in the folder where the script is located.
     ."\install_all_msi (v10).ps1" -Install

.EXAMPLE
     Uninstalls all the MSIs that are in the folder \\server\share\MSIs.
     ."\install_all_msi (v10).ps1" -Uninstall -MSIPath \\server\share\MSIs

.EXAMPLE
     Uninstalls all the MSIs that are in the folder \\server\share\MSIs. Logfiles are written to C:\Logs
     ."\install_all_msi (v10).ps1" -Uninstall -MSIPath \\server\share\MSIs -LogLocation C:\Logs

.NOTES
    Author:  Willem-Jan Vroom
    Website: https://www.vroom.cc/
    Twitter: @TheStingPilot

 v0.1:
   * Initial version.

 v1.0:
   * Added: properties handler:
         - defaultproperties.csv -> will be applied to all MSI packages or MSI / MST combination in the folder.
                                    This file can be in either the location where the script is, or in the location
                                    where the MSI files are. 
         - .csv     -> will be applied only to the given MSI or MSI / MST combination.
                                    This file must be in the same directory as the MSIs.
     This file must have the following header layout:
         Property,Value
         ALLUSERS,1
         ADDLOCAL,ALL
     The content may be different. 
   * Install and uninstall switch added.
   * Check if user has admin rights. It throws up an error in case not.
   * Added the command line options MSIPath and LogLocation.
   * Added patch support. The pathname must start with the same name as the MSI.
   * Check if both install and uninstall switches are used.
   * Bugfix: error messages when there are quotes around the MSI file name.

#>

[CmdLetBinding()]

param
 (
  # Use this switch to specify an installation.
  [Parameter(Mandatory=$False)]
  [Switch] $Install,

  # Use this switch to specify an uninstall.
  [Parameter(Mandatory=$False)]
  [Switch] $Uninstall,

  # Specify the location where the MSI files are located.
  [Parameter(Mandatory=$False)]
  [String] $MSIPath = "",

  # Specify the logfile location.
  [Parameter(Mandatory=$False)]
  [String] $LogLocation = ""
 )

# ========================================================================================================================
# Function block
# ========================================================================================================================

  Function CreateLogFile
  {

   <#
   .NOTES
   ========================================================================================================================
   Created with:     Windows PowerShell ISE
   Created on:       9-January-2019
   Created by:       Willem-Jan Vroom
   Organization:     
   Functionname:     CreateLogFile
   ========================================================================================================================
   .SYNOPSIS

   This function creates the logfile 

   #>

   param
    (
     [string] $LogFile
    )

   New-Item $LogFile -Force -ItemType File | Out-Null
  }
 
Function WriteToLog
 {
  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       9-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     WriteToLog
  ========================================================================================================================
  .SYNOPSIS

  This function adds a line to the logfile

  #>

  param
     (
      [string] $LogFile,
      [string] $line
     )

   $timeStamp = (Get-Date).ToString('G').Replace("/","-")
   $line = $timeStamp + " - " + $line
   Add-Content -Path $LogFile -Value $line -Force
 }

 Function Import-PropertyFile
  {
   <#
   .NOTES
   ========================================================================================================================
   Created with:     Windows PowerShell ISE
   Created on:       9-January-2019
   Created by:       Willem-Jan Vroom
   Organization:     
   Functionname:     Import-PropertyFile
   ========================================================================================================================
   .SYNOPSIS

   This function imports all the properties that are mentioned in the given property-file 

   #>

   param
    (
     [string] $PropertyFile
    )

   $arrItems      = @()
   $strProperties = ""

   if(Test-Path $PropertyFile)
    {
     $arrItems = @(Import-CSV $PropertyFile)

     if($arrItems.Count -ge 1)
      {
       WriteToLog -LogFile $strLogFile -line "The property file $PropertyFile is applied."
       ForEach($objItem in $arrItems)
        {
         $strProperty    = $objItem.Property
         $strValue       = $objItem.Value
         $strLine        = $strProperty + "=" + $strValue + " "
         $strProperties += $strLine 
        }
       }
    }
    
    Return $strProperties
  }

Function Get-MSIFileInformation
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       9-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Get-MSIFileInformation
  ========================================================================================================================
  .SYNOPSIS

  This function reads the various properties from a MSI file. 
  This function has been found on http://www.scconfigmgr.com/2014/08/22/how-to-get-msi-file-information-with-powershell/
  All credits, including the copyright go to Nickolaj Andersen.

  #>


  param
   (
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [System.IO.FileInfo]$Path,
 
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [ValidateSet("ProductCode", "ProductVersion", "ProductName", "Manufacturer", "ProductLanguage", "FullVersion")]
    [string]$Property
   )
  
  Process 
   {
    try 
     {
        # Read property from MSI database
        $WindowsInstaller = New-Object -ComObject WindowsInstaller.Installer
        $MSIDatabase = $WindowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $WindowsInstaller, @($Path.FullName, 0))
        $Query = "SELECT Value FROM Property WHERE Property = '$($Property)'"
        $View = $MSIDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $MSIDatabase, ($Query))
        $View.GetType().InvokeMember("Execute", "InvokeMethod", $null, $View, $null)
        $Record = $View.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $View, $null)
        $Value = $Record.GetType().InvokeMember("StringData", "GetProperty", $null, $Record, 1)
         
        # Commit database and close view
        $MSIDatabase.GetType().InvokeMember("Commit", "InvokeMethod", $null, $MSIDatabase, $null)
        $View.GetType().InvokeMember("Close", "InvokeMethod", $null, $View, $null)           
        $MSIDatabase = $null
        $View = $null
 
        # Return the value
        return $Value
     } 
    catch 
     {
      Write-Warning -Message $_.Exception.Message ; break
     }
  }
  End 
  {
    # Run garbage collection and release ComObject
    [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WindowsInstaller) | Out-Null
    [System.GC]::Collect()
  }
}

Function Check-HasAdminRights
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       11-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Check-HasAdminRights
  ========================================================================================================================
  .SYNOPSIS

  This function checks if an user has admin rights. The function returns $true or $false

  #>

  If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
   {
    Return $True
   }
    else
   {
    Return $False
   }
 }

 Function Remove-TrailingCharacter
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       18-January-2019
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-TrailingCharacter
=============================================================================================================================================
.SYNOPSIS

This function removes a trailing character from a string
#>

param
(
 [string] $RemoveCharacterFrom = "",
 [string] $Character           = "" 
)

if($RemoveCharacterFrom.Length -gt 0)
 { 
  if(($RemoveCharacterFrom.SubString($RemoveCharacterFrom.Length-1,1)) -eq $Character) 
  {
   $RemoveCharacterFrom = $RemoveCharacterFrom.Substring(0,$RemoveCharacterFrom.Length-1)
  }
 }


 Return $RemoveCharacterFrom

}

 Function Add-TrailingCharacter
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-TrailingCharacter
=============================================================================================================================================
.SYNOPSIS

This function adds a trailing backslash to a string
#>

param
(
 [string] $AddCharacterTo = "",
 [string] $Character      = ""
)

if($AddCharacterTo.Length -gt 0)
 { 
  $AddCharacterTo = Remove-TrailingCharacter -RemoveCharacterFrom $AddCharacterTo -Character $([char]34)
  if(($AddCharacterTo.SubString($AddCharacterTo.Length-1,1)) -ne $Character) 
  {
   $AddCharacterTo = $AddCharacterTo + $Character
  }
 }
Else
 {
  $AddCharacterTo = $Character
 }

 Return $AddCharacterTo

}

Function Get-AllFilesWithPattern
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       13-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Get-AllFilesWithPattern
  ========================================================================================================================
  .SYNOPSIS

  Find all files in the given folder that matches a filter.

  #>

  param
   (
    [string] $FolderToLookIn,
    [string] $Pattern
   )

   $FolderToLookIn = Remove-TrailingCharacter -Character "\" -RemoveCharacterFrom $FolderToLookIn

   $arrItems = @()
   $arrItems = Get-ChildItem -Path $FolderToLookIn -Filter $Pattern
   $arrItems | Sort-Object -Property Name | Out-Null

   Return $arrItems
 }

Function Get-LastItemOfAnArryAndPutItInAString
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       13-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Get-LastItemOfAnArryAndPutItInAString
  ========================================================================================================================
  .SYNOPSIS

  Returns the last item of string.

  #>

  param
   (
    [string] $FileName,
    [string] $OldExtension,
    [string] $NewExtension,
    [string] $WhereToLook
   )

   $arrFiles    = @()
   $strFileName = ""
   $FilePattern = $FileName -Replace($OldExtension,$NewExtension)
   $arrFiles    = Get-AllFilesWithPattern -FolderToLookIn $WhereToLook -Pattern $FilePattern
   
   if($arrFiles.Count -gt 0)
    {
     $strFileName = $arrFiles[-1].ToString()
    }
 
   Return $strFileName
}

# ========================================================================================================================
# End function block
# ========================================================================================================================

# ========================================================================================================================
# Define the variables.
# ========================================================================================================================

  $strCurrentDir          = Split-Path -parent $MyInvocation.MyCommand.Definition

  if($MSIPath.Length -eq 0)
   {
    $MSIPath = $strCurrentDir
   }
  
  if($LogLocation.Length -eq 0)
   {
    $LogLocation = $Env:Windir + "\SYSTEM32\LogFiles"
   }
      
  $LogLocation            = Add-TrailingCharacter -AddCharacterTo $LogLocation   -Character "\"
  $MSIPath                = Add-TrailingCharacter -AddCharacterTo $MSIPath       -Character "\"
  $strCurrentDir          = Add-TrailingCharacter -AddCharacterTo $strCurrentDir -Character "\"

  $strTransform           = ""
  $strDefaultPropFile     = $MSIPath + "defaultproperties.csv"

  $strDefaultProperties   = ""
  $strActivity            = ""
  $strPatch               = ""
  $strSingleOrMultipleMSI = "MultipleMSI"

  $arrDefaultProperties   = @()
  $arrMSIFiles            = @()
  $arrMSPFiles            = @()
  $arrMSIFiles            = Get-AllFilesWithPattern -FolderToLookIn $MSIPath -Pattern "*.msi"
  $numMSIFiles            = $arrMSIFiles.Count
  $numCounter             = 1

# ========================================================================================================================
# Stop the script for a non admin user
# ========================================================================================================================

  if(-not(Check-HasAdminRights))
  {
   Write-Error "The current user has no admin rights. Please rerun the script with elevated rights." -Category PermissionDenied
   Exit 999
  }

# ========================================================================================================================
# Create the log file location if not exists
# ========================================================================================================================

  if(-not (Test-Path $LogLocation))
   {
    New-Item -Path $LogLocation -ItemType Directory -Force -Confirm:$False | Out-Null
   }

# ========================================================================================================================
# Define the logfile for the install or uninstall of all the MSIs. 
# ========================================================================================================================
 
  $strLastPartOfFileName = " (" + (Get-Date).ToString('G') + ").log"
  $strLastPartOfFileName = $strLastPartOfFileName.Replace(":","-").Replace("/","-")

  if($numMSIFiles -eq 1)
   {
    $strSingleOrMultipleMSI = "SingleMSI"
   }

  if($Install -or (-not $Uninstall))
   {
    $strLogFile            = $LogLocation + $strSingleOrMultipleMSI + $strLastPartOfFileName
    $strActivity           = "Installing MSIs in the folder $MSIPath"
   }
    else
   {
    $strLogFile            = $LogLocation + "Uninstall" + $strSingleOrMultipleMSI + $strLastPartOfFileName
    $strActivity           = "Uninstalling MSIs in the folder $MSIPath"
   }
 
  CreateLogFile -LogFile $strLogFile

# ========================================================================================================================
# Give an error message if both parameters install and uninstall are used. 
# ========================================================================================================================

if($Install -and $Uninstall)
 {
  $strErrorMessage = "Both install and uninstall parameters are mentioned. That is not possible. Only one of them should be used."
  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
  WriteToLog -line "FATAL ERROR!" -LogFile $strLogFile
  WriteToLog -line $strErrorMessage -LogFile $strLogFile
  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
  Write-Error $strErrorMessage -Category InvalidArgument
  Exit 991
 }

# ========================================================================================================================
# Write default settings to the logfile
# ========================================================================================================================

  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
  WriteToLog -line "Log location: $LogLocation" -LogFile $strLogFile
  WriteToLog -line "MSI Path:     $MSIPath"     -LogFile $strLogFile
  WriteToLog -line $($strActivity + ":")        -LogFile $strLogFile

  ForEach ($objMSIFile in $arrMSIFiles)
   {
    WriteToLog -line " * $($objMSIFile.Name)" -LogFile $strLogFile
   }
  WriteToLog -line "========================================================================================================================" -LogFile $strLogFile

# ========================================================================================================================
# In case of an installation:
# Define the default properties.
# ========================================================================================================================

  if ($Install -or (-not $Uninstall))
   {
    $strDefaultProperties = Import-PropertyFile -PropertyFile $strDefaultPropFile
   }
  
# ========================================================================================================================
# Start the real installation or uninstall.
# The installation is skipped if a MSI has already been installed.
# The uninstall is only done if the product has already been installed.
# ========================================================================================================================

  ForEach ($objMSIFile in $arrMSIFiles)
   {
    Write-Progress -Activity $($strActivity + ".") -Status "Processing $objMSIFile." -PercentComplete ($numCounter / $numMSIFiles * 100)
    $strMSIFileName    = $MSIPath + $objMSIFile
    $strProductName    = Get-MSIFileInformation -Path $strMSIFileName -Property ProductName
    $strProductVersion = Get-MSIFileInformation -Path $strMSIFileName -Property ProductVersion
    $strProductCode    = Get-MSIFileInformation -Path $strMSIFileName -Property ProductCode
    $strRegPathX64     = "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\"+$strProductCode
    $strRegPathX64     = $strRegPathX64 -replace(" ","")
    $strRegPathX86     = $strRegPathX64.Replace("WOW6432Node\","")
    
    if($Install -or (-not $Uninstall))
     {
           
      # ========================================================================================================================
      # The install
      # ========================================================================================================================

      $strTransform      = ""
      $strPatch          = ""
      $strMSTFileName    = Get-LastItemOfAnArryAndPutItInAString -FileName $objMSIFile -OldExtension ".msi" -NewExtension "*.mst" -WhereToLook $MSIPath
      $strMSPFileName    = Get-LastItemOfAnArryAndPutItInAString -FileName $objMSIFile -OldExtension ".msi" -NewExtension "*.msp" -WhereToLook $MSIPath
      $strPropFile       = $strMSIFileName.Replace("msi","csv")

      # ========================================================================================================================
      # Apply a patch (msp file) (if available)
      # ========================================================================================================================

      if($strMSPFileName.Length -ge 1)
       {
        $strPatch = " /update " + $([char]34) + $MSIPath + $strMSPFileName + $([char]34)
        WriteToLog -LogFile $strLogFile -line "The patch file '$strMSPFileName' has been found and is applied."
       }

      # ========================================================================================================================
      # Apply a transform file (mst) (if available)
      # ========================================================================================================================

      if($strMSTFileName.Length -ge 1)
       {
        $strMSTFileName = $MSIPath + $strMSTFileName
        $strTransform   = "TRANSFORMS=" + $([char]34) + $strMSTFileName + $([char]34)+" "
        WriteToLog -LogFile $strLogFile -line "The transform file '$strMSTFileName' has been found and is applied."
       }

      WriteToLog -line "Installing application: $strProductName"    -LogFile $strLogFile
      WriteToLog -line "ProductVersion:         $strProductVersion" -LogFile $strLogFile

      if(-not ((Test-Path $strRegPathX64) -or (Test-Path $strRegPathX86)))
       {
        $strProperties     = " "
        $strProperties     = Import-PropertyFile -PropertyFile $strPropFile
        $strMSILogFile     = "/l*v " + $([char]34) + $LogLocation + $strProductName + " " + $strProductVersion +".log" + $([char]34)
        $strArguments      = "/i "   + $([char]34) + $MSIPath + $objMSIFile + $([char]34) + $strPatch + " /qb! " + $strDefaultProperties + $strProperties + $strTransform + $strMSILogFile

        $strArguments      = $strArguments  -replace("   ","")
      
        WriteToLog -line "Command that is run:       msiexec $($strArguments)" -LogFile $strLogFile
        $StartProcess = (Start-Process -FilePath "msiexec.exe" -ArgumentList $strArguments -Wait -PassThru)
        WriteToLog -line "Result: $($StartProcess.ExitCode)" -LogFile $strLogFile
        WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
       }
        else
       {
        WriteToLog -line "This application has already been installed, thus skipping." -LogFile $strLogFile
        WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
       }
     }
      else
     {   
        
      # ========================================================================================================================
      # The uninstall
      # ========================================================================================================================

      WriteToLog -line "Uninstalling application: $strProductName"    -LogFile $strLogFile
      WriteToLog -line "ProductVersion:           $strProductVersion" -LogFile $strLogFile
      
      if((Test-Path $strRegPathX64) -or (Test-Path $strRegPathX86))
       {
        $strMSILogFile     = "/l*v " + $([char]34) + $LogLocation + "Uninstall_"+ $strProductName + " " + $strProductVersion +".log" + $([char]34)
        $strArguments      = "/x "   + $strProductCode + " /qb! " + $strMSILogFile

        $strArguments      = $strArguments  -replace("   ","")

        WriteToLog -line "Command that is run:         msiexec $($strArguments)" -LogFile $strLogFile
        $StartProcess = (Start-Process -FilePath "msiexec.exe" -ArgumentList $strArguments -Wait -PassThru)
        WriteToLog -line "Result: $($StartProcess.ExitCode)" -LogFile $strLogFile

        WriteToLog -line "========================================================================================================================" -LogFile $strLogFile
       }
        else
       {
        WriteToLog -line "This application has not been installed, thus skipping." -LogFile $strLogFile
        WriteToLog -line " " -LogFile $strLogFile
       }
     }
    $numCounter++
   }

# ========================================================================================================================
# Done!
# ========================================================================================================================

Current version: install_all_msi (v10).zip




All expired user accounts and the accounts that are about to expire

A colleague of mine asked me to write a script with the Active Directory users with an account about to expire. So I created this script for him:

Account with an expiry date.

Account with an expiry date.


$arrUsers = @()
$arrOU    = @("OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan","OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan")

ForEach($objOU in $arrOU)
 {
  $arrUsers += (Search-ADAccount -AccountExpiring -SearchBase $objOU -UsersOnly) | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},"Enabled","LastLogonDate","LockedOut","ObjectClass","PasswordExpired","PasswordNeverExpires","SamAccountName","UserPrincipalName"
 }
$arrUsers | Sort-Object -Property "OU","Name" | Export-Csv "c:\temp\usersabouttoexpire.csv" -NoTypeInformation

It does what it should do, but not flexible. It can be used in one situation but needs modification for other situations. Also, you cannot specify the number of days when the account is about to expire.

So I created the script AccountsAboutToExpire (v10).ps1.

There are some parameters:

  • OUs: Specify the OUs, seperated by a comma.
  • IncludeChildOUs: Use this switch to include the child OU’s.
  • NumberOfDaysToExpirationDate: Specify the number of days in which the account expires. Default = 7

This script does not only give the accounts that are about to expire, but also the expired user accounts.

A demonstration of this script can be found on my YouTube channel: the script and AppV Repository or view it below:

The script:

<#
.SYNOPSIS
    Gives the accounts that about to expire within the given period.

.DESCRIPTION
    This script gives the accounts that are about to expire within the given period in a CSV file.

.EXAMPLE
    Reports all the accounts that are about to expire in the OUs OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan and OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan
    ."\AccountsAboutToExpire V10).ps1" -OUs "OU=Users,OU=OU1,DC=testdomain,DC=local,DC=lan","OU=Users,OU=OU2,DC=testdomain,DC=local,DC=lan"

.EXAMPLE
    Reports all the accounts that are about to expire in the OU DC=testdomain,DC=local,DC=lan, including the child OU's
    ."\AccountsAboutToExpire V10).ps1" -OUs "DC=testdomain,DC=local,DC=lan" -IncludeChildOUs

.EXAMPLE
    Reports all the accounts that are about to expire witin 60 days in the OU DC=testdomain,DC=local,DC=lan, including the child OU's
    ."\AccountsAboutToExpire V10).ps1" -OUs "DC=testdomain,DC=local,DC=lan" -IncludeChildOUs -NumberOfDaysToExpirationDate 60

.NOTES
    Author:  Willem-Jan Vroom
    Website: 
    Twitter: @TheStingPilot

v0.1:
   * Initial version. 

v1.0:
   * Included:
      - All the expired user accounts
      - Sort on expiration date. 
#>

[CmdLetBinding()]

param
(
# Specify the OUs, seperated by a comma. 
[Parameter(Mandatory=$True)]
[string[]] $OUs,

# Use this switch to include the child OU's. 
[Parameter(Mandatory=$False)]
[switch]  $IncludeChildOUs,

# Specify the number of days in which the account expires. Default = 7
[Parameter(Mandatory=$False)]
[string]  $NumberOfDaysToExpirationDate=7
)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

  Function Get-AccountsAboutToExpire
   {
  
    param
     (
      [string] $OU,
      [Switch] $InclChildOUs,
      [string] $NumDays
     )

    $arrItems       = @()
    $arrAboutToExp  = New-Object PSObject
    $arrExpired     = New-Object PSObject
    $strSearchScope = "OneLevel"

    if($InclChildOUs)
     {
      $strSearchScope = "SubTree"
     } 

    Try
     {
      $arrAboutToExp  = (Search-ADAccount -AccountExpiring -SearchBase $objOU -UsersOnly -SearchScope $strSearchScope -TimeSpan $NumDays) | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},@{Name="ExpiredAccount";Expression={$False}},"Enabled","LastLogonDate","LockedOut","PasswordExpired","PasswordNeverExpires"
      $arrExpired     = (Search-ADAccount -AccountExpired  -SearchBase $objOU -UsersOnly -SearchScope $strSearchScope)                    | Select-Object "AccountExpirationDate","Name",@{Name="OU";Expression={($_."DistinguishedName" -split "=",3)[-1]}},@{Name="ExpiredAccount";Expression={$True}}, "Enabled","LastLogonDate","LockedOut","PasswordExpired","PasswordNeverExpires" 
      $arrItems      += $arrAboutToExp
      $arrItems      += $arrExpired
     }
      Catch
     {
      Write-Warning "The OU $OU has not been found."
     }

    Return $arrItems

   }

Function Check-HasAdminRights
 {

  <#
  .NOTES
  ========================================================================================================================
  Created with:     Windows PowerShell ISE
  Created on:       11-January-2019
  Created by:       Willem-Jan Vroom
  Organization:     
  Functionname:     Check-HasAdminRights
  ========================================================================================================================
  .SYNOPSIS

  This function checks if an user has admin rights. The function returns $true or $false

  #>

  If (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator"))
   {
    Return $True
   }
    else
   {
    Return $False
   }
 }

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# ========================================================================================================================
# Stop the script for a non admin user
# ========================================================================================================================

  if(-not(Check-HasAdminRights))
  {
   Write-Error "The current user has no admin rights. Please rerun the script with elevated rights." -Category PermissionDenied
   Exit 999
  }

# =============================================================================================================================================
# Check if the module ActiveDirectory has been loaded.
# =============================================================================================================================================

  if(-not(Get-Module -ListAvailable ActiveDirectory))
   {
    Write-Warning "The module ActiveDirectory is not found. Thus quitting."
    Exit 9
   }

# =============================================================================================================================================
# Define the variables
# =============================================================================================================================================

  $arrUsers      = @()
  $strCurrentDir = Split-Path -parent $MyInvocation.MyCommand.Definition
  
# =============================================================================================================================================
# Define the output file
# =============================================================================================================================================

  $strOutputFile = "UserAccounts About to Expire (" + (Get-Date).ToString('G') + ").csv"
  $strOutputFile = $strOutputFile.replace(":","-").Replace("/","-")
  $strOutputFile = $strCurrentDir + "\" + $strOutputFile  

# =============================================================================================================================================
# Process the OUs.
# =============================================================================================================================================

  ForEach($objOU in $OUs)
   {
    if($IncludeChildOUs)
     {
      $arrUsers += Get-AccountsAboutToExpire -OU $objOU -NumDays $NumberOfDaysToExpirationDate -InclChildOUs
     }
      else
     {
      $arrUsers += Get-AccountsAboutToExpire -OU $objOU -NumDays $NumberOfDaysToExpirationDate
     }
   }

# =============================================================================================================================================
# Write the output to a csv file.
# =============================================================================================================================================
  
  $arrUsers | Sort-Object -Property "AccountExpirationDate","OU","Name" | Export-Csv $strOutputFile -NoTypeInformation

Get default help.

Get default help.

 

Get detailed help.

Get detailed help.

 

Output in Excel.

Output in Excel.

 

Current version: AccountsAboutToExpire (v10)

Previous versions:




Improved script to migrate users from Windows 7 to Windows 10 automatically

In my article, I wrote about a script to perform a user migration. In the meantime, that script has been improved to perform a more efficient and errorless migration.

The following improvements have been implemented:

  • Provide a mappingsfile for an automatic migration. That mappingsfile contains both the old and the new group name.
  • Perform a rollback in case of inpredicted results.
  • In case of different profile locations: copy the old desktop and the old favorites to a new location.
  • Improved help

To do this, I created a Powershell script.

The users who need to be migrated from Windows 7 to Windows 10 are in a csv file:

  • The column Userid contains the userid
  • The column NewOU contains the OU where the user should be moved to. This column can be empty.
  • The colum VDIGroup contains the new VDI group. This column can be empty.
  • The colum GroupsToAdd contains the groups the user should be added to. Split the groups with a comma. This column can be empty.
  • The colum GroupsToRemove contains the groups the user should be removed from. Split the groups with a comma. This column can be empty.

An example:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

Parameters for a migration:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.
  • FileForAutomaticMigration: CSV filename that contains the old and new group name for a fully automated migration. If not specified, then there is no automatic migration. Default is empty.
  • LogFilePrefix: The name the logfile starts with. So the logfiles are grouped together.
  • ProductionRun: Use this swith to update Active Directory. If not specified, the script is run in test mode.
  • CreateRollBackFile: Use this switch to create a rollback file. That makes it possible to perform a rollback in case of unpredicted behavior.
  • FullCleanUp: Use this swith to remove all unneeded groups.
  • RemoveOldADGroups: Use this switch to remove the old AD groups in case of an automated migration.
  • ClearProfilePath: Use this switch to clear the profile path.
  • ClearHomeFolder: Use this swicth to clear the drive mapping and home folder.
  • RemoveOldCitrixGroups: Use this switch to remove all the old Citrix groups from the users’ account.
  • RunDirectGroupInventory: Use this switch to display all the direct group membership in the result file.
  • RunIndirectGroupInventory: Use this switch to display all the indirect group membership in the result file.

Parameters for an user inventory, based on an OU:

  • CreateFileWithUseridsInCSVFormat: Use this switch to create a File with all the userids to migrate based on an OU.
  • srcOU: The OU (distinguised name) where all the userids are found that needs to be migrated. If the OU contains spaces, then add quotes around the OU name.
  • dstOU: The destination OU (distinguised name). If this one can is empty then the users are not moved to another OU. If the OU contains spaces, then add quotes around the OU name.
  • Windows10VDIGroup: The new Windows 10 VDI group. If the VDI group contains spaces, then add quotes around it.

Parameters to copy the desktop and internet explorer favorites to another location:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.
  • ProfilePathFrom: The old profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
  • ProfilePathTo: The new profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
  • ProductionRun: Use this switch to update Active Directory. If not specified, the script is run in test mode.

The script:

<#
.SYNOPSIS
    Helps to migrate users from - for example - Windows 7 to Windows 10.
 
.DESCRIPTION
    This script helps to migrate users from Windows 7 to Windows 10. It offers the following options:
    * Run an inventory from all the users in an OU and al its sub OU's.
    * Move the users from one OU to another OU.
    * Add the users to a new group, based on a group mapping file.
    * Run the script is a test modus, so nothing is changed.
    * Check the users' indirect group membership.
    * Copy content from the old profile to the new profile.
 
.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' in test mode:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' and add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in test mode:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv
 
.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' and add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in production:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv', add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in production and create a rollback file:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -CreateRollBackFile

.EXAMPLE
     Perform a rollback in production
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat RBF_.csv -ProductionRun

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv' and add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' in production.
     Also run both the direct- and indirect group membership inventory:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -RunDirectGroupInventory -RunIndirectGroupInventory

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv', add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv' and remove the old group in production:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -RemoveOldADGroups

.EXAMPLE
     Perform the actions as described in the CSV Input File 'Userids-to-migrate.csv', add the groups as per mappingsfile 'MappingOldGroupToNewGroup.csv', remove the old group in production and run the inventory for indirect group membership:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -FileForAutomaticMigration MappingOldGroupToNewGroup.csv -ProductionRun -RemoveOldADGroups -RunIndirectGroupInventory

.EXAMPLE
     Create a csv file with all the userids to migrate from the OU testdomain.local.lan\OU1\Old OU
     ."\Migrate users (v10).ps1" -CreateFileWithUseridsInCSVFormat -srcOU "OU=Old OU,OU=OU1,DC=testdomain,DC=local,DC=lan"
     
.EXAMPLE
     Create a csv file with all the userids to migrate from the OU testdomain.local.lan\OU1\Ould OU. The new Citrix VDI group is 'Citrix10VDI'
     ."\Migrate users (v10).ps1" -CreateFileWithUseridsInCSVFormat -srcOU "OU=Old OU,OU=OU1,DC=testdomain,DC=local,DC=lan" -Windows10VDIGroup Citrix10VDI

.EXAMPLE
     Create a csv file with all the userids to migrate from the OU testdomain.local.lan\OU1\Ould OU. The new OU is testdomain.local.lan\OU1\OU2\Users. The new Citrix VDI group is 'Citrix10VDI'
     ."\Migrate users (v10).ps1" -CreateFileWithUseridsInCSVFormat -srcOU "OU=Old OU,OU=OU1,DC=testdomain,DC=local,DC=lan" -dstOU "OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan" -Windows10VDIGroup Citrix10VDI

.EXAMPLE
     Migrate the IE favorites and the desktop folder from the old profile to the new profile in test mode:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -copyProfile -ProfilePathFrom "\\server\share\w 7" -ProfilePathTo \\server\share\w10

.EXAMPLE
     Migrate the IE favorites and the desktop folder from the old profile to the new profile in production:
     ."\Migrate users (v10).ps1" -FileWithUseridsInCSVFormat Userids-to-migrate.csv -copyProfile -ProfilePathFrom "\\server\share\w 7" -ProfilePathTo \\server\share\w10 -ProductionRun

.NOTES
    Author:  Willem-Jan Vroom
    Website: https://www.vroom.cc/
    Twitter: @TheStingPilot

 v0.1:
   * Initial version.

 v0.2:
   * The logfilename has been changed.

 v0.3:
   * The logfile mentions 'RUNNING IN TEST MODE' in case the swith -ProductionRun is not used.

 v1.0:
   * Introduction automatic migration.
   * Introduction Indirect Group Inventory
   * Automatic creation of the File with the userids to migrate in CSV format.
   * Cleanup old Citrix groups
   * The results log file has a different lay-out
   * Parametersetnames
   * Improved help.
   * Copy desktop and favorites from the old profile to the new profile.
   * Clear the homedir path in Active Directory.
   * Rollback scenario has been added.
#>

[CmdLetBinding()]

param
(
[CmdletBinding(DefaultParameterSetName = "Default")]

# CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.
# The filename with the users to be modified.
#
# This file has the following layout:
#
# "Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
# "userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
# "userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
# "userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
# "userid4","","","","old_appl_group1,old_appl_group2"
#
# You can add more columns, but these columns are ignored. 
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[String] $FileWithUseridsInCSVFormat = "",

# CSV filename that contains the old and new group name for a fully automated migration. If not specified, then there is no automatic migration. Defualt is empty.
#
# This file has the following layout:
#
# "OldGroup","NewGroup"
# "gg_appl_old1","appl_new1"
# "gg_appl_old2","appl_new2"
# "gg_appl_old1","appl_new3"
# "gg_appl_old4","appl_new4"
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[String] $FileForAutomaticMigration = "",

# The name the logfile starts with. So the logfiles are grouped together.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]

[String] $LogFilePrefix = "ZZZ-Logfile_",

# Use this swith to update Active Directory. If not specified, the script is run in test mode.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]
[Switch] $ProductionRun,

# Use this swith to create a rollback file. That makes it possible to perform a rollback in case of unpredicted behavior.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $CreateRollBackFile,

# Use this swith to remove all unneeded groups.
# If this switch is used then the following groups will be removed from the users' account:
# * All gg_appl groups
# * WM-Users
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $FullCleanUp,

# Use this swith to remove the old AD groups in case of an automated migration.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RemoveOldADGroups,

# Use this swith to clear the profile path.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $ClearProfilePath,

# Use this swith to clear the drive mapping and home folder.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $ClearHomeFolder,

# Use this swith to remove all the old Citrix groups from the users' account.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RemoveOldCitrixGroups,

# Use this swith to display all the direct group membership in the result file.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RunDirectGroupInventory,

# Use this swith to display all the indirect group membership in the result file.
[Parameter(Mandatory=$False,
           ParameterSetName="Default")]
[Switch] $RunIndirectGroupInventory,

# Use this swith to create a File with all the userids to migrate based on an OU.
[Parameter(Mandatory=$False,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[Switch] $CreateFileWithUseridsInCSVFormat,

# The OU (distinguised name) where all the userids are found that needs to be migrated. If the OU contains spaces, then add quotes around the OU name.
[Parameter(Mandatory=$True,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[string] $srcOU,

# The destination OU (distinguised name). If this one can is empty then the users are not moved to another OU. If the OU contains spaces, then add quotes around the OU name.
[Parameter(Mandatory=$False,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[string] $dstOU,

# The new Windows 10 VDI group. If the VDI group contains spaces, then add quotes around it.
[Parameter(Mandatory=$False,
           ParameterSetName="CreateFileWithUseridsInCSVFormat")]
[string] $Windows10VDIGroup="",

# Use this switch if you want to copy desktop and favorites from the users' old profile to the users' new profile.
[Parameter(Mandatory=$False,
           ParameterSetName="CopyProfile")]
[switch] $copyProfile,

# The old profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
[Parameter(Mandatory=$True,
           ParameterSetName="CopyProfile")]
[string] $ProfilePathFrom="",

# The new profile path (exluding userid). If the profile path contains spaces, then add quotes around it.
[Parameter(Mandatory=$True,
           ParameterSetName="CopyProfile")]
[string] $ProfilePathTo=""

)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

Function Write-EntryToResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
=============================================================================================================================================
.SYNOPSIS

This function adds the success or failure information to the array that contains the log
information.

#>
param
 (
  [string] $strUserid,
  [string] $Result  = "",
  [string] $Action  = "",
  [string] $Message = ""
  
 )
 $Record                = [ordered] @{"Timestamp"="";"Username" = "";"Result"= "";"Action"= "";"Message"= ""}
 $Record."Timestamp"    = (Get-Date -UFormat "%a %e %b %Y %X").ToString()
 $Record."Username"     = $strUserid
 $Record."Result"       = $Result
 $Record."Action"       = $Action
 $Record."Information"  = $Message
 $objRecord             = New-Object PSObject -Property $Record
 $Global:arrTable      += $objRecord
}

Function Export-ResultsLogFileToCSV
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Export-ResultsLogFileToCSV
=============================================================================================================================================
.SYNOPSIS

This function writes the logfile content to a CSV file.

#>

 if($Global:arrTable.Count -gt 0)
   {
    $Global:arrTable | Export-Csv $strCSVLogFileSucces -NoTypeInformation
   } 
    Else
   {
    Write-Host "Something went wrong while writing the logfile $strCSVLogFileSucces. Maybe nothing to report..."
   } 
}

Function Export-RollBackFileToCSV
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       29-November-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Export-RollBackFileToCSV
=============================================================================================================================================
.SYNOPSIS

This function writes the rollbackfile content to a CSV file.

#>

 if($Global:arrTableWithRollBackRecords.Count -gt 0)
   {
    $Global:arrTableWithRollBackRecords | Export-Csv $strCSVRollBackFile -NoTypeInformation
   } 
    Else
   {
    Write-Host "Something went wrong while writing the file $strCSVRollBackFile. Maybe nothing to report..."
   } 
}

Function Remove-ProfilePathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ProfilePathFromUserProfileInAD
=============================================================================================================================================
.SYNOPSIS

This function clears the ProfilePath from AD.

#>

  param
   (
    [string] $strUserid
   )

   Write-EntryToResultsFile -strUserid $strUserid -Message "Profilepath: $((Get-ADUser -Identity $strUserid -Properties profilePath).profilePath)" -Action "Inventory" -Result "Success"

   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear profilePath -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear profile path" -Result "Success"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Clear profile path" -Result "Error"
       Continue
      }
}

Function Remove-HomeFolderPathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-HomeFolderPathFromUserProfileInAD
=============================================================================================================================================
.SYNOPSIS

This function clears the Home Folder Path from AD.

#>

  param
   (
    [string] $strUserid
   )

   Write-EntryToResultsFile -strUserid $strUserid -Message "Homedrive:     $((Get-ADUser -Identity $strUserid -Properties homeDrive).homeDrive)" -Action "Inventory" -Result "Success"
   Write-EntryToResultsFile -strUserid $strUserid -Message "Homedirectory: $((Get-ADUser -Identity $strUserid -Properties homeDirectory).homeDirectory)" -Action "Inventory" -Result "Success"

   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear homeDirectory -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear homedirectory path" -Result "Success"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Clear homedirectory path" -Result "Error"
       Continue
      }

    Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear homeDrive -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear homedrive" -Result "Success"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Clear homedrive" -Result "Error"
       Continue
      }
}

Function Move-ADUserToOtherOU
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Move-ADUserToOtherOU
=============================================================================================================================================
.SYNOPSIS

This function moves the user to another OU.

#>

  param
   (
    [string] $strUserid,
    [string] $strDestinationOU
   )
   Try
     {
      if($strDestinationOU.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Move-ADObject -Identity $strUserDN -TargetPath $strDestinationOU -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Move AD User to OU $strDestinationOU." -Result "Success"
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Move AD User to OU $strDestinationOU." -Result "Error"
      Continue
     }
}

Function Add-ADMemberToGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-ADMemberToGroup
=============================================================================================================================================
.SYNOPSIS

This function adds a user to an AD group.

#>

  param
   (
    [string] $strUserid,
    [string] $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Add-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Add AD group $strADGroupName to user $strUserid." -Result "Success"
       if($Global:strForRollBackGroupsToRemove.Length -eq 0)
       {
        $Global:strForRollBackGroupsToRemove = $strADGroupName
       }
       else
       {
        $Global:strForRollBackGroupsToRemove = $Global:strForRollBackGroupsToRemove + "," + $strADGroupName
       }
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Add AD group $strADGroupName to user $strUserid." -Result "Error"
      Continue
     }
}

Function Remove-ADMemberFromGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ADMemberFromGroup
=============================================================================================================================================
.SYNOPSIS

This function removes a user from an AD group.

#>

  param
   (
    [string] $strUserid,
    [string] $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Remove-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Remove AD group $strADGroupName from user $strUserid." -Result "Success"
       if($Global:strForRollBackGroupsToAdd.Length -eq 0)
       {
        $Global:strForRollBackGroupsToAdd = $strADGroupName
       }
       else
       {
        $Global:strForRollBackGroupsToAdd = $Global:strForRollBackGroupsToAdd + "," + $strADGroupName
       }

      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -Message $_.Exception.Message -Action "Remove AD group $strADGroupName from user $strUserid." -Result "Error"
      Continue
     }
}

Function Remove-UserFromMultipleGroups
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       12-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-UserFromMultipleGroups
=============================================================================================================================================
.SYNOPSIS

Delete multiple groups from the users' account. The group names are in an array.

#>

  param
   (
    [string] $arrGroupsToSearchFor,
    [string] $strUserid
   )

    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
    $strGroup=(Get-ADGroup $objGroup).Name
    forEach($objGroupMustContain in $arrGroupsToSearchFor)
     {
      $strGroupMustContain = $objGroupMustContain.ToString().ToLower()
      if($strGroup.ToLower().IndexOf($strGroupMustContain) -eq 0)
       {
        Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strGroup
       }  
     }
    }


}

Function Migrate-ADUserToNewGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       11-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Migrate-ADUserToNewGroup
=============================================================================================================================================
.SYNOPSIS

Perform the translating from the old group name to the new group name.

#>

  param
   (
    [string] $strUserid,
    [string] $strADGroupName
   )
    
   $bolOldGroupNameHasBeenFound = $False
   
   ForEach($objGroupNameForAutomaticMigration in $arrGroupNamesForAutomaticMigration)
    {
     Try
     {
      $strOldGroup = $objGroupNameForAutomaticMigration.OldGroup
      $strNewGroup = $objGroupNameForAutomaticMigration.NewGroup
     }
      Catch
     {
      Write-Host "There is something wrong with the CSV filename $FileForAutomaticMigration while processing $strUserid."
      Exit 1 
     }
     if ($strADGroupName -eq $strOldGroup)
     {
      $bolOldGroupNameHasBeenFound = $true
      if ($strNewGroup.Length -gt 0)
       {
        Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strNewGroup
        if ($RemoveOldADGroups)
         {
          Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strOldGroup
         }    
       }
        Else
       {
        Write-EntryToResultsFile -strUserid $strUserid -Action "Migrate users" -Message "No new groupname specified for $strADGroupName in $FileForAutomaticMigration." -Result "Error"
       }
     }

    }
    if(-not($bolOldGroupNameHasBeenFound))
    {
     Write-EntryToResultsFile -strUserid $strUserid -Action "Migrate users" -Message "The group $strADGroupName has not been found in $FileForAutomaticMigration." -Result "Error"
    }
  }

Function Find-InArry
{
<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       17-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Find-InArry
=============================================================================================================================================
.SYNOPSIS

This function checks two arrays for content. I had to write my own function as the build-in functions like
Compare-Object or $arrOnlyIndirectMembers | Where {$arrDirectAndIndirectMembers -notcontains $arrDirectMembers}
did not did their work properly. 

The swith 'NotContains' can be $True or $False. If not specified on the command line, then it is $False.

#>


param
(
 [string[]] $SearchIn,
 [string[]] $LookFor,
 [switch] $NotContains
)

$Contains = -not $NotContains

$tmpArray = @()
ForEach ($objSearchIn in $SearchIn)
 {
  $bolFound = $false
  ForEach ($objLookFor in $LookFor)
  {
   if($objSearchIn -eq $objLookFor)
    {
     $bolFound = $True
     if ($bolFound -and $Contains)
     {
      $tmpArray+=$objLookFor
     }
    } 
  }
  if (-not($bolFound) -and -not $Contains)
  {
   $tmpArray+=$objSearchIn
  }
 }
 Return $tmpArray
}

Function Show-IndirectGroupsInResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       18-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Show-IndirectGroupsInResultsFile
=============================================================================================================================================
.SYNOPSIS

This function shows only the indirect group membership in the results file.

#>

  param
   (
    [string] $strUserid
   )

   $dn                          = (Get-ADUser $strUserid).DistinguishedName
   $arrOnlyDirectMembers        = @()
   $arrDirectAndIndirectMembers = @()
   $arrOnlyIndirectMembers      = @()
      
   $arrDirectAndIndirectMembers = Get-ADGroup  -LDAPFilter ("(member:1.2.840.113556.1.4.1941:={0})" -f $dn)   | Select-Object sAMAccountName | Sort-Object sAMAccountName
   $arrOnlyDirectMembers        = Get-ADPrincipalGroupMembership $dn                                          | Select-Object sAMAccountName | Sort-Object sAMAccountName
   $arrOnlyIndirectMembers      = Find-InArry -SearchIn $arrDirectAndIndirectMembers -NotContains -LookFor $arrOnlyDirectMembers

   $arrOnlyIndirectMembers | ForEach($_) {
    $tmpValue = $_
    $tmpValue = $TmpValue -Replace("@{sAMAccountName=","")
    $tmpValue = $TmpValue -Replace("}","")
    Write-EntryToResultsFile -strUserid $strUserid -Result "Information" -Action "Indirect group member" -Message $TmpValue
   }

}

Function Show-DirectGroupsInResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       15-November-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Show-DirectGroupsInResultsFile
=============================================================================================================================================
.SYNOPSIS

This function shows only the direct group membership in the results file.

#>

  param
   (
    [string] $strUserid
   )

   $arrOnlyDirectMembers        = @()
     
   $arrOnlyDirectMembers        = Get-ADPrincipalGroupMembership ((Get-ADUser $strUserid).DistinguishedName)  | Select-Object sAMAccountName | Sort-Object sAMAccountName
   
   $arrOnlyDirectMembers | ForEach($_) {
    $tmpValue = $_.sAMAccountName    
    Write-EntryToResultsFile -strUserid $strUserid -Result "Information" -Action "Direct group member" -Message $TmpValue
   }

}

Function Create-FileWithUseridsInCSVFormat
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       18-October-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Create-FileWithUseridsInCSVFormat
=============================================================================================================================================
.SYNOPSIS

This function cretes the file with all the userids to migrate, based on OU.

#>
  Param
    (
     [string] $srcOU ="",
     [string] $dstOU= "",
     [string] $Windows10VDIGroup= ""
    )

   $strBaseFileName = "UseridsToMigrate (" + (Get-Date).ToString('G') + ").csv"
   $strBaseFileName = $strBaseFileName -replace ":","-"
   $strBaseFileName = $strBaseFileName -replace "/","-"
   $strFileName     = $strCurrentPath + "\" + $strBaseFileName
      
   if($strFileName.Length -gt 260)
   {
    $valLength       = ($strCurrentPath.Length)-4
    $strBaseFileName = ($strBaseFileName.Substring(0,260-$valLength)) + ".csv"
    $strFileName     = $strCurrentPath + "\" + $strBaseFileName
   }

   $arrTableWithUserids = @()
   $arrUserids          = (Get-ADUser -Filter {Enabled -eq "True"} -SearchScope Subtree -SearchBase $srcOU)
   $valCounter          = 1

   $Users = ForEach ($objUserid in $arrUserids)
   {
    $strUserDetails = Get-ADUser $objUserid -Properties Name,sAMAccountName,department,cn, DistinguishedName, mail
    Write-Progress -Activity "Create CSV File with the userids for migration" -Status "Processing user $($strUserDetails.cn)." -PercentComplete ($valCounter / $arrUserids.Count * 100)
    $UserRecord                   = [ordered] @{"Userid" = "";"Full name"= "";"Mail"="";"Department"= "";"CurrentOU"= "";"NewOU"="";"VDIGroup"= "";"GroupsToAdd"= "";"GroupsToRemove"= ""}
    $UserRecord."Userid"          = $strUserDetails.sAMAccountName
    $UserRecord."Full name"       = $strUserDetails.cn
    $UserRecord."Mail"            = $strUserDetails.mail
    $UserRecord."Department"      = $strUserDetails.department
    $UserRecord."CurrentOU"       = "OU="+ ($strUserDetails.DistinguishedName -split "=",3)[-1]
    $UserRecord."NewOU"           = $dstOU
    $UserRecord."VDIGroup"        = $Windows10VDIGroup
    $UserRecord."GroupsToAdd"     = ""
    $UserRecord."GroupsToRemove"  = ""
    $objRecordWithUserids         = New-Object PSObject -Property $UserRecord
    $arrTableWithUserids         += $objRecordWithUserids
    $valCounter++
   }

   If($arrTableWithUserids.Count -gt 0)
   {
    $arrTableWithUserids | Export-Csv $strFileName -NoTypeInformation
   }
    Else
   {
    Write-Host "Something went wrong while writing the file $strFileName. Maybe nothing to report..."
   } 
}

Function Add-TrailingBackSlash
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-TrailingBackSlash
=============================================================================================================================================
.SYNOPSIS

This function adds a trailing backslash to a string
#>

param
(
 [string] $AddBackslashTo ="" 
)

if($AddBackslashTo.Length -gt 0)
 { 
  if(($AddBackslashTo.SubString($AddBackslashTo.Length-1,1)) -ne "\") 
  {
   $AddBackslashTo = $AddBackslashTo + "\"
  }
 }
Else
 {
  $AddBackslashTo = "\"
 }

 Return $AddBackslashTo

}

Function Copy-UserProfileFiles
{
<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Copy-UserProfileFiles
Source:           https://techblog.dorogin.com/powershell-how-to-recursively-copy-a-folder-structure-excluding-some-child-folders-and-files-a1de7e70f1b
=============================================================================================================================================
.SYNOPSIS

This function really copies the files.
#> 

param
 (
  [string] $OldFilesDirectory,
  [string] $NewFilesDirectory
 )

 $arrExclude      = @("desktop.ini","$RECYCLE.BIN","Google Chrome.lnk","thumbs.db")
 $arrExcludeMatch = @(".pst")
 New-Item -Path $NewFilesDirectory -ItemType Directory -Force -WhatIf:(-not($ProductionRun)) | Out-Null
  Try
   {
    Get-ChildItem -Path $OldFilesDirectory -Recurse -Exclude $arrExclude |
    where { $arrExcludeMatch -eq $null -or $_.FullName.Replace($OldFilesDirectory, "") -notmatch $arrExcludeMatch } | 
    Copy-Item -Destination {
        if ($_.PSIsContainer) 
        {
         Join-Path $NewFilesDirectory $_.Parent.FullName.Substring($OldFilesDirectory.length)
        } 
         else 
        {
         Join-Path $NewFilesDirectory $_.FullName.Substring($OldFilesDirectory.length)
        }
    } -Force -Exclude $arrExclude -WhatIf:(-not($ProductionRun)) -Confirm:$false -Erroraction SilentlyContinue
    Write-EntryToResultsFile -strUserid $Userid -Result "Success" -Action "Copy from $OldFilesDirectory to $NewFilesDirectory."
   }
    Catch
   {
    Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Copy from $OldFilesDirectory to $NewFilesDirectory." -Message $_.Exception.Message
    Continue 
   }

}

Function Copy-UserProfile
{
<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Copy-UserProfile
=============================================================================================================================================
.SYNOPSIS

This function copies the favorites and desktop from the old profile to the new profile

#>

param
 (
  [string] $Userid="",
  [string] $OldProfilePath="",
  [string] $NewProfilePath=""
 )

 # Perform some checking
 $bolErrorsFound = $False

 $OldProfilePath = (Add-TrailingBackSlash -AddBackslashTo $OldProfilePath) + $Userid 
 $NewProfilePath = (Add-TrailingBackSlash -AddBackslashTo $NewProfilePath) + $Userid

 if(-not(test-path($OldProfilePath)))
 {
  $bolErrorsFound = $True
  Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Check directory" -Message "The directory $OldProfilePath does not exists."
 }

 if(-not(test-path($NewProfilePath)))
 {
  $bolErrorsFound = $True
  Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Check directory" -Message "The directory $NewProfilePath does not exists."
 }

 if(test-path($NewProfilePath + "\Desktop\Oude desktop Windows 7"))
 {
  $bolErrorsFound = $True
  Write-EntryToResultsFile -strUserid $Userid -Result "Error" -Action "Check directory" -Message "The directory $($NewProfilePath + "\Desktop\Oude desktop Windows 7") exists. That means that the profile has already been copied."
 }

 If($bolErrorsFound)
 {
  Return
 }

 # Copy the Favorites and desktop

  Copy-UserProfileFiles -OldFilesDirectory ($strOldFolder = $OldProfilePath +"\Favorites") -NewFilesDirectory ($strNewFolder = $NewProfilePath +"\Favorites")
  Copy-UserProfileFiles -OldFilesDirectory ($strOldFolder = $OldProfilePath +"\Desktop")   -NewFilesDirectory ($strNewFolder = $NewProfilePath +"\Desktop\Oude desktop Windows 7")
 
}

Function Add-RollBackRecord
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-RollBackRecord
=============================================================================================================================================
.SYNOPSIS

This function adds an item to the rollback file

#>

  param
   (
    [string] $Userid,
    [string] $NewOU,
    [string] $VDIGroup,
    [string] $GroupsToAdd,
    [string] $GroupsToRemove
   )
  
  $RollBackRecord                       = [ordered] @{"Userid" = "";"NewOU"="";"VDIGroup"= "";"GroupsToAdd"= "";"GroupsToRemove"= ""}
  $RollBackRecord."Userid"              = $Userid
  $RollBackRecord."NewOU"               = $NewOU
  $RollBackRecord."VDIGroup"            = $VDIGroup
  $RollBackRecord."GroupsToAdd"         = $GroupsToAdd
  $RollBackRecord."GroupsToRemove"      = $GroupsToRemove
  $objRollBackRecord                    = New-Object PSObject -Property $RollBackRecord
  $Global:arrTableWithRollBackRecords  += $objRollBackRecord
  }

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# =============================================================================================================================================
# Declares the variables.
# Modify $strPrefixOldGroupName for your own enfironment.
# =============================================================================================================================================

  $valCounter                         = 1
  $Global:arrTable                    = @()
  $Global:arrTableWithRollBackRecords = @()
  $strCurrentPath                     = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                     = $MyInvocation.MyCommand.Name
  $strPrefixOldGroupName              = "gg_appl_"
  $strPrefixNewGroupName              = "Appl_"
  $arrGroupMustContainForFullCleanUp  = @($strPrefixOldGroupName,"WM-Users")

# =============================================================================================================================================
# check the length of the current directory. Stop the script if more that 248 characters. Otherwise the results cannot be written to the given
# folder.
# =============================================================================================================================================

  If($strCurrentPath.Length -gt 248)
  {
   Write-Host "The current directory $strCurrentPath has a length of more than 248 characters. The script will end with exit code 999 now."
   Exit 999
  }

# =============================================================================================================================================
# Check if the parameter CreateFileWithUseridsInCSVFormat is used. In that case, create the file and stop.
# =============================================================================================================================================

  if($CreateFileWithUseridsInCSVFormat)
  {
   Create-FileWithUseridsInCSVFormat -srcOU $srcOU -dstOU $dstOU -Windows10VDIGroup $Windows10VDIGroup
   Exit 0
  }
    
# =============================================================================================================================================
# Define the CSV Import File. 
# =============================================================================================================================================

  if($FileWithUseridsInCSVFormat.Length -eq 0)
   {
    $strCSVFileName = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($FileWithUseridsInCSVFormat.ToLower().IndexOf(".csv") -eq -1)
    {
     $FileWithUseridsInCSVFormat = $FileWithUseridsInCSVFormat + ".csv"
    }
    $strCSVFileName = $FileWithUseridsInCSVFormat
   }

# =============================================================================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# =============================================================================================================================================

  if (-not(Split-Path($strCSVFileName)))
  { 
   $strCSVFileName = $strCurrentPath + "\" + $strCSVFileName
  }

# =============================================================================================================================================
# In case of an automated migration check if the string $FileForAutomaticMigration is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# If the file exists then read all the content, otherwise quit.
# =============================================================================================================================================

  if($FileForAutomaticMigration.Length -gt 0)
   {
    if (-not(Split-Path($FileForAutomaticMigration)))
    {
     $FileForAutomaticMigration = $strCurrentPath + "\" + $FileForAutomaticMigration
    }
   
    if(Test-Path($FileForAutomaticMigration))
     {
      $arrGroupNamesForAutomaticMigration = @(Import-Csv $FileForAutomaticMigration)
      if ($arrGroupNamesForAutomaticMigration.Count -eq 0)
      {
      Write-Host "The file $FileForAutomaticMigration seems to be empty..."
      Exit 1
      }
     }
      else
     {
      Write-Host "The file $FileForAutomaticMigration does not exists. Thus quitting."
      Exit 1
     }
   }  

# =============================================================================================================================================
# Define the log file. This log file contains all the results.
# =============================================================================================================================================

  $strLastPartOfFileName = " (" + (Get-Date).ToString('G') + ").csv"
  $strLastPartOfFileName = $strLastPartOfFileName -replace ":","-"
  $strLastPartOfFileName = $strLastPartOfFileName -replace "/","-"
  
  If(-not($ProductionRun))
  {
   $strLastPartOfFileName = " (RUNNING IN TEST MODE)" + $strLastPartOfFileName
  }

  $strCSVLogFileSucces   = $strCSVFileName -Replace ".csv", $strLastPartOfFileName

  $strPathName           = (Split-Path $strCSVLogFileSucces) + "\"
  $strFileName           = $strCSVLogFileSucces.Substring($strPathName.Length,($strCSVLogFileSucces.Length - $strPathName.Length)) 

  $strCSVLogFileSucces   = $strPathName + $LogFilePrefix + $strFileName
  $strCSVRollBackFile    = $strPathName + "RBF_" + $strFileName

  $valFileLengthLogFile = $strCSVLogFileSucces.Length

  if($valFileLengthLogFile -gt 260)
  {
   Write-Host "The file name $strCSVLogFileSucces is too long. The maximum file length is 260 characters. This one is $valFileLengthLogFile long.`n No log file is generated, and therefore, this application will quit."
   Exit 2
  }

# =============================================================================================================================================
# Read the CSV file.
# =============================================================================================================================================
      
  If(Test-Path $strCSVFileName)
  {
   $arrUserids = @(Import-Csv $strCSVFileName)
  }
   Else
  {
   Write-Host "The import file $strCSVFileName does not exists."
   Exit 1
  }
    
# =============================================================================================================================================
# Find all the arguments and puthem in the log file
# Source: https://ss64.com/ps/psboundparameters.html
# =============================================================================================================================================

  Write-EntryToResultsFile -strUserid "" -Result "Information" -Action "Used script" -Message $strCurrentPath + "\" + $strCurrentFile

  foreach($boundparam in $PSBoundParameters.GetEnumerator()) 
  {
   Write-EntryToResultsFile -strUserid "" -Result "Information" -Action "Key: $($boundparam.Key)" -Message "Value: $($boundparam.Value)"
  }
  
# =============================================================================================================================================
# Process the users in the CSV file.
# =============================================================================================================================================

  Clear-Host

  $strActivity                       = "Modifying users in Active Directory."

  If(-not($ProductionRun))
  {
  $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
  }

  ForEach($objUser in $arrUserids)
  {
     
   $strUserid = $objUser.Userid
   Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)

   if($CreateRollBackFile)
   {
    $Global:strForRollBackGroupsToAdd    = ""
    $Global:strForRollBackGroupsToRemove = ""
    $strUserDetails                      = Get-ADUser $strUserid -Properties cn, DistinguishedName
    $strCurrentUserOU                    = "OU=" + ($strUserDetails.DistinguishedName -split "=",3)[-1]
   }

   Try
   {
    $strNewOU          = $objUser.NewOU
    $strVDIGroup       = $objUser.VDIGroup
    $arrGroupsToAdd    = $objUser.GroupsToAdd.Split(",")
    $arrGroupsToRemove = $objUser.GroupsToRemove.Split(",")
   }
    Catch
   {
    Write-EntryToResultsFile -strUserid $strUserid -Result "Error" -Message "There is something wrong with the CSV filename $strCSVFileName while processing $strUserid."
    Export-ResultsLogFileToCSV
    Exit 1 
   }
   
   # Copy Profile
   if($copyProfile)
   {
    Copy-UserProfile -Userid $strUserid -OldProfilePath $ProfilePathFrom -NewProfilePath $ProfilePathTo
   }
     

   if(-not $copyProfile)
   {

    # Clear the profile path
    If($ClearProfilePath)
     {
      Remove-ProfilePathFromUserProfileInAD -strUserid $strUserid
     }
   
    # Clear the homedrive and home directory
    If($ClearHomeFolder)
     {
      Remove-HomeFolderPathFromUserProfileInAD -strUserid $strUserid
     }
    # Move the user to another OU:
    Move-ADUserToOtherOU -strUserid $strUserid -strDestinationOU $strNewOU
   
    # Add the user to the new VDI group:
    Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strVDIGroup
   
    # Add the user to various groups:
    ForEach($objGroupToAdd in $arrGroupsToAdd)
    {
     Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $objGroupToAdd
    }
   
    # Remove the user to various groups:
    ForEach($objGroupToRemove in $arrGroupsToRemove)
    {
     Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $objGroupToRemove
    }
   }

   # Automatic migration

   if($FileForAutomaticMigration.Length -gt 0 -and -not $copyProfile)
   {
    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
     $strGroupName = (Get-ADGroup $objGroup).Name
     if ($strGroupName.ToLower().IndexOf($strPrefixOldGroupName) -eq 0)
     {
      Migrate-ADUserToNewGroup -strUserid $strUserid -strADGroupName $strGroupName
     }
    }
   }

   # Full Clean Up
   if($FullCleanUp -and -not $copyProfile)
   {
   Remove-UserFromMultipleGroups -arrGroupsToSearchFor $arrGroupMustContainForFullCleanUp -strUserid $strUserid
   }

   # Remove old Citrix groups
   if($RemoveOldCitrixGroups -and -not $copyProfile)
   {
   $arrCitrixOU = @("OU=Citrix,OU=OU1,DC=testdomain,DC=local,DC=lan")

   ForEach($objCitrixOU in $arrCitrixOU)
     {
     $arrWithCitrixGroups = get-adgroup -filter "*" -SearchBase $objCitrixOU | Where-Object Name -Match "Citrix VDI*"
     ForEach ($objWithCitrixGroups in $arrWithCitrixGroups)
      {    
       Remove-UserFromMultipleGroups -arrGroupsToSearchFor $objWithCitrixGroups.Name -strUserid $strUserid
      }
     }

     Remove-UserFromMultipleGroups -arrGroupsToSearchFor @("CitrixTestUsers","CitrixProductionUsers") -strUserid $strUserid
   }

   # Perform the Direct Group Inventory 
   if($RunDirectGroupInventory -and -not $ProductionRun -and -not $copyProfile)
   {
    Show-DirectGroupsInResultsFile -strUserid $strUserid
   }

   # Perform the Indirect Group Inventory 
   if($RunIndirectGroupInventory -and -not $ProductionRun -and -not $copyProfile)
   {
    Show-IndirectGroupsInResultsFile -strUserid $strUserid
   }

   if($CreateRollBackFile)
   {
    Add-RollBackRecord -Userid $strUserid -NewOU $strCurrentUserOU -VDIGroup "" -GroupsToAdd $Global:strForRollBackGroupsToAdd -GroupsToRemove $Global:strForRollBackGroupsToRemove
   }

   $valCounter++
   
  }
  
  if($ProductionRun -and ($RunDirectGroupInventory -or $RunIndirectGroupInventory) -and -not $copyProfile)
  {
   $valCounter = 1
   $maxCounter = 30
   For($valCounter = 1; $valCounter -le $maxCounter;$valCounter++)
    {
     Write-Progress -Activity "Processing changes in Active Directory" -Status "Waiting $valCounter of $maxCounter seconds."  -PercentComplete ($valCounter / $maxCounter * 100)
     Sleep 1
    }
 
   # =============================================================================================================================================
   # Inventory the indirect user groups the user belongs to.
   # This can only be done after updating Active Directory, otherwise incorrect results are shown.
   # =============================================================================================================================================

     $strActivity                       = "Inventory the indirect and / or direct groups the user belongs to."

     If(-not($ProductionRun))
      {
       $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
      }

     $valCounter = 1
     ForEach($objUser in $arrUserids)
      {
       $strUserid = $objUser.Userid
       Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)
       if ($RunDirectGroupInventory)
        {
         Show-DirectGroupsInResultsFile -strUserid $strUserid
        }
       if ($RunIndirectGroupInventory) 
        {
         Show-IndirectGroupsInResultsFile -strUserid $strUserid
        }
       $valCounter++
      }
  }

# =============================================================================================================================================
# Write the results to the csv file.
# =============================================================================================================================================
  
  Export-ResultsLogFileToCSV
   
# =============================================================================================================================================
# Write the Rollback file to a csv file.
# =============================================================================================================================================

  if($CreateRollBackFile)
  {
   Export-RollBackFileToCSV
  }

Current version: Migrate users (v10)

Any feedback to improve this script is appreciated. You can download the previous versions here:

  1. Migrate users (v01)
  2. Migrate users (v02)
  3. Migrate users (v03)



Migrate users from Windows 7 to Windows 10 automatically

For a migration from Windows 7 to Windows 10 it is needed to do the following with the users’ account:

  • Move it to a different OU
  • Add it to a new VDI group
  • Add it to the new application groups
  • Remove it from the previous application groups
  • Clear the profile path, if needed.

To do this, I created a Powershell script.

The users who need to be migrated from Windows 7 to Windows 10 are in a csv file:

  • The column Userid contains the userid
  • The column NewOU contains the OU where the user should be moved to. This column can be empty.
  • The colum VDIGroup contains the new VDI group. This column can be empty.
  • The colum GroupsToAdd contains the groups the user should be added to. Split the groups with a comma. This column can be empty.
  • The colum GroupsToRemove contains the groups the user should be removed from. Split the groups with a comma. This column can be empty.

An example:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

The are some parameters:

  • FileWithUseridsInCSVFormat: CSV Filename that contains all the userids that should be migrated. If not mentioned than the script name is used.
  • LogFilePrefix: The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_
  • ProductionRun: Use the swith ProductionRun to modify. If not specified, the script is run in test mode.
  • FullCleanUp: Use the swith FullCleanUp to remove all unneeded groups.
  • ClearProfilePath: Use the swith ClearProfilePath to clear the profile path.

The script:

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Filename:         Migrate users (v03).ps1
=============================================================================================================================================
.DESCRIPTION:

This script prepares the users for the migration from Windows 7 to Windows 10.

.USAGE:

Create a CSV file with the following layout:

"Userid","NewOU","VDIGroup","GroupsToAdd","GroupsToRemove"
"userid1","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","Appl_Group1,Appl_Group2,Appl_Group3","old_appl_group1,old_appl_group2,old_appl_group3,old_appl_group4"
"userid2","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","Citrix VDI Windows 10","",""
"userid3","OU=Users,OU=OU2,OU=OU1,DC=testdomain,DC=local,DC=lan","","",""
"userid4","","","","old_appl_group1,old_appl_group2"

.PARAMETERS:

-FileWithUseridsInCSVFormat:
The filename with the users to be modified.

-LogFilePrefix
The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_

-ProductionRun
If the switch is used then ActiveDirectory is modified. If not used then a test run is done, and Active Directory is not modified.

-FullCleanUp
If this switch is used then the following groups will be removed from the users' account:
 * All gg_appl groups
 * WM-Users

 -ClearProfilePath
 If this switch is used then the profile path will be cleared.
  
.VERSION HISTORY:
 v0.1:
   * Initial version.

 v0.2:
   * The logfilename has been changed.

 V0.3:
   * The logfile mentions 'RUNNING IN TEST MODE' in case the swith -ProductionRun is not used.
#>

param
(
[Parameter(HelpMessage="CSV Filename that contains all the userids that should be migrated. Default = the script name, with the csv extension.")]
[String] $FileWithUseridsInCSVFormat="",

[Parameter(HelpMessage="The name the logfile starts with. So the logfiles are grouped together. Default = ZZZ-Logfile_")]
[String] $LogFilePrefix = "ZZZ-Logfile_",

[Parameter(HelpMessage="Use the swith ProductionRun to modify. If not specified, the script is run in test mode.")]
[Switch] $ProductionRun,

[Parameter(HelpMessage="Use the swith FullCleanUp to remove all unneeded groups.")]
[Switch] $FullCleanUp,

[Parameter(HelpMessage="Use the swith ClearProfilePath to clear the profile path.")]
[Switch] $ClearProfilePath
)

# =============================================================================================================================================
# Function block
# =============================================================================================================================================

Function Write-EntryToResultsFile
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       03-August-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Write-EntryToResultsFile
=============================================================================================================================================
.DESCRIPTION:

This function adds the success or failure information to the array that contains the log
information.

#>
param
 (
  $strUserid,
  $ErrorMessage = "",
  $Action       = ""
 )
 $Record            = [ordered] @{"Username" = "";"Action"= "";"Testmode"="";"Error"= ""}
 $Record."Username" = $strUserid
 $Record."Action"   = $Action
 $Record."Testmode" = -not($ProductionRun)
 $Record."Error"    = $ErrorMessage
 $objRecord         = New-Object PSObject -Property $Record
 $Global:arrTable   += $objRecord
}

Function Remove-ProfilePathFromUserProfileInAD
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ProfilePathFromUserProfileInAD
=============================================================================================================================================
.DESCRIPTION:

This function clears the ProfilePath from AD.

#>

  param
   (
    $strUserid
   )
   Try
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Set-ADUser -Identity $strUserDN -Clear profilePath -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Clear profile path"
      }
       Catch
      {
       Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Clear profile path"
       Continue
      }
}

Function Move-ADUserToOtherOU
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Move-ADUserToOtherOU
=============================================================================================================================================
.DESCRIPTION:

This function moves the user to another OU.

#>

  param
   (
    $strUserid,
    $strDestinationOU
   )
   Try
     {
      if($strDestinationOU.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Move-ADObject -Identity $strUserDN -TargetPath $strDestinationOU -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Move AD User to OU $strDestinationOU."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Move AD User to OU $strDestinationOU."
      Continue
     }
}

Function Add-ADMemberToGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Add-ADMemberToGroup
=============================================================================================================================================
.DESCRIPTION:

This function adds a user to an AD group.

#>

  param
   (
    $strUserid,
    $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Add-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Add AD group $strADGroupName to user."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Add AD group $strADGroupName to user."
      Continue
     }
}

Function Remove-ADMemberFromGroup
{

<#
.NOTES
=============================================================================================================================================
Created with:     Windows PowerShell ISE
Created on:       06-September-2018
Created by:       Willem-Jan Vroom
Organization:     
Functionname:     Remove-ADMemberFromGroup
=============================================================================================================================================
.DESCRIPTION:

This function removes a user from an AD group.

#>

  param
   (
    $strUserid,
    $strADGroupName
   )
   Try
     {
      if($strADGroupName.Length -gt 0)
      {
       $strUserDN = (Get-ADUser -Identity $strUserid).distinguishedName
       Remove-ADGroupMember -Identity $strADGroupName -Members $strUserDN -WhatIf:(-not($ProductionRun)) -Confirm:$false
       Write-EntryToResultsFile -strUserid $strUserid -Action "Remove AD group $strADGroupName from user."
      }
     }
      Catch
     {
      Write-EntryToResultsFile -strUserid $strUserid -ErrorMessage $_.Exception.Message -Action "Remove AD group $strADGroupName from user."
      Continue
     }
}

# =============================================================================================================================================
# End function block
# =============================================================================================================================================

# =============================================================================================================================================
# Declares the variables.
# =============================================================================================================================================

  $valCounter                        = 1
  $Global:arrTable                   = @()
  $strActivity                       = "Modifying users in Active Directory."
  $arrGroupMustContainForFullCleanUp = @("gg_appl","WM-Users")
  $strCurrentPath                    = Split-Path -parent $MyInvocation.MyCommand.Definition
  $strCurrentFile                    = $MyInvocation.MyCommand.Name


# =============================================================================================================================================
# Define the CSV Import File. 
# =============================================================================================================================================

  if($FileWithUseridsInCSVFormat.Length -eq 0)
   {
    $strCSVFileName = $strCurrentFile -Replace ".ps1",".csv"
   }
    else
   {
    if($FileWithUseridsInCSVFormat.ToLower().IndexOf(".csv") -eq -1)
    {
     $FileWithUseridsInCSVFormat = $FileWithUseridsInCSVFormat + ".csv"
    }
    $strCSVFileName = $FileWithUseridsInCSVFormat
   }

# =============================================================================================================================================
# Check if the string $strCSVFileName is a path. In that case, nothing has to be done.
# In case it is not a path, then the current location should be added.
# =============================================================================================================================================

  if (-not(Split-Path($strCSVFileName)))
  { 
   $strCSVFileName = $strCurrentPath + "\" + $strCSVFileName
  }

# =============================================================================================================================================
# Define the log file. This log file contains all the results.
# =============================================================================================================================================

  $strLastPartOfFileName = " (" + (Get-Date).ToString('F') + ").csv"
  $strLastPartOfFileName = $strLastPartOfFileName -replace ":","-"
  
  If(-not($ProductionRun))
  {
  $strLastPartOfFileName = " (RUNNING IN TEST MODE)" + $strLastPartOfFileName
  }

  $strCSVLogFileSucces   = $strCSVFileName -Replace ".csv", $strLastPartOfFileName

  $strPathName           = (Split-Path $strCSVLogFileSucces) + "\"
  $strFileName           = $strCSVLogFileSucces.Substring($strPathName.Length,($strCSVLogFileSucces.Length - $strPathName.Length)) 

  $strCSVLogFileSucces   = $strPathName + $LogFilePrefix + $strFileName
  
# =============================================================================================================================================
# Read the CSV file.
# =============================================================================================================================================
      
  if(Test-Path $strCSVFileName)
  {
   $arrUserids = @(Import-Csv $strCSVFileName)
  }
   Else
  {
   Write-Host "The import file $strCSVFileName does not exists."
   Exit 1
  }
    
# =============================================================================================================================================
# Process the users in the CSV file.
# =============================================================================================================================================

  Clear-Host

  If(-not($ProductionRun))
  {
  $strActivity = $strActivity + " (RUNNING IN TEST MODE)"
  }

  ForEach($objUser in $arrUserids)
  {
   $strUserid = $objUser.Userid
   Write-Progress -Activity $strActivity -Status "Processing user $strUserid" -PercentComplete ($valCounter / $arrUserids.Count * 100)
   Try
   {
    $strNewOU          = $objUser.NewOU
    $strVDIGroup       = $objUser.VDIGroup
    $arrGroupsToAdd    = $objUser.GroupsToAdd.Split(",")
    $arrGroupsToRemove = $objUser.GroupsToRemove.Split(",")
   }
    Catch
   {
    Write-Host "There is something wrong with the CSV filename $strCSVFileName while processing $strUserid."
    Exit 1 
   }
   
   # Clear the profile path
   If($ClearProfilePath)
   {
    Remove-ProfilePathFromUserProfileInAD -strUserid $strUserid
   }
   
   # Move the user to another OU:
   Move-ADUserToOtherOU -strUserid $strUserid -strDestinationOU $strNewOU
   
   # Add the user to the new VDI group:
   Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $strVDIGroup

   # Add the user to various groups:
   ForEach($objGroupToAdd in $arrGroupsToAdd)
   {
    Add-ADMemberToGroup -strUserid $strUserid -strADGroupName $objGroupToAdd
   }
  
   # Remove the user to various groups:
   ForEach($objGroupToRemove in $arrGroupsToRemove)
   {
    Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $objGroupToRemove
   }
  
   # Full Clean Up
   if($FullCleanUp)
   {
    $arrGroups = @(Get-ADUser $strUserid -Properties MemberOf).MemberOf
    forEach($objGroup in $arrGroups)
    {
    $strGroup=(Get-ADGroup $objGroup).Name
    forEach($objGroupMustContain in $arrGroupMustContainForFullCleanUp)
     {
      $strGroupMustContain = $objGroupMustContain.ToString().ToLower()
      if($strGroup.ToLower().IndexOf($strGroupMustContain) -eq 0)
       {
        Remove-ADMemberFromGroup -strUserid $strUserid -strADGroupName $strGroup
       }  
     }
    }
   }

   $valCounter++
   Sleep 2
  }

  Sleep 2

# =============================================================================================================================================
# Write the results to the csv file.
# =============================================================================================================================================
  
  If($Global:arrTable.Count -gt 0)
  {
    $Global:arrTable | Export-Csv $strCSVLogFileSucces -NoTypeInformation
  }
   Else
  {
    Write-Host "Something went wrong while writing the logfile $strCSVLogFileSucces. Maybe nothing to report..."
  } 

Any feedback to improve this script is appreciated. You can download the scripts here:

  1. Migrate users (v01)
  2. Migrate users (v02)
  3. Migrate users (v03)