Archive for the ‘Patch Management’ Category

Unattended Install and Upgrade of Adobe Reader

Previously we explored how to increase the success rate of unattended application upgrades using our handy “killAndExec” VBScript. This works well for about 80% of our applications. What about Adobe Reader? Well… not so much.

Thankfully, the stock Adobe Reader installer deals with open files quite nicely, and does not care if Reader is in use by a browser during silent installs (this is the main reason that we needed killAndExec.vbs in the first place). However, not all Reader install operations are full installs. Interestingly, Adobe is one of the few vendors that I deal with that actually uses MSI patch files (MSP). To install a patch release of Reader (i.e. 10.1.4), you need first to install the base version (10.1), then one or more patch MSP files. This is easy for new installs… just run “setup.exe” silently, then run “msiexec /p (patchFileName) /qn” to install the patch.

However, the situation gets more complicated for upgrades. If the base product already is installed, setup.exe will return an error code. So, for upgrade scenarios, I have put together another handy VBScript to handle base version detection. The script follows:

option explicit
' Install Adobe Reader Script:
' J. Greg Mackinnon, 2012-06-12
' Intended to perform unattended installations of Adobe Reader by MS SCCM 2012.
' Installs the version of Adobe Reader in the same directory as the script, if not already installed.
' Also installs the current Reader patch, if requested.
' Installer string is specified in "sInstall".
' Requires: 
'     Adobe Reader setup and patch files in the same directory as the script.
'     "setup" and "basever" arguments required.
'     "patch" argument optional.
' Returns:
'     - Code 100 - if required arguments are not provided to the script.
'     - Return code of setup program added to the return code of the patch program, if no other errors occur.

dim oExec, oFS, oLog, oShell
dim cScrArgs
dim iExit
dim sBaseVer, sInstall, sLog, sOut, sPatch, sPath, sPF, sScrArg, sTemp, sVer
dim bDoPatch

sLog = "installAdobeReader.log"
iExit = cLng(0)

' Instantiate objects:
Set oShell = CreateObject( "WScript.Shell" )
Set oFS = CreateObject("Scripting.FileSystemObject")
sTemp = oShell.ExpandEnvironmentStrings("%TEMP%")
Set oLog = oFS.OpenTextFile(sTemp & "\" & sLog, 2, True)

' Define Functions
function echoAndLog(sText)
'EchoAndLog Function:
' Writes string data provided by "sText" to the console and to Log file
' Requires: 
'     sText - a string containig text to write
'     oLog - a pre-existing Scripting.FileSystemObject.OpenTextFile object
	'If we are in cscript, then echo output to the command line:
	If LCase( Right( WScript.FullName, 12 ) ) = "\cscript.exe" Then
		wscript.echo sText
	end if
	'Write output to log either way:
	oLog.writeLine sText
end function

sub subHelp
	echoAndLog "installAdobeReader.vbs Script"
	echoAndLog "by J. Greg Mackinnon, University of Vermont"
	echoAndLog ""
	echoAndLog "Runs Adobe Reader silent setup (if not already present), then applies "
	echoAndLog "any specified MSP patch files for Reader."
	echoAndLog "Logs output to 'installAdobeReader.log' in the %temp% directory."
	echoAndLog ""
	echoAndLog "Required arguments and syntax:"
	echoAndLog "/setup:""[setupFile]"""
	echoAndLog "     The primary Adobe Reader installation program.  If switches "
	echoAndLog "    are required for setup to run silently, they must be provided."
	echoAndLog "/basever:"
	echoAndLog "     The base Adobe Reader product version for which to check (i.e. 10.1)"
	echoAndLog ""
	echoAndLog "Optional arguments and syntax:"
	echoAndLog "/patch:""[patchFile]"""
	echoAndLog "     MSP patch file to install after Adobe Reader setup completes."
end sub
' End Functions

' Parse Arguments
if WScript.Arguments.Named.Count > 0 Then
	Set cScrArgs = WScript.Arguments.Named
'	For Each sScrArg in cScrArgs
'		echoAndLog sScrArg 'Echo supplied arguments to console
'	Next

	for each sScrArg in cScrArgs
		select case LCase(sScrArg)
			Case "setup"
				sInstall = cScrArgs.Item(sScrArg)
			Case "patch"
				sPatch = cScrArgs.Item(sScrArg)
			Case "basever"
				sBaseVer = CStr(cScrArgs.Item(sScrArg))
			Case Else
				echoAndLog vbCrLf & "Unknown switch or argument: " & sScrArg & "."
				echoAndLog "**********************************" & vbCrLf
		end select
	if (IsNull(sInstall) or IsEmpty(sInstall)) then
		echoAndLog "Required argument 'setup' was not provided."
		echoAndLog "**********************************" & vbCrLf
	elseif (IsNull(sBaseVer) or IsEmpty(sBaseVer)) then
		echoAndLog "Required argument 'basever' was not provided."
		echoAndLog "**********************************" & vbCrLf
	elseif (IsNull(sPatch) or IsEmpty(sPatch)) then 
		bDoPatch = False
		bDoPatch = True
	end if
elseif WScript.Arguments.Named.Count = 0 then 'Detect if required args are not defined.
	echoAndLog vbCrLf & "Required arguments were not specified."
	echoAndLog "**********************************" & vbCrLf
end if
' End Argument Parsing

' Begin Main

' Complete version and installation strings:
sInstall = ".\" & sInstall
sPatch = "msiexec.exe /p " & sPatch & " /qb /norestart"

' Build path to Adobe Reader executable:
sPF = oShell.ExpandEnvironmentStrings( "%ProgramFiles%" )
sPath = sPF & "\Adobe\Reader 10.0\Reader\AcroRd32.exe"
echoAndLog "Acrobat Reader Path: " & sPath

' Get the version string on the currently installed Reader executable:
on error resume next
sVer = oFS.GetFileVersion(sPath)
on error goto 0
echoAndLog "Version of currently installer Adober Reader: " & sVer

' See if we already have the base version installed:
if InStr(Left(cStr(sVer),4),sBaseVer) then
	'Reader 10.1.x is already installed skip base product installation.
	echoAndLog "Base product installed.  Skipping setup..."
	'Install the base product.
	echoAndLog "Installing base product with command: " & sInstall
	set oExec = oShell.Exec(sInstall)
	Do While Not oExec.Status  1
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode)
end if

