Windows

Removing Duplicate Windows Boot Manager Entries in your MDT Task Sequence

We recently purchased quite a few Dell XPS 13 9365 laptops and while testing our image; I noticed that the list of Windows Boot Manager entries in the BIOS was growing each time I would image the laptop. In order to get around this, I had to create the following script that would automatically scan through the Boot Configuration Data (BCD) store and remove all of the duplicate GUID’s that appear after running the image.

# CONFIGURE MDT LOGGING
$TSenv = New-Object -COMObject Microsoft.SMS.TSEnvironment 
$LogPath = $TSenv.Value("LogPath")  
$LogFile = "$LogPath\WindowsBootManagerRemoval.log"

Start-Transcript $LogFile

# LOCATE GUID
$Identifiers = BCDEDIT /ENUM FIRMWARE | Select-String "identifier" | ForEach-Object { $_ -replace "identifier" }

If($Identifiers -ne $Null) {
    
    $IdentifierList = $Identifiers.Replace(" ","") | Where-Object {$_ -notcontains "{fwbootmgr}" -and $_ -notcontains "{bootmgr}"}
    
    Write-Host "" 
    Write-Host "--------------------------------------------------------------------------------" 
    Write-Host "PREPARING TO REMOVE DUPLICATE WINDOWS BOOT MANAGER ENTRIES"
    Write-Host "--------------------------------------------------------------------------------" 
    Write-Host "" 

    # REMOVE GUIDS
    ForEach($Identifier in $IdentifierList) {
        BCDEDIT /Delete $Identifier
        Write-Host "DELETED $Identifier"
    }
}
Else {
    Write-Host "SKIPPING - COULD NOT LOCATE DUPLICATE WINDOWS BOOT MANAGER ENTRIES"
}

Once you are ready to use the script, go ahead and copy it over to your Deployment Share and add it to your State Restore group in your MDT Task Sequence.
MDT Task Sequence

Since this was not happening to all of our computer models, I made sure to add a Task Sequence condition that forced this step to only run for the Dell XPS 13 9365 laptops.

Feel free to leave any questions below!

Windows 10 Creators Update 1703 Cleanup Script

The following script is intended to run after an in place upgrade (Ex: 1607 to 1703). If you do not know how to run a post script after an upgrade, please refer to my previous post here.

The Powershell script will copy your old wallpapers from C:\Windows.old\windows\Web\Wallpaper\Windows\ and C:\Windows.old\windows\Web\4K\Wallpaper\Windows\ to their appropriate folders. It will also uninstall OneDrive, and prevent OneDriveSetup.exe and Windows Defender from running at logon. As well as remove the Contact Support application, move Office 2016 applications to their appropriate folder in the Start Menu (During my testing, these shortcuts moved around randomly), and attempt to remove any new apps that have reappeared with the upgrade.

Logging is enabled in the script and the entire cleanup log can be located in C:\Logs\1703-Upgrade.log

<#  

.FUNCTIONS
    1 - Set-FilePermissions
        Configures file permissions

    2 - Set-FileOwnership
        Configures ownership of files

    3 - Get-TimeStamp
        Configures timestamp for logs

    4 - Write-Log
        Creates a log for the script

#>

# Configure Functions

Function Set-FilePermissions {
    param (
     [parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]$File,
     [parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]$User,
     [parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]$Control,
     [parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]$Access
    )

    $ACL = Get-ACL "$File"
    Set-Acl -Path "$File" -AclObject $ACL
    $Permission = New-Object  system.security.accesscontrol.filesystemaccessrule("$User","$Control","$Access")
    $Acl.SetAccessRule($Permission)
    Set-Acl -Path "$File" -AclObject $ACL

}

Function Set-FileOwnership {
    param (
     [parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]$File,
     [parameter(Mandatory=$true)]
     [ValidateNotNullOrEmpty()]$User
    )

    $ACL = Get-ACL "$File"
    $Group = New-Object System.Security.Principal.NTAccount("$User")
    $ACL.SetOwner($Group)
    Set-Acl -Path "$File" -AclObject $ACL

}

function Get-TimeStamp {
    
    return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
    
}

function Write-Log {
    param (
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]$Passed,
    [parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]$Failed
    )
    
    If ($ProcessError.Count -eq 0) {
        Write-Output "$(Get-TimeStamp) $Passed" >> C:\Logs\1703-Upgrade.log
    }
    Else {
        Write-Output "$(Get-TimeStamp) $Failed" >> C:\Logs\1703-Upgrade.log
        $Global:Errors++
        $ProcessError.Clear()
    }
}

<# -- Script begins below --  #>

$Errors = 0

Write-Output "$(Get-TimeStamp) CLEANUP SCRIPT STARTED" > C:\Logs\1703-Upgrade.log
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) START COPYING WALLPAPERS" >> C:\Logs\1703-Upgrade.log

# Update Wallpaper
Set-FileOwnership -File "C:\windows\web\Wallpaper\Windows\img0.jpg" -User Users
Set-FilePermissions -File "C:\windows\web\Wallpaper\Windows\img0.jpg" -User Users -Control FullControl -Access Allow
Copy-Item "C:\Windows.old\windows\Web\Wallpaper\Windows\img0.jpg" -Destination "C:\windows\web\Wallpaper\Windows\img0.jpg" -Force -ErrorVariable +ProcessError
Write-Log -Passed "Copied C:\windows\web\Wallpaper\Windows\img0.jpg to C:\windows\web\Wallpaper\Windows\img0.jpg" -Failed "Failed to copy C:\windows\web\Wallpaper\Windows\img0.jpg to C:\windows\web\Wallpaper\Windows\img0.jpg"

# Update 4k Wallpapers
$Wallpapers = Get-ChildItem C:\Windows\Web\4K\Wallpaper\Windows
ForEach($Wallpaper in $Wallpapers) {
    Set-FileOwnership -File $Wallpaper.FullName -User Users
    Set-FilePermissions -File $Wallpaper.FullName -User Users -Control FullControl -Access Allow

    $FileName = $Wallpaper.Name
    $FilePath = $Wallpaper.FullName

    Copy-Item C:\Windows.old\windows\Web\4K\Wallpaper\Windows\$FileName -Destination $Wallpaper.FullName -Force -ErrorVariable +ProcessError
    Write-Log -Passed "Copied C:\Windows.old\windows\Web\4K\Wallpaper\Windows\$FileName to $FilePath" -Failed "Failed to copy C:\Windows.old\windows\Web\4K\Wallpaper\Windows\$FileName to $FilePath"
}   

# Uninstall OneDrive
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) UNINSTALLING ONEDRIVE" >> C:\Logs\1703-Upgrade.log
Start-Process C:\Windows\SysWOW64\OneDriveSetup.exe /uninstall -Wait -ErrorVariable +ProcessError
Write-Log -Passed "Uninstalled OneDrive successfully" -Failed "Failed to uninstall OneDrive"

# Rename OneDriveSetup.exe (This is to prevent OneDrive First Run)
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) RENAMING ONEDRIVESETUP.EXE TO PREVENT ONEDRIVE FROM RUNNING AT LOGON" >> C:\Logs\1703-Upgrade.log
Set-FileOwnership -File C:\Windows\SysWOW64\OneDriveSetup.exe -User Users
Set-FilePermissions -File C:\Windows\SysWOW64\OneDriveSetup.exe -User Users -Control FullControl -Access Allow
Rename-Item C:\Windows\SysWOW64\OneDriveSetup.exe C:\Windows\SysWOW64\OneDriveSetup.exe.old -ErrorVariable +ProcessError
Write-Log -Passed "Renamed C:\Windows\SysWOW64\OneDriveSetup.exe to C:\Windows\SysWOW64\OneDriveSetup.exe.old" -Failed "Failed to rename C:\Windows\SysWOW64\OneDriveSetup.exe to C:\Windows\SysWOW64\OneDriveSetup.exe.old"

# Remove OneDrive run key
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) REMOVING ONEDRIVE RUN KEY FROM THE DEFAULT'S NTUSER.DAT FILE" >> C:\Logs\1703-Upgrade.log
cmd /c REG LOAD "HKLM\DEFAULT_USER" "C:\Users\Default\NTUSER.DAT" 
Remove-ItemProperty -Path "HKLM:\DEFAULT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "OneDriveSetup" -ErrorVariable +ProcessError
Write-Log -Passed "Removed OneDriveSetup from the default profile's run key" -Failed "Failed to remove OneDriveSetup from the default profile's run key"
cmd /c REG UNLOAD "HKLM\DEFAULT_USER"

# Delete run key for Windows Defender
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) REMOVING WINDOWS DEFENDER FROM THE CURRENT USER RUN KEY" >> C:\Logs\1703-Upgrade.log
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" -Name "SecurityHealth" -ErrorVariable +ProcessError
Write-Log -Passed "Removed SecurityHealth from the current user's run key" -Failed "Failed to remove SecurityHealth from the current user's run key"

# Remove Contact Support
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) REMOVING THE CONTACT SUPPORT APPLICATION" >> C:\Logs\1703-Upgrade.log
Get-WindowsCapability -online | ? {$_.Name -like ‘*ContactSupport*’} | Remove-WindowsCapability –online -ErrorVariable +ProcessError
Write-Log -Passed "Removed the Contact Support application" -Failed "Failed to remove the Contact Support application"

# Move Office 2016 Applications to correct folder in the Start Menu if needed
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) MOVING OFFICE 2016 APPLICATIONS TO CORRECT FOLDER IN START MENU" >> C:\Logs\1703-Upgrade.log
$OfficePrograms = GCI "C:\ProgramData\Microsoft\Windows\Start Menu\Programs" | Where-Object name -like "*2016.lnk"

If($OfficePrograms.count -gt 0) {
    ForEach($Program in $OfficePrograms) {
    $OfficeFilePath = $Program.FullName
    $OfficeFileName = $Program.Name
    Copy-Item "$OfficeFilePath" -Destination "C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Office 2016\$OfficeFileName"  -Force -ErrorVariable +ProcessError
    Remove-Item "$OfficeFilePath"
    Write-Log -Passed "Copied $OfficeFilePath to C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Office 2016\$OfficeFileName" -Failed "Failed to copy $OfficeFilePath to C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Office 2016\$OfficeFileName"

    }
}
Else {
    Write-Output "$(Get-TimeStamp) Did not find any Office 2016 programs outside of C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Office 2016" >> C:\Logs\1703-Upgrade.log
}


# Remove 1703 apps
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) PREPARING TO REMOVE APPS" >> C:\Logs\1703-Upgrade.log
$AppsList = "Microsoft.WindowsFeedbackHub", "Microsoft.XboxIdentityProvider", "Microsoft.Windows.HolographicFirstRun", "Windows.ContactSupport", "Microsoft.XboxGameCallableUI", "HoloShell", "HoloItemPlayerApp", "HoloCamera", "Microsoft.OneConnect", "Microsoft.People", "Microsoft.XboxSpeechToTextOverlay", "Microsoft.XboxGameOverlay", "Microsoft.SkypeApp", "Microsoft.MicrosoftSolitaireCollection", "Microsoft.MicrosoftOfficeHub", "Microsoft.3DBuilder", "Microsoft.Getstarted", "Microsoft.Microsoft3DViewer", "Microsoft.Office.OneNote", "Microsoft.XboxApp", "Microsoft.ZuneMusic", "Microsoft.ZuneVideo", "Microsoft.MSPaint"
ForEach ($App in $AppsList)
{
  $PackageFullName = (Get-AppxPackage $App).PackageFullName
  $ProPackageFullName = (Get-AppxProvisionedPackage -online | where {$_.Displayname -eq $App}).PackageName
  Write-Host $PackageFullName
  Write-Host $ProPackageFullName

  If ($PackageFullName)
  {
    Write-Host “Removing Package: $App”
    Remove-AppxPackage -package $PackageFullName -ErrorVariable +ProcessError
    Write-Log -Passed "Removed $App" -Failed "Failed to remove $App"

  }
  else
  {
    Write-Output "$(Get-TimeStamp) Unable to find package: $App” >> C:\Logs\1703-Upgrade.log
  }

  if ($ProPackageFullName)
  {
    Write-Host “Removing Provisioned Package: $ProPackageFullName”
    Remove-AppxProvisionedPackage -online -packagename $ProPackageFullName -ErrorVariable +ProcessError
    Write-Log -Passed "Removed $ProPackageFullName" -Failed "Failed to remove $ProPackageFullName"
  }
  else
  {
    Write-Output "$(Get-TimeStamp) Unable to find provisioned package: $App” >> C:\Logs\1703-Upgrade.log
  }
}
$ErrorCount = $Errors
Write-Output " " >> C:\Logs\1703-Upgrade.log
Write-Output "$(Get-TimeStamp) CLEANUP COMPLETE - FOUND $ErrorCount ERRORS" >> C:\Logs\1703-Upgrade.log

Fix Citrix Receiver DPI issues

Have you tried running a Citrix published application with Windows 10 after modifying your DPI settings? Well, you may have notice that your application is scaled incorrectly and your cursor is a little off. If you have a user that MUST use a higher DPI setting, then you can run the following command on the user’s PC to fix the issue:

reg add "HKCU\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" /v "C:\Program Files (x86)\Citrix\ICA Client\wfica32.exe" /t REG_SZ /d "~ WIN7RTM" /f

This command will automatically set the compatibility settings for “C:\Program Files (x86)\Citrix\ICA Client\wfica32.exe” and it will configure the EXE to run in compatibility mode for Windows 7.

However if you do not need to scale your published application, then you can run the following command to disable display scaling on high DPI settings for “C:\Program Files (x86)\Citrix\ICA Client\wfica32.exe”:

reg add "HKCU\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers" /v "C:\Program Files (x86)\Citrix\ICA Client\wfica32.exe" /t REG_SZ /d "~ HIGHDPIAWARE" /f

How to run a post script after a Windows 10 feature upgrade with SetupConfig.ini

If you are planning on upgrading your Windows 10 OS from 1607 to 1703 you may have noticed that a few apps have reappeared. Luckily for us, Microsoft has provided a way to add parameters to upgrades with the SetupConfig.ini file.

For example, you can create a Setupconfig.ini with the following:
Note that the header [SetupConfig] is required.

[SetupConfig]
NoReboot
ShowOobe=None
Telemetry=Enable

This is equivalent to the following command line:

Setup.exe /NoReboot /ShowOobe None /Telemetry Enable

How does the upgrade use the SetupConfig.ini file?
If the update is delivered through Windows Update, Windows Setup searches in a default location for a setupconfig file. You can include the setupconfig file here:
“%systemdrive%\Users\Default\AppData\Local\Microsoft\Windows\WSUS\SetupConfig.ini”

How do you run a post script?
You can easily add a post script by adding the PostOOBE parameter to the SetupConfig file as shown below:

[SetupConfig]
PostOOBE=C:\SetupComplete.cmd

How do you run a Powershell script?
The only way that I have been able to run a Powershell script is by running it from the .cmd file that I have called using the PostOOBE parameter.

Inside of the cmd file, you would have to add the following command to launch a Powershell script:

Powershell.exe -ExecutionPolicy Bypass -File "C:\Scripts\Remove-Apps-1703.ps1" -WindowStyle Hidden

I hope this post helps you understand the power of this ini file. Feel free to leave any questions in the comments and stay tuned for the cleanup script that I am currently working on!

UPDATE: The post clean up script can be found here!

How to remove the Contact Support app in Windows 10 1607 and above

The Contact Support app can now officially be removed but the process is a little different than how you would normally remove a Windows app with Powershell. Luckily it isn’t too difficult and it can be done with a one liner!

Get-WindowsCapability -online | ? {$_.Name -like ‘*ContactSupport*’} | Remove-WindowsCapability –online

New laptop chassis type is not recognized by MDT

I recently ran into an issue where MDT could not determine if our new Dell XPS 13 9365 was a laptop or desktop. After running wmic path win32_systemenclosure get chassistypes, I was able to determine that the chassis type 31 was not listed in MDT’s ZTIGather.wsf. Adding 31 to ZTIGather.wsf (Line 417) as shown below, fixed the issue and MDT was now able to determine that the XPS 13 9365 was a laptop!

Example:

Select Case objInstance.ChassisTypes(0)
Case "8", "9", "10", "11", "12", "14", "18", "21", "31"
	bIsLaptop = true
Case "3", "4", "5", "6", "7", "15", "16"
	bIsDesktop = true
Case "23"
	bIsServer = true
Case Else
	' Do nothing
End Select

Feel free to leave any questions in the comment section!

“The universal unique identifier (UUID) type is not supported” MDT Fix

During our Windows 10 testing, we noticed that some users would randomly come across the universal unique identifier (UUID) type is not supported error when they logged onto their computer for the first time. In order to get around this error, Microsoft provided a work around that would work with SCCM. Click here for the article.

Unfortunately this does not work well with MDT because the administrator account does not have permission to add a value to the “HKLM\SYSTEM\CurrentControlSet\Services\gpsvc” registry key.
The following Powershell script will fix this by changing the owner of the key to the Administrators group and also providing full access to the Administrators group. This will be temporary since sysprep seems to revert the permissions after it has processed. Fortunately the value stays with the registry key!

Note: In order to have this fix work successfully with MDT, we will need to configure the script to run before the sysprep step in your capture task sequence.

$definition = @"
using System;
using System.Runtime.InteropServices;
 
namespace Win32Api
{
 
public class NtDll
{
[DllImport("ntdll.dll", EntryPoint="RtlAdjustPrivilege")]
public static extern int RtlAdjustPrivilege(ulong Privilege, bool Enable, bool CurrentThread, ref bool Enabled);
}
}
"@
 
