Author Archives: J. Greg MacKinnon

Manifests and Digital Signatures for Self Extracting Scripts

It has been quite in this little corner of the blogosphere lately. Must be because I am not doing anything, right?

Wrong. I have run out of time to blog. But today I will make an exception because I need to provide an update to an old post:

As a substitute for becoming a real programmer, I have for years been writing VBScripts and wrapping them up with the Z-Zip Self-Extracting executable. After the release of Windows 8, this model became more difficult. Out-of-box, the 7-Zip self extractor started generating application compatibility troubleshooter pop-ups on clients. Even prior to that, clients would get warnings asking them “do you really want to execute this scary unsigned possibly-from-a-murdering-hacker” when they launched our executables.

The solution for this is, of course, to add an application manifest to the self-extractor, and then to digitally sign the resulting executable. Easy, right?

I actually did this a few years ago for our venerable Wi-Fi profile installation tool. It was not quite easy, and unfortunately I never did get the process fully automated. The roadblock was in automating the addition of a manifest to the application. Microsoft’s tool for this, “mt.exe”, from the Windows SDK, consistently corrupts my executables. Others in the blogosphere have identified the tool “Resource Hacker” to fill this need:

I added this tool to my ugly-old script packaging batch files, and had good success with eliminating the program compatibility dialogs:

..\bin\resource_hacker\ResourceHacker.exe -addoverwrite %fname%.exe, %fname%.exe, %fname%.manifest, 24,1,

I also was able to streamline the signing process with the following batch code:

set fname=fixThunderbirdMailboxPath
set SDKPath="C:\Program Files (x86)\Windows Kits\10\bin\x86"
set TimeStampURL=""
set /P CertPath="Enter the full path to the PKCS12/PFX signing certificate:"
set /P CertPass="Enter the password for certificate file:"
%SDKPath%\signtool.exe sign /f "%CertPath%" /p "%CertPass%" /t %TimeStampUrl% /v %fname%.exe

Renewing SSL Certificates on ADFS 3.0 (Server 2012 R2)

I recently had to replace the public-facing service communication certificates on our primary ADFS deployment on Server 2012 R2. I followed a procedure that I thought had a reasonable chance of actually doing what I wanted it to:

  1. Obtained a new private key with signed certificate.
  2. Saved the file to a pfx, and imported it onto each node in the ADFS cluster
  3. Set permissions on the certificate according to documentation
  4. Used the ADFS MMC -> Certificates -> Set Service Communications Certificate.

Everything seemed to go okay, but after a bit we started to get some complaints that some of our users could not access the Office 365 Pro Plus software download page. This was a curiosity to me, because I could not reproduce the problem. A colleague later noticed a raft of SSL errors in the System event log on one of the ADFS nodes, and disabled it in the load balancer configuration.

