Understanding the ‘Correct’ File Version used for Intune Win32 App Detection Method

Depending on the developer for an application, various & differing version numbers can be added into the metadata of an application/executable/binary which can sometimes be confusing when creating manual detecton rules as part of an Intune Win32 App.

A good example of this is ‘CMTrace.exe’ from Microsoft. The screenshot below shows the ‘Details’ tab of ‘CMTrace.exe’ file properties with ‘File version’ highlighted:

You can see from the screenshot that the ‘File Version’ displayed is ‘5.0.9068.1000′ which is what I initially used for my Detection Rule as part of a CMTrace Win32 App in Intune:

Unfortunately this resulted in a continuous loop of ‘CMTrace.exe’ being reinstalled as it wasn’t being detected correctly by Intune – this was visible in the log file below:

C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log. 

Turns out that Intune actually uses a different property of the file metadata to determine the version number of a file. This is documented in the Microsoft article below:

https://learn.microsoft.com/en-us/mem/intune/apps/apps-win32-troubleshoot#detecting-the-win32-app-file-version-by-using-powershell

Two quick & easy ways to retrieve the correct ‘Version’ value to use in a Detection Rule are shown in the PowerShell commands below. Both commands are querying the same file in the same location on a reference computer i.e., ‘CMTrace.exe’ in the ‘C:\Windows’ directory:

[System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Windows\cmtrace.exe").FileVersion

(Get-Item -Path "C:\Windows\CMTrace.exe").VersionInfo.FileVersion

The value returned from these queries was ‘5.00.9068.1000′.

The additional zero in the version number was the difference that broke the configured Detection Rule.

Using the correct value returned by either of these two PowerShell commands (they are both returning the same information from .NET) will match the value retrieved by Intune when checking if the Win32 App is installed or not.

This will result in the Win32 App being installing once and then correctly being detected as installed thereafter i.e., the Detection Rule will now successfully work as expected.

/ JC

App-V Client and Sequencer Support Update

Microsoft have announced a change to their stance on App-V support.

Whilst the streaming server will still be deprecated, the App-V Client and Sequencer will now be “un-deprecated” and moved into Fixed Extended Support as part of Windows.

So good news if you consume App-V apps via ConfigMgr, not so good if you use them streamed over the network.

https://learn.microsoft.com/en-us/microsoft-desktop-optimization-pack/app-v/appv-support-policy

Registry Changes with PowerShell

It’s a common requirement to configure Registry values on an endpoint when building or configuring them.

I like to use PowerShell to do this as it’s easy to document and keep track of changes.

One issue with the standard PowerShell method (Set-ItemProperty) to do this is that you usually need a key to exist before you can add values into it. This usually means having to figure out if a key exists and if not, running several ‘New-Item’ commands to create the key (and potentially the tree/path to the key) itself first.

Fine for a few settings but not ideal when you have 10’s or even 100’s of registry settings to deploy.

Using a .REG file avoids this as it imports the key structure and the key values too. But this adds obfuscation into mix as you need to open each .REG file to see what it contains and that can be unintuitive sometimes.

The solution to this is to call registry changes directly using the “[Microsoft.Win32.Registry]::SetValue” .NET method as this behaves in the same way as importing .REG files in that it will create the key and path to the key regardless of whether the key/path already exists. Quick and easy (also highly performant!).

The format of these commands wasn’t initially obvious from Microsoft documentation but after a lot of trial and error, frustration and profuse swearing, I can provide the following examples for the most common types of Registry changes:

<#
.DESCRIPTION
    Script to configure Registry Items
.EXAMPLE
    Powershell.exe -ExecutionPolicy Bypass -File <ScriptName>.ps1
.NOTES
    VERSION     AUTHOR              CHANGE
    1.0         Jonathan Conway     Initial Script creation
    [Microsoft.Win32.Registry]::SetValue("keyName", "valueName", "value", [Microsoft.Win32.RegistryValueKind]::DWord)
    [Microsoft.Win32.Registry]::SetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Example", "ExampleValueName", "ExampleValue", [Microsoft.Win32.RegistryValueKind]::String)
    [Microsoft.Win32.Registry]::LocalMachine.DeleteSubKey("SOFTWARE\Example")
   
    Valid Root names are: "HKEY_CURRENT_USER", "HKEY_LOCAL_MACHINE", "HKEY_CLASSES_ROOT", "HKEY_USERS", "HKEY_PERFORMANCE_DATA", "HKEY_CURRENT_CONFIG", and "HKEY_DYN_DATA".
    Valid "RegistryValueKind" values: "String", "ExpandString", "Binary", "DWord", "MultiString" and "QWord".
#>
# Binary Value
$ExampleBinaryValue = [byte[]](150, 18, 6, 230, 16, 0, 0, 0)
[Microsoft.Win32.Registry]::SetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Test", "BinaryExampleName", [byte[]]$ExampleBinaryValue, [Microsoft.Win32.RegistryValueKind]::Binary)
# Dword Value
[Microsoft.Win32.Registry]::SetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Test", "DwordExampleName", [int]"1", [Microsoft.Win32.RegistryValueKind]::DWord)
# String Value
[Microsoft.Win32.Registry]::SetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Test", "StringExampleName", [string]"ExampleValue", [Microsoft.Win32.RegistryValueKind]::String)
# MultiString Value
$MultiStringExampleValue = @(
    "ExampleValue1",
    "ExampleValue2",
    "ExampleValue3",
    "ExampleValue4"
)
[Microsoft.Win32.Registry]::SetValue("HKEY_LOCAL_MACHINE\SOFTWARE\Test", "ExampleName", [string[]]$MultiStringExampleValue, [Microsoft.Win32.RegistryValueKind]::MultiString)

Posting this so I never have to go through the ordeal of getting these commands formatted correctly… And of course to benefit the community too! 😉

/ JC

Extract Content From MSI Files

To extract the contents of a Microsoft Software Installer (MSI) file you need to run the following command against the MSI that you are working with:

msiexec /a <FullPathToMSISourceFile> /qb TARGETDIR=<FullPathToExtractedContentFolder>

So a working example of this would be as follows:

msiexec /a "C:\Temp\Nutanix-VirtIO-1.2.3-x64.msi" /qb TARGETDIR="C:\Temp\Extract"

/ JC

Intune | Force Microsoft Edge Update to Latest Version During Windows Autopilot

Quick and simple post today. Had a customer deploying Windows 10 IoT Enterprise LTSC 2021 (yes I am aware that IoT Enterprise and LTSC are not officially supported for Windows Autopilot at time of writing but it works fine so… ) and there was a requirement to update the version of Edge included in this version of Windows so that it was at a version which supported some of the more recent Intune Configuration Profile policy settings. Should this not happen then the policies would not apply until such time as Edge updated itself which may be some time after a user had logged into the device.

To accomplish this I wrapped the following PowerShell script into a Win32 app and had it configured as a Blocking application on the Enrolment Status Page (ESP) being used for Autopilot.

The result is Microsoft Edge updating to the latest available version before any users log in for the first time.

<#
.DESCRIPTION
    PowerShell script to force update of Microsoft Edge Stable
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
    VERSION     AUTHOR              CHANGE
    1.0         Jonathan Conway     Initial script creation
#>

$Exe = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Microsoft\EdgeUpdate\MicrosoftEdgeUpdate.exe"
$Arguments = "/silent /install appguid={56EB18F8-B008-4CBD-B6D2-8C97FE7E9062}&appname=Microsoft%20Edge&needsadmin=True"
return (Start-Process $($Exe.FullName) -ArgumentList $Arguments -NoNewWindow -PassThru -Wait).ExitCode

Be sure to configure the detection method according to your environment. For me, I set this to a version of “Greater than or equal to: 100.0.0000.00” to detect the installation but you may want to use a higher version number depending on your own circumstances:

Let me know if this works for you (or if you have any issues with the script) in the comments.

/ JC

Intune Proactive Remediation: Detect & Remove User-installed Instances of Zoom

Had a requirement to detect and remove any user installations of Zoom (i.e. installed using standard user permissions and located in the user profile) via Intune. The supported route for uninstalling Zoom is use a Zoom-provided tool called ‘CleanZoom.exe’ so the script checks for that tool being present and if not, downloads and extracts it directly from Zoom before running the tool to remove any user installations of Zoom. Also needed a log file to show when this has been done from the client (this can obviously be removed if not needed).

Proactive Remediations to the rescue again!

Detection:

<#
.DESCRIPTION
	Proactive Remediation | Detection
.EXAMPLE
	PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
	VERSION     AUTHOR              CHANGE
    1.0         Jonathan Conway     Initial script creation
#>

# Discovery
try {
    # Run Test and store as variable
    $Test = Get-ChildItem -Path "C:\Users\" -Filter "Zoom.exe" -Recurse -Force -ErrorAction SilentlyContinue

    # Check where test is compliant or not - if no instances of Zoom are discovered then mark as 'Compliant' and exit with 0
    if ($null -eq $Test) {
        Write-Output "Compliant"
        exit 0
    }
    # If instances of Zoom are discovered then mark as 'Non Compliant' and exit with 1
    else {
        Write-Warning "Non Compliant"
        exit 1
    }
}

catch {
    # If any errors occur then return 'Non Compliant'
    Write-Warning "Non Compliant"
    exit 1
}

Remediation:

<#
.DESCRIPTION
	Proactive Remediation | Remediation
.EXAMPLE
	PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
	VERSION     AUTHOR              CHANGE
    1.0         Jonathan Conway     Initial script creation
#>

# Logging
$LogPath = "C:\Support\Zoom\"
Start-Transcript -Path $LogPath\ZoomCleanup.log -Append -NoClobber

# Variables
$CleanZoomTool = "C:\Support\Zoom\CleanZoom.exe"

# Check to see if 'C:\Support\Zoom' exists
$CheckZoomFolder = Test-Path -Path "C:\Support\Zoom\" -PathType Container

# If 'C:\Support\Zoom' folder does not exist then create it
if ($CheckZoomFolder -eq $false) {

	# Create folder
	Write-Output "'C:\Support\Zoom' folder does not exist - creating it"
	New-Item -Path "C:\Support" -Name "Zoom" -ItemType "Directory" -Force

}
else {
	Write-Output "'C:\Support\Zoom' folder exists - continuing"
}

# Check if CleanZoom.exe exists on the device
$CheckZoomClean = Test-Path -Path $CleanZoomTool -PathType "Leaf"

# If CleanZoom.exe does not exist on the device - download from Zoom website and extract locally
if ($CheckZoomClean -eq $false) {

	Write-Output "'C:\Support\Zoom\CleanZoom.exe' does not exist - downloading and extracting it"
	Invoke-WebRequest -Uri "https://assets.zoom.us/docs/msi-templates/CleanZoom.zip" -OutFile "C:\Support\Zoom\CleanZoom.zip"
	Expand-Archive -Path "C:\Support\Zoom\CleanZoom.zip" -DestinationPath "C:\Support\Zoom" -Force
	Remove-Item -Path "C:\Support\Zoom\CleanZoom.zip" -Force

}
else {
	Write-Output "'C:\Support\Zoom\CleanZoom.exe' exists - continuing"
}

try {
	# Run CleanZoom.exe to remove any installed instances of Zoom client in User Profiles
	Write-Output "Running CleanZoom.exe to remove Zoom instances from User Profile areas"
	Start-Process -FilePath $CleanZoomTool -ArgumentList "/silent"
	exit 0
}
catch {
	Write-Output "CleanZoom.exe failed to run"
	exit 1
}

Stop-Transcript

/ JC

Intune Proactive Remediation: BitLocker Key Escrow to Azure AD After MCM OSD Task Sequence

Recently had a customer requirement to encrypt Windows 10 devices using a MCM Task Sequence and then have the Recovery Keys escrowed into AAD once an Intune Drive Encryption policy was applied via Co-management workload shift (Endpoint Protection).

By default, Windows will escrow to where you tell it in the Task Sequence and not escrow into AAD. In my case the Task Sequence was storing the Recovery Key into on-prem Active Directory.

The Discovery script checks Event Viewer for an Event 845 including the text “was backed up successfully to your Azure AD” having been logged in the last 7 days (this can obviously be amended to suit individual requirements).

If non-compliant then the Remediation script forces the key to be escrowed using the ‘BackupToAAD-BitLockerKeyProtector’ PowerShell cmdlet.

Detection:

<#
.DESCRIPTION
    Script to check for BitLocker Key escrow into Azure AD
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
    VERSION     AUTHOR              CHANGE
    1.0         Jonathan Conway     Initial script creation
#>

# Check for Event 845 in BitLocker API Management Event Log over last 7 days - if contains text "was backed up successfully to your Azure AD" then Detection is complete
try {
    $Result = Get-WinEvent -FilterHashTable @{LogName = "Microsoft-Windows-BitLocker/BitLocker Management"; StartTime = (Get-Date).AddDays(-7) } | Where-Object { ($_.Id -eq "845" -and $_.Message -match "was backed up successfully to your Azure AD") } | Format-Table -Property "Message"
    $ID = $Result | Measure-Object

    if ($ID.Count -ge 1) {
        Write-Output "BitLocker Recovery Key escrow to Azure AD succeeded = Compliant"
        exit 0
    }

    # If Event is not detected then mark as 'Non Compliant' and exit with 1
    else {
        Write-Warning "BitLocker Escrow Event Missing = Non Compliant"
        exit 1
    }
}

catch {
    Write-Warning "An error occurred = Non Compliant"
    exit 1
}

Remediation:

<#
.DESCRIPTION
    Script to remediate BitLocker Key escrow into Azure AD
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
	VERSION     AUTHOR              CHANGE
    1.0         Jonathan Conway     Initial script creation
#>

# Escrow BitLocker Recovery Key for OSDrive into Azure AD
$BitLockerVolume = Get-BitLockerVolume -MountPoint $env:SystemRoot
$RecoveryPasswordKeyProtector = $BitLockerVolume.KeyProtector | Where-Object { $_.KeyProtectorType -like "RecoveryPassword" }
BackupToAAD-BitLockerKeyProtector -MountPoint $BitLockerVolume.MountPoint -KeyProtectorId $RecoveryPasswordKeyProtector.KeyProtectorId -ErrorAction SilentlyContinue

/ JC

Finding Your Windows 10 OEM Product Key Embedded In Firmware/WMI

To retrieve the OEM Windows 10 Product Key which is usually stored within a devices WMI repository you can use the following PowerShell command:

(Get-CimInstance -ClassName SoftwareLicensingService).OA3xOriginalProductKey

To retrieve and then install an OEM Windows 10 Product Key, the following PowerShell can be used:

<#
.DESCRIPTION
    Script to activate Windows 10 using OEM licence key
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
    Author(s):  Jonathan Conway
    Modified:   08/09/2021
    Version:    1.0
#>

$OemProductKey = (Get-CimInstance -ClassName "SoftwareLicensingService").OA3xOriginalProductKey
$cscript = 'C:\Windows\System32\cscript.exe'

# Install Product Key
return (Start-Process $cscript -ArgumentList "/B C:\Windows\System32\slmgr.vbs -ipk $OemProductKey" -NoNewWindow -PassThru -Wait).ExitCode
return (Start-Process $cscript -ArgumentList "/B C:\Windows\System32\slmgr.vbs -ato" -NoNewWindow -PassThru -Wait).ExitCode

You can also use WMI to accomplish the same things which is demonstrated in the PowerShell script below:

<#
.DESCRIPTION
    Script to activate Windows 10 using OEM licence key
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
    Author(s):  Jonathan Conway
    Modified:   10/05/2022
    Version:    1.0
#>

# Variables
$SLS = Get-CimInstance -ClassName "SoftwareLicensingService"
$OemProductKey = (Get-CimInstance -ClassName "SoftwareLicensingService").OA3xOriginalProductKey

# Display and Activate Product Key
Write-Output "$OemProductKey"
$SLS | Invoke-CimMethod -MethodName "InstallProductKey" -Arguments @{ ProductKey = "$OemProductKey" }
$SLS | Invoke-CimMethod -MethodName "RefreshLicenseStatus"

# JC

PowerShell | Working with Trusted Platform Modules (TPM) via WMI during OSD

Because the legacy WMI PowerShell cmdlets (e.g. Get-WmiObject) are eventually going to be deprecated, I always try to use the newer CIM-based PowerShell cmdlets (e.g. Get-CimInstance) wherever possible.

This can be a bit confusing sometimes though and it can appear that the new CIM cmdlets have less functionality than their older WMI counterparts. This isn’t the case as I explain later on in the blog post.

This perceived difference is especially true when working with TPM chips on devices. Below is an example of running a query against the ‘Win32_Tpm‘ class in WMI using both the old and new cmdlets.

The legacy ‘Get-WmiObject‘ cmdlet shows ‘70‘ Properties/Methods while the newer ‘Get-CimInstance‘ cmdlet shows only ‘20‘.

(Get-WmiObject -Namespace 'root/cimv2/Security/MicrosoftTpm' -Class 'Win32_Tpm' | Get-Member).Count
70

(Get-CimInstance -Namespace 'root/cimv2/Security/MicrosoftTpm' -Class 'Win32_Tpm' | Get-Member).Count
20

One WMI Method that I use regularly with OSD is the ‘SetPhysicalPresenceRequest‘ Method to configure a TPM to be cleared, activated and enabled. If you use the value of ‘14‘ for the request then you need to configure the firmware/BIOS to not require Physical Presence otherwise you’ll need someone to physically press a key to confirm the TPM clear is allowed.

If you can’t configure the firmware/BIOS to disable requiring physical presence confirmation then you can use the request value of ‘10‘ which won’t ask for physical confirmation but is slightly less effective. Using ‘10‘ should still mean your TPM is ready to be accessed by encryption-related commands later on in the Task Sequence though.

To use this command in a MCM Task Sequence I would historically use a ‘Run Command Line‘ task to run the following PowerShell command:

powershell.exe -ExecutionPolicy bypass -Command "(Get-WmiObject -Namespace "root\CIMV2\Security\MicrosoftTpm" -Class Win32_TPM).SetPhysicalPresenceRequest(14)"

Given my previous statement that I want to use the more modern ‘Get-CimInstance‘ cmdlets I looked into how this could be done with the newer cmdlets so that if or when the legacy WmiObject cmdlets are no longer available in Windows, my Task Sequence commands will continue to run successfully without any changes being needed.

By running ‘Get-WmiObject‘ we can see that ‘SetPhysicalPresenceRequest‘ is listed as an available Method for us to use:

Get-WmiObject -Namespace 'root/cimv2/Security/MicrosoftTpm' -Class 'Win32_Tpm' | Get-Member -MemberType Method


   TypeName: System.Management.ManagementObject#root\cimv2\Security\MicrosoftTpm\Win32_Tpm

Name                                  MemberType Definition
----                                  ---------- ----------
AddBlockedCommand                     Method     System.Management.ManagementBaseObject AddBlockedCommand(System.UIn...
ChangeOwnerAuth                       Method     System.Management.ManagementBaseObject ChangeOwnerAuth(System.Strin...
Clear                                 Method     System.Management.ManagementBaseObject Clear(System.String OwnerAuth)
ConvertToOwnerAuth                    Method     System.Management.ManagementBaseObject ConvertToOwnerAuth(System.St...
CreateEndorsementKeyPair              Method     System.Management.ManagementBaseObject CreateEndorsementKeyPair()
Disable                               Method     System.Management.ManagementBaseObject Disable(System.String OwnerA...
DisableAutoProvisioning               Method     System.Management.ManagementBaseObject DisableAutoProvisioning(Syst...
Enable                                Method     System.Management.ManagementBaseObject Enable(System.String OwnerAuth)
EnableAutoProvisioning                Method     System.Management.ManagementBaseObject EnableAutoProvisioning()
GetCapLockoutInfo                     Method     System.Management.ManagementBaseObject GetCapLockoutInfo()
GetDictionaryAttackParameters         Method     System.Management.ManagementBaseObject GetDictionaryAttackParameters()
GetOwnerAuth                          Method     System.Management.ManagementBaseObject GetOwnerAuth()
GetOwnerAuthForEscrow                 Method     System.Management.ManagementBaseObject GetOwnerAuthForEscrow()
GetOwnerAuthStatus                    Method     System.Management.ManagementBaseObject GetOwnerAuthStatus()
GetPhysicalPresenceConfirmationStatus Method     System.Management.ManagementBaseObject GetPhysicalPresenceConfirmat...
GetPhysicalPresenceRequest            Method     System.Management.ManagementBaseObject GetPhysicalPresenceRequest()
GetPhysicalPresenceResponse           Method     System.Management.ManagementBaseObject GetPhysicalPresenceResponse()
GetPhysicalPresenceTransition         Method     System.Management.ManagementBaseObject GetPhysicalPresenceTransition()
GetSrkADThumbprint                    Method     System.Management.ManagementBaseObject GetSrkADThumbprint(System.By...
GetSrkPublicKeyModulus                Method     System.Management.ManagementBaseObject GetSrkPublicKeyModulus()
GetTcgLog                             Method     System.Management.ManagementBaseObject GetTcgLog()
ImportOwnerAuth                       Method     System.Management.ManagementBaseObject ImportOwnerAuth(System.Strin...
IsActivated                           Method     System.Management.ManagementBaseObject IsActivated()
IsAutoProvisioningEnabled             Method     System.Management.ManagementBaseObject IsAutoProvisioningEnabled()
IsCommandBlocked                      Method     System.Management.ManagementBaseObject IsCommandBlocked(System.UInt...
IsCommandPresent                      Method     System.Management.ManagementBaseObject IsCommandPresent(System.UInt...
IsEnabled                             Method     System.Management.ManagementBaseObject IsEnabled()
IsEndorsementKeyPairPresent           Method     System.Management.ManagementBaseObject IsEndorsementKeyPairPresent()
IsFIPS                                Method     System.Management.ManagementBaseObject IsFIPS()
IsKeyAttestationCapable               Method     System.Management.ManagementBaseObject IsKeyAttestationCapable()
IsLockedOut                           Method     System.Management.ManagementBaseObject IsLockedOut()
IsOwned                               Method     System.Management.ManagementBaseObject IsOwned()
IsOwnerClearDisabled                  Method     System.Management.ManagementBaseObject IsOwnerClearDisabled()
IsOwnershipAllowed                    Method     System.Management.ManagementBaseObject IsOwnershipAllowed()
IsPhysicalClearDisabled               Method     System.Management.ManagementBaseObject IsPhysicalClearDisabled()
IsPhysicalPresenceHardwareEnabled     Method     System.Management.ManagementBaseObject IsPhysicalPresenceHardwareEn...
IsReady                               Method     System.Management.ManagementBaseObject IsReady()
IsReadyInformation                    Method     System.Management.ManagementBaseObject IsReadyInformation()
IsSrkAuthCompatible                   Method     System.Management.ManagementBaseObject IsSrkAuthCompatible()
OwnerAuthEscrowed                     Method     System.Management.ManagementBaseObject OwnerAuthEscrowed(System.Str...
Provision                             Method     System.Management.ManagementBaseObject Provision(System.Boolean For...
RemoveBlockedCommand                  Method     System.Management.ManagementBaseObject RemoveBlockedCommand(System....
ResetAuthLockOut                      Method     System.Management.ManagementBaseObject ResetAuthLockOut(System.Stri...
ResetSrkAuth                          Method     System.Management.ManagementBaseObject ResetSrkAuth(System.String O...
SelfTest                              Method     System.Management.ManagementBaseObject SelfTest()
SetPhysicalPresenceRequest            Method     System.Management.ManagementBaseObject SetPhysicalPresenceRequest(S...
TakeOwnership                         Method     System.Management.ManagementBaseObject TakeOwnership(System.String ...

Running the same command with the ‘Get-CimInstance‘ cmdlet brings back significantly fewer Methods and most importantly ‘SetPhysicalPresenceRequest‘ is missing from the list of Methods!!!!

Get-CimInstance -Namespace 'root/cimv2/Security/MicrosoftTpm' -ClassName 'Win32_Tpm' | Get-Member -MemberType Method


   TypeName: Microsoft.Management.Infrastructure.CimInstance#root/cimv2/Security/MicrosoftTpm/Win32_Tpm

Name                      MemberType Definition
----                      ---------- ----------
Clone                     Method     System.Object ICloneable.Clone()
Dispose                   Method     void Dispose(), void IDisposable.Dispose()
Equals                    Method     bool Equals(System.Object obj)
GetCimSessionComputerName Method     string GetCimSessionComputerName()
GetCimSessionInstanceId   Method     guid GetCimSessionInstanceId()
GetHashCode               Method     int GetHashCode()
GetObjectData             Method     void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System....
GetType                   Method     type GetType()
ToString                  Method     string ToString()

“Where’s my bloody Method?” I asked whilst preparing myself to overcome OCD and continue using the legacy command…

However, under the covers the ‘SetPhysicalPresenceRequest‘ method still exists in WMI but we just can’t see it as easily using ‘Get-CimInstance‘. In order to view these hidden Methods we need to run a slightly different PowerShell command as per below:

(Get-CimInstance -Namespace root/cimv2/Security/MicrosoftTpm -ClassName Win32_Tpm).CimClass.CimClassMethods

Name                                  ReturnType Parameters                                                         Qua
                                                                                                                    lif
                                                                                                                    ier
                                                                                                                    s
----                                  ---------- ----------                                                         ---
IsEnabled                                 UInt32 {IsEnabled}                                                        {De
IsOwned                                   UInt32 {IsOwned}                                                          {De
IsActivated                               UInt32 {IsActivated}                                                      {De
IsPhysicalClearDisabled                   UInt32 {IsPhysicalClearDisabled}                                          {De
IsOwnerClearDisabled                      UInt32 {IsOwnerClearDisabled}                                             {De
IsPhysicalPresenceHardwareEnabled         UInt32 {IsPhysicalPresenceHardwareEnabled}                                {De
IsOwnershipAllowed                        UInt32 {IsOwnershipAllowed}                                               {De
IsCommandPresent                          UInt32 {CommandOrdinal, IsCommandPresent}                                 {De
Enable                                    UInt32 {OwnerAuth}                                                        {De
Disable                                   UInt32 {OwnerAuth}                                                        {De
IsEndorsementKeyPairPresent               UInt32 {IsEndorsementKeyPairPresent}                                      {De
CreateEndorsementKeyPair                  UInt32 {}                                                                 {De
TakeOwnership                             UInt32 {OwnerAuth}                                                        {De
Clear                                     UInt32 {OwnerAuth}                                                        {De
IsSrkAuthCompatible                       UInt32 {IsSrkAuthCompatible}                                              {De
ResetSrkAuth                              UInt32 {OwnerAuth}                                                        {De
ChangeOwnerAuth                           UInt32 {NewOwnerAuth, OldOwnerAuth}                                       {De
SelfTest                                  UInt32 {SelfTestResult}                                                   {De
ConvertToOwnerAuth                        UInt32 {OwnerPassPhrase, OwnerAuth}                                       {De
SetPhysicalPresenceRequest                UInt32 {Request, RequestParameter}                                        {De
GetPhysicalPresenceRequest                UInt32 {Request}                                                          {De
GetPhysicalPresenceTransition             UInt32 {Transition}                                                       {De
GetPhysicalPresenceResponse               UInt32 {Request, Response}                                                {De
AddBlockedCommand                         UInt32 {CommandOrdinal}                                                   {De
RemoveBlockedCommand                      UInt32 {CommandOrdinal}                                                   {De
IsCommandBlocked                          UInt32 {CommandOrdinal, IsCommandBlocked}                                 {De
ResetAuthLockOut                          UInt32 {OwnerAuth}                                                        {De
IsReady                                   UInt32 {IsReady}                                                          {De
IsReadyInformation                        UInt32 {Information, IsReady}                                             {De
IsAutoProvisioningEnabled                 UInt32 {IsAutoProvisioningEnabled}                                        {De
EnableAutoProvisioning                    UInt32 {}                                                                 {De
DisableAutoProvisioning                   UInt32 {OnlyForNextBoot}                                                  {De
GetOwnerAuth                              UInt32 {OwnerAuth}                                                        {De
Provision                                 UInt32 {ForceClear_Allowed, PhysicalPresencePrompts_Allowed, Information} {De
ImportOwnerAuth                           UInt32 {OwnerAuth}                                                        {De
GetPhysicalPresenceConfirmationStatus     UInt32 {Operation, ConfirmationStatus}                                    {De
GetSrkPublicKeyModulus                    UInt32 {SrkPublicKeyModulus}                                              {De
GetSrkADThumbprint                        UInt32 {SrkPublicKeyModulus, SrkADThumbprint}                             {De
GetTcgLog                                 UInt32 {TcgLog}                                                           {De
IsKeyAttestationCapable                   UInt32 {TestResult}                                                       {De
GetOwnerAuthForEscrow                     UInt32 {OwnerAuth, OwnerAuthStatus}                                       {De
OwnerAuthEscrowed                         UInt32 {OwnerAuth}                                                        {De
GetOwnerAuthStatus                        UInt32 {OwnerAuthStatus}                                                  {De
IsFIPS                                    UInt32 {IsFIPS}                                                           {De
GetDictionaryAttackParameters             UInt32 {LockoutRecovery, MaxTries, RecoveryTime}                          {De
GetCapLockoutInfo                         UInt32 {LockoutCounter, MaxTries}                                         {De
IsLockedOut                               UInt32 {IsLockedOut}                                                      {De

So we can now see the required ‘SetPhysicalPresenceRequest‘ method. But how do we use it in a MCM Task Sequence in the same manner as the legacy cmdlet?

The answer is below – we need to pipe one cmdlet (Get-CimInstance) into another (Invoke-CimMethod) to achieve the same result as the legacy cmdlet:

powershell.exe -ExecutionPolicy Bypass -Command "Get-CimInstance -Namespace 'root/cimv2/Security/MicrosoftTpm' -ClassName 'Win32_TPM' | Invoke-CimMethod -MethodName 'SetPhysicalPresenceRequest' -Arguments @{Request='14'}"
Run Command Line

Running the newer CIM commands in my MCM ‘Run Command Line‘ task now gives me the same result as the legacy command did and balance is once again restored to the galaxy…

/ JC

ConfigMgr OSD | Windows 10 Disk Partitioning to Correctly Include Recovery Tools Partition

Edit 28/02/24: Updated to reflect increase of Recovery partition to 2048Mb.

How should disks be partitioned for Windows 10 so that the Recovery Partition is configured properly? Turns out the answer isn’t straightforward and that the standard tools provided provided in ConfigMgr by Microsoft don’t quite allow you to do it properly by default…

WinRE is important as it is used by many of the reset features used in Windows 10, especially in Modern Management scenarios. This includes manual recovery, Factory Reset and newer options such as Windows Autopilot Reset.

This partition is used to help recover an OS when not bootable which is why it needs to be located on a separate partition. This partition should be placed immediately after the Windows partition to allow Windows to modify and recreate the partition in the future if future updates (i.e. newer versions of Windows) require a larger Recovery Image. It also allows the device to be rebuilt (i.e., have the OS reinstalled) without affecting the Recovery Partition.

If this resizing/recreation of the Recovery Partition can’t be done during OS upgrades then the ability to use the WinRE environment can be removed and is tricky to remediate afterwards.

To produce this post I have read through a lot of other well-respected blogs and also analysed what a lot of experts have suggested on Twitter etc. This article is the summary of that analysis as well as providing a script written by me which can be used during ConfigMgr Task Sequences to create the required disk partitions correctly. The main resources I used were:

miketerrill.net

garytown.com

Lets start with the recommendations from Microsoft.

UEFI | GPT

For UEFI, Microsoft recommend the following partition layout for Windows 10 (Docs link can be found here):

diagram of default partition layout: system, msr, windows, and recovery
Microsoft Recommended Partition Layout

From this diagram we can see that Microsoft recommend a ‘System‘ partition’ (also known as a EFI System Partition (ESP)) which provides a device with a partition to boot from. It shouldn’t contain any files other than what is intended for booting the device.

‘System’ Partition

Following that is a ‘Microsoft Reserved‘ partition (MSR) which is used for partition management. No user data can be stored on this partition.

‘Microsoft Reserved’ Partition

The next partition is the ‘Windows‘ partition which is obviously used for the Windows Operating System.

‘Windows’ Partition

And the last partition (and the one which causes the most questions) is the ‘Recovery Tools‘ partition which will host a copy of the ‘Windows Recovery Environment’ (WinRE). WinRE is a recovery environment that can repair common causes of unbootable operating systems.

The Recovery Tools partition presents a problem as there is no built-in way in a Task Sequence of ensuring that the Recovery Tools partition is located at the end of the disk (i.e., after the Windows partition) and is configured correctly – this is the main reason for writing this blog post.

The table below summarises the best recommended sizes for each partition based on the Microsoft recommendations and also various experts around the web:

NameSize (Mb)Format
System (EFI)360 fixed sizeFAT32
MSR128 fixed size
Windows (Primary)100% of remaining space on diskNTFS
Recovery 2048 fixed sizeNTFS
UEFI Disk Partitions
UEFI | GPT Format and Partition Disk Step

BIOS | MBR

For traditional BIOS, Microsoft recommend the following partition layout for Windows 10 (Docs link can be found here). Note that ‘BIOS | MBR’ is now considered legacy and has limited use case scenarios with Modern Device Management. Many modern security controls are based on the newer UEFI firmware and so ‘BIOS | MBR’ and is therefore rarely used:

diagram of default partition layout: system, windows, and recovery

From this diagram we can see that Microsoft again recommends a ‘System‘ partition which is used to boot the device.

‘System’ Partition

Following that is the ‘Windows‘ partition to be used for the Windows 10 operating system.

‘Windows’ Partition

And the final partition is once again the ‘Recovery Tools‘ partition which was mentioned previously in the UEFI section above.

The table below summarises the best recommended sizes for each partition based on the Microsoft recommendations and also various experts around the web:

NameSize (Mb)Format
System360 fixed sizeNTFS
Windows (Primary)100% of remaining space on diskNTFS
Recovery 2048 fixed sizeNTFS
Traditional BIOS Disk Partitions
BIOS | MBR Format and Partition Disk Step

Recovery Tools Partition Solution

To create the Recovery Tools partition in the location and size required we need to use ‘Diskpart’ to create the Recovery Partition once the built-in ‘Format and Partition Disk‘ step has been completed.

The way this is done is to shrink the ‘Windows’ partition by the size needed (in this case 984Mb) and create a new partition with that newly-available space called ‘Recovery’.

For UEFI deployments, the ‘Recovery Tools’ partition needs to have it’s ‘ID‘ set to ‘de94bba4-06d1-4d40-a16a-bfd50179d6ac‘, be given the GPT attribute of ‘0x8000000000000001‘ and needs to be set to have a Partition Type of ‘27‘ so that it is hidden.

To accomplish this I wrote a PowerShell script to run Diskpart with the required settings. The script should be added to the Task Sequence just after the built-in ‘Format and Partition Disk’ step as per the images below:

Location of ‘Create Recovery Partition’ Step

The script works best when entered as a PowerShell script as part of a ‘Run PowerShell Script‘ task in a Task Sequence:

‘Create Recovery Partition’ Script Task

The script for automating the creation of the Recovery Partition is embedded below. It will detect if the device is configured for BIOS or UEFI and create the partition accordingly:

<#
.DESCRIPTION
    Script to create Recovery Partition during OSD and hide System Partition (if needed) on MBR

    This script requires the following optional Boot Image components: 'WinPE-DismCmdlets', 'WinPE-PowerShell', 'WinPE-StorageWMI'
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES

    VERSION     AUTHOR              CHANGE
    1.5         Jonathan Conway     Initial script creation
#>

# Loads the Task Sequence environment
$tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment

# Configures script log path to match tsenv logs folder
$LogPath = $tsenv.Value("_SMSTSLogPath")

# Determines if firmware configured as UEFI
$UEFI = $tsenv.Value("_SMSTSBootUEFI")

# Get OS Disk information
[string]$OSDrivePartitionNumber = (Get-Disk | Where-Object {$PSItem.BusType -ne 'USB'} | Get-Partition | Where-Object {$PSItem.Size -gt '5GB' -and $PSItem.Type -eq 'Basic'}).PartitionNumber

# Recovery partition size in Mb
$RecoveryPartitionSize = '2048'

If ($UEFI -eq $true) {

    'select disk 0',
    'list partition',
    "select partition $OSDrivePartitionNumber",
    "shrink desired=$RecoveryPartitionSize minimum=$RecoveryPartitionSize",
    'create partition primary',
    'format quick fs=ntfs label=Recovery',
    'set id="de94bba4-06d1-4d40-a16a-bfd50179d6ac"',
    'gpt attributes=0x8000000000000001',
    'list partition' | diskpart | Tee-Object -FilePath "$LogPath\Pwsh-OsdDiskpart.log"
}

else {
    'select disk 0',
    'list partition',
    "select partition $OSDrivePartitionNumber",
    "shrink desired=$RecoveryPartitionSize minimum=$RecoveryPartitionSize",
    'create partition primary',
    'format quick fs=ntfs label=Recovery',
    'set id=27',
    'list partition',
    'select partition 1', 'set id=17', 'list partition' | diskpart | Tee-Object -FilePath "$LogPath\Pwsh-OsdDiskpart.log"
}

The script uses variables for the OS Drive Partition and Recovery Partition Size so both of these can be modified according to specific requirements in the event that this is needed.

So this solution should allow you to correctly configure the disk partitions needed to install Windows 10 and also ensure that a functional Recovery Tools partition is always present.

/ JC

PowerShell: Automate Naming of Captured WIM File During MDT Reference Image Creation

If you’re Old Skool like me and still use MDT to produce Windows 10 Reference Images then this script may be useful to save some time and hassle.

The script basically automates the creation of the filename for the backup WIM file so that all that is required to produce a new image bi-annually (or as often as you like) is to run a Build & Capture Task Sequence which (providing the VM has access to WSUS or Microsoft Update) will include the all the latest patches.

It produces a filename in the following format which includes the Windows 10 version being captured, architecture, language and the date.

W10X64_20H2_en-GB_19042.572_2020-10-22_1525.wim

Because the date includes the time the image is captured, the filename will always be unique so there will never be an occasion where the image can’t be captured to a pre-existing WIM file being present with the same name.

The net result is that the Reference Image creation can be as simple as booting a VM, choosing a Task Sequence then collecting the WIM file it produces at the end of the process.

The script produces an MDT variable called ‘%WimFileName%‘ which is then used to populate the ‘BackupFile‘ property in the Task Sequence – this is demonstrated in the images below:

Configure: Set WIM Filename
Set: BackupFile

The script content is embedded below. Copy and paste into a blank text file and save as ‘Pwsh-SetWimFilename.ps1‘ and copy it into your MDT Deployment share in the following location:

DeploymentShare\Scripts\Custom

Script Content:

<#
.DESCRIPTION
    Script to automate the naming of the WIM File Name during MDT OSD
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1 [-Debug]
.NOTES
    Author(s):  Jonathan Conway
    Modified:   17/11/2021
    Version:    1.5

    Option [-Debug] switch can be run locally to output results to screen to test WIM File Name is correct
#>

Param (
    [Switch]$Debug
)

Begin {

    # Variables: Information from Registry
    $OsRegistryInfo = Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    [string]$DisplayVersion = $OsRegistryInfo.DisplayVersion
    [string]$ReleaseId = $OsRegistryInfo.ReleaseId
    [string]$Ubr = $OsRegistryInfo.UBR
    [string]$EditionId = $OsRegistryInfo.EditionID
    [string]$OsCurrentBuildNumber = $OsRegistryInfo.CurrentBuildNumber

    # Variables: Change 'ReleaseID' to new Windows Release naming format if Windows 10 20H2 or later
    if ($ReleaseId -gt '2004' -and $ReleaseId -lt '2009') {

        if ($ReleaseId -match "^(..)(01|02|03|04|05|06)$") {
            [string]$ReleaseId1stHalf = $ReleaseId.Substring(0, 2)
            [string]$ReleaseId2ndHalf = $ReleaseId.Substring(2, 2)
            [string]$ReleaseId2ndHalfReplaced = $ReleaseId2ndHalf -replace "$ReleaseId2ndHalf", "H1"
            [string]$ReleaseId = "$ReleaseId1stHalf" + "$ReleaseId2ndHalfReplaced"
        }

        if ($ReleaseId -match "^(..)(07|08|09|10|11|12)$") {
            [string]$ReleaseId1stHalf = $ReleaseId.Substring(0, 2)
            [string]$ReleaseId2ndHalf = $ReleaseId.Substring(2, 2)
            [string]$ReleaseId2ndHalfReplaced = $ReleaseId2ndHalf -replace "$ReleaseId2ndHalf", "H2"
            [string]$ReleaseId = "$ReleaseId1stHalf" + "$ReleaseId2ndHalfReplaced"
        }

    }

    elseif ($ReleaseId -ge '2009') {
        $ReleaseId = $DisplayVersion
    }

    # Variables: Information from WMI
    $OsWmiInfo = Get-CimInstance -ClassName 'Win32_OperatingSystem'

    # Variables: OS 'Caption' information
    $Caption = $OsWmiInfo.Caption
    [String]$RegExPattern = '(Microsoft\ (Windows|Hyper-V)\ (10|11|Server\ (2016.*?|2019.*?)))'
    [String]$MachineOS = ($Caption | Select-String -AllMatches -Pattern $RegExPattern | Select-Object -ExpandProperty 'Matches').Value

    # Variables: Media Language
    [string]$OsLanguageNumberCode = $OsWmiInfo.OSLanguage

    if ($OsLanguageNumberCode -eq '2057') {
        $OsLanguage = 'en-GB'
    }
    if ($OsLanguageNumberCode -eq '1033') {
        $OsLanguage = 'en-US'
    }

    # Variables: Date Information
    $BuildDate = Get-Date -Format "yyyy-MM-dd_HHmm"

    # Variables: OS Architecture
    if ($OsWmiInfo.OSArchitecture -eq '64-bit') {
        $Architecture = 'X64'
    }
    if ($OsWmiInfo.OSArchitecture -eq '32-bit') {
        $Architecture = 'X86'
    }

}

Process {

    # Microsoft Hyper-V Server
    if ($MachineOS -like 'Microsoft Hyper-V Server*') {
        # Variables: Set OS Prefix
        $HypervPrefix = 'HVS'

        # Variables: Set Windows Server Version
        $WindowsServerVersion = $MachineOS.TrimStart("Microsoft Hyper-V Server")

        # Hyper-V Server: Create Wim File Name string
        $WimFileName = "$HypervPrefix" + "$WindowsServerVersion" + "$Architecture" + '_' + "$OsLanguage" + '_' + "$OsCurrentBuildNumber" + '.' + "$Ubr" + '_' + "$BuildDate" + '.' + 'wim'
    }

    # Microsoft Windows 10
    if ($MachineOS -like 'Microsoft Windows 10*') {
        # Variables: Set OS Prefix
        $Os = 'W10'

        # Variables: OS Edition
        if ($EditionId -eq 'Enterprise') {
            $Edition = 'ENT'
        }
        if ($EditionId -eq 'IoTEnterprise') {
            $Edition = 'IOT'
            $ReleaseId = $OsRegistryInfo.DisplayVersion
        }
        if ($EditionId -eq 'EnterpriseS') {
            $Edition = 'IOT'
            $Channel = 'LTSC'
        }
        if ($EditionId -eq 'Professional') {
            $Edition = 'PRO'
        }

        if ($EditionId -eq 'EnterpriseS') {
            # Windows 10 IoT LTSC: Create Wim File Name string
            $WimFileName = "$Os" + "$Architecture" + '_' + "$Edition" + '_' + "2019" + '_' + "$Channel" + '_' + "$OsLanguage" + '_' + "$OsCurrentBuildNumber" + '.' + "$Ubr" + '_' + "$BuildDate" + '.' + 'wim'
        }
        else {
            # Windows 10: Create Wim File Name string
            $WimFileName = "$Os" + "$Architecture" + '_' + "$Edition" + '_' + "$ReleaseId" + '_' + "$OsLanguage" + '_' + "$OsCurrentBuildNumber" + '.' + "$Ubr" + '_' + "$BuildDate" + '.' + 'wim'
        }

    }

    # Microsoft Windows 11
    if ($MachineOS -like 'Microsoft Windows 11*') {
        # Variables: Set OS Prefix
        $Os = 'W11'

        # Variables: Set Display Version
        $OsDisplayVersion = $OsRegistryInfo.DisplayVersion

        # Variables: OS Edition
        if ($EditionId -eq 'Enterprise') {
            $Edition = 'ENT'
        }
        if ($EditionId -eq 'IoTEnterprise') {
            $Edition = 'IOT'
        }
        if ($EditionId -eq 'Professional') {
            $Edition = 'PRO'
        }

        # Windows 10: Create Wim File Name string
        $WimFileName = "$Os" + "$Architecture" + '_' + "$Edition" + '_' + "$OsDisplayVersion" + '_' + "$OsLanguage" + '_' + "$OsCurrentBuildNumber" + '.' + "$Ubr" + '_' + "$BuildDate" + '.' + 'wim'
    }

    # Microsoft Windows Server
    if ($MachineOS -like 'Microsoft Windows Server*') {
        # Variables: Set OS Prefix
        $ServerPrefix = 'WS'

        # Variables: Set Windows Server Version
        $WindowsServerVersion = $MachineOS.TrimStart("Microsoft Windows Server")

        # Windows Server: Create Wim File Name string
        $WimFileName = "$ServerPrefix" + "$WindowsServerVersion" + "$Architecture" + '_' + "$OsLanguage" + '_' + "$OsCurrentBuildNumber" + '.' + "$Ubr" + '_' + "$BuildDate" + '.' + 'wim'
    }

}

End {

    # If Debug is true then write WIM file name to host
    if ($Debug) {
        Write-Host "Caption is:         `"$Caption`"" -BackgroundColor 'Green' -ForegroundColor 'Black'
        Write-Host "MachineOS is:       `"$MachineOS`"" -BackgroundColor 'Green' -ForegroundColor 'Black'
        Write-Host "MachineOS is:       `"$EditionId`"" -BackgroundColor 'Green' -ForegroundColor 'Black'
        Write-Host "WIM File Name is:   `"$WimFileName`"" -BackgroundColor 'Green' -ForegroundColor 'Black'
    }

    else {
        # Set MDT Task Sequence Variable to be used to populate 'BackupFile'
        $tsenv:WimFileName = "$WimFileName"
    }

}

It is possible to test the output of the script by copying the script onto a device and running it with the ‘-debug’ switch. This will display the WIM Filename in the PowerShell console so you can check to see if it is correct:

Debug Script Output

/ JC

PowerShell Gather: Moving Away from MDT-Integrated Task Sequences in Microsoft Configuration Manager (MCM)

I’ve been a huge fan of MDT over the years and still use it to create my Windows Reference images to this day as it’s so straightforward for me to make tweaks to a WIM file if I need to.

Historically I have always recommended and implemented MDT-Integrated Task Sequences in Configuration Manager to take advantage of all the additional capabilities that MDT provides.

Recently though I have started to move to using a standard MCM OSD Task Sequence as they are so much more simple and require less maintenance.

The most useful thing from MDT Integration that I use day-to-day for OSD is the ‘MDT Gather’ step to collect information about the device and deployment at various points in the Task Sequence. This allows various aspects of a deployment to be controlled dynamically based on numerous pre-defined variables such as the classic IsDesktop/IsLaptop scenarios etc.

The downside to this is the steps require a MCM Package to be created and maintained plus it adds unnecessary time to the deployment when downloading the Toolkit Package.

It is possible to retain this useful capability by replacing the MDT Toolkit/Gather steps with a PowerShell script which can be added directly into the ‘Run PowerShell Script’ Task Sequence step and that’s why I’m here writing this post 🙂

I found a script which was created by Johan Schrewelius (with contributions from various others) which did the majority of what I wanted. His script can be accessed on the Technet PowerShell Gallery (Link).

By reworking this script and adding functionality that I specifically needed, I now have a lightweight and solid ‘Gather’ solution which can be easily added to any MCM Task Sequence.

v1.0 of the script collects the following information. The example is one of my lab devices so you can see what the info looks like. I expect this list to expand over time as new requirements crop up:

Architecture = X64
AssetTag = CZCXXXXXXX
BIOSReleaseDate = 12/25/2019 00:00:00 -BIOSVersion = N01 Ver. 02.45
BitlockerEncryptionMethod = AES_256
DefaultGateway = 192.168.1.1
IPAddress = 192.168.1.201
IsBDE = True
IsCoffeeLakeOrLater = False
IsDesktop = True
IsLaptop = False
IsOnBattery = False
IsOnEthernet = True
IsServer = False
IsTablet = False
IsVM = False
MacAddress = 48:0F:CF:46:09:F5
Make = HP
Memory = 49031.58203125
Model = HP EliteDesk 800 G2 SFF
OSBuildNumber = 17763.1158
OSCurrentBuild = 17763
OSCurrentVersion = 10.0.17763
ProcessorFamily = 6700
ProcessorManufacturer = GenuineIntel
ProcessorName = Intel(R) Core(TM) i7-6700 CPU @ 3.40GHz
ProcessorSpeed = 3408
Product = 8054
SerialNumber = CZCXXXXXXX
UUID = D41CCC6A-E086-13G5-9C43-BC0000EE0000
Vendor = HP
VMPlatform = N/A

I’ve been using it with customers for a while and am happy now that it’s robust/mature enough to be shared on GitHub for others to use as well if they want to:

https://github.com/jonconwayuk/PowerShell_Gather

It can be added into a Task Sequence as per the image below using the ‘Run PowerShell Script’ step with the Execution Policy set to ‘Bypass’. Each time it runs, it will add the collected variables into the running Task Sequence environment and can be used throughout the Task Sequence:

‘Run PowerShell Script’ Step with Pwsh-Gather.ps1 script added

Below is an example of how variables can be utilised – in this example the condition is to control some BitLocker tasks which I only wanted to run on physical devices which are also laptops:

Conditions using Gather Variables

It also creates a log file (Pwsh-Gather.log) in the standard Task Sequence logging directory defined as the built in variable “_SMSTSLogPath” which can be reviewed using cmtrace.exe.

For testing, the script can be run locally on a device by using the ‘-Debug’ parameter as per the example below from an ‘Administrator’ PowerShell prompt:

PS C:\Users\Administrator\Documents\PowerShell_Gather> .\Pwsh-Gather.ps1 -Debug

Feel free to start using the script and let me know if there are any improvements or additions that you’d like to see and I’ll try and accommodate them when time permits. Hopefully people find it useful!

/ JC

Command Line to Test Windows 10 Upgrade Compatibility

To test to see if a device has any blockers for updating to a newer release of Windows 10 without actually attempting an upgrade, you can run the following commands by mounting an ISO for the target Windows 10 version via File Explorer and then running the following commands from an Administrator Command Prompt:

start /wait SETUP.EXE /Auto Upgrade /Quiet /NoReboot /DynamicUpdate Enable /Compat ScanOnly
echo %errorlevel%

This will run the Windows 10 setup engine (downloading any updated setup components – this can be disabled by replacing ‘Enable’ with ‘Disable’ in the command) and will produce a return code to say if there are any issues which would block the upgrade.

More details on the return codes can be found at the following link:

https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/windows-setup-command-line-options#compat

In the example below the code returned is “-1047526896”.

Covert that from Decimal to Hex (use Windows Calculator in ‘Programmer’ mode) and the result is “0xC1900208” which equates to not finding any compatibility issues:

This means my device is good to go for updating to the new release of Windows 10 🙂

/ JC

MCM Software Updates | Load Balancing with Even and Odd Collections

Recently a customer asked me to help BAU Support find a way to reduce the impact of deploying Windows 10 Software Updates (LCU) over their Wireless LAN in a particular building with a large number of clients. The last round of patching had pretty much destroyed their WiFi during the deployment. I decided the first thing to do was to load balance the deployments by devising a way of splitting the clients roughly into two groups.

I created MCM Collections based on Odd and Even numbers i.e. if a device ends in an odd number become a member of one collection and if it ends in an even number it becomes a member of another collection.

To accomplish this I used square brackets to define a range of characters as part of the WQL query for the Collections: One for devices ending in an odd number and one for devices ending in an event number.

This query results in devices ending in odd numbers:

/* Odd Numbers */
select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,
SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,
SMS_R_SYSTEM.ResourceDomainORWorkgroup,
SMS_R_SYSTEM.Client from SMS_R_System
where SMS_R_System.Name like "%[13579]"

And another collection returning only devices ending in even numbers:

/* Even Numbers */
select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,
SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,
SMS_R_SYSTEM.ResourceDomainORWorkgroup,
SMS_R_SYSTEM.Client from SMS_R_System
where SMS_R_System.Name like "%[02468]"

The next issue was that not all client device names ended with a number – some ended with a letter. Fantastic…

To cater for this I divided the alphabet by odd and even letters and added the odd letters to odd query and the even letters to the even query.

The resultant query returned only devices ending in odd numbers or letters:

/* Odd Numbers and Odd Letters */
select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,
SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,
SMS_R_SYSTEM.ResourceDomainORWorkgroup,
SMS_R_SYSTEM.Client from SMS_R_System
where SMS_R_System.Name like "%[13579acegikmoqsuwy]"

And conversely, the following query returned only devices which ended with an even number or letter:

/* Even Numbers and Even Letters */
select SMS_R_SYSTEM.ResourceID,SMS_R_SYSTEM.ResourceType,
SMS_R_SYSTEM.Name,SMS_R_SYSTEM.SMSUniqueIdentifier,
SMS_R_SYSTEM.ResourceDomainORWorkgroup,
SMS_R_SYSTEM.Client from SMS_R_System
where SMS_R_System.Name like "%[02468bdfhjlnprtvxz]"

This isn’t the perfect solution (there won’t be an exact 50% split of devices) but it was good enough for my customer as a tactical solution.

Used in conjunction with MCM Phased Deployments etc. should see a large reduction in the load on the WLAN and allow users to work normally during patching in the future.

/ JC

Testing ConfigMgr packages & applications before adding them into ConfigMgr using PsExec.exe

To test that an application is 99.9% sure to work when deployed via ConfigMgr it is important to initially test installers by running them in the SYSTEM context on a test machine.

Applications and Packages run under the SYSTEM context when deployed via ConfigMgr and can behave differently when compared to running them as a Local Administrator account or a standard user.

Testing them with PsExec (from the Microsoft PSTools suite – part of Microsoft Sysinternals) means that you can be pretty sure that they will work once you’ve added them into ConfigMgr but saves you time before you create the package/application, distribute the content and test in a deployment etc. only to find it fails and have to start over again.

Running the command below from an ‘Administrator’ command prompt will mimic the ConfigMgr deployment by running any commands issued in the resulting Command Prompt as the LOCAL SYSTEM account:

psexec -s -i cmd.exe

You’ll know that the command prompt is running as SYSTEM by running ‘whoami’ as per the screenshot below:

Once tested in this way, you can go ahead and add your package or application into ConfigMgr and be confident that any deployments to clients via Software Distribution or Task Sequence will be successful.

/ JC

Using MCM USB Bootable Media in UEFI/GPT/BitLocker Scenarios When Local and Remote Boot Images Are Different

A customer recently had a requirement for rebuilds to be done in remote sites via USB flash drives configured as MCM Bootable Media due to a lack of local MCM Distribution Points and PXE Boot capability.

Using devices in UEFI mode with BitLocker enabled makes this tricky when the Boot Image associated with the Task Sequence becomes out of sync with the Boot Image on the USB media. If the boot images don’t match then MCM attempts to pre-stage onto the local disk and fails as the OSDisk is unavailable due to it being encrypted with BitLocker (the drive appears as “RAW” and cannot be accessed) and none of the other partitions are large enough or available.

I worked around this by creating a PowerShell PreStart script and adding it to the Boot Media ISO image. The script runs before the Task Sequence begins. It creates a Diskpart configuration text file on the fly in the ‘X:\Windows\Temp’ folder of the running WinPE. After creating the Diskpart configuration file, it then runs Diskpart referencing the configuration file in order to create suitably-sized/lettered partitions to successfully boot from using UEFI and that are also accessible for the Task Sequence to download and pre-stage the latest Boot Image if it’s required (i.e. if it’s different to the boot image on the USB).

Problem solved!

The command for the PreStart script that I used was:

cmd /C PowerShell.exe -ExecutionPolicy ByPass -File PreStart.ps1

And the PowerShell code contained with PreStart.ps1 is shown below:

<#
.DESCRIPTION
    Configures GPT disk layout using DiskPart.exe to avoid Boot Image mismatching when using MCM Bootable Media
.EXAMPLE
    PowerShell.exe -ExecutionPolicy ByPass -File .ps1
.NOTES
    Author:         Jonathan Conway
    Modified:       06/04/2019
    Version:        1.0
#>

# Display warning and request confirmation from engineer
$Shell = New-Object -ComObject "WScript.Shell"
$Button = $Shell.Popup("Proceeding will wipe all local data from all local drives. Hold Power Button until device powers off to cancel. Click OK to proceed.", 0, "WARNING", 0)

# Set variables
$DiskPartFile = "X:\Windows\Temp\DiskpartConfig.txt"

if (Get-Volume | Where-Object {$_.DriveLetter -eq 'C' -and $_.DriveType -eq 'Removable'}) {
Get-Partition -DriveLetter 'C' | Set-Partition -NewDriveLetter 'U'
}

# Create contents of DiskPart configuration file
Write-Output "SELECT DISK 0" | Out-File -Encoding utf8 -FilePath "$DiskpartFile"
Write-Output "CLEAN" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "CONVERT GPT" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "CREATE PARTITION EFI SIZE=200" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "ASSIGN LETTER=S" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "FORMAT QUICK FS=FAT32" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "CREATE PARTITION MSR SIZE=128" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "CREATE PARTITION PRIMARY" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "ASSIGN LETTER=C" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "FORMAT QUICK FS=NTFS" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append
Write-Output "EXIT" | Out-File -Encoding utf8 -FilePath "$DiskpartFile" -Append

# Run DiskPart
Start-Process -FilePath "diskpart.exe" -ArgumentList "/s $DiskPartFile" -Wait

In my environment this formats the disks in a way which allows my Task Sequence to progress whatever state the UEFI partitions are in (i.e. BitLocker enabled or not).

A pop up warning is shown on screen stating:

Proceeding will wipe all local data from all local drives. Hold Power Button until device powers off to cancel. Click OK to proceed“.

Clicking OK continues ahead and starts the Diskpart process before progressing to the Task Sequence selection screen 🙂

/ JC

MCM Collection Query for Duplicate MAC Addresses

The WQL query below is can be used in MCM to display all duplicate MAC addresses which will likely cause issues with OSD:


SELECT R.ResourceID, R.ResourceType, R.Name, R.SMSUniqueIdentifier, R.ResourceDomainORWorkgroup, R.Client
FROM SMS_R_System AS r full JOIN SMS_R_System AS s1 ON s1.ResourceId = r.ResourceId full JOIN SMS_R_System AS s2 ON s2.Name = s1.Name
WHERE s1.Name = s2.Name AND s1.ResourceId != s2.ResourceId
ORDER BY r.MACAddresses

The WQL query below is a prompted query to show entries for a specified MAC Address to see if it is being used my multiple devices in the MCM database:


SELECT SMS_R_System.Name, SMS_R_System.MACAddresses
FROM  SMS_R_System
WHERE SMS_R_System.MACAddresses = ##PRM:SMS_R_System.MACAddresses##

/ JC

WMI/WQL “LIKE” Query Wildcards With Examples

Quick post today.

Standard Windows/DOS wildcards don’t work in WMI “LIKE” queries as they use WQL language instead:

Multiple Characters = "%" (Percentage)
Single Character    = "_" (Underscore)

For reference, the corresponding Windows wildcards are:

Multiple Characters = "*" (Asterisk) 
Single Character    = "?" (Question Mark)

Note: when using wildcards in ConfigMgr Task Sequences pay attention to what is being done. If you’re querying a value from WMI then you should use “%” and “_” as wildcards:

SELECT * FROM Win32_ComputerSystem WHERE Name LIKE 'PC%'
SELECT * FROM Win32_ComputerSystem WHERE Name LIKE 'PC_____'

If you’re querying a Task Sequence variable then the Windows wildcards (“*” and “?” should be used:

OSDComputerName LIKE "PC*"
OSDComputerName LIKE "PC?????"

/ JC

Use RoboCopy in ‘Run PowerShell Script’ MCM Task Sequence Steps

I like to use PowerShell for all my scripting these days (all VB and batch files have now been rewritten in PoSh) and I also like to use RoboCopy for any file copies that I need to do such as in an OSD Task Sequence.

The pain in the arse with RoboCopy is the return/exit codes it uses which cause issues when used in PowerShell scripts.

The return codes used by PowerShell are:

0 No files were copied. No failure was encountered. No files were mismatched. The files already exist in the destination directory; therefore, the copy operation was skipped.

1 All files were copied successfully.

2 There are some additional files in the destination directory that are not present in the source directory. No files were copied.

3 Some files were copied. Additional files were present. No failure was encountered.

5 Some files were copied. Some files were mismatched. No failure was encountered.

6 Additional files and mismatched files exist. No files were copied and no failures were encountered. This means that the files already exist in the destination directory.

7 Files were copied, a file mismatch was present, and additional files were present.

8 Several files did not copy.

Because PowerShell expects an exit code of ‘0’ for success, if RoboCopy completes with an exit code of ‘1’ (i.e. All files were copied successfully) then it throws an exit code other than ‘0’.

In an OSD Task Sequence this is picked up as an error and will therefore cause the Task Sequence to fail. Bollocks.

This can easily be prevented using a wee bit of code at the end of the script used to run the RoboCopy.

In the example below I am copying a single ISO image using a PowerShell script in a Task Sequence (using a ‘Run PowerShell Script’ task). The resulting PowerShell exit code will equal ‘1’ as “all files will be copied successfully”.

<#
.SYNOPSIS
    Copies VM Bootable ISO
.DESCRIPTION
	Copies the VM Bootable ISO from the package folder to C:\Media
.EXAMPLE
	PowerShell.exe -ExecutionPolicy ByPass -File <ScriptName>.ps1
.NOTES
	Author:		Jonathan Conway
	Version:	1.0
	Created:	29/11/2017
#>

# Set variable for newest ISO in package folder (in case there are more than one then the most recent will be chosen)
$ISO = Get-ChildItem '.\*.iso' | Sort-Object 'LastWriteTime' | Select-Object -last '1' | Select-Object -ExpandProperty 'Name'

# Run ROBOCOPY to copy the Bootable ISO image to "C:\Media"
& ROBOCOPY ".\" "C:\Media" $ISO

# Robocopy for a single file returns a exit code of "1" (i.e. All files were copied successfully) which causes a Task Sequence error - this "if" statement changes exit code to a "0"
if ($LASTEXITCODE -eq '1') {
    EXIT 0
}

To prevent a Task Sequence failure I can intercept the ‘$LASTEXITCODE’ variable and exit the script with a ‘0’ using an ‘if’ statement.

This will then be picked up by the running Task Sequence and consumed as a ‘success’ which will subsequently allow the Task Sequence to progress without error.

Marvellous!

/ JC

Check TPM Status from the Command Line (Enabled | Activated | Owned)

Quick and simple way to see if the TPM on a computer is Enabled, Activated and Owned – all of which are required before using them for BitLocker:

wmic /namespace:\\root\cimv2\security\microsofttpm path win32_tpm get IsEnabled_InitialValue
wmic /namespace:\\root\cimv2\security\microsofttpm path win32_tpm get IsActivated_InitialValue
wmic /namespace:\\root\cimv2\security\microsofttpm path win32_tpm get IsOwned_InitialValue

As long as they all return as “True” you’re good to go.

/ JC

Confirm Service Account Credentials The Easy Way with PowerShell (e.g. MCM Network Access Account)

Sometimes you will have an AD Service Account configured and you might not be sure what the password is – a good example of this that sometimes catches me out is the MCM Network Access Account.

To safely test the account username and password we can use PowerShell with the following simple and safe command:

Start-Process -FilePath winver.exe /c -Credential (Get-Credential)

This will attempt to run “winver.exe” and a prompt will appear asking for credentials:

AccountCredsPrompt

If the account credentials that you enter are not correct you will see the following error:

AccountCredsFail

But if the credentials provided are correct then “winver.exe” will open as expected and no error message will be produced:

AccountCredsSuccess

Simple but effective 🙂

/ JC

Add CMTrace.exe to Computers Being Deployed via Task Sequence

To make sure you have CMTrace.exe available for use on machines that are deployed via ConfigMgr Task Sequences you can add a “Run Command Line” task immediately after the “Apply Operating System Image” that copies the executable from the boot image being used to deploy the OS (CMtrace.exe is included by default ConfigMgr WinPE boot images – WinPE is mapped as X:\ during OSD) and results in it being available once OSD completes:

 cmd /c xcopy X:\SMS\BIN\x64\CMTrace.exe %OSDTargetSystemDrive%\Windows\System32\ /E /H /C /I /Q /Y

This command line will need to be amended in the unlikely scenario (it’s 2017 after all) that you’re deploying a 32-bit Operating System to change the xcopy target path accordingly.

/ JC

Note: This was originally documented on TechNet yonks ago: Link

Use Task Scheduler to Schedule Server Reboot Out of Hours

You may from time to time have a requirement to reboot a server out of hours after implementing a change that requires a restart.

Rather than logging in at Silly O’Clock at night you can use the Windows Task Scheduler to set up a Task to have an unattended reboot occur out of hours.

Open “Task Scheduler” from the Start menu and select “Create New Task“.

Complete the “General” tab by adding the following values for “Name“, “Account” and “Configure for“:

1

By using the “SYSTEM” account we can be sure that the required permissions to reboot/shutdown the computer are present.

On the “Triggers” tab click on “New” and configure a time suitable for your environment. In my example I have chosen a one time event at “22:00:00” as this is deemed out of hours:

2

On the “Actions” tab click on “New” and configure the task as per below:

3

In the “Program/script” field add the word “shutdown“.

In the “Add arguments (optional)” field make sure the following is added:

/r /t 0 /c "Planned Server Reboot via Task Scheduler Task" /f

Click “OK” twice and you’re done.

The command that you have just configured passes the following instructions to the “Shutdown.exe” executable:

  • /r = Reboot
  • /t 0 = waits 0 seconds before restarting
  • /c = comment to be added into the System log in Event Viewer
  • /f = forces the reboot even if users are logged on, programs are open, files are locked etc.

/ JC

MBAM Client Deployment PowerShell Error 0x803d0006 – MCM OSD in Disconnected/Offline Environments

Whilst deploying MBAM as part of a Windows 10 OSD Task Sequence in MCM CB the “MbamClientDeployment.ps1” task was failing I was getting the error message shown below in the client “smsts.log” file:

 HRESULT: 0x803d0006 

I logged into one of the failed clients, opened Internet Explorer and attempted to connect to the URL for the MBAM Core Service manually – this took 42 feckin seconds! Obviously this is far too slow for the connection via the PowerShell script to be successful so the next question was why was this taking so long…

After a period of frustration, emotion and profuse swearing I ended up digging a bit deeper to see what was happening under the hood when trying to connect to the URL of the MBAM Recovery and Hardware Service (i.e. https://mbam01.testlab.local/MBAMRecoveryAndHardwareService/CoreService.svc).

To do this I installed a tool called Fiddler (sounds dodgy but it’s a lightweight freeware tool for monitoring web connections – far simpler to implement and use when compared to WireShark or Microsoft Message Analyzer) on a client and once again accessed the URL via Internet Explorer to see what connection attempts were being made by the client when attempting to access the MBAM service.

Turns out that calls were being made to Windows Update URLs and various “crl.microsoft.com” URLs. Basically the clients were trying to download the latest Root level Certificate Revocation Lists/Certificates from Microsoft’s servers over the internet. Because the clients didn’t have access to the internet due to firewalls blocking, the clients eventually timed out trying to connect to Microsoft which subsequently took the response time for the MBAM service connection over the allowed limit. This resulted in a timeout occurring when MbamClientDeployment.ps1 ran.

If the clients were able to access the internet (and able to connect to the URLs they were reaching out to) then there wouldn’t have been a problem and the script would have completed without any problems.

To make absolutely sure I tested this by unchecking the Internet Explorer option “Internet Options | Advanced | Check for server certificate revocation” on the client – rebooted the client and retried: I was able to hit the MBAM web service immediately with zero delay. Ticked the box again, rebooted, retried and the response was back up to 42 seconds (i.e. buggered again).

I don’t think it’s possible (or desirable) to disable certificate revocation checking for all certificates so another solution had to be found to this problem.

In the end the solution was to disable the automatic updating of the Root CA certificates/CRLs using the following registry key:

 HKLM\Software\Policies\Microsoft\SystemCertificates\AuthRoot "DisableRootAutoUpdate" | Dword = 1 

I accomplished this via a PowerShell script running as part of the Task Sequence.

  1. PowerShell script to set the “DisableRootAutoUpdate” registry key
  2. Reboot
  3. MbamClientDeployment.ps1

This disables the automatic update of the Root CA’s resulting in there being no delay in the MBAM service connection and consequently the MBAM PowerShell script completes successfully.

The PowerShell script to make the registry changes contains the following lines of code:

New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\SystemCertificates" -Name "AuthRoot" -Force
New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\SystemCertificates\AuthRoot" -Name "DisableRootAutoUpdate" -PropertyType "Dword" -Value "1"

Once the “MbamClientDeployment.ps1” script has completed Root certificate auto update needs to be re-enabled (so SSL websites work as expected) by deleting the registry key that we created with the above PowerShell.

This can be done in your Task Sequence using a “Run Command Line” step called something like “Enable Certificate Checking” with the following command:

 reg.exe DELETE "HKLM\SOFTWARE\Policies\Microsoft\SystemCertificates\AuthRoot" /f 

Hopefully this will save someone else the hours of misery that I endured to get this to work…

/ JC

Open Local Computer Certificates MMC From Single Command

A quick and simple post today.

Since Windows 8/Windows Server 2012 you can directly open the Local Computer Certificates MMC console by running the following command:

 certlm 

This will launch “certlm.msc” showing the information that you want.

You can still open the default store using “certmgr.msc” as you could on previous versions of Windows.

/ JC

MCM Windows 10 Upgrade Task Sequence: BitLocker PIN Protector Issues on Laptops

I’ve recently been looking at using MCM Windows Upgrade Task Sequences to migrate from Windows 10 1511 to Windows 10 1607 for a customer.

On desktop devices this process ran through as expected and didn’t cause any real problems (i.e. nothing that I wasn’t expecting or that couldn’t be easily resolved).

Laptops with PIN protectors enabled did present a problem however…

It seems that while the upgrade process disables BitLocker automatically, PIN protectors become active again too early (i.e. before the end of the Task Sequence) meaning that end users would need to enter their PIN at least twice (possibly more depending on any additional Restart Computer actions in the Task Sequence) before the Task Sequence completes successfully.

Obviously this might be a pain in the arse for end users so it would be nice to find a way to avoid them having to enter their PIN repeatedly.

My solution for this is shown in the image and text below (steps highlighted in red are the ones required to resolve this specific issue):

upgradets

The commands for each of the highlighted tasks are:

# Disable BitLocker Protectors Indefinitely
cmd.exe /c &quot;manage-bde -protectors -disable C: -RC 0&quot;

# Re-enable BitLocker Protectors
cmd.exe /c &quot;manage-bde -protectors -enable C:&quot;

# Disable BitLocker Protectors for Single Reboot
cmd.exe /c &quot;manage-bde -protectors -disable C:&quot;

The first highlighted command disables BitLocker protectors indefinitely (Reboot Count = “0” turns off protectors until you issue an “-enable” command) which means you can reboot the device as many times as you like without BitLocker rearing it’s ugly head at all.

The second highlighted command re-enables all BitLocker protectors which therefore reverses the “Disable BitLocker Protectors Indefinitely” command.

The final highlighted command (“Disable BitLocker Protectors for Single Reboot”) disables BitLocker protectors on the C: drive again but using the default Reboot Count (when “-RC” isn’t specified the default value is used which is “1”) which only disables the protectors for a single reboot.

This means that after the final “Restart Computer” task the OS has been upgraded, all custom actions in the TS will have completed and all protectors for the C: drive are all switched back on.

Bloody marvellous.

/ JC

“Finish Installing Device Software” in Windows 10 Action Center

If you get a message in the Windows 10 Action Center saying “Finish installing device software” with a red/white cross and a UAC symbol on then it’s likely that a driver is missing, a driver needs some software installed or that Windows needs your permission to resolve one of these actions (hence the UAC symbol).

In can be an absolute pain to figure out which device/driver is causing this issue but one way to track it down is to click to allow the install to complete and then look in the file:

C:\Windows\Inf\setupapi.dev.log”

Look for Finish-Install actions (typically they will be the latest entries in the log file if you’ve just clicked to complete the action immediately before looking) and that should lead to you to identifying the troublesome device. Back of the net.

/ JC

Increase the Speed of PXE Boot/TFTP When Using ConfigMgr Distribution Point

If you’re looking to improve the performance (quite significantly in my experience) of Trivial File Transfer Protocol/TFTP (in other words to improve the download speed of your ConfigMgr boot images to your clients from the DP) you can add some registry keys on the server hosting the PXE-enabled Distribution Point to achieve this.

The two keys required (and the values I suggest using) are:

New-ItemProperty -Path &quot;HKLM:\SOFTWARE\Microsoft\SMS\DP&quot; -Name RamDiskTFTPBlockSize -PropertyType DWord -Value &quot;16384&quot;
New-ItemProperty -Path &quot;HKLM:\SOFTWARE\Microsoft\SMS\DP&quot; -Name RamDiskTFTPWindowSize -PropertyType DWord -Value &quot;8&quot;

Once the keys have been added, restart the “Windows Deployment Service Server” service and that’s you finished. 🙂

One caveat with this change is that it can also adversely affect performance and in some cases even break PXE altogether (as I found out with my Hyper-V 2012 R2 VM’s – VM’s stopped working completely while physical hardware zipped along like shit off a stick) so some tweaking of the values may be required depending on your environment. Basically one size does not fit all with this change.

Can’t find any proper information on ConfigMgr but it looks like this is a supported change from looking at this TechNet post: link

UPDATE 30/09/2016 – some much better detail and testing results for this TFTP optimisation provided by Jörgen Nilsson on the excellent ccmexec.com website – link

/ JC

Further simplifying servicing models for Windows 7 and Windows 8.1

Microsoft look to be moving to a cumulative approach to updates with Windows 7 and 8.1 which seems to be similar to what they have already done for Windows 10.

Single Cumulative Updates instead of multiple individual patches moving forward. Better late than never I guess… 😉

See the full blog post on TechNet:

https://blogs.technet.microsoft.com/windowsitpro/2016/08/15/further-simplifying-servicing-model-for-windows-7-and-windows-8-1/

/ JC

Windows 10 Language Packs are Release Specific

Just a heads up – after wasting the best part of a day trying to figure out what was wrong it turns out that Language Packs for Windows 10 are release specific and only seem to work with the corresponding release of Windows.

What this means is that Language Packs for Windows 10 1511 won’t install offline via MDT when creating a reference image using 1607 Windows 10 media.

1511 Language Packs only work with 1511 media and NOT 1607 media.

Bugger.

Need to wait for 1607 Language Packs to be released then eh… 😉

/ JC

Run PowerShell Scripts as Windows Scheduled Tasks

It can be useful to have a PowerShell script which runs as a Windows Scheduled task to perform otherwise manual tasks. Being a lazy bugger I like to automate as many boring, repetitive tasks as I can so PowerShell and Scheduled Tasks are my friends…

A good example of this would be if you needed to run a cleanup of WSUS to remove declined, superseded, expired updates etc.

The script I want to run looks like the following:

<#
.DESCRIPTION
Cleans up WSUS on local server
.EXAMPLE
PowerShell.exe -ExecutionPolicy ByPass -File WSUSCleanup.ps1
.NOTES
Author:	Jonathan Conway
Created:	21/07/2016
Version:	1.1
#>

# Set WSUS port number (standard is 8530 on Windows Server 2012 R2 but can be customised)
$WSUSPortNumber = 8530

# Connect to local server using PowerShell
Get-WsusServer -Name $env:computername -PortNumber $WSUSPortNumber

# Perform required cleanup commands
Get-WsusServer | Invoke-WsusServerCleanup -CleanupObsoleteUpdates -CleanupUnneededContentFiles -CompressUpdates -DeclineExpiredUpdates -DeclineSupersededUpdates | `
Out-File -FilePath C:\Tools\Scripts\wsuscleanup.log

In order to run this as a Scheduled Task in Windows I’d need to run it as SYSTEM (NT AUTHORITY\SYSTEM) – change the “Configure for:” section at the bottom to match the OS you’re using as well, for compatibility purposes.

Configure a Trigger – once a week should be more than enough for this particular task.

The action should be configured to “Start a Program” which would be as per the command line example below (example assumes you have a script called WSUSCleanup.ps1 located in a folder called “C:\Tools\Scripts”):

PowerShell.exe -ExecutionPolicy ByPass -File C:\Tools\Scripts\WSUSCleanup.ps1

It should end up looking a bit like this:

ScheduledTask

And that’s about it – should run as per the Trigger Schedule.

/ JC

How To Deploy & Run PowerShell Scripts via ConfigMgr

A customer recently had a requirement to deploy a PowerShell script to configure a setting for App-V 5.0.

Normally I’d do this with a Batch file called “Configure.cmd” containing the code displayed below. This works for the majority of tasks:

@echo off
PUSHD %~dp0

PowerShell.exe -ExecutionPolicy Bypass -File ".\PowerShellScriptFileName.ps1"

POPD

As usual, I tested the deployment before adding into ConfigMgr by using psexec running under the System context (i.e. the same context that ConfigMgr deployments run under) with the command below. One the command prompt is open you can run the required installer as the System context:

psexec /s cmd.exe

This completed successfully and made the configurations that I’d wanted. Bosh. Bloody awesome I thought…

On this occasion however I needed to reconfigure a 32-bit application (App-V 5.0) on a 64-bit operating system (Windows 7 x64).

This doesn’t play well when deployed via ConfigMgr is seems and ends up running using the SysWOW64 redirection – the result in my case was that registry changes were made by the PowerShell cmdlet in the Wow6432Node area of the registry.

In order to get around this I discovered a new concept to me called “Sysnative” which is a virtual directory and special alias that can be used by applications/scripts to access the 64-bit “System32” folder – which is exactly what I wanted to happen in this instance for my script to produce the required result.

Therefore to use this you need to change the location that PowerShell is called from to %WinDir%\Sysnative (you can’t see it in Windows Explorer btw – but trust me, this little bugger does indeed exist):

@echo off
PUSHD %~dp0

%WinDir%\Sysnative\windowsPowershell\v1.0\Powershell.exe -ExecutionPolicy Bypass -File ".\PowerShellScriptFileName.ps1"

POPD

Once changed to reference the correct PowerShell.exe my script punted out the desired results. Magic 🙂

/ JC

RoboCopy a Single File to See Accurate Progress/Time Estimates

RoboCopy is used as a tool to copy folders but it can also be used to copy single files if required.

This is handy if you’re copying a large file (such as an ISO or WIM image) and want some sort of feedback from a command prompt on how it’s progressing (not something that happens with xcopy or copy).

Say you want to copy a file called BigFile.iso from the root of the C:\ Drive to the root of the E:\ drive you could use the following command

robocopy C:\ E:\ BigFile.iso

/ JC

Creating Custom WinPE 3.1 Boot Image (For Deploying Windows XP from SCCM 2012 R2) Automated via Batch File

Recently a customer wanted the ability to be able to rebuild Windows XP machines (!) via SCCM 2012 R2 by just adding machines into a rebuild collection.

This doesn’t work out of the box with the version of WinPE that ships with SCCM so to get it to work you need to create a custom Boot image based on WinPE 3.1, add it into ConfigMgr and associate it with the Windows XP Task Sequence – this allows WinPE to pre-stage onto the local disk and for the machine to successfully reboot into it.

The following code can be added into a Batch File and executed as an Administrator to automate the creation of the Boot Image and add the required components.

@echo off

echo:
echo # REMOVE DIRECTORY IF IT EXISTS
echo:

RD C:\TEMP\WinPE\LegacyWinPEx86 /S /Q

echo:
echo # CREATE X86 WINPE FOLDER STRUCTURE
echo:

CALL "C:\Program Files\Windows AIK\Tools\PETools\copype.cmd" x86 C:\TEMP\WinPE\LegacyWinPEx86

echo:
echo # COPY WIM FILE TO ISO\SOURCES DIRECTORY AND RENAME AS BOOT.WIM
echo:

COPY C:\TEMP\WinPE\LegacyWinPEx86\winpe.wim C:\TEMP\WinPE\LegacyWinPEx86\ISO\sources\boot.wim

echo:
echo # MOUNT THE BOOT.WIM FILE IN THE MOUNT DIRECTORY
echo:
Dism /Mount-Wim /WimFile:C:\TEMP\WinPE\LegacyWinPEx86\ISO\sources\boot.wim /index:1 /MountDir:C:\TEMP\WinPE\LegacyWinPEx86\mount

echo:
echo # ADD OPTIONAL COMPONENTS TO WINPE IMAGE
echo:

Dism /image:C:\TEMP\WinPE\LegacyWinPEx86\mount /Add-Package /PackagePath:"C:\Program Files\Windows AIK\Tools\PETools\x86\WinPE_FPs\winpe-wmi.cab"
Dism /image:C:\TEMP\WinPE\LegacyWinPEx86\mount /Add-Package /PackagePath:"C:\Program Files\Windows AIK\Tools\PETools\x86\WinPE_FPs\winpe-scripting.cab"
Dism /image:C:\TEMP\WinPE\LegacyWinPEx86\mount /Add-Package /PackagePath:"C:\Program Files\Windows AIK\Tools\PETools\x86\WinPE_FPs\winpe-wds-tools.cab"
Dism /image:C:\TEMP\WinPE\LegacyWinPEx86\mount /Add-Package /PackagePath:"C:\Program Files\Windows AIK\Tools\PETools\x86\WinPE_FPs\winpe-hta.cab"
Dism /image:C:\TEMP\WinPE\LegacyWinPEx86\mount /Add-Package /PackagePath:"C:\Program Files\Windows AIK\Tools\PETools\x86\WinPE_FPs\winpe-mdac.cab"

echo:
echo # SET SCRATCH SPACE TO 128MB
echo:

Dism /Set-ScratchSpace:128 /Image:C:\TEMP\WinPE\LegacyWinPEx86\mount

echo:
echo # ADD ANY REQUIRED DRIVERS TO THE IMAGE
echo:

Dism /Image:C:\TEMP\WinPE\LegacyWinPEx86\mount /Add-Driver /Driver:C:\TEMP\WinPE\Drivers /Recurse

echo:
echo # UNMOUNT IMAGE AND COMMIT CHANGES
echo:

Dism /Unmount-Wim /MountDir:C:\TEMP\WinPE\LegacyWinPEx86\mount /Commit

/ JC

WinPE Versions Linked to Full OS Versions

WinPE 1.0 [Windows XP] [5.1.2600.x] [First version of WinPE]
WinPE 1.1 [Windows XP SP1] [5.1.2600.x]
WinPE 1.2 [Windows Server 2003] [5.2.3790.x]
WinPE 1.5 [Windows XP SP2] [5.1.2600.x] [Windows PE 2004]
WinPE 1.6 [Windows Server 2003 SP1] [5.2.3790.x] [Windows PE 2005]
WinPE 2.0 [Windows Vista] [6.0.6000.x]
WinPE 2.1 [Windows Server 2008] [6.0.6001.x]
WinPE 2.2 [Windows Server 2008 SP2] [6.0.6002.x]
WinPE 3.0 [Windows 7] [6.1.7600.x] [Windows AIK 2.0]
WinPE 3.1 [Windows 7 SP1] [6.1.7601.x] [Windows AIK Supplement for Windows 7 SP1]
WinPE 4.0 [Windows 8] [6.2.9200.x] [Windows ADK 8.0]
WinPE 5.0 [Windows 8.1] [6.3.9300.x] [Windows ADK 8.1]
WinPE 5.1 [Windows 8.1 Update 1] [6.3.9600.x] [Windows ADK 8.1 Update]
WinPE 10.0.10586 [Windows 10] [1511 – 10586.104] [Windows ADK 10.1.10586.0]

/ JC

Command Line to Display UUID or MAC Address of a Computer

In the event that you’re using UUID or MAC Address to uniquely identify computers in a database (the MDT database or ConfigMgr for example) you might want a quick and easy way of getting these values from the Command Prompt on the target computer…

UUID

wmic csproduct get "UUID" &gt; C:\UUID.txt

MAC Address

wmic nic get "MACAddress" &gt; C:\MAC.txt

or

ipconfig /all | find /i "phy" &gt; C:\MAC.txt

/ JC

Run Programs as System Account

An easy way to run programs (like regedit.exe) as SYSTEM is by using PSExec which is part of the Microsoft Sysinternals Suite.

psexec -i -d -s ProgramName.exe

For example to use PSExec to run regedit on the local machine in the SYSTEM context (and be able to interact with it on the desktop) run the command:

psexec -i -d -s regedit.exe

/ JC

MCM ConfigMgr OSD Task Sequence | Client Log File Locations

Below are the locations of the ConfigMgr Client log files as a Task Sequence progresses from WinPE to the full version of Windows.

Windows PE before HDD format:

X:\Windows\Temp\smstslog\smsts.log

Windows PE after HDD format:

X:\smstslog\smsts.log and copied to C:\_SMSTaskSequence\Logs\Smstslog\smsts.log

Full Windows before ConfigMgr Client:

C:\_SMSTaskSequence\Logs\Smstslog\smsts.log

Full Windows after ConfigMgr Client is installed:

C:\Windows\CCM\Logs\Smstslog\smsts.log

Full Windows after Task Sequence finishes:

C:\Windows\CCM\Logs\smsts.log

/ JC

WMI Query and Scripting Tools

It’s often useful to be able to view WMI information and subsequently create queries to use in MDT and ConfigMgr.

The best tool I’ve found for doing this is WMI Explorer which is a free tool available on Codeplex:

WMI Explorer: https://wmie.codeplex.com/

Alternatives that I have also used are:

Goverland Free WMI Explorer: http://www.goverlan.com/wmix.php
Windows PowerShell Scriptomatic: https://www.microsoft.com/en-us/download/details.aspx?id=24121
Microsoft Scriptomatic 2.0: http://www.microsoft.com/en-gb/download/details.aspx?id=12028

/ JC

Log Into Windows Locally Without Entering The Computername Prefix Every Time

If (like me) you need to log in locally to Windows machines quite a lot, it can be an annoyance to have to type in the Computername prefix every time you need to do so e.g “CLIENT1\Administrator”.

Luckily Microsoft have given us a shortcut way of doing this so we can save time. Just substitute the Computername for a “.\”

So “CLIENT1\Administrator” would become “.\Administrator”

Simples.

/ JC

How Many Activations Do I Have Left On My Licence Keys?

To find out this information you’d normally have to go through the pain in the arse that is “telephoning Microsoft”.

Far easier though is to import the keys into the Volume Activation Management Tool (VAMT) and use it to check online with Microsoft over the Interweb.

VAMT is part of the Windows Assessment and Deployment Kit (Windows ADK) and can be downloaded for nothing from:

http://www.microsoft.com/en-gb/download/details.aspx?id=39982

Simply import the relevant keys and use the “Refresh product key data online” to retrieve the number of activations left.

/ JC

When I Close The Lid?

To ensure that laptops don’t enter a sleep state when you close the lid you can run a script at deployment time that utilises the powercfg.exe command line utility.

To set the action for “When I close the lid” to ‘Do nothing’ use the following commands in a PowerShell script as part of your MDT Task Sequence:

powercfg.exe -SETACVALUEINDEX "381b4222-f694-41f0-9685-ff5bb260df2e" "4f971e89-eebd-4455-a8de-9e59040e7347" "5ca83367-6e45-459f-a27b-476b1d01c936" 000
powercfg.exe -SETDCVALUEINDEX "381b4222-f694-41f0-9685-ff5bb260df2e" "4f971e89-eebd-4455-a8de-9e59040e7347" "5ca83367-6e45-459f-a27b-476b1d01c936" 000

/ JC

Can’t Connect to C$ After Security Hardening (Windows Client & Server)

Simple registry fix will sort that:

  1. Click Start, click Run, type regedit, and then press ENTER.
  2. Locate and then click the following registry subkey:
  3. HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System
  4. If the LocalAccountTokenFilterPolicy registry entry does not exist, follow these steps:
  5. On the Edit menu, point to New, and then click DWORD Value.
  6. Type LocalAccountTokenFilterPolicy, and then press ENTER.
  7. Right-click LocalAccountTokenFilterPolicy, and then click Modify.
  8. In the Value data box, type 1, and then click OK.
  9. Exit Registry Editor.

http://support.microsoft.com/kb/951016

/ JC