Add-Type -TypeDefinition $definition -PassThru
 
$bEnabled = $false
$res = [Win32Api.NtDll]::RtlAdjustPrivilege(9, $true, $false, [ref]$bEnabled)

# Change Owner to the local Administrators group
$regKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Services\gpsvc",[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::TakeOwnership)
$regACL = $regKey.GetAccessControl()
$regACL.SetOwner([System.Security.Principal.NTAccount]"Administrators")
$regKey.SetAccessControl($regACL)

# Change Permissions for the local Administrators group
$regKey = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey("SYSTEM\CurrentControlSet\Services\gpsvc",[Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree,[System.Security.AccessControl.RegistryRights]::ChangePermissions)
$regACL = $regKey.GetAccessControl()
$regRule = New-Object System.Security.AccessControl.RegistryAccessRule ("Administrators","FullControl","ContainerInherit","None","Allow")
$regACL.SetAccessRule($regRule)
$regKey.SetAccessControl($regACL)

# Add registry key fix
cmd /c reg add "HKLM\SYSTEM\CurrentControlSet\Services\gpsvc" /v Type /t REG_DWORD /d 0x10 /f

Feel free to leave any questions in the comments!

Add a wireless profile to your MDT task sequence

First you will need to export the configuration for the wireless profile that you would like to add to your task sequence.
Note: You will need to connect to the wireless profile before you can export it.

:: ADD YOUR WIRELESS PROFILE NAME
SET WIFI-PROFILE="YOUR WIRELESS PROFILE"
:: EXPORT WIRELESS CONFIGURATION
NETSH WLAN EXPORT PROFILE "%WIFI-PROFILE%" FOLDER="%USERPROFILE%\Desktop" KEY=Clear

The export should have copied an XML file to your desktop. In order to keep this simple, go ahead and rename the XML file WirelessProfile.xml. Now, copy WirelessProfile.xml and place it inside of your Deployment Share.
For this example, I will be copying the XML file into a folder called Custom, inside of your scripts folder.

Now go ahead and open up your task sequence and add a “Run Command Line” task inside of the State Restore group.

You can name the task anything you would like but in this example I have named it “Add Wireless Profile”.

Last but not least, you will need to add the following in the Command Line field:

NETSH WLAN ADD PROFILE FILENAME="%SCRIPTROOT%\Custom\WirelessProfile.xml" USER=All

Update (5/2/2018):
There seems to be an issue with running XML files that are not stored locally. If the command to add the wireless does not work, try copying the XML file locally and then running the command.

Now you will have a pre-configured Wireless profile!

Registry Keys for Windows 10 Privacy Settings

The following registry keys in this post control the privacy settings in Windows 10 1607. These settings can be found in the GUI by going to SETTINGS\PRIVACY.
Read more

Pin shortcuts to a user in a specific Active Directory group.

The following Powershell script will pin the Chrome shortcut to the Windows 10 start menu for anyone inside of a specific Active Directory group. In order to pin to the start menu, you will need to verify if your shortcut can be pinned through the GUI. To check this, you can right click your shortcut and see if you have “Pin to Start” available in your context menu. If you do not see this, then you may want to try copying the shortcut to:
%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs

In order to configure the script, you will need to provide values for the following variables:
$Group = YOUR AD GROUP
$Shortcut = “THE SHORTCUT NAME WITH THE EXTENSION”
$Location = “THE SHORTCUT’S LOCATION”

# Variables that need to be set
$Group = "YOUR AD GROUP HERE"
$Shortcut = "Google Chrome.lnk"
$Location = "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"


# Get User's Info
$objSearcher = New-Object System.DirectoryServices.DirectorySearcher
$objSearcher.SearchRoot = New-Object System.DirectoryServices.DirectoryEntry
$objSearcher.Filter = "(&(objectCategory=User)(SAMAccountName=$env:USERNAME))"
$objSearcher.SearchScope = "Subtree"
$obj = $objSearcher.FindOne()
$User = $obj.Properties["distinguishedname"]

# Get Group Info
$objSearcher.Filter = "(&(objectCategory=group)(SamAccountname=$Group))"
$objSearcher.SearchScope = "Subtree"
$obj = $objSearcher.FindOne()
[String[]]$Members = $obj.Properties["member"]

If ($Members -contains $User) { 
    $object= New-Object -ComObject shell.application
    $folder = $object.Namespace("$Location")
    $file= $folder.parsename("$Shortcut")
    $file.InvokeVerb('pintostartscreen')  
}