'Now install the patch:
if bDoPatch = True then
	echoAndLog "Patch installation requested."
	echoAndLog "Installing patch with command: " & sPatch
	set oExec = oShell.Exec(sPatch)
	Do While Not oExec.Status  1
	sOut = oExec.StdOut.ReadAll()
	echoAndLog "Return code from patch installer: " & oExec.ExitCode
	echoAndLog "Standard output: " & sOut
	iExit = cLng(oExec.ExitCode) + iExit
	echoAndLog "Patch installation was not requested.  Exiting."
end if

' End Main

WSUS Reporting with PowerShell

I have been trying to determine if our SCCM service has most of our domain clients registered, and have decided that the WSUS client database may be the best source of information on currently active domain members. As previously mentioned, WSUS is not pre-configured with a lot of useful infrastructure reports, but pulling data out with PowerShell is not overly difficult. Have a gander… this script generates a count of all current clients, counts by OS type, a count of Virtual Machine clients, and a few counts based of various source IP addresses.

#Get WSUS Computers script
# Finds and counts all registered computers matching various criteria specified in the script
# Optionally, the found computer names to the file defined in $outFile, forced to uppercase, trimmed of whitespace, and sorted.
# Generates an object $out, that is sent to the console at the end of the script.

set-psdebug -strict

#Initialize Variables
	#$outFile = [string] "\\files\shared\ets\SAA\jgm\WSUSXps.txt"

	$hwModel = "Virtual|vm"
	$ipMatch = "^132.198|^10.245" # specify your internal network ip ranges here, in RegEx format.
	$wsusParentGroup = [string] "All Computers"
	$wsusgroup = ""
	$WindowsUpdateServer= [string] "" #specify your WSUS server here
	$useSecureConnection = [bool] $true
	$portNumber = [int] "443" #required if you have added SSL protection to your WSUS (which you should do).

#Instantiate Objects:
	#Required WSUS Assembly – auto installed with WSUS Administration Tools
	$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($WindowsUpdateServer,$useSecureConnection,$portNumber)
	$computerScope = new-object Microsoft.UpdateServices.Administration.ComputerTargetScope
	$computerScope.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All
	$computers = $wsus.GetComputerTargets($computerScope)
	$wsusData = new-object System.Object
	$out = @()

$wsusData | add-member -type NoteProperty -name Criteria -value ("Total comptuers")
$wsusData | add-member -type NoteProperty -name Count -value ($computers.count)
$out += $wsusData
remove-variable wsusData

$osType = "Windows 7"
$filtComps = $computers | ? {$_.OSDescription -match $osType}
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Windows 7")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

$osType = "Windows Vista"
$filtComps = $computers | ? {$_.OSDescription -match $osType} 
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Windows Vista")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

$osType = "Windows XP"
# final "select" in the pipeline if you want to generate a list of computer names matching the criteria.
$filtComps = $computers | ? {$_.OSDescription -match $osType} | select-object -Property FullDomainName
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("XP Professional")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

#Filter for virtual machine models
$filtComps = $computers | ? {$_.Model -match $hwModel}
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Virtual Machines")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData 

$filtComps = $computers | ? {$_.IPAddress -notmatch $ipMatch} | select-object -Property IPAddress
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("Non-UVM Addresses")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

## Following section does not produce useful data... WSUS does not see NAT-based addresses, on the public IP in front of the NAT.
## However, it is a good regex... it matches any non-routable (private) IPv4 address.  Take note for future use.
#$ipMatch = "^10.|^192.168.|^72.[1-2][0-9].|^72.3[0-1]."
#$filtComps = $computers | ? {$_.IPAddress -match $ipMatch}
#$wsusData = new-object System.Object
#$wsusData | add-member -type NoteProperty -name Criteria -value ("NAT Addresses")
#$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
#$out += $wsusData
#remove-variable wsusData

$ipMatch = "^10.245." # Our Wi-Fi and VPN clients fall in this IP range.  Substitute your internal (non-routed) IPs here.
$filtComps = $computers | ? {$_.IPAddress -match $ipMatch} 
$wsusData = new-object System.Object
$wsusData | add-member -type NoteProperty -name Criteria -value ("UVM Wireless/VPN Addresses")
$wsusData | add-member -type NoteProperty -name Count -value ($filtComps.count)
$out += $wsusData
remove-variable wsusData

#Generate file output by: removing all but the RDN of the computer name, trimming any whitespace, forcing to uppercase, 
# sorting, suppressing headers, then writing to file.
#$filtComps | foreach {$_.FullDomainName.split('.')[0]} | foreach {$_.Trim()} | foreach {$_.ToUpper()} | `
	#sort-object | Format-Table -HideTableHeaders | Out-File -FilePath $outFile -Force
$out | Format-Table -AutoSize

WSUS – Missing Servers

All of the Server 2003 virtual machines are missing from the WSUS inventory. Poor servers… they are missing the party.

Well not really, they are still getting updates, according to the logs. However, they are not reporting in. They are party lurkers.

This is an old problem, and only took a little digging. All of our 2003 VMs came from a common VMware template. Since the template once connected to our WSUS server, it already had a unique SusID in the registry, so all the clones have the same ID.

To fix, I used the cmd script here:

I had to remove the “pause” commands, then I used “dsquery” to find all of of the relevant servers in our infrastructure:

dsquery * ou=Servers,ou=ets,ou=resources,dc=campus,dc=ad,dc=uvm,dc=edu -attr cn operatingSystem -limit 2000 > servers.txt

I isolated the Server 2003 systems from this list:

find "2003" servers.txt > 2003servers.txt

I then did some quick text processing to remove everything but the host names from the output file.  The final step, we use psexec.exe from SysInternals to run the SusID reset script:

psexec.exe @2003servers.txt -s -c AU_Clean_SID.cmd

I ended up running “psexec.exe” a second time to force the “wuauclt.exe /resetauthorization /detectnow” bit a second time.  Psexec.exe requires the “-d” switch when running this command remotely.  I think the WUAU service needed time to get fully operational before running the authorization token reset.  Perhaps a pause command would be of assistance in cmd script linked above?  Anyway, all of the Server 2003 hosts have come back to the party and are socializing nicely.

For my next trick, I am going to try to match up the WUAU XP client list with our AD XP client list to see if we have a lot of silent XP systems.  If we rely on standard tools, we could use a query similar to the following to extract XP computer objects with their last logon time:

dsquery * domainRoot -Filter "(&(objectclass=computer)(operatingSystem=Windows XP Professional))" -attr Name LastLogonTimeStamp -limit 20000 > xp.txt

There is an excellent cmd script here that will convert the “lastLogonTimestamp” into human-readable format.

However, it probably would be easier to use “dsquery computer domainRoot -inactive 4″, since processing of “lastLogonTimestamp” against current local time can be challenging in CMD (easier with PowerShell).

WSUS – Programatic Access

Windows Server Update Service – great service, poor interface. Ever try to manage computers in bulk with the WSUS GUI? You can’t… there are limited filtering and search options, and the GUI hangs a lot when performing group operations. Surely there is a better way…

Maybe, at least if you are a programmer. If not, then there at least is another way, if not better.

.NET to the rescue…

Use the “Microsoft.UpdateServices.Administration” Reflection Assembly to expose WSUS .NET objects to PowerShell, and you are limited only by your PS-foo.

My task last night was to add a list of computer objects (obtained using “dsquery” to a named group in WSUS.  Because I wanted to save time, I used Google to get some base code to work with. The code below is derived largely from the good work found here:

Ultimately, I am not sure if this saved time, because troubleshooting other people’s code generally takes about twice as long a fixing your own.

Anyway, the following script will get the job done. It requires an input file (srvlist.txt) with one computer name per line. You will need to add your WSUS server, port number, SSL boolean value, and WSUS target group to make the script run.

#Script to add machines to a WSUS group automatically:
#The script needs Admin credentials and the WSUS Administration Console installed on the machine where it runs
#Initialize Variables
	$wsusGroup = [string] "ServerGroupC"
	$wsusParentGroup = [string] "All Computers"
	$date = get-date
	$date = [string] $ + $date.month + $date.year + $date.hour + $date.minute
	$succeslog = [string] ".\logs\" + $date + "_success.log"
	$errorlog = [string] ".\logs\" + $date + "_errors.log"
	$WindowsUpdateServer= [string] ""
	$useSecureConnection = [bool] $true
	$portNumber = [int] "443"

#Instantiate Objects:
	#Required WSUS Assembly – auto installed with WSUS Administration Tools
	if (!$wsus) {
		$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($WindowsUpdateServer,$useSecureConnection,$portNumber)
	$serverList = Get-Content ".\srvlist.txt"
	$updateGroups = $Wsus.GetComputerTargetGroups()
	$updateGroup = $UpdateGroups | Where-Object{$_.Name -eq $wsusgroup} | Where-Object{$_.getparenttargetgroup().name -eq $wsusparentgroup}
	$computerScope = new-object Microsoft.UpdateServices.Administration.ComputerTargetScope
	$computerScope.IncludedInstallationStates = [Microsoft.UpdateServices.Administration.UpdateInstallationStates]::All
	$computers = $wsus.GetComputerTargets($computerScope)
	$wsusServers = @()
	$WsusServersShortNames = @()

#Create arrays:
# $wsusServer = Array of WSUS Computer objects
# $wsusServerShortName = Array strings, with one server RDN per line
Write-Host "Collecting Server List from WSUS…"
$computers | foreach-object {
	$wsusServer = $_.FullDomainName
	#cut off DNS suffix and store shortname
	$wsusServerShortName = $WsusServer.split(‘.’)[0]
	$wsusServers += $WsusServer
	$wsusServersShortNames += $wsusServerShortName
} #End ForEach $computers

#loop to add servers to group
ForEach ($server in $serverList)  {
		#Check if server Netbios name is present in WSUS, if present move to group – if not log an error
		$wsusComputer = $wsusServersShortNames | Where-Object {$_ -eq $server.Trim()} #Checks for a match in WSUS for the current server in the import list.
		If ($wsusComputer) {
			$searchStr = [string] $server.Trim() + "\." #String representing a RegEx match for the relative part of the server FQDN
			$wsusComputer1 = $wsusServers | where-object {$_ -match $searchStr } #Get a WSUS computer object representing the current server in the import list.
			If ($wsusComputer1.getType().Name -match "string") { #Current $wsusComptuer1 must be a [string] object, or next step will fail.
				Write-Host "$wsusComputer1 will be added to $($ group"
				$computer = $wsus.GetComputerTargetByName($wsusComputer1)
				out-file -append -inputobject "$Server added to $($ group" -filepath $succeslog
			Else {
				#More than one server was matched in WSUS – this will happen if your regEx is not properly formed.
				write-host "count $($wsusComputer1.count)"
				Out-File -append -inputobject "$werver has ambiguous name – check server in WSUS and add to group manually" -filepath $errorlog
		} #End If $wsusComputer
	Else {
		Write-Host "$Server not found in WSUS"
		out-file -append -inputobject "$Server not found in WSUS" -filepath $errorlog
} #End ForEach $server

WSUS and Secunia CSI – Deployment Hurdles

After many years of saying that we should do it, we have purchased a third-party patch management solution.   The good folks at Secunia cut us a great deal, and we are not in the process of deploying Secunia CSI, which will be integrated with our existing WSUS (Windows Server Update Service) instance.  Here are some notes on the installation process:

WSUS SSL Configuration:

Secunia CSI uses the “System Center Update Publisher” (SCUP) API to publish third-party update packages into WSUS.  SCUP requires that SSL be implemented in WSUS.  Unfortunately, we had not done that yet, so there was some work to be done before we could even connect the CSI console to our WSUS server.

  1. Group Policy for Windows Update needed to be updated point clients to the HTTPS version of our WSUS host.
  2. The WSUS host had to be reconfigured to require SSL for the “ApiRemoting30″, “ClientWebService”, “DssAuthWebService”, “ServerSyncWebService”, and “SimpleAuthWebService” IIS virtual directories.  Also, WSUSUtil needs to be run to signal the update service to use SSL, as documented here:
  3. After making these changes, we found that the WSUS management console and Secunia WSUS integration utilities were failing to connect to WSUS (fontunately, Windows Update clients were not having this problem).  The management utilities were reporting “access denied” messages, which apparently were related to authentication failures.  This fix for this was not obvious, not did we find it documented anywhere.  Since our WSUS server is using an externally-refereneable “Internet” DNS name, we found that we also needed to set an SPN for the service that was running WSUS (in this case, the Network Service).  The command follows:
    setspn.exe -A http/[wsusDnsName] [wsusHostName]
    (where wsusDnsName is the DNS name on the WSUS server SSL certificate, and wsusHostName is the short computer name of the WSUS server Active Directory account.  You can verify that the SPN was added successfully using “setspn -L [wsusHostName]

Secunia CSI Signing Certificate Configuration:

Microsoft’s SCUP API requires that a code signing certificate is present in WSUS for the signing of packages imported into WSUS.  The Secunia CSI console will create a self-signed certificate for this purpose, but the cert will have a non-configurable 512-bit key length, and this cert will have to be pushed to the “Trusted Publishers” store of every WSUS client.  I prefer to use our AD CA Servers to generate a Code Signing cert.  This scenario is supported by Secunia, but it is not well documented.  Here is what we needed to do… it was not fun:

  1. On our CA server, create a new Certificate Template by cloning the “Computer” template, and making the following selections:
    1. Select “Server 2003″ as the compatibility level for the template.  “Server 2008″ templates apparently are not compatible with the SCUP API (or so I have read, and I see no reason to take chances).
    2. Set the minimum key size to “2048″ or longer.
    3. Set “Subject Name” to “Supply in the request”.  Corresponsingly, you should set “Issuance Requirements” to “CA certificate manager approval”.  Under “Extensions”, select “Application Policies”, and add “Code Signing”.  Remove all other policies.
    4. Under “Security”, add the computer account of the WSUS server, with Read and Enroll rights.
    5. Configure the CA to issue certs from this new template.
  2. Use the Certificates MMC on the WSUS server to request a code signing cert for the local computer account.  Failing this, you can assign rights to the template to a user account, then use the CA web interface to request a certificate.  You then can transfer the cert to the local computer account cert store.
  3. The CSI certificate import tool is pretty fussy about the certificate format, and the requirements of the tool are not documented.  We found the following requiments, once met, allowed for successful signing of packages:
    1. The certificate needs to be exported to a “pfx” file (PKCS #12 format) before import via CSI
    2. The PFX file must contain only the signing cert, and no other certificates in the signing cert chain.
    3. The PFX file must not be passphrase protected.

    So what are we to do? The certificate management GUI on windows does not allow the export of PFX files with no passphrase! The solution is found in “CertUtil.exe”:

    1. Use “certutil -store” to list the certs in the system account personal certificates store. Identify the serial number of the code signing certificate.
    2. Use “certutil -exportpfx My [CertSerialNumber] [exportFileName] NoChain,NoRoot”
  4. Now import the PFX file  that you just created using the CSI Console.  It should work, unless I have missed something else important.

CSI Console Configuration on Server 2008:

Server 2008 / 2008 R2 security settings will cause some problems for the CSI console, unless you make the following configuration changes, as documented in the CSI FAQ, here:
and here:

To summarize:

  1. Add to your “Trusted Sites” intenet security zones (in the Internet Settings control panel).
  2. In Internet Options → Advanced → Scroll to Security → Uncheck ‘Do not save encrypted pages to disk.