When I finally got around to investigating, I noticed that the system log reported problems from source ‘HTTPEvent’, with details DeviceObject: \Device\Http\ReqQueue, Endpoint: (and also Endpoint: What gives?

I found a related TechNet Blog that shed some light on the subject:

According to this document, after setting the Service Communications Certificate in the MMC, you must run:
to fetch the certificate thumbprint of the Service Communications Cert. Take note of the certificate thumbprint, then run:
Set-ADFSSslCertificate -Thumbprint [yourThumbprint]

“Set-AdfsSslCertificate” will fix the HTTP.SYS bindings used by ADFS. Apparently the MMC does not set the bindings, which is pretty annoying because this leaves the service in a pretty darn broken state. The HTTP bindings are mentioned in this TechNet documentation:
BUT, the docs do not explicitly state that the Set-AdfsSslCertificate cmdlet needs to be run on all of the ADFS server nodes in your farm. This also is a key missing detail.

Good Documentation… you always take it for granted, until you don’t have it anymore.

Note above that I mentioned a binding problem with the address This was a carry-over from our initial deployment of ADFS 3. Back then, Microsoft did not provide a health check URL for ADFS, and the supplemental binding was needed to allow health monitor connections from our F5 load balancer without using SNI, which is required by ADFS 3.0, but not supported on the F5. These days (and if you have KB2975719 installed), you can instead monitor the following URL from your F5:
More details on this solution can be found here:

While it is nice having a proper health check, problems can arise when your ADFS server HTTP bindings go sour. It would seem that nothing is perfect.

Ping plotting with PowerShell

Waaaay back we used to use a spiffy little tool called “ping plotter” to discover vacant IP addresses on our subnets. I had not had to do an exhaustive study of this for awhile. When it came up again today, I thought “I’ll bet we can do that with two lines of PowerShell.” But I was wrong… it took three lines, since I needed to initiate an array variable:

#Initialize $range as an array variable:
$range = @()

#Populate $range with integers from 2 though 254 
#  (for a Class C ipv4 subnet):
for ($i=2; $i -le 254; $i++) {$range += $i}

#Write out IP addresses for systems that do not have registered DNS 
#  names in the IP subnet
$range | % {$ip = "192.168.1." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {write-host $ip}}

Now some variations… write out only addresses with no DNS entry and that do not respond to ping. (This will help to weed out addresses that are in use that for whatever reason to not have a DNS name.):

$range | % {$ip = "132.198.102." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {$out2 = & ping $ip -n 1; `
  if ($out2 -match 'Destination host unreachable') {write-host $ip}}}

… and perhaps most usefully, write out addresses with no DNS entry and that cannot be located using ARP. (This will weed out in use addresses with no DNS and a firewall that blocks ICMP packets.):

$range | % {$ip = "132.198.102." + $_ ; $out = & nslookup $ip 2>&1; `
  if ($out -match 'Non-existent domain') {& ping $ip -n 1 > $null; `
  $out2 = arp -a $ip; if ($out2 -match 'No ARP Entries') {write-host $ip}}}

Server 2012 boot hang in vSphere/ESXi

Over the past year or so we have been having some problems with Server 2012 and 2012 R2 virtual machines hanging during reboot operations. The systems hang at the “spash screen”, showing the Windows logo and the ring of spinning dots… forever!

Finally I was able to find an fix for this problem here:

From a tip-off here:

The problem? Well, probably it is best that you just read the TechNet social thread, if you really want to know. It is none too exciting, and all very aggravating. The fix? Run a PowerShell script, then vMotion your machines to force ESXi to re-read the VMX file for your guests.

I am posting my variation on the script in the KB here, because VMware’s script is incomplete, and difficult to read.

#Script intended to correct the bug where Windows Server 2012+ systems on VM Hardware v10 will hang at the 
#boot-up splash screen.  Problem is caused by the failure of ESX to clear the "TS" counter on system reboot.

Set-PSDebug -Strict

#Initialize the VIToolkit:
if ( (Get-PSSnapin -Name VMware.VimAutomation.Core -ErrorAction SilentlyContinue) -eq $null ) {
    Add-PsSnapin VMware.VimAutomation.Core

#Connect to your virtual center:
$viServ = ""
Connect-VIServer -Server $viServ

#Get all VMs in the vCenter:
$vms = Get-VM 
#Loop though the VMs:
ForEach ($vm in $vms){
	#Get a "View" object for each VM.  Views expose useful data that is not contined in the VM object:
	$vmv = Get-VM $vm | Get-View
	$name = $vmv.Name
	$guestid = $vmv.Summary.Config.GuestId
	if ($guestid -like "windows8*Guest") {
		#windows8*Guest will match Windows 8 client 32-bit and 64-bit, as well as 
		# "windows8Server64Guest" (which is Windows Server 2012 and 2012 R2).
		#We need to update the VMX file for the VM, which is dong using a VirtualMachineConfigSpec.
		$vmx = New-Object VMware.Vim.VirtualMachineConfigSpec
		$vmx.extraConfig += New-Object VMware.Vim.OptionValue
		$vmx.extraConfig[0].key = "monitor_control.enable_softResetClearTSC"
		$vmx.extraConfig[0].value = "TRUE"
		write-host "Edited" $  
		$ | out-file -FilePath c:\local\temp\softTscOut.txt -NoClobber -Append

Discover user rights in SharePoint

Among the top items capable of derailing your whole day or week are requests from auditors.  Who has access to a resource?  When did they exercise those rights?  In the pas few months, I have had several requests of this sort related to SharePoint rights.  Since I have once again started working on our SharePoint 2010-to-2013 migration project, and most of the SharePoint Powershell cmdlets were fresh in my mind, I though I would take a crack at this somewhat intimidating task.

As usual, writing a useful script took more time that I would have liked, but I am fairly pleased with the results.  The final product makes heavy use of Regular Expressions.  Special thanks go out to RegEx Hero, an online .NET regular expressions tester:
AND, of course, to the site:

Using .NET-style RegEx named capture groups, I was able to eliminate redundant loops though the SharePoint web site list, thus making it possible to crawl all SharePoint web and site-level ACLs in only a few minutes. Hurray!

This code will work only on SharePoint 2010 farms that use Windows authentication. There may be limitations related to sites with multiple Windows domains as well. I will need to update this script in the near future to handle claims authentication, but we will cross that bridge when we come to it.

The script has some pretty convoluted loops that may not make any intuitive sense… I have tried to insert comments to explain what is going on. If you do choose to use this script in your environment and find it difficult to understand, feel free to contact me with questions.

I think that the script has been posted without wordpress-induced errors. The main problem there is the use of .NET style named capture groups, which use ‘greater than’/’less than’ characters. WordPress hates gt/lt, which resemble XML/HTML/XHTML tags. I had to enter these bits of code as HTML escape sequences instead of raw code. It looks like it worked this time.

#J. Greg Mackinnon, 2015-03-25
#Recurses though the web application provided in -webAppplication for the users (samaccountname format) provided in -Users,
#for the domain provided in -Domain.
# Requires: Microsoft.SharePoint.PowerShell PSSnapin
#          ActiveDirectory PowerShell module
# Provides: Comma-separated value file with user, permission, and site data for all discovered permissions, 
#          at path provided in the -logPath parameter.          
        HelpMessage='Enter a username or comma-separated list of usernames in samAccountName format.')]
        [string]$domain = 'CAMPUS',
        HelpMessage='Enter the URL of the SharePoint web application for which all webs will be searched.')]
        HelpMessage='Enter the URL of the single SharePoint site for which all subwebs will be searched.')]
    [string]$logPath = 'c:\local\temp\findUserPermissions.log'
Set-PSDebug -Strict

function getPermType($mask) {
    #SharePoint permissions/roles are exposed programatically as "PermissionMask" attribute of the SPWeb.Permissions.Member objects.
    #Most of these masks are numeric.  This function will convert the numeric value into a "friendly" string, derived by examining 
    #site permissions in a web browser:
    [string]$return = switch ($mask) {
        'FullMask'   {[string]'FullControl'}
        '1012866047' {[string]'Design'}
        '1011028719' {[string]'Contribute'}
        '138612833'  {[string]'Read'}
        '138612801'  {[string]'ViewOnly'}
        '134287360'  {[string]'List-Library:UnknownAccess'} #Note that this mask has the label "Limited Access" in the GUI.
        default      {[string]"Unknown:$mask"}
    return $return

function checkADGroupMembers {
    param ([string]$adGroupName,[string]$adGroupSid,[string]$userRegex)
    #Searches the AD Group provided in -adGroup for matches against the regex provided in -userRegex.
    #Search is performed against the domain of the computer running the script.
    #The userRegex value can be generated using the "regexifyDomainUser" function. It MUST capture the 
    # username/samAccountName to a named capture group called 'name'.
    #Returns boolean true/false.
    #Need to add ability to return RegEx match objects from the search.
    #Optional ability to specify the domain to search?
    [Int32]$i = $adGroupName.IndexOf('\')
    [string]$groupSam = $adGroupName.Substring($i + 1)
    #write-host "Getting members of:" $groupSam
    [String[]]$returns = @()
    #Domain Users could take a long time to process, so let's just assume that the user is a domain user:
    if (($groupSam -eq 'domain users') -or ($groupSam -eq 'authenticated users')) {
        $returns += "!AllUsers!"
        return $returns
    #Get-ADGroupMember will error frequently because SharePoint contains a lot of orphaned groups.
    #Use "ErrorAction Stop" for force an breaking error when this happens, and just set $match to $false/
    try {
        [array]$grpMembers = @()
        if ($adGroupSid) {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $adGroupSid -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
        } else {
            $grpMembers += Get-ADGroupMember -Recursive -Identity $groupSam -ErrorAction Stop `
                | Select-Object -ExpandProperty SamAccountName 
    } catch {
        write-host "    AD group $groupSam does not exist" -ForegroundColor Red
        $returns = $null
    if ($grpMembers.count -gt 0) {
        foreach ($memb in $grpMembers) {
            #write-host "testing" $memb "against" $userregex
            if ($memb -match $userRegex) {
                #write-host "regexmatch found: " $matches.user
                #Return the current regex named group "user".  THis is just showing off...
                #I could simply skip the capture groups and just return $memb.
                $returns += $matches.user
    } else {
        write-host "    AD group $groupSam has no members" -ForegroundColor Red
    if ($returns.count -gt 0) {
        return $returns
    } else {
        return $null

function regexifyDomainUser {
    param ([string]$user,[string]$domain)
    #Converts the provided domain\username pair into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    [string]$regexUser = '^' + $domain + '\\(?<user>' + $userMatch + ')$'
    return $regexUser
function regexifyUser {
    #Converts the provided username into a regex that can 
    #be used to search for the same pattern in a larger string.
    #This regex will return the username in a capture group named "user".
    param ([string]$user)
    if ($user -match '\.') {
        $userMatch = $user.Replace('.','\.')
    } else {
        $userMatch = $user
    [string]$regexUser = '^(?<user>' + $userMatch + ')$'
    return $regexUser

if ((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell
Import-Module ActiveDirectory

#Set up regex strings that contain all of the samAccountNames provided in the input parameters.
#We want to be able to match all possible users with one regex for processing efficiency.

#DomainUsers will match against DOMAIN\username.
[string]$regexDomainUsers = regexifyDomainUser -user $users[0] -domain $domain
#Users will match againstjust username.
[string]$regexUsers = regexifyUser -user $users[0]
if ($users.count -gt 1) {
    for ($i=1; $i -lt $users.count; $i++) {
        #Set up a regex for matching the user in domain\username format:
        [string]$regexDomainUser = '|' + $(regexifyDomainUser -user $users[$i] -domain $domain)
        $regexDomainUsers += $regexDomainUser
        [string]$regexUser = '|' + $(regexifyUser -user $users[$i])
        $regexUsers += $regexUser

## Initialize output log:
if (Test-Path -LiteralPath $logPath) {
    Remove-Item -LiteralPath $logPath -Force
#CSV header row:
[string]$out = "user,Role,WebUrl,aceDetails"
$out | Out-File -FilePath $logPath 

#Get selected SharePoint sites:
[array]$sites = @()
if ($spSite) {                 #If spSite is specified, search only one site:
    $sites += Get-SPSite -Identity $spSite -Limit All
} elseif ($webApplication){    #If webApplication is specified, search all sites in the webapp:
    $sites += Get-SPSite -WebApplication $webApplication -Limit All
} else {                       #Otherwise, search all web applications defined on the local farm:
    $sites += Get-SPSite -Limit All

##Begin Main Loop:
foreach ($site in $sites) {
    $webs = @()
    # Gets webs in current site
    $webs = $site | % {Get-SPWeb -Site $_ -Limit All}
    foreach ($web in $webs) {
        Write-Host "Testing web:" $web.url -ForegroundColor cyan 
        $webPerms = @()
        $webPerms = $web.Permissions
        foreach ($perm in $webPerms) {
            #Scenario 1: ACL entry is for this specific user:
            if ($perm.member.loginName -match $regexDomainUsers) {
                [string]$user = ($perm.member.loginName).split('\') | select -Last 1
                write-host "    Found match $user in the web ACL list." -ForegroundColor yellow
                [string]$aclData = 'Acl:Direct'
                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                $out | Out-File -Append -FilePath $logPath
            #Scenario 2: ACL entry is an Active Directory Group
            if ($perm.Member.IsDomainGroup) {
                [String[]]$ADGroupusers = @()
                $ADGroupUsers = checkADGroupMembers -adGroupName $perm.Member.LoginName -adGroupSid $perm.Member.Sid -userRegex $regexUsers
                if ($ADGroupUsers.count -gt 0) {
                    foreach ($user in $ADGroupUsers) {
                        write-host "    Found user $user in AD Group:" $perm.member.loginName ", which is on the web ACL list." -ForegroundColor yellow
                        [string]$aclData = 'Acl:Embedded:' + $perm.Member.LoginName 
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData 
                        $out | Out-File -Append -FilePath $logPath
            #Scenarios: ACE is for a SharePoint group:
            if ($perm.Member.GetType().Name -eq 'SPGroup') {
                $members = @()
                $members += $perm.member.users
                foreach ($member in $members) {
                    #Scenario 3: ACL is a SharePoint group, and the group contains a matching user:
                    if ($member.loginName -match $regexDomainUsers) {
                        [string]$user = ($member.loginName).split('\') | select -Last 1
                        write-host "    Found match for" $user "in a Sharepoint group that is in the web site ACL." -ForegroundColor yellow

                        [string]$aclData = 'SPGroup:Direct:' + $perm.Member.LoginName
                        [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                        $out | Out-File -Append -FilePath $logPath
                    } #End Scenario 3
                    #Scenario 4: ACL is a SharePoint group, and the group contains an AD group that contains a matching user:
                    if ($Member.IsDomainGroup) {
                        [String[]]$ADGroupUsers = @()
                        $ADGroupUsers = checkADGroupMembers -adGroupName $Member.LoginName -adGroupSod $Member.LoginName.Sid -userRegex $regexUsers
                        if ($ADGroupUsers.count -gt 0) {
                            foreach ($user in $ADGroupUsers) {
                                write-host "    Found user $user in an AD Group that is found in a SharePoint group that appears in the web site ACL." -ForegroundColor yellow
                                [string]$aclData = 'SPGroup:Embedded:' + $perm.Member.LoginName + ':' + $member.loginName
                                [string]$out = $user + ',' + (getPermType($perm.PermissionMask)) + ',' + $web.Url + ',' + $aclData
                                $out | Out-File -Append -FilePath $logPath
                    } #End Scenario 4
            } #End SPGroup Eval
        } #End foreach $perm
        foreach ($admin in $web.SiteAdministrators) {
            #Enumerate Web Site Administrators (should be at least two)
            if ($admin.LoginName -match $regexDomainUsers) {
                [string]$user = $matches.user
                write-host "    Found web site administrator match for:" $user -ForegroundColor yellow
                [string]$out = $user + ',' + 'webSiteAdministrator,' + $web.Url
                $out | Out-File -Append -FilePath $logPath
                #exit #for debugging
    if ($site.owner.UserLogin -match $regexDomainUsers) {
        #Discover Site Owner (should be only one)
        [string]$user = $matches.user
        write-host "    Found site collection owned by:" $user -ForegroundColor yellow
        [string]$out = $user + ',' + 'SiteCollectionOwner,' + $site.Url
        $out | Out-File -Append -FilePath $logPath
        #exit #for debugging

Migrating to the SCCM UDI for OSD, part 6c: Operations – Applications

Continued from:

The following is a procedure for updating Application information in the UVM UDI environment.  Use it as a template for your own operations:

  1. When adding a new application (or app version): Categorize the application:
    1. In the SCCM console under Software Library->Application Management->Applications, select the new application and get “properties”.
    2. Under the “General Information” tab, Click “Select” next to the “Administrative Categories”.  Add the new application to an existing App Category or create a new category, as appropriate.  Applications that are categorized will be added to UDI. Non-categorized applications will not be available.
  2. Update the UDI config files:
    1. Run C:\local\scripts\CM-TaskSequences\build-UDIAppList.ps1.
    2. Locate the “MDT 2013 Files” package in the SCCM console under Software Library->Application Management->Packages, and run an “Update Distribution Points” action.  Verify that distribution was successful before proceeding.

And that is the whole story of the SCCM/UDI migration here at UVM to date. Clearly there is room for improvement. I will try to keep this series updated with revisions as we make them. As always, I hope that these posts will be of help to others in similar situations. If any of the code in this series makes its way into your environment, please let me know. I also am happy to answer any [short] questions about the topics covered here.

-J. Greg Mackinnon | Systems Administrator | University of Vermont
Phone: 802-656-8251 | Web:

Series Index:

Migrating to the SCCM UDI for OSD, part 6b: Operations – Operating Systems

Continued from part 6a:

The following is a procedure for updating Operating System image data in UDI that is specific to the UVM environment.  Use it as a template for your own operations:

      1. Add the new WIM image to SCCM. The WIM must contain only one OS image (e.g. at index 1).
      2. Run c:\local\scripts\CM-TaskSequences\build-udiInfoFiles.ps1.
        (This make a CSV files available to the UDI client that matches the OS Image Name to the version of the OS within the image.) (Required for driver injection.)
      3. Run c:\local\scripts\CM-TaskSequences\build-udiImageList.ps1.
        (This updates the UDI XML-based control file that generates the OS selection options to the end-user in the UDI Wizard.)
      4. Run c:\local\scripts\Update-OSApplyTaskSequence.ps1. When prompted, provide the name of the UDI task sequence to be updated.
        (This script updates the task sequence so that the OS selected in the UDI Wizard can be applied to the UDI target machine.)
      5. Locate the “MDT 2013 Files” package in the SCCM console under Software Library->Application Management->Packages, and run an “Update Distribution Points” action.  Verify that distribution was successful before proceeding.
        (This makes the new CSV info files and Wizard configs available to end-users.)


Series Index:

Migrating to the SCCM UDI for OSD, part 6a: Operations – Drivers

Continued from part 5:

The following is a operational procedure for updating applications, specific to the UVM environment. Use it as a template for managing your own procedures:

  1. If adding a new model: Establish the driver package source:
    1. Download the driver packages from Dell (or Lenovo). You will need different packages for each supported OS.
    2. Determine the WMI model name:
      1. Wmic computersystem get model -or-
      2. Get-WmiObject -class win32_computersystem
        (or more verbosely get-wmiobject -namespace root\cimv2 -class win32_computersystem -property model)
    3. Create a new folder under the matching OS version folder (i.e. Win7 or Win8) within the driver import source directory: “\\confman3\sources\drivers\import“. The name of this new folder must match the model information discovered in step 2. Extract the new drivers into this folder.
  2. If updating a model: Update the driver source:
    1. Locate the source folder for the driver package to update. Remove all contents of the directory including the “.hash” file in the root.
    2. Extract the new drivers into this folder.
  3. If updating a model: Cleanup existing drivers in SCCM:
    1. Find the package to be updated in the SCCM management console. Delete it.
    2. Right-click and get the properties on any driver in the SCCM console. Call up the list of driver categories, and delete the category for the driver to be updated.
  4. Import the drivers from source:
    1. Run C:\local\scripts\CM-Drivers\ImportDrivers.ps1 to import the new drivers into the SCCM environment. The script will create new packages and driver categories for each new folder that you created.
    2. Wait.
  5. Distribute the drivers:
    1. Refresh the SCCM console to reveal the new driver packages and categories.
    2. Right-click the driver package and select “Distribute Content”. Distribute the drivers to “Confman2”.
    3. Monitor the distribution process in the bottom pane of the SCCM console. Make sure distribution succeeds before proceeding.
  6. If adding a model/package: Update the MDT files with the new driver information:
    1. Run C:\local\scripts\CM-TaskSequences\build-UdiInfoFiles.ps1 on Confman3
    2. Locate the “MDT 2013 Files” package in the SCCM console under Software Library->Application Management->Packages, and run an “Update Distribution Points” action. Verify that distribution was successful before proceeding. This will publish a new list of drivers packages to the UDI clients.
  7. If adding a model/package: Update the OS Installation Task Sequence:
    1. Run C:\local\scripts\CM-TaskSequences\Update-DriverInjectionTaskSequence.ps1. Specify the name of the Task Sequence to update when prompted, or provide the name using the “-name” parameter. This script will update the Task Sequence to allow for injection of the new driver package, if one is available for the current model.
  8. If adding or updating WinPE, Peripheral, or “Other” drivers: Update the boot media:
    1. In the SCCM console under Software Library->Operating Systems->Drivers, select “Saved Searches”, then select WinPE 64-bit or WinPE 32-bit. Once the drivers have been filtered, Select All, and then right-click and select Edit->Boot Images. Select the MDT boot image for your architecture (32-bit or 64-bit).
    2. Select “Task Sequences” in the console, then select “Create Task Sequence Media”:
      1. Select to create “Bootable Media”
      2. Select “Site Based Media”
      3. Select “CD/DVD Set”, and specify the path: \\CONFMAN3\sources\os\boot\mdt\iso\UDI-x64.iso
      4. Clear “Protect media with password”, and select “Import PKI certificate”. Select a certificate and password from our super-secret source location. Select “Allow user device affinity with auto-approval”. (If the certificate is expired, see the separate procedure below for generating a new workgroup computer certificate.)
      5. For boot media, select the MDT boot image that you updated in part 8a. For distribution point, select CONFMAN2. For Management Point, select CONFMAN3.
      6. Complete the wizard, and then distribute the boot media to the usual locations (\\files\software\deploy).

As noted above, you need a custom Workstation Authentication certificate to generate the boot media.  If your certificate has expired or you want to generate a new certificate for any other reason, use the following procedure, adopted from:

  1. From a workstation that has access to your Certificate Authority web interface, open Internet Explorer using your admin account, and access:
    Logon as a user with rights to generate workgroup certificates.  Currently this is scoped to high-level admins in our organization.

    1. Add the site to the “Trusted sites” zone in your Internet Settings Control Panel.
    2. Activate “compatibility view” if your CA is still running on Server 2008 R2, otherwise the required ActiveX controls will not load.
    3. Select “Request a certificate”.
    4. Select “Create and submit a request to this CA”.
    5. Select the template: “UVM – SCCM Workgroup or WinPE Client Authentication”
      • Note: This is a lightly-modified copy of the stock “Workstation Authentication” certificate template.  As per MS requirements, the certificate has been forced to use the legacy “Server 2003” certificate format, not the Vista+ “CNG” format.
      • An Enterprise Admin can add permissions to the template to allow enrollment rights for additional users/groups, if necessary.
    6. Fill in the identifying information and friendly name for the certificate.  Make sure to select the option to “Mark keys as exportable”, then click “Submit”.
    7. When prompted, install the certificate.  The certificate will be installed into the “Personal” or “My” store for the user running Internet Explorer.
  2. Run MMC.EXE as the user who requested the certificate:
    1. Add the “Certificates” snap-in for the current user.
    2. Navigate to the “personal” store.  Locate the new certificate.
    3. Right-click the certificate and select All Tasks -> Export.
      1. In the certificate export wizard, select the option “Yes, export the private key.”
      2. Select Personal Information Exchange – PCKS #12 (.PFX), and ensure that “Include all certificates in the certification path if possible” AND “Export all extended properties” are checked, then click Next.
      3. Type in a password and confirm it in the boxes provided on the Password screen, then click Next. (Save this password for later use)
      4. Browse for a location to which to export the certificate.  Make sure that it is somewhere accessible from SCCM, give it a name (i.e. ‘WinPE-Cert.pfx’) and click Save.
  3. Use this new certificate file when completing step 8.4, above.


Series Index:

Migrating to the SCCM UDI for OSD, part 5: Quirks

Continued from part 4:

In the previous parts of this blog series I have shown how PowerShell and VBScript can be used to automate Driver, Application, and Operating System selection and installation in MDT/UDI. In this final post I will discuss a few final UDI quirks that we worked though without using scripts.

UDI Boot Media:

We experienced an odd problem during our OS Deployments when booting from SCCM/UDI media. The deployment process starts, as documented, with a dialog allowing the user to configure the networking environment for OS deployment, followed by a Task Sequence selection dialog. After selecting the task sequence, we expected some scripts to run, followed by the initiation of the UDI Wizard dialogs. Instead what we found was that UDI would first format an partition the local drive, copy a new WinPE environment to the drive, and then restart into WinPE running from local disk before brining up the UDI Wizard. This created a substantial delay between Task Sequence selection and the start of the Wizard. My impression was that users would find this delay unacceptable.

Our MS consultant suggested that this was happening because the revision of the UDI/WinPE environment on our boot media did not match boot image referenced by our active task sequence. To test this, we rebuilt all of the boot images in our environment from the Windows 8.1 ADK sources, then created a new MDT boot image from the ADK image, created a new task sequence referencing the ADK image, and finally generated new UDI Boot Media with the MDT boot image as a source. After completing these actions and updating our distribution points, we were able to get the UDI Wizard to start after Task Sequence selection without first requiring a reboot.

Improper OS Drive Letter Assignment

In our first deployments, we found that at the end of the Task Sequence, our operating system was left running on a D: or E: drive, instead of the usual “C:” drive. Knowing full well that this could cause massive end user confusion, I set out to correct this problem.

I was able to find a few potential solutions in Michael Neihaus’s always dependable TechNet blog:

After weighing the options, I chose to try the “Delete Mounted Devices from registry” option. This required me to import a custom task sequence into our SCCM instance, and then copy/paste the required Task Sequence steps into our production Task Sequence. This approach instantly was successful, so I am sticking with it. Thanks again, Michael.

Failure of UserExit Script Processing

Under MDT/LTI, we made use of the “User Exit Script” capabilities of MDT to generate unique default computer names for UDI clients. Cursory inspection of the docs suggested that User Exit script functionality also is supported under SCCM/UDI, so I decided to transplant our script. However, after doing so I found that the default presented in the UDI Wizard was #GenUniComp()#, rather than a generated computer name.

#GenUniComp()# is the value assigned in our CustomSettings.ini file for the OSDComputerName variable. However, MDI/UDI is supposed to “exit” any code placed between the hash tags (#) out to the Windows Scripting Host. “GenUniComp” is the name of a VBScript function that I defined in our “ZUVMUserExit.vbs” User Exit script. The function works well enough under MDT/LTI, so what is going wrong in UDI?

To diagnose this problem, it helps to know a bit about how User Exit scripts are processed. “UserExit” is set as a variable in the CustomSettings.ini for your environment. This variable is detected and processed by the ZTIGather.wsf script that is run several times during each MDT/LTI and SCCM/UDI deployment. To troubleshoot problems with the script, you should look to the SCCM/UDI log files generated by ZTIGather.wsf. When I examined the ZTIGather logs, I could find no reference at all to the UserExit variable getting processed by the ZTIGather script. The next logical question is, why not?

Logically, the problem is not likely to be caused by an error in the script. First, the same script works just fine under MDT/LTI. Additionally, the script uses the MDT logging functions, so if it had executed, we would have expected to see at least some log output. I thus assumed that the script was not getting executed at all. I started looking at the ZTIGather logs from the top, and found reported “processing [Settings]…” followed immediately by “processing [Default]…”. Interestingly, I had defined the UserExit variable under the [Settings] section of CustomSettings.ini, which worked find under MDT/LTI. However, the SCCM version of ZTIGather appears to process only “Priority” and “Properties” settings under the [Settings] section. My UserExit definition was getting ignored because it was in the wrong place. After moving the “UserExit” definition to the [Default] section, UDI started to process the GenUniComp() function of my User Exit script just as it had under MDT/UDI.

In the final section of this series, I will present operational instructions for the use of these scripts in our environment. These documents are tailored for the UVM environment, but might serve as a template for instructions at other sites.


Series Index:

Migrating to the SCCM UDI for OSD, part 4: Applications

Continued from part 3b:

Previously in this series I demonstrated how you can manage Operating System selection and application in UDI, and one (rather complicated) method for managing drivers in SCCM/UDI. To complete our UDI experience, I now present a handy script for automation of populating select SCCM applications into UDI.

There is some conventional wisdom floating around out there in the ‘tubes that you should not deploy the new SCCM 2012-style “Applications” in SCCM OSD Task Sequences. Instead, it is asserted that you should use only traditional application “Packages”. Since we have developed all or our application installer using the new “Application” objects, I have chosen to ignore this guidance. Fortunately, it appears that the earlier bugs that led to dispensing of this advice have been corrected in the 2012 R2 CU2-CU3 timeframe.

The script below will make use of Configuration Manager application tags to determine which Applications should be populated into UDI. Additionally, the tags are used to generate application groups in the UDI wizard. If you don’t want an application to appear in UDI, don’t tag it. Applications can have multiple tags, and thus can end up getting defined more than once un UDI. This does not appear to cause any problems during deployment.

This script will re-write the entire UDI designer “.app” file, which is an XML-formatted document that defines all of the applications and groups that will be displayed during the UDI Wizard (the default name for this file is ‘’). Because I am writing out the entire file, I chose to use the System.Xml.XmlTextWriter .Net Framework class to do the heavy lifting for me. I probably could have used System.Xml.XmlDocument (which I used in part 3 of this series for updating the main UDI XML control file), but the XmlTextWriter seemed to be a more direct route to getting the job done in this case.

Note that in this code I am again using the SCCM PowerShell cmdlets. In this case, I am using Get-CMCategory and Get-CMApplication. These commands easily could be replaced with simple WMI queries (as seen in part 3 of this series). I also am using some SCCM managed code. I call the “Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer” class in order to use the “Deserialize” method. This method converts the large volume of data in the SDMPackageXML attribute of the application object into somewhat more accessible PowerShell objects.

To use this script in your environment, you will need to update the $outPath, $CMSiteCode, and $CMBinPath variables to match your environment. After running the script, you will need to redistribute the content of your MDT Files package to your distribution points. You UDI clients will read the new data as soon as it is made available on your DP.

# build-UDIImageList.ps1
# J. Greg Mackinnon
# Created: 2015-01-26
# Updated: 2015-03-17 - Added host output to indicate task progress.  Removed block comments for blogging clarity.
# Populates the UDI Configuration Wizard XML APP file with all categorized applications gathered 
# from Configuration Manager.  
# Requires: a local installation of the Configuration Manager administration tools.
#   Modify $outPath, $CMSiteCode, and $CMBinPath to match your environment.
Set-PSDebug -Strict

[string] $outPath = 'O:\sources\os\mdt\files\Scripts\'

[string] $CMSiteCode = 'UVM'
[string] $CMBinPath = 'F:\Program Files\Microsoft Configuration Manager\AdminConsole\bin\'
[string] $CMModName = 'ConfigurationManager.psd1'
[string] $CMModPath = Join-Path -Path $CMBinPath -ChildPath $CMModName

[string] $CMDrive = $CMSiteCode + ':\'

Import-Module -Name $CMModPath | out-null
Push-Location $CMDrive

#Gather current "Administrative Categories" used to classify Applications, capture to an Array of strings:
write-host "Gathering Application Categories..."
[String[]] $CMAppGroups = Get-CMCategory -CategoryType AppCategories | % {$_.LocalizedCategoryInstanceName}

[int] $appCount = 1

# Output XML requires: 
    # DisplayName (map to LocalizedDisplayName)
    # Name, 
    # Guid (with ScopeID, ApplicationGUID... maps to "ModelName" CMApplication Property), 
    # description (optional? map to LocalizedDescription), 
    # type (deployment type?), 
    # ProductID (which can be found in the sdmpackagexml.deploymenttypes[#].installer.productcode)
# All separated into "ApplicationGroup" stanzas, with name= attributes, I think we can map this to "LocalizedCategoryInstanceNames"

#$xmlDoc = New-Object System.Xml.XmlDocument # Note this is the same as the  type accelerator
$utf8 = New-Object System.Text.UTF8Encoding
# Create The Document Writer:
$XmlWriter = New-Object System.XMl.XmlTextWriter($outPath,$utf8)
$xmlWriter.Formatting = "Indented"
$xmlWriter.Indentation = "5"

write-host "Gathering CM Applications..."
$CMApps = Get-CMApplication | Select-Object -Property LocalizedDisplayName,LocalizedDescription,ModelName,LocalizedCategoryInstanceNames,IsLatest,IsExpired,IsSuperseded,SDMPackageXML | ? {$_.IsLatest -and ($_.IsExpired -eq $false) -and ($_.IsSuperseded -eq $false)}

foreach ($group in $CMAppGroups) {
    write-host "Processing group: $group" -ForegroundColor yellow
    foreach ($app in $CMApps) {
        if ($app.LocalizedCategoryInstanceNames.contains($group)) {
            write-host "    Adding application " $app.LocalizedDisplayName -ForegroundColor cyan

            $appXml = [Microsoft.ConfigurationManagement.ApplicationManagement.Serialization.SccmSerializer]::Deserialize($app.SDMPackageXML,$true)




                        $xmlWriter.WriteEndElement() # <-- End Setter
                    $xmlWriter.WriteEndElement() # <-- End Match
                    foreach ($type in $appXml.DeploymentTypes) {
                                if ($type.Installer.Technology -match 'MSI') {
                                } else {
                                    $xmlWriter.WriteString(' ')
                            $xmlWriter.WriteEndElement() # <-- End Setter
                        $xmlWriter.WriteEndElement() # <-- End Match
                $xmlWriter.writeEndElement() # <-- End ApplicationMappings

            $xmlWriter.WriteEndElement() # <-- End Application
            $appCount++ # Increment the appCount by one for use in the "ID" Application element property.
    $XmlWriter.WriteEndElement() # <-- End ApplicationGroup

$xmlWriter.WriteEndElement() # <-- End Application
$xmlWriter.WriteEndDocument() # <-- End XML Document
# Finish The Document


And that is all of the new code that I needed to write for SCCM/UDI. In the final part of this series, I will discuss a few SCCM/UDI quirks that were easily corrected without custom coding:

Series